@orkestrel/mcp 0.0.8 → 0.0.10

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.
@@ -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
- /** The MCP protocol revision this server implements (the default negotiated version). */
7
- var MCP_PROTOCOL_VERSION = "2025-06-18";
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, else falls back to {@link MCP_PROTOCOL_VERSION}. Frozen so the list is
14
- * an immutable contract. The package does not advertise `2025-03-26` because that
15
- * revision mandates JSON-RPC batching, while this package accepts only individual
16
- * JSON-RPC messages.
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 SUPPORTED_PROTOCOL_VERSIONS = Object.freeze(["2025-06-18"]);
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('Method not found', -32601, { method: 'missing' })
53
- * error.code // -32601
54
- * error.context // { method: 'missing' }
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 jsonRPCResult(id, result) {
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 jsonRPCError(id, code, message, data) {
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 MCPToolResult} — the
288
- * value (or error) as a `text` content block.
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; the `success: true` branch serializes `result.value` (via
296
- * `JSON.stringify`) into one `text` block.
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 buildToolResult(result) {
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: result.value === void 0 ? "" : JSON.stringify(result.value)
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
- * {@link SUPPORTED_PROTOCOL_VERSIONS}, else falls back to {@link MCP_PROTOCOL_VERSION}.
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 initializeResult(name, version, requested) {
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 !== void 0 && SUPPORTED_PROTOCOL_VERSIONS.includes(requested) ? requested : MCP_PROTOCOL_VERSION,
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 `transport.send` throw or rejection is caught and routed
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 response = await server.handle(message);
377
- if (response !== void 0) await transport.send(response);
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
- * - **The method switch.** `initialize` negotiates the protocol version + advertises
479
- * the tools capability; `notifications/initialized` is a notification (no
480
- * response); `ping` returns `{}`; `tools/list` lists the registry's tools (its
481
- * `parameters` renamed to `inputSchema`); `tools/call` runs a tool by name (the
482
- * {@link ToolManagerInterface} isolates a tool throw into a `success: false`
483
- * result, which maps to an `isError: true` tool result — so the server adds NO
484
- * try/catch). An unknown method `-32601`; a `tools/call` with a missing /
485
- * non-string `name` `-32602`.
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,249 @@ 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
- #name;
503
- #version;
504
- #tools;
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.#name = options.name;
511
- this.#version = options.version;
512
- this.#tools = options.tools;
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 name() {
518
- return this.#name;
1368
+ get identity() {
1369
+ return this.#options.identity;
519
1370
  }
520
- get version() {
521
- return this.#version;
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
- this.#emitter.emit("request", request.method, id);
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, options);
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, options) {
527
1403
  switch (request.method) {
528
1404
  case "initialize": {
529
1405
  const requested = request.params?.["protocolVersion"];
530
- return jsonRPCResult(id, initializeResult(this.#name, this.#version, (0, _orkestrel_contract.isString)(requested) ? requested : void 0));
1406
+ return buildJSONRPCResult(id, buildInitializeResult(this.#options.identity.name, this.#options.identity.version, (0, _orkestrel_contract.isString)(requested) ? requested : void 0));
1407
+ }
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, options);
1412
+ return "jsonrpc" in result ? result : buildJSONRPCResult(id, result);
531
1413
  }
532
- case "ping": return jsonRPCResult(id, {});
533
- case "tools/list": return jsonRPCResult(id, { tools: buildToolDescriptors(this.#tools) });
534
- case "tools/call": return this.#call(request, id);
535
- default: return jsonRPCError(id, JSONRPC_METHOD_NOT_FOUND, `Method not found: ${request.method}`);
1414
+ default: return buildJSONRPCError(id, JSONRPC_METHOD_NOT_FOUND, `Method not found: ${request.method}`);
536
1415
  }
537
1416
  }
538
- async handle(message) {
539
- let parsed;
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, options);
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, options);
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, options);
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
- parsed = JSON.parse(message);
542
- } catch {
543
- return JSON.stringify(jsonRPCError(null, JSONRPC_PARSE_ERROR, "Parse error"));
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 #call(request, id) {
1546
+ async #runTool(request, id, options) {
551
1547
  const params = request.params;
552
1548
  const name = params?.["name"];
553
- if (!(0, _orkestrel_contract.isString)(name)) return jsonRPCError(id, JSONRPC_INVALID_PARAMS, "Invalid params: a string `name` is required");
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
- return jsonRPCResult(id, buildToolResult(await this.#tools.execute({
1553
+ const result = await this.#options.tools.execute({
558
1554
  id: callId,
559
1555
  name,
560
- arguments: args
561
- })));
1556
+ arguments: args,
1557
+ ...options.caller === void 0 ? {} : { caller: options.caller }
1558
+ });
1559
+ if (!result.success && !isBoundedString(result.error, this.#limits.content)) return buildJSONRPCError(id, JSONRPC_SERVER_ERROR, "Server limit exceeded: tool content is too large");
1560
+ if (result.success && result.value !== void 0 && !isBoundedJSON(result.value, {
1561
+ bytes: this.#limits.content,
1562
+ depth: this.#limits.depth
1563
+ })) return buildJSONRPCError(id, JSONRPC_SERVER_ERROR, "Server limit exceeded: tool content is too large or unsafe");
1564
+ const built = (0, _orkestrel_contract.attempt)(() => buildCallResult(result));
1565
+ return built.success ? built.value : buildJSONRPCError(id, JSONRPC_SERVER_ERROR, "Server could not serialize tool content");
562
1566
  }
563
1567
  };
564
1568
  //#endregion
565
1569
  //#region src/core/MCPClient.ts
566
1570
  /**
567
1571
  * A transport-agnostic Model Context Protocol CLIENT — connects to a REMOTE MCP server
568
- * over an injected {@link ClientTransportInterface}, runs the `initialize` handshake,
569
- * and exposes the server's tools as local {@link ToolInterface}s an agent can run.
1572
+ * over an injected {@link ClientTransportInterface}, negotiates the modern or legacy
1573
+ * wire era, and exposes the server's tools as local {@link ToolInterface}s an agent can run.
570
1574
  *
571
1575
  * @remarks
572
1576
  * - **The mirror of `MCPServer`.** The server DISPATCHES requests over a tool registry;
573
- * this client ISSUES them over a transport. `connect` runs `initialize`, validates and
574
- * exposes the negotiated `protocol`, then sends `notifications/initialized`; `tools()`
575
- * lists the remote tools and wraps each as a
1577
+ * this client ISSUES them over a transport. `connect` probes `server/discover` unless
1578
+ * pinned legacy, falls back to `initialize` only for a legacy peer, and exposes the
1579
+ * negotiated `version`; `tools()` lists the remote tools and wraps each as a
576
1580
  * local {@link ToolInterface} whose `execute` calls back through `call`; `call` runs a
577
1581
  * remote `tools/call` and returns the tool's value (a remote `isError: true` throws
578
1582
  * locally, so an agent's {@link import('@orkestrel/tool').ToolManagerInterface}
@@ -581,9 +1585,9 @@ var MCPServer = class {
581
1585
  * `id` ({@link #nextId}); a single transport `message` subscription resolves / rejects
582
1586
  * the matching {@link #pending} entry by `id`. A message that is NOT a response to a
583
1587
  * pending request is a server NOTIFICATION — re-surfaced on the `notification` event.
584
- * - **Per-request deadline.** `#request` races `AbortSignal.timeout(this.#timeout)` (the
585
- * taverna idiom never a raw `setTimeout`): a server that never replies REJECTS the
586
- * pending request once the deadline fires, never hanging.
1588
+ * - **Per-request deadline.** Each `#request` receives its own deadline: ordinary calls use
1589
+ * `this.#timeout`, while an explicitly bounded discovery uses the shorter probe deadline.
1590
+ * `AbortSignal.timeout` (never a raw `setTimeout`) rejects only that pending request.
587
1591
  * - **Transport-agnostic.** Imports only core siblings (JSON-RPC + the tool vocabulary);
588
1592
  * the concrete transport is injected. Wire fields are narrowed via the contracts
589
1593
  * guards (no `as`).
@@ -593,7 +1597,7 @@ var MCPServer = class {
593
1597
  *
594
1598
  * @example
595
1599
  * ```ts
596
- * const client = new MCPClient({ transport, name: 'agent', version: '1.0.0' })
1600
+ * const client = new MCPClient({ transport, identity: { name: 'agent', version: '1.0.0' } })
597
1601
  * await client.connect()
598
1602
  * const tools = await client.tools()
599
1603
  * agent.context.tools.add(tools) // the remote tools are now the agent's
@@ -603,22 +1607,32 @@ var MCPServer = class {
603
1607
  var MCPClient = class {
604
1608
  #emitter;
605
1609
  #transport;
606
- #name;
607
- #version;
1610
+ #identity;
1611
+ #capabilities;
1612
+ #pin;
608
1613
  #timeout;
1614
+ #probe;
609
1615
  #pending = /* @__PURE__ */ new Map();
610
1616
  #nextId = 0;
611
1617
  #connected = false;
612
- #protocol = void 0;
1618
+ #version = void 0;
1619
+ #era = void 0;
1620
+ #offer;
613
1621
  constructor(options) {
614
1622
  this.#emitter = new _orkestrel_emitter.Emitter({
615
1623
  ...options.on !== void 0 ? { on: options.on } : {},
616
1624
  ...options.error !== void 0 ? { error: options.error } : {}
617
1625
  });
