| Title: | Lightweight Stdio MCP Server for R |
| Version: | 0.2.0 |
| Description: | Provides a lightweight Model Context Protocol (MCP) server for exposing 'R' functions as tools over standard input and output ('stdio'). Designed for local, client-launched integrations, with protocol-aware tool definitions and results, JSON Schema helpers, and optional interoperability with 'ellmer'. |
| License: | MIT + file LICENSE |
| URL: | https://tosidata.github.io/mcplite/, https://github.com/tosidata/mcplite |
| BugReports: | https://github.com/tosidata/mcplite/issues |
| Depends: | R (≥ 4.1.0) |
| Imports: | jsonlite (≥ 0.9.16), nanonext (≥ 1.6.0), otel |
| Suggests: | ellmer (≥ 0.3.0), otelsdk, processx, testthat (≥ 3.0.0), withr |
| Config/testthat/edition: | 3 |
| Encoding: | UTF-8 |
| Config/roxygen2/version: | 8.0.0 |
| Config/Needs/website: | pkgdown, rmarkdown |
| NeedsCompilation: | no |
| Packaged: | 2026-10-20 09:38:26 UTC; juha |
| Author: | Juha Itkonen [aut, cre, cph] |
| Maintainer: | Juha Itkonen <juha@tosidata.com> |
| Repository: | CRAN |
| Date/Publication: | 2026-10-20 10:20:02 UTC |
mcplite: Lightweight stdio MCP server for R
Description
Expose R functions as tools for a local Model Context Protocol (MCP) client.
Define tools, save them in a script ending with mcp_server(), and configure
the client to launch that script with Rscript --vanilla and an absolute
script path. The server blocks until standard input closes.
Define tools and return results
Use mcplite::tool() with the tool-types helpers to describe arguments.
Tools remain directly callable R functions. Return ordinary R values for
text conversion, or tool_result() with content_text() and other
content-blocks constructors for structured or rich output. See
mcplite-results for exact return-value mappings and payload JSON options.
Schemas do not validate values or enforce permissions. Tools own domain validation, authorization, safe side effects, and output limits. Keep stdout exclusively for MCP protocol messages; use stderr for diagnostics.
Scope and next steps
mcplite is tools-only and stdio-only; embedded resources and resource links
in results do not provide resource endpoints. See mcp_server() for server
use, supported protocol versions, and optional tracing. Compatible
ellmer::tool() objects are accepted, but ellmer is not required.
Website guides: Get started, Write tools, and Return results.
Author(s)
Maintainer: Juha Itkonen juha@tosidata.com [copyright holder]
Authors:
Juha Itkonen juha@tosidata.com [copyright holder]
See Also
Useful links:
Report bugs at https://github.com/tosidata/mcplite/issues
Construct protocol-native MCP content blocks
Description
These constructors create the only content-block objects accepted by
tool_result(). All R-facing names use snake_case; MCP names such as
mimeType and _meta are created only when a result is serialized.
Usage
content_text(text, annotations = list(), meta = list())
content_image(data, mime_type, annotations = list(), meta = list())
content_audio(data, mime_type, annotations = list(), meta = list())
content_resource_link(
uri,
name,
title = NULL,
description = NULL,
mime_type = NULL,
size = NULL,
annotations = list(),
meta = list()
)
content_resource(
uri,
text = NULL,
blob = NULL,
mime_type = NULL,
annotations = list(),
meta = list()
)
Arguments
text |
A single text string. For |
annotations |
Optional named MCP annotations. Known R-facing fields are
|
meta |
Optional named outer content-block metadata. For
|
data |
A scalar base64 string containing inline image or audio data. |
mime_type |
A single MIME type string. Optional for resources and resource links. |
uri |
A single non-empty author-supplied resource URI. |
name |
A single non-empty resource-link name. |
title |
Optional resource-link display title. |
description |
Optional resource-link description. |
size |
Optional non-negative whole-number resource size in bytes. |
blob |
A scalar base64 string containing an embedded resource payload;
exactly one of |
Details
Image, audio, and resource blob data must already be scalar base64 strings. The constructors do not read files, download URLs, infer MIME types, encode, decode, or validate base64 data, or invent resource URIs. Tool authors own payload integrity, accurate MIME types, meaningful URIs, authorization, sanitization, and payload size decisions.
Resource links only transport an author-supplied URI. mcplite remains a
tools-only server and does not make that URI readable or implement
resources/read. Embedded resources are self-contained content blocks.
Value
A validated content block for use in tool_result().
Run an MCP server for R tools over stdio
Description
mcp_server() runs a
Model Context Protocol server over
standard input and output. It exposes tools created with mcplite::tool() so
MCP clients can discover and call R functions. Compatible ellmer::tool()
objects can also be supplied when users already use ellmer, but ellmer is not
required for ordinary mcplite tools.
Usage
mcp_server(tools, instructions = NULL)
Arguments
tools |
Tool definitions to expose. Supply one |
instructions |
Optional server instructions to advertise to clients using a protocol version that supports them. |
Value
mcp_server() is called for its side effect of serving MCP requests.
It blocks the current R process until standard input closes.
Run a server
Supply one tool or a list of tools directly. Put the complete setup in a
script that defines or sources tools and ends with
mcplite::mcp_server(actual_tool_or_list). Configure the MCP client to launch
it with Rscript --vanilla /absolute/path/to/server.R; use an absolute script
path because the client's working directory is not guaranteed. The call
blocks until standard input closes, so use a client-launched script rather
than an interactive R console.
Tools may return ordinary R values for text conversion or opt into native
content and structured output with tool_result(). See mcplite-results
for exact mappings and payload JSON options. Set those options before
constructing results and starting the server, and keep them stable while it
runs. Tools own input validation, output conformance, permissions, and safe
side effects; schemas are not authorization or sandboxing.
Keep stdout exclusively for MCP protocol messages. Send diagnostics and telemetry to stderr or a remote exporter, not stdout.
Server scope
mcplite is a tools-only, stdio-only server. It does not implement JSON-RPC
batching, HTTP transports, sessions, prompts, resource listing or reading,
subscriptions, cancellation, sampling, elicitation, roots, Tasks, MCP Apps,
progress notifications, or server-initiated and other multi-round-trip
requests. Embedded resource blocks are self-contained, and resource links
do not make their URIs readable through mcplite.
Protocol compatibility
The same server definition supports four MCP versions. A tool remains available when its output schema cannot be represented by the client's protocol. Compatible explicit or generated text content remains available when structured content is omitted. Tool authors still own full schema validity and runtime input/output conformance.
| MCP version | Lifecycle | Output schemas | Structured content | Audio, resource links, per-content metadata |
2026-07-28 | Protocol selected per request | Every supported shape | Every supported JSON value | Supported |
2025-11-25 | initialize first | Direct object-root only | Object values only | Supported |
2025-06-18 | initialize first | Direct object-root only | Object values only | Supported |
2024-11-05 | initialize first | Omitted | Omitted | Unsupported |
Text, images, embedded resources, annotations, and result metadata work in
every supported version. A 2024-11-05 call returning audio or a resource
link receives a contained tool-level error instead of partial content or
server termination.
Modern 2026-07-28 clients select the protocol with per-request metadata.
server/discover, tools/list, and tools/call do not require initialize.
Discovery advertises 2026-07-28, 2025-11-25, 2025-06-18, and
2024-11-05 in that preference order. Every successful modern result
includes resultType = "complete" and the server's canonical identity in
_meta["io.modelcontextprotocol/serverInfo"]. Modern server/discover and
tools/list results also include public cache hints with a 300000 millisecond
TTL. Modern ping is unsupported. Legacy clients retain ping, tools/list,
and tools/call after initialization.
OpenTelemetry tracing
mcplite automatically creates one OpenTelemetry server span for every
parsed MCP request or notification that passes JSON-RPC envelope validation.
This includes retained legacy initialization and ping, notifications, modern
discovery, tool listing and calls, and valid unknown methods. Blank input,
JSON parse failures, and malformed JSON-RPC envelopes do not create MCP
operation spans.
Tool authors do not need to call otel::start_local_active_span() for the
MCP operation or tool invocation. The server span remains active while tool
code runs, so optional tool-authored spans can become children without being
required. mcplite does not create a redundant automatic tool-execution
child span.
Provider and exporter configuration belongs to the standard otel and
otelsdk environment variables and APIs. mcplite does not add telemetry
arguments, choose an exporter, or configure a provider. With no exporter
configured, tracing is an effective no-op and MCP behavior is unchanged. A
safe stderr configuration is
OTEL_R_TRACES_EXPORTER=stderr Rscript --vanilla /absolute/path/to/server.R;
a remote exporter such as OTLP is also suitable.
Do not use a stdout or console exporter with a stdio MCP server. Standard output is reserved exclusively for MCP protocol messages, so telemetry written there will corrupt the protocol stream.
Remote W3C parent context may be supplied in params._meta.traceparent, with
optional params._meta.tracestate. Malformed propagation data is ignored,
and _meta is not passed to tool functions. By default, spans contain
selected low-cardinality operation metadata; raw requests and responses,
tool arguments and results, _meta, trace headers, and condition messages
are not recorded as span attributes.
Examples
if (identical(Sys.getenv("MCPLITE_CAN_BLOCK_PROCESS"), "true")) {
add_numbers <- tool(
function(x, y) {
x + y
},
name = "add_numbers",
description = "Add two numbers and return the result.",
arguments = list(
x = type_number("First number."),
y = type_number("Second number.")
)
)
mcp_server(list(add_numbers))
}
Return-value mappings and payload JSON options
Description
Tools can return ordinary R values for text conversion, or tool_result()
for explicit MCP content and structured data. This topic defines the shared
serialization contract. See tool_result() for construction and fallback,
and mcp_server() under "Protocol compatibility" for version projection.
Ordinary return values
Character scalars become literal text and character vectors are joined with
newlines. Other JSON-serializable values become JSON in one text content
block. Bare lists are ordinary values, not MCP result objects. Ordinary
NULL becomes the text "{}"; an ordinary unnamed list() becomes "[]".
For per-tool conversion choices, return your own JSON as ordinary character
text, for example as.character(jsonlite::toJSON(value, na = "null")).
JSON-looking strings and jsonlite json objects are never parsed or inserted
as raw JSON. In structured_content, they remain string values.
Structured-content mappings
Structured values follow jsonlite::toJSON() and the payload options below,
with one root convention: a bare empty list at the structured root represents
{}. Nested unnamed empty lists retain jsonlite's [] mapping. NULL
means absence of the structured-content field, not a JSON null value.
With default options:
| R value | Structured-content meaning |
list(ok = TRUE) | JSON object |
c("a", "b") | JSON array |
"done" | JSON string scalar, not parsed as JSON |
list() or structure(list(), names = character()) | Empty object {} |
I(list()) | Explicit empty array [] |
NA or NA_character_ | JSON null |
NA_integer_, NA_real_, or NA_complex_ | JSON string "NA" |
NULL | No structured-content field |
Classes, dimensions, and explicit I() or jsonlite::unbox()
representations retain their jsonlite mappings. Tool authors are
responsible for conformance with any advertised output schema.
Payload JSON options
Configure these package-global R options before constructing results and starting the server, and keep them stable while the server runs:
-
mcplite.json.null:"null"(default) or"list". Named payloadNULLfields remain present, as JSONnullor{}, respectively. -
mcplite.json.na: unset by default, which omits jsonlite'snaargument and preserves its class-specific defaults. Set"null"or"string"to select that jsonlite mapping."null"also mapsNaNand infinities to JSONnulland retains missing data-frame fields instead of omitting them. -
mcplite.json.auto_unbox:TRUE(default) orFALSE. SettingFALSEkeeps singleton payload vectors as arrays; explicitI()andjsonlite::unbox()representations still apply.
Remove an option with NULL, for example options(mcplite.json.na = NULL),
to restore its default. This omits na; it does not forward na = NULL.
These options apply to ordinary noncharacter return-value JSON and to
structured content, its representability checks, generated text fallback,
and protocol-version projection. Protocol fields, schemas, annotations,
and all result and content metadata retain fixed encoding.
See Also
mcplite, tool_result(), content_text()
Examples
local({
old <- options(mcplite.json.na = "null", mcplite.json.auto_unbox = FALSE)
on.exit(options(old))
result <- tool_result(structured_content = list(
count = 1L,
values = c(NA_real_, NaN, Inf, -Inf),
rows = data.frame(id = 1L, missing = NA_real_)
))
result$content[[1]]$text
})
Define an MCP tool
Description
tool() wraps an R function with MCP metadata. The resulting object can be
supplied directly, or in a list, to mcp_server(). Ordinary function return
values use the default single-text-block conversion; return tool_result()
to opt into protocol-native content, structured output, or result metadata.
Usage
tool(
fun,
description,
...,
arguments = list(),
name = NULL,
annotations = list(),
output_schema = NULL
)
Arguments
fun |
Function to expose as a tool. |
description |
A single string describing when and how to use the tool. |
... |
Not used. Supply argument schemas with |
arguments |
Named list of argument schemas created by the |
name |
Optional tool name. If omitted, |
annotations |
Optional named list of MCP tool annotations to advertise. |
output_schema |
Optional output schema created by a supported |
Value
The supplied function with MCP metadata attached. It remains directly
callable as an R function and can be supplied to mcp_server().
Arguments and defaults
Incoming JSON arrays reach fun as R lists, not atomic vectors. Schemas do
not coerce or validate argument values or supply R defaults. Marking a
property required = FALSE permits omission in the advertised schema; the
R function must provide its own default for calls that omit that argument.
Return values
Ordinary character values become literal text, not parsed JSON. Bare lists are ordinary values, not MCP result objects. See Return-value mappings for exact conversions, empty and missing values, and payload JSON options. Configure those options before constructing results and starting the server.
Complete schemas and structured output
arguments also accepts one complete object-root schema from
type_from_schema(). Complete input schemas must have direct root
type = "object"; JSON Schema 2020-12 keywords such as $schema, $defs,
$ref, composition, and conditionals are preserved.
An output_schema advertises the expected structured_content shape. A
client's protocol can omit an incompatible output schema without removing
the tool; see mcp_server() under "Protocol compatibility" for the exact
version rules. mcplite checks schema container shape and serializability,
not full schema validity or runtime input/output conformance. Tool authors
own those guarantees.
Examples
add_numbers <- tool(
function(x, y) {
x + y
},
name = "add_numbers",
description = "Add two numbers and return the result.",
arguments = list(
x = type_number("First number."),
y = type_number("Second number.")
)
)
add_numbers(2, 3)
Define MCP tool schemas
Description
These helpers create common JSON Schema descriptions for tool arguments and
output. type_from_schema() wraps a complete schema supplied as a list or
JSON string without removing JSON Schema 2020-12 keywords. A complete tool
input schema can be passed directly as arguments = type_from_schema(...)
when it has direct root type = "object".
Usage
type_boolean(description = NULL, required = TRUE)
type_integer(description = NULL, required = TRUE)
type_number(description = NULL, required = TRUE)
type_string(description = NULL, required = TRUE)
type_enum(values, description = NULL, required = TRUE)
type_array(items, description = NULL, required = TRUE)
type_object(
.description = NULL,
...,
.required = TRUE,
.additional_properties = FALSE
)
type_from_schema(text = NULL, path = NULL)
type_ignore()
Arguments
description |
Optional argument description. |
required |
Whether the argument is listed as required in the parent
schema. |
values |
Allowed enum values. |
items |
Type helper describing each array item. |
.description |
Optional object description. |
... |
Named properties for object schemas. |
.required |
Whether the object itself is listed as required in its parent schema. |
.additional_properties |
Whether to allow additional properties. |
text |
A JSON Schema as a list or JSON string. |
path |
Path to a JSON Schema file. Exactly one of |
Details
When $schema is absent, MCP treats schemas as JSON Schema 2020-12. Authors
are responsible for schema validity and for runtime input/output conformance,
domain validation, authorization or access checks, side-effect safety,
output sanitization, and rate limiting where needed. List input preserves the
supplied R list shape: use a named empty list for {} and an unnamed empty
list for [].
Incoming JSON arrays remain R lists. These helpers do not coerce or validate argument values or supply R defaults. If an argument may be omitted, give the R function a default for it.
type_ignore() omits an argument from the advertised schema. It does not
reject a caller-supplied argument or enforce permissions.
Value
A lightweight mcplite tool type object.
Examples
type_string("A label.")
type_array(type_integer(), description = "Integer values.")
type_object(
.description = "A labeled score.",
label = type_string(),
score = type_number(required = FALSE)
)
type_from_schema(list(
type = "string",
minLength = 1
))
Construct protocol-native MCP tool results
Description
tool_result() is the explicit opt-in boundary for returning protocol-native
MCP results. Its content must be created with one of the content_*()
constructors; ordinary R values, including bare lists and JSON strings,
retain the single-text-block conversion used by mcplite::tool().
Usage
tool_result(
content = list(),
structured_content = NULL,
is_error = FALSE,
meta = list()
)
Arguments
content |
One content block created by a |
structured_content |
Optional R value representable as a JSON object,
array, or scalar. |
is_error |
Whether the result reports a tool error. |
meta |
Optional named result metadata. MCP wire names such as |
Value
A protocol-native tool result for return from a tool function.
Content and fallback
When structured_content is supplied and content is omitted,
tool_result() generates exactly one JSON text fallback block. Explicitly
supplied content is preserved as-is and no fallback is appended. Keep that
content useful on its own: a client's protocol may omit structured content
or restrict its shape. See mcp_server() under "Protocol compatibility".
See Return-value mappings for exact R-to-JSON mappings and
payload options. JSON-looking strings and jsonlite json objects remain
string values in structured_content; they are not parsed as raw JSON.
Tool authors own conformance with any advertised output schema.
Tool errors
is_error = TRUE reports a tool-level error while retaining the normal MCP
tool-result envelope.
See Also
mcplite-results, content_text(), content_image(),
content_audio(), content_resource_link(), and content_resource().
Examples
tool_result(content_text("Done."))
tool_result(
content = list(
content_text("Generated the plot."),
content_image("base64-data", "image/png")
)
)
tool_result(structured_content = list(ok = TRUE, count = 2L))