@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.
Files changed (128) hide show
  1. package/CHANGELOG.md +121 -0
  2. package/LICENSE +21 -0
  3. package/README.md +111 -2
  4. package/dist/cjs/Diagnostics.d.ts +46 -0
  5. package/dist/cjs/Diagnostics.d.ts.map +1 -0
  6. package/dist/cjs/Diagnostics.js +29 -0
  7. package/dist/cjs/Diagnostics.js.map +7 -0
  8. package/dist/cjs/McpClient.d.ts +304 -0
  9. package/dist/cjs/McpClient.d.ts.map +1 -0
  10. package/dist/cjs/McpClient.js +622 -0
  11. package/dist/cjs/McpClient.js.map +7 -0
  12. package/dist/cjs/McpError.d.ts +44 -0
  13. package/dist/cjs/McpError.d.ts.map +1 -0
  14. package/dist/cjs/McpError.js +41 -0
  15. package/dist/cjs/McpError.js.map +7 -0
  16. package/dist/cjs/McpFlows.d.ts +112 -0
  17. package/dist/cjs/McpFlows.d.ts.map +1 -0
  18. package/dist/cjs/McpFlows.js +127 -0
  19. package/dist/cjs/McpFlows.js.map +7 -0
  20. package/dist/cjs/index.d.ts +39 -0
  21. package/dist/cjs/index.d.ts.map +1 -0
  22. package/dist/cjs/index.js +41 -0
  23. package/dist/cjs/index.js.map +7 -0
  24. package/dist/cjs/internal/DiagnosticReporter.d.ts +17 -0
  25. package/dist/cjs/internal/DiagnosticReporter.d.ts.map +1 -0
  26. package/dist/cjs/internal/DiagnosticReporter.js +56 -0
  27. package/dist/cjs/internal/DiagnosticReporter.js.map +7 -0
  28. package/dist/cjs/internal/HttpTransport.d.ts +67 -0
  29. package/dist/cjs/internal/HttpTransport.d.ts.map +1 -0
  30. package/dist/cjs/internal/HttpTransport.js +298 -0
  31. package/dist/cjs/internal/HttpTransport.js.map +7 -0
  32. package/dist/cjs/internal/JsonLimits.d.ts +29 -0
  33. package/dist/cjs/internal/JsonLimits.d.ts.map +1 -0
  34. package/dist/cjs/internal/JsonLimits.js +51 -0
  35. package/dist/cjs/internal/JsonLimits.js.map +7 -0
  36. package/dist/cjs/internal/Limits.d.ts +36 -0
  37. package/dist/cjs/internal/Limits.d.ts.map +1 -0
  38. package/dist/cjs/internal/Limits.js +34 -0
  39. package/dist/cjs/internal/Limits.js.map +7 -0
  40. package/dist/cjs/internal/Rpc.d.ts +141 -0
  41. package/dist/cjs/internal/Rpc.d.ts.map +1 -0
  42. package/dist/cjs/internal/Rpc.js +92 -0
  43. package/dist/cjs/internal/Rpc.js.map +7 -0
  44. package/dist/cjs/internal/StdioTransport.d.ts +78 -0
  45. package/dist/cjs/internal/StdioTransport.d.ts.map +1 -0
  46. package/dist/cjs/internal/StdioTransport.js +310 -0
  47. package/dist/cjs/internal/StdioTransport.js.map +7 -0
  48. package/dist/cjs/internal/Transport.d.ts +87 -0
  49. package/dist/cjs/internal/Transport.d.ts.map +1 -0
  50. package/dist/cjs/internal/Transport.js +116 -0
  51. package/dist/cjs/internal/Transport.js.map +7 -0
  52. package/dist/cjs/package.json +1 -0
  53. package/dist/esm/Diagnostics.d.ts +46 -0
  54. package/dist/esm/Diagnostics.d.ts.map +1 -0
  55. package/dist/esm/Diagnostics.js +26 -0
  56. package/dist/esm/Diagnostics.js.map +1 -0
  57. package/dist/esm/McpClient.d.ts +304 -0
  58. package/dist/esm/McpClient.d.ts.map +1 -0
  59. package/dist/esm/McpClient.js +671 -0
  60. package/dist/esm/McpClient.js.map +1 -0
  61. package/dist/esm/McpError.d.ts +44 -0
  62. package/dist/esm/McpError.d.ts.map +1 -0
  63. package/dist/esm/McpError.js +43 -0
  64. package/dist/esm/McpError.js.map +1 -0
  65. package/dist/esm/McpFlows.d.ts +112 -0
  66. package/dist/esm/McpFlows.d.ts.map +1 -0
  67. package/dist/esm/McpFlows.js +167 -0
  68. package/dist/esm/McpFlows.js.map +1 -0
  69. package/dist/esm/index.d.ts +39 -0
  70. package/dist/esm/index.d.ts.map +1 -0
  71. package/dist/esm/index.js +39 -0
  72. package/dist/esm/index.js.map +1 -0
  73. package/dist/esm/internal/DiagnosticReporter.d.ts +17 -0
  74. package/dist/esm/internal/DiagnosticReporter.d.ts.map +1 -0
  75. package/dist/esm/internal/DiagnosticReporter.js +44 -0
  76. package/dist/esm/internal/DiagnosticReporter.js.map +1 -0
  77. package/dist/esm/internal/HttpTransport.d.ts +67 -0
  78. package/dist/esm/internal/HttpTransport.d.ts.map +1 -0
  79. package/dist/esm/internal/HttpTransport.js +266 -0
  80. package/dist/esm/internal/HttpTransport.js.map +1 -0
  81. package/dist/esm/internal/JsonLimits.d.ts +29 -0
  82. package/dist/esm/internal/JsonLimits.d.ts.map +1 -0
  83. package/dist/esm/internal/JsonLimits.js +55 -0
  84. package/dist/esm/internal/JsonLimits.js.map +1 -0
  85. package/dist/esm/internal/Limits.d.ts +36 -0
  86. package/dist/esm/internal/Limits.d.ts.map +1 -0
  87. package/dist/esm/internal/Limits.js +41 -0
  88. package/dist/esm/internal/Limits.js.map +1 -0
  89. package/dist/esm/internal/Rpc.d.ts +141 -0
  90. package/dist/esm/internal/Rpc.d.ts.map +1 -0
  91. package/dist/esm/internal/Rpc.js +129 -0
  92. package/dist/esm/internal/Rpc.js.map +1 -0
  93. package/dist/esm/internal/StdioTransport.d.ts +78 -0
  94. package/dist/esm/internal/StdioTransport.d.ts.map +1 -0
  95. package/dist/esm/internal/StdioTransport.js +332 -0
  96. package/dist/esm/internal/StdioTransport.js.map +1 -0
  97. package/dist/esm/internal/Transport.d.ts +87 -0
  98. package/dist/esm/internal/Transport.d.ts.map +1 -0
  99. package/dist/esm/internal/Transport.js +146 -0
  100. package/dist/esm/internal/Transport.js.map +1 -0
  101. package/docs/README.md +139 -0
  102. package/docs/api.md +469 -0
  103. package/docs/concepts/the-session.md +135 -0
  104. package/docs/concepts/tools-as-flows.md +116 -0
  105. package/docs/guides/bound-an-untrusted-server.md +158 -0
  106. package/docs/guides/configure-servers-for-the-cli.md +167 -0
  107. package/docs/guides/connect-a-server.md +161 -0
  108. package/docs/guides/grant-authority-to-mcp-tools.md +130 -0
  109. package/docs/guides/handle-a-failed-tool-call.md +125 -0
  110. package/docs/guides/select-the-tools-a-run-sees.md +92 -0
  111. package/docs/guides/testing.md +132 -0
  112. package/docs/guides/validate-structured-output.md +103 -0
  113. package/docs/installation.md +117 -0
  114. package/docs/quickstart.md +200 -0
  115. package/docs/troubleshooting.md +316 -0
  116. package/package.json +157 -3
  117. package/src/Diagnostics.ts +47 -0
  118. package/src/McpClient.ts +985 -0
  119. package/src/McpError.ts +52 -0
  120. package/src/McpFlows.ts +210 -0
  121. package/src/index.ts +42 -0
  122. package/src/internal/DiagnosticReporter.ts +47 -0
  123. package/src/internal/HttpTransport.ts +400 -0
  124. package/src/internal/JsonLimits.ts +53 -0
  125. package/src/internal/Limits.ts +48 -0
  126. package/src/internal/Rpc.ts +219 -0
  127. package/src/internal/StdioTransport.ts +491 -0
  128. 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.