@smthrs/mcp 0.0.0-stage → 1.0.0-rc.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +121 -0
- package/LICENSE +21 -0
- package/README.md +111 -2
- package/dist/cjs/Diagnostics.d.ts +46 -0
- package/dist/cjs/Diagnostics.d.ts.map +1 -0
- package/dist/cjs/Diagnostics.js +29 -0
- package/dist/cjs/Diagnostics.js.map +7 -0
- package/dist/cjs/McpClient.d.ts +304 -0
- package/dist/cjs/McpClient.d.ts.map +1 -0
- package/dist/cjs/McpClient.js +622 -0
- package/dist/cjs/McpClient.js.map +7 -0
- package/dist/cjs/McpError.d.ts +44 -0
- package/dist/cjs/McpError.d.ts.map +1 -0
- package/dist/cjs/McpError.js +41 -0
- package/dist/cjs/McpError.js.map +7 -0
- package/dist/cjs/McpFlows.d.ts +112 -0
- package/dist/cjs/McpFlows.d.ts.map +1 -0
- package/dist/cjs/McpFlows.js +127 -0
- package/dist/cjs/McpFlows.js.map +7 -0
- package/dist/cjs/index.d.ts +39 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +41 -0
- package/dist/cjs/index.js.map +7 -0
- package/dist/cjs/internal/DiagnosticReporter.d.ts +17 -0
- package/dist/cjs/internal/DiagnosticReporter.d.ts.map +1 -0
- package/dist/cjs/internal/DiagnosticReporter.js +56 -0
- package/dist/cjs/internal/DiagnosticReporter.js.map +7 -0
- package/dist/cjs/internal/HttpTransport.d.ts +67 -0
- package/dist/cjs/internal/HttpTransport.d.ts.map +1 -0
- package/dist/cjs/internal/HttpTransport.js +298 -0
- package/dist/cjs/internal/HttpTransport.js.map +7 -0
- package/dist/cjs/internal/JsonLimits.d.ts +29 -0
- package/dist/cjs/internal/JsonLimits.d.ts.map +1 -0
- package/dist/cjs/internal/JsonLimits.js +51 -0
- package/dist/cjs/internal/JsonLimits.js.map +7 -0
- package/dist/cjs/internal/Limits.d.ts +36 -0
- package/dist/cjs/internal/Limits.d.ts.map +1 -0
- package/dist/cjs/internal/Limits.js +34 -0
- package/dist/cjs/internal/Limits.js.map +7 -0
- package/dist/cjs/internal/Rpc.d.ts +141 -0
- package/dist/cjs/internal/Rpc.d.ts.map +1 -0
- package/dist/cjs/internal/Rpc.js +92 -0
- package/dist/cjs/internal/Rpc.js.map +7 -0
- package/dist/cjs/internal/StdioTransport.d.ts +78 -0
- package/dist/cjs/internal/StdioTransport.d.ts.map +1 -0
- package/dist/cjs/internal/StdioTransport.js +310 -0
- package/dist/cjs/internal/StdioTransport.js.map +7 -0
- package/dist/cjs/internal/Transport.d.ts +87 -0
- package/dist/cjs/internal/Transport.d.ts.map +1 -0
- package/dist/cjs/internal/Transport.js +116 -0
- package/dist/cjs/internal/Transport.js.map +7 -0
- package/dist/cjs/package.json +1 -0
- package/dist/esm/Diagnostics.d.ts +46 -0
- package/dist/esm/Diagnostics.d.ts.map +1 -0
- package/dist/esm/Diagnostics.js +26 -0
- package/dist/esm/Diagnostics.js.map +1 -0
- package/dist/esm/McpClient.d.ts +304 -0
- package/dist/esm/McpClient.d.ts.map +1 -0
- package/dist/esm/McpClient.js +671 -0
- package/dist/esm/McpClient.js.map +1 -0
- package/dist/esm/McpError.d.ts +44 -0
- package/dist/esm/McpError.d.ts.map +1 -0
- package/dist/esm/McpError.js +43 -0
- package/dist/esm/McpError.js.map +1 -0
- package/dist/esm/McpFlows.d.ts +112 -0
- package/dist/esm/McpFlows.d.ts.map +1 -0
- package/dist/esm/McpFlows.js +167 -0
- package/dist/esm/McpFlows.js.map +1 -0
- package/dist/esm/index.d.ts +39 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +39 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/internal/DiagnosticReporter.d.ts +17 -0
- package/dist/esm/internal/DiagnosticReporter.d.ts.map +1 -0
- package/dist/esm/internal/DiagnosticReporter.js +44 -0
- package/dist/esm/internal/DiagnosticReporter.js.map +1 -0
- package/dist/esm/internal/HttpTransport.d.ts +67 -0
- package/dist/esm/internal/HttpTransport.d.ts.map +1 -0
- package/dist/esm/internal/HttpTransport.js +266 -0
- package/dist/esm/internal/HttpTransport.js.map +1 -0
- package/dist/esm/internal/JsonLimits.d.ts +29 -0
- package/dist/esm/internal/JsonLimits.d.ts.map +1 -0
- package/dist/esm/internal/JsonLimits.js +55 -0
- package/dist/esm/internal/JsonLimits.js.map +1 -0
- package/dist/esm/internal/Limits.d.ts +36 -0
- package/dist/esm/internal/Limits.d.ts.map +1 -0
- package/dist/esm/internal/Limits.js +41 -0
- package/dist/esm/internal/Limits.js.map +1 -0
- package/dist/esm/internal/Rpc.d.ts +141 -0
- package/dist/esm/internal/Rpc.d.ts.map +1 -0
- package/dist/esm/internal/Rpc.js +129 -0
- package/dist/esm/internal/Rpc.js.map +1 -0
- package/dist/esm/internal/StdioTransport.d.ts +78 -0
- package/dist/esm/internal/StdioTransport.d.ts.map +1 -0
- package/dist/esm/internal/StdioTransport.js +332 -0
- package/dist/esm/internal/StdioTransport.js.map +1 -0
- package/dist/esm/internal/Transport.d.ts +87 -0
- package/dist/esm/internal/Transport.d.ts.map +1 -0
- package/dist/esm/internal/Transport.js +146 -0
- package/dist/esm/internal/Transport.js.map +1 -0
- package/docs/README.md +139 -0
- package/docs/api.md +469 -0
- package/docs/concepts/the-session.md +135 -0
- package/docs/concepts/tools-as-flows.md +116 -0
- package/docs/guides/bound-an-untrusted-server.md +158 -0
- package/docs/guides/configure-servers-for-the-cli.md +167 -0
- package/docs/guides/connect-a-server.md +161 -0
- package/docs/guides/grant-authority-to-mcp-tools.md +130 -0
- package/docs/guides/handle-a-failed-tool-call.md +125 -0
- package/docs/guides/select-the-tools-a-run-sees.md +92 -0
- package/docs/guides/testing.md +132 -0
- package/docs/guides/validate-structured-output.md +103 -0
- package/docs/installation.md +117 -0
- package/docs/quickstart.md +200 -0
- package/docs/troubleshooting.md +316 -0
- package/package.json +157 -3
- package/src/Diagnostics.ts +47 -0
- package/src/McpClient.ts +985 -0
- package/src/McpError.ts +52 -0
- package/src/McpFlows.ts +210 -0
- package/src/index.ts +42 -0
- package/src/internal/DiagnosticReporter.ts +47 -0
- package/src/internal/HttpTransport.ts +400 -0
- package/src/internal/JsonLimits.ts +53 -0
- package/src/internal/Limits.ts +48 -0
- package/src/internal/Rpc.ts +219 -0
- package/src/internal/StdioTransport.ts +491 -0
- package/src/internal/Transport.ts +178 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "The life of a session"
|
|
3
|
+
description: "How an MCP session starts, what it holds, when it ends, and what happens to in-flight requests: the handshake, the catalog snapshot, scope-bound teardown, and cancellation."
|
|
4
|
+
sidebar:
|
|
5
|
+
order: 2
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
An MCP session is a subprocess. Everything surprising about this package follows
|
|
9
|
+
from that: the process has to be started, negotiated with, bounded, and torn
|
|
10
|
+
down, and any of those steps can end the session in the middle of a call.
|
|
11
|
+
|
|
12
|
+
## Starting
|
|
13
|
+
|
|
14
|
+
`McpClient.connect` performs five steps before it returns, and a failure at any
|
|
15
|
+
of them fails the whole effect:
|
|
16
|
+
|
|
17
|
+
1. Spawn the command with `stdin`, `stdout`, and `stderr` piped. A spawn that
|
|
18
|
+
fails is `spawn_failed`.
|
|
19
|
+
2. Send `initialize`, proposing `2025-06-18` and disclosing the frozen
|
|
20
|
+
`McpClient.clientInfo` identity: name `"smithers"` and this package's
|
|
21
|
+
version.
|
|
22
|
+
3. Validate the result. The server's `protocolVersion` must be one of
|
|
23
|
+
`McpClient.supportedProtocolVersions`, and its `capabilities` must declare a
|
|
24
|
+
`tools` object. A server that serves no tools is refused with
|
|
25
|
+
`protocol_error` rather than connected and left useless.
|
|
26
|
+
4. Send `notifications/initialized`. This is a notification, not a request: the
|
|
27
|
+
server never replies, and the handshake is not complete until the client
|
|
28
|
+
sends it.
|
|
29
|
+
5. Walk `tools/list`, following `nextCursor` until the server stops sending one.
|
|
30
|
+
|
|
31
|
+
Steps 2 through 5 are each bounded by `handshakeTimeoutMs`, which defaults to 10
|
|
32
|
+
seconds and is separate from the deadline later tool calls get.
|
|
33
|
+
|
|
34
|
+
## Holding
|
|
35
|
+
|
|
36
|
+
What the session holds is small: the server name, the tool catalog, and a way to
|
|
37
|
+
call one entry of it.
|
|
38
|
+
|
|
39
|
+
The catalog is a **deeply frozen snapshot**, including tool entries and their
|
|
40
|
+
schemas. Editing the public catalog cannot change future dispatch or validation;
|
|
41
|
+
make a separate copy if a consumer needs a mutable view. It is fetched once and
|
|
42
|
+
never refreshed. A server
|
|
43
|
+
that later announces `notifications/tools/list_changed` is not re-polled, and
|
|
44
|
+
server-initiated notifications are received and dropped. To pick up a changed
|
|
45
|
+
catalog, close the scope and connect again.
|
|
46
|
+
|
|
47
|
+
That choice is what makes the projection stable. `McpFlows` derives flow
|
|
48
|
+
declarations from the catalog, and a declaration digest that changed underneath a
|
|
49
|
+
running cell would be refused by the boundary that recorded it.
|
|
50
|
+
|
|
51
|
+
`callTool` checks the name against that snapshot before it writes anything: an
|
|
52
|
+
unknown tool fails with `tool_not_found` without a JSON-RPC frame reaching the
|
|
53
|
+
server.
|
|
54
|
+
|
|
55
|
+
## Ending
|
|
56
|
+
|
|
57
|
+
The connection is scoped, and the scope is the lifetime. Closing it tears the
|
|
58
|
+
process down. Four other events end a session, and all five are the same
|
|
59
|
+
terminal transition:
|
|
60
|
+
|
|
61
|
+
| Event | The error every waiter gets |
|
|
62
|
+
| ---------------------------- | ---------------------------------------------- |
|
|
63
|
+
| The scope closes. | `connection_closed`, "connection scope closed" |
|
|
64
|
+
| The child exits. | `connection_closed`, "exited with code N" |
|
|
65
|
+
| Its stdout reaches EOF. | `connection_closed`, "stdout closed" |
|
|
66
|
+
| Its stdin closes. | `connection_closed`, "stdin closed" |
|
|
67
|
+
| A tagged reply is malformed. | `protocol_error`, naming what was malformed |
|
|
68
|
+
|
|
69
|
+
Whichever arrives first wins. It closes the connection once, fails every pending
|
|
70
|
+
request with that one error, and rejects all later traffic with the same error,
|
|
71
|
+
so a caller never waits on a reply that can no longer come. A clean child exit
|
|
72
|
+
is still a closed session: Node reports an ordinary exit by ending stdout
|
|
73
|
+
successfully. When a healthy connection's scope closes, its terminal reason is
|
|
74
|
+
recorded before I/O teardown, so cleanup does not report "stdin closed".
|
|
75
|
+
|
|
76
|
+
For `spawn_failed`, `timeout`, and `connection_closed`, the message withholds
|
|
77
|
+
process details. A child's stderr may contain credentials, so the client
|
|
78
|
+
redacts each complete stderr line as it arrives and caps only the redacted text.
|
|
79
|
+
A cap cannot cut a credential's recognizable prefix, and a line longer than
|
|
80
|
+
64 KiB is withheld whole. The bounded, whitespace-collapsed tail is available
|
|
81
|
+
only to an explicitly installed
|
|
82
|
+
[private host diagnostic observer](../api.md#diagnostics); it is never appended
|
|
83
|
+
to the ordinary error. Without that observer, private details are discarded.
|
|
84
|
+
|
|
85
|
+
## Noise and protocol traffic on stdout
|
|
86
|
+
|
|
87
|
+
MCP servers commonly log to stdout, so this client distinguishes noise from
|
|
88
|
+
protocol:
|
|
89
|
+
|
|
90
|
+
- A blank line, invalid JSON, a JSON scalar, an array, `null`, or a JSON object
|
|
91
|
+
with no own `jsonrpc` property is **noise**, and is dropped.
|
|
92
|
+
- An object that does carry `jsonrpc` is **protocol traffic**. Its version must
|
|
93
|
+
be exactly `"2.0"`. A valid method-bearing envelope without an id is a
|
|
94
|
+
notification and is dropped; with an id it is a server request. Anything
|
|
95
|
+
else must be a valid reply with an id and exactly one of `result` or `error`.
|
|
96
|
+
|
|
97
|
+
Server `ping` requests receive `{ result: {} }`, preserving the exact string or
|
|
98
|
+
integer id, including while a tool call is pending. Unsupported server methods
|
|
99
|
+
receive JSON-RPC `-32601` (`Method not found`); they are never silently dropped.
|
|
100
|
+
Server-request ids are independent of client-request ids. Reply frames use the
|
|
101
|
+
same outbound byte and queue bounds, with `requestTimeoutMs` limiting queue
|
|
102
|
+
admission; a blocked response closes the connection instead of hanging the
|
|
103
|
+
reader. This implements the negotiated
|
|
104
|
+
[MCP ping contract](https://modelcontextprotocol.io/specification/2025-06-18/basic/utilities/ping).
|
|
105
|
+
|
|
106
|
+
A malformed tagged envelope closes the connection with `protocol_error`. A
|
|
107
|
+
well-formed reply for an id nobody is waiting on is dropped. The raw frame is
|
|
108
|
+
never attached to the error.
|
|
109
|
+
|
|
110
|
+
A valid error reply with `id: null` cannot be correlated to a request. Its code,
|
|
111
|
+
message, and data go only to the private diagnostic observer as `remote-error`,
|
|
112
|
+
then the reply is dropped. The connection stays open and pending requests keep
|
|
113
|
+
waiting for their own replies or deadlines. A null id on a result, or a malformed
|
|
114
|
+
error object with a null id, still closes the connection with `protocol_error`.
|
|
115
|
+
|
|
116
|
+
## Cancelling
|
|
117
|
+
|
|
118
|
+
Every MCP tool flow is declared `irreversible`, so an abandoned in-flight
|
|
119
|
+
mutation is a durability problem rather than a tidiness one. When a `tools/call`
|
|
120
|
+
times out or its fiber is interrupted before dispatch, the writer skips its
|
|
121
|
+
queued frame. Once handed to the writer, an abandoned request triggers one
|
|
122
|
+
best-effort `notifications/cancelled` for that request id.
|
|
123
|
+
|
|
124
|
+
The notification is best effort. A full outbound queue drops it rather than
|
|
125
|
+
delaying the deadline it reports, because a cancellation that made a timeout
|
|
126
|
+
late would defeat the timeout. `initialize` is never cancelled, and a request
|
|
127
|
+
that already received a reply, whether a result or a JSON-RPC error, is never
|
|
128
|
+
cancelled either.
|
|
129
|
+
|
|
130
|
+
## Next
|
|
131
|
+
|
|
132
|
+
- [Connect a server](../guides/connect-a-server.md): the options that shape all
|
|
133
|
+
of this.
|
|
134
|
+
- [Bound an untrusted server](../guides/bound-an-untrusted-server.md): the ten
|
|
135
|
+
limits and what each one protects.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "A remote tool as a flow"
|
|
3
|
+
description: "What McpFlows projects from a tool catalog: the flow name, the input document, the result shape, and the declaration every MCP tool flow shares."
|
|
4
|
+
sidebar:
|
|
5
|
+
order: 1
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
The whole adapter is one idea: a remote MCP tool is a flow declaration plus the
|
|
9
|
+
code that runs it, which is what every other flow already is. Nothing about the
|
|
10
|
+
harness, the registry, or the cell loop learns that a given flow proxies a
|
|
11
|
+
remote `tools/call`. A cell that reads a file and a cell that calls an MCP tool
|
|
12
|
+
run the identical two lines: find the flow in `ctx.flows`, invoke it with
|
|
13
|
+
`ctx.call`.
|
|
14
|
+
|
|
15
|
+
That is why this package is small. It is a client and a projector, not a second
|
|
16
|
+
capability system.
|
|
17
|
+
|
|
18
|
+
## What one tool becomes
|
|
19
|
+
|
|
20
|
+
`McpFlows.mcp` walks the catalog and produces one `FlowBinding.Binding` per
|
|
21
|
+
tool. Each binding declares:
|
|
22
|
+
|
|
23
|
+
| Field | Value |
|
|
24
|
+
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
25
|
+
| `name` | `<prefix>/<tool>`, where the prefix defaults to `mcp/<server>`. |
|
|
26
|
+
| `description` | `MCP tool "<tool>" on server "<server>"`, followed by `. Server-supplied description: <text>` when the server sent one, so the model reads the text as the remote server's. |
|
|
27
|
+
| `input` | `McpFlows.Args`, a permissive `Record<string, unknown>`. |
|
|
28
|
+
| `inputDocument` | The server's own `inputSchema` document, carried by value. |
|
|
29
|
+
| `output` | `McpFlows.Result`: `content`, `isError`, and optional `structuredContent`. |
|
|
30
|
+
| `capabilities` | `McpFlows.capabilities`, the same frozen list for every tool. |
|
|
31
|
+
| `effects` | `McpFlows.effects`, the same conservative envelope for every tool. |
|
|
32
|
+
|
|
33
|
+
The name is scoped by server so two servers may offer a tool of the same name
|
|
34
|
+
without colliding. A tool name may not contain `/`, which is what keeps
|
|
35
|
+
`mcp/<server>/<tool>` unambiguous.
|
|
36
|
+
|
|
37
|
+
## Two schemas, on purpose
|
|
38
|
+
|
|
39
|
+
The runtime decode and the disclosed parameter shape are different documents,
|
|
40
|
+
and the difference is deliberate.
|
|
41
|
+
|
|
42
|
+
`input` is `McpFlows.Args`, which accepts any string-keyed record. The adapter
|
|
43
|
+
does not validate arguments, because the remote server owns that decision: its
|
|
44
|
+
schema is the authority, and a second validator here would either duplicate it
|
|
45
|
+
or disagree with it.
|
|
46
|
+
|
|
47
|
+
`inputDocument` is the server's own JSON Schema, passed through unchanged. That
|
|
48
|
+
is what a caller reading `ctx.flows` sees, and what a model is shown. So the
|
|
49
|
+
model gets the real parameter shape while the boundary stays out of the
|
|
50
|
+
server's way.
|
|
51
|
+
|
|
52
|
+
This is why every catalog entry must declare an `inputSchema` whose `type` is
|
|
53
|
+
exactly `"object"`. A tool with no usable parameter document would be disclosed
|
|
54
|
+
to a model as if it had one, so the catalog is rejected at connect time
|
|
55
|
+
instead.
|
|
56
|
+
|
|
57
|
+
## A tool-level failure is data
|
|
58
|
+
|
|
59
|
+
A `tools/call` that reaches the server and comes back is a success, whatever it
|
|
60
|
+
says. MCP distinguishes "the call failed" from "the tool ran and reported a
|
|
61
|
+
problem", and the second one arrives as `{ isError: true }` on a successful
|
|
62
|
+
reply. The binding returns it in the success channel.
|
|
63
|
+
|
|
64
|
+
`McpError` is the other channel entirely, reserved for failures of the session:
|
|
65
|
+
the server would not start, the pipe closed, a reply could not be parsed.
|
|
66
|
+
The binding explicitly publishes `McpError.message` as a catchable
|
|
67
|
+
`flow_failed` result. The client authors this model-facing text with remote
|
|
68
|
+
error bodies and process diagnostics withheld. Custom clients must preserve
|
|
69
|
+
that public-message contract. See
|
|
70
|
+
[Handle a failed tool call](../guides/handle-a-failed-tool-call.md).
|
|
71
|
+
|
|
72
|
+
## One declaration for every tool
|
|
73
|
+
|
|
74
|
+
Every projected flow declares the same authority and the same effects, because
|
|
75
|
+
an MCP tool is opaque code this adapter does not control. Guessing narrower
|
|
76
|
+
would be a claim nobody can back.
|
|
77
|
+
|
|
78
|
+
`McpFlows.effects` declares `reads: ["**"]`, `writes: ["**"]`,
|
|
79
|
+
`mode: "expected"`, `onConflict: "serialize"`, and `tier: "irreversible"`. The
|
|
80
|
+
irreversible tier is why a timed-out call sends a cancellation: an abandoned
|
|
81
|
+
in-flight mutation is a durability problem.
|
|
82
|
+
|
|
83
|
+
`McpFlows.capabilities` is one exact `namespace:operation:resource` string per
|
|
84
|
+
host action, at resource `**`, derived from `Capability.Action.literals`. It is
|
|
85
|
+
spelled out one action at a time rather than as a wildcard because the cell
|
|
86
|
+
boundary parses a declared capability with `Capability.parse`, which requires
|
|
87
|
+
exactly three colon-separated components. A bare `"*"` parses as nothing, and an
|
|
88
|
+
unparseable declaration counts as unauthorized, so a wildcard would refuse every
|
|
89
|
+
MCP tool before it ran, under every envelope including an unrestricted one.
|
|
90
|
+
|
|
91
|
+
Deriving the list rather than restating it means a new action in
|
|
92
|
+
[`@smthrs/capability`](/api/capability) cannot be silently dropped from what an
|
|
93
|
+
MCP tool declares.
|
|
94
|
+
|
|
95
|
+
The consequence for a host is that the projected declaration is a ceiling, not a
|
|
96
|
+
recommendation. Narrowing it is the host's decision and its own step; see
|
|
97
|
+
[Grant authority to MCP tools](../guides/grant-authority-to-mcp-tools.md).
|
|
98
|
+
|
|
99
|
+
## Where the source goes
|
|
100
|
+
|
|
101
|
+
`FlowBinding.Source` is the same type `StandardFlows.filesystem` and
|
|
102
|
+
`StandardFlows.shell` return, so a projected server composes with them in one
|
|
103
|
+
array:
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
const catalog = yield * FlowBinding.catalog([
|
|
107
|
+
StandardFlows.filesystem(filesystemServices),
|
|
108
|
+
StandardFlows.shell(shellServices),
|
|
109
|
+
mcpSource
|
|
110
|
+
])
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Duplicate flow names fail composition rather than dispatching one descriptor to
|
|
114
|
+
another implementation, which is another reason the default prefix carries the
|
|
115
|
+
server name. For the harness side of this, see
|
|
116
|
+
[Bind flows](/pkg/harness/guides/bind-flows).
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Bound an untrusted server"
|
|
3
|
+
description: "The ten limits McpClient enforces on a remote server, their default values, what each one protects, and the rules a tool name must satisfy."
|
|
4
|
+
sidebar:
|
|
5
|
+
order: 6
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
The server sizes the frames it sends, the catalog it declares, and the flow
|
|
9
|
+
names a model reads. All three are inputs from a process you do not control, so
|
|
10
|
+
every one of them is bounded, and every bound is an option with an exported
|
|
11
|
+
default constant.
|
|
12
|
+
|
|
13
|
+
You rarely need to change these. Read this page when a real server trips one, or
|
|
14
|
+
when you are deciding what to allow a server you do not trust.
|
|
15
|
+
|
|
16
|
+
## The ten limits
|
|
17
|
+
|
|
18
|
+
| Option | Default | Constant | What it bounds |
|
|
19
|
+
| ----------------------- | ------- | ---------------------------------------- | --------------------------------------------------- |
|
|
20
|
+
| `handshakeTimeoutMs` | 10000 | `McpClient.defaultHandshakeTimeoutMs` | Each `initialize` and `tools/list` exchange. |
|
|
21
|
+
| `requestTimeoutMs` | 120000 | `McpClient.defaultRequestTimeoutMs` | Each later `tools/call` exchange. |
|
|
22
|
+
| `queueCapacity` | 64 | `McpClient.defaultQueueCapacity` | Outbound frames waiting to be written. |
|
|
23
|
+
| `maxFrameBytes` | 1048576 | `McpClient.defaultMaxFrameBytes` | One inbound JSON-RPC frame, in UTF-8 bytes. |
|
|
24
|
+
| `maxOutboundFrameBytes` | 1048576 | `McpClient.defaultMaxOutboundFrameBytes` | One outbound JSON-RPC frame, in UTF-8 bytes. |
|
|
25
|
+
| `maxTools` | 256 | `McpClient.defaultMaxTools` | Tools accepted across every catalog page. |
|
|
26
|
+
| `maxToolNameBytes` | 128 | `McpClient.defaultMaxToolNameBytes` | UTF-8 bytes in one tool name. |
|
|
27
|
+
| `maxToolDocumentBytes` | 65536 | `McpClient.defaultMaxToolDocumentBytes` | One tool's description plus JSON `inputSchema`. |
|
|
28
|
+
| `maxCatalogPages` | 32 | `McpClient.defaultMaxCatalogPages` | `tools/list` pages walked while following a cursor. |
|
|
29
|
+
| `maxStderrBytes` | 2048 | `McpClient.defaultMaxStderrBytes` | Child stderr retained as a diagnostic tail. |
|
|
30
|
+
|
|
31
|
+
Every one must be a positive safe integer. A zero, a negative number, a fraction,
|
|
32
|
+
or a number above `Number.MAX_SAFE_INTEGER`
|
|
33
|
+
fails with `protocol_error` naming the option, before the process is spawned.
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
const client = yield * McpClient.connect({
|
|
37
|
+
server: "large",
|
|
38
|
+
command: "node",
|
|
39
|
+
args: ["server.mjs"],
|
|
40
|
+
maxTools: 512,
|
|
41
|
+
maxFrameBytes: 4 * 1024 * 1024
|
|
42
|
+
})
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## What each one is actually protecting
|
|
46
|
+
|
|
47
|
+
**The two frame sizes** protect memory. The stdout reader never retains more
|
|
48
|
+
than one bounded partial frame, so a server that writes a gigabyte without a
|
|
49
|
+
newline fails at the limit rather than growing the heap. The outbound bound
|
|
50
|
+
catches the mirror case: a tool argument large enough to be its own problem is
|
|
51
|
+
refused before it is written.
|
|
52
|
+
|
|
53
|
+
**`queueCapacity`** bounds outbound frames waiting on a server that has stopped
|
|
54
|
+
reading its stdin. Past capacity, an offer blocks until the deadline for that
|
|
55
|
+
request expires.
|
|
56
|
+
|
|
57
|
+
**`maxTools`, `maxToolNameBytes`, `maxToolDocumentBytes`, and
|
|
58
|
+
`maxCatalogPages`** bound the catalog, which is the part a model reads.
|
|
59
|
+
`maxToolDocumentBytes` counts the UTF-8 bytes of a tool's description plus its
|
|
60
|
+
JSON-encoded `inputSchema`, so one tool cannot flood the model's context with
|
|
61
|
+
server-authored text. A catalog past `maxTools` fails with
|
|
62
|
+
`invalid_response` rather than being truncated, because a silently truncated
|
|
63
|
+
toolset is worse than a refused connection. `maxCatalogPages` and a repeated
|
|
64
|
+
cursor both end a cursor walk that would otherwise never finish.
|
|
65
|
+
|
|
66
|
+
**`maxStderrBytes`** bounds a diagnostic buffer. Child stderr is drained
|
|
67
|
+
continuously and only the tail is retained, so a chatty server cannot fill
|
|
68
|
+
memory through the channel that exists to explain its failures.
|
|
69
|
+
The tail never enters an ordinary error. Only an explicitly installed
|
|
70
|
+
[private diagnostic observer](../api.md#diagnostics) may receive it, wrapped in
|
|
71
|
+
`Redacted`. This per-event bound does not bound memory retained by a host's own
|
|
72
|
+
observer; that observer must also limit its history.
|
|
73
|
+
|
|
74
|
+
**The two deadlines** are separate because they answer different questions. A
|
|
75
|
+
server that never completes a handshake is broken and should fail fast; a tool
|
|
76
|
+
that takes a minute may be working.
|
|
77
|
+
|
|
78
|
+
## JSON depth and expansion
|
|
79
|
+
|
|
80
|
+
Bytes alone do not make recursive consumers safe. All incoming JSON-RPC messages
|
|
81
|
+
are checked iteratively before catalog/result validation, and may contain at
|
|
82
|
+
most `McpClient.maxJsonDepth` (128) nested object/array containers, counting the
|
|
83
|
+
wire envelope. This bound is fixed, not increased by raising `maxFrameBytes`.
|
|
84
|
+
Outgoing arguments have the same bound: their root object already counts as
|
|
85
|
+
container three, inside the envelope and `params`. A scalar adds no container.
|
|
86
|
+
An oversized depth or numeric overflow to infinity is a typed `protocol_error`,
|
|
87
|
+
not a stack-overflow defect.
|
|
88
|
+
|
|
89
|
+
Argument copying also stops when a lower bound on its expanded encoded size
|
|
90
|
+
exceeds `maxOutboundFrameBytes`. A compact JavaScript graph with shared children
|
|
91
|
+
can expand exponentially as JSON; each repeated occurrence consumes the budget.
|
|
92
|
+
Exact UTF-8 frame sizing, including escapes and protocol overhead, still runs
|
|
93
|
+
before writing. The argument root must be a JSON object. A rejected argument
|
|
94
|
+
does not send a request or close the session.
|
|
95
|
+
|
|
96
|
+
The host still owns memory it allocated before calling this library and any
|
|
97
|
+
executable Proxy traps it supplies. Incoming frames are parsed before the
|
|
98
|
+
iterative depth check, with parse allocation bounded by `maxFrameBytes`; this is
|
|
99
|
+
not a constant-memory streaming JSON parser.
|
|
100
|
+
|
|
101
|
+
## The rules a tool name must satisfy
|
|
102
|
+
|
|
103
|
+
Beyond the byte length, a tool name may not contain:
|
|
104
|
+
|
|
105
|
+
- `/`, because the name is embedded in `mcp/<server>/<tool>`.
|
|
106
|
+
- A control character (Unicode category Cc): C0 (U+0000 through U+001F),
|
|
107
|
+
U+007F, and C1 (U+0080 through U+009F).
|
|
108
|
+
- A format character (Unicode category Cf), which includes zero-width marks
|
|
109
|
+
(U+200B through U+200F, U+2060, U+FEFF), bidi overrides and isolates
|
|
110
|
+
(U+202A through U+202E, U+2066 through U+2069), and the soft hyphen.
|
|
111
|
+
- U+2028 or U+2029, the line and paragraph separators.
|
|
112
|
+
- A lone UTF-16 surrogate.
|
|
113
|
+
|
|
114
|
+
The name also may not be exactly `.` or `..`, which read as path segments.
|
|
115
|
+
|
|
116
|
+
A name that breaks any of these fails the connection with `invalid_response`.
|
|
117
|
+
The name reaches a model inside a flow name and the journal inside a declaration
|
|
118
|
+
digest. An invisible or reordering character would let one tool read as another
|
|
119
|
+
to the model or to a person reviewing an approval, so none of these is cosmetic.
|
|
120
|
+
|
|
121
|
+
Two tools with the same name in one catalog also fail: a duplicate would make
|
|
122
|
+
`mcp/<server>/<tool>` ambiguous.
|
|
123
|
+
|
|
124
|
+
## The rules a catalog entry must satisfy
|
|
125
|
+
|
|
126
|
+
- `tools` must be an array, and every element an object.
|
|
127
|
+
- `name` must be a non-empty string.
|
|
128
|
+
- `inputSchema` must be an object whose `type` is exactly `"object"`. A tool
|
|
129
|
+
with no usable parameter document is refused rather than disclosed to a model
|
|
130
|
+
as if it had one.
|
|
131
|
+
- `outputSchema`, when present, must be a JSON object.
|
|
132
|
+
- `nextCursor`, when present, must be a non-empty string, and must not repeat a
|
|
133
|
+
cursor already seen.
|
|
134
|
+
|
|
135
|
+
## Persisting a configuration
|
|
136
|
+
|
|
137
|
+
`McpClient.ConnectOptionsSchema` is the authoritative decoder for a stored
|
|
138
|
+
server entry. It requires a non-empty `server` and `command`, string `args`, a
|
|
139
|
+
string-valued `env` record, and positive integers for every limit:
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
import * as McpClient from "@smthrs/mcp/McpClient"
|
|
143
|
+
import { Schema } from "effect"
|
|
144
|
+
|
|
145
|
+
const decode = Schema.decodeUnknownSync(McpClient.ConnectOptionsSchema)
|
|
146
|
+
const options = decode(JSON.parse(fileContents))
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Use it wherever connection options arrive from a file or a database, so a bad
|
|
150
|
+
entry is a decode failure at the edge rather than a `protocol_error` at connect
|
|
151
|
+
time. See
|
|
152
|
+
[Configure servers for the CLI](./configure-servers-for-the-cli.md).
|
|
153
|
+
|
|
154
|
+
## Next
|
|
155
|
+
|
|
156
|
+
- [The life of a session](../concepts/the-session.md): what the limits are
|
|
157
|
+
protecting.
|
|
158
|
+
- [Troubleshooting](../troubleshooting.md): the message each limit produces.
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Configure servers for the CLI"
|
|
3
|
+
description: "Write the --mcp-config file the Smithers CLI reads, know which fields it validates, and understand when a bad entry fails."
|
|
4
|
+
sidebar:
|
|
5
|
+
order: 7
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
The CLI composes this package behind one flag. `smthrs --mcp-config <path>`
|
|
9
|
+
names a file of MCP servers that the local executor connects at startup and
|
|
10
|
+
projects into a run's flow catalog, so a flow can call them.
|
|
11
|
+
|
|
12
|
+
`SMITHERS_MCP_CONFIG` sets the same path. The flag wins when both are present,
|
|
13
|
+
and omitting both configures no MCP servers.
|
|
14
|
+
|
|
15
|
+
## Install the server
|
|
16
|
+
|
|
17
|
+
Install a reviewed server version and its dependencies before supplying any
|
|
18
|
+
credentials. This example pins the deprecated GitHub server to `2025.4.8`;
|
|
19
|
+
review it for your use before running it. Use a dedicated directory, review and
|
|
20
|
+
retain `package.json` and `package-lock.json`, then install from that lockfile:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
mkdir -p /path/to/mcp-servers
|
|
24
|
+
env -u GITHUB_TOKEN -u GITHUB_PERSONAL_ACCESS_TOKEN npm install --prefix /path/to/mcp-servers --package-lock-only --ignore-scripts --save-exact @modelcontextprotocol/server-github@2025.4.8
|
|
25
|
+
# Review the pinned package and lockfile before installing.
|
|
26
|
+
env -u GITHUB_TOKEN -u GITHUB_PERSONAL_ACCESS_TOKEN npm ci --prefix /path/to/mcp-servers --ignore-scripts
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Both npm commands remove the GitHub credential variables from their environment.
|
|
30
|
+
Launch the installed executable directly and supply the token only in the
|
|
31
|
+
server's `env`. This server reads `GITHUB_PERSONAL_ACCESS_TOKEN`.
|
|
32
|
+
|
|
33
|
+
## The file
|
|
34
|
+
|
|
35
|
+
A JSON array. Each entry is a stdio server (`command`) or a Streamable HTTP
|
|
36
|
+
server (`url`), never both, plus `McpFlows.ProjectionOptions`:
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
[
|
|
40
|
+
{
|
|
41
|
+
"server": "github",
|
|
42
|
+
"command": "/path/to/mcp-servers/node_modules/.bin/mcp-server-github",
|
|
43
|
+
"args": [],
|
|
44
|
+
"env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_..." },
|
|
45
|
+
"exclude": ["delete_repository"],
|
|
46
|
+
"namePrefix": "github"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"server": "reports",
|
|
50
|
+
"command": "node",
|
|
51
|
+
"args": ["./tools/reports-server.mjs"],
|
|
52
|
+
"cwd": "/srv/reports",
|
|
53
|
+
"requestTimeoutMs": 30000
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
By default, tools arrive as flows named `mcp/<server>/<tool>`. With the custom
|
|
59
|
+
`namePrefix` above, GitHub tools arrive as `github/<tool>`.
|
|
60
|
+
|
|
61
|
+
## Remote servers
|
|
62
|
+
|
|
63
|
+
A remote server takes a `url` instead of a `command`. Its bearer token is
|
|
64
|
+
never written in the file: `bearerTokenEnv` names the environment variable the
|
|
65
|
+
CLI reads it from.
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
[
|
|
69
|
+
{
|
|
70
|
+
"server": "tracker",
|
|
71
|
+
"url": "https://mcp.tracker.example/mcp",
|
|
72
|
+
"bearerTokenEnv": "TRACKER_MCP_TOKEN",
|
|
73
|
+
"namePrefix": "tracker"
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Export `TRACKER_MCP_TOKEN` from your secret store in the shell that runs
|
|
79
|
+
`smthrs`; never type the token on the command line.
|
|
80
|
+
|
|
81
|
+
Requests go through the host's egress client, so its proxy settings and
|
|
82
|
+
capability grants apply. A grant that denies `net:post` to the server's origin
|
|
83
|
+
stops the executor before the first request is sent.
|
|
84
|
+
|
|
85
|
+
## What the flag validates
|
|
86
|
+
|
|
87
|
+
The CLI decodes every stdio entry with `McpClient.ConnectOptionsSchema` and
|
|
88
|
+
every HTTP entry with `McpClient.HttpConnectOptionsSchema` before any layer is
|
|
89
|
+
built. A bad file is a usage error naming the flag and path.
|
|
90
|
+
|
|
91
|
+
A stdio entry:
|
|
92
|
+
|
|
93
|
+
| Field | Requirement |
|
|
94
|
+
| ------------------------------------------------------------------- | --------------------------------- |
|
|
95
|
+
| `server`, `command` | Required non-empty strings. |
|
|
96
|
+
| `args` | Required array of strings. |
|
|
97
|
+
| `cwd` | Optional non-empty string. |
|
|
98
|
+
| `env` | Optional object of string values. |
|
|
99
|
+
| `handshakeTimeoutMs`, `requestTimeoutMs` | Optional positive integers. |
|
|
100
|
+
| `queueCapacity`, `maxFrameBytes`, `maxOutboundFrameBytes` | Optional positive integers. |
|
|
101
|
+
| `maxStderrBytes`, `maxTools`, `maxToolNameBytes`, `maxCatalogPages` | Optional positive integers. |
|
|
102
|
+
| `maxToolDocumentBytes` | Optional positive integer. |
|
|
103
|
+
|
|
104
|
+
An HTTP entry:
|
|
105
|
+
|
|
106
|
+
| Field | Requirement |
|
|
107
|
+
| -------------------------------------------------------------------------- | ------------------------------------------------------------------- |
|
|
108
|
+
| `server` | Required non-empty string. |
|
|
109
|
+
| `url` | Required absolute `http:` or `https:` URL without user or password. |
|
|
110
|
+
| `bearerTokenEnv` | Optional environment variable name, set and non-empty at startup. |
|
|
111
|
+
| `handshakeTimeoutMs`, `requestTimeoutMs`, `maxFrameBytes` | Optional positive integers. |
|
|
112
|
+
| `maxOutboundFrameBytes`, `maxTools`, `maxToolNameBytes`, `maxCatalogPages` | Optional positive integers. |
|
|
113
|
+
| `maxToolDocumentBytes` | Optional positive integer. |
|
|
114
|
+
|
|
115
|
+
Four failures are usage errors that name the path: the file is missing, it
|
|
116
|
+
cannot be read, it is not valid JSON, or it is not an array of entries accepted
|
|
117
|
+
by the schemas. For example, `maxTools: 0` fails here before connecting a server.
|
|
118
|
+
An unset `bearerTokenEnv` variable is a usage error naming the server and the
|
|
119
|
+
variable.
|
|
120
|
+
|
|
121
|
+
## Select and name tools
|
|
122
|
+
|
|
123
|
+
The CLI preserves these projection fields and passes them to
|
|
124
|
+
`McpFlows.connected`:
|
|
125
|
+
|
|
126
|
+
| Field | Meaning |
|
|
127
|
+
| ------------ | ------------------------------------------------------------------------- |
|
|
128
|
+
| `include` | Optional array of exact tool names. Omitted or empty includes every tool. |
|
|
129
|
+
| `exclude` | Optional array of exact tool names to drop, applied after `include`. |
|
|
130
|
+
| `namePrefix` | Optional replacement for the default `mcp/<server>` flow-name prefix. |
|
|
131
|
+
|
|
132
|
+
The checked projection constructor rejects an empty `namePrefix` before
|
|
133
|
+
spawning and unknown `include` names after reading the catalog. These are
|
|
134
|
+
projection errors, separate from the connection schema's file validation.
|
|
135
|
+
|
|
136
|
+
## When a bad server fails
|
|
137
|
+
|
|
138
|
+
Every configured server is connected when the executor starts, because naming it
|
|
139
|
+
in the file is an operator opting into it. A server that fails to spawn, is
|
|
140
|
+
denied by the egress grants, or refuses the handshake takes the executor down
|
|
141
|
+
loudly rather than leaving a run silently short of the tools it was configured
|
|
142
|
+
to have.
|
|
143
|
+
|
|
144
|
+
That is the right trade for a configured dependency and the wrong one for an
|
|
145
|
+
experiment. Try a new server in a scratch config before adding it to the one a
|
|
146
|
+
production run uses.
|
|
147
|
+
|
|
148
|
+
## The flag does nothing under --remote
|
|
149
|
+
|
|
150
|
+
Under `--remote`, the executor is not this process's to configure, so
|
|
151
|
+
`--mcp-config` is meaningless. Configure MCP servers where the executor actually
|
|
152
|
+
runs.
|
|
153
|
+
|
|
154
|
+
## The other direction
|
|
155
|
+
|
|
156
|
+
This flag is Smithers calling somebody else's tools. The reverse, an agent such
|
|
157
|
+
as Claude Code driving Smithers, is the Smithers MCP server: see
|
|
158
|
+
[`smthrs mcp`](/cli/mcp) and
|
|
159
|
+
[Wire the MCP server into an agent](/pkg/cli/guides/wire-the-mcp-server). The two
|
|
160
|
+
share a name and nothing else.
|
|
161
|
+
|
|
162
|
+
## Next
|
|
163
|
+
|
|
164
|
+
- [Bound an untrusted server](./bound-an-untrusted-server.md): what the numeric
|
|
165
|
+
fields do.
|
|
166
|
+
- [Select the tools a run sees](./select-the-tools-a-run-sees.md): narrowing a
|
|
167
|
+
large catalog and choosing a projection constructor.
|