618
1626
  this.#transport = options.transport;
619
- this.#name = options.name ?? "taverna";
620
- this.#version = options.version ?? "1.0.0";
1627
+ this.#identity = options.identity ?? {
1628
+ name: "taverna",
1629
+ version: "1.0.0"
1630
+ };
1631
+ this.#capabilities = options.capabilities ?? {};
1632
+ this.#pin = options.version;
1633
+ this.#offer = options.version ?? "2026-07-28";
621
1634
  this.#timeout = options.timeout ?? 3e4;
1635
+ this.#probe = options.timeout === void 0 ? void 0 : Math.min(options.timeout, 50);
622
1636
  this.#transport.emitter.on("message", (message) => this.#receive(message));
623
1637
  }
624
1638
  get emitter() {
@@ -627,8 +1641,8 @@ var MCPClient = class {
627
1641
  get connected() {
628
1642
  return this.#connected;
629
1643
  }
630
- get protocol() {
631
- return this.#protocol;
1644
+ get version() {
1645
+ return this.#version;
632
1646
  }
633
1647
  get transport() {
634
1648
  return this.#transport;
@@ -639,38 +1653,68 @@ var MCPClient = class {
639
1653
  async connect() {
640
1654
  if (this.#connected) return;
641
1655
  await this.#transport.start();
642
- const result = await this.#request("initialize", {
643
- protocolVersion: MCP_PROTOCOL_VERSION,
644
- capabilities: {},
645
- clientInfo: {
646
- name: this.#name,
647
- version: this.#version
1656
+ if (this.#era === "legacy" || this.#pin !== void 0 && inferEra(this.#pin) === "legacy") {
1657
+ await this.#initialize(this.#pin ?? "2025-11-25");
1658
+ return;
1659
+ }
1660
+ let discovery;
1661
+ try {
1662
+ try {
1663
+ discovery = await this.discover();
1664
+ } catch (error) {
1665
+ if (!isMCPError(error) || error.code !== -32022) throw error;
1666
+ if (this.#pin !== void 0) throw error;
1667
+ const supported = (0, _orkestrel_contract.isRecord)(error.context) ? error.context["supported"] : void 0;
1668
+ const retry = (0, _orkestrel_contract.isArray)(supported) ? inferVersion(supported.filter((version) => (0, _orkestrel_contract.isString)(version))) : void 0;
1669
+ if (retry === void 0) throw error;
1670
+ this.#offer = retry;
1671
+ discovery = await this.discover();
648
1672
  }
649
- });
650
- const protocol = (0, _orkestrel_contract.isRecord)(result) ? result["protocolVersion"] : void 0;
651
- if (!(0, _orkestrel_contract.isString)(protocol) || !SUPPORTED_PROTOCOL_VERSIONS.includes(protocol)) {
652
- await this.#transport.close();
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");
1673
+ } catch (error) {
1674
+ if (!(this.#pin !== "2026-07-28" && this.#era === void 0 && (!isMCPError(error) || error.code !== -32022))) throw error;
1675
+ await this.#initialize(MCP_PROTOCOL_VERSION);
1676
+ return;
655
1677
  }
656
- this.#protocol = protocol;
1678
+ const version = inferVersion(discovery.supportedVersions);
1679
+ if (version === void 0) throw new MCPError("MCP server supports no compatible protocol version", MCP_UNSUPPORTED_VERSION, { supported: discovery.supportedVersions });
1680
+ this.#version = version;
1681
+ this.#era = "modern";
657
1682
  this.#connected = true;
658
- await this.#transport.send({
659
- jsonrpc: "2.0",
660
- method: "notifications/initialized"
661
- });
662
1683
  this.#emitter.emit("connect");
663
1684
  }
1685
+ async discover() {
1686
+ const result = await this.#request("server/discover", void 0, this.#probe, this.#version ?? this.#offer);
1687
+ if (!(0, _orkestrel_contract.isRecord)(result)) throw new MCPError("MCP server returned a malformed discovery result", JSONRPC_INVALID_PARAMS, result);
1688
+ const advertised = result["supportedVersions"];
1689
+ const capabilities = result["capabilities"];
1690
+ const ttl = result["ttlMs"];
1691
+ const scope = result["cacheScope"];
1692
+ const instructions = result["instructions"];
1693
+ const metadata = result["_meta"];
1694
+ const resultType = result["resultType"];
1695
+ 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);
1696
+ const supportedVersions = [];
1697
+ for (const version of advertised) if (isMCPVersion(version)) supportedVersions.push(version);
1698
+ return {
1699
+ supportedVersions,
1700
+ capabilities,
1701
+ resultType: resultType ?? "complete",
1702
+ ttlMs: ttl,
1703
+ cacheScope: scope,
1704
+ ...instructions === void 0 ? {} : { instructions },
1705
+ ...metadata === void 0 ? {} : { _meta: metadata }
1706
+ };
1707
+ }
664
1708
  async disconnect() {
665
1709
  if (!this.#connected) return;
666
1710
  this.#connected = false;
667
- this.#protocol = void 0;
1711
+ this.#version = void 0;
668
1712
  for (const id of this.#pending.keys()) this.#settle(id, /* @__PURE__ */ new Error("MCP client disconnected"), true);
669
1713
  await this.#transport.close();
670
1714
  this.#emitter.emit("disconnect");
671
1715
  }
672
1716
  async tools() {
673
- const result = await this.#request("tools/list");
1717
+ const result = await this.#request("tools/list", void 0, this.#timeout);
674
1718
  if (!(0, _orkestrel_contract.isRecord)(result) || !(0, _orkestrel_contract.isArray)(result["tools"])) return [];
675
1719
  const tools = [];
676
1720
  for (const descriptor of result["tools"]) {
@@ -684,7 +1728,7 @@ var MCPClient = class {
684
1728
  const result = await this.#request("tools/call", {
685
1729
  name,
686
1730
  arguments: args
687
- });
1731
+ }, this.#timeout);
688
1732
  const text = this.#text(result);
689
1733
  if ((0, _orkestrel_contract.isRecord)(result) && result["isError"] === true) throw new Error(text.length > 0 ? text : `MCP tool '${name}' failed`);
690
1734
  if (text.length === 0) return void 0;
@@ -694,24 +1738,46 @@ var MCPClient = class {
694
1738
  return text;
695
1739
  }
696
1740
  }
697
- #request(method, params) {
1741
+ #request(method, params, deadline, version) {
698
1742
  this.#nextId += 1;
699
1743
  const id = this.#nextId;
1744
+ const timeout = deadline;
1745
+ const modern = version ?? (this.#era === "modern" ? this.#version : void 0);
1746
+ const stamped = modern === void 0 ? params : {
1747
+ ...params ?? {},
1748
+ _meta: {
1749
+ [MCP_META_VERSION]: modern,
1750
+ [MCP_META_CAPABILITIES]: this.#capabilities,
1751
+ [MCP_META_CLIENT]: this.#identity
1752
+ }
1753
+ };
700
1754
  const request = {
701
1755
  jsonrpc: "2.0",
702
1756
  id,
703
1757
  method,
704
- ...params === void 0 ? {} : { params }
1758
+ ...stamped === void 0 ? {} : { params: stamped }
705
1759
  };
706
1760
  return new Promise((resolve, reject) => {
707
- const deadline = AbortSignal.timeout(this.#timeout);
708
- const timeout = this.#timeoutRequest.bind(this, id, method);
709
- deadline.addEventListener("abort", timeout, { once: true });
1761
+ if (timeout === void 0) {
1762
+ this.#pending.set(id, {
1763
+ resolve,
1764
+ reject,
1765
+ method
1766
+ });
1767
+ this.#transport.send(request).catch((error) => {
1768
+ this.#settle(id, error instanceof Error ? error : new Error(String(error)), true);
1769
+ });
1770
+ return;
1771
+ }
1772
+ const signal = AbortSignal.timeout(timeout);
1773
+ const abort = this.#timeoutRequest.bind(this, id, method, timeout);
1774
+ signal.addEventListener("abort", abort, { once: true });
710
1775
  this.#pending.set(id, {
711
1776
  resolve,
712
1777
  reject,
713
- deadline,
714
- timeout
1778
+ method,
1779
+ deadline: signal,
1780
+ timeout: abort
715
1781
  });
716
1782
  this.#transport.send(request).catch((error) => {
717
1783
  this.#settle(id, error instanceof Error ? error : new Error(String(error)), true);
@@ -720,9 +1786,15 @@ var MCPClient = class {
720
1786
  }
721
1787
  #receive(message) {
722
1788
  if (isJSONRPCResponse(message) && isRequestId(message.id)) {
723
- if (this.#pending.has(message.id)) {
1789
+ const pending = this.#pending.get(message.id);
1790
+ if (pending !== void 0) {
1791
+ if (pending.method === "server/discover" && (message.error === void 0 || message.error.code !== -32601 && message.error.code !== -32600)) this.#era = "modern";
724
1792
  if (message.error !== void 0) this.#settle(message.id, new MCPError(message.error.message, message.error.code, message.error.data), true);
725
- else this.#settle(message.id, message.result, false);
1793
+ else {
1794
+ const resultType = (0, _orkestrel_contract.isRecord)(message.result) ? message.result["resultType"] : void 0;
1795
+ 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);
1796
+ else this.#settle(message.id, message.result, false);
1797
+ }
726
1798
  return;
727
1799
  }
728
1800
  }
@@ -745,14 +1817,42 @@ var MCPClient = class {
745
1817
  for (const block of result["content"]) if ((0, _orkestrel_contract.isRecord)(block) && (0, _orkestrel_contract.isString)(block["text"])) parts.push(block["text"]);
746
1818
  return parts.join("\n");
747
1819
  }
748
- #timeoutRequest(id, method) {
749
- this.#settle(id, /* @__PURE__ */ new Error(`MCP request '${method}' timed out after ${this.#timeout}ms`), true);
1820
+ async #initialize(version) {
1821
+ const result = await this.#request("initialize", {
1822
+ protocolVersion: version,
1823
+ capabilities: {},
1824
+ clientInfo: this.#identity
1825
+ }, this.#timeout);
1826
+ const protocol = (0, _orkestrel_contract.isRecord)(result) ? result["protocolVersion"] : void 0;
1827
+ if (protocol === void 0) {
1828
+ await this.#transport.close();
1829
+ throw new Error("MCP server returned no protocol version");
1830
+ }
1831
+ if (!(0, _orkestrel_contract.isString)(protocol)) {
1832
+ await this.#transport.close();
1833
+ throw new Error("MCP server returned a malformed protocol version");
1834
+ }
1835
+ if (!isMCPVersion(protocol) || inferEra(protocol) !== "legacy") {
1836
+ await this.#transport.close();
1837
+ throw new Error(`MCP server negotiated unsupported protocol version '${protocol}'`);
1838
+ }
1839
+ this.#version = protocol;
1840
+ this.#era = "legacy";
1841
+ this.#connected = true;
1842
+ await this.#transport.send({
1843
+ jsonrpc: "2.0",
1844
+ method: "notifications/initialized"
1845
+ });
1846
+ this.#emitter.emit("connect");
1847
+ }
1848
+ #timeoutRequest(id, method, timeout) {
1849
+ this.#settle(id, /* @__PURE__ */ new Error(`MCP request '${method}' timed out after ${timeout}ms`), true);
750
1850
  }
751
1851
  #settle(id, value, failed) {
752
1852
  const pending = this.#pending.get(id);
753
1853
  if (pending === void 0) return;
754
1854
  this.#pending.delete(id);
755
- pending.deadline.removeEventListener("abort", pending.timeout);
1855
+ if (pending.deadline !== void 0 && pending.timeout !== void 0) pending.deadline.removeEventListener("abort", pending.timeout);
756
1856
  if (failed) pending.reject(value);
757
1857
  else pending.resolve(value);
758
1858
  }
@@ -773,8 +1873,8 @@ var MCPClient = class {
773
1873
  * `isError: true` tool result), so a misbehaving tool never crashes a dispatch. Subscribe to the
774
1874
  * `request` event via `server.emitter.on('request', …)` for tracing.
775
1875
  *
776
- * @param options - `name` / `version` (the server identity), `tools` (the live
777
- * registry to expose), an optional `description`, and the reserved `on`
1876
+ * @param options - `identity` (the server identity), `tools` (the live
1877
+ * registry to expose), optional `instructions`, and the reserved `on`
778
1878
  * {@link import('@orkestrel/emitter').EmitterHooks} (see {@link MCPServerOptions})
779
1879
  * @returns A working {@link MCPServerInterface}
780
1880
  *
@@ -785,7 +1885,7 @@ var MCPClient = class {
785
1885
  * const tools = createToolManager()
786
1886
  * tools.add(createTool({ name: 'add', execute: (a) => Number(a.x) + Number(a.y) }))
787
1887
  *
788
- * const server = createMCPServer({ name: 'calculator', version: '1.0.0', tools })
1888
+ * const server = createMCPServer({ identity: { name: 'calculator', version: '1.0.0' }, tools })
789
1889
  * server.emitter.on('request', (method, id) => log(method, id))
790
1890
  *
791
1891
  * // A transport pumps message strings through `handle`:
@@ -814,8 +1914,8 @@ function createMCPServer(options) {
814
1914
  * to `connect` / `disconnect` / `notification` via `client.on(...)` (or
815
1915
  * `client.emitter.on(...)`).
816
1916
  *
817
- * @param options - `transport` (the carrier; REQUIRED), `name` / `version` (the client
818
- * identity), `timeout` (the per-request deadline), and the reserved `on`
1917
+ * @param options - `transport` (the carrier; REQUIRED), an optional `identity`
1918
+ * (the client identity), `timeout` (the per-request deadline), and the reserved `on`
819
1919
  * {@link import('@orkestrel/emitter').EmitterHooks} (see {@link MCPClientOptions})
820
1920
  * @returns A working {@link MCPClientInterface}
821
1921
  *
@@ -877,8 +1977,11 @@ function createDuplexClientTransport(transport) {
877
1977
  };
878
1978
  }
879
1979
  //#endregion
1980
+ exports.DEFAULT_MCP_CACHE_TTL = DEFAULT_MCP_CACHE_TTL;
880
1981
  exports.DEFAULT_MCP_CLIENT_NAME = DEFAULT_MCP_CLIENT_NAME;
881
1982
  exports.DEFAULT_MCP_CLIENT_VERSION = DEFAULT_MCP_CLIENT_VERSION;
1983
+ exports.DEFAULT_MCP_LIMITS = DEFAULT_MCP_LIMITS;
1984
+ exports.DEFAULT_MCP_PROBE_TIMEOUT = DEFAULT_MCP_PROBE_TIMEOUT;
882
1985
  exports.DEFAULT_MCP_REQUEST_TIMEOUT = DEFAULT_MCP_REQUEST_TIMEOUT;
883
1986
  exports.JSONRPC_INVALID_PARAMS = JSONRPC_INVALID_PARAMS;
884
1987
  exports.JSONRPC_INVALID_REQUEST = JSONRPC_INVALID_REQUEST;
@@ -887,25 +1990,63 @@ exports.JSONRPC_PARSE_ERROR = JSONRPC_PARSE_ERROR;
887
1990
  exports.JSONRPC_SERVER_ERROR = JSONRPC_SERVER_ERROR;
888
1991
  exports.MCPClient = MCPClient;
889
1992
  exports.MCPError = MCPError;
1993
+ exports.MCPMethodManager = MCPMethodManager;
890
1994
  exports.MCPServer = MCPServer;
1995
+ exports.MCP_HEADER_MISMATCH = MCP_HEADER_MISMATCH;
1996
+ exports.MCP_LEGACY_VERSION = MCP_LEGACY_VERSION;
1997
+ exports.MCP_META_CAPABILITIES = MCP_META_CAPABILITIES;
1998
+ exports.MCP_META_CLIENT = MCP_META_CLIENT;
1999
+ exports.MCP_META_SERVER = MCP_META_SERVER;
2000
+ exports.MCP_META_SUBSCRIPTION = MCP_META_SUBSCRIPTION;
2001
+ exports.MCP_META_VERSION = MCP_META_VERSION;
2002
+ exports.MCP_MISSING_CAPABILITY = MCP_MISSING_CAPABILITY;
2003
+ exports.MCP_MODERN_VERSION = MCP_MODERN_VERSION;
891
2004
  exports.MCP_PROTOCOL_VERSION = MCP_PROTOCOL_VERSION;
2005
+ exports.MCP_UNSUPPORTED_VERSION = MCP_UNSUPPORTED_VERSION;
892
2006
  exports.SUPPORTED_PROTOCOL_VERSIONS = SUPPORTED_PROTOCOL_VERSIONS;
893
2007
  exports.bindClient = bindClient;
894
2008
  exports.bindServer = bindServer;
2009
+ exports.buildCallResult = buildCallResult;
2010
+ exports.buildDiscoverResult = buildDiscoverResult;
2011
+ exports.buildInitializeResult = buildInitializeResult;
2012
+ exports.buildJSONRPCError = buildJSONRPCError;
2013
+ exports.buildJSONRPCResult = buildJSONRPCResult;
2014
+ exports.buildModernResult = buildModernResult;
2015
+ exports.buildSubscriptionAcknowledgement = buildSubscriptionAcknowledgement;
2016
+ exports.buildSubscriptionFilter = buildSubscriptionFilter;
2017
+ exports.buildSubscriptionResult = buildSubscriptionResult;
895
2018
  exports.buildToolDescriptors = buildToolDescriptors;
896
- exports.buildToolResult = buildToolResult;
897
2019
  exports.createDuplexClientTransport = createDuplexClientTransport;
898
2020
  exports.createMCPClient = createMCPClient;
899
2021
  exports.createMCPServer = createMCPServer;
900
- exports.initializeResult = initializeResult;
2022
+ exports.inferEra = inferEra;
2023
+ exports.inferVersion = inferVersion;
2024
+ exports.isBoundedJSON = isBoundedJSON;
2025
+ exports.isBoundedString = isBoundedString;
2026
+ exports.isElicitPrimitiveSchema = isElicitPrimitiveSchema;
2027
+ exports.isElicitRequest = isElicitRequest;
2028
+ exports.isElicitRequestFormParams = isElicitRequestFormParams;
2029
+ exports.isElicitRequestURLParams = isElicitRequestURLParams;
2030
+ exports.isElicitResult = isElicitResult;
2031
+ exports.isFormElicitationSupported = isFormElicitationSupported;
901
2032
  exports.isInitializeRequest = isInitializeRequest;
2033
+ exports.isInputRequest = isInputRequest;
2034
+ exports.isInputRequests = isInputRequests;
2035
+ exports.isInputRequiredResult = isInputRequiredResult;
902
2036
  exports.isJSONRPCMessage = isJSONRPCMessage;
903
2037
  exports.isJSONRPCRequest = isJSONRPCRequest;
904
2038
  exports.isJSONRPCResponse = isJSONRPCResponse;
905
2039
  exports.isMCPError = isMCPError;
2040
+ exports.isMCPVersion = isMCPVersion;
2041
+ exports.isModernRequest = isModernRequest;
906
2042
  exports.isRequestId = isRequestId;
907
- exports.jsonRPCError = jsonRPCError;
908
- exports.jsonRPCResult = jsonRPCResult;
2043
+ exports.isSubscriptionFilter = isSubscriptionFilter;
2044
+ exports.matchesSubscriptionNotification = matchesSubscriptionNotification;
909
2045
  exports.parseJSONRPCMessage = parseJSONRPCMessage;
2046
+ exports.parseMCPInputState = parseMCPInputState;
2047
+ exports.parseRequestContext = parseRequestContext;
2048
+ exports.sendStream = sendStream;
2049
+ exports.serializeStream = serializeStream;
2050
+ exports.stampSubscriptionNotification = stampSubscriptionNotification;
910
2051
 
911
2052
  //# sourceMappingURL=index.cjs.map