MCP connections
Connect an MCP server, then pass its tools to run. The connector supports Streamable HTTP and local stdio processes through the MCP SDK.
const connection = await connectMcp(
{
name: "catalog",
url: "https://your-server.example/mcp",
},
{
identity,
authorize: async (actor, tool) => permissions.canRead(actor, tool),
},
);
try {
const result = await run({
provider,
tools: connection.tools,
identity,
input: "Find product A",
system: "Use the catalog to answer product questions.",
signal: AbortSignal.timeout(30_000),
});
} finally {
await connection.close();
}
Import connectMcp and run from Yada’s public entry point. identity must come from your authenticated backend. The permission callback is application code; each invocation checks current access. Each connection is bound to its original actor and tenant.
Configuration
| Field | Purpose |
|---|---|
name |
Unique server namespace, such as catalog. Lowercase letters, digits, and underscores; starts with a letter, up to 32 characters. |
url |
Remote MCP endpoint. |
token |
Optional bearer token issued for the MCP server, separate from your model API key. |
readTools |
Optional filter of tools your application has reviewed as read-only. |
confirmTools |
Standard mutating tools (no prepare/receipt contract) to run after one human approval: a list of names, or "*". See Confirmed tools. |
resources |
true adds the read tools <server>__list_resources and <server>__read_resource. See Resources, prompts and catalog changes. |
writeTools |
Explicit mapping of reviewed mutations to a version, preparation helper and receipt helper. |
headers |
Optional extra HTTP headers. authorization is rejected when token or OAuth is used. |
command, args |
Local server executable and arguments, instead of url and token. |
env, cwd |
Optional stdio environment variables (merged over the SDK default environment) and working directory. |
stderr |
Stdio only: "ignore" (default) or "inherit". |
timeoutMs |
Optional per-server deadline for connection, discovery and calls, 1,000 to 600,000 ms; default 30,000. |
For a local server:
const server = {
name: "catalog",
command: "node",
args: ["/absolute/path/to/catalog-server.js"],
readTools: ["lookup"],
};
A stdio server that needs credentials, such as GitHub’s MCP server:
const github = {
name: "github",
command: "docker",
args: [
"run",
"-i",
"--rm",
"-e",
"GITHUB_PERSONAL_ACCESS_TOKEN",
"ghcr.io/github/github-mcp-server",
],
env: { GITHUB_PERSONAL_ACCESS_TOKEN: process.env.GITHUB_TOKEN ?? "" },
timeoutMs: 60_000,
};
Use trusted server configuration for URLs and commands. The stdio transport launches and owns the process; closing the connection terminates it. Server stderr is suppressed unless stderr: "inherit". HTTP redirects are refused so credentials are not forwarded to another destination. For OAuth login and token refresh, see MCP OAuth. Hosts can also supply a valid bearer token.
Tool behavior
Remote tool names are prefixed: lookup becomes catalog__lookup. Use distinct connection names when combining tools from multiple servers. run rejects duplicate names.
Tool catalogs are discovered automatically. Without readTools, only tools advertising readOnlyHint: true are exposed as reads. With readTools, the host explicitly selects reviewed read tools, including tools without annotations. Tools explicitly marked as mutating are rejected from reads. Every tool still requires host authorization; annotations alone do not grant access. This does not sandbox or prevent side effects inside a remote server.
Discovery follows pagination with bounded page/tool counts and rejects duplicate tools, repeated cursors, or missing configured tools. The combined server__tool name must fit the provider-compatible 64-character limit; longer names fail with an error stating that limit. Reconnect to refresh a changed tool catalog.
Inputs are validated against the advertised JSON Schema before transmission. Draft 7 and draft 2020-12 schemas are supported; external references are not fetched and asynchronous schema extensions are rejected. Tool arguments and rendered results are bounded to 32,000 UTF-8 bytes. Structured read results become JSON text; text blocks are joined in order. A read tool that returns isError: true yields {"error":"tool_failed","message":...} (message bounded to 4,000 characters) so the model can react; write-path errors still fail. Non-text blocks render as placeholders: [image: mime], [audio: mime], [resource: uri], [resource_link: uri name]. Declared output schemas are validated against the discovered schema, including tools from earlier discovery pages.
Connection setup defaults to the server’s timeoutMs, which can be overridden with the third connectMcp argument. Discovery and calls use timeoutMs as the request timeout and honor cancellation signals. Always close connections after use. Image blocks in read results become image parts when they are png, jpeg, webp or gif, within 5 MB, and at most 4 per result; otherwise (and for audio) they render as [image: mime] placeholders. External tool content remains untrusted data.
Approved writes
Use writes only with a server whose implementation your application has reviewed against the Yada action compatibility contract below. This contract is specific to Yada; MCP annotations do not establish idempotency, receipt lookup, authorization or optimistic concurrency guarantees. Ordinary MCP servers that lack these guarantees cannot safely be enabled as write tools.
const connection = await connectMcp(
{
name: "catalog",
url: "https://your-server.example/mcp",
writeTools: {
rename: {
version: "reviewed-implementation-1",
prepareTool: "prepare_rename",
receiptTool: "rename_receipt",
},
},
},
{ identity, authorize },
);
Pass these tools to the existing durable worker and approval flow. Each mapped mutation implements WriteTool; plain run without an action executor cannot execute it. Helpers remain hidden from the model. Mutation names, helper names and explicit readTools cannot overlap; helpers cannot be reused between mappings. Helpers must advertise readOnlyHint: true, and mutations cannot advertise that hint as true. These are consistency checks alongside the host’s implementation review.
The permission callback receives original remote tool names. Preparation checks access to both the mutation and preparation helper; execution checks the mutation; reconciliation checks both the mutation and receipt helper. Every phase enforces the connection’s actor and tenant, current authorization, cancellation and closure, including direct method calls.
The server contract uses these tools/call envelopes:
| Phase | arguments |
Required structuredContent |
|---|---|---|
| Preparation | { "arguments": <original model arguments> } |
{ "preview": <nonempty string>, "version": <state version> } |
| Execution | Original model arguments, unchanged | { "status": "completed", "receipt": <nonempty string> } |
| Receipt lookup | {} |
{ "status": "completed", "receipt": <nonempty string> } or { "status": "unknown" } |
Every request adds top-level params._meta["io.yada/action"]:
{
"contractVersion": 1,
"tool": "rename",
"actorId": "authenticated actor",
"tenantId": "authenticated tenant"
}
Execution also adds operationId and the approved state version from the durable action context. Receipt lookup adds only operationId; it must resolve that operation without issuing the mutation again. Model arguments cannot select or override this metadata. The preparation state version is a nonempty string of at most 1,024 characters; operation IDs are nonempty strings of at most 256 characters. Result envelopes are strict, size-bounded objects; text-only responses, empty receipts, additional fields or missing fields cannot establish successful execution.
The server must authenticate its actual connection credentials and validate the metadata’s actor and tenant against that authenticated principal. Treating client metadata alone as authorization is insufficient. Preparation must be read-only. Execution must atomically enforce business authorization, the approved state version and operation-ID idempotency, scoped to the actor, tenant and mutation. Return the authoritative receipt for repeated operation IDs without performing another write. Receipt lookup must be read-only and return an authoritative result for that exact scope; missing, delayed or ambiguous outcomes stay unknown.
Transport failures, malformed responses, server errors and an execution response of unknown leave the durable action reconciling. Recovery calls only the receipt helper. It never resends an uncertain mutation, including after reconnect or process restart.
The adapter’s WriteTool.version combines the reviewed version with a fingerprint of the target URL or stdio command/arguments, helper mapping and all three input/output schemas. Rotating bearer tokens are excluded. Changed policy prevents an old approval from crossing endpoints, mappings or schemas. reconnectingMcp also pins effect, version and specification, delegates all three write phases, reauthorizes each call and never replays a failed mutation.
Read-only literals retain McpConnection<ReadTool> through overloads. Configurations that might include writeTools return McpConnection<Tool>; narrow tool.effect before calling phase-specific methods. McpReadConfig expresses a configuration that cannot contain writes.
Confirmed tools
confirmTools exposes the mutating tools of any MCP server, with no server changes, through confirmTool: each call needs one approval in the durable worker flow and runs at most once. There is no receipt helper, so an uncertain outcome (timeout, transport failure) becomes “outcome unknown” and is never retried.
const connection = await connectMcp(
{
name: "tracker",
url: "https://tracker.example/mcp",
confirmTools: ["create_issue"],
},
{ identity, authorize },
);
"*" confirms every discovered tool that has no other role and is not annotated readOnlyHint: true. Names in confirmTools cannot overlap readTools, writeTools or write helpers, and each must exist. The approved version covers the tool name and input schema; reconnectingMcp includes it in its contract check. A definitive remote isError: true response is returned to the model as {"error":"tool_failed","message":...}, as for reads.
Resources, prompts and catalog changes
With resources: true, the model gets <server>__list_resources (optional cursor) and <server>__read_resource (uri). Both are reads, authorized with the names list_resources and read_resource. Output is text only (binary content renders as [blob: mime]) and truncated to 32,000 characters.
connection.prompts(identity) lists the server’s prompts and connection.getPrompt(identity, name, args) returns the rendered messages as text for the host to use as system or user input. Both are host calls, always present on connectMcp and reconnectingMcp connections. They run the same checks as tools: access.authorize is called with the names list_prompts and get_prompt, the identity must match the connection owner, and they fail after close() and with the stale error below when applicable. Listing follows at most 100 cursor pages and throws beyond that, like tool discovery.
When the server sends notifications/tools/list_changed, Yada rediscovers the catalog and compares the tools it exposes (names, descriptions, schemas, annotations). If unchanged, the connection continues. Otherwise it is stale: isConnected() returns false and new calls fail with MCP tool contract changed; reconnect required until the host reconnects. reconnectingMcp does this automatically and accepts the new connection only if its contract matches the original. After a failed reconnect it rethrows the same error without reconnecting for 30 seconds.
Optional environment configuration
Leave YADA_MCP_SERVERS unset to use no MCP servers, or supply a JSON array:
YADA_MCP_SERVERS=[{"name":"catalog","url":"https://your-server.example/mcp","headers":{"x-api-key":"..."},"timeoutMs":60000}]
mcpServersFromEnv(process.env) validates this array and returns server configurations for connectMcp. Each server has a unique name. Connect them with the appropriate identity and authorization callback, combine their tools arrays for run, and close all connections after use. OAuth stores and callback handlers are host code, not JSON settings. The environment loader does not launch processes or connect by itself.