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.tsThe 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
nameis a trace label, not an address. Exposeexport const approvedRefund = stitch({ name: 'issueRefund', … })and the agent can callapprovedRefundbut notissueRefund. Renaming a stitch in the registry you hand over really hides the old name. The one exception is a module'sdefaultexport: it has no export name, sostitch mcp --modulekeys it by its configuredname(ordefaultwhen it has none), because that is the only name it has. - Inherited object keys are not stitches.
constructorortoStringis anunknown stitcherror 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:
| Tool | readOnlyHint | destructiveHint | openWorldHint |
|---|---|---|---|
list_stitches | true | — | false |
describe_stitch | true | — | false |
run_stitch | false | true | true |
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.