@smthrs/mcp 0.0.0-stage → 1.0.0-rc.4

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 +113 -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 +113 -0
  66. package/dist/esm/McpFlows.d.ts.map +1 -0
  67. package/dist/esm/McpFlows.js +168 -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 +211 -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,671 @@
1
+ /**
2
+ * A minimal MCP client covering the `initialize` handshake, `tools/list`, and
3
+ * `tools/call`, over stdio or Streamable HTTP.
4
+ *
5
+ * This is deliberately not a general MCP SDK. Smithers has exactly one
6
+ * consumer of an MCP session: {@link McpFlows}, which needs a tool catalog
7
+ * and a way to invoke one entry from it, so the client exposes only that.
8
+ * Resources, prompts, sampling, and roots are not wired up; add them here
9
+ * when a flow adapter needs them, not speculatively.
10
+ *
11
+ * @since 1.0.0-rc.0
12
+ */
13
+ import { isRecord } from "@smthrs/canonical/Record";
14
+ import { Effect, Exit, Result, Schema, Scope } from "effect";
15
+ import * as DiagnosticReporter from "./internal/DiagnosticReporter.js";
16
+ import * as HttpTransport from "./internal/HttpTransport.js";
17
+ import * as JsonLimits from "./internal/JsonLimits.js";
18
+ import * as Limits from "./internal/Limits.js";
19
+ import * as StdioTransport from "./internal/StdioTransport.js";
20
+ import * as Transport from "./internal/Transport.js";
21
+ import { McpError } from "./McpError.js";
22
+ const isHttp = (options) => options.url !== undefined;
23
+ const PositiveInteger = Schema.Int.check(Schema.isGreaterThan(0));
24
+ /**
25
+ * Authoritative decoder for a persisted stdio MCP server entry.
26
+ *
27
+ * The schema requires non-empty server and command names, string arguments,
28
+ * a plain string-valued environment record, and positive-integer limits.
29
+ *
30
+ * @category schemas
31
+ * @since 1.0.0-rc.0
32
+ */
33
+ export const ConnectOptionsSchema = Schema.Struct({
34
+ server: Schema.NonEmptyString,
35
+ command: Schema.NonEmptyString,
36
+ args: Schema.Array(Schema.String),
37
+ cwd: Schema.optional(Schema.NonEmptyString),
38
+ env: Schema.optional(Schema.Record(Schema.String, Schema.String)),
39
+ handshakeTimeoutMs: Schema.optional(PositiveInteger),
40
+ requestTimeoutMs: Schema.optional(PositiveInteger),
41
+ queueCapacity: Schema.optional(PositiveInteger),
42
+ maxFrameBytes: Schema.optional(PositiveInteger),
43
+ maxOutboundFrameBytes: Schema.optional(PositiveInteger),
44
+ maxStderrBytes: Schema.optional(PositiveInteger),
45
+ maxTools: Schema.optional(PositiveInteger),
46
+ maxToolNameBytes: Schema.optional(PositiveInteger),
47
+ maxToolDocumentBytes: Schema.optional(PositiveInteger),
48
+ maxCatalogPages: Schema.optional(PositiveInteger)
49
+ });
50
+ /** An absolute `http:` or `https:` URL without credentials: userinfo would put a secret in the file. */
51
+ const isEndpoint = (url) => {
52
+ if (!URL.canParse(url))
53
+ return false;
54
+ const parsed = new URL(url);
55
+ return (parsed.protocol === "http:" || parsed.protocol === "https:") && parsed.username === "" &&
56
+ parsed.password === "";
57
+ };
58
+ /**
59
+ * Authoritative decoder for a persisted Streamable HTTP MCP server entry.
60
+ *
61
+ * The schema requires a non-empty server name, an absolute `http:` or
62
+ * `https:` `url` without userinfo, and positive-integer limits. A bearer
63
+ * credential is never stored in the entry: `bearerTokenEnv` names the
64
+ * environment variable the host reads it from and turns into an
65
+ * {@link AuthProvider}.
66
+ *
67
+ * @category schemas
68
+ * @since 1.0.0-rc.1
69
+ */
70
+ export const HttpConnectOptionsSchema = Schema.Struct({
71
+ server: Schema.NonEmptyString,
72
+ url: Schema.String.check(Schema.makeFilter(isEndpoint, { expected: "an http: or https: URL without credentials" })),
73
+ bearerTokenEnv: Schema.optionalKey(Schema.String.check(Schema.isPattern(/^[A-Za-z_][A-Za-z0-9_]*$/))),
74
+ handshakeTimeoutMs: Schema.optional(PositiveInteger),
75
+ requestTimeoutMs: Schema.optional(PositiveInteger),
76
+ maxFrameBytes: Schema.optional(PositiveInteger),
77
+ maxOutboundFrameBytes: Schema.optional(PositiveInteger),
78
+ maxTools: Schema.optional(PositiveInteger),
79
+ maxToolNameBytes: Schema.optional(PositiveInteger),
80
+ maxToolDocumentBytes: Schema.optional(PositiveInteger),
81
+ maxCatalogPages: Schema.optional(PositiveInteger)
82
+ });
83
+ /**
84
+ * Frozen identity disclosed to every MCP server during initialization.
85
+ *
86
+ * @category constants
87
+ * @since 1.0.0-rc.0
88
+ */
89
+ export const clientInfo = Object.freeze({
90
+ name: "smithers",
91
+ version: "1.0.0-rc.4"
92
+ });
93
+ /**
94
+ * MCP revisions whose `tools/list` and `tools/call` shapes this client
95
+ * decodes. The frozen list always proposes `2025-06-18` first.
96
+ *
97
+ * @category constants
98
+ * @since 1.0.0-rc.0
99
+ */
100
+ export const supportedProtocolVersions = Object.freeze([
101
+ "2025-06-18",
102
+ "2025-03-26",
103
+ "2024-11-05"
104
+ ]);
105
+ /**
106
+ * Default deadline for each MCP handshake request.
107
+ *
108
+ * @category constants
109
+ * @since 1.0.0-rc.0
110
+ */
111
+ export const defaultHandshakeTimeoutMs = 10_000;
112
+ /**
113
+ * Default deadline for each tool request.
114
+ *
115
+ * @category constants
116
+ * @since 1.0.0-rc.0
117
+ */
118
+ export const defaultRequestTimeoutMs = Transport.defaultRequestTimeoutMs;
119
+ /**
120
+ * Default number of outbound frames allowed to wait in memory.
121
+ *
122
+ * @category constants
123
+ * @since 1.0.0-rc.0
124
+ */
125
+ export const defaultQueueCapacity = StdioTransport.defaultQueueCapacity;
126
+ /**
127
+ * Default maximum inbound JSON-RPC frame size.
128
+ *
129
+ * @category constants
130
+ * @since 1.0.0-rc.0
131
+ */
132
+ export const defaultMaxFrameBytes = Transport.defaultMaxFrameBytes;
133
+ /**
134
+ * Default maximum outbound JSON-RPC frame size.
135
+ *
136
+ * @category constants
137
+ * @since 1.0.0-rc.0
138
+ */
139
+ export const defaultMaxOutboundFrameBytes = Transport.defaultMaxOutboundFrameBytes;
140
+ /**
141
+ * Default maximum child-stderr tail retained for connection diagnostics.
142
+ *
143
+ * @category constants
144
+ * @since 1.0.0-rc.0
145
+ */
146
+ export const defaultMaxStderrBytes = StdioTransport.defaultMaxStderrBytes;
147
+ /**
148
+ * Default maximum number of tools in a remote catalog.
149
+ *
150
+ * @category constants
151
+ * @since 1.0.0-rc.0
152
+ */
153
+ export const defaultMaxTools = 256;
154
+ /**
155
+ * Default maximum UTF-8 byte length of one remote tool name.
156
+ *
157
+ * @category constants
158
+ * @since 1.0.0-rc.0
159
+ */
160
+ export const defaultMaxToolNameBytes = 128;
161
+ /**
162
+ * Default maximum UTF-8 bytes of one tool's description plus its JSON-encoded
163
+ * `inputSchema`, the server-authored text a model reads for that tool.
164
+ *
165
+ * @category constants
166
+ * @since 1.0.0-rc.1
167
+ */
168
+ export const defaultMaxToolDocumentBytes = 65_536;
169
+ /**
170
+ * Default maximum number of remote catalog pages.
171
+ *
172
+ * @category constants
173
+ * @since 1.0.0-rc.0
174
+ */
175
+ export const defaultMaxCatalogPages = 32;
176
+ /**
177
+ * Maximum nested JSON containers, including the JSON-RPC envelope. Fixed so
178
+ * accepted server schemas and values remain safe for recursive consumers.
179
+ *
180
+ * @category constants
181
+ * @since 1.0.0-rc.0
182
+ */
183
+ export const maxJsonDepth = JsonLimits.maxDepth;
184
+ const invalidResponse = (server, message) => new McpError({ code: "invalid_response", message, server });
185
+ const asInitialize = (server, result) => {
186
+ if (!isRecord(result)) {
187
+ return Result.fail(Limits.protocolError(server, `MCP server "${server}" returned a malformed initialize result: result is not an object`));
188
+ }
189
+ if (typeof result.protocolVersion !== "string") {
190
+ return Result.fail(Limits.protocolError(server, `MCP server "${server}" returned a malformed initialize result: protocolVersion is not a string`));
191
+ }
192
+ if (!supportedProtocolVersions.includes(result.protocolVersion)) {
193
+ return Result.fail(Limits.protocolError(server, `MCP server "${server}" speaks an unsupported protocol version; this client speaks ${supportedProtocolVersions.join(", ")}`));
194
+ }
195
+ if (!isRecord(result.capabilities)) {
196
+ return Result.fail(Limits.protocolError(server, `MCP server "${server}" returned a malformed initialize result: capabilities is not an object`));
197
+ }
198
+ if (!Object.hasOwn(result.capabilities, "tools") || !isRecord(result.capabilities.tools)) {
199
+ return Result.fail(Limits.protocolError(server, `MCP server "${server}" does not serve tools: its initialize result declares no tools capability`));
200
+ }
201
+ return Result.succeed(undefined);
202
+ };
203
+ const nameEncoder = new TextEncoder();
204
+ // Cc covers C0, DEL, and C1. Cf covers bidi overrides and isolates (U+202A-202E,
205
+ // U+2066-2069), zero-width marks (U+200B-200F, U+2060, U+FEFF), and soft hyphen:
206
+ // all invisible text that makes `mcp/<server>/<tool>` read as another tool.
207
+ // Cs catches a lone surrogate, which UTF-8 encoding would silently replace.
208
+ const forbiddenToolNameCharacter = /[/\p{Cc}\p{Cf}\p{Cs}\u2028\u2029]/u;
209
+ const isForbiddenToolName = (name) => name === "." || name === ".." || forbiddenToolNameCharacter.test(name);
210
+ const asToolPage = (server, result, limits, seen, described) => {
211
+ const tools = isRecord(result) ? result.tools : undefined;
212
+ if (!Array.isArray(tools)) {
213
+ return Result.fail(invalidResponse(server, `MCP server "${server}" returned a tools/list result with no tools array`));
214
+ }
215
+ for (const [index, tool] of tools.entries()) {
216
+ if (described.length >= limits.maxTools) {
217
+ return Result.fail(invalidResponse(server, `MCP server "${server}" returned more than ${limits.maxTools} tools`));
218
+ }
219
+ if (!isRecord(tool)) {
220
+ return Result.fail(invalidResponse(server, `MCP server "${server}" returned tools[${index}], which is not an object`));
221
+ }
222
+ const record = tool;
223
+ if (typeof record.name !== "string" || record.name === "") {
224
+ return Result.fail(invalidResponse(server, `MCP server "${server}" returned tools[${index}] with no name`));
225
+ }
226
+ if (nameEncoder.encode(record.name).byteLength > limits.maxToolNameBytes) {
227
+ return Result.fail(invalidResponse(server, `MCP server "${server}" returned a tool name longer than ${limits.maxToolNameBytes} bytes`));
228
+ }
229
+ if (isForbiddenToolName(record.name)) {
230
+ return Result.fail(invalidResponse(server, `MCP server "${server}" returned a tool name that is "." or "..", or contains "/" or an invisible or control character`));
231
+ }
232
+ if (seen.has(record.name)) {
233
+ return Result.fail(invalidResponse(server, `MCP server "${server}" returned a duplicate tool name at catalog index ${index}`));
234
+ }
235
+ if (!isRecord(record.inputSchema) || record.inputSchema.type !== "object") {
236
+ return Result.fail(invalidResponse(server, `MCP server "${server}" returned a tool whose inputSchema is not a JSON Schema object of type "object"`));
237
+ }
238
+ const description = typeof record.description === "string" ? record.description : undefined;
239
+ const documentBytes = nameEncoder.encode(description ?? "").byteLength +
240
+ nameEncoder.encode(JSON.stringify(record.inputSchema)).byteLength;
241
+ if (documentBytes > limits.maxToolDocumentBytes) {
242
+ return Result.fail(invalidResponse(server, `MCP server "${server}" returned a tool description and inputSchema longer than ${limits.maxToolDocumentBytes} bytes`));
243
+ }
244
+ let outputSchema;
245
+ if (Object.hasOwn(record, "outputSchema")) {
246
+ if (!isRecord(record.outputSchema)) {
247
+ return Result.fail(invalidResponse(server, `MCP server "${server}" returned a tool whose outputSchema is not a JSON object`));
248
+ }
249
+ outputSchema = record.outputSchema;
250
+ }
251
+ seen.add(record.name);
252
+ described.push({
253
+ name: record.name,
254
+ description,
255
+ inputSchema: record.inputSchema,
256
+ outputSchema
257
+ });
258
+ }
259
+ const nextCursor = isRecord(result) && Object.hasOwn(result, "nextCursor") ? result.nextCursor : undefined;
260
+ if (nextCursor === undefined)
261
+ return Result.succeed({ nextCursor: undefined });
262
+ if (typeof nextCursor !== "string" || nextCursor === "") {
263
+ return Result.fail(invalidResponse(server, `MCP server "${server}" returned a tools/list cursor that is not a non-empty string`));
264
+ }
265
+ return Result.succeed({ nextCursor });
266
+ };
267
+ // Container guards deliberately inspect only the shape; traversal below owns
268
+ // cooperative interruption. Schema.Int excludes unsafe integers, unlike JSON Schema.
269
+ const outputTypes = {
270
+ null: Schema.Null,
271
+ boolean: Schema.Boolean,
272
+ object: Schema.declare(isRecord),
273
+ array: Schema.declare(Array.isArray),
274
+ number: Schema.Number,
275
+ string: Schema.String,
276
+ integer: Schema.Number.check(Schema.makeFilter(Number.isInteger))
277
+ };
278
+ // Every traversal step consumes a slice slot, including enum-key construction.
279
+ // Yielding keeps validation interruptible after the transport has completed.
280
+ const runJsonWork = (work) => Effect.gen(function* () {
281
+ while (true) {
282
+ for (let steps = 0; steps < 1_024; steps += 1) {
283
+ const next = work.next();
284
+ if (next.done)
285
+ return next.value;
286
+ }
287
+ yield* Effect.yieldNow;
288
+ }
289
+ });
290
+ // Keys are canonical JSON: object order is irrelevant, array order is not.
291
+ // Inputs here are depth-checked parsed JSON, never caller-owned arguments.
292
+ const enumKey = function* (value) {
293
+ yield;
294
+ if (Array.isArray(value)) {
295
+ const items = [];
296
+ for (const item of value)
297
+ items.push(yield* enumKey(item));
298
+ return `[${items.join(",")}]`;
299
+ }
300
+ if (isRecord(value)) {
301
+ const members = [];
302
+ for (const key of Object.keys(value).sort()) {
303
+ members.push(`${JSON.stringify(key)}:${yield* enumKey(value[key])}`);
304
+ }
305
+ return `{${members.join(",")}}`;
306
+ }
307
+ return JSON.stringify(value);
308
+ };
309
+ /**
310
+ * Compile the supported output-schema subset once per catalog. Effect Schema
311
+ * owns type unions; this cooperative compatibility traversal retains composite
312
+ * enums, own-property requirements and ignored unsupported keywords. Importing
313
+ * the whole document would enforce constraints this client does not support.
314
+ */
315
+ const compileOutputSchema = function* (schema) {
316
+ yield;
317
+ const types = new Set();
318
+ for (const candidate of Array.isArray(schema.type) ? schema.type : [schema.type]) {
319
+ yield;
320
+ if (typeof candidate === "string" && Object.hasOwn(outputTypes, candidate)) {
321
+ types.add(candidate);
322
+ }
323
+ }
324
+ const accepts = Schema.is(types.size === 0 ? Schema.Unknown : Schema.Union([...types].map((type) => outputTypes[type])));
325
+ const typeReason = `expected ${[...types].join(" or ")}`;
326
+ let enumIndex;
327
+ if (Array.isArray(schema.enum)) {
328
+ enumIndex = new Set();
329
+ for (const member of schema.enum)
330
+ enumIndex.add(yield* enumKey(member));
331
+ }
332
+ const required = [];
333
+ if (Array.isArray(schema.required)) {
334
+ for (const key of schema.required) {
335
+ yield;
336
+ if (typeof key === "string")
337
+ required.push(key);
338
+ }
339
+ }
340
+ const properties = [];
341
+ if (isRecord(schema.properties)) {
342
+ for (const [key, property] of Object.entries(schema.properties)) {
343
+ yield;
344
+ if (isRecord(property))
345
+ properties.push([key, yield* compileOutputSchema(property)]);
346
+ }
347
+ }
348
+ const items = isRecord(schema.items) ? yield* compileOutputSchema(schema.items) : undefined;
349
+ return function* (value, path) {
350
+ yield;
351
+ if (enumIndex !== undefined && !enumIndex.has(yield* enumKey(value))) {
352
+ return { path, reason: "expected a declared enum value" };
353
+ }
354
+ if (!accepts(value))
355
+ return { path, reason: typeReason };
356
+ if (isRecord(value)) {
357
+ for (const key of required) {
358
+ yield;
359
+ if (!Object.hasOwn(value, key))
360
+ return { path: `${path}.${key}`, reason: "required property is missing" };
361
+ }
362
+ for (const [key, validate] of properties) {
363
+ yield;
364
+ if (!Object.hasOwn(value, key))
365
+ continue;
366
+ const issue = yield* validate(value[key], `${path}.${key}`);
367
+ if (issue !== undefined)
368
+ return issue;
369
+ }
370
+ }
371
+ if (Array.isArray(value) && items !== undefined) {
372
+ for (const [index, item] of value.entries()) {
373
+ const issue = yield* items(item, `${path}[${index}]`);
374
+ if (issue !== undefined)
375
+ return issue;
376
+ }
377
+ }
378
+ return undefined;
379
+ };
380
+ };
381
+ const asToolResult = function* (server, result, outputSchema, diagnostic, validate) {
382
+ if (!isRecord(result)) {
383
+ return Result.fail(invalidResponse(server, `MCP server "${server}" returned a tools/call result that is not an object`));
384
+ }
385
+ const hasContent = Object.hasOwn(result, "content");
386
+ const hasStructuredContent = Object.hasOwn(result, "structuredContent");
387
+ if (!hasContent && !hasStructuredContent) {
388
+ return Result.fail(invalidResponse(server, `MCP server "${server}" returned a tools/call result with no content array`));
389
+ }
390
+ if (hasContent && !Array.isArray(result.content)) {
391
+ return Result.fail(invalidResponse(server, `MCP server "${server}" returned a tools/call result with no content array`));
392
+ }
393
+ const content = [];
394
+ const blocks = hasContent ? result.content : [];
395
+ for (const [index, block] of blocks.entries()) {
396
+ yield;
397
+ if (!isRecord(block)) {
398
+ return Result.fail(invalidResponse(server, `MCP server "${server}" returned a tools/call result whose content[${index}] is not an object`));
399
+ }
400
+ content.push(block);
401
+ }
402
+ if (Object.hasOwn(result, "isError") && typeof result.isError !== "boolean") {
403
+ return Result.fail(invalidResponse(server, `MCP server "${server}" returned a tools/call result whose isError is not a boolean`));
404
+ }
405
+ let structuredContent;
406
+ if (hasStructuredContent) {
407
+ if (!isRecord(result.structuredContent)) {
408
+ return Result.fail(invalidResponse(server, `MCP server "${server}" returned a tools/call result whose structuredContent is not a JSON object`));
409
+ }
410
+ structuredContent = result.structuredContent;
411
+ if (validate !== undefined) {
412
+ const issue = yield* validate(structuredContent, "structuredContent");
413
+ if (issue !== undefined) {
414
+ diagnostic("invalid-response", { issue, outputSchema });
415
+ return Result.fail(invalidResponse(server, `MCP server "${server}" returned structuredContent that its own outputSchema rejects: ${issue.reason}; property path withheld`));
416
+ }
417
+ }
418
+ }
419
+ return Result.succeed({
420
+ content,
421
+ isError: result.isError === true,
422
+ structuredContent
423
+ });
424
+ };
425
+ const renderJsonPath = (path) => typeof path === "string" ?
426
+ path :
427
+ `${renderJsonPath(path.parent)}${typeof path.key === "number" ? `[${path.key}]` : `.${path.key}`}`;
428
+ const jsonFailure = (path, reason) => Result.fail({ path: renderJsonPath(path), reason });
429
+ const reflect = (thunk) => {
430
+ try {
431
+ return Result.succeed(thunk());
432
+ }
433
+ catch {
434
+ return Result.fail("a property that threw when read");
435
+ }
436
+ };
437
+ const ownDescriptors = (object) => reflect(() => Object.getOwnPropertyDescriptors(object));
438
+ const isAccessor = (descriptor) => Object.hasOwn(descriptor, "get") || Object.hasOwn(descriptor, "set");
439
+ // Lower bounds on encoded size avoid expanding repeated references into an
440
+ // enormous tree before the transport's exact UTF-8 frame check. String/key
441
+ // UTF-16 length is a lower bound even with escapes and surrogate pairs.
442
+ const spendJson = (budget, bytes) => {
443
+ budget.remaining -= bytes;
444
+ return budget.remaining >= 0;
445
+ };
446
+ const snapshotJson = (value, path, ancestors, budget) => {
447
+ const bytes = typeof value === "string" ?
448
+ value.length + 2
449
+ : typeof value === "number" || typeof value === "boolean" ?
450
+ String(value).length
451
+ : value === null ?
452
+ 4
453
+ : 2;
454
+ if (!spendJson(budget, bytes))
455
+ return jsonFailure(path, "JSON expansion exceeds the outbound frame budget");
456
+ if (value === null)
457
+ return Result.succeed(null);
458
+ if (typeof value === "boolean" || typeof value === "string")
459
+ return Result.succeed(value);
460
+ if (typeof value === "number") {
461
+ return Number.isFinite(value) ? Result.succeed(value) : jsonFailure(path, "a non-finite number");
462
+ }
463
+ if (typeof value === "undefined")
464
+ return jsonFailure(path, "undefined");
465
+ if (typeof value === "bigint")
466
+ return jsonFailure(path, "a bigint");
467
+ if (typeof value === "function")
468
+ return jsonFailure(path, "a function");
469
+ if (typeof value === "symbol")
470
+ return jsonFailure(path, "a symbol");
471
+ if (ancestors.has(value))
472
+ return jsonFailure(path, "a cyclic reference");
473
+ // Arguments live inside the wire envelope and its params object.
474
+ if (ancestors.size + 3 > maxJsonDepth)
475
+ return jsonFailure(path, `JSON nesting exceeds ${maxJsonDepth} containers`);
476
+ const array = reflect(() => Array.isArray(value));
477
+ if (Result.isFailure(array))
478
+ return jsonFailure(path, array.failure);
479
+ if (array.success) {
480
+ const length = reflect(() => value.length);
481
+ if (Result.isFailure(length))
482
+ return jsonFailure(path, length.failure);
483
+ if (!Number.isSafeInteger(length.success) || length.success < 0) {
484
+ return jsonFailure(path, "a property that threw when read");
485
+ }
486
+ if (!spendJson(budget, Math.max(0, length.success - 1))) {
487
+ return jsonFailure(path, "JSON expansion exceeds the outbound frame budget");
488
+ }
489
+ const descriptors = ownDescriptors(value);
490
+ if (Result.isFailure(descriptors))
491
+ return jsonFailure(path, descriptors.failure);
492
+ ancestors.add(value);
493
+ const copied = [];
494
+ const memberPath = { parent: path, key: 0 };
495
+ for (let index = 0; index < length.success; index += 1) {
496
+ memberPath.key = index;
497
+ // Missing slots must not resolve through Object.prototype.
498
+ const descriptor = Object.hasOwn(descriptors.success, index) ? descriptors.success[index] : undefined;
499
+ if (descriptor !== undefined && isAccessor(descriptor)) {
500
+ ancestors.delete(value);
501
+ return jsonFailure(memberPath, "an accessor property");
502
+ }
503
+ const member = descriptor === undefined ? undefined : descriptor.value;
504
+ const snapshot = snapshotJson(member, memberPath, ancestors, budget);
505
+ if (Result.isFailure(snapshot)) {
506
+ ancestors.delete(value);
507
+ return snapshot;
508
+ }
509
+ copied.push(snapshot.success);
510
+ }
511
+ ancestors.delete(value);
512
+ return Result.succeed(copied);
513
+ }
514
+ const object = value;
515
+ const prototype = reflect(() => Object.getPrototypeOf(object));
516
+ if (Result.isFailure(prototype))
517
+ return jsonFailure(path, prototype.failure);
518
+ if (prototype.success !== Object.prototype && prototype.success !== null) {
519
+ return jsonFailure(path, "an object with a non-plain prototype");
520
+ }
521
+ const symbols = reflect(() => Object.getOwnPropertySymbols(object));
522
+ if (Result.isFailure(symbols))
523
+ return jsonFailure(path, symbols.failure);
524
+ const descriptors = ownDescriptors(object);
525
+ if (Result.isFailure(descriptors))
526
+ return jsonFailure(path, descriptors.failure);
527
+ for (const key of symbols.success) {
528
+ if (Object.hasOwn(descriptors.success, key) && descriptors.success[key].enumerable) {
529
+ return jsonFailure(path, "a symbol-keyed property");
530
+ }
531
+ }
532
+ ancestors.add(object);
533
+ const copied = {};
534
+ let members = 0;
535
+ const memberPath = { parent: path, key: "" };
536
+ for (const key of Object.keys(descriptors.success)) {
537
+ memberPath.key = key;
538
+ const descriptor = descriptors.success[key];
539
+ if (descriptor.enumerable !== true)
540
+ continue;
541
+ if (!spendJson(budget, key.length + 3 + Math.min(1, members++))) {
542
+ ancestors.delete(object);
543
+ return jsonFailure(path, "JSON expansion exceeds the outbound frame budget");
544
+ }
545
+ if (isAccessor(descriptor)) {
546
+ ancestors.delete(object);
547
+ return jsonFailure(memberPath, "an accessor property");
548
+ }
549
+ const snapshot = snapshotJson(descriptor.value, memberPath, ancestors, budget);
550
+ if (Result.isFailure(snapshot)) {
551
+ ancestors.delete(object);
552
+ return snapshot;
553
+ }
554
+ Object.defineProperty(copied, key, {
555
+ configurable: true,
556
+ enumerable: true,
557
+ value: snapshot.success,
558
+ writable: true
559
+ });
560
+ }
561
+ ancestors.delete(object);
562
+ return Result.succeed(copied);
563
+ };
564
+ const snapshotArguments = (server, args, diagnostic, maxBytes) => {
565
+ const snapshot = snapshotJson(args, "arguments", new Set(), { remaining: maxBytes });
566
+ if (Result.isFailure(snapshot)) {
567
+ diagnostic("invalid-arguments", snapshot.failure);
568
+ return Result.fail(Limits.protocolError(server, `MCP server "${server}" was sent a tool argument that is not JSON: ${snapshot.failure.reason}; property path withheld`));
569
+ }
570
+ if (!isRecord(snapshot.success)) {
571
+ return Result.fail(Limits.protocolError(server, `MCP server "${server}" tool arguments must be a JSON object`));
572
+ }
573
+ return Result.succeed(snapshot.success);
574
+ };
575
+ /**
576
+ * Connects to an MCP server over stdio (`command`) or Streamable HTTP (`url`),
577
+ * completes the `initialize` handshake, and fetches its tool catalog once, up
578
+ * front. Failed or interrupted setup closes its subprocess, I/O fibers, or
579
+ * HTTP session before returning to the caller; a successful session stays
580
+ * open until the caller scope closes.
581
+ *
582
+ * The tool catalog is a snapshot: a server that changes its tools after
583
+ * connecting (a `notifications/tools/list_changed` push) is not re-polled.
584
+ * {@link McpFlows} rebuilds by reconnecting to refresh.
585
+ * Catalog input schemas must declare `type: "object"`. A later tool result may
586
+ * omit `content` when it carries `structuredContent`; a declared output schema
587
+ * is enforced for the documented keyword subset.
588
+ *
589
+ * @category constructors
590
+ * @since 1.0.0-rc.0
591
+ */
592
+ export const connect = (options) => Effect.acquireUseRelease(Effect.flatMap(Effect.scope, Scope.fork), (scope) => Effect.gen(function* () {
593
+ const diagnostic = yield* DiagnosticReporter.make(options.server);
594
+ const decodeResponse = (response, decoded) => Effect.fromResult(decoded).pipe(Effect.tapError(() => Effect.sync(() => diagnostic("invalid-response", response))));
595
+ const handshakeTimeoutMs = options.handshakeTimeoutMs ?? defaultHandshakeTimeoutMs;
596
+ const maxArgumentBytes = options.maxOutboundFrameBytes ?? defaultMaxOutboundFrameBytes;
597
+ const maxTools = options.maxTools ?? defaultMaxTools;
598
+ const maxToolNameBytes = options.maxToolNameBytes ?? defaultMaxToolNameBytes;
599
+ const maxToolDocumentBytes = options.maxToolDocumentBytes ?? defaultMaxToolDocumentBytes;
600
+ const maxCatalogPages = options.maxCatalogPages ?? defaultMaxCatalogPages;
601
+ yield* Limits.checkPositiveIntegers(options.server, [
602
+ ["handshakeTimeoutMs", handshakeTimeoutMs],
603
+ ["maxTools", maxTools],
604
+ ["maxToolNameBytes", maxToolNameBytes],
605
+ ["maxToolDocumentBytes", maxToolDocumentBytes],
606
+ ["maxCatalogPages", maxCatalogPages]
607
+ ]);
608
+ const transport = yield* (isHttp(options)
609
+ ? HttpTransport.connect(options)
610
+ : StdioTransport.connect(options));
611
+ const initialized = yield* transport.request("initialize", {
612
+ protocolVersion: supportedProtocolVersions[0],
613
+ capabilities: {},
614
+ clientInfo
615
+ }, handshakeTimeoutMs);
616
+ yield* decodeResponse(initialized, asInitialize(options.server, initialized));
617
+ // A notification, not a request: the server never replies to it, and the
618
+ // handshake is not complete until the client sends it.
619
+ yield* transport.notify("notifications/initialized", undefined, handshakeTimeoutMs);
620
+ const tools = [];
621
+ const toolNames = new Set();
622
+ const cursors = new Set();
623
+ let params = {};
624
+ let pageCount = 0;
625
+ while (true) {
626
+ const listed = yield* transport.request("tools/list", params, handshakeTimeoutMs);
627
+ pageCount += 1;
628
+ const page = yield* decodeResponse(listed, asToolPage(options.server, listed, { maxTools, maxToolNameBytes, maxToolDocumentBytes }, toolNames, tools));
629
+ if (page.nextCursor === undefined)
630
+ break;
631
+ if (cursors.has(page.nextCursor)) {
632
+ diagnostic("invalid-response", listed);
633
+ return yield* Effect.fail(invalidResponse(options.server, `MCP server "${options.server}" repeated a tools/list cursor`));
634
+ }
635
+ if (pageCount >= maxCatalogPages) {
636
+ return yield* Effect.fail(invalidResponse(options.server, `MCP server "${options.server}" returned more than ${maxCatalogPages} tools/list pages`));
637
+ }
638
+ cursors.add(page.nextCursor);
639
+ params = { cursor: page.nextCursor };
640
+ }
641
+ JsonLimits.freezeParsed(tools);
642
+ const validators = new Map();
643
+ for (const tool of tools) {
644
+ if (tool.outputSchema !== undefined) {
645
+ validators.set(tool.name, yield* runJsonWork(compileOutputSchema(tool.outputSchema)));
646
+ }
647
+ }
648
+ const callTool = (name, args) => {
649
+ const tool = tools.find((candidate) => candidate.name === name);
650
+ if (tool === undefined) {
651
+ return Effect.fail(new McpError({
652
+ code: "tool_not_found",
653
+ message: `MCP server "${options.server}" has no requested tool`,
654
+ server: options.server
655
+ }));
656
+ }
657
+ const snapshot = snapshotArguments(options.server, args, diagnostic, maxArgumentBytes);
658
+ if (Result.isFailure(snapshot)) {
659
+ // Do not inspect the rejected object again: it may contain throwing
660
+ // accessors or proxies. Even diagnostic observers see only the safe
661
+ // rejection, never a second traversal of executable user properties.
662
+ return Effect.fail(snapshot.failure);
663
+ }
664
+ return Effect.flatMap(transport.request("tools/call", { name, arguments: snapshot.success }), (result) => Effect.flatMap(runJsonWork(asToolResult(options.server, result, tool.outputSchema, diagnostic, validators.get(name))), (decoded) => decodeResponse(result, decoded)));
665
+ };
666
+ return { server: options.server, tools, callTool };
667
+ }).pipe(Scope.provide(scope)),
668
+ // Closing a failed attempt also detaches it from the caller's scope.
669
+ // Successful sessions remain owned by that scope until it closes.
670
+ (scope, exit) => Exit.isFailure(exit) ? Scope.close(scope, exit) : Effect.void);
671
+ //# sourceMappingURL=McpClient.js.map