@orkestrel/mcp 0.0.8 → 0.0.9
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/dist/src/browser/index.d.ts +25 -11
- package/dist/src/browser/index.js +43 -17
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/core/index.cjs +1262 -122
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +1097 -168
- package/dist/src/core/index.d.ts +1097 -168
- package/dist/src/core/index.js +1219 -120
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +360 -67
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +200 -32
- package/dist/src/server/index.d.ts +200 -32
- package/dist/src/server/index.js +351 -69
- package/dist/src/server/index.js.map +1 -1
- package/package.json +3 -3
package/dist/src/core/index.cjs
CHANGED
|
@@ -1,21 +1,81 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
let _orkestrel_contract = require("@orkestrel/contract");
|
|
3
3
|
let _orkestrel_emitter = require("@orkestrel/emitter");
|
|
4
|
+
let _orkestrel_server = require("@orkestrel/server");
|
|
4
5
|
let _orkestrel_tool = require("@orkestrel/tool");
|
|
5
6
|
//#region src/core/constants.ts
|
|
6
|
-
/**
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* The revision offered and defaulted to in the legacy `initialize` handshake.
|
|
9
|
+
*
|
|
10
|
+
* @remarks
|
|
11
|
+
* This is deliberately a legacy revision, and the newest one supported. 2026-07-28 is stateless
|
|
12
|
+
* and defines no `initialize`, so it can never be the handshake's version — a client that offers
|
|
13
|
+
* it is asking to negotiate a revision with no negotiation.
|
|
14
|
+
*/
|
|
15
|
+
var MCP_PROTOCOL_VERSION = "2025-11-25";
|
|
16
|
+
/** The legacy fallback anchor used when an initialize request cannot be accepted as modern. */
|
|
17
|
+
var MCP_LEGACY_VERSION = "2025-06-18";
|
|
18
|
+
/** The modern revision offered by an unpinned client during discovery. */
|
|
19
|
+
var MCP_MODERN_VERSION = "2026-07-28";
|
|
8
20
|
/**
|
|
9
21
|
* The MCP protocol revisions this server can negotiate.
|
|
10
22
|
*
|
|
11
23
|
* @remarks
|
|
12
24
|
* `initialize` echoes the client's requested `protocolVersion` when it appears in
|
|
13
|
-
* this list
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
|
|
25
|
+
* this list. Frozen in client-preference and discovery-advertisement order. The
|
|
26
|
+
* package does not advertise `2025-03-26` because that revision mandates JSON-RPC
|
|
27
|
+
* batching, while this package accepts only individual JSON-RPC messages.
|
|
28
|
+
*/
|
|
29
|
+
var SUPPORTED_PROTOCOL_VERSIONS = Object.freeze([
|
|
30
|
+
"2026-07-28",
|
|
31
|
+
"2025-11-25",
|
|
32
|
+
"2025-06-18"
|
|
33
|
+
]);
|
|
34
|
+
/** Reserved modern `_meta` key carrying the request's protocol revision. */
|
|
35
|
+
var MCP_META_VERSION = "io.modelcontextprotocol/protocolVersion";
|
|
36
|
+
/** Reserved modern `_meta` key carrying the client's open capability record. */
|
|
37
|
+
var MCP_META_CAPABILITIES = "io.modelcontextprotocol/clientCapabilities";
|
|
38
|
+
/** Reserved modern `_meta` key carrying the optional client identity. */
|
|
39
|
+
var MCP_META_CLIENT = "io.modelcontextprotocol/clientInfo";
|
|
40
|
+
/** Reserved modern `_meta` key carrying the server identity on results. */
|
|
41
|
+
var MCP_META_SERVER = "io.modelcontextprotocol/serverInfo";
|
|
42
|
+
/** Reserved modern `_meta` key carrying a `subscriptions/listen` request id. */
|
|
43
|
+
var MCP_META_SUBSCRIPTION = "io.modelcontextprotocol/subscriptionId";
|
|
44
|
+
/** MCP reserved error: required HTTP metadata does not match the request body. */
|
|
45
|
+
var MCP_HEADER_MISMATCH = -32020;
|
|
46
|
+
/** MCP reserved error: an operation needs a client capability that was not declared. */
|
|
47
|
+
var MCP_MISSING_CAPABILITY = -32021;
|
|
48
|
+
/** MCP reserved error: a request names an unsupported protocol revision. */
|
|
49
|
+
var MCP_UNSUPPORTED_VERSION = -32022;
|
|
50
|
+
/**
|
|
51
|
+
* Default modern result freshness lifetime in milliseconds.
|
|
52
|
+
*
|
|
53
|
+
* @remarks
|
|
54
|
+
* `ttlMs` is required on cacheable results, while zero means immediately stale
|
|
55
|
+
* rather than uncached, so the neutral usable default is one minute.
|
|
56
|
+
*/
|
|
57
|
+
var DEFAULT_MCP_CACHE_TTL = 6e4;
|
|
58
|
+
/**
|
|
59
|
+
* Secure server bounds used when the matching `limit` option leaf is absent or malformed.
|
|
60
|
+
*
|
|
61
|
+
* @remarks
|
|
62
|
+
* One MiB admits ordinary JSON-RPC requests and substantial tool arguments; 16 KiB admits
|
|
63
|
+
* extension-rich modern metadata and signed multi-round state; four MiB admits substantial
|
|
64
|
+
* JSON tool output without allowing an unconfigured service to serialize arbitrary process
|
|
65
|
+
* memory; 64 metadata keys admits the reserved keys plus many extensions; 128 concurrent
|
|
66
|
+
* streams admits a busy service while bounding retained producers; depth 32 admits ordinary
|
|
67
|
+
* JSON documents while rejecting stack-hostile nesting. Frozen so callers cannot alter the
|
|
68
|
+
* defaults observed by later servers.
|
|
17
69
|
*/
|
|
18
|
-
var
|
|
70
|
+
var DEFAULT_MCP_LIMITS = Object.freeze({
|
|
71
|
+
message: 1048576,
|
|
72
|
+
metadata: 16384,
|
|
73
|
+
keys: 64,
|
|
74
|
+
state: 16384,
|
|
75
|
+
content: 4194304,
|
|
76
|
+
subscriptions: 128,
|
|
77
|
+
depth: 32
|
|
78
|
+
});
|
|
19
79
|
/** JSON-RPC 2.0 reserved error: invalid JSON was received (the message did not parse). */
|
|
20
80
|
var JSONRPC_PARSE_ERROR = -32700;
|
|
21
81
|
/** JSON-RPC 2.0 reserved error: the payload was not a valid Request object. */
|
|
@@ -35,6 +95,8 @@ var DEFAULT_MCP_CLIENT_VERSION = "1.0.0";
|
|
|
35
95
|
* is unset — a request the remote server does not answer within it rejects.
|
|
36
96
|
*/
|
|
37
97
|
var DEFAULT_MCP_REQUEST_TIMEOUT = 3e4;
|
|
98
|
+
/** The maximum discovery-probe deadline used when a client deadline is configured. */
|
|
99
|
+
var DEFAULT_MCP_PROBE_TIMEOUT = 50;
|
|
38
100
|
//#endregion
|
|
39
101
|
//#region src/core/errors.ts
|
|
40
102
|
/**
|
|
@@ -45,13 +107,19 @@ var DEFAULT_MCP_REQUEST_TIMEOUT = 3e4;
|
|
|
45
107
|
* {@link MCPClient} throws this error only for a remote JSON-RPC `error` response.
|
|
46
108
|
* Local lifecycle and transport conditions such as disconnects and request timeouts
|
|
47
109
|
* remain plain `Error`s. `context` carries the response's optional `error.data`
|
|
48
|
-
* unchanged and is `undefined` when the peer omitted it.
|
|
110
|
+
* unchanged and is `undefined` when the peer omitted it. This includes the modern
|
|
111
|
+
* reserved paths: `-32020` carries no context, `-32021` may carry
|
|
112
|
+
* `requiredCapabilities`, and `-32022` carries the peer's `supported` revisions and
|
|
113
|
+
* `requested` revision for negotiation recovery.
|
|
49
114
|
*
|
|
50
115
|
* @example
|
|
51
116
|
* ```ts
|
|
52
|
-
* const error = new MCPError('
|
|
53
|
-
*
|
|
54
|
-
*
|
|
117
|
+
* const error = new MCPError('Unsupported protocol version', -32022, {
|
|
118
|
+
* supported: ['2026-07-28'],
|
|
119
|
+
* requested: '2024-11-05',
|
|
120
|
+
* })
|
|
121
|
+
* error.code // -32022
|
|
122
|
+
* error.context // { supported: ['2026-07-28'], requested: '2024-11-05' }
|
|
55
123
|
* ```
|
|
56
124
|
*/
|
|
57
125
|
var MCPError = class extends Error {
|
|
@@ -93,6 +161,161 @@ function isMCPError(value) {
|
|
|
93
161
|
//#endregion
|
|
94
162
|
//#region src/core/validators.ts
|
|
95
163
|
/**
|
|
164
|
+
* Determine whether a value is a string within a UTF-8 byte bound.
|
|
165
|
+
*
|
|
166
|
+
* @param value - The unknown value to inspect
|
|
167
|
+
* @param bytes - The maximum accepted encoded bytes
|
|
168
|
+
* @returns `true` only for a string whose UTF-8 representation fits the bound
|
|
169
|
+
*
|
|
170
|
+
* @example
|
|
171
|
+
* ```ts
|
|
172
|
+
* isBoundedString('€', 3) // true
|
|
173
|
+
* isBoundedString('€', 2) // false
|
|
174
|
+
* ```
|
|
175
|
+
*/
|
|
176
|
+
function isBoundedString(value, bytes) {
|
|
177
|
+
if (!(0, _orkestrel_contract.isString)(value) || !Number.isFinite(bytes) || !Number.isInteger(bytes) || bytes < 0) return false;
|
|
178
|
+
let measured = 0;
|
|
179
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
180
|
+
const code = value.charCodeAt(index);
|
|
181
|
+
if (code <= 127) measured += 1;
|
|
182
|
+
else if (code <= 2047) measured += 2;
|
|
183
|
+
else if (code >= 55296 && code <= 56319) {
|
|
184
|
+
const next = value.charCodeAt(index + 1);
|
|
185
|
+
if (next >= 56320 && next <= 57343) {
|
|
186
|
+
measured += 4;
|
|
187
|
+
index += 1;
|
|
188
|
+
} else measured += 3;
|
|
189
|
+
} else measured += 3;
|
|
190
|
+
if (measured > bytes) return false;
|
|
191
|
+
}
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Determine whether a value is bounded, cycle-free JSON with safe property names.
|
|
196
|
+
*
|
|
197
|
+
* @remarks
|
|
198
|
+
* Traversal is iterative, ancestor-aware, and contained by {@link attempt}; deep input,
|
|
199
|
+
* cycles, accessors, hostile proxies, `Map`/`Set`, and the prototype-pollution keys
|
|
200
|
+
* `__proto__`, `constructor`, and `prototype` return `false` rather than throwing.
|
|
201
|
+
* The byte count matches `JSON.stringify` without first allocating the serialization.
|
|
202
|
+
*
|
|
203
|
+
* @param value - The unknown value to inspect
|
|
204
|
+
* @param limits - Serialized byte, optional key, and nesting-depth bounds
|
|
205
|
+
* @returns `true` only for safe JSON satisfying every bound
|
|
206
|
+
*
|
|
207
|
+
* @example
|
|
208
|
+
* ```ts
|
|
209
|
+
* isBoundedJSON({ ok: true }, { bytes: 16, keys: 1, depth: 1 }) // true
|
|
210
|
+
* ```
|
|
211
|
+
*/
|
|
212
|
+
function isBoundedJSON(value, limits) {
|
|
213
|
+
const outcome = (0, _orkestrel_contract.attempt)(() => {
|
|
214
|
+
const limit = (0, _orkestrel_contract.sanitizeBudget)(limits.bytes, 0);
|
|
215
|
+
const depth = (0, _orkestrel_contract.sanitizeBudget)(limits.depth, 0);
|
|
216
|
+
const breadth = limits.keys === void 0 ? void 0 : (0, _orkestrel_contract.sanitizeBudget)(limits.keys, 0);
|
|
217
|
+
let bytes = 0;
|
|
218
|
+
let keys = 0;
|
|
219
|
+
const ancestors = /* @__PURE__ */ new WeakSet();
|
|
220
|
+
const pending = [{
|
|
221
|
+
value,
|
|
222
|
+
depth: 0,
|
|
223
|
+
closing: false
|
|
224
|
+
}];
|
|
225
|
+
while (pending.length > 0) {
|
|
226
|
+
const frame = pending.pop();
|
|
227
|
+
if (frame === void 0) return false;
|
|
228
|
+
const entry = frame.value;
|
|
229
|
+
if (frame.closing) {
|
|
230
|
+
if (typeof entry !== "object" || entry === null) return false;
|
|
231
|
+
ancestors.delete(entry);
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (frame.depth > depth) return false;
|
|
235
|
+
if (entry === null) bytes += 4;
|
|
236
|
+
else if ((0, _orkestrel_contract.isBoolean)(entry)) bytes += entry ? 4 : 5;
|
|
237
|
+
else if ((0, _orkestrel_contract.isNumber)(entry)) bytes += Number.isFinite(entry) ? String(entry).length : 4;
|
|
238
|
+
else if ((0, _orkestrel_contract.isString)(entry)) {
|
|
239
|
+
bytes += 2;
|
|
240
|
+
if (bytes > limit) return false;
|
|
241
|
+
for (let index = 0; index < entry.length; index += 1) {
|
|
242
|
+
const code = entry.charCodeAt(index);
|
|
243
|
+
if (code === 34 || code === 92 || code === 8 || code === 9 || code === 10 || code === 12 || code === 13) bytes += 2;
|
|
244
|
+
else if (code <= 31) bytes += 6;
|
|
245
|
+
else if (code <= 127) bytes += 1;
|
|
246
|
+
else if (code <= 2047) bytes += 2;
|
|
247
|
+
else if (code >= 55296 && code <= 56319) {
|
|
248
|
+
const next = entry.charCodeAt(index + 1);
|
|
249
|
+
if (next >= 56320 && next <= 57343) {
|
|
250
|
+
bytes += 4;
|
|
251
|
+
index += 1;
|
|
252
|
+
} else bytes += 6;
|
|
253
|
+
} else if (code >= 56320 && code <= 57343) bytes += 6;
|
|
254
|
+
else bytes += 3;
|
|
255
|
+
if (bytes > limit) return false;
|
|
256
|
+
}
|
|
257
|
+
continue;
|
|
258
|
+
} else if (typeof entry === "object") {
|
|
259
|
+
if (ancestors.has(entry)) return false;
|
|
260
|
+
const names = (0, _orkestrel_contract.enumerableKeys)(entry);
|
|
261
|
+
if (names === void 0) return false;
|
|
262
|
+
for (const name of names) if (name === "__proto__" || name === "constructor" || name === "prototype") return false;
|
|
263
|
+
if (Array.isArray(entry)) {
|
|
264
|
+
bytes += 2 + Math.max(0, entry.length - 1);
|
|
265
|
+
if (bytes > limit || names.length !== entry.length) return false;
|
|
266
|
+
ancestors.add(entry);
|
|
267
|
+
pending.push({
|
|
268
|
+
value: entry,
|
|
269
|
+
depth: frame.depth,
|
|
270
|
+
closing: true
|
|
271
|
+
});
|
|
272
|
+
for (let index = entry.length - 1; index >= 0; index -= 1) {
|
|
273
|
+
const descriptor = Object.getOwnPropertyDescriptor(entry, String(index));
|
|
274
|
+
if (descriptor === void 0 || !Object.hasOwn(descriptor, "value")) return false;
|
|
275
|
+
pending.push({
|
|
276
|
+
value: descriptor.value,
|
|
277
|
+
depth: frame.depth + 1,
|
|
278
|
+
closing: false
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
if (!(0, _orkestrel_contract.isRecord)(entry)) return false;
|
|
284
|
+
keys += names.length;
|
|
285
|
+
if (breadth !== void 0 && keys > breadth) return false;
|
|
286
|
+
bytes += 2 + Math.max(0, names.length - 1) + names.length;
|
|
287
|
+
if (bytes > limit) return false;
|
|
288
|
+
ancestors.add(entry);
|
|
289
|
+
pending.push({
|
|
290
|
+
value: entry,
|
|
291
|
+
depth: frame.depth,
|
|
292
|
+
closing: true
|
|
293
|
+
});
|
|
294
|
+
for (let index = names.length - 1; index >= 0; index -= 1) {
|
|
295
|
+
const name = names[index];
|
|
296
|
+
if (name === void 0) return false;
|
|
297
|
+
const descriptor = Object.getOwnPropertyDescriptor(entry, name);
|
|
298
|
+
if (descriptor === void 0 || !Object.hasOwn(descriptor, "value")) return false;
|
|
299
|
+
pending.push({
|
|
300
|
+
value: descriptor.value,
|
|
301
|
+
depth: frame.depth + 1,
|
|
302
|
+
closing: false
|
|
303
|
+
});
|
|
304
|
+
pending.push({
|
|
305
|
+
value: name,
|
|
306
|
+
depth: frame.depth,
|
|
307
|
+
closing: false
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
continue;
|
|
311
|
+
} else return false;
|
|
312
|
+
if (bytes > limit) return false;
|
|
313
|
+
}
|
|
314
|
+
return bytes <= limit;
|
|
315
|
+
});
|
|
316
|
+
return outcome.success && outcome.value;
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
96
319
|
* Determine whether a value is a valid JSON-RPC REQUEST `id` — a string, a number,
|
|
97
320
|
* or absent.
|
|
98
321
|
*
|
|
@@ -116,6 +339,263 @@ function isRequestId(value) {
|
|
|
116
339
|
return (0, _orkestrel_contract.isUndefined)(value) || (0, _orkestrel_contract.isString)(value) || (0, _orkestrel_contract.isNumber)(value);
|
|
117
340
|
}
|
|
118
341
|
/**
|
|
342
|
+
* Determine whether a value is a supported {@link MCPVersion}.
|
|
343
|
+
*
|
|
344
|
+
* @param value - The unknown value to inspect
|
|
345
|
+
* @returns `true` when the value is one of {@link SUPPORTED_PROTOCOL_VERSIONS}
|
|
346
|
+
*/
|
|
347
|
+
function isMCPVersion(value) {
|
|
348
|
+
return (0, _orkestrel_contract.isString)(value) && SUPPORTED_PROTOCOL_VERSIONS.some((version) => version === value);
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Determine whether a value is an MCP {@link SubscriptionFilter}.
|
|
352
|
+
*
|
|
353
|
+
* @remarks
|
|
354
|
+
* Every filter field is optional. Boolean notification families accept only booleans, and
|
|
355
|
+
* `resourceSubscriptions` accepts only an array of string URIs. Unknown fields remain open
|
|
356
|
+
* for protocol extensions and are ignored by the built-in subscription matcher. Total over
|
|
357
|
+
* hostile input.
|
|
358
|
+
*
|
|
359
|
+
* @param value - The unknown value to inspect
|
|
360
|
+
* @returns `true` when every recognized filter field has its protocol shape
|
|
361
|
+
*/
|
|
362
|
+
function isSubscriptionFilter(value) {
|
|
363
|
+
if (!(0, _orkestrel_contract.isRecord)(value)) return false;
|
|
364
|
+
const tools = value["toolsListChanged"];
|
|
365
|
+
if (!(0, _orkestrel_contract.isUndefined)(tools) && !(0, _orkestrel_contract.isBoolean)(tools)) return false;
|
|
366
|
+
const prompts = value["promptsListChanged"];
|
|
367
|
+
if (!(0, _orkestrel_contract.isUndefined)(prompts) && !(0, _orkestrel_contract.isBoolean)(prompts)) return false;
|
|
368
|
+
const resources = value["resourcesListChanged"];
|
|
369
|
+
if (!(0, _orkestrel_contract.isUndefined)(resources) && !(0, _orkestrel_contract.isBoolean)(resources)) return false;
|
|
370
|
+
const subscriptions = value["resourceSubscriptions"];
|
|
371
|
+
return (0, _orkestrel_contract.isUndefined)(subscriptions) || (0, _orkestrel_contract.arrayOf)(_orkestrel_contract.isString)(subscriptions);
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Determine whether a client capability record declares form-mode elicitation.
|
|
375
|
+
*
|
|
376
|
+
* @remarks
|
|
377
|
+
* The protocol's empty `elicitation` object is the implicit form-only declaration.
|
|
378
|
+
* A non-empty declaration must carry a record-valued `form` member; URL-only support
|
|
379
|
+
* does not authorize a form request. Total over hostile input.
|
|
380
|
+
*
|
|
381
|
+
* @param value - The client capability record to inspect
|
|
382
|
+
* @returns `true` when form-mode elicitation is declared
|
|
383
|
+
*
|
|
384
|
+
* @example
|
|
385
|
+
* ```ts
|
|
386
|
+
* isFormElicitationSupported({ elicitation: {} }) // true — implicit form mode
|
|
387
|
+
* isFormElicitationSupported({ elicitation: { url: {} } }) // false
|
|
388
|
+
* ```
|
|
389
|
+
*/
|
|
390
|
+
function isFormElicitationSupported(value) {
|
|
391
|
+
try {
|
|
392
|
+
if (!(0, _orkestrel_contract.isRecord)(value)) return false;
|
|
393
|
+
const elicitation = value["elicitation"];
|
|
394
|
+
if (!(0, _orkestrel_contract.isRecord)(elicitation)) return false;
|
|
395
|
+
if ((0, _orkestrel_contract.isRecord)(elicitation["form"])) return true;
|
|
396
|
+
return Object.keys(elicitation).length === 0;
|
|
397
|
+
} catch {
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Determine whether a value is one restricted primitive form-elicitation schema.
|
|
403
|
+
*
|
|
404
|
+
* @param value - The unknown value to inspect
|
|
405
|
+
* @returns `true` for a supported boolean, numeric, string, or string-array schema
|
|
406
|
+
*
|
|
407
|
+
* @example
|
|
408
|
+
* ```ts
|
|
409
|
+
* isElicitPrimitiveSchema({ type: 'boolean', default: true }) // true
|
|
410
|
+
* isElicitPrimitiveSchema({ type: 'object' }) // false
|
|
411
|
+
* ```
|
|
412
|
+
*/
|
|
413
|
+
function isElicitPrimitiveSchema(value) {
|
|
414
|
+
try {
|
|
415
|
+
if (!(0, _orkestrel_contract.isRecord)(value)) return false;
|
|
416
|
+
const title = value["title"];
|
|
417
|
+
const description = value["description"];
|
|
418
|
+
if (!(0, _orkestrel_contract.isUndefined)(title) && !(0, _orkestrel_contract.isString)(title)) return false;
|
|
419
|
+
if (!(0, _orkestrel_contract.isUndefined)(description) && !(0, _orkestrel_contract.isString)(description)) return false;
|
|
420
|
+
const fallback = value["default"];
|
|
421
|
+
if (value["type"] === "boolean") return (0, _orkestrel_contract.isUndefined)(fallback) || (0, _orkestrel_contract.isBoolean)(fallback);
|
|
422
|
+
if (value["type"] === "number" || value["type"] === "integer") {
|
|
423
|
+
const minimum = value["minimum"];
|
|
424
|
+
const maximum = value["maximum"];
|
|
425
|
+
return ((0, _orkestrel_contract.isUndefined)(minimum) || (0, _orkestrel_contract.isNumber)(minimum)) && ((0, _orkestrel_contract.isUndefined)(maximum) || (0, _orkestrel_contract.isNumber)(maximum)) && ((0, _orkestrel_contract.isUndefined)(fallback) || (0, _orkestrel_contract.isNumber)(fallback));
|
|
426
|
+
}
|
|
427
|
+
if (value["type"] === "string") {
|
|
428
|
+
const minimum = value["minLength"];
|
|
429
|
+
const maximum = value["maxLength"];
|
|
430
|
+
const format = value["format"];
|
|
431
|
+
const choices = value["enum"];
|
|
432
|
+
const names = value["enumNames"];
|
|
433
|
+
const titled = value["oneOf"];
|
|
434
|
+
return ((0, _orkestrel_contract.isUndefined)(minimum) || (0, _orkestrel_contract.isNumber)(minimum)) && ((0, _orkestrel_contract.isUndefined)(maximum) || (0, _orkestrel_contract.isNumber)(maximum)) && ((0, _orkestrel_contract.isUndefined)(format) || format === "uri" || format === "email" || format === "date" || format === "date-time") && ((0, _orkestrel_contract.isUndefined)(fallback) || (0, _orkestrel_contract.isString)(fallback)) && ((0, _orkestrel_contract.isUndefined)(choices) || (0, _orkestrel_contract.arrayOf)(_orkestrel_contract.isString)(choices)) && ((0, _orkestrel_contract.isUndefined)(names) || (0, _orkestrel_contract.arrayOf)(_orkestrel_contract.isString)(names)) && ((0, _orkestrel_contract.isUndefined)(titled) || (0, _orkestrel_contract.arrayOf)(_orkestrel_contract.isRecord)(titled) && titled.every((choice) => (0, _orkestrel_contract.isString)(choice["const"]) && (0, _orkestrel_contract.isString)(choice["title"])));
|
|
435
|
+
}
|
|
436
|
+
if (value["type"] !== "array") return false;
|
|
437
|
+
const minimum = value["minItems"];
|
|
438
|
+
const maximum = value["maxItems"];
|
|
439
|
+
const items = value["items"];
|
|
440
|
+
if (!(0, _orkestrel_contract.isUndefined)(minimum) && !(0, _orkestrel_contract.isNumber)(minimum) || !(0, _orkestrel_contract.isUndefined)(maximum) && !(0, _orkestrel_contract.isNumber)(maximum) || !(0, _orkestrel_contract.isUndefined)(fallback) && !(0, _orkestrel_contract.arrayOf)(_orkestrel_contract.isString)(fallback) || !(0, _orkestrel_contract.isRecord)(items)) return false;
|
|
441
|
+
if (items["type"] === "string") return (0, _orkestrel_contract.arrayOf)(_orkestrel_contract.isString)(items["enum"]);
|
|
442
|
+
const choices = items["anyOf"];
|
|
443
|
+
return (0, _orkestrel_contract.arrayOf)(_orkestrel_contract.isRecord)(choices) && choices.every((choice) => (0, _orkestrel_contract.isString)(choice["const"]) && (0, _orkestrel_contract.isString)(choice["title"]));
|
|
444
|
+
} catch {
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Determine whether a value is a form-mode elicitation parameter object.
|
|
450
|
+
*
|
|
451
|
+
* @param value - The unknown value to inspect
|
|
452
|
+
* @returns `true` when `value` has the restricted form elicitation shape
|
|
453
|
+
*
|
|
454
|
+
* @example
|
|
455
|
+
* ```ts
|
|
456
|
+
* isElicitRequestFormParams({
|
|
457
|
+
* message: 'Continue?',
|
|
458
|
+
* requestedSchema: { type: 'object', properties: {} },
|
|
459
|
+
* }) // true
|
|
460
|
+
* ```
|
|
461
|
+
*/
|
|
462
|
+
function isElicitRequestFormParams(value) {
|
|
463
|
+
try {
|
|
464
|
+
if (!(0, _orkestrel_contract.isRecord)(value)) return false;
|
|
465
|
+
const mode = value["mode"];
|
|
466
|
+
if (!(0, _orkestrel_contract.isUndefined)(mode) && mode !== "form") return false;
|
|
467
|
+
if (!(0, _orkestrel_contract.isString)(value["message"])) return false;
|
|
468
|
+
const schema = value["requestedSchema"];
|
|
469
|
+
if (!(0, _orkestrel_contract.isRecord)(schema) || schema["type"] !== "object" || !(0, _orkestrel_contract.isRecord)(schema["properties"])) return false;
|
|
470
|
+
const dialect = schema["$schema"];
|
|
471
|
+
if (!(0, _orkestrel_contract.isUndefined)(dialect) && !(0, _orkestrel_contract.isString)(dialect)) return false;
|
|
472
|
+
const required = schema["required"];
|
|
473
|
+
return ((0, _orkestrel_contract.isUndefined)(required) || (0, _orkestrel_contract.arrayOf)(_orkestrel_contract.isString)(required)) && Object.values(schema["properties"]).every((property) => isElicitPrimitiveSchema(property));
|
|
474
|
+
} catch {
|
|
475
|
+
return false;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Determine whether a value is a URL-mode elicitation parameter object.
|
|
480
|
+
*
|
|
481
|
+
* @param value - The unknown value to inspect
|
|
482
|
+
* @returns `true` when `value` has the URL elicitation shape
|
|
483
|
+
*
|
|
484
|
+
* @example
|
|
485
|
+
* ```ts
|
|
486
|
+
* isElicitRequestURLParams({ mode: 'url', message: 'Authenticate', url: 'https://example.test' })
|
|
487
|
+
* ```
|
|
488
|
+
*/
|
|
489
|
+
function isElicitRequestURLParams(value) {
|
|
490
|
+
return (0, _orkestrel_contract.isRecord)(value) && value["mode"] === "url" && (0, _orkestrel_contract.isString)(value["message"]) && (0, _orkestrel_contract.isString)(value["url"]);
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Determine whether a value is an embedded `elicitation/create` request.
|
|
494
|
+
*
|
|
495
|
+
* @param value - The unknown value to inspect
|
|
496
|
+
* @returns `true` when `value` is a form- or URL-mode elicitation request
|
|
497
|
+
*
|
|
498
|
+
* @example
|
|
499
|
+
* ```ts
|
|
500
|
+
* isElicitRequest({
|
|
501
|
+
* method: 'elicitation/create',
|
|
502
|
+
* params: { message: 'Continue?', requestedSchema: { type: 'object', properties: {} } },
|
|
503
|
+
* }) // true
|
|
504
|
+
* ```
|
|
505
|
+
*/
|
|
506
|
+
function isElicitRequest(value) {
|
|
507
|
+
if (!(0, _orkestrel_contract.isRecord)(value) || value["method"] !== "elicitation/create") return false;
|
|
508
|
+
return isElicitRequestFormParams(value["params"]) || isElicitRequestURLParams(value["params"]);
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Determine whether a value is one legal embedded multi-round-trip request.
|
|
512
|
+
*
|
|
513
|
+
* @param value - The unknown value to inspect
|
|
514
|
+
* @returns `true` for elicitation, deprecated sampling, or deprecated roots requests
|
|
515
|
+
*
|
|
516
|
+
* @example
|
|
517
|
+
* ```ts
|
|
518
|
+
* isInputRequest({ method: 'roots/list' }) // true — legal but not produced by this package
|
|
519
|
+
* ```
|
|
520
|
+
*/
|
|
521
|
+
function isInputRequest(value) {
|
|
522
|
+
if (isElicitRequest(value)) return true;
|
|
523
|
+
if (!(0, _orkestrel_contract.isRecord)(value)) return false;
|
|
524
|
+
const params = value["params"];
|
|
525
|
+
if (value["method"] === "sampling/createMessage") return (0, _orkestrel_contract.isRecord)(params);
|
|
526
|
+
return value["method"] === "roots/list" && ((0, _orkestrel_contract.isUndefined)(params) || (0, _orkestrel_contract.isRecord)(params));
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Determine whether a value is a server-keyed map of embedded input requests.
|
|
530
|
+
*
|
|
531
|
+
* @param value - The unknown value to inspect
|
|
532
|
+
* @returns `true` when every own value is a legal {@link InputRequest}
|
|
533
|
+
*
|
|
534
|
+
* @example
|
|
535
|
+
* ```ts
|
|
536
|
+
* isInputRequests({ confirm: { method: 'roots/list' } }) // true; maps, never arrays
|
|
537
|
+
* ```
|
|
538
|
+
*/
|
|
539
|
+
function isInputRequests(value) {
|
|
540
|
+
try {
|
|
541
|
+
return (0, _orkestrel_contract.isRecord)(value) && Object.values(value).every((request) => isInputRequest(request));
|
|
542
|
+
} catch {
|
|
543
|
+
return false;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Determine whether a value is one elicitation response.
|
|
548
|
+
*
|
|
549
|
+
* @param value - The unknown value to inspect
|
|
550
|
+
* @returns `true` when action/content have the protocol shape
|
|
551
|
+
*
|
|
552
|
+
* @example
|
|
553
|
+
* ```ts
|
|
554
|
+
* isElicitResult({ action: 'accept', content: { approved: true } }) // true
|
|
555
|
+
* ```
|
|
556
|
+
*/
|
|
557
|
+
function isElicitResult(value) {
|
|
558
|
+
try {
|
|
559
|
+
if (!(0, _orkestrel_contract.isRecord)(value)) return false;
|
|
560
|
+
const action = value["action"];
|
|
561
|
+
if (action !== "accept" && action !== "decline" && action !== "cancel") return false;
|
|
562
|
+
const content = value["content"];
|
|
563
|
+
if ((0, _orkestrel_contract.isUndefined)(content)) return true;
|
|
564
|
+
if (!(0, _orkestrel_contract.isRecord)(content)) return false;
|
|
565
|
+
return Object.values(content).every((item) => (0, _orkestrel_contract.isString)(item) || (0, _orkestrel_contract.isNumber)(item) || (0, _orkestrel_contract.isBoolean)(item) || (0, _orkestrel_contract.arrayOf)(_orkestrel_contract.isString)(item));
|
|
566
|
+
} catch {
|
|
567
|
+
return false;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Determine whether a value is an MCP input-required result.
|
|
572
|
+
*
|
|
573
|
+
* @remarks
|
|
574
|
+
* Enforces the at-least-one-of rule at runtime: `inputRequests`, `requestState`, or
|
|
575
|
+
* both must be present and valid. Total over hostile input.
|
|
576
|
+
*
|
|
577
|
+
* @param value - The unknown value to inspect
|
|
578
|
+
* @returns `true` when `value` is a valid input-required result
|
|
579
|
+
*
|
|
580
|
+
* @example
|
|
581
|
+
* ```ts
|
|
582
|
+
* isInputRequiredResult({ resultType: 'input_required', requestState: 'opaque' }) // true
|
|
583
|
+
* isInputRequiredResult({ resultType: 'input_required' }) // false
|
|
584
|
+
* ```
|
|
585
|
+
*/
|
|
586
|
+
function isInputRequiredResult(value) {
|
|
587
|
+
try {
|
|
588
|
+
if (!(0, _orkestrel_contract.isRecord)(value) || value["resultType"] !== "input_required") return false;
|
|
589
|
+
const inputRequests = value["inputRequests"];
|
|
590
|
+
const requestState = value["requestState"];
|
|
591
|
+
if (!(0, _orkestrel_contract.isUndefined)(inputRequests) && !isInputRequests(inputRequests)) return false;
|
|
592
|
+
if (!(0, _orkestrel_contract.isUndefined)(requestState) && !(0, _orkestrel_contract.isString)(requestState)) return false;
|
|
593
|
+
return !(0, _orkestrel_contract.isUndefined)(inputRequests) || !(0, _orkestrel_contract.isUndefined)(requestState);
|
|
594
|
+
} catch {
|
|
595
|
+
return false;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
119
599
|
* Determine whether a parsed value is a {@link JSONRPCRequest}.
|
|
120
600
|
*
|
|
121
601
|
* @remarks
|
|
@@ -194,6 +674,28 @@ function isJSONRPCMessage(value) {
|
|
|
194
674
|
function isInitializeRequest(value) {
|
|
195
675
|
return isJSONRPCRequest(value) && value.method === "initialize";
|
|
196
676
|
}
|
|
677
|
+
/**
|
|
678
|
+
* Determine whether a JSON-RPC request uses the modern per-request MCP wire shape.
|
|
679
|
+
*
|
|
680
|
+
* @remarks
|
|
681
|
+
* Presence routes and validity answers: this guard checks only that
|
|
682
|
+
* `params._meta` carries the reserved protocol-version key. The key's value is
|
|
683
|
+
* deliberately not narrowed here, so a present non-string version remains modern
|
|
684
|
+
* and is rejected later by `parseRequestContext` rather than falling through to
|
|
685
|
+
* legacy dispatch. Total over hostile and malformed input.
|
|
686
|
+
*
|
|
687
|
+
* @param value - The already-parsed value to inspect
|
|
688
|
+
* @returns `true` when the value is a request carrying the reserved version key
|
|
689
|
+
*/
|
|
690
|
+
function isModernRequest(value) {
|
|
691
|
+
try {
|
|
692
|
+
if (!isJSONRPCRequest(value)) return false;
|
|
693
|
+
const metadata = value.params?.["_meta"];
|
|
694
|
+
return (0, _orkestrel_contract.isRecord)(metadata) && Object.hasOwn(metadata, "io.modelcontextprotocol/protocolVersion");
|
|
695
|
+
} catch {
|
|
696
|
+
return false;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
197
699
|
//#endregion
|
|
198
700
|
//#region src/core/parsers.ts
|
|
199
701
|
/**
|
|
@@ -219,6 +721,122 @@ function isInitializeRequest(value) {
|
|
|
219
721
|
function parseJSONRPCMessage(value) {
|
|
220
722
|
return isJSONRPCMessage(value) ? value : void 0;
|
|
221
723
|
}
|
|
724
|
+
/**
|
|
725
|
+
* Parse the reserved modern request metadata into an {@link MCPRequestContext}.
|
|
726
|
+
*
|
|
727
|
+
* @remarks
|
|
728
|
+
* This is the validity step after {@link isModernRequest}: a defined result can
|
|
729
|
+
* only come from a guard-positive request, while a guard-positive request returns
|
|
730
|
+
* `undefined` exactly when its required modern metadata is malformed. The version
|
|
731
|
+
* must be a string but need not be supported; unsupported strings belong to the
|
|
732
|
+
* dedicated protocol-version error path. Client identity is optional, but when
|
|
733
|
+
* present it must carry string `name` and `version` members. Total over hostile and
|
|
734
|
+
* malformed input.
|
|
735
|
+
*
|
|
736
|
+
* @param value - The already-parsed request candidate to coerce
|
|
737
|
+
* @returns The validated modern request context, or `undefined`
|
|
738
|
+
*/
|
|
739
|
+
function parseRequestContext(value) {
|
|
740
|
+
try {
|
|
741
|
+
if (!isModernRequest(value)) return void 0;
|
|
742
|
+
const metadata = value.params?.["_meta"];
|
|
743
|
+
if (!(0, _orkestrel_contract.isRecord)(metadata)) return void 0;
|
|
744
|
+
const version = metadata[MCP_META_VERSION];
|
|
745
|
+
const capabilities = metadata[MCP_META_CAPABILITIES];
|
|
746
|
+
if (!(0, _orkestrel_contract.isString)(version) || !(0, _orkestrel_contract.isRecord)(capabilities)) return void 0;
|
|
747
|
+
const client = metadata[MCP_META_CLIENT];
|
|
748
|
+
if (client === void 0) return {
|
|
749
|
+
version,
|
|
750
|
+
capabilities
|
|
751
|
+
};
|
|
752
|
+
if (!(0, _orkestrel_contract.isRecord)(client)) return void 0;
|
|
753
|
+
const name = client["name"];
|
|
754
|
+
const clientVersion = client["version"];
|
|
755
|
+
if (!(0, _orkestrel_contract.isString)(name) || !(0, _orkestrel_contract.isString)(clientVersion)) return void 0;
|
|
756
|
+
return {
|
|
757
|
+
version,
|
|
758
|
+
capabilities,
|
|
759
|
+
identity: {
|
|
760
|
+
name,
|
|
761
|
+
version: clientVersion
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
} catch {
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Parse the verified value embedded in an opaque signed `requestState` token.
|
|
770
|
+
*
|
|
771
|
+
* @remarks
|
|
772
|
+
* This parser does not verify the HMAC; {@link import('@orkestrel/server').verifyToken}
|
|
773
|
+
* performs that boundary first and returns the JSON string parsed here. The protected
|
|
774
|
+
* payload binds the authenticated principal, token lifetime, originating request id,
|
|
775
|
+
* server-assigned input key, tool name, and optional consumer state. Total over malformed
|
|
776
|
+
* or hostile input.
|
|
777
|
+
*
|
|
778
|
+
* @param value - The HMAC-verified token value to parse
|
|
779
|
+
* @returns The protected input state, or `undefined` when malformed
|
|
780
|
+
*
|
|
781
|
+
* @example
|
|
782
|
+
* ```ts
|
|
783
|
+
* parseMCPInputState('{"principal":"user-1","ttl":1000,"origin":1,"key":"k","name":"reply"}')
|
|
784
|
+
* // { principal: 'user-1', ttl: 1000, origin: 1, key: 'k', name: 'reply' }
|
|
785
|
+
* ```
|
|
786
|
+
*/
|
|
787
|
+
function parseMCPInputState(value) {
|
|
788
|
+
try {
|
|
789
|
+
if (!(0, _orkestrel_contract.isString)(value)) return void 0;
|
|
790
|
+
const parsed = JSON.parse(value);
|
|
791
|
+
if (!(0, _orkestrel_contract.isRecord)(parsed)) return void 0;
|
|
792
|
+
const principal = parsed["principal"];
|
|
793
|
+
const ttl = parsed["ttl"];
|
|
794
|
+
const origin = parsed["origin"];
|
|
795
|
+
const key = parsed["key"];
|
|
796
|
+
const name = parsed["name"];
|
|
797
|
+
const state = parsed["state"];
|
|
798
|
+
if (!(0, _orkestrel_contract.isString)(principal) || !(0, _orkestrel_contract.isNumber)(ttl) || !Number.isFinite(ttl)) return void 0;
|
|
799
|
+
if (!(0, _orkestrel_contract.isString)(origin) && !(0, _orkestrel_contract.isNumber)(origin)) return void 0;
|
|
800
|
+
if (!(0, _orkestrel_contract.isString)(key) || !(0, _orkestrel_contract.isString)(name)) return void 0;
|
|
801
|
+
if (!(0, _orkestrel_contract.isUndefined)(state) && !(0, _orkestrel_contract.isString)(state)) return void 0;
|
|
802
|
+
return {
|
|
803
|
+
principal,
|
|
804
|
+
ttl,
|
|
805
|
+
origin,
|
|
806
|
+
key,
|
|
807
|
+
name,
|
|
808
|
+
...(0, _orkestrel_contract.isString)(state) ? { state } : {}
|
|
809
|
+
};
|
|
810
|
+
} catch {
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
//#endregion
|
|
815
|
+
//#region src/core/inferers.ts
|
|
816
|
+
/**
|
|
817
|
+
* Infer the wire era for an MCP protocol revision.
|
|
818
|
+
*
|
|
819
|
+
* @param version - The protocol revision to classify
|
|
820
|
+
* @returns `'modern'` for `2026-07-28`, `'legacy'` for either supported legacy
|
|
821
|
+
* revision, or `undefined` when the revision is unsupported
|
|
822
|
+
*/
|
|
823
|
+
function inferEra(version) {
|
|
824
|
+
switch (version) {
|
|
825
|
+
case "2026-07-28": return "modern";
|
|
826
|
+
case "2025-11-25":
|
|
827
|
+
case "2025-06-18": return "legacy";
|
|
828
|
+
default: return;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* Infer the newest supported protocol revision present in a peer's offer.
|
|
833
|
+
*
|
|
834
|
+
* @param offered - The protocol revisions offered by the peer
|
|
835
|
+
* @returns The newest locally supported offered revision, or `undefined`
|
|
836
|
+
*/
|
|
837
|
+
function inferVersion(offered) {
|
|
838
|
+
for (const version of SUPPORTED_PROTOCOL_VERSIONS) if (offered.includes(version)) return version;
|
|
839
|
+
}
|
|
222
840
|
//#endregion
|
|
223
841
|
//#region src/core/helpers.ts
|
|
224
842
|
/**
|
|
@@ -229,7 +847,7 @@ function parseJSONRPCMessage(value) {
|
|
|
229
847
|
* @param result - The method's return value
|
|
230
848
|
* @returns The success response envelope
|
|
231
849
|
*/
|
|
232
|
-
function
|
|
850
|
+
function buildJSONRPCResult(id, result) {
|
|
233
851
|
return {
|
|
234
852
|
jsonrpc: "2.0",
|
|
235
853
|
id,
|
|
@@ -246,7 +864,7 @@ function jsonRPCResult(id, result) {
|
|
|
246
864
|
* @param data - An OPTIONAL machine-readable payload (omitted from the envelope when absent)
|
|
247
865
|
* @returns The error response envelope
|
|
248
866
|
*/
|
|
249
|
-
function
|
|
867
|
+
function buildJSONRPCError(id, code, message, data) {
|
|
250
868
|
return {
|
|
251
869
|
jsonrpc: "2.0",
|
|
252
870
|
id,
|
|
@@ -284,21 +902,24 @@ function buildToolDescriptors(manager) {
|
|
|
284
902
|
});
|
|
285
903
|
}
|
|
286
904
|
/**
|
|
287
|
-
* Map an executed tool's {@link ToolResult} to an MCP {@link
|
|
288
|
-
* value
|
|
905
|
+
* Map an executed tool's {@link ToolResult} to an MCP {@link MCPCallResult} — the
|
|
906
|
+
* value as structured content plus a backwards-compatible `text` block, or the
|
|
907
|
+
* error as a `text` block.
|
|
289
908
|
*
|
|
290
909
|
* @remarks
|
|
291
910
|
* The {@link ToolManagerInterface} already isolates a thrown tool into a
|
|
292
911
|
* `success: false` result (so the server adds NO try/catch around `execute`):
|
|
293
912
|
* that branch builds an `isError: true` result carrying `result.error`, so the
|
|
294
913
|
* model sees the failure as a tool result it can react to rather than a protocol
|
|
295
|
-
* error;
|
|
296
|
-
* `JSON.stringify`) into one `text`
|
|
914
|
+
* error; a valued `success: true` branch carries `result.value` unchanged as
|
|
915
|
+
* `structuredContent` and serializes it (via `JSON.stringify`) into one `text`
|
|
916
|
+
* block. A value-less success retains the required empty `content` block and
|
|
917
|
+
* omits `structuredContent`.
|
|
297
918
|
*
|
|
298
919
|
* @param result - The tool's execution outcome
|
|
299
920
|
* @returns The MCP tool-call result
|
|
300
921
|
*/
|
|
301
|
-
function
|
|
922
|
+
function buildCallResult(result) {
|
|
302
923
|
if (!result.success) return {
|
|
303
924
|
content: [{
|
|
304
925
|
type: "text",
|
|
@@ -306,10 +927,129 @@ function buildToolResult(result) {
|
|
|
306
927
|
}],
|
|
307
928
|
isError: true
|
|
308
929
|
};
|
|
309
|
-
return { content: [{
|
|
930
|
+
if (result.value === void 0) return { content: [{
|
|
310
931
|
type: "text",
|
|
311
|
-
text:
|
|
932
|
+
text: ""
|
|
312
933
|
}] };
|
|
934
|
+
return {
|
|
935
|
+
content: [{
|
|
936
|
+
type: "text",
|
|
937
|
+
text: JSON.stringify(result.value)
|
|
938
|
+
}],
|
|
939
|
+
structuredContent: result.value
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
function buildModernResult(result, identity, ttl, scope) {
|
|
943
|
+
const currentMetadata = (0, _orkestrel_contract.isRecord)(result) ? result["_meta"] : void 0;
|
|
944
|
+
const metadata = {
|
|
945
|
+
...(0, _orkestrel_contract.isRecord)(currentMetadata) ? currentMetadata : {},
|
|
946
|
+
[MCP_META_SERVER]: identity
|
|
947
|
+
};
|
|
948
|
+
if (ttl === void 0) return {
|
|
949
|
+
...result,
|
|
950
|
+
resultType: "complete",
|
|
951
|
+
_meta: metadata
|
|
952
|
+
};
|
|
953
|
+
return {
|
|
954
|
+
...result,
|
|
955
|
+
resultType: "complete",
|
|
956
|
+
ttlMs: ttl,
|
|
957
|
+
cacheScope: scope ?? "private",
|
|
958
|
+
_meta: metadata
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
/**
|
|
962
|
+
* Intersect a requested subscription filter with the notification families a server supports.
|
|
963
|
+
*
|
|
964
|
+
* @param requested - The notification families requested by the client
|
|
965
|
+
* @param supported - The notification families the server can actually produce
|
|
966
|
+
* @returns The exact subset the server will honour
|
|
967
|
+
*/
|
|
968
|
+
function buildSubscriptionFilter(requested, supported) {
|
|
969
|
+
const toolsListChanged = requested.toolsListChanged === true && supported.toolsListChanged === true;
|
|
970
|
+
const promptsListChanged = requested.promptsListChanged === true && supported.promptsListChanged === true;
|
|
971
|
+
const resourcesListChanged = requested.resourcesListChanged === true && supported.resourcesListChanged === true;
|
|
972
|
+
const supportedResources = new Set(supported.resourceSubscriptions ?? []);
|
|
973
|
+
const resourceSubscriptions = requested.resourceSubscriptions?.filter((uri) => supportedResources.has(uri));
|
|
974
|
+
return {
|
|
975
|
+
...toolsListChanged ? { toolsListChanged: true } : {},
|
|
976
|
+
...promptsListChanged ? { promptsListChanged: true } : {},
|
|
977
|
+
...resourcesListChanged ? { resourcesListChanged: true } : {},
|
|
978
|
+
...resourceSubscriptions !== void 0 && resourceSubscriptions.length > 0 ? { resourceSubscriptions } : {}
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
/**
|
|
982
|
+
* Determine whether a produced notification belongs to an honoured subscription filter.
|
|
983
|
+
*
|
|
984
|
+
* @param notification - The server notification offered by the configured producer
|
|
985
|
+
* @param filter - The filter acknowledged to the client
|
|
986
|
+
* @returns `true` when the notification belongs on this subscription stream
|
|
987
|
+
*/
|
|
988
|
+
function matchesSubscriptionNotification(notification, filter) {
|
|
989
|
+
if (notification.method === "notifications/tools/list_changed") return filter.toolsListChanged === true;
|
|
990
|
+
if (notification.method === "notifications/prompts/list_changed") return filter.promptsListChanged === true;
|
|
991
|
+
if (notification.method === "notifications/resources/list_changed") return filter.resourcesListChanged === true;
|
|
992
|
+
if (notification.method !== "notifications/resources/updated") return false;
|
|
993
|
+
const uri = notification.params?.["uri"];
|
|
994
|
+
return typeof uri === "string" && filter.resourceSubscriptions?.includes(uri) === true;
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* Stamp a subscription notification with the request id reserved for its held-open stream.
|
|
998
|
+
*
|
|
999
|
+
* @param notification - The notification to copy and stamp
|
|
1000
|
+
* @param id - The `subscriptions/listen` request id
|
|
1001
|
+
* @returns The stamped notification, preserving its other params and metadata
|
|
1002
|
+
*/
|
|
1003
|
+
function stampSubscriptionNotification(notification, id) {
|
|
1004
|
+
const metadata = notification.params?.["_meta"];
|
|
1005
|
+
return {
|
|
1006
|
+
jsonrpc: notification.jsonrpc,
|
|
1007
|
+
method: notification.method,
|
|
1008
|
+
params: {
|
|
1009
|
+
...notification.params,
|
|
1010
|
+
_meta: {
|
|
1011
|
+
...(0, _orkestrel_contract.isRecord)(metadata) ? metadata : {},
|
|
1012
|
+
[MCP_META_SUBSCRIPTION]: id
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
/**
|
|
1018
|
+
* Build the first notification carrying a subscription id for a listen request.
|
|
1019
|
+
*
|
|
1020
|
+
* @param notifications - The exact notification filter the server will honour
|
|
1021
|
+
* @param id - The `subscriptions/listen` request id
|
|
1022
|
+
* @returns The stamped subscription acknowledgement notification
|
|
1023
|
+
*/
|
|
1024
|
+
function buildSubscriptionAcknowledgement(notifications, id) {
|
|
1025
|
+
return stampSubscriptionNotification({
|
|
1026
|
+
jsonrpc: "2.0",
|
|
1027
|
+
method: "notifications/subscriptions/acknowledged",
|
|
1028
|
+
params: { notifications }
|
|
1029
|
+
}, id);
|
|
1030
|
+
}
|
|
1031
|
+
/**
|
|
1032
|
+
* Build the terminating response for a subscription source that closes gracefully.
|
|
1033
|
+
*
|
|
1034
|
+
* @param id - The `subscriptions/listen` request id
|
|
1035
|
+
* @param identity - The server identity included by the modern result stamping site
|
|
1036
|
+
* @returns The complete modern result carrying the required subscription id metadata
|
|
1037
|
+
*/
|
|
1038
|
+
function buildSubscriptionResult(id, identity) {
|
|
1039
|
+
return buildJSONRPCResult(id, buildModernResult({ _meta: { [MCP_META_SUBSCRIPTION]: id } }, identity));
|
|
1040
|
+
}
|
|
1041
|
+
/**
|
|
1042
|
+
* Build the mandatory modern `server/discover` result.
|
|
1043
|
+
*
|
|
1044
|
+
* @param options - The server identity, instructions, and cache configuration
|
|
1045
|
+
* @returns The supported revisions, tools capability, and required modern cache stamps
|
|
1046
|
+
*/
|
|
1047
|
+
function buildDiscoverResult(options) {
|
|
1048
|
+
return buildModernResult({
|
|
1049
|
+
supportedVersions: SUPPORTED_PROTOCOL_VERSIONS.filter(isMCPVersion),
|
|
1050
|
+
capabilities: { tools: {} },
|
|
1051
|
+
...options.instructions === void 0 ? {} : { instructions: options.instructions }
|
|
1052
|
+
}, options.identity, options.cache?.ttl ?? 6e4, options.cache?.scope);
|
|
313
1053
|
}
|
|
314
1054
|
/**
|
|
315
1055
|
* Build the MCP `initialize` result — the negotiated protocol version, the
|
|
@@ -317,7 +1057,8 @@ function buildToolResult(result) {
|
|
|
317
1057
|
*
|
|
318
1058
|
* @remarks
|
|
319
1059
|
* Version negotiation echoes the client's `requested` version when it is one of the
|
|
320
|
-
*
|
|
1060
|
+
* supported legacy revisions. A modern or unsupported request receives the newest
|
|
1061
|
+
* supported legacy revision; the client decides whether to continue.
|
|
321
1062
|
* `capabilities.tools` is an empty object — this server advertises the tools
|
|
322
1063
|
* capability with no sub-options (no list-changed notification yet).
|
|
323
1064
|
*
|
|
@@ -326,9 +1067,10 @@ function buildToolResult(result) {
|
|
|
326
1067
|
* @param requested - The client's requested protocol version (negotiated when supported)
|
|
327
1068
|
* @returns The `initialize` result payload
|
|
328
1069
|
*/
|
|
329
|
-
function
|
|
1070
|
+
function buildInitializeResult(name, version, requested) {
|
|
1071
|
+
const newestLegacy = SUPPORTED_PROTOCOL_VERSIONS.find((candidate) => inferEra(candidate) === "legacy") ?? "2025-06-18";
|
|
330
1072
|
return {
|
|
331
|
-
protocolVersion: requested
|
|
1073
|
+
protocolVersion: isMCPVersion(requested) && inferEra(requested) === "legacy" ? requested : newestLegacy,
|
|
332
1074
|
capabilities: { tools: {} },
|
|
333
1075
|
serverInfo: {
|
|
334
1076
|
name,
|
|
@@ -337,6 +1079,64 @@ function initializeResult(name, version, requested) {
|
|
|
337
1079
|
};
|
|
338
1080
|
}
|
|
339
1081
|
/**
|
|
1082
|
+
* Serialize a typed {@link MCPStream} into its string mirror — each yielded
|
|
1083
|
+
* notification and the terminating response, already `JSON.stringify`d.
|
|
1084
|
+
*
|
|
1085
|
+
* @remarks
|
|
1086
|
+
* The string-boundary half of the held-open arm: `handle` returns this so a transport
|
|
1087
|
+
* writes each message with no second parse, exactly as it writes a unary reply string.
|
|
1088
|
+
* The terminating response arrives as the returned generator's OWN `return` value, so a
|
|
1089
|
+
* consumer distinguishes "one more notification" from "this is the answer" without a
|
|
1090
|
+
* sentinel.
|
|
1091
|
+
*
|
|
1092
|
+
* @param stream - The typed held-open result to serialize
|
|
1093
|
+
* @returns The same sequence with every message serialized to a string
|
|
1094
|
+
*
|
|
1095
|
+
* @example
|
|
1096
|
+
* ```ts
|
|
1097
|
+
* const text = serializeStream(stream)
|
|
1098
|
+
* for (let next = await text.next(); ; next = await text.next()) {
|
|
1099
|
+
* if (next.done === true) return next.value // the terminating response, serialized
|
|
1100
|
+
* log(next.value) // one serialized notification
|
|
1101
|
+
* }
|
|
1102
|
+
* ```
|
|
1103
|
+
*/
|
|
1104
|
+
async function* serializeStream(stream) {
|
|
1105
|
+
let next = await stream.next();
|
|
1106
|
+
while (!next.done) {
|
|
1107
|
+
yield JSON.stringify(next.value);
|
|
1108
|
+
next = await stream.next();
|
|
1109
|
+
}
|
|
1110
|
+
return JSON.stringify(next.value);
|
|
1111
|
+
}
|
|
1112
|
+
/**
|
|
1113
|
+
* Pump an {@link MCPTextStream} onto a transport — every notification in order, then the
|
|
1114
|
+
* terminating response.
|
|
1115
|
+
*
|
|
1116
|
+
* @remarks
|
|
1117
|
+
* The generator's `return` value is a message like any other on the wire: it is sent
|
|
1118
|
+
* LAST and closes the exchange. Sends are awaited one at a time so the transport
|
|
1119
|
+
* receives the sequence in the order the method produced it.
|
|
1120
|
+
*
|
|
1121
|
+
* @param stream - The serialized held-open result to write out
|
|
1122
|
+
* @param transport - The duplex channel to write each message to
|
|
1123
|
+
* @returns Resolves once the terminating response has been sent
|
|
1124
|
+
*
|
|
1125
|
+
* @example
|
|
1126
|
+
* ```ts
|
|
1127
|
+
* const answer = await server.handle(message)
|
|
1128
|
+
* if (typeof answer !== 'string') await sendStream(answer, transport)
|
|
1129
|
+
* ```
|
|
1130
|
+
*/
|
|
1131
|
+
async function sendStream(stream, transport) {
|
|
1132
|
+
let next = await stream.next();
|
|
1133
|
+
while (!next.done) {
|
|
1134
|
+
await transport.send(next.value);
|
|
1135
|
+
next = await stream.next();
|
|
1136
|
+
}
|
|
1137
|
+
await transport.send(next.value);
|
|
1138
|
+
}
|
|
1139
|
+
/**
|
|
340
1140
|
* Pipe an {@link MCPTransportInterface} into an {@link MCPServerInterface} — every
|
|
341
1141
|
* inbound message runs through `server.handle`, and a defined reply is written back
|
|
342
1142
|
* via `transport.send`.
|
|
@@ -344,7 +1144,11 @@ function initializeResult(name, version, requested) {
|
|
|
344
1144
|
* @remarks
|
|
345
1145
|
* `server.handle` already turns a malformed message into a serialized `-32700` /
|
|
346
1146
|
* `-32600` reply and a notification into `undefined` (no reply), so this binder adds
|
|
347
|
-
* no parsing of its own. A
|
|
1147
|
+
* no parsing of its own. A HELD-OPEN reply arrives as an
|
|
1148
|
+
* {@link import('./types.js').MCPTextStream} instead of a string: this is the one place
|
|
1149
|
+
* that pumps it, writing each notification in order and then the generator's returned
|
|
1150
|
+
* terminating response ({@link sendStream}). A `transport.send` throw or rejection —
|
|
1151
|
+
* mid-stream included — is caught and routed
|
|
348
1152
|
* to `server.emitter`'s `error` event (never rethrown, never an unhandled rejection);
|
|
349
1153
|
* a listener on that event that itself throws is swallowed (the end of the line —
|
|
350
1154
|
* the caller's own bug, never this binder's). The returned unbind DETACHES this
|
|
@@ -373,8 +1177,10 @@ function bindServer(server, transport) {
|
|
|
373
1177
|
transport.listen(async (message) => {
|
|
374
1178
|
if (!active) return;
|
|
375
1179
|
try {
|
|
376
|
-
const
|
|
377
|
-
if (
|
|
1180
|
+
const answer = await server.handle(message);
|
|
1181
|
+
if (answer === void 0) return;
|
|
1182
|
+
if (typeof answer === "string") await transport.send(answer);
|
|
1183
|
+
else await sendStream(answer, transport);
|
|
378
1184
|
} catch (error) {
|
|
379
1185
|
try {
|
|
380
1186
|
server.emitter.emit("error", error);
|
|
@@ -463,6 +1269,40 @@ function bindClient(client, transport) {
|
|
|
463
1269
|
};
|
|
464
1270
|
}
|
|
465
1271
|
//#endregion
|
|
1272
|
+
//#region src/core/MCPMethodManager.ts
|
|
1273
|
+
/**
|
|
1274
|
+
* The modern method registry an {@link import('./types.js').MCPServerInterface}
|
|
1275
|
+
* dispatches through — a name-keyed store of {@link MCPMethodHandler}s that owns its
|
|
1276
|
+
* map rather than exposing one.
|
|
1277
|
+
*
|
|
1278
|
+
* @remarks
|
|
1279
|
+
* - **One seam.** The server registers its built-in modern methods here at construction
|
|
1280
|
+
* and resolves EVERY modern method from here, so a consumer's method and a built-in
|
|
1281
|
+
* are the same kind of thing on the same path.
|
|
1282
|
+
* - **Registration is a write, not a merge.** `add` under a name already present
|
|
1283
|
+
* REPLACES it, which is how a consumer overrides a built-in; there is no precedence
|
|
1284
|
+
* rule to remember.
|
|
1285
|
+
* - **A narrower contract than a `Map`.** Callers register and resolve; they cannot
|
|
1286
|
+
* iterate, clear, or otherwise reach the server's internal state through it.
|
|
1287
|
+
*
|
|
1288
|
+
* @example
|
|
1289
|
+
* ```ts
|
|
1290
|
+
* const methods = new MCPMethodManager()
|
|
1291
|
+
* methods.add('tools/list', async (request) => buildJSONRPCResult(request.id ?? null, { tools: [] }))
|
|
1292
|
+
* methods.method('tools/list') // the handler
|
|
1293
|
+
* methods.method('tools/nope') // undefined → the dispatch branch answers -32601
|
|
1294
|
+
* ```
|
|
1295
|
+
*/
|
|
1296
|
+
var MCPMethodManager = class {
|
|
1297
|
+
#handlers = /* @__PURE__ */ new Map();
|
|
1298
|
+
add(name, handler) {
|
|
1299
|
+
this.#handlers.set(name, handler);
|
|
1300
|
+
}
|
|
1301
|
+
method(name) {
|
|
1302
|
+
return this.#handlers.get(name);
|
|
1303
|
+
}
|
|
1304
|
+
};
|
|
1305
|
+
//#endregion
|
|
466
1306
|
//#region src/core/MCPServer.ts
|
|
467
1307
|
/**
|
|
468
1308
|
* A transport-agnostic Model Context Protocol server — dispatches JSON-RPC 2.0
|
|
@@ -475,14 +1315,15 @@ function bindClient(client, transport) {
|
|
|
475
1315
|
* `JSON.parse`s the raw message (a failure → a `-32700` response), narrows it to
|
|
476
1316
|
* a request (a non-request → a `-32600` response), dispatches, and serializes the
|
|
477
1317
|
* response back to a string (`undefined` for a notification).
|
|
478
|
-
* - **
|
|
479
|
-
* the
|
|
480
|
-
*
|
|
481
|
-
*
|
|
482
|
-
*
|
|
483
|
-
*
|
|
484
|
-
*
|
|
485
|
-
*
|
|
1318
|
+
* - **Dual-era dispatch.** A request carrying the reserved modern version key uses
|
|
1319
|
+
* modern metadata validation and the registered method seam. Every other request
|
|
1320
|
+
* uses the legacy `initialize` / `ping` / `tools/list` / `tools/call` switch. The
|
|
1321
|
+
* wire era is selected per request and never stored.
|
|
1322
|
+
* - **One modern seam.** `server/discover`, `tools/list`, `tools/call`, and
|
|
1323
|
+
* `subscriptions/listen` are
|
|
1324
|
+
* registered on `methods` at construction and resolved from it on every dispatch —
|
|
1325
|
+
* the same path a later method or a consumer's own takes, with an unregistered
|
|
1326
|
+
* method still answering `-32601`.
|
|
486
1327
|
* - **Provider-agnostic.** Imports only core siblings — JSON-RPC + the tool registry,
|
|
487
1328
|
* no HTTP, no model. Wire fields are narrowed via the contracts guards (no `as`).
|
|
488
1329
|
* - **Observable (§13).** The owned `emitter` fires `request` at the top of every
|
|
@@ -493,86 +1334,248 @@ function bindClient(client, transport) {
|
|
|
493
1334
|
* ```ts
|
|
494
1335
|
* const tools = createToolManager()
|
|
495
1336
|
* tools.add(createTool({ name: 'add', execute: (a) => Number(a.x) + Number(a.y) }))
|
|
496
|
-
* const server = new MCPServer({ name: 'demo', version: '1.0.0', tools })
|
|
1337
|
+
* const server = new MCPServer({ identity: { name: 'demo', version: '1.0.0' }, tools })
|
|
497
1338
|
* await server.handle('{"jsonrpc":"2.0","method":"ping","id":1}') // '{"jsonrpc":"2.0","id":1,"result":{}}'
|
|
498
1339
|
* ```
|
|
499
1340
|
*/
|
|
500
1341
|
var MCPServer = class {
|
|
501
1342
|
#emitter;
|
|
502
|
-
#
|
|
503
|
-
#
|
|
504
|
-
#
|
|
1343
|
+
#options;
|
|
1344
|
+
#methods;
|
|
1345
|
+
#limits;
|
|
1346
|
+
#subscriptions = 0;
|
|
505
1347
|
constructor(options) {
|
|
506
1348
|
this.#emitter = new _orkestrel_emitter.Emitter({
|
|
507
1349
|
...options.on !== void 0 ? { on: options.on } : {},
|
|
508
1350
|
...options.error !== void 0 ? { error: options.error } : {}
|
|
509
1351
|
});
|
|
510
|
-
this.#
|
|
511
|
-
this.#
|
|
512
|
-
|
|
1352
|
+
this.#options = options;
|
|
1353
|
+
this.#limits = {
|
|
1354
|
+
message: (0, _orkestrel_contract.sanitizeBudget)(options.limit?.message, DEFAULT_MCP_LIMITS.message),
|
|
1355
|
+
metadata: (0, _orkestrel_contract.sanitizeBudget)(options.limit?.metadata, DEFAULT_MCP_LIMITS.metadata),
|
|
1356
|
+
keys: (0, _orkestrel_contract.sanitizeBudget)(options.limit?.keys, DEFAULT_MCP_LIMITS.keys),
|
|
1357
|
+
state: (0, _orkestrel_contract.sanitizeBudget)(options.limit?.state, DEFAULT_MCP_LIMITS.state),
|
|
1358
|
+
content: (0, _orkestrel_contract.sanitizeBudget)(options.limit?.content, DEFAULT_MCP_LIMITS.content),
|
|
1359
|
+
subscriptions: (0, _orkestrel_contract.sanitizeBudget)(options.limit?.subscriptions, DEFAULT_MCP_LIMITS.subscriptions),
|
|
1360
|
+
depth: (0, _orkestrel_contract.sanitizeBudget)(options.limit?.depth, DEFAULT_MCP_LIMITS.depth)
|
|
1361
|
+
};
|
|
1362
|
+
this.#methods = new MCPMethodManager();
|
|
1363
|
+
this.#register();
|
|
513
1364
|
}
|
|
514
1365
|
get emitter() {
|
|
515
1366
|
return this.#emitter;
|
|
516
1367
|
}
|
|
517
|
-
get
|
|
518
|
-
return this.#
|
|
1368
|
+
get identity() {
|
|
1369
|
+
return this.#options.identity;
|
|
519
1370
|
}
|
|
520
|
-
get
|
|
521
|
-
return this.#
|
|
1371
|
+
get methods() {
|
|
1372
|
+
return this.#methods;
|
|
522
1373
|
}
|
|
523
|
-
async dispatch(request) {
|
|
1374
|
+
async dispatch(request, options = {}) {
|
|
524
1375
|
const id = request.id ?? null;
|
|
525
|
-
|
|
1376
|
+
const modern = isModernRequest(request);
|
|
1377
|
+
const era = modern ? "modern" : "legacy";
|
|
1378
|
+
this.#emitter.emit("request", request.method, id, era);
|
|
526
1379
|
if (request.id === void 0) return;
|
|
1380
|
+
const metadata = request.params?.["_meta"];
|
|
1381
|
+
if (metadata !== void 0 && !isBoundedJSON(metadata, {
|
|
1382
|
+
bytes: this.#limits.metadata,
|
|
1383
|
+
keys: this.#limits.keys,
|
|
1384
|
+
depth: this.#limits.depth
|
|
1385
|
+
})) return buildJSONRPCError(id, JSONRPC_INVALID_PARAMS, "Invalid params: `_meta` exceeds the configured limit or contains an unsafe value");
|
|
1386
|
+
return modern ? this.#modern(request, id, options) : this.#legacy(request, id);
|
|
1387
|
+
}
|
|
1388
|
+
async handle(message, options) {
|
|
1389
|
+
if (!isBoundedString(message, this.#limits.message)) return JSON.stringify(buildJSONRPCError(null, JSONRPC_PARSE_ERROR, "Parse error"));
|
|
1390
|
+
let parsed;
|
|
1391
|
+
try {
|
|
1392
|
+
parsed = JSON.parse(message);
|
|
1393
|
+
} catch {
|
|
1394
|
+
return JSON.stringify(buildJSONRPCError(null, JSONRPC_PARSE_ERROR, "Parse error"));
|
|
1395
|
+
}
|
|
1396
|
+
const decoded = parseJSONRPCMessage(parsed);
|
|
1397
|
+
if (decoded === void 0 || !("method" in decoded)) return JSON.stringify(buildJSONRPCError(null, JSONRPC_INVALID_REQUEST, "Invalid Request"));
|
|
1398
|
+
const answer = await this.dispatch(decoded, options);
|
|
1399
|
+
if (answer === void 0) return void 0;
|
|
1400
|
+
return Symbol.asyncIterator in answer ? serializeStream(answer) : JSON.stringify(answer);
|
|
1401
|
+
}
|
|
1402
|
+
async #legacy(request, id) {
|
|
527
1403
|
switch (request.method) {
|
|
528
1404
|
case "initialize": {
|
|
529
1405
|
const requested = request.params?.["protocolVersion"];
|
|
530
|
-
return
|
|
1406
|
+
return buildJSONRPCResult(id, buildInitializeResult(this.#options.identity.name, this.#options.identity.version, (0, _orkestrel_contract.isString)(requested) ? requested : void 0));
|
|
531
1407
|
}
|
|
532
|
-
case "ping": return
|
|
533
|
-
case "tools/list": return
|
|
534
|
-
case "tools/call":
|
|
535
|
-
|
|
1408
|
+
case "ping": return buildJSONRPCResult(id, {});
|
|
1409
|
+
case "tools/list": return buildJSONRPCResult(id, { tools: buildToolDescriptors(this.#options.tools) });
|
|
1410
|
+
case "tools/call": {
|
|
1411
|
+
const result = await this.#runTool(request, id);
|
|
1412
|
+
return "jsonrpc" in result ? result : buildJSONRPCResult(id, result);
|
|
1413
|
+
}
|
|
1414
|
+
default: return buildJSONRPCError(id, JSONRPC_METHOD_NOT_FOUND, `Method not found: ${request.method}`);
|
|
536
1415
|
}
|
|
537
1416
|
}
|
|
538
|
-
|
|
539
|
-
|
|
1417
|
+
#register() {
|
|
1418
|
+
this.#methods.add("server/discover", (request) => this.#discover(request));
|
|
1419
|
+
this.#methods.add("tools/list", (request) => this.#list(request));
|
|
1420
|
+
this.#methods.add("tools/call", (request, options) => this.#call(request, options));
|
|
1421
|
+
this.#methods.add("subscriptions/listen", (request, options) => this.#subscribe(request, options));
|
|
1422
|
+
}
|
|
1423
|
+
async #modern(request, id, options) {
|
|
1424
|
+
const context = parseRequestContext(request);
|
|
1425
|
+
if (context === void 0) return buildJSONRPCError(id, JSONRPC_INVALID_PARAMS, "Invalid params: malformed modern request metadata");
|
|
1426
|
+
if (inferEra(context.version) === void 0) return buildJSONRPCError(id, MCP_UNSUPPORTED_VERSION, `Unsupported protocol version: ${context.version}`, {
|
|
1427
|
+
supported: SUPPORTED_PROTOCOL_VERSIONS,
|
|
1428
|
+
requested: context.version
|
|
1429
|
+
});
|
|
1430
|
+
const handler = this.#methods.method(request.method);
|
|
1431
|
+
if (handler === void 0) return buildJSONRPCError(id, JSONRPC_METHOD_NOT_FOUND, `Method not found: ${request.method}`);
|
|
1432
|
+
return handler(request, options);
|
|
1433
|
+
}
|
|
1434
|
+
async #discover(request) {
|
|
1435
|
+
return buildJSONRPCResult(request.id ?? null, buildDiscoverResult(this.#options));
|
|
1436
|
+
}
|
|
1437
|
+
async #list(request) {
|
|
1438
|
+
return buildJSONRPCResult(request.id ?? null, buildModernResult({ tools: buildToolDescriptors(this.#options.tools) }, this.#options.identity, this.#options.cache?.ttl ?? 6e4, this.#options.cache?.scope));
|
|
1439
|
+
}
|
|
1440
|
+
async #call(request, options = {}) {
|
|
1441
|
+
const id = request.id ?? null;
|
|
1442
|
+
const input = await this.#input(request, options);
|
|
1443
|
+
if (input !== void 0) return input;
|
|
1444
|
+
const result = await this.#runTool(request, id);
|
|
1445
|
+
return "jsonrpc" in result ? result : buildJSONRPCResult(id, buildModernResult(result, this.#options.identity));
|
|
1446
|
+
}
|
|
1447
|
+
async #input(request, options) {
|
|
1448
|
+
const configured = this.#options.input;
|
|
1449
|
+
if (configured === void 0) return void 0;
|
|
1450
|
+
const id = request.id;
|
|
1451
|
+
if (id === void 0) return void 0;
|
|
1452
|
+
const params = request.params;
|
|
1453
|
+
const name = params?.["name"];
|
|
1454
|
+
if (!(0, _orkestrel_contract.isString)(name)) return void 0;
|
|
1455
|
+
const rawArguments = params?.["arguments"];
|
|
1456
|
+
const args = (0, _orkestrel_contract.isRecord)(rawArguments) ? rawArguments : {};
|
|
1457
|
+
const requestState = params?.["requestState"];
|
|
1458
|
+
const inputResponses = params?.["inputResponses"];
|
|
1459
|
+
if (requestState === void 0 && inputResponses === void 0) {
|
|
1460
|
+
const elicitation = await configured.elicit({
|
|
1461
|
+
request,
|
|
1462
|
+
name,
|
|
1463
|
+
arguments: args
|
|
1464
|
+
}, options);
|
|
1465
|
+
if (elicitation === void 0) return void 0;
|
|
1466
|
+
const principal = await configured.principal(request);
|
|
1467
|
+
return this.#required(request, name, elicitation, principal);
|
|
1468
|
+
}
|
|
1469
|
+
if (!isBoundedString(requestState, this.#limits.state) || !(0, _orkestrel_contract.isRecord)(inputResponses)) return buildJSONRPCError(id, JSONRPC_INVALID_PARAMS, "Invalid params: `inputResponses` and `requestState` are required together");
|
|
1470
|
+
const state = parseMCPInputState(await (0, _orkestrel_server.verifyToken)(requestState, configured.secret));
|
|
1471
|
+
const principal = await configured.principal(request);
|
|
1472
|
+
if (state === void 0 || state.principal !== principal || state.ttl !== configured.ttl || state.origin === id || state.name !== name || !Object.hasOwn(inputResponses, state.key)) return buildJSONRPCError(id, JSONRPC_INVALID_PARAMS, "Invalid params: request state could not be verified for this retry");
|
|
1473
|
+
const response = inputResponses[state.key];
|
|
1474
|
+
if (!isElicitResult(response)) return buildJSONRPCError(id, JSONRPC_INVALID_PARAMS, "Invalid params: the elicitation response is missing or malformed");
|
|
1475
|
+
const elicitation = await configured.elicit({
|
|
1476
|
+
request,
|
|
1477
|
+
name,
|
|
1478
|
+
arguments: args,
|
|
1479
|
+
response,
|
|
1480
|
+
...state.state !== void 0 ? { state: state.state } : {}
|
|
1481
|
+
}, options);
|
|
1482
|
+
return elicitation === void 0 ? void 0 : this.#required(request, name, elicitation, principal);
|
|
1483
|
+
}
|
|
1484
|
+
async #required(request, name, elicitation, principal) {
|
|
1485
|
+
const id = request.id;
|
|
1486
|
+
if (id === void 0) return buildJSONRPCError(null, JSONRPC_INVALID_REQUEST, "Invalid Request");
|
|
1487
|
+
const context = parseRequestContext(request);
|
|
1488
|
+
if (context === void 0 || !isFormElicitationSupported(context.capabilities)) return buildJSONRPCError(id, MCP_MISSING_CAPABILITY, "Server requires the elicitation capability for this request", { requiredCapabilities: { elicitation: {} } });
|
|
1489
|
+
if (!isElicitRequestFormParams(elicitation.request) || principal.length === 0 || !Number.isFinite(this.#options.input?.ttl) || (this.#options.input?.ttl ?? 0) <= 0) return buildJSONRPCError(id, JSONRPC_INVALID_PARAMS, "Invalid params: elicitation policy returned an invalid form or signing context");
|
|
1490
|
+
const configured = this.#options.input;
|
|
1491
|
+
if (configured === void 0) return buildJSONRPCError(id, JSONRPC_INVALID_PARAMS, "Invalid params: input is not configured");
|
|
1492
|
+
const key = crypto.randomUUID();
|
|
1493
|
+
const protectedState = {
|
|
1494
|
+
principal,
|
|
1495
|
+
ttl: configured.ttl,
|
|
1496
|
+
origin: id,
|
|
1497
|
+
key,
|
|
1498
|
+
name,
|
|
1499
|
+
...elicitation.state !== void 0 ? { state: elicitation.state } : {}
|
|
1500
|
+
};
|
|
1501
|
+
if (!isBoundedJSON(protectedState, {
|
|
1502
|
+
bytes: this.#limits.state,
|
|
1503
|
+
depth: this.#limits.depth
|
|
1504
|
+
})) return buildJSONRPCError(id, JSONRPC_INVALID_PARAMS, "Invalid params: request state exceeds the configured limit");
|
|
1505
|
+
const requestState = await (0, _orkestrel_server.signToken)(JSON.stringify(protectedState), {
|
|
1506
|
+
secret: configured.secret,
|
|
1507
|
+
ttl: configured.ttl
|
|
1508
|
+
});
|
|
1509
|
+
if (!isBoundedString(requestState, this.#limits.state)) return buildJSONRPCError(id, JSONRPC_INVALID_PARAMS, "Invalid params: request state exceeds the configured limit");
|
|
1510
|
+
return buildJSONRPCResult(id, {
|
|
1511
|
+
resultType: "input_required",
|
|
1512
|
+
inputRequests: { [key]: {
|
|
1513
|
+
method: "elicitation/create",
|
|
1514
|
+
params: {
|
|
1515
|
+
...elicitation.request,
|
|
1516
|
+
mode: "form"
|
|
1517
|
+
}
|
|
1518
|
+
} },
|
|
1519
|
+
requestState,
|
|
1520
|
+
_meta: { [MCP_META_SERVER]: this.#options.identity }
|
|
1521
|
+
});
|
|
1522
|
+
}
|
|
1523
|
+
async #subscribe(request, options) {
|
|
1524
|
+
const id = request.id;
|
|
1525
|
+
if (id === void 0) return buildJSONRPCError(null, JSONRPC_INVALID_REQUEST, "Invalid Request");
|
|
1526
|
+
const requested = request.params?.["notifications"];
|
|
1527
|
+
if (!isSubscriptionFilter(requested)) return buildJSONRPCError(id, JSONRPC_INVALID_PARAMS, "Invalid params: a valid `notifications` filter is required");
|
|
1528
|
+
return this.#subscription(requested, id, options);
|
|
1529
|
+
}
|
|
1530
|
+
async *#subscription(requested, id, options) {
|
|
1531
|
+
if (this.#subscriptions >= this.#limits.subscriptions) return buildJSONRPCError(id, JSONRPC_SERVER_ERROR, "Server limit reached: too many live subscriptions");
|
|
1532
|
+
this.#subscriptions += 1;
|
|
540
1533
|
try {
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
1534
|
+
const configured = this.#options.subscription;
|
|
1535
|
+
const notifications = buildSubscriptionFilter(requested, configured?.notifications ?? {});
|
|
1536
|
+
yield buildSubscriptionAcknowledgement(notifications, id);
|
|
1537
|
+
if (configured !== void 0) {
|
|
1538
|
+
const source = await configured.listen(notifications, options);
|
|
1539
|
+
for await (const notification of source) if (matchesSubscriptionNotification(notification, notifications)) yield stampSubscriptionNotification(notification, id);
|
|
1540
|
+
}
|
|
1541
|
+
return buildSubscriptionResult(id, this.#options.identity);
|
|
1542
|
+
} finally {
|
|
1543
|
+
this.#subscriptions -= 1;
|
|
544
1544
|
}
|
|
545
|
-
const decoded = parseJSONRPCMessage(parsed);
|
|
546
|
-
if (decoded === void 0 || !("method" in decoded)) return JSON.stringify(jsonRPCError(null, JSONRPC_INVALID_REQUEST, "Invalid Request"));
|
|
547
|
-
const response = await this.dispatch(decoded);
|
|
548
|
-
return response === void 0 ? void 0 : JSON.stringify(response);
|
|
549
1545
|
}
|
|
550
|
-
async #
|
|
1546
|
+
async #runTool(request, id) {
|
|
551
1547
|
const params = request.params;
|
|
552
1548
|
const name = params?.["name"];
|
|
553
|
-
if (!(0, _orkestrel_contract.isString)(name)) return
|
|
1549
|
+
if (!(0, _orkestrel_contract.isString)(name)) return buildJSONRPCError(id, JSONRPC_INVALID_PARAMS, "Invalid params: a string `name` is required");
|
|
554
1550
|
const rawArguments = params?.["arguments"];
|
|
555
1551
|
const args = (0, _orkestrel_contract.isRecord)(rawArguments) ? rawArguments : {};
|
|
556
1552
|
const callId = request.id === void 0 ? crypto.randomUUID() : String(request.id);
|
|
557
|
-
|
|
1553
|
+
const result = await this.#options.tools.execute({
|
|
558
1554
|
id: callId,
|
|
559
1555
|
name,
|
|
560
1556
|
arguments: args
|
|
561
|
-
})
|
|
1557
|
+
});
|
|
1558
|
+
if (!result.success && !isBoundedString(result.error, this.#limits.content)) return buildJSONRPCError(id, JSONRPC_SERVER_ERROR, "Server limit exceeded: tool content is too large");
|
|
1559
|
+
if (result.success && result.value !== void 0 && !isBoundedJSON(result.value, {
|
|
1560
|
+
bytes: this.#limits.content,
|
|
1561
|
+
depth: this.#limits.depth
|
|
1562
|
+
})) return buildJSONRPCError(id, JSONRPC_SERVER_ERROR, "Server limit exceeded: tool content is too large or unsafe");
|
|
1563
|
+
const built = (0, _orkestrel_contract.attempt)(() => buildCallResult(result));
|
|
1564
|
+
return built.success ? built.value : buildJSONRPCError(id, JSONRPC_SERVER_ERROR, "Server could not serialize tool content");
|
|
562
1565
|
}
|
|
563
1566
|
};
|
|
564
1567
|
//#endregion
|
|
565
1568
|
//#region src/core/MCPClient.ts
|
|
566
1569
|
/**
|
|
567
1570
|
* A transport-agnostic Model Context Protocol CLIENT — connects to a REMOTE MCP server
|
|
568
|
-
* over an injected {@link ClientTransportInterface},
|
|
569
|
-
* and exposes the server's tools as local {@link ToolInterface}s an agent can run.
|
|
1571
|
+
* over an injected {@link ClientTransportInterface}, negotiates the modern or legacy
|
|
1572
|
+
* wire era, and exposes the server's tools as local {@link ToolInterface}s an agent can run.
|
|
570
1573
|
*
|
|
571
1574
|
* @remarks
|
|
572
1575
|
* - **The mirror of `MCPServer`.** The server DISPATCHES requests over a tool registry;
|
|
573
|
-
* this client ISSUES them over a transport. `connect`
|
|
574
|
-
*
|
|
575
|
-
* lists the remote tools and wraps each as a
|
|
1576
|
+
* this client ISSUES them over a transport. `connect` probes `server/discover` unless
|
|
1577
|
+
* pinned legacy, falls back to `initialize` only for a legacy peer, and exposes the
|
|
1578
|
+
* negotiated `version`; `tools()` lists the remote tools and wraps each as a
|
|
576
1579
|
* local {@link ToolInterface} whose `execute` calls back through `call`; `call` runs a
|
|
577
1580
|
* remote `tools/call` and returns the tool's value (a remote `isError: true` throws
|
|
578
1581
|
* locally, so an agent's {@link import('@orkestrel/tool').ToolManagerInterface}
|
|
@@ -581,9 +1584,9 @@ var MCPServer = class {
|
|
|
581
1584
|
* `id` ({@link #nextId}); a single transport `message` subscription resolves / rejects
|
|
582
1585
|
* the matching {@link #pending} entry by `id`. A message that is NOT a response to a
|
|
583
1586
|
* pending request is a server NOTIFICATION — re-surfaced on the `notification` event.
|
|
584
|
-
* - **Per-request deadline.** `#request`
|
|
585
|
-
*
|
|
586
|
-
*
|
|
1587
|
+
* - **Per-request deadline.** Each `#request` receives its own deadline: ordinary calls use
|
|
1588
|
+
* `this.#timeout`, while an explicitly bounded discovery uses the shorter probe deadline.
|
|
1589
|
+
* `AbortSignal.timeout` (never a raw `setTimeout`) rejects only that pending request.
|
|
587
1590
|
* - **Transport-agnostic.** Imports only core siblings (JSON-RPC + the tool vocabulary);
|
|
588
1591
|
* the concrete transport is injected. Wire fields are narrowed via the contracts
|
|
589
1592
|
* guards (no `as`).
|
|
@@ -593,7 +1596,7 @@ var MCPServer = class {
|
|
|
593
1596
|
*
|
|
594
1597
|
* @example
|
|
595
1598
|
* ```ts
|
|
596
|
-
* const client = new MCPClient({ transport, name: 'agent', version: '1.0.0' })
|
|
1599
|
+
* const client = new MCPClient({ transport, identity: { name: 'agent', version: '1.0.0' } })
|
|
597
1600
|
* await client.connect()
|
|
598
1601
|
* const tools = await client.tools()
|
|
599
1602
|
* agent.context.tools.add(tools) // the remote tools are now the agent's
|
|
@@ -603,22 +1606,32 @@ var MCPServer = class {
|
|
|
603
1606
|
var MCPClient = class {
|
|
604
1607
|
#emitter;
|
|
605
1608
|
#transport;
|
|
606
|
-
#
|
|
607
|
-
#
|
|
1609
|
+
#identity;
|
|
1610
|
+
#capabilities;
|
|
1611
|
+
#pin;
|
|
608
1612
|
#timeout;
|
|
1613
|
+
#probe;
|
|
609
1614
|
#pending = /* @__PURE__ */ new Map();
|
|
610
1615
|
#nextId = 0;
|
|
611
1616
|
#connected = false;
|
|
612
|
-
#
|
|
1617
|
+
#version = void 0;
|
|
1618
|
+
#era = void 0;
|
|
1619
|
+
#offer;
|
|
613
1620
|
constructor(options) {
|
|
614
1621
|
this.#emitter = new _orkestrel_emitter.Emitter({
|
|
615
1622
|
...options.on !== void 0 ? { on: options.on } : {},
|
|
616
1623
|
...options.error !== void 0 ? { error: options.error } : {}
|
|
617
1624
|
});
|
|
618
1625
|
this.#transport = options.transport;
|
|
619
|
-
this.#
|
|
620
|
-
|
|
1626
|
+
this.#identity = options.identity ?? {
|
|
1627
|
+
name: "taverna",
|
|
1628
|
+
version: "1.0.0"
|
|
1629
|
+
};
|
|
1630
|
+
this.#capabilities = options.capabilities ?? {};
|
|
1631
|
+
this.#pin = options.version;
|
|
1632
|
+
this.#offer = options.version ?? "2026-07-28";
|
|
621
1633
|
this.#timeout = options.timeout ?? 3e4;
|
|
1634
|
+
this.#probe = options.timeout === void 0 ? void 0 : Math.min(options.timeout, 50);
|
|
622
1635
|
this.#transport.emitter.on("message", (message) => this.#receive(message));
|
|
623
1636
|
}
|
|
624
1637
|
get emitter() {
|
|
@@ -627,8 +1640,8 @@ var MCPClient = class {
|
|
|
627
1640
|
get connected() {
|
|
628
1641
|
return this.#connected;
|
|
629
1642
|
}
|
|
630
|
-
get
|
|
631
|
-
return this.#
|
|
1643
|
+
get version() {
|
|
1644
|
+
return this.#version;
|
|
632
1645
|
}
|
|
633
1646
|
get transport() {
|
|
634
1647
|
return this.#transport;
|
|
@@ -639,38 +1652,68 @@ var MCPClient = class {
|
|
|
639
1652
|
async connect() {
|
|
640
1653
|
if (this.#connected) return;
|
|
641
1654
|
await this.#transport.start();
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
1655
|
+
if (this.#era === "legacy" || this.#pin !== void 0 && inferEra(this.#pin) === "legacy") {
|
|
1656
|
+
await this.#initialize(this.#pin ?? "2025-11-25");
|
|
1657
|
+
return;
|
|
1658
|
+
}
|
|
1659
|
+
let discovery;
|
|
1660
|
+
try {
|
|
1661
|
+
try {
|
|
1662
|
+
discovery = await this.discover();
|
|
1663
|
+
} catch (error) {
|
|
1664
|
+
if (!isMCPError(error) || error.code !== -32022) throw error;
|
|
1665
|
+
if (this.#pin !== void 0) throw error;
|
|
1666
|
+
const supported = (0, _orkestrel_contract.isRecord)(error.context) ? error.context["supported"] : void 0;
|
|
1667
|
+
const retry = (0, _orkestrel_contract.isArray)(supported) ? inferVersion(supported.filter((version) => (0, _orkestrel_contract.isString)(version))) : void 0;
|
|
1668
|
+
if (retry === void 0) throw error;
|
|
1669
|
+
this.#offer = retry;
|
|
1670
|
+
discovery = await this.discover();
|
|
648
1671
|
}
|
|
649
|
-
})
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
if ((0, _orkestrel_contract.isString)(protocol)) throw new Error(`MCP server negotiated unsupported protocol version '${protocol}'`);
|
|
654
|
-
throw new Error("MCP server returned a non-string protocol version");
|
|
1672
|
+
} catch (error) {
|
|
1673
|
+
if (!(this.#pin !== "2026-07-28" && this.#era === void 0 && (!isMCPError(error) || error.code !== -32022))) throw error;
|
|
1674
|
+
await this.#initialize(MCP_PROTOCOL_VERSION);
|
|
1675
|
+
return;
|
|
655
1676
|
}
|
|
656
|
-
|
|
1677
|
+
const version = inferVersion(discovery.supportedVersions);
|
|
1678
|
+
if (version === void 0) throw new MCPError("MCP server supports no compatible protocol version", MCP_UNSUPPORTED_VERSION, { supported: discovery.supportedVersions });
|
|
1679
|
+
this.#version = version;
|
|
1680
|
+
this.#era = "modern";
|
|
657
1681
|
this.#connected = true;
|
|
658
|
-
await this.#transport.send({
|
|
659
|
-
jsonrpc: "2.0",
|
|
660
|
-
method: "notifications/initialized"
|
|
661
|
-
});
|
|
662
1682
|
this.#emitter.emit("connect");
|
|
663
1683
|
}
|
|
1684
|
+
async discover() {
|
|
1685
|
+
const result = await this.#request("server/discover", void 0, this.#probe, this.#version ?? this.#offer);
|
|
1686
|
+
if (!(0, _orkestrel_contract.isRecord)(result)) throw new MCPError("MCP server returned a malformed discovery result", JSONRPC_INVALID_PARAMS, result);
|
|
1687
|
+
const advertised = result["supportedVersions"];
|
|
1688
|
+
const capabilities = result["capabilities"];
|
|
1689
|
+
const ttl = result["ttlMs"];
|
|
1690
|
+
const scope = result["cacheScope"];
|
|
1691
|
+
const instructions = result["instructions"];
|
|
1692
|
+
const metadata = result["_meta"];
|
|
1693
|
+
const resultType = result["resultType"];
|
|
1694
|
+
if (!(0, _orkestrel_contract.isArray)(advertised) || !(0, _orkestrel_contract.isRecord)(capabilities) || !(0, _orkestrel_contract.isNumber)(ttl) || scope !== "public" && scope !== "private" || resultType !== void 0 && resultType !== "complete" || instructions !== void 0 && !(0, _orkestrel_contract.isString)(instructions) || metadata !== void 0 && !(0, _orkestrel_contract.isRecord)(metadata)) throw new MCPError("MCP server returned a malformed discovery result", JSONRPC_INVALID_PARAMS, result);
|
|
1695
|
+
const supportedVersions = [];
|
|
1696
|
+
for (const version of advertised) if (isMCPVersion(version)) supportedVersions.push(version);
|
|
1697
|
+
return {
|
|
1698
|
+
supportedVersions,
|
|
1699
|
+
capabilities,
|
|
1700
|
+
resultType: resultType ?? "complete",
|
|
1701
|
+
ttlMs: ttl,
|
|
1702
|
+
cacheScope: scope,
|
|
1703
|
+
...instructions === void 0 ? {} : { instructions },
|
|
1704
|
+
...metadata === void 0 ? {} : { _meta: metadata }
|
|
1705
|
+
};
|
|
1706
|
+
}
|
|
664
1707
|
async disconnect() {
|
|
665
1708
|
if (!this.#connected) return;
|
|
666
1709
|
this.#connected = false;
|
|
667
|
-
this.#
|
|
1710
|
+
this.#version = void 0;
|
|
668
1711
|
for (const id of this.#pending.keys()) this.#settle(id, /* @__PURE__ */ new Error("MCP client disconnected"), true);
|
|
669
1712
|
await this.#transport.close();
|
|
670
1713
|
this.#emitter.emit("disconnect");
|
|
671
1714
|
}
|
|
672
1715
|
async tools() {
|
|
673
|
-
const result = await this.#request("tools/list");
|
|
1716
|
+
const result = await this.#request("tools/list", void 0, this.#timeout);
|
|
674
1717
|
if (!(0, _orkestrel_contract.isRecord)(result) || !(0, _orkestrel_contract.isArray)(result["tools"])) return [];
|
|
675
1718
|
const tools = [];
|
|
676
1719
|
for (const descriptor of result["tools"]) {
|
|
@@ -684,7 +1727,7 @@ var MCPClient = class {
|
|
|
684
1727
|
const result = await this.#request("tools/call", {
|
|
685
1728
|
name,
|
|
686
1729
|
arguments: args
|
|
687
|
-
});
|
|
1730
|
+
}, this.#timeout);
|
|
688
1731
|
const text = this.#text(result);
|
|
689
1732
|
if ((0, _orkestrel_contract.isRecord)(result) && result["isError"] === true) throw new Error(text.length > 0 ? text : `MCP tool '${name}' failed`);
|
|
690
1733
|
if (text.length === 0) return void 0;
|
|
@@ -694,24 +1737,46 @@ var MCPClient = class {
|
|
|
694
1737
|
return text;
|
|
695
1738
|
}
|
|
696
1739
|
}
|
|
697
|
-
#request(method, params) {
|
|
1740
|
+
#request(method, params, deadline, version) {
|
|
698
1741
|
this.#nextId += 1;
|
|
699
1742
|
const id = this.#nextId;
|
|
1743
|
+
const timeout = deadline;
|
|
1744
|
+
const modern = version ?? (this.#era === "modern" ? this.#version : void 0);
|
|
1745
|
+
const stamped = modern === void 0 ? params : {
|
|
1746
|
+
...params ?? {},
|
|
1747
|
+
_meta: {
|
|
1748
|
+
[MCP_META_VERSION]: modern,
|
|
1749
|
+
[MCP_META_CAPABILITIES]: this.#capabilities,
|
|
1750
|
+
[MCP_META_CLIENT]: this.#identity
|
|
1751
|
+
}
|
|
1752
|
+
};
|
|
700
1753
|
const request = {
|
|
701
1754
|
jsonrpc: "2.0",
|
|
702
1755
|
id,
|
|
703
1756
|
method,
|
|
704
|
-
...
|
|
1757
|
+
...stamped === void 0 ? {} : { params: stamped }
|
|
705
1758
|
};
|
|
706
1759
|
return new Promise((resolve, reject) => {
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
1760
|
+
if (timeout === void 0) {
|
|
1761
|
+
this.#pending.set(id, {
|
|
1762
|
+
resolve,
|
|
1763
|
+
reject,
|
|
1764
|
+
method
|
|
1765
|
+
});
|
|
1766
|
+
this.#transport.send(request).catch((error) => {
|
|
1767
|
+
this.#settle(id, error instanceof Error ? error : new Error(String(error)), true);
|
|
1768
|
+
});
|
|
1769
|
+
return;
|
|
1770
|
+
}
|
|
1771
|
+
const signal = AbortSignal.timeout(timeout);
|
|
1772
|
+
const abort = this.#timeoutRequest.bind(this, id, method, timeout);
|
|
1773
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
710
1774
|
this.#pending.set(id, {
|
|
711
1775
|
resolve,
|
|
712
1776
|
reject,
|
|
713
|
-
|
|
714
|
-
|
|
1777
|
+
method,
|
|
1778
|
+
deadline: signal,
|
|
1779
|
+
timeout: abort
|
|
715
1780
|
});
|
|
716
1781
|
this.#transport.send(request).catch((error) => {
|
|
717
1782
|
this.#settle(id, error instanceof Error ? error : new Error(String(error)), true);
|
|
@@ -720,9 +1785,15 @@ var MCPClient = class {
|
|
|
720
1785
|
}
|
|
721
1786
|
#receive(message) {
|
|
722
1787
|
if (isJSONRPCResponse(message) && isRequestId(message.id)) {
|
|
723
|
-
|
|
1788
|
+
const pending = this.#pending.get(message.id);
|
|
1789
|
+
if (pending !== void 0) {
|
|
1790
|
+
if (pending.method === "server/discover" && (message.error === void 0 || message.error.code !== -32601 && message.error.code !== -32600)) this.#era = "modern";
|
|
724
1791
|
if (message.error !== void 0) this.#settle(message.id, new MCPError(message.error.message, message.error.code, message.error.data), true);
|
|
725
|
-
else
|
|
1792
|
+
else {
|
|
1793
|
+
const resultType = (0, _orkestrel_contract.isRecord)(message.result) ? message.result["resultType"] : void 0;
|
|
1794
|
+
if ((0, _orkestrel_contract.isRecord)(message.result) && Object.hasOwn(message.result, "resultType") && resultType !== "complete") this.#settle(message.id, new MCPError(`MCP result type '${String(resultType)}' is not supported`, JSONRPC_INVALID_PARAMS, { resultType }), true);
|
|
1795
|
+
else this.#settle(message.id, message.result, false);
|
|
1796
|
+
}
|
|
726
1797
|
return;
|
|
727
1798
|
}
|
|
728
1799
|
}
|
|
@@ -745,14 +1816,42 @@ var MCPClient = class {
|
|
|
745
1816
|
for (const block of result["content"]) if ((0, _orkestrel_contract.isRecord)(block) && (0, _orkestrel_contract.isString)(block["text"])) parts.push(block["text"]);
|
|
746
1817
|
return parts.join("\n");
|
|
747
1818
|
}
|
|
748
|
-
#
|
|
749
|
-
|
|
1819
|
+
async #initialize(version) {
|
|
1820
|
+
const result = await this.#request("initialize", {
|
|
1821
|
+
protocolVersion: version,
|
|
1822
|
+
capabilities: {},
|
|
1823
|
+
clientInfo: this.#identity
|
|
1824
|
+
}, this.#timeout);
|
|
1825
|
+
const protocol = (0, _orkestrel_contract.isRecord)(result) ? result["protocolVersion"] : void 0;
|
|
1826
|
+
if (protocol === void 0) {
|
|
1827
|
+
await this.#transport.close();
|
|
1828
|
+
throw new Error("MCP server returned no protocol version");
|
|
1829
|
+
}
|
|
1830
|
+
if (!(0, _orkestrel_contract.isString)(protocol)) {
|
|
1831
|
+
await this.#transport.close();
|
|
1832
|
+
throw new Error("MCP server returned a malformed protocol version");
|
|
1833
|
+
}
|
|
1834
|
+
if (!isMCPVersion(protocol) || inferEra(protocol) !== "legacy") {
|
|
1835
|
+
await this.#transport.close();
|
|
1836
|
+
throw new Error(`MCP server negotiated unsupported protocol version '${protocol}'`);
|
|
1837
|
+
}
|
|
1838
|
+
this.#version = protocol;
|
|
1839
|
+
this.#era = "legacy";
|
|
1840
|
+
this.#connected = true;
|
|
1841
|
+
await this.#transport.send({
|
|
1842
|
+
jsonrpc: "2.0",
|
|
1843
|
+
method: "notifications/initialized"
|
|
1844
|
+
});
|
|
1845
|
+
this.#emitter.emit("connect");
|
|
1846
|
+
}
|
|
1847
|
+
#timeoutRequest(id, method, timeout) {
|
|
1848
|
+
this.#settle(id, /* @__PURE__ */ new Error(`MCP request '${method}' timed out after ${timeout}ms`), true);
|
|
750
1849
|
}
|
|
751
1850
|
#settle(id, value, failed) {
|
|
752
1851
|
const pending = this.#pending.get(id);
|
|
753
1852
|
if (pending === void 0) return;
|
|
754
1853
|
this.#pending.delete(id);
|
|
755
|
-
pending.deadline.removeEventListener("abort", pending.timeout);
|
|
1854
|
+
if (pending.deadline !== void 0 && pending.timeout !== void 0) pending.deadline.removeEventListener("abort", pending.timeout);
|
|
756
1855
|
if (failed) pending.reject(value);
|
|
757
1856
|
else pending.resolve(value);
|
|
758
1857
|
}
|
|
@@ -773,8 +1872,8 @@ var MCPClient = class {
|
|
|
773
1872
|
* `isError: true` tool result), so a misbehaving tool never crashes a dispatch. Subscribe to the
|
|
774
1873
|
* `request` event via `server.emitter.on('request', …)` for tracing.
|
|
775
1874
|
*
|
|
776
|
-
* @param options - `
|
|
777
|
-
* registry to expose),
|
|
1875
|
+
* @param options - `identity` (the server identity), `tools` (the live
|
|
1876
|
+
* registry to expose), optional `instructions`, and the reserved `on`
|
|
778
1877
|
* {@link import('@orkestrel/emitter').EmitterHooks} (see {@link MCPServerOptions})
|
|
779
1878
|
* @returns A working {@link MCPServerInterface}
|
|
780
1879
|
*
|
|
@@ -785,7 +1884,7 @@ var MCPClient = class {
|
|
|
785
1884
|
* const tools = createToolManager()
|
|
786
1885
|
* tools.add(createTool({ name: 'add', execute: (a) => Number(a.x) + Number(a.y) }))
|
|
787
1886
|
*
|
|
788
|
-
* const server = createMCPServer({ name: 'calculator', version: '1.0.0', tools })
|
|
1887
|
+
* const server = createMCPServer({ identity: { name: 'calculator', version: '1.0.0' }, tools })
|
|
789
1888
|
* server.emitter.on('request', (method, id) => log(method, id))
|
|
790
1889
|
*
|
|
791
1890
|
* // A transport pumps message strings through `handle`:
|
|
@@ -814,8 +1913,8 @@ function createMCPServer(options) {
|
|
|
814
1913
|
* to `connect` / `disconnect` / `notification` via `client.on(...)` (or
|
|
815
1914
|
* `client.emitter.on(...)`).
|
|
816
1915
|
*
|
|
817
|
-
* @param options - `transport` (the carrier; REQUIRED),
|
|
818
|
-
* identity), `timeout` (the per-request deadline), and the reserved `on`
|
|
1916
|
+
* @param options - `transport` (the carrier; REQUIRED), an optional `identity`
|
|
1917
|
+
* (the client identity), `timeout` (the per-request deadline), and the reserved `on`
|
|
819
1918
|
* {@link import('@orkestrel/emitter').EmitterHooks} (see {@link MCPClientOptions})
|
|
820
1919
|
* @returns A working {@link MCPClientInterface}
|
|
821
1920
|
*
|
|
@@ -877,8 +1976,11 @@ function createDuplexClientTransport(transport) {
|
|
|
877
1976
|
};
|
|
878
1977
|
}
|
|
879
1978
|
//#endregion
|
|
1979
|
+
exports.DEFAULT_MCP_CACHE_TTL = DEFAULT_MCP_CACHE_TTL;
|
|
880
1980
|
exports.DEFAULT_MCP_CLIENT_NAME = DEFAULT_MCP_CLIENT_NAME;
|
|
881
1981
|
exports.DEFAULT_MCP_CLIENT_VERSION = DEFAULT_MCP_CLIENT_VERSION;
|
|
1982
|
+
exports.DEFAULT_MCP_LIMITS = DEFAULT_MCP_LIMITS;
|
|
1983
|
+
exports.DEFAULT_MCP_PROBE_TIMEOUT = DEFAULT_MCP_PROBE_TIMEOUT;
|
|
882
1984
|
exports.DEFAULT_MCP_REQUEST_TIMEOUT = DEFAULT_MCP_REQUEST_TIMEOUT;
|
|
883
1985
|
exports.JSONRPC_INVALID_PARAMS = JSONRPC_INVALID_PARAMS;
|
|
884
1986
|
exports.JSONRPC_INVALID_REQUEST = JSONRPC_INVALID_REQUEST;
|
|
@@ -887,25 +1989,63 @@ exports.JSONRPC_PARSE_ERROR = JSONRPC_PARSE_ERROR;
|
|
|
887
1989
|
exports.JSONRPC_SERVER_ERROR = JSONRPC_SERVER_ERROR;
|
|
888
1990
|
exports.MCPClient = MCPClient;
|
|
889
1991
|
exports.MCPError = MCPError;
|
|
1992
|
+
exports.MCPMethodManager = MCPMethodManager;
|
|
890
1993
|
exports.MCPServer = MCPServer;
|
|
1994
|
+
exports.MCP_HEADER_MISMATCH = MCP_HEADER_MISMATCH;
|
|
1995
|
+
exports.MCP_LEGACY_VERSION = MCP_LEGACY_VERSION;
|
|
1996
|
+
exports.MCP_META_CAPABILITIES = MCP_META_CAPABILITIES;
|
|
1997
|
+
exports.MCP_META_CLIENT = MCP_META_CLIENT;
|
|
1998
|
+
exports.MCP_META_SERVER = MCP_META_SERVER;
|
|
1999
|
+
exports.MCP_META_SUBSCRIPTION = MCP_META_SUBSCRIPTION;
|
|
2000
|
+
exports.MCP_META_VERSION = MCP_META_VERSION;
|
|
2001
|
+
exports.MCP_MISSING_CAPABILITY = MCP_MISSING_CAPABILITY;
|
|
2002
|
+
exports.MCP_MODERN_VERSION = MCP_MODERN_VERSION;
|
|
891
2003
|
exports.MCP_PROTOCOL_VERSION = MCP_PROTOCOL_VERSION;
|
|
2004
|
+
exports.MCP_UNSUPPORTED_VERSION = MCP_UNSUPPORTED_VERSION;
|
|
892
2005
|
exports.SUPPORTED_PROTOCOL_VERSIONS = SUPPORTED_PROTOCOL_VERSIONS;
|
|
893
2006
|
exports.bindClient = bindClient;
|
|
894
2007
|
exports.bindServer = bindServer;
|
|
2008
|
+
exports.buildCallResult = buildCallResult;
|
|
2009
|
+
exports.buildDiscoverResult = buildDiscoverResult;
|
|
2010
|
+
exports.buildInitializeResult = buildInitializeResult;
|
|
2011
|
+
exports.buildJSONRPCError = buildJSONRPCError;
|
|
2012
|
+
exports.buildJSONRPCResult = buildJSONRPCResult;
|
|
2013
|
+
exports.buildModernResult = buildModernResult;
|
|
2014
|
+
exports.buildSubscriptionAcknowledgement = buildSubscriptionAcknowledgement;
|
|
2015
|
+
exports.buildSubscriptionFilter = buildSubscriptionFilter;
|
|
2016
|
+
exports.buildSubscriptionResult = buildSubscriptionResult;
|
|
895
2017
|
exports.buildToolDescriptors = buildToolDescriptors;
|
|
896
|
-
exports.buildToolResult = buildToolResult;
|
|
897
2018
|
exports.createDuplexClientTransport = createDuplexClientTransport;
|
|
898
2019
|
exports.createMCPClient = createMCPClient;
|
|
899
2020
|
exports.createMCPServer = createMCPServer;
|
|
900
|
-
exports.
|
|
2021
|
+
exports.inferEra = inferEra;
|
|
2022
|
+
exports.inferVersion = inferVersion;
|
|
2023
|
+
exports.isBoundedJSON = isBoundedJSON;
|
|
2024
|
+
exports.isBoundedString = isBoundedString;
|
|
2025
|
+
exports.isElicitPrimitiveSchema = isElicitPrimitiveSchema;
|
|
2026
|
+
exports.isElicitRequest = isElicitRequest;
|
|
2027
|
+
exports.isElicitRequestFormParams = isElicitRequestFormParams;
|
|
2028
|
+
exports.isElicitRequestURLParams = isElicitRequestURLParams;
|
|
2029
|
+
exports.isElicitResult = isElicitResult;
|
|
2030
|
+
exports.isFormElicitationSupported = isFormElicitationSupported;
|
|
901
2031
|
exports.isInitializeRequest = isInitializeRequest;
|
|
2032
|
+
exports.isInputRequest = isInputRequest;
|
|
2033
|
+
exports.isInputRequests = isInputRequests;
|
|
2034
|
+
exports.isInputRequiredResult = isInputRequiredResult;
|
|
902
2035
|
exports.isJSONRPCMessage = isJSONRPCMessage;
|
|
903
2036
|
exports.isJSONRPCRequest = isJSONRPCRequest;
|
|
904
2037
|
exports.isJSONRPCResponse = isJSONRPCResponse;
|
|
905
2038
|
exports.isMCPError = isMCPError;
|
|
2039
|
+
exports.isMCPVersion = isMCPVersion;
|
|
2040
|
+
exports.isModernRequest = isModernRequest;
|
|
906
2041
|
exports.isRequestId = isRequestId;
|
|
907
|
-
exports.
|
|
908
|
-
exports.
|
|
2042
|
+
exports.isSubscriptionFilter = isSubscriptionFilter;
|
|
2043
|
+
exports.matchesSubscriptionNotification = matchesSubscriptionNotification;
|
|
909
2044
|
exports.parseJSONRPCMessage = parseJSONRPCMessage;
|
|
2045
|
+
exports.parseMCPInputState = parseMCPInputState;
|
|
2046
|
+
exports.parseRequestContext = parseRequestContext;
|
|
2047
|
+
exports.sendStream = sendStream;
|
|
2048
|
+
exports.serializeStream = serializeStream;
|
|
2049
|
+
exports.stampSubscriptionNotification = stampSubscriptionNotification;
|
|
910
2050
|
|
|
911
2051
|
//# sourceMappingURL=index.cjs.map
|