Release candidate — 1.0.0-rc.9
StitchAPI
Surfaces

MCP

Expose a single code-mode run_stitch tool to agents instead of one tool per endpoint.

Reach for the MCP surface when an agent should call your stitches: it exposes a single code-mode run_stitch tool over MCP, plus a list_stitches discovery tool — so the agent's tool list stays tiny no matter how many stitches you register, and never touches a credential.

Example

Start the server with the stitch CLI. It loads your stitches module and speaks JSON-RPC over stdio (newline-delimited), with a startup notice on stderr:

stitch mcp --module ./stitches.ts

The agent then calls one tool — run_stitch — passing the stitch name and its input. The tool call looks like this:

{
    "name": "run_stitch",
    "input": {
        "name": "getUser",
        "input": { "params": { "id": "42" }, "query": { "expand": "owner" } }
    }
}

input carries { params, query, body, headers }; run_stitch runs the named stitch behind the capability boundary and returns its validated result.

The agent's object is the call input, not the request: a slot with a declared input schema is rebuilt from what that schema returned, so a key the model invented is stripped before the request. A slot with no schema stays a passthrough — declare the slots an agent may fill.

Options

run_stitch takes { name, input } and covers every registered stitch with one tool — name selects the stitch, input is its { params, query, body, headers } object. This is code-mode: one tool, not one tool per path.

list_stitches is the discovery tool. It takes no arguments and returns each stitch's name, method, and path, so the agent knows what to pass to run_stitch. The path is the route template: a literal query written into it (/v1/{id}?sig=…) is the operator's pinned default, and can be a credential, so it is cut. A {?limit} template operator is part of the template and stays.

Which names a call resolves

A tool call resolves name against the registry's own keys and nothing else. For stitch mcp --module, a key is the export name. The names list_stitches returns are exactly the names run_stitch and describe_stitch accept:

  • A stitch's configured name is a trace label, not an address. Expose export const approvedRefund = stitch({ name: 'issueRefund', … }) and the agent can call approvedRefund but not issueRefund. Renaming a stitch in the registry you hand over really hides the old name. The one exception is a module's default export: it has no export name, so stitch mcp --module keys it by its configured name (or default when it has none), because that is the only name it has.
  • Inherited object keys are not stitches. constructor or toString is an unknown stitch error like any other missing name.

stitch run and stitch serve resolve names the same way.

stitch mcp --module ./stitches.ts exposes every stitch the module exports, including a login stitch or a write you only meant to call from code. To expose fewer, build the registry yourself and pass it to serveStdio (see Programmatic API). The object you hand over is the allow-list.

Errors the agent sees

A failed call returns an isError tool result whose text is the error message. Every URL in that message — absolute, schemeless or relative — has lost its userinfo and the values of its secret-bearing query parameters. A transport error often quotes the request URL, and with apiKey({ in: 'query' }) that URL carries the key. The model sees Failed to parse URL from http://api.vendor.test:99999/v1/metrics?api_key=REDACTED, never the key itself. The scrub happens in core, where the engine turns the transport's throw into the stitch's error, so the same message reaches every other surface too (see Errors & pitfalls); the MCP server scrubs once more at its own boundary as a second line. The scrubber matches the same parameter names as the trace sinks, including any name apiKey({ in: 'query', name }) registers. describe_stitch scrubs the endpoint it reports the same way, so a credential written into a baseUrl does not reach the model either. A key in a header is still the better choice: it is never part of a URL.

Tool annotations

Each tool carries MCP annotations, so a host can decide whether to ask a human before a call:

ToolreadOnlyHintdestructiveHintopenWorldHint
list_stitchestrue—false
describe_stitchtrue—false
run_stitchfalsetruetrue

run_stitch is one tool for every stitch you register, a write included, so it claims nothing that does not hold for all of them. A host reading these hints still cannot tell a read from a refund. To make that distinction, register only the stitches the agent may call.

Why one tool beats one-per-path: a tool per registered stitch floods the agent's context — every name, description, and input schema is re-sent on each turn. Code mode keeps the tool list at two entries however many stitches you register, so the agent spends its context on the task, not on a tool catalog. The full design of the tool lives in the agents section; see run_stitch & code mode.

Anti-pattern: don't register one MCP tool per stitch or path — say, a separate tool for each route under api.example.com — to make the agent's calls "more explicit"; that re-sends every tool's name, description, and input schema on each turn and floods the model's context. Expose the single code-mode run_stitch tool (with list_stitches for discovery) and let the agent select the stitch by name instead. See run_stitch & code mode.

Programmatic API

The CLI is a thin wrapper. To run the MCP server in your own process, import serveStdio from the stitchapi/mcp subpath and hand it a registry of named stitches:

import * as stitches from './stitches';

import { serveStdio } from 'stitchapi/mcp';

// Reads newline-delimited JSON-RPC from stdin, writes responses to stdout.
const { close } = serveStdio(stitches);

// later
await close();

serveStdio(registry, options?) wires the run_stitch / list_stitches server to stdio and returns a handle whose close() detaches the transport. Pass { stdin, stdout } to bind a different stream pair instead of stdio, and server to set the identity reported in the MCP initialize result — a bare string names it (server: 'orders-api' ≡ server: { name: 'orders-api' }). The version defaults to the package version baked in at build time, so it can never drift from the release you installed; set server: { name, version } only when you want to report your own.

The capability boundary holds here: the agent calls run_stitch by name and never sees the token, cookie, or signing key the stitch holds — it invokes a capability, not a credential.

The transport writes JSON-RPC to stdout and its startup notice to stderr, so keep stdout clean — don't log to it or pipe anything else through it, or the framing breaks.

See also

On this page