@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.
- package/README.md +120 -26
- package/dist/auth/callback.d.ts +14 -0
- package/dist/auth/callback.js +79 -0
- package/dist/auth/challenge.d.ts +9 -0
- package/dist/auth/challenge.js +52 -0
- package/dist/auth/contracts.d.ts +18 -0
- package/dist/auth/contracts.js +19 -0
- package/dist/auth/http.d.ts +2 -0
- package/dist/auth/http.js +2 -0
- package/dist/auth/index.d.ts +6 -0
- package/dist/auth/index.js +6 -0
- package/dist/auth/login.d.ts +62 -0
- package/dist/auth/login.js +175 -0
- package/dist/auth/oauth-http.d.ts +6 -0
- package/dist/auth/oauth-http.js +32 -0
- package/dist/auth/resource-server.d.ts +25 -0
- package/dist/auth/resource-server.js +94 -0
- package/dist/auth/url-policy.d.ts +10 -0
- package/dist/auth/url-policy.js +35 -0
- package/dist/client/resource-budget.d.ts +1 -0
- package/dist/client/resource-budget.js +1 -0
- package/dist/client/transport.d.ts +10 -0
- package/dist/client/transport.js +1 -0
- package/dist/client.d.ts +208 -26
- package/dist/client.js +1128 -90
- package/dist/extensions/apps.d.ts +58 -0
- package/dist/extensions/apps.js +55 -0
- package/dist/extensions/elicitation.d.ts +18 -0
- package/dist/extensions/elicitation.js +55 -0
- package/dist/extensions/index.d.ts +5 -0
- package/dist/extensions/index.js +5 -0
- package/dist/extensions/maturity.d.ts +44 -0
- package/dist/extensions/maturity.js +24 -0
- package/dist/extensions/skills/contracts.d.ts +53 -0
- package/dist/extensions/skills/contracts.js +1 -0
- package/dist/extensions/skills/index.d.ts +52 -0
- package/dist/extensions/skills/index.js +402 -0
- package/dist/extensions/tasks/contracts.d.ts +39 -0
- package/dist/extensions/tasks/contracts.js +1 -0
- package/dist/extensions/tasks/file-store.d.ts +12 -0
- package/dist/extensions/tasks/file-store.js +152 -0
- package/dist/extensions/tasks/index.d.ts +82 -0
- package/dist/extensions/tasks/index.js +221 -0
- package/dist/extensions/tasks/record.d.ts +3 -0
- package/dist/extensions/tasks/record.js +37 -0
- package/dist/hub/discover.d.ts +5 -1
- package/dist/hub/discover.js +36 -13
- package/dist/index.d.ts +3 -2
- package/dist/index.js +4 -2
- package/dist/interchange/client/capabilities.d.ts +4 -0
- package/dist/interchange/client/capabilities.js +53 -0
- package/dist/interchange/client/catalog.d.ts +14 -0
- package/dist/interchange/client/catalog.js +52 -0
- package/dist/interchange/client/contracts.d.ts +47 -0
- package/dist/interchange/client/contracts.js +1 -0
- package/dist/interchange/client/projection.d.ts +6 -0
- package/dist/interchange/client/projection.js +92 -0
- package/dist/interchange/client/skills.d.ts +16 -0
- package/dist/interchange/client/skills.js +87 -0
- package/dist/interchange/client.d.ts +8 -0
- package/dist/interchange/client.js +6 -0
- package/dist/interchange/server/contracts.d.ts +65 -0
- package/dist/interchange/server/contracts.js +1 -0
- package/dist/interchange/server.d.ts +5 -0
- package/dist/interchange/server.js +172 -0
- package/dist/naming.d.ts +2 -0
- package/dist/naming.js +7 -0
- package/dist/oauth-discovery.d.ts +107 -1
- package/dist/oauth-discovery.js +292 -56
- package/dist/protocol/content.d.ts +35 -0
- package/dist/protocol/content.js +64 -0
- package/dist/protocol/envelopes.d.ts +14 -0
- package/dist/protocol/envelopes.js +22 -0
- package/dist/protocol/errors.d.ts +26 -0
- package/dist/protocol/errors.js +31 -0
- package/dist/protocol/extensions.d.ts +34 -0
- package/dist/protocol/extensions.js +86 -0
- package/dist/protocol/headers.d.ts +12 -0
- package/dist/protocol/headers.js +128 -0
- package/dist/protocol/index.d.ts +11 -0
- package/dist/protocol/index.js +11 -0
- package/dist/protocol/json-schema.d.ts +8 -0
- package/dist/protocol/json-schema.js +152 -0
- package/dist/protocol/legacy.d.ts +12 -0
- package/dist/protocol/legacy.js +18 -0
- package/dist/protocol/methods.d.ts +116 -0
- package/dist/protocol/methods.js +78 -0
- package/dist/protocol/resource-budget.d.ts +9 -0
- package/dist/protocol/resource-budget.js +206 -0
- package/dist/protocol/schemas.d.ts +115 -0
- package/dist/protocol/schemas.js +553 -0
- package/dist/protocol/types.d.ts +295 -0
- package/dist/protocol/types.js +3 -0
- package/dist/protocol/ui.d.ts +3 -0
- package/dist/protocol/ui.js +18 -0
- package/dist/server/contracts.d.ts +67 -0
- package/dist/server/contracts.js +1 -0
- package/dist/server/errors.d.ts +6 -0
- package/dist/server/errors.js +21 -0
- package/dist/server/extensions.d.ts +7 -0
- package/dist/server/extensions.js +61 -0
- package/dist/server/fixtures.d.ts +2 -0
- package/dist/server/fixtures.js +34 -0
- package/dist/server/legacy.d.ts +9 -0
- package/dist/server/legacy.js +435 -0
- package/dist/server/tools.d.ts +17 -0
- package/dist/server/tools.js +93 -0
- package/dist/server.d.ts +14 -0
- package/dist/server.js +406 -0
- package/dist/sidecar-bundle.d.ts +7 -7
- package/dist/sidecar-bundle.js +36 -29
- package/dist/test-server.js +12 -3
- package/dist/tool.d.ts +8 -0
- package/dist/tool.js +35 -19
- package/dist/url.js +8 -3
- package/docs/conformance.md +135 -0
- package/docs/integration-status.md +98 -0
- package/docs/legacy-server.md +58 -0
- package/docs/migration.md +143 -0
- package/docs/public-api.md +106 -0
- package/examples/README.md +28 -0
- package/examples/standalone-client.ts +14 -0
- package/examples/standalone-server.ts +37 -0
- package/package.json +40 -5
package/dist/client.js
CHANGED
|
@@ -1,40 +1,135 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
69
|
-
|
|
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:
|
|
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
|
-
...(
|
|
308
|
+
...(bodyParams !== undefined ? { params: bodyParams } : {}),
|
|
84
309
|
}),
|
|
85
310
|
...(signal !== undefined ? { signal } : {}),
|
|
86
311
|
});
|
|
87
312
|
if (!response.ok) {
|
|
88
|
-
await response
|
|
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
|
-
|
|
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(
|
|
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 ||
|
|
108
|
-
|
|
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
|
|
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
|
-
:
|
|
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:
|
|
126
|
-
body: JSON.stringify({
|
|
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
|
-
|
|
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
|
-
|
|
506
|
+
yield JSON.parse(dataLines.join("\n"));
|
|
208
507
|
}
|
|
209
508
|
catch {
|
|
210
|
-
|
|
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
|
|
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
|
|
544
|
+
protocolVersion,
|
|
227
545
|
capabilities: {},
|
|
228
|
-
clientInfo: { name: "@corbits/mcp", version: "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 ??
|
|
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 {
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
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(
|
|
261
|
-
if (names.size !==
|
|
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
|
|
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
|
|
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
|
+
}
|