@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,103 @@
1
+ ---
2
+ title: "Validate structured output"
3
+ description: "What the client checks when a tool declares an outputSchema and returns structuredContent: the five supported JSON Schema keywords, what is ignored, and how a violation is reported."
4
+ sidebar:
5
+ order: 5
6
+ ---
7
+
8
+ A tool may declare an `outputSchema` and answer with `structuredContent`. When
9
+ both are present, this client validates the value against that schema before
10
+ returning it, so a downstream consumer never sees structured output the server's
11
+ own contract rejects.
12
+
13
+ You do not switch this on. It applies whenever the catalog entry declared a
14
+ schema.
15
+
16
+ ## What is checked
17
+
18
+ Output schemas are compiled once at catalog load. Type unions use Effect
19
+ Schema, with compatibility checks for exactly five supported keywords:
20
+
21
+ | Keyword | Behavior |
22
+ | ------------ | ---------------------------------------------------------------------------------------------------------------------------- |
23
+ | `type` | All seven JSON Schema type names (`null`, `boolean`, `object`, `array`, `number`, `string`, `integer`), and arrays of names. |
24
+ | `required` | Each named string property must be an own property of the object. |
25
+ | `properties` | Each declared property present in the value is validated against its subschema. |
26
+ | `items` | A single schema applied to every element of an array. Tuple form is not supported. |
27
+ | `enum` | The value must equal one of the declared values, compared by JSON value rather than by reference. |
28
+
29
+ Validation recurses through `properties` and `items`, so a schema three levels
30
+ deep is checked three levels deep. Types, required names, child validators and
31
+ enum membership indexes are retained from the frozen catalog, including
32
+ composite JSON enum values. Compilation and result
33
+ validation yield between bounded work slices so host timers and Effect
34
+ interruption can run during large checks. The request timeout still covers the
35
+ transport request. An outer Effect timeout can also interrupt result validation.
36
+
37
+ ## What is ignored
38
+
39
+ Every other keyword. `minLength`, `pattern`, `additionalProperties`, `oneOf`,
40
+ `$ref`, and the rest are skipped. `integer` accepts every number for which
41
+ `Number.isInteger` is true, including values outside the safe-integer range.
42
+ This compatibility contract is narrower than a general JSON Schema importer.
43
+
44
+ This is a deliberate choice, not an oversight: a partial validator that rejected
45
+ data for a constraint it does not implement would be worse than one that admits
46
+ what it cannot judge. A tool whose contract depends on `pattern` should validate
47
+ its own output.
48
+
49
+ ## How a violation is reported
50
+
51
+ A violation fails with `invalid_response`, naming the expected constraint but
52
+ withholding the property path:
53
+
54
+ ```text
55
+ MCP server "reports" returned structuredContent that its own outputSchema
56
+ rejects: expected number; property path withheld
57
+ ```
58
+
59
+ Neither property names nor values are safe to echo into model-facing errors.
60
+ A trusted host may inspect the path and rejected response through the optional
61
+ [private diagnostic observer](../api.md#diagnostics). Its details are bounded
62
+ and wrapped in `Redacted`, not implicitly logged.
63
+
64
+ Validation never mutates or replaces the server's value. It either passes it
65
+ through unchanged or fails.
66
+
67
+ ## The shapes a result may take
68
+
69
+ Three combinations are all valid:
70
+
71
+ | Result | Outcome |
72
+ | ------------------------ | ------------------------------------------------------ |
73
+ | `content` only | Returned as sent, with `structuredContent: undefined`. |
74
+ | `structuredContent` only | Accepted, and returned with `content: []`. |
75
+ | Both | Both returned. |
76
+
77
+ A result carrying neither fails with `invalid_response`. A `content` value that
78
+ is not an array, a `content` element that is not an object, an `isError` that is
79
+ not a boolean, or a `structuredContent` that is not a JSON object all fail the
80
+ same way.
81
+
82
+ Without a declared `outputSchema`, any JSON-object `structuredContent` passes
83
+ through unchanged.
84
+
85
+ ## Reading it downstream
86
+
87
+ `McpFlows.Result` carries the field through the flow's output schema, so a cell
88
+ sees it directly:
89
+
90
+ ```js
91
+ const report = await ctx.call("mcp/reports/summary", { period: "2026-Q1" })
92
+ const rows = report.structuredContent?.rows ?? []
93
+ ```
94
+
95
+ `structuredContent` is optional on the flow output, so a tool that never sends
96
+ one produces a result with the field absent rather than null.
97
+
98
+ ## Next
99
+
100
+ - [Handle a failed tool call](./handle-a-failed-tool-call.md): the other
101
+ failures a call can produce.
102
+ - [Bound an untrusted server](./bound-an-untrusted-server.md): the limits that
103
+ apply before validation ever runs.
@@ -0,0 +1,117 @@
1
+ ---
2
+ title: "Installation"
3
+ description: "Install @smthrs/mcp, the platform services a stdio connection requires, the import forms, and the packages a host composition adds."
4
+ sidebar:
5
+ order: 1
6
+ ---
7
+
8
+ ## Availability
9
+
10
+ `@smthrs/mcp` is not published to npm yet. Its source is on
11
+ [GitHub](https://github.com/smithersai/smithers), and the `smthrs` command line
12
+ is the worked example of everything on this page: it composes this package
13
+ behind `--mcp-config`.
14
+
15
+ Not on npm yet; see [Installation](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/flow/docs/installation.md#use-the-libraries).
16
+
17
+ ## Requirements
18
+
19
+ - Node.js 26.4.0 or later.
20
+ - [`effect`](https://effect.website) 4.0.0-rc.115, a peer dependency and the
21
+ version this package is built against. Connection and tool-call operations
22
+ return an `Effect`, and every schema is an `effect/Schema`. `McpFlows.mcp`
23
+ is synchronous and returns a `FlowBinding.Source`; `Diagnostics.layer` is a
24
+ `Layer` constructor.
25
+
26
+ The package ships as both ESM and CommonJS with TypeScript declarations. The
27
+ `@smthrs/*` packages the adapter composes are ordinary dependencies and install
28
+ with it.
29
+
30
+ ## Provide a process spawner
31
+
32
+ Every constructor that opens a connection requires two services from the
33
+ caller's environment:
34
+
35
+ | Requirement | Why |
36
+ | --------------------- | --------------------------------------------------------------------------------------- |
37
+ | `ChildProcessSpawner` | An MCP server is a subprocess. The spawner is Effect's process port, not a Node import. |
38
+ | `Scope` | The connection is scoped. Closing the scope tears the server process down. |
39
+
40
+ On Node, `@effect/platform-node` provides the spawner:
41
+
42
+ ```bash
43
+ pnpm add @effect/platform-node@4.0.0-rc.115 @effect/platform-node-shared@4.0.0-rc.115
44
+ ```
45
+
46
+ ```ts
47
+ import { NodeServices } from "@effect/platform-node"
48
+ import { Effect } from "effect"
49
+
50
+ const runnable = Effect.provide(Effect.scoped(program), NodeServices.layer)
51
+ ```
52
+
53
+ These are the same services a host already provides for
54
+ `StandardFlows.shell`, so a composition that runs shell commands already has
55
+ them.
56
+
57
+ ## Import forms
58
+
59
+ The root entry point re-exports every module as a namespace:
60
+
61
+ ```ts
62
+ import { McpClient, McpError, McpFlows } from "@smthrs/mcp"
63
+ ```
64
+
65
+ Each module is also importable from its own subpath, which is the form the
66
+ [API reference](./api.md) uses in its examples:
67
+
68
+ ```ts
69
+ import * as McpClient from "@smthrs/mcp/McpClient"
70
+ import * as McpFlows from "@smthrs/mcp/McpFlows"
71
+ ```
72
+
73
+ `McpError` is a namespace either way. The error class is `McpError.McpError`:
74
+
75
+ ```ts
76
+ import * as McpError from "@smthrs/mcp/McpError"
77
+
78
+ const failure = new McpError.McpError({ code: "timeout", message: "...", server: "github" })
79
+ ```
80
+
81
+ Two subpath forms are not public: `@smthrs/mcp/internal/*` and
82
+ `@smthrs/mcp/*/index`. Both are blocked in the package's export map, so the
83
+ JSON-RPC codec and the stdio transport are not importable.
84
+ `@smthrs/mcp/package.json` is exported.
85
+
86
+ ## What a host composition adds
87
+
88
+ The projected `FlowBinding.Source` is only useful to something that composes a
89
+ flow catalog. [`@smthrs/harness`](/api/harness),
90
+ [`@smthrs/capability`](/api/capability), and [`@smthrs/core`](/api/core)
91
+ are runtime dependencies of this package and install with it:
92
+
93
+ - `@smthrs/harness` owns `FlowBinding`, the contract this package implements.
94
+ `FlowBinding.catalog` merges this source with the others a host offers. See
95
+ [Bind flows](/pkg/harness/guides/bind-flows).
96
+ - `@smthrs/capability` owns the action vocabulary `McpFlows.capabilities` is
97
+ derived from.
98
+
99
+ What a host adds is the thing that runs the cell loop:
100
+
101
+ The runtime `@smthrs/*` dependencies of this package are `@smthrs/canonical`,
102
+ `@smthrs/capability`, `@smthrs/core`, `@smthrs/harness`, `@smthrs/journal`,
103
+ and `@smthrs/kernel`. `@smthrs/registry` and `@smthrs/model` are development
104
+ dependencies only. A host that imports any of them directly declares it in its
105
+ own manifest.
106
+
107
+ [`@smthrs/agent`](/api/agent) takes the projected sources as its `flows` option,
108
+ alongside `StandardFlows.filesystem` and the rest.
109
+
110
+ The CLI already wires all of this. `smthrs --mcp-config <path>` connects every
111
+ server the file names and adds its flows to a run's catalog; see
112
+ [Configure servers for the CLI](./guides/configure-servers-for-the-cli.md).
113
+
114
+ ## Next step
115
+
116
+ Connect to a real server and call one of its tools in the
117
+ [Quickstart](./quickstart.md).
@@ -0,0 +1,200 @@
1
+ ---
2
+ title: "Quickstart"
3
+ description: "Connect to a real MCP server over stdio, call one of its tools, and read the flows the adapter projects from its catalog."
4
+ sidebar:
5
+ order: 2
6
+ ---
7
+
8
+ This quickstart connects to a real MCP server in its own process, calls two of
9
+ its tools, and prints the flows the adapter projects from its catalog. Nothing
10
+ is mocked: the server is a separate program speaking newline-delimited JSON-RPC,
11
+ which is the only thing MCP is.
12
+
13
+ By the end you will have seen both halves of this package. `McpClient` owns the
14
+ session, and `McpFlows` turns that session's catalog into flows a Smithers run
15
+ can call.
16
+
17
+ ## Prerequisites
18
+
19
+ Node.js 26.4.0 or later, and a project that depends on `@smthrs/mcp`,
20
+ `@effect/platform-node`, and `effect`. See
21
+ [Installation](./installation.md).
22
+
23
+ ## Write the server
24
+
25
+ Create `quickstart-server.mjs`. It imports nothing, which is the point: the
26
+ process on the other end of an MCP session need not be yours.
27
+
28
+ ```js
29
+ import { createInterface } from "node:readline"
30
+
31
+ const tools = [
32
+ {
33
+ name: "word_count",
34
+ description: "Counts the words in a piece of text",
35
+ inputSchema: {
36
+ type: "object",
37
+ properties: { text: { type: "string" } },
38
+ required: ["text"]
39
+ },
40
+ outputSchema: {
41
+ type: "object",
42
+ properties: { words: { type: "integer" } },
43
+ required: ["words"]
44
+ }
45
+ },
46
+ {
47
+ name: "explode",
48
+ description: "Always reports a tool-level failure",
49
+ inputSchema: { type: "object", properties: {} }
50
+ }
51
+ ]
52
+
53
+ const send = (message) => process.stdout.write(`${JSON.stringify(message)}\n`)
54
+
55
+ createInterface({ input: process.stdin }).on("line", (line) => {
56
+ const request = JSON.parse(line)
57
+ if (request.method === "initialize") {
58
+ send({
59
+ jsonrpc: "2.0",
60
+ id: request.id,
61
+ result: {
62
+ protocolVersion: "2025-06-18",
63
+ capabilities: { tools: {} },
64
+ serverInfo: { name: "quickstart", version: "1.0.0" }
65
+ }
66
+ })
67
+ return
68
+ }
69
+ if (request.method === "tools/list") {
70
+ send({ jsonrpc: "2.0", id: request.id, result: { tools } })
71
+ return
72
+ }
73
+ if (request.method === "tools/call") {
74
+ if (request.params.name === "explode") {
75
+ send({
76
+ jsonrpc: "2.0",
77
+ id: request.id,
78
+ result: { content: [{ type: "text", text: "the tool refused" }], isError: true }
79
+ })
80
+ return
81
+ }
82
+ const text = String(request.params.arguments.text ?? "")
83
+ const words = text.split(/\s+/u).filter((word) => word.length > 0).length
84
+ send({
85
+ jsonrpc: "2.0",
86
+ id: request.id,
87
+ result: {
88
+ content: [{ type: "text", text: String(words) }],
89
+ structuredContent: { words },
90
+ isError: false
91
+ }
92
+ })
93
+ }
94
+ })
95
+ ```
96
+
97
+ Two details matter later. The `initialize` result declares a `tools`
98
+ capability, which this client requires: a server that serves no tools is
99
+ refused during the handshake. And `explode` answers a successful JSON-RPC reply
100
+ carrying `isError: true`, which is MCP's way of saying "the tool ran and
101
+ reported a problem", not "the call failed".
102
+
103
+ ## Connect and call
104
+
105
+ Create `quickstart.ts`. `McpClient.connect` spawns the server, completes the
106
+ handshake, and fetches the tool catalog once, up front:
107
+
108
+ ```ts
109
+ import { NodeServices } from "@effect/platform-node"
110
+ import * as McpClient from "@smthrs/mcp/McpClient"
111
+ import * as McpFlows from "@smthrs/mcp/McpFlows"
112
+ import { Effect } from "effect"
113
+
114
+ const program = Effect.scoped(Effect.gen(function*() {
115
+ const client = yield* McpClient.connect({
116
+ server: "quickstart",
117
+ command: process.execPath,
118
+ args: ["quickstart-server.mjs"]
119
+ })
120
+
121
+ const counted = yield* client.callTool("word_count", { text: "durable flows release notes" })
122
+ const refused = yield* client.callTool("explode", {})
123
+
124
+ const source = McpFlows.mcp(client)
125
+ const bindings = yield* source.bindings()
126
+
127
+ return {
128
+ tools: client.tools.map((tool) => tool.name),
129
+ counted,
130
+ refused,
131
+ sourceName: source.name,
132
+ flowNames: bindings.map((binding) => binding.descriptor.name)
133
+ }
134
+ }))
135
+
136
+ console.log(await Effect.runPromise(Effect.provide(program, NodeServices.layer)))
137
+ ```
138
+
139
+ `Effect.scoped` is what owns the subprocess. When the scope closes, the server
140
+ process is torn down and every request still outstanding fails with
141
+ `connection_closed` rather than hanging.
142
+
143
+ ## Run it
144
+
145
+ Run the file with a TypeScript runner:
146
+
147
+ ```bash
148
+ npx tsx quickstart.ts
149
+ ```
150
+
151
+ ```text
152
+ {
153
+ tools: [ 'word_count', 'explode' ],
154
+ counted: {
155
+ content: [ { type: 'text', text: '4' } ],
156
+ isError: false,
157
+ structuredContent: { words: 4 }
158
+ },
159
+ refused: {
160
+ content: [ { type: 'text', text: 'the tool refused' } ],
161
+ isError: true,
162
+ structuredContent: undefined
163
+ },
164
+ sourceName: 'mcp/quickstart',
165
+ flowNames: [ 'mcp/quickstart/word_count', 'mcp/quickstart/explode' ]
166
+ }
167
+ ```
168
+
169
+ ## What just happened
170
+
171
+ `connect` spawned the process, negotiated `2025-06-18`, sent
172
+ `notifications/initialized`, and walked `tools/list` to the end of its cursor
173
+ before returning. `client.tools` is that catalog, and it is a snapshot: this
174
+ client never re-polls it.
175
+
176
+ `callTool("explode", {})` **succeeded**. Its `isError: true` came back in the
177
+ success channel, because a tool that runs and reports a problem is data. Only a
178
+ failure of the session itself, a server that will not start, a closed pipe, a
179
+ reply this client cannot parse, becomes an `McpError`. See
180
+ [Handle a failed tool call](./guides/handle-a-failed-tool-call.md).
181
+
182
+ `word_count` declared an `outputSchema`, so its `structuredContent` was checked
183
+ against that schema before it was returned. `explode` declared none, so nothing
184
+ was checked. See
185
+ [Validate structured output](./guides/validate-structured-output.md).
186
+
187
+ `McpFlows.mcp(client)` produced a `FlowBinding.Source` named `mcp/quickstart`
188
+ holding one binding per tool. Each binding's descriptor carries the server's own
189
+ `inputSchema` as its input document, so a model reading the catalog sees
190
+ `{ type: "object", properties: { text: { type: "string" } }, required: ["text"] }`
191
+ rather than this package's permissive record type.
192
+
193
+ ## Next steps
194
+
195
+ - [A remote tool as a flow](./concepts/tools-as-flows.md): what the projection
196
+ produces, and why nothing downstream knows the flow is remote.
197
+ - [Grant authority to MCP tools](./guides/grant-authority-to-mcp-tools.md): the
198
+ one step between a projected source and a cell that can actually call it.
199
+ - [Select the tools a run sees](./guides/select-the-tools-a-run-sees.md): keep a
200
+ fifty-tool server out of the model's context window.