@corbits/mcp 0.3.1 → 0.4.0

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 (124) hide show
  1. package/README.md +120 -26
  2. package/dist/auth/callback.d.ts +14 -0
  3. package/dist/auth/callback.js +79 -0
  4. package/dist/auth/challenge.d.ts +9 -0
  5. package/dist/auth/challenge.js +52 -0
  6. package/dist/auth/contracts.d.ts +18 -0
  7. package/dist/auth/contracts.js +19 -0
  8. package/dist/auth/http.d.ts +2 -0
  9. package/dist/auth/http.js +2 -0
  10. package/dist/auth/index.d.ts +6 -0
  11. package/dist/auth/index.js +6 -0
  12. package/dist/auth/login.d.ts +62 -0
  13. package/dist/auth/login.js +175 -0
  14. package/dist/auth/oauth-http.d.ts +6 -0
  15. package/dist/auth/oauth-http.js +32 -0
  16. package/dist/auth/resource-server.d.ts +25 -0
  17. package/dist/auth/resource-server.js +94 -0
  18. package/dist/auth/url-policy.d.ts +10 -0
  19. package/dist/auth/url-policy.js +35 -0
  20. package/dist/client/resource-budget.d.ts +1 -0
  21. package/dist/client/resource-budget.js +1 -0
  22. package/dist/client/transport.d.ts +10 -0
  23. package/dist/client/transport.js +1 -0
  24. package/dist/client.d.ts +208 -26
  25. package/dist/client.js +1128 -90
  26. package/dist/extensions/apps.d.ts +58 -0
  27. package/dist/extensions/apps.js +55 -0
  28. package/dist/extensions/elicitation.d.ts +18 -0
  29. package/dist/extensions/elicitation.js +55 -0
  30. package/dist/extensions/index.d.ts +5 -0
  31. package/dist/extensions/index.js +5 -0
  32. package/dist/extensions/maturity.d.ts +44 -0
  33. package/dist/extensions/maturity.js +24 -0
  34. package/dist/extensions/skills/contracts.d.ts +53 -0
  35. package/dist/extensions/skills/contracts.js +1 -0
  36. package/dist/extensions/skills/index.d.ts +52 -0
  37. package/dist/extensions/skills/index.js +402 -0
  38. package/dist/extensions/tasks/contracts.d.ts +39 -0
  39. package/dist/extensions/tasks/contracts.js +1 -0
  40. package/dist/extensions/tasks/file-store.d.ts +12 -0
  41. package/dist/extensions/tasks/file-store.js +152 -0
  42. package/dist/extensions/tasks/index.d.ts +82 -0
  43. package/dist/extensions/tasks/index.js +221 -0
  44. package/dist/extensions/tasks/record.d.ts +3 -0
  45. package/dist/extensions/tasks/record.js +37 -0
  46. package/dist/hub/discover.d.ts +5 -1
  47. package/dist/hub/discover.js +36 -13
  48. package/dist/index.d.ts +3 -2
  49. package/dist/index.js +4 -2
  50. package/dist/interchange/client/capabilities.d.ts +4 -0
  51. package/dist/interchange/client/capabilities.js +53 -0
  52. package/dist/interchange/client/catalog.d.ts +14 -0
  53. package/dist/interchange/client/catalog.js +52 -0
  54. package/dist/interchange/client/contracts.d.ts +47 -0
  55. package/dist/interchange/client/contracts.js +1 -0
  56. package/dist/interchange/client/projection.d.ts +6 -0
  57. package/dist/interchange/client/projection.js +92 -0
  58. package/dist/interchange/client/skills.d.ts +16 -0
  59. package/dist/interchange/client/skills.js +87 -0
  60. package/dist/interchange/client.d.ts +8 -0
  61. package/dist/interchange/client.js +6 -0
  62. package/dist/interchange/server/contracts.d.ts +65 -0
  63. package/dist/interchange/server/contracts.js +1 -0
  64. package/dist/interchange/server.d.ts +5 -0
  65. package/dist/interchange/server.js +172 -0
  66. package/dist/naming.d.ts +2 -0
  67. package/dist/naming.js +7 -0
  68. package/dist/oauth-discovery.d.ts +107 -1
  69. package/dist/oauth-discovery.js +292 -56
  70. package/dist/protocol/content.d.ts +35 -0
  71. package/dist/protocol/content.js +64 -0
  72. package/dist/protocol/envelopes.d.ts +14 -0
  73. package/dist/protocol/envelopes.js +22 -0
  74. package/dist/protocol/errors.d.ts +26 -0
  75. package/dist/protocol/errors.js +31 -0
  76. package/dist/protocol/extensions.d.ts +34 -0
  77. package/dist/protocol/extensions.js +86 -0
  78. package/dist/protocol/headers.d.ts +12 -0
  79. package/dist/protocol/headers.js +128 -0
  80. package/dist/protocol/index.d.ts +11 -0
  81. package/dist/protocol/index.js +11 -0
  82. package/dist/protocol/json-schema.d.ts +8 -0
  83. package/dist/protocol/json-schema.js +152 -0
  84. package/dist/protocol/legacy.d.ts +12 -0
  85. package/dist/protocol/legacy.js +18 -0
  86. package/dist/protocol/methods.d.ts +116 -0
  87. package/dist/protocol/methods.js +78 -0
  88. package/dist/protocol/resource-budget.d.ts +9 -0
  89. package/dist/protocol/resource-budget.js +206 -0
  90. package/dist/protocol/schemas.d.ts +115 -0
  91. package/dist/protocol/schemas.js +553 -0
  92. package/dist/protocol/types.d.ts +295 -0
  93. package/dist/protocol/types.js +3 -0
  94. package/dist/protocol/ui.d.ts +3 -0
  95. package/dist/protocol/ui.js +18 -0
  96. package/dist/server/contracts.d.ts +67 -0
  97. package/dist/server/contracts.js +1 -0
  98. package/dist/server/errors.d.ts +6 -0
  99. package/dist/server/errors.js +21 -0
  100. package/dist/server/extensions.d.ts +7 -0
  101. package/dist/server/extensions.js +61 -0
  102. package/dist/server/fixtures.d.ts +2 -0
  103. package/dist/server/fixtures.js +34 -0
  104. package/dist/server/legacy.d.ts +9 -0
  105. package/dist/server/legacy.js +435 -0
  106. package/dist/server/tools.d.ts +17 -0
  107. package/dist/server/tools.js +93 -0
  108. package/dist/server.d.ts +14 -0
  109. package/dist/server.js +406 -0
  110. package/dist/sidecar-bundle.d.ts +7 -7
  111. package/dist/sidecar-bundle.js +36 -29
  112. package/dist/test-server.js +12 -3
  113. package/dist/tool.d.ts +8 -0
  114. package/dist/tool.js +35 -19
  115. package/dist/url.js +8 -3
  116. package/docs/conformance.md +135 -0
  117. package/docs/integration-status.md +98 -0
  118. package/docs/legacy-server.md +58 -0
  119. package/docs/migration.md +143 -0
  120. package/docs/public-api.md +106 -0
  121. package/examples/README.md +28 -0
  122. package/examples/standalone-client.ts +14 -0
  123. package/examples/standalone-server.ts +37 -0
  124. package/package.json +40 -5
package/dist/client.js CHANGED
@@ -1,40 +1,135 @@
1
- // MCP streamable-HTTP transport client (2025-03-26 spec). Each call sends one
2
- // request and awaits its one reply, which is all initialize/tools-list/
3
- // tools-call need; the session `initialize` opens is carried in `session`.
4
- // No resumption.
1
+ // Streamable-HTTP JSON-RPC client for MCP 2026-07-28, with a legacy
2
+ // fallback. A 2026 server is stateless: `server/discover` then one POST per
3
+ // call, carrying protocol metadata in `_meta` and in `Mcp-*` headers, and no
4
+ // session id. Modern cancellation closes the response stream; only legacy
5
+ // requests send `notifications/cancelled`. No SSE resumption or HTTP+SSE.
5
6
  import { type } from "arktype";
6
- export const McpToolSchema = type({
7
- name: "string",
8
- "description?": "string",
9
- inputSchema: "Record<string, unknown>",
10
- "annotations?": {
11
- "readOnlyHint?": "boolean",
12
- "destructiveHint?": "boolean",
13
- "idempotentHint?": "boolean",
14
- "openWorldHint?": "boolean",
15
- },
16
- });
7
+ import { createResourceBudget, } from "./client/resource-budget.js";
8
+ import { MCP_PROTOCOL_VERSION, } from "./protocol/types.js";
9
+ import { McpError, McpErrorCode } from "./protocol/errors.js";
10
+ import { MCP_EXT_TASKS, MCP_EXT_UI, MCP_EXT_SKILLS, validateExtensionSettings, } from "./protocol/extensions.js";
11
+ import { McpToolSchema, PromptSchema, PromptResult, ResourceSchema, TemplateSchema, CompletionResult, SkillSchema, TaskHandleSchema, ToolCallResult, DiscoverResult, JsonRpcResponse, validateRequest, validateResult, validateNotification, validateTask, record, validateProgressToken, } from "./protocol/schemas.js";
12
+ import { encodeHeaderValue, paramHeaders, collectHeaderSlots, } from "./protocol/headers.js";
13
+ import { mcpNameFor } from "./protocol/methods.js";
14
+ export { MCP_PROTOCOL_VERSION, McpError, McpErrorCode, MCP_EXT_TASKS, MCP_EXT_UI, MCP_EXT_SKILLS, McpToolSchema, };
15
+ const LEGACY_PROTOCOL_VERSION = "2025-03-26";
16
+ /** Legacy handshake versions, newest first. */
17
+ const LEGACY_VERSIONS = [
18
+ "2025-11-25",
19
+ "2025-06-18",
20
+ "2025-03-26",
21
+ "2024-11-05",
22
+ ];
23
+ const META_PROTOCOL = "io.modelcontextprotocol/protocolVersion";
24
+ const META_CLIENT_INFO = "io.modelcontextprotocol/clientInfo";
25
+ const META_CLIENT_CAPABILITIES = "io.modelcontextprotocol/clientCapabilities";
26
+ const META_SUBSCRIPTION = "io.modelcontextprotocol/subscriptionId";
27
+ const META_SERVER_INFO = "io.modelcontextprotocol/serverInfo";
28
+ const CLIENT_INFO = { name: "@corbits/mcp", version: "0.3.1" };
29
+ const UI_MIME_TYPES = ["text/html;profile=mcp-app"];
30
+ const MODERN_ERROR_CODES = new Set([
31
+ McpErrorCode.HeaderMismatch,
32
+ McpErrorCode.MissingRequiredClientCapability,
33
+ McpErrorCode.UnsupportedProtocolVersion,
34
+ ]);
17
35
  /** The run-time `tools/call` and `initialize` bound when a caller sets none. */
18
36
  export const DEFAULT_TIMEOUT_MS = 60_000;
19
- const PROTOCOL_VERSION = "2025-03-26";
20
37
  /** Largest response body or SSE frame read before the stream is cancelled. */
21
38
  const MAX_MESSAGE_BYTES = 4 * 1024 * 1024;
22
- const JsonRpcResponse = type({
23
- jsonrpc: "'2.0'",
24
- id: "string | number",
25
- "result?": "unknown",
26
- "error?": { code: "number", message: "string", "data?": "unknown" },
27
- });
28
- export class McpError extends Error {
29
- /** The HTTP status, when the server answered with a non-2xx one. */
30
- status;
31
- constructor(message, status) {
32
- super(message);
33
- this.name = "McpError";
34
- this.status = status;
39
+ /** A cursor loop that never ends is a misbehaving server, not a catalog. */
40
+ const MAX_PAGES = 1_000;
41
+ let nextId = 1;
42
+ const activeProgressTokens = new Set();
43
+ function reserveProgressToken(token) {
44
+ if (token === undefined)
45
+ return;
46
+ if (activeProgressTokens.has(token))
47
+ throw new McpError("progress token is already active");
48
+ activeProgressTokens.add(token);
49
+ }
50
+ function validationContext(opts) {
51
+ return {
52
+ ...opts.validation,
53
+ era: opts.session !== undefined && !isModernSession(opts.session)
54
+ ? "legacy"
55
+ : "modern",
56
+ extensions: enabledExtensions(opts),
57
+ ...(opts.session?.capabilities !== undefined
58
+ ? { serverCapabilities: opts.session.capabilities }
59
+ : {}),
60
+ ...(opts.inputSchema !== undefined
61
+ ? { inputSchema: opts.inputSchema }
62
+ : {}),
63
+ ...(opts.outputSchema !== undefined
64
+ ? { outputSchema: opts.outputSchema }
65
+ : {}),
66
+ clientCapabilities: clientCapabilities(opts),
67
+ };
68
+ }
69
+ export function createMcpRequestTransport(url, defaults = {}) {
70
+ return {
71
+ async request(method, params, options = {}) {
72
+ const opts = { ...defaults, ...options };
73
+ const callOpts = opts.session === undefined ? asModern(opts) : opts;
74
+ const { result } = await sendRequest(url, method, params, callOpts);
75
+ return result;
76
+ },
77
+ listen(filter, observer, options = {}) {
78
+ return mcpListen(url, filter, observer, { ...defaults, ...options });
79
+ },
80
+ };
81
+ }
82
+ function isModernSession(session) {
83
+ return session?.protocolVersion === MCP_PROTOCOL_VERSION;
84
+ }
85
+ function enabledExtensions(opts) {
86
+ if (opts.extensions !== undefined)
87
+ return opts.extensions;
88
+ return opts.session?.extensions ?? [];
89
+ }
90
+ function requireExtension(opts, id) {
91
+ if (!enabledExtensions(opts).includes(id)) {
92
+ throw new McpError(`extension ${id} is not enabled`);
35
93
  }
36
94
  }
37
- let nextId = 1;
95
+ function clientCapabilities(opts) {
96
+ if (opts.elicitation !== undefined) {
97
+ const { elicitation, ...base } = opts;
98
+ const capabilities = clientCapabilities(base);
99
+ const modes = {
100
+ ...(elicitation.form === true ? { form: {} } : {}),
101
+ ...(elicitation.url === true ? { url: {} } : {}),
102
+ };
103
+ const { elicitation: _inherited, ...rest } = capabilities;
104
+ return {
105
+ ...rest,
106
+ ...(Object.keys(modes).length > 0 ? { elicitation: modes } : {}),
107
+ };
108
+ }
109
+ if (opts.clientCapabilities === undefined &&
110
+ opts.extensions === undefined &&
111
+ opts.uiMimeTypes === undefined &&
112
+ opts.session?.clientCapabilities !== undefined) {
113
+ return opts.session.clientCapabilities;
114
+ }
115
+ const extensions = enabledExtensions(opts);
116
+ validateExtensionSettings(Object.fromEntries(extensions.map((id) => [id, {}])));
117
+ if (extensions.length === 0)
118
+ return opts.clientCapabilities ?? {};
119
+ const advertised = {};
120
+ for (const id of extensions) {
121
+ if (id === MCP_EXT_UI) {
122
+ const mimeTypes = opts.uiMimeTypes ?? opts.session?.uiMimeTypes;
123
+ advertised[id] = {
124
+ mimeTypes: [...(mimeTypes ?? UI_MIME_TYPES)],
125
+ };
126
+ }
127
+ else {
128
+ advertised[id] = {};
129
+ }
130
+ }
131
+ return { ...opts.clientCapabilities, extensions: advertised };
132
+ }
38
133
  /** A reply to request `id`: a message carrying `result` or `error` and no
39
134
  * `method`, which would make it a server request reusing the id. */
40
135
  function isResponseTo(message, id) {
@@ -44,92 +139,250 @@ function isResponseTo(message, id) {
44
139
  !("method" in message) &&
45
140
  ("result" in message || "error" in message));
46
141
  }
47
- function headersFor(session) {
48
- return {
142
+ function isNotification(message) {
143
+ return (typeof message === "object" &&
144
+ message !== null &&
145
+ message.jsonrpc === "2.0" &&
146
+ typeof message.method === "string" &&
147
+ !("id" in message));
148
+ }
149
+ function collectHeaders(schema, toolName) {
150
+ try {
151
+ return collectHeaderSlots(schema);
152
+ }
153
+ catch (cause) {
154
+ console.warn(`excluding MCP tool ${toolName}: ${cause instanceof Error ? cause.message : String(cause)}`);
155
+ return undefined;
156
+ }
157
+ }
158
+ function requestHeaders(opts, method, mcpName, extra) {
159
+ const headers = {
49
160
  "content-type": "application/json",
50
161
  accept: "application/json, text/event-stream",
51
- ...(session === undefined
52
- ? {}
53
- : { "mcp-protocol-version": session.protocolVersion }),
54
- ...(session?.sessionId === undefined
55
- ? {}
56
- : { "mcp-session-id": session.sessionId }),
57
162
  };
163
+ if (isModernSession(opts.session)) {
164
+ headers["MCP-Protocol-Version"] = MCP_PROTOCOL_VERSION;
165
+ headers["Mcp-Method"] = method;
166
+ if (mcpName !== undefined)
167
+ headers["Mcp-Name"] = encodeHeaderValue(mcpName);
168
+ for (const [key, value] of Object.entries(extra))
169
+ headers[key] = value;
170
+ return headers;
171
+ }
172
+ if (opts.session !== undefined) {
173
+ headers["mcp-protocol-version"] = opts.session.protocolVersion;
174
+ if (opts.session.sessionId !== undefined) {
175
+ headers["mcp-session-id"] = opts.session.sessionId;
176
+ }
177
+ }
178
+ return headers;
179
+ }
180
+ function progressParams(params, opts, id) {
181
+ const metadata = params?._meta === undefined ? {} : record(params._meta, "request _meta");
182
+ const token = opts.progressToken !== undefined
183
+ ? opts.progressToken
184
+ : (metadata.progressToken ??
185
+ (opts.onProgress !== undefined ? id : undefined));
186
+ if (Object.hasOwn(metadata, "progressToken"))
187
+ validateProgressToken(metadata.progressToken);
188
+ if (token !== undefined)
189
+ validateProgressToken(token);
190
+ return {
191
+ ...params,
192
+ _meta: {
193
+ ...metadata,
194
+ ...(token !== undefined ? { progressToken: token } : {}),
195
+ },
196
+ };
197
+ }
198
+ function modernParams(params, opts, id) {
199
+ const base = progressParams(params, opts, id);
200
+ return {
201
+ ...base,
202
+ _meta: {
203
+ ...record(base._meta),
204
+ [META_PROTOCOL]: MCP_PROTOCOL_VERSION,
205
+ [META_CLIENT_INFO]: CLIENT_INFO,
206
+ [META_CLIENT_CAPABILITIES]: clientCapabilities(opts),
207
+ },
208
+ };
209
+ }
210
+ function stripMeta(params) {
211
+ if (params === undefined)
212
+ return {};
213
+ const rest = {};
214
+ for (const [key, value] of Object.entries(params)) {
215
+ if (key !== "_meta")
216
+ rest[key] = value;
217
+ }
218
+ return rest;
219
+ }
220
+ /** A fresh request. The aborted call signal must not cancel this notification,
221
+ * and progress fields belong to the call that was cancelled. */
222
+ function cancellationOpts(opts) {
223
+ return {
224
+ ...(opts.fetch !== undefined ? { fetch: opts.fetch } : {}),
225
+ ...(opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}),
226
+ ...(opts.session !== undefined ? { session: opts.session } : {}),
227
+ ...(opts.extensions !== undefined ? { extensions: opts.extensions } : {}),
228
+ ...(opts.uiMimeTypes !== undefined
229
+ ? { uiMimeTypes: opts.uiMimeTypes }
230
+ : {}),
231
+ };
232
+ }
233
+ function deliverProgress(message, token, onProgress, previous) {
234
+ if (token === undefined || !isNotification(message)) {
235
+ return previous;
236
+ }
237
+ if (message.method !== "notifications/progress")
238
+ return previous;
239
+ const params = message.params;
240
+ if (params === undefined || params.progressToken !== token)
241
+ return previous;
242
+ validateNotification("notifications/progress", params);
243
+ if (typeof params.progress !== "number")
244
+ return previous;
245
+ const progress = {
246
+ progressToken: token,
247
+ progress: params.progress,
248
+ };
249
+ if (typeof params.total === "number")
250
+ progress.total = params.total;
251
+ if (typeof params.message === "string")
252
+ progress.message = params.message;
253
+ if (previous !== undefined && progress.progress <= previous)
254
+ throw new McpError("progress must strictly increase");
255
+ onProgress?.(progress);
256
+ return progress.progress;
58
257
  }
59
258
  /**
60
259
  * Send one JSON-RPC request over streamable HTTP and return its `result`.
61
260
  * Handles both response shapes the spec allows: a direct `application/json`
62
261
  * body, and a `text/event-stream` whose `data:` frames each carry a
63
262
  * JSON-RPC message -- the first one matching this request's id wins.
263
+ * Progress notifications on that stream are delivered and not treated as the
264
+ * response.
64
265
  */
65
266
  async function sendRequest(url, method, params, opts) {
267
+ if (method === "resources/read" &&
268
+ opts.maxDecodedBytes !== undefined &&
269
+ (!Number.isSafeInteger(opts.maxDecodedBytes) || opts.maxDecodedBytes < 0))
270
+ throw new McpError("maxDecodedBytes must be a nonnegative safe integer");
66
271
  const fetchImpl = opts.fetch ?? fetch;
67
272
  const id = nextId++;
68
- const timeout = opts.timeoutMs === undefined
69
- ? undefined
70
- : AbortSignal.timeout(opts.timeoutMs);
273
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
274
+ const timeout = AbortSignal.timeout(timeoutMs);
71
275
  const signals = [timeout, opts.signal].filter((s) => s !== undefined);
72
276
  const signal = signals.length > 0 ? AbortSignal.any(signals) : undefined;
277
+ const modern = isModernSession(opts.session);
278
+ const bodyParams = modern
279
+ ? modernParams(params, opts, id)
280
+ : progressParams(params, opts, id);
281
+ if (method !== "initialize")
282
+ validateRequest(method, bodyParams, validationContext(opts));
283
+ const progressToken = record(bodyParams._meta).progressToken;
284
+ let previousProgress;
285
+ const extra = method === "tools/call"
286
+ ? paramHeaders(opts.inputSchema, params?.arguments, String(params?.name ?? ""))
287
+ : {};
73
288
  let message;
74
289
  let sessionId;
290
+ const notifyCancel = () => {
291
+ void sendNotification(url, "notifications/cancelled", cancellationOpts(opts), {
292
+ requestId: id,
293
+ reason: "the caller aborted the request",
294
+ }).catch(() => undefined);
295
+ };
296
+ reserveProgressToken(progressToken);
297
+ if (!modern && method !== "initialize" && opts.signal !== undefined) {
298
+ opts.signal.addEventListener("abort", notifyCancel, { once: true });
299
+ }
75
300
  try {
76
301
  const response = await fetchImpl(url, {
77
302
  method: "POST",
78
- headers: headersFor(opts.session),
303
+ headers: requestHeaders(opts, method, mcpNameFor(method, params), extra),
79
304
  body: JSON.stringify({
80
305
  jsonrpc: "2.0",
81
306
  id,
82
307
  method,
83
- ...(params !== undefined ? { params } : {}),
308
+ ...(bodyParams !== undefined ? { params: bodyParams } : {}),
84
309
  }),
85
310
  ...(signal !== undefined ? { signal } : {}),
86
311
  });
87
312
  if (!response.ok) {
88
- await response.body?.cancel();
89
- throw new McpError(`MCP server ${url} responded ${response.status} to ${method}`, response.status);
313
+ throw await errorFrom(response, url, method, signal);
90
314
  }
91
315
  sessionId = response.headers.get("mcp-session-id") ?? undefined;
92
316
  const contentType = response.headers.get("content-type") ?? "";
317
+ const budget = method === "resources/read" && opts.maxDecodedBytes !== undefined
318
+ ? createResourceBudget(opts.maxDecodedBytes, contentType.includes("text/event-stream"))
319
+ : undefined;
93
320
  message = contentType.includes("text/event-stream")
94
- ? await readSseJsonRpc(response, id, signal)
95
- : parseJson(await readCapped(response, signal));
321
+ ? await readSseJsonRpc(response, id, signal, (frame) => {
322
+ previousProgress = deliverProgress(frame, progressToken, opts.onProgress, previousProgress);
323
+ }, budget)
324
+ : parseJson(await readCapped(response, signal, budget));
96
325
  }
97
326
  catch (cause) {
98
327
  if (opts.signal?.aborted === true) {
99
- throw new McpError(`${method} to MCP server ${url} was cancelled`);
328
+ throw new McpError(`${method} to MCP server ${url} was cancelled`, undefined, undefined, undefined, {
329
+ cancelled: true,
330
+ });
100
331
  }
101
332
  if (timeout?.aborted === true) {
102
- throw new McpError(`MCP server ${url} did not answer ${method} within ${String(opts.timeoutMs)}ms`);
333
+ throw new McpError(`MCP server ${url} did not answer ${method} within ${String(timeoutMs)}ms`, undefined, undefined, undefined, { timedOut: true });
103
334
  }
104
335
  throw cause;
105
336
  }
337
+ finally {
338
+ opts.signal?.removeEventListener("abort", notifyCancel);
339
+ if (progressToken !== undefined)
340
+ activeProgressTokens.delete(progressToken);
341
+ }
106
342
  const parsed = JsonRpcResponse(message);
107
- if (parsed instanceof type.errors || !isResponseTo(message, id)) {
108
- throw new McpError(`MCP server ${url} sent a malformed response to ${method}${parsed instanceof type.errors ? `: ${parsed.summary}` : ""}`);
343
+ if (parsed instanceof type.errors ||
344
+ !isResponseTo(message, id) ||
345
+ (parsed.error !== undefined && Object.hasOwn(parsed, "result"))) {
346
+ throw new McpError(`MCP server ${url} sent a malformed response to ${method}`);
109
347
  }
110
348
  if (parsed.error !== undefined) {
111
- throw new McpError(`MCP server ${url} rejected ${method}: ${parsed.error.message}`);
349
+ throw new McpError(`MCP server ${url} rejected ${method}: ${parsed.error.message}`, undefined, parsed.error.code, parsed.error.data, { source: "peer" });
112
350
  }
351
+ if (method !== "initialize")
352
+ validateResult(method, parsed.result, {
353
+ ...validationContext(opts),
354
+ // Never accept a caller-supplied validation context as proof of a wire budget.
355
+ maxDecodedBytes: method === "resources/read" ? opts.maxDecodedBytes : undefined,
356
+ });
113
357
  return { result: parsed.result, sessionId };
114
358
  }
115
359
  /** Send a JSON-RPC notification; the server answers 202 with no body. */
116
- async function sendNotification(url, method, opts) {
360
+ async function sendNotification(url, method, opts, params) {
117
361
  const fetchImpl = opts.fetch ?? fetch;
118
- const timeout = opts.timeoutMs === undefined
362
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
363
+ const timeout = AbortSignal.timeout(timeoutMs);
364
+ const modern = isModernSession(opts.session);
365
+ const bodyParams = params === undefined
119
366
  ? undefined
120
- : AbortSignal.timeout(opts.timeoutMs);
367
+ : modern
368
+ ? modernParams(params, cancellationOpts(opts), 0)
369
+ : params;
121
370
  let response;
122
371
  try {
123
372
  response = await fetchImpl(url, {
124
373
  method: "POST",
125
- headers: headersFor(opts.session),
126
- body: JSON.stringify({ jsonrpc: "2.0", method }),
374
+ headers: requestHeaders(opts, method, undefined, {}),
375
+ body: JSON.stringify({
376
+ jsonrpc: "2.0",
377
+ method,
378
+ ...(bodyParams !== undefined ? { params: bodyParams } : {}),
379
+ }),
127
380
  ...(timeout !== undefined ? { signal: timeout } : {}),
128
381
  });
129
382
  }
130
383
  catch (cause) {
131
384
  if (timeout?.aborted === true) {
132
- throw new McpError(`MCP server ${url} did not answer ${method} within ${String(opts.timeoutMs)}ms`);
385
+ throw new McpError(`MCP server ${url} did not answer ${method} within ${String(opts.timeoutMs)}ms`, undefined, undefined, undefined, { timedOut: true });
133
386
  }
134
387
  throw cause;
135
388
  }
@@ -138,6 +391,50 @@ async function sendNotification(url, method, opts) {
138
391
  throw new McpError(`MCP server ${url} responded ${response.status} to ${method}`, response.status);
139
392
  }
140
393
  }
394
+ async function errorFrom(response, url, method, signal) {
395
+ if (response.status === 401 || response.status === 403) {
396
+ void response.body?.cancel().catch(() => undefined);
397
+ const challenge = response.headers.get("www-authenticate");
398
+ return new McpError(`MCP server ${url} responded ${response.status} to ${method}`, response.status, undefined, undefined, challenge === null ? undefined : { wwwAuthenticate: challenge });
399
+ }
400
+ let code;
401
+ let data;
402
+ let detail;
403
+ try {
404
+ const text = await readCapped(response, signal);
405
+ if (text.length > 0) {
406
+ let parsed;
407
+ try {
408
+ parsed = JSON.parse(text);
409
+ }
410
+ catch {
411
+ parsed = undefined;
412
+ }
413
+ if (typeof parsed === "object" && parsed !== null) {
414
+ const error = parsed.error;
415
+ if (typeof error === "object" && error !== null) {
416
+ const record = error;
417
+ if (typeof record.code === "number" &&
418
+ typeof record.message === "string") {
419
+ code = record.code;
420
+ detail = record.message;
421
+ data = record.data;
422
+ }
423
+ }
424
+ }
425
+ }
426
+ }
427
+ catch (cause) {
428
+ if (cause instanceof McpError && cause.message.includes("larger than"))
429
+ throw cause;
430
+ }
431
+ const message = detail === undefined
432
+ ? `MCP server ${url} responded ${response.status} to ${method}`
433
+ : `MCP server ${url} rejected ${method}: ${detail}`;
434
+ return new McpError(message, response.status, code, data, {
435
+ source: code === undefined ? "local" : "peer",
436
+ });
437
+ }
141
438
  function parseJson(text) {
142
439
  try {
143
440
  return JSON.parse(text);
@@ -150,7 +447,8 @@ function overflow() {
150
447
  return new McpError(`MCP server sent a message larger than ${String(MAX_MESSAGE_BYTES)} bytes`);
151
448
  }
152
449
  /** Read a body's chunks, cancelling the stream when the signal aborts. */
153
- async function* chunks(response, signal) {
450
+ async function* chunks(response, signal, budget) {
451
+ signal?.throwIfAborted();
154
452
  const body = response.body;
155
453
  if (body === null) {
156
454
  throw new McpError("MCP server sent a response with no body");
@@ -161,8 +459,11 @@ async function* chunks(response, signal) {
161
459
  try {
162
460
  for (;;) {
163
461
  const { done, value } = await reader.read();
164
- if (done)
462
+ if (done) {
463
+ budget?.finish();
165
464
  return;
465
+ }
466
+ budget?.push(value);
166
467
  yield value;
167
468
  }
168
469
  }
@@ -171,30 +472,29 @@ async function* chunks(response, signal) {
171
472
  await reader.cancel().catch(() => undefined);
172
473
  }
173
474
  }
174
- async function readCapped(response, signal) {
475
+ async function readCapped(response, signal, budget) {
175
476
  const decoder = new TextDecoder();
176
477
  let text = "";
177
478
  let bytes = 0;
178
- for await (const chunk of chunks(response, signal)) {
479
+ for await (const chunk of chunks(response, signal, budget)) {
179
480
  bytes += chunk.byteLength;
180
- if (bytes > MAX_MESSAGE_BYTES)
481
+ if (budget === undefined && bytes > MAX_MESSAGE_BYTES)
181
482
  throw overflow();
182
483
  text += decoder.decode(chunk, { stream: true });
183
484
  }
184
485
  return text + decoder.decode();
185
486
  }
186
- /** Read an SSE body and return the first JSON-RPC message whose id matches. */
187
- async function readSseJsonRpc(response, id, signal) {
487
+ async function* sseMessages(response, signal, budget) {
188
488
  const decoder = new TextDecoder();
189
489
  let buffer = "";
190
- for await (const chunk of chunks(response, signal)) {
490
+ for await (const chunk of chunks(response, signal, budget)) {
191
491
  buffer += decoder.decode(chunk, { stream: true });
192
492
  const frames = buffer.split(/\r\n\r\n|\n\n|\r\r/);
193
493
  buffer = frames.pop() ?? "";
194
- if (buffer.length > MAX_MESSAGE_BYTES)
494
+ if (budget === undefined && buffer.length > MAX_MESSAGE_BYTES)
195
495
  throw overflow();
196
496
  for (const frame of frames) {
197
- if (frame.length > MAX_MESSAGE_BYTES)
497
+ if (budget === undefined && frame.length > MAX_MESSAGE_BYTES)
198
498
  throw overflow();
199
499
  const dataLines = frame
200
500
  .split(/\r\n|\r|\n/)
@@ -202,30 +502,48 @@ async function readSseJsonRpc(response, id, signal) {
202
502
  .map((line) => line.slice(5).trim());
203
503
  if (dataLines.length === 0)
204
504
  continue;
205
- let candidate;
206
505
  try {
207
- candidate = JSON.parse(dataLines.join("\n"));
506
+ yield JSON.parse(dataLines.join("\n"));
208
507
  }
209
508
  catch {
210
- continue;
509
+ // A non-JSON data frame is not the response; keep reading.
211
510
  }
212
- if (isResponseTo(candidate, id))
213
- return candidate;
214
511
  }
215
512
  }
513
+ }
514
+ /** Read an SSE body and return the first JSON-RPC message whose id matches. */
515
+ async function readSseJsonRpc(response, id, signal, onMessage, budget) {
516
+ for await (const candidate of sseMessages(response, signal, budget)) {
517
+ if (isResponseTo(candidate, id))
518
+ return candidate;
519
+ onMessage(candidate);
520
+ }
216
521
  throw new McpError("MCP server closed the event stream without a matching response");
217
522
  }
218
523
  const InitializeResult = type({
219
524
  "protocolVersion?": "string",
220
525
  "serverInfo?": { name: "string", "version?": "string" },
221
526
  });
527
+ function serverInfoOf(meta) {
528
+ const info = meta?.[META_SERVER_INFO];
529
+ if (typeof info !== "object" || info === null)
530
+ return undefined;
531
+ const name = info.name;
532
+ if (typeof name !== "string")
533
+ return undefined;
534
+ const version = info.version;
535
+ return typeof version === "string" ? { name, version } : { name };
536
+ }
222
537
  /** `initialize` handshake followed by `notifications/initialized`. Pass the
223
- * returned session to every later call on this server. */
538
+ * returned session to every later call on a legacy server. */
224
539
  export async function mcpInitialize(url, opts = {}) {
540
+ return initializeLegacy(url, opts, LEGACY_PROTOCOL_VERSION);
541
+ }
542
+ async function initializeLegacy(url, opts, protocolVersion) {
225
543
  const { result, sessionId } = await sendRequest(url, "initialize", {
226
- protocolVersion: PROTOCOL_VERSION,
544
+ protocolVersion,
227
545
  capabilities: {},
228
- clientInfo: { name: "@corbits/mcp", version: "0.2.0" },
546
+ clientInfo: { name: "@corbits/mcp", version: "0.3.1" },
229
547
  }, {
230
548
  ...(opts.fetch !== undefined ? { fetch: opts.fetch } : {}),
231
549
  ...(opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}),
@@ -239,7 +557,7 @@ export async function mcpInitialize(url, opts = {}) {
239
557
  throw new McpError(`MCP server ${url} sent a protocolVersion that is not printable ASCII`);
240
558
  }
241
559
  const session = {
242
- protocolVersion: info.protocolVersion ?? PROTOCOL_VERSION,
560
+ protocolVersion: info.protocolVersion ?? protocolVersion,
243
561
  ...(sessionId !== undefined ? { sessionId } : {}),
244
562
  ...(info.serverInfo !== undefined ? { serverInfo: info.serverInfo } : {}),
245
563
  };
@@ -249,30 +567,750 @@ export async function mcpInitialize(url, opts = {}) {
249
567
  });
250
568
  return session;
251
569
  }
570
+ function supportedFrom(data) {
571
+ if (typeof data !== "object" || data === null)
572
+ return [];
573
+ const supported = data.supported;
574
+ if (!Array.isArray(supported))
575
+ return [];
576
+ return supported.filter((version) => typeof version === "string");
577
+ }
578
+ function legacyVersion(supported) {
579
+ return LEGACY_VERSIONS.find((version) => supported.includes(version));
580
+ }
581
+ function shouldFallback(error) {
582
+ if (!(error instanceof McpError))
583
+ return false;
584
+ if (error.status !== undefined &&
585
+ error.status !== 400 &&
586
+ error.status !== 404)
587
+ return false;
588
+ if (error.cancelled)
589
+ return false;
590
+ if (error.timedOut)
591
+ return true;
592
+ // Streamable HTTP backward compatibility: inspect HTTP 400 bodies before
593
+ // falling back. Recognized modern errors require correcting the request,
594
+ // while empty or unrecognized bodies may come from a legacy session server.
595
+ if (error.status === 400)
596
+ return error.code === undefined || !MODERN_ERROR_CODES.has(error.code);
597
+ // Preserve the existing fallback rules for other transport responses.
598
+ if (error.code === McpErrorCode.MethodNotFound)
599
+ return true;
600
+ if (error.status === 404)
601
+ return true;
602
+ if (/unsupported protocol version/i.test(error.message))
603
+ return true;
604
+ if (/server not initialized/i.test(error.message))
605
+ return true;
606
+ if (error.status !== undefined)
607
+ return false;
608
+ if (error.code !== undefined && MODERN_ERROR_CODES.has(error.code))
609
+ return false;
610
+ // Malformed responses and any other error are real failures, not legacy.
611
+ return false;
612
+ }
613
+ /**
614
+ * Probe `server/discover`. A 2026-07-28 server returns a stateless session:
615
+ * later calls send no `initialize`, no `notifications/initialized`, and no
616
+ * `Mcp-Session-Id`. A method-not-found, a timeout, or a server that only
617
+ * speaks a 2025 (or earlier) version falls back to `initialize`. HTTP 400
618
+ * also falls back unless its body contains a recognized modern error. Authz
619
+ * failures, rate limits, server errors, and malformed responses are real
620
+ * failures and surface to the caller instead of triggering the legacy
621
+ * handshake.
622
+ */
623
+ export async function mcpConnect(url, opts = {}) {
624
+ const probing = {
625
+ protocolVersion: MCP_PROTOCOL_VERSION,
626
+ clientCapabilities: clientCapabilities(opts),
627
+ ...(opts.extensions !== undefined && opts.extensions.length > 0
628
+ ? { extensions: opts.extensions }
629
+ : {}),
630
+ ...(opts.uiMimeTypes !== undefined
631
+ ? { uiMimeTypes: opts.uiMimeTypes }
632
+ : {}),
633
+ };
634
+ const probeOpts = { ...opts, session: probing };
635
+ try {
636
+ const { result } = await sendRequest(url, "server/discover", undefined, probeOpts);
637
+ const kind = resultKind(result);
638
+ if (kind !== "complete") {
639
+ throw new McpError(`MCP server ${url} sent a ${kind} result for server/discover`);
640
+ }
641
+ const parsed = DiscoverResult(result);
642
+ if (parsed instanceof type.errors) {
643
+ throw new McpError(`MCP server ${url} sent a malformed server/discover result: ${parsed.summary}`);
644
+ }
645
+ if (parsed.supportedVersions.includes(MCP_PROTOCOL_VERSION)) {
646
+ const meta = parsed._meta;
647
+ const serverInfo = serverInfoOf(meta);
648
+ return {
649
+ ...probing,
650
+ ...(serverInfo !== undefined ? { serverInfo } : {}),
651
+ capabilities: parsed.capabilities,
652
+ ...(parsed.instructions !== undefined
653
+ ? { instructions: parsed.instructions }
654
+ : {}),
655
+ };
656
+ }
657
+ const legacy = legacyVersion(parsed.supportedVersions);
658
+ if (legacy !== undefined)
659
+ return initializeLegacy(url, opts, legacy);
660
+ throw new McpError(`MCP server ${url} does not speak ${MCP_PROTOCOL_VERSION}`, undefined, McpErrorCode.UnsupportedProtocolVersion, { supported: parsed.supportedVersions, requested: MCP_PROTOCOL_VERSION });
661
+ }
662
+ catch (cause) {
663
+ if (cause instanceof McpError &&
664
+ cause.code === McpErrorCode.UnsupportedProtocolVersion &&
665
+ (cause.status === undefined ||
666
+ cause.status === 400 ||
667
+ cause.status === 404)) {
668
+ const legacy = legacyVersion(supportedFrom(cause.data));
669
+ if (legacy !== undefined)
670
+ return initializeLegacy(url, opts, legacy);
671
+ throw cause;
672
+ }
673
+ if (shouldFallback(cause))
674
+ return initializeLegacy(url, opts, LEGACY_PROTOCOL_VERSION);
675
+ throw cause;
676
+ }
677
+ }
678
+ function resultKind(result) {
679
+ if (typeof result !== "object" ||
680
+ result === null ||
681
+ !("resultType" in result)) {
682
+ return "complete";
683
+ }
684
+ const value = result.resultType;
685
+ if (value === undefined)
686
+ return "complete";
687
+ if (value === "complete" || value === "input_required" || value === "task") {
688
+ return value;
689
+ }
690
+ return "invalid";
691
+ }
692
+ function parseInputRequired(method, params, result, url) {
693
+ if (typeof result !== "object" || result === null) {
694
+ throw new McpError(`MCP server ${url} sent a malformed input_required result`);
695
+ }
696
+ const record = result;
697
+ if (record.inputRequests === undefined && record.requestState === undefined) {
698
+ throw new McpError(`MCP server ${url} sent an input_required result with neither inputRequests nor requestState`);
699
+ }
700
+ if (record.requestState !== undefined &&
701
+ typeof record.requestState !== "string") {
702
+ throw new McpError(`MCP server ${url} sent a non-string requestState`);
703
+ }
704
+ if (record.inputRequests !== undefined) {
705
+ if (typeof record.inputRequests !== "object" ||
706
+ record.inputRequests === null ||
707
+ Array.isArray(record.inputRequests)) {
708
+ throw new McpError(`MCP server ${url} sent a malformed inputRequests map`);
709
+ }
710
+ for (const value of Object.values(record.inputRequests)) {
711
+ if (typeof value !== "object" ||
712
+ value === null ||
713
+ typeof value.method !== "string") {
714
+ throw new McpError(`MCP server ${url} sent a malformed input request`);
715
+ }
716
+ }
717
+ }
718
+ return {
719
+ resultType: "input_required",
720
+ method,
721
+ params: stripMeta(params),
722
+ ...(record._meta !== undefined ? { _meta: record._meta } : {}),
723
+ ...(record.inputRequests !== undefined
724
+ ? { inputRequests: record.inputRequests }
725
+ : {}),
726
+ ...(typeof record.requestState === "string"
727
+ ? { requestState: record.requestState }
728
+ : {}),
729
+ };
730
+ }
731
+ function parseTask(result, url) {
732
+ const parsed = TaskHandleSchema(result);
733
+ if (parsed instanceof type.errors) {
734
+ throw new McpError(`MCP server ${url} sent a malformed task handle: ${parsed.summary}`);
735
+ }
736
+ return { resultType: "task", ...parsed };
737
+ }
738
+ async function requestResult(url, method, params, opts) {
739
+ const result = await createMcpRequestTransport(url, opts).request(method, params);
740
+ const kind = resultKind(result);
741
+ if (kind === "task")
742
+ return parseTask(result, url);
743
+ if (kind === "input_required")
744
+ return parseInputRequired(method, params, result, url);
745
+ return result;
746
+ }
747
+ function isInputRequired(result) {
748
+ return (typeof result === "object" &&
749
+ result !== null &&
750
+ result.resultType === "input_required");
751
+ }
752
+ function isTaskHandle(result) {
753
+ return (typeof result === "object" &&
754
+ result !== null &&
755
+ result.resultType === "task");
756
+ }
757
+ function readCache(result, url, method) {
758
+ if (typeof result !== "object" || result === null)
759
+ return {};
760
+ const record = result;
761
+ const cache = {};
762
+ if ("ttlMs" in record && record.ttlMs !== undefined) {
763
+ if (typeof record.ttlMs !== "number") {
764
+ throw new McpError(`MCP server ${url} sent a non-numeric ttlMs for ${method}`);
765
+ }
766
+ cache.ttlMs = record.ttlMs < 0 ? 0 : record.ttlMs;
767
+ }
768
+ if ("cacheScope" in record && record.cacheScope !== undefined) {
769
+ if (record.cacheScope !== "public" && record.cacheScope !== "private") {
770
+ throw new McpError(`MCP server ${url} sent an invalid cacheScope for ${method}`);
771
+ }
772
+ cache.cacheScope = record.cacheScope;
773
+ }
774
+ return cache;
775
+ }
776
+ function mergeCache(pages) {
777
+ let sawTtl = false;
778
+ let omittedTtl = false;
779
+ let ttl = Number.POSITIVE_INFINITY;
780
+ let scope;
781
+ let scopeOnEveryPage = pages.length > 0;
782
+ for (const page of pages) {
783
+ if (page.ttlMs === undefined)
784
+ omittedTtl = true;
785
+ else {
786
+ sawTtl = true;
787
+ ttl = Math.min(ttl, page.ttlMs);
788
+ }
789
+ if (page.cacheScope === undefined)
790
+ scopeOnEveryPage = false;
791
+ else if (scope === undefined)
792
+ scope = page.cacheScope;
793
+ else if (scope !== page.cacheScope) {
794
+ throw new McpError("MCP server changed cacheScope between pages");
795
+ }
796
+ }
797
+ const merged = {};
798
+ if (sawTtl)
799
+ merged.ttlMs = omittedTtl ? 0 : ttl;
800
+ if (scopeOnEveryPage && scope !== undefined)
801
+ merged.cacheScope = scope;
802
+ return merged;
803
+ }
804
+ function attachCache(items, cache) {
805
+ if (cache.ttlMs !== undefined) {
806
+ Object.defineProperty(items, "ttlMs", {
807
+ value: cache.ttlMs,
808
+ enumerable: false,
809
+ });
810
+ }
811
+ if (cache.cacheScope !== undefined) {
812
+ Object.defineProperty(items, "cacheScope", {
813
+ value: cache.cacheScope,
814
+ enumerable: false,
815
+ });
816
+ }
817
+ return items;
818
+ }
819
+ function nextCursorOf(result, url, method) {
820
+ if (typeof result !== "object" ||
821
+ result === null ||
822
+ !("nextCursor" in result)) {
823
+ return undefined;
824
+ }
825
+ const cursor = result.nextCursor;
826
+ if (cursor === undefined)
827
+ return undefined;
828
+ if (typeof cursor !== "string") {
829
+ throw new McpError(`MCP server ${url} sent a non-string nextCursor for ${method}`);
830
+ }
831
+ return cursor;
832
+ }
833
+ async function eachPage(url, method, base, opts) {
834
+ const pages = [];
835
+ const caches = [];
836
+ let cursor;
837
+ for (let page = 0; page < MAX_PAGES; page++) {
838
+ const params = cursor === undefined ? base : { ...base, cursor };
839
+ const result = await requestResult(url, method, params !== undefined && Object.keys(params).length > 0
840
+ ? params
841
+ : undefined, opts);
842
+ if (isInputRequired(result) || isTaskHandle(result)) {
843
+ throw new McpError(`MCP server ${url} sent ${result.resultType} for ${method}`);
844
+ }
845
+ pages.push(result);
846
+ caches.push(readCache(result, url, method));
847
+ const next = nextCursorOf(result, url, method);
848
+ if (next === undefined)
849
+ return { pages, cache: mergeCache(caches) };
850
+ cursor = next;
851
+ }
852
+ throw new McpError(`MCP server ${url} sent more than ${String(MAX_PAGES)} ${method} pages`);
853
+ }
252
854
  const ToolsListResult = type({ tools: McpToolSchema.array() });
253
- /** `tools/list`: the remote server's tool catalog. */
855
+ /** `tools/list`: the remote server's tool catalog, following cursors. */
254
856
  export async function mcpListTools(url, opts = {}) {
255
- const { result } = await sendRequest(url, "tools/list", undefined, opts);
256
- const parsed = ToolsListResult(result);
257
- if (parsed instanceof type.errors) {
258
- throw new McpError(`MCP server ${url} sent a malformed tools/list result: ${parsed.summary}`);
857
+ const { pages, cache } = await eachPage(url, "tools/list", undefined, opts);
858
+ const tools = [];
859
+ for (const result of pages) {
860
+ const parsed = ToolsListResult(result);
861
+ if (parsed instanceof type.errors) {
862
+ throw new McpError(`MCP server ${url} sent a malformed tools/list result: ${parsed.summary}`);
863
+ }
864
+ for (const tool of parsed.tools) {
865
+ if (collectHeaders(tool.inputSchema, tool.name) === undefined)
866
+ continue;
867
+ tools.push(tool);
868
+ }
259
869
  }
260
- const names = new Set(parsed.tools.map((tool) => tool.name));
261
- if (names.size !== parsed.tools.length) {
870
+ const names = new Set(tools.map((tool) => tool.name));
871
+ if (names.size !== tools.length) {
262
872
  throw new McpError(`MCP server ${url} sent a tools/list result with duplicate tool names`);
263
873
  }
264
- return parsed.tools;
874
+ return attachCache(tools, cache);
265
875
  }
266
876
  /** `tools/call`: invoke a remote tool by name with JSON arguments. */
267
- const ToolCallResult = type({
268
- content: "unknown",
269
- "isError?": "boolean",
270
- });
271
877
  export async function mcpCallTool(url, name, toolArguments, opts = {}) {
272
- const { result } = await sendRequest(url, "tools/call", { name, arguments: toolArguments }, opts);
878
+ const params = { name, arguments: toolArguments };
879
+ const result = await requestResult(url, "tools/call", params, opts);
880
+ if (isInputRequired(result) || isTaskHandle(result))
881
+ return result;
273
882
  const parsed = ToolCallResult(result);
274
883
  if (parsed instanceof type.errors) {
275
884
  throw new McpError(`MCP server ${url} sent a malformed tools/call result for ${name}: ${parsed.summary}`);
276
885
  }
886
+ return {
887
+ resultType: "complete",
888
+ content: parsed.content,
889
+ ...(parsed.isError !== undefined ? { isError: parsed.isError } : {}),
890
+ ...(parsed.structuredContent !== undefined
891
+ ? { structuredContent: parsed.structuredContent }
892
+ : {}),
893
+ ...(parsed._meta !== undefined ? { _meta: parsed._meta } : {}),
894
+ };
895
+ }
896
+ /** Retry a call that came back `input_required`, echoing `requestState`. */
897
+ export async function mcpContinue(url, pending, inputResponses, opts = {}) {
898
+ const params = { ...pending.params, inputResponses };
899
+ if (pending.requestState !== undefined)
900
+ params.requestState = pending.requestState;
901
+ else
902
+ delete params.requestState;
903
+ if (pending.method === "tools/call") {
904
+ return continueMethod(url, pending.method, params, opts, (value) => {
905
+ const parsed = ToolCallResult(value);
906
+ if (parsed instanceof type.errors) {
907
+ throw new McpError(`MCP server ${url} sent a malformed tools/call result: ${parsed.summary}`);
908
+ }
909
+ return {
910
+ resultType: "complete",
911
+ content: parsed.content,
912
+ ...(parsed.isError !== undefined ? { isError: parsed.isError } : {}),
913
+ ...(parsed.structuredContent !== undefined
914
+ ? { structuredContent: parsed.structuredContent }
915
+ : {}),
916
+ ...(parsed._meta !== undefined ? { _meta: parsed._meta } : {}),
917
+ };
918
+ });
919
+ }
920
+ if (pending.method === "prompts/get") {
921
+ return continueMethod(url, pending.method, params, opts, (value) => {
922
+ const parsed = PromptResult(value);
923
+ if (parsed instanceof type.errors) {
924
+ throw new McpError(`MCP server ${url} sent a malformed prompts/get result: ${parsed.summary}`);
925
+ }
926
+ return completePrompt(parsed);
927
+ });
928
+ }
929
+ if (pending.method === "resources/read") {
930
+ return continueMethod(url, pending.method, params, opts, (value) => completeRead(value, url));
931
+ }
932
+ throw new McpError(`cannot retry ${pending.method}`);
933
+ }
934
+ async function continueMethod(url, method, params, opts, complete) {
935
+ const result = await requestResult(url, method, params, opts);
936
+ if (isInputRequired(result) || isTaskHandle(result))
937
+ return result;
938
+ return complete(result);
939
+ }
940
+ function completePrompt(parsed) {
941
+ return {
942
+ resultType: "complete",
943
+ messages: parsed.messages,
944
+ ...(parsed.description !== undefined
945
+ ? { description: parsed.description }
946
+ : {}),
947
+ ...(parsed._meta !== undefined ? { _meta: parsed._meta } : {}),
948
+ };
949
+ }
950
+ /** `prompts/list`, following cursors. */
951
+ export async function mcpListPrompts(url, opts = {}) {
952
+ const { pages, cache } = await eachPage(url, "prompts/list", undefined, opts);
953
+ const prompts = [];
954
+ for (const result of pages) {
955
+ const parsed = type({ prompts: PromptSchema.array() })(result);
956
+ if (parsed instanceof type.errors) {
957
+ throw new McpError(`MCP server ${url} sent a malformed prompts/list result: ${parsed.summary}`);
958
+ }
959
+ prompts.push(...parsed.prompts);
960
+ }
961
+ return attachCache(prompts, cache);
962
+ }
963
+ /** `prompts/get`. */
964
+ export async function mcpGetPrompt(url, name, args, opts = {}) {
965
+ const params = { name };
966
+ if (args !== undefined)
967
+ params.arguments = args;
968
+ return continueMethod(url, "prompts/get", params, opts, (result) => {
969
+ const parsed = PromptResult(result);
970
+ if (parsed instanceof type.errors) {
971
+ throw new McpError(`MCP server ${url} sent a malformed prompts/get result: ${parsed.summary}`);
972
+ }
973
+ return completePrompt(parsed);
974
+ });
975
+ }
976
+ function completeRead(result, url) {
977
+ const parsed = type({
978
+ contents: "unknown[]",
979
+ "_meta?": "Record<string, unknown>",
980
+ })(result);
981
+ if (parsed instanceof type.errors) {
982
+ throw new McpError(`MCP server ${url} sent a malformed resources/read result: ${parsed.summary}`);
983
+ }
984
+ const cache = readCache(result, url, "resources/read");
985
+ return {
986
+ resultType: "complete",
987
+ contents: parsed.contents,
988
+ ...(parsed._meta !== undefined ? { _meta: parsed._meta } : {}),
989
+ ...(cache.ttlMs !== undefined ? { ttlMs: cache.ttlMs } : {}),
990
+ ...(cache.cacheScope !== undefined ? { cacheScope: cache.cacheScope } : {}),
991
+ };
992
+ }
993
+ /** `resources/list`, following cursors. */
994
+ export async function mcpListResources(url, opts = {}) {
995
+ const { pages, cache } = await eachPage(url, "resources/list", undefined, opts);
996
+ const resources = [];
997
+ for (const result of pages) {
998
+ const parsed = type({ resources: ResourceSchema.array() })(result);
999
+ if (parsed instanceof type.errors) {
1000
+ throw new McpError(`MCP server ${url} sent a malformed resources/list result: ${parsed.summary}`);
1001
+ }
1002
+ resources.push(...parsed.resources);
1003
+ }
1004
+ return attachCache(resources, cache);
1005
+ }
1006
+ /** `resources/read`. */
1007
+ export async function mcpReadResource(url, uri, opts = {}) {
1008
+ return continueMethod(url, "resources/read", { uri }, opts, (result) => completeRead(result, url));
1009
+ }
1010
+ /** `resources/templates/list`, following cursors. */
1011
+ export async function mcpListResourceTemplates(url, opts = {}) {
1012
+ const { pages, cache } = await eachPage(url, "resources/templates/list", undefined, opts);
1013
+ const templates = [];
1014
+ for (const result of pages) {
1015
+ const parsed = type({ resourceTemplates: TemplateSchema.array() })(result);
1016
+ if (parsed instanceof type.errors) {
1017
+ throw new McpError(`MCP server ${url} sent a malformed resources/templates/list result: ${parsed.summary}`);
1018
+ }
1019
+ templates.push(...parsed.resourceTemplates);
1020
+ }
1021
+ return attachCache(templates, cache);
1022
+ }
1023
+ /** `completion/complete`. */
1024
+ export async function mcpComplete(url, ref, argument, opts = {}) {
1025
+ const params = { ref, argument };
1026
+ if (opts.context !== undefined)
1027
+ params.context = { arguments: opts.context };
1028
+ const result = await requestResult(url, "completion/complete", params, opts);
1029
+ if (isInputRequired(result) || isTaskHandle(result)) {
1030
+ throw new McpError(`MCP server ${url} sent ${result.resultType} for completion/complete`);
1031
+ }
1032
+ const parsed = CompletionResult(result);
1033
+ if (parsed instanceof type.errors) {
1034
+ throw new McpError(`MCP server ${url} sent a malformed completion/complete result: ${parsed.summary}`);
1035
+ }
277
1036
  return parsed;
278
1037
  }
1038
+ /** Extension methods and the listen stream are 2026-only. A missing session
1039
+ * still speaks 2026; a legacy session does not. */
1040
+ function asModern(opts) {
1041
+ if (opts.session === undefined) {
1042
+ return {
1043
+ ...opts,
1044
+ session: {
1045
+ protocolVersion: MCP_PROTOCOL_VERSION,
1046
+ clientCapabilities: clientCapabilities(opts),
1047
+ ...(opts.extensions !== undefined && opts.extensions.length > 0
1048
+ ? { extensions: opts.extensions }
1049
+ : {}),
1050
+ ...(opts.uiMimeTypes !== undefined
1051
+ ? { uiMimeTypes: opts.uiMimeTypes }
1052
+ : {}),
1053
+ },
1054
+ };
1055
+ }
1056
+ if (!isModernSession(opts.session)) {
1057
+ throw new McpError(`this method requires protocol ${MCP_PROTOCOL_VERSION}`);
1058
+ }
1059
+ return opts;
1060
+ }
1061
+ function subscriptionParams(filter) {
1062
+ const notifications = {};
1063
+ if (filter.toolsListChanged !== undefined) {
1064
+ notifications.toolsListChanged = filter.toolsListChanged;
1065
+ }
1066
+ if (filter.promptsListChanged !== undefined) {
1067
+ notifications.promptsListChanged = filter.promptsListChanged;
1068
+ }
1069
+ if (filter.resourcesListChanged !== undefined) {
1070
+ notifications.resourcesListChanged = filter.resourcesListChanged;
1071
+ }
1072
+ if (filter.resourceSubscriptions !== undefined) {
1073
+ notifications.resourceSubscriptions = [...filter.resourceSubscriptions];
1074
+ }
1075
+ if (filter.taskIds !== undefined)
1076
+ notifications.taskIds = [...filter.taskIds];
1077
+ return { notifications };
1078
+ }
1079
+ function asNotification(message) {
1080
+ const meta = message.params?._meta;
1081
+ let subscriptionId;
1082
+ if (typeof meta === "object" && meta !== null) {
1083
+ const id = meta[META_SUBSCRIPTION];
1084
+ if (typeof id === "string" || typeof id === "number")
1085
+ subscriptionId = id;
1086
+ }
1087
+ return {
1088
+ method: message.method,
1089
+ ...(message.params !== undefined ? { params: message.params } : {}),
1090
+ ...(subscriptionId !== undefined ? { subscriptionId } : {}),
1091
+ };
1092
+ }
1093
+ /**
1094
+ * `subscriptions/listen`. Resolves when the server closes the stream with a
1095
+ * result after acknowledgement, or rejects when `signal` aborts and closes
1096
+ * the stream. Long-lived by default; timeoutMs explicitly bounds its lifetime.
1097
+ */
1098
+ export async function mcpListen(url, filter, onNotification, opts = {}) {
1099
+ if (filter.taskIds !== undefined)
1100
+ requireExtension(opts, MCP_EXT_TASKS);
1101
+ const callOpts = asModern(opts);
1102
+ const fetchImpl = callOpts.fetch ?? fetch;
1103
+ const id = nextId++;
1104
+ const bodyParams = modernParams(subscriptionParams(filter), callOpts, id);
1105
+ validateRequest("subscriptions/listen", bodyParams, validationContext(callOpts));
1106
+ const progressToken = record(bodyParams._meta).progressToken;
1107
+ const timeout = callOpts.timeoutMs === undefined
1108
+ ? undefined
1109
+ : AbortSignal.timeout(callOpts.timeoutMs);
1110
+ const signals = [timeout, opts.signal].filter((s) => s !== undefined);
1111
+ const signal = signals.length > 0 ? AbortSignal.any(signals) : undefined;
1112
+ reserveProgressToken(progressToken);
1113
+ try {
1114
+ const response = await fetchImpl(url, {
1115
+ method: "POST",
1116
+ headers: requestHeaders(callOpts, "subscriptions/listen", undefined, {}),
1117
+ body: JSON.stringify({
1118
+ jsonrpc: "2.0",
1119
+ id,
1120
+ method: "subscriptions/listen",
1121
+ params: bodyParams,
1122
+ }),
1123
+ ...(signal !== undefined ? { signal } : {}),
1124
+ });
1125
+ if (!response.ok)
1126
+ throw await errorFrom(response, url, "subscriptions/listen", signal);
1127
+ const contentType = response.headers.get("content-type") ?? "";
1128
+ let acknowledged;
1129
+ const finish = (message) => {
1130
+ const parsed = JsonRpcResponse(message);
1131
+ if (parsed instanceof type.errors ||
1132
+ !isResponseTo(message, id) ||
1133
+ (parsed.error !== undefined && Object.hasOwn(parsed, "result"))) {
1134
+ throw new McpError(`MCP server ${url} sent a malformed response to subscriptions/listen`);
1135
+ }
1136
+ if (parsed.error !== undefined) {
1137
+ throw new McpError(`MCP server ${url} rejected subscriptions/listen: ${parsed.error.message}`, undefined, parsed.error.code, parsed.error.data, { source: "peer" });
1138
+ }
1139
+ if (acknowledged === undefined)
1140
+ throw new McpError("subscription result preceded acknowledgement");
1141
+ const result = validateResult("subscriptions/listen", parsed.result, validationContext(callOpts));
1142
+ const metadata = result._meta;
1143
+ if (metadata?.[META_SUBSCRIPTION] !== id)
1144
+ throw new McpError("subscription result has mismatched subscriptionId");
1145
+ };
1146
+ if (!contentType.includes("text/event-stream")) {
1147
+ finish(parseJson(await readCapped(response, signal)));
1148
+ return;
1149
+ }
1150
+ for await (const message of sseMessages(response, signal)) {
1151
+ if (isNotification(message)) {
1152
+ validateNotification(message.method, message.params, validationContext(callOpts));
1153
+ const notification = asNotification(message);
1154
+ if (notification.subscriptionId !== id)
1155
+ throw new McpError("subscription notification has mismatched subscriptionId");
1156
+ if (message.method === "notifications/subscriptions/acknowledged") {
1157
+ if (acknowledged !== undefined)
1158
+ throw new McpError("duplicate subscription acknowledgement");
1159
+ acknowledged = message.params?.notifications;
1160
+ for (const [key, value] of Object.entries(acknowledged)) {
1161
+ const requested = filter[key];
1162
+ if (Array.isArray(value)
1163
+ ? !Array.isArray(requested) ||
1164
+ value.some((item) => !requested.includes(item))
1165
+ : value === true && requested !== true)
1166
+ throw new McpError("subscription acknowledged an unrequested filter");
1167
+ }
1168
+ }
1169
+ else {
1170
+ if (acknowledged === undefined)
1171
+ throw new McpError("subscription notification preceded acknowledgement");
1172
+ const keys = {
1173
+ "notifications/tools/list_changed": "toolsListChanged",
1174
+ "notifications/prompts/list_changed": "promptsListChanged",
1175
+ "notifications/resources/list_changed": "resourcesListChanged",
1176
+ };
1177
+ const key = keys[message.method];
1178
+ const allowed = key !== undefined
1179
+ ? acknowledged[key] === true
1180
+ : message.method === "notifications/resources/updated"
1181
+ ? Array.isArray(acknowledged.resourceSubscriptions) &&
1182
+ acknowledged.resourceSubscriptions.includes(message.params?.uri)
1183
+ : message.method === "notifications/tasks"
1184
+ ? Array.isArray(acknowledged.taskIds) &&
1185
+ acknowledged.taskIds.includes(message.params?.taskId)
1186
+ : false;
1187
+ if (!allowed)
1188
+ throw new McpError("unrequested subscription notification");
1189
+ }
1190
+ onNotification(notification);
1191
+ continue;
1192
+ }
1193
+ if (isResponseTo(message, id)) {
1194
+ finish(message);
1195
+ return;
1196
+ }
1197
+ }
1198
+ throw new McpError("MCP server closed the event stream without a matching response");
1199
+ }
1200
+ catch (cause) {
1201
+ if (opts.signal?.aborted === true) {
1202
+ throw new McpError(`subscriptions/listen to MCP server ${url} was cancelled`, undefined, undefined, undefined, { cancelled: true });
1203
+ }
1204
+ if (timeout?.aborted === true) {
1205
+ throw new McpError(`MCP server ${url} did not answer subscriptions/listen within ${String(callOpts.timeoutMs)}ms`, undefined, undefined, undefined, { timedOut: true });
1206
+ }
1207
+ throw cause;
1208
+ }
1209
+ finally {
1210
+ if (progressToken !== undefined)
1211
+ activeProgressTokens.delete(progressToken);
1212
+ }
1213
+ }
1214
+ function parseTaskState(result, _url, _method) {
1215
+ return { ...validateTask(result, true), resultType: "complete" };
1216
+ }
1217
+ /** `tasks/get`. Requires the tasks extension. */
1218
+ export async function mcpGetTask(url, taskId, opts = {}) {
1219
+ requireExtension(opts, MCP_EXT_TASKS);
1220
+ const result = await requestResult(url, "tasks/get", { taskId }, asModern(opts));
1221
+ if (isInputRequired(result) || isTaskHandle(result)) {
1222
+ throw new McpError(`MCP server ${url} sent ${result.resultType} for tasks/get`);
1223
+ }
1224
+ return parseTaskState(result, url, "tasks/get");
1225
+ }
1226
+ /** `tasks/update`. Requires the tasks extension. */
1227
+ export async function mcpUpdateTask(url, taskId, inputResponses, opts = {}) {
1228
+ requireExtension(opts, MCP_EXT_TASKS);
1229
+ const result = await requestResult(url, "tasks/update", { taskId, inputResponses }, asModern(opts));
1230
+ if (isInputRequired(result) || isTaskHandle(result)) {
1231
+ throw new McpError(`MCP server ${url} sent ${result.resultType} for tasks/update`);
1232
+ }
1233
+ }
1234
+ /** `tasks/cancel`. Requires the tasks extension. Cooperative: the server only acknowledges. */
1235
+ export async function mcpCancelTask(url, taskId, opts = {}) {
1236
+ requireExtension(opts, MCP_EXT_TASKS);
1237
+ const result = await requestResult(url, "tasks/cancel", { taskId }, asModern(opts));
1238
+ if (isInputRequired(result) || isTaskHandle(result)) {
1239
+ throw new McpError(`MCP server ${url} sent ${result.resultType} for tasks/cancel`);
1240
+ }
1241
+ }
1242
+ function requireSkills(opts) {
1243
+ requireExtension(opts, MCP_EXT_SKILLS);
1244
+ const extensions = opts.session?.capabilities?.extensions;
1245
+ if (typeof extensions !== "object" ||
1246
+ extensions === null ||
1247
+ !Object.hasOwn(extensions, MCP_EXT_SKILLS)) {
1248
+ throw new McpError("server has not declared the skills extension; discover before calling skills methods");
1249
+ }
1250
+ }
1251
+ /** `skills/list`, following cursors. Requires the skills extension. */
1252
+ export async function mcpListSkills(url, opts = {}) {
1253
+ requireSkills(opts);
1254
+ const { pages, cache } = await eachPage(url, "skills/list", undefined, asModern(opts));
1255
+ const skills = [];
1256
+ for (const result of pages) {
1257
+ const parsed = type({ skills: SkillSchema.array() })(result);
1258
+ if (parsed instanceof type.errors) {
1259
+ throw new McpError(`MCP server ${url} sent a malformed skills/list result: ${parsed.summary}`);
1260
+ }
1261
+ skills.push(...parsed.skills);
1262
+ }
1263
+ return attachCache(skills, cache);
1264
+ }
1265
+ /** `skills/get`. Requires the skills extension. */
1266
+ export async function mcpGetSkill(url, uri, opts = {}) {
1267
+ requireSkills(opts);
1268
+ const result = await requestResult(url, "skills/get", { uri }, asModern(opts));
1269
+ if (isInputRequired(result) || isTaskHandle(result)) {
1270
+ throw new McpError(`MCP server ${url} sent ${result.resultType} for skills/get`);
1271
+ }
1272
+ const parsed = type({ skill: SkillSchema })(result);
1273
+ if (parsed instanceof type.errors) {
1274
+ throw new McpError(`MCP server ${url} sent a malformed skills/get result: ${parsed.summary}`);
1275
+ }
1276
+ const cache = readCache(result, url, "skills/get");
1277
+ return {
1278
+ resultType: "complete",
1279
+ skill: parsed.skill,
1280
+ ...(cache.ttlMs !== undefined ? { ttlMs: cache.ttlMs } : {}),
1281
+ ...(cache.cacheScope !== undefined ? { cacheScope: cache.cacheScope } : {}),
1282
+ };
1283
+ }
1284
+ function directoryReadEnabled(session) {
1285
+ const capabilities = session?.capabilities;
1286
+ if (typeof capabilities !== "object" || capabilities === null)
1287
+ return false;
1288
+ const extensions = capabilities.extensions;
1289
+ if (typeof extensions !== "object" || extensions === null)
1290
+ return false;
1291
+ const skills = extensions[MCP_EXT_SKILLS];
1292
+ if (typeof skills !== "object" || skills === null)
1293
+ return false;
1294
+ return skills.directoryRead === true;
1295
+ }
1296
+ /**
1297
+ * `resources/directory/read`. Sent only when the skills extension is enabled
1298
+ * and the server declared `directoryRead`.
1299
+ */
1300
+ export async function mcpReadResourceDirectory(url, uri, opts = {}) {
1301
+ requireSkills(opts);
1302
+ const modern = asModern(opts);
1303
+ if (!directoryReadEnabled(modern.session)) {
1304
+ throw new McpError(`MCP server did not declare ${MCP_EXT_SKILLS} directoryRead`);
1305
+ }
1306
+ const { pages, cache } = await eachPage(url, "resources/directory/read", { uri }, modern);
1307
+ const resources = [];
1308
+ for (const result of pages) {
1309
+ const parsed = type({ resources: ResourceSchema.array() })(result);
1310
+ if (parsed instanceof type.errors) {
1311
+ throw new McpError(`MCP server ${url} sent a malformed resources/directory/read result: ${parsed.summary}`);
1312
+ }
1313
+ resources.push(...parsed.resources);
1314
+ }
1315
+ return attachCache(resources, cache);
1316
+ }