@orkestrel/mcp 0.0.25 → 0.0.27

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,5 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  let _orkestrel_contract = require("@orkestrel/contract");
3
+ let _orkestrel_codec = require("@orkestrel/codec");
3
4
  let _orkestrel_emitter = require("@orkestrel/emitter");
4
5
  let _orkestrel_tool = require("@orkestrel/tool");
5
6
  //#region src/core/constants.ts
@@ -50,6 +51,53 @@ var MCP_META_SUBSCRIPTION = "io.modelcontextprotocol/subscriptionId";
50
51
  * the extension defines no options, so presence is the entire declaration.
51
52
  */
52
53
  var MCP_EXTENSION_TASKS = "io.modelcontextprotocol/tasks";
54
+ /**
55
+ * The opening marker of the Base64 sentinel a standard MCP header value travels in.
56
+ *
57
+ * @remarks
58
+ * The markers are LOWERCASE and exact, and this constant with {@link MCP_SENTINEL_SUFFIX} is
59
+ * their ONE spelling in this package: {@link import('@orkestrel/mcp').encodeSentinel} builds a
60
+ * sentinel from them and {@link import('@orkestrel/mcp').decodeSentinel} recognizes one by
61
+ * them, so the two directions cannot drift apart.
62
+ */
63
+ var MCP_SENTINEL_PREFIX = "=?base64?";
64
+ /** The closing marker of the Base64 sentinel a standard MCP header value travels in. */
65
+ var MCP_SENTINEL_SUFFIX = "?=";
66
+ /**
67
+ * The request-header prefix an `x-mcp-header` annotation projects a tool argument onto.
68
+ *
69
+ * @remarks
70
+ * The full field name is this prefix followed by the annotation's own value verbatim, so
71
+ * `x-mcp-header: 'Region'` becomes `Mcp-Param-Region`. HTTP field names are case-insensitive,
72
+ * which is why {@link MCP_HEADER_ANNOTATION} values are unique case-insensitively within one
73
+ * `inputSchema`.
74
+ */
75
+ var MCP_PARAM_PREFIX = "Mcp-Param-";
76
+ /**
77
+ * The tool-schema annotation key naming the header one parameter projects into.
78
+ *
79
+ * @remarks
80
+ * It is valid ONLY on a primitive property schema statically reachable from the `inputSchema`
81
+ * root through `properties` keys alone. An occurrence anywhere else — under `items`, a
82
+ * composition or conditional keyword, or a `$ref` target — makes the whole tool definition
83
+ * invalid, which is what {@link import('@orkestrel/mcp').buildHeaderParameters} decides.
84
+ */
85
+ var MCP_HEADER_ANNOTATION = "x-mcp-header";
86
+ /**
87
+ * The `tools/list` pages one modern `tools/call` walks to reach its own annotations.
88
+ *
89
+ * @remarks
90
+ * The HTTP POST handler reads a called tool's {@link MCP_HEADER_ANNOTATION} annotations by
91
+ * dispatching `tools/list` fresh on every `tools/call`, following `nextCursor` until the
92
+ * named tool is found or the answer carries no cursor. The walk is bounded because its cost
93
+ * is paid per call: at a page size of 100 this bound reaches 800 definitions, and a consumer
94
+ * whose replacement `tools/list` pages more finely than that pays the extra dispatches on
95
+ * every call it serves. The built-in listing answers the whole registry on one page and
96
+ * never reaches the second. A definition further in than the walk reaches reads as no
97
+ * definition, so its {@link MCP_PARAM_PREFIX} headers are forwarded untouched — the same
98
+ * answer a name no served definition annotates receives.
99
+ */
100
+ var MCP_LOOKUP_PAGES = 8;
53
101
  /** MCP reserved error: required HTTP metadata does not match the request body. */
54
102
  var MCP_HEADER_MISMATCH = -32020;
55
103
  /**
@@ -83,10 +131,11 @@ var DEFAULT_MCP_CACHE_TTL = 6e4;
83
131
  * One MiB admits ordinary JSON-RPC requests and substantial tool arguments; 16 KiB admits
84
132
  * extension-rich modern metadata and signed multi-round state; four MiB admits substantial
85
133
  * JSON tool output without allowing an unconfigured service to serialize arbitrary process
86
- * memory; 64 metadata keys admits the reserved keys plus many extensions; 128 concurrent
87
- * streams admits a busy service while bounding retained producers; depth 32 admits ordinary
88
- * JSON documents while rejecting stack-hostile nesting. Frozen so callers cannot alter the
89
- * defaults observed by later servers.
134
+ * memory; 64 keys admits `_meta`'s reserved keys plus many extensions, and bounds a produced
135
+ * result's breadth by the same leaf; 128 concurrent streams admits a busy service while
136
+ * bounding retained producers; depth 32 admits ordinary JSON documents while rejecting
137
+ * stack-hostile nesting. Frozen so callers cannot alter the defaults observed by later
138
+ * servers.
90
139
  */
91
140
  var DEFAULT_MCP_LIMITS = Object.freeze({
92
141
  message: 1048576,
@@ -432,17 +481,18 @@ function parseRequestContext(value, limits = {
432
481
  * This parser does not open the opaque continuation carrier; the configured
433
482
  * continuation port performs that boundary first. The protected
434
483
  * payload binds the authenticated principal, absolute expiry, ORIGINAL request id, version,
435
- * method, server-assigned key, tool name, argument digest, the exact issued elicitation
436
- * schema, and optional application state. Every member is required except application state:
437
- * a payload missing its schema cannot have its accepted response enforced, so it is refused
438
- * rather than admitted unenforced. Total over malformed or hostile input.
484
+ * method, the exact round that was issued, tool name, argument digest, and optional
485
+ * application state. Every member is required except application state: a payload missing its
486
+ * round cannot have the client's answers enforced, so it is refused rather than admitted
487
+ * unenforced. An EMPTY round is refused for the same reason — a retry against it would answer
488
+ * no question at all. Total over malformed or hostile input.
439
489
  *
440
490
  * @param value - The opened canonical continuation value to parse
441
491
  * @returns The protected input state, or `undefined` when malformed
442
492
  *
443
493
  * @example
444
494
  * ```ts
445
- * parseMCPInputState('{"principal":"user-1","expiry":2000,"id":1,"version":"2026-07-28","method":"tools/call","key":"k","name":"reply","digest":"abc","schema":{"type":"object","properties":{}}}')
495
+ * parseMCPInputState('{"principal":"user-1","expiry":2000,"id":1,"version":"2026-07-28","method":"tools/call","requests":{"k":{"method":"roots/list"}},"name":"reply","digest":"abc"}')
446
496
  * ```
447
497
  */
448
498
  function parseMCPInputState(value) {
@@ -455,26 +505,24 @@ function parseMCPInputState(value) {
455
505
  const id = parsed["id"];
456
506
  const version = parsed["version"];
457
507
  const method = parsed["method"];
458
- const key = parsed["key"];
508
+ const requests = parsed["requests"];
459
509
  const name = parsed["name"];
460
510
  const digest = parsed["digest"];
461
- const schema = parsed["schema"];
462
511
  const state = parsed["state"];
463
512
  if (!(0, _orkestrel_contract.isString)(principal) || principal.length === 0 || !(0, _orkestrel_contract.isNumber)(expiry) || !Number.isFinite(expiry)) return;
464
513
  if (!isJSONRPCId(id)) return void 0;
465
- if (!(0, _orkestrel_contract.isString)(version) || !(0, _orkestrel_contract.isString)(method) || !(0, _orkestrel_contract.isString)(key) || !(0, _orkestrel_contract.isString)(name)) return;
514
+ if (!(0, _orkestrel_contract.isString)(version) || !(0, _orkestrel_contract.isString)(method) || !(0, _orkestrel_contract.isString)(name)) return void 0;
466
515
  if (!(0, _orkestrel_contract.isString)(digest) || !(0, _orkestrel_contract.isUndefined)(state) && !(0, _orkestrel_contract.isJSONValue)(state)) return void 0;
467
- if (!isMCPElicitSchema(schema)) return void 0;
516
+ if (!isMCPInputRequestMap(requests) || Object.keys(requests).length === 0) return void 0;
468
517
  return {
469
518
  principal,
470
519
  expiry,
471
520
  id,
472
521
  version,
473
522
  method,
474
- key,
523
+ requests,
475
524
  name,
476
525
  digest,
477
- schema,
478
526
  ...(0, _orkestrel_contract.isUndefined)(state) ? {} : { state }
479
527
  };
480
528
  } catch {
@@ -513,6 +561,68 @@ function isFormElicitationSupported(value) {
513
561
  }
514
562
  }
515
563
  /**
564
+ * Computes the capabilities one round of input requests needs and the client did not declare.
565
+ *
566
+ * @remarks
567
+ * The protocol's rule is about SENDING: a server never issues a request kind the client's
568
+ * declared capabilities exclude. So this reads the round rather than the method, and it
569
+ * answers with the refusal's own payload — the `requiredCapabilities` record a
570
+ * `MissingRequiredClientCapability` error carries, keyed by each missing capability, in the
571
+ * `ClientCapabilities` shape the schema defines rather than as a list of names.
572
+ *
573
+ * Each kind maps to one declaration: `sampling/createMessage` to `sampling`, `roots/list` to
574
+ * `roots`, a form elicitation to what {@link isFormElicitationSupported} accepts, and a
575
+ * URL-mode elicitation to a record-valued `elicitation.url`. A request this package cannot
576
+ * recognize needs nothing, because {@link import('./validators.js').isMCPInputRequestMap}
577
+ * has already refused the round it would have travelled in. Total over hostile input.
578
+ *
579
+ * The `elicitation` value names the ARM the round needs, so a client can act on the refusal
580
+ * by declaring exactly what the payload asks for. A missing URL arm answers `{ url: {} }`, a
581
+ * missing form arm answers the empty record this package reads as form-only, and a round
582
+ * needing both answers `{ form: {}, url: {} }`. An empty record for a URL round would name
583
+ * the declaration a URL-capable client already sent, and refuse the identical round again.
584
+ *
585
+ * @param requests - The round the server is about to issue
586
+ * @param capabilities - The client capability record the request declared
587
+ * @returns The missing capabilities, or `undefined` when the client declared every one
588
+ *
589
+ * @example
590
+ * ```ts
591
+ * computeMissingCapabilities({ answer: { method: 'roots/list' } }, {}) // { roots: {} }
592
+ * computeMissingCapabilities({ answer: { method: 'roots/list' } }, { roots: {} }) // undefined
593
+ * ```
594
+ */
595
+ function computeMissingCapabilities(requests, capabilities) {
596
+ const owned = (0, _orkestrel_contract.attempt)(() => (0, _orkestrel_contract.cloneJSONRecord)(capabilities));
597
+ const declared = owned.success ? owned.value : {};
598
+ const missing = {};
599
+ let formUndeclared = false;
600
+ let urlUndeclared = false;
601
+ for (const request of Object.values(requests)) {
602
+ if (request.method === "sampling/createMessage") {
603
+ if (!(0, _orkestrel_contract.isRecord)(declared["sampling"])) missing["sampling"] = {};
604
+ continue;
605
+ }
606
+ if (request.method === "roots/list") {
607
+ if (!(0, _orkestrel_contract.isRecord)(declared["roots"])) missing["roots"] = {};
608
+ continue;
609
+ }
610
+ const elicitation = declared["elicitation"];
611
+ if (request.params.mode === "url") {
612
+ if (!(0, _orkestrel_contract.isRecord)(elicitation) || !(0, _orkestrel_contract.isRecord)(elicitation["url"])) urlUndeclared = true;
613
+ continue;
614
+ }
615
+ if (!isFormElicitationSupported(declared)) formUndeclared = true;
616
+ }
617
+ if (formUndeclared && !urlUndeclared) missing["elicitation"] = {};
618
+ if (urlUndeclared && !formUndeclared) missing["elicitation"] = { url: {} };
619
+ if (formUndeclared && urlUndeclared) missing["elicitation"] = {
620
+ form: {},
621
+ url: {}
622
+ };
623
+ return Object.keys(missing).length === 0 ? void 0 : Object.freeze(missing);
624
+ }
625
+ /**
516
626
  * Determines whether a client capability record declares the stable Tasks extension.
517
627
  *
518
628
  * @remarks
@@ -780,7 +890,7 @@ async function digestJSON(value, limits) {
780
890
  const serialized = serializeJSON(value, limits);
781
891
  if (serialized === void 0) return void 0;
782
892
  const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(serialized));
783
- return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
893
+ return (0, _orkestrel_codec.encodeHex)(new Uint8Array(digest));
784
894
  }
785
895
  /**
786
896
  * Builds one official progress notification for the original request stream.
@@ -1355,6 +1465,321 @@ function decodeBoundedMessage(message, limits) {
1355
1465
  return parsed.success ? parseJSONRPCMessage(parsed.value, limits) : void 0;
1356
1466
  }
1357
1467
  /**
1468
+ * Reads the value one standard MCP request header carries, decoding the Base64 sentinel.
1469
+ *
1470
+ * @remarks
1471
+ * The sentinel format is `=?base64?{Base64OfUTF8}?=`, spelled once as
1472
+ * {@link MCP_SENTINEL_PREFIX} and {@link MCP_SENTINEL_SUFFIX} and read from there by both
1473
+ * directions of the codec.
1474
+ * The markers alone decide whether a value is a sentinel: a value carrying the prefix and the
1475
+ * suffix is one, and its payload is then held to `decodeBase64` from `@orkestrel/codec` — the
1476
+ * canonical RFC 4648 § 4 grammar, which admits exactly one spelling per byte sequence — and to
1477
+ * well-formed UTF-8. A payload leaving a non-zero bit in the sextet its padding discards is a
1478
+ * second spelling of a byte, so it is refused: `=?base64?QR==?=` reaches for the byte
1479
+ * `=?base64?QQ==?=` spells canonically, and only the canonical spelling decodes. A malformed
1480
+ * payload answers `undefined` rather than falling back to the literal, because the protocol
1481
+ * requires a server to REJECT invalid characters, and a fallback would admit the very value
1482
+ * the rule exists to refuse. A value missing either marker is a literal and comes back
1483
+ * unchanged.
1484
+ *
1485
+ * `decodeUTF8` from `@orkestrel/codec` reads the bytes back as text: strict RFC 3629, where an
1486
+ * overlong, an encoded surrogate, a code point past U+10FFFF, and a truncated sequence each
1487
+ * answer `undefined` rather than a replacement character, and total, so the refusal arrives as
1488
+ * that value instead of as a throw. It also keeps a leading U+FEFF as a character of the
1489
+ * value, where the platform decoder consumes it as a byte order mark — which is what lets a
1490
+ * value leading with U+FEFF survive {@link encodeSentinel} and come back whole.
1491
+ *
1492
+ * {@link import('./validators.js').isStandardBase64} is a wider and separate rule: it names
1493
+ * JSON Schema `byte` membership for the blob, image, and audio content a peer sends, where
1494
+ * this package receives liberally. It does not govern this payload.
1495
+ *
1496
+ * Optional whitespace is excluded first, per RFC 9110 § 5.5: a recipient parses a field value
1497
+ * with its surrounding spaces and horizontal tabs removed, so a peer that padded a plain value
1498
+ * still matches the body. A value whose own leading or trailing whitespace is significant
1499
+ * cannot survive that, which is what {@link encodeSentinel} encodes it for.
1500
+ *
1501
+ * Total — never throws, whatever the input.
1502
+ *
1503
+ * @param value - The raw header field value the peer sent
1504
+ * @returns The carried value, or `undefined` when the sentinel's payload is invalid
1505
+ *
1506
+ * @example
1507
+ * ```ts
1508
+ * decodeSentinel('=?base64?Y2Fmw6k=?=') // 'café'
1509
+ * decodeSentinel(' search ') // 'search' — optional whitespace excluded
1510
+ * decodeSentinel('=?base64?SGVsbG8?=') // undefined — invalid padding
1511
+ * decodeSentinel('=?base64?QR==?=') // undefined — a non-canonical spelling
1512
+ * ```
1513
+ */
1514
+ function decodeSentinel(value) {
1515
+ const field = value.replace(/^[ \t]+|[ \t]+$/g, "");
1516
+ if (!(field.length >= 11 && field.startsWith("=?base64?") && field.endsWith("?="))) return field;
1517
+ const payload = field.slice(MCP_SENTINEL_PREFIX.length, field.length - 2);
1518
+ const bytes = (0, _orkestrel_codec.decodeBase64)(payload);
1519
+ if (bytes === void 0) return void 0;
1520
+ return (0, _orkestrel_codec.decodeUTF8)(bytes);
1521
+ }
1522
+ /**
1523
+ * Builds the wire form one standard MCP request header value must travel as.
1524
+ *
1525
+ * @remarks
1526
+ * The exact inverse of {@link decodeSentinel}, and its membership rule is stated as that
1527
+ * inverse rather than as a second list that could drift: a value travels LITERALLY when it is
1528
+ * plain printable ASCII — every code point in `U+0020`–`U+007E`, the RFC 9110 field-value
1529
+ * range this package admits — and {@link decodeSentinel} gives it back unchanged. Every other
1530
+ * value travels wrapped in {@link MCP_SENTINEL_PREFIX} and {@link MCP_SENTINEL_SUFFIX}, the
1531
+ * same markers the decode recognizes a sentinel by. `encodeBase64` from `@orkestrel/codec`
1532
+ * spells the payload, so the wire form carries the canonical spelling {@link decodeSentinel}
1533
+ * accepts.
1534
+ *
1535
+ * That one rule covers each row of the protocol's encoding table. A non-ASCII value and a
1536
+ * value carrying a control character fail the ASCII test. A value with leading or trailing
1537
+ * whitespace comes back trimmed, so it fails the round trip. A value already wearing the
1538
+ * sentinel markers decodes to something else, or to nothing, so it fails the round trip too
1539
+ * and is encoded rather than read back as a sentinel it never was.
1540
+ *
1541
+ * The bytes come from the platform `TextEncoder`, not from codec's `encodeUTF8`, and that is a
1542
+ * ruling rather than an oversight. `TextEncoder` is total: it spells ill-formed text — a lone
1543
+ * surrogate, which has no UTF-8 spelling — with the replacement character, so this function
1544
+ * answers a `string` for every input. `encodeUTF8` refuses that text with `undefined`, which
1545
+ * would widen this return to `string | undefined` and oblige every header projection to handle
1546
+ * a value it cannot send. The decode side carries no such tension, so it reads back through
1547
+ * codec's strict `decodeUTF8`.
1548
+ *
1549
+ * @param value - The value the header must carry
1550
+ * @returns The literal value, or its Base64 sentinel form
1551
+ *
1552
+ * @example
1553
+ * ```ts
1554
+ * encodeSentinel('search') // 'search'
1555
+ * encodeSentinel('café') // '=?base64?Y2Fmw6k=?='
1556
+ * ```
1557
+ */
1558
+ function encodeSentinel(value) {
1559
+ if (/^[ -~]*$/.test(value) && decodeSentinel(value) === value) return value;
1560
+ return `${MCP_SENTINEL_PREFIX}${(0, _orkestrel_codec.encodeBase64)(new TextEncoder().encode(value))}?=`;
1561
+ }
1562
+ /**
1563
+ * Counts every {@link MCP_HEADER_ANNOTATION} key one JSON value carries, at any position.
1564
+ *
1565
+ * @remarks
1566
+ * The companion of {@link extractHeaderAnnotations}, which reads only the annotations a
1567
+ * `properties` chain reaches. Comparing the two answers is how
1568
+ * {@link buildHeaderParameters} decides reachability without a second walk that would have
1569
+ * to re-state which JSON Schema keywords are traversable: an annotation the reachable walk
1570
+ * did not read is one sitting under `items`, a composition or conditional keyword, a `$ref`
1571
+ * target, or any other position, and the protocol makes the whole tool definition invalid for
1572
+ * it.
1573
+ *
1574
+ * Iterative and ancestor-tracked, so a deeply nested or self-referential value terminates
1575
+ * rather than exhausting the stack. Total — never throws, whatever the input.
1576
+ *
1577
+ * @param value - The value to scan, normally a tool's `inputSchema`
1578
+ * @returns How many annotation keys the value carries
1579
+ *
1580
+ * @example
1581
+ * ```ts
1582
+ * countHeaderAnnotations({ properties: { region: { 'x-mcp-header': 'Region' } } }) // 1
1583
+ * ```
1584
+ */
1585
+ function countHeaderAnnotations(value) {
1586
+ let total = 0;
1587
+ const seen = /* @__PURE__ */ new Set();
1588
+ const pending = [value];
1589
+ while (pending.length > 0) {
1590
+ const node = pending.pop();
1591
+ if ((0, _orkestrel_contract.isArray)(node)) {
1592
+ if (seen.has(node)) continue;
1593
+ seen.add(node);
1594
+ for (const item of node) pending.push(item);
1595
+ continue;
1596
+ }
1597
+ if (!(0, _orkestrel_contract.isRecord)(node) || seen.has(node)) continue;
1598
+ seen.add(node);
1599
+ for (const [key, member] of Object.entries(node)) if (key === "x-mcp-header") total += 1;
1600
+ else pending.push(member);
1601
+ }
1602
+ return total;
1603
+ }
1604
+ /**
1605
+ * Reads every `x-mcp-header` annotation reachable from a schema node through `properties`.
1606
+ *
1607
+ * @remarks
1608
+ * Reachability is the protocol's own rule: an annotation counts only where a chain of
1609
+ * `properties` keys leads to it from the `inputSchema` root, so `path` is both the schema
1610
+ * position and the position the call's `arguments` carry the value at. A property named
1611
+ * `items` is reachable like any other, because the chain is read by key POSITION rather than
1612
+ * by key name.
1613
+ *
1614
+ * `undefined` means the definition is invalid rather than empty: a reachable annotation whose
1615
+ * value is not an {@link import('./validators.js').isFieldToken} token, one sitting on the
1616
+ * schema ROOT (which is no property), one on a leaf whose declared type is not an
1617
+ * {@link import('./validators.js').isMCPHeaderPrimitive} primitive, or a chain deeper than
1618
+ * `DEFAULT_MCP_LIMITS.depth` — which is also what makes a self-referential schema terminate.
1619
+ * A node that is not a record carries nothing and answers an empty list, because a leaf the
1620
+ * walk cannot read is not a violation.
1621
+ *
1622
+ * @param schema - The schema node to read
1623
+ * @param path - The `properties` keys already traversed; the root is called with `[]`
1624
+ * @returns The annotations reachable from this node, or `undefined` when one is invalid
1625
+ *
1626
+ * @example
1627
+ * ```ts
1628
+ * extractHeaderAnnotations({ properties: { region: { type: 'string', 'x-mcp-header': 'Region' } } }, [])
1629
+ * // → [{ name: 'Region', path: ['region'], primitive: 'string' }]
1630
+ * ```
1631
+ */
1632
+ function extractHeaderAnnotations(schema, path) {
1633
+ if (path.length > DEFAULT_MCP_LIMITS.depth) return void 0;
1634
+ if (!(0, _orkestrel_contract.isRecord)(schema)) return [];
1635
+ const found = [];
1636
+ const annotation = schema[MCP_HEADER_ANNOTATION];
1637
+ if (annotation !== void 0) {
1638
+ if (path.length === 0 || !isFieldToken(annotation)) return void 0;
1639
+ const primitive = schema["type"];
1640
+ if (!isMCPHeaderPrimitive(primitive)) return void 0;
1641
+ found.push({
1642
+ name: annotation,
1643
+ path,
1644
+ primitive
1645
+ });
1646
+ }
1647
+ const properties = schema["properties"];
1648
+ if ((0, _orkestrel_contract.isRecord)(properties)) for (const [key, leaf] of Object.entries(properties)) {
1649
+ const nested = extractHeaderAnnotations(leaf, [...path, key]);
1650
+ if (nested === void 0) return void 0;
1651
+ found.push(...nested);
1652
+ }
1653
+ return found;
1654
+ }
1655
+ /**
1656
+ * Builds the `x-mcp-header` projections one tool's `inputSchema` declares.
1657
+ *
1658
+ * @remarks
1659
+ * The single decision both sides of the protocol make about an annotated tool: an HTTP
1660
+ * CLIENT excludes a definition this refuses from the `tools/list` result it delivers, and a
1661
+ * SERVER recognizes exactly the `Mcp-Param-*` names this returns for its own definitions.
1662
+ *
1663
+ * `undefined` means the definition is invalid, and every rule the protocol states produces
1664
+ * it: a value that is not an RFC 9110 token, a non-primitive or untyped annotated leaf, a
1665
+ * name repeated case-insensitively within the schema, an annotation the `properties` chain
1666
+ * does not reach, and a schema that is not a record at all. An empty list is the valid answer
1667
+ * for a schema carrying no annotation.
1668
+ *
1669
+ * Total — never throws, and a cyclic or stack-hostile schema is refused rather than followed.
1670
+ *
1671
+ * @param schema - The tool's advertised `inputSchema`
1672
+ * @returns The declared projections, or `undefined` when the definition is invalid
1673
+ *
1674
+ * @example
1675
+ * ```ts
1676
+ * buildHeaderParameters({
1677
+ * type: 'object',
1678
+ * properties: { region: { type: 'string', 'x-mcp-header': 'Region' } },
1679
+ * }) // → [{ name: 'Region', path: ['region'], primitive: 'string' }]
1680
+ * ```
1681
+ */
1682
+ function buildHeaderParameters(schema) {
1683
+ if (!(0, _orkestrel_contract.isRecord)(schema)) return void 0;
1684
+ const found = extractHeaderAnnotations(schema, []);
1685
+ if (found === void 0 || found.length !== countHeaderAnnotations(schema)) return void 0;
1686
+ const taken = /* @__PURE__ */ new Set();
1687
+ for (const parameter of found) {
1688
+ const key = parameter.name.toLowerCase();
1689
+ if (taken.has(key)) return void 0;
1690
+ taken.add(key);
1691
+ }
1692
+ return found;
1693
+ }
1694
+ /**
1695
+ * Renders one projected argument as the text its `Mcp-Param-*` header carries.
1696
+ *
1697
+ * @remarks
1698
+ * The protocol's conversion table, and the ONE place it is stated: a string travels as
1699
+ * itself, an integer in decimal, and a boolean as lowercase `true` or `false`. The value's
1700
+ * runtime shape must match the leaf's declared type, so a schema that declares `integer` and
1701
+ * an argument that supplies a string, a fraction, or a magnitude outside the IEEE 754 safe
1702
+ * range carries NOTHING — a header that cannot round-trip the body value is worse than an
1703
+ * absent one, and the tool's own argument validation owns the disagreement.
1704
+ *
1705
+ * @param value - The argument value read at the parameter's path
1706
+ * @param primitive - The leaf's declared type
1707
+ * @returns The header text, or `undefined` when the value cannot travel as that type
1708
+ *
1709
+ * @example
1710
+ * ```ts
1711
+ * renderHeaderValue(42, 'integer') // '42'
1712
+ * renderHeaderValue(false, 'boolean') // 'false'
1713
+ * ```
1714
+ */
1715
+ function renderHeaderValue(value, primitive) {
1716
+ if (primitive === "string") return (0, _orkestrel_contract.isString)(value) ? value : void 0;
1717
+ if (primitive === "boolean") return (0, _orkestrel_contract.isBoolean)(value) ? value ? "true" : "false" : void 0;
1718
+ return (0, _orkestrel_contract.isNumber)(value) && Number.isSafeInteger(value) ? String(value) : void 0;
1719
+ }
1720
+ /**
1721
+ * Builds the `Mcp-Param-*` request headers one `tools/call` carries.
1722
+ *
1723
+ * @remarks
1724
+ * The projection SEP-2243 requires of an HTTP client, and the same derivation a server runs
1725
+ * to know what the request should have carried. Each parameter's value is read at its exact
1726
+ * property path in the call's own `arguments`; an absent or `null` value omits its header
1727
+ * entirely, which is the protocol's distinction between "not supplied" and "supplied empty".
1728
+ * The rendered text then travels through {@link encodeSentinel}, so a value carrying
1729
+ * non-ASCII, control, or edge whitespace characters reaches the peer intact.
1730
+ *
1731
+ * @param parameters - The projections the tool's `inputSchema` declares
1732
+ * @param values - The call's `arguments` record
1733
+ * @returns The header field names and values, empty when nothing projects
1734
+ *
1735
+ * @example
1736
+ * ```ts
1737
+ * buildHeaderProjection(
1738
+ * [{ name: 'Region', path: ['region'], primitive: 'string' }],
1739
+ * { region: 'us-west1' },
1740
+ * ) // → { 'Mcp-Param-Region': 'us-west1' }
1741
+ * ```
1742
+ */
1743
+ function buildHeaderProjection(parameters, values) {
1744
+ const headers = {};
1745
+ for (const parameter of parameters) {
1746
+ let carried = values;
1747
+ for (const key of parameter.path) carried = (0, _orkestrel_contract.isRecord)(carried) ? carried[key] : void 0;
1748
+ if (carried === void 0 || carried === null) continue;
1749
+ const text = renderHeaderValue(carried, parameter.primitive);
1750
+ if (text !== void 0) headers[`${MCP_PARAM_PREFIX}${parameter.name}`] = encodeSentinel(text);
1751
+ }
1752
+ return headers;
1753
+ }
1754
+ /**
1755
+ * Reads one named tool's advertised `inputSchema` out of a `tools/list` answer.
1756
+ *
1757
+ * @remarks
1758
+ * The answer is read as foreign data end to end — a dispatched response, an error envelope,
1759
+ * and a result whose `tools` member is absent or is not an array all read as "no schema"
1760
+ * rather than as a fault. That is what lets the HTTP POST handler ask its own dispatcher
1761
+ * which `Mcp-Param-*` names a `tools/call` may carry without narrowing anything first.
1762
+ *
1763
+ * @param response - The `tools/list` answer, normally a {@link JSONRPCResponse}
1764
+ * @param name - The tool whose schema to read
1765
+ * @returns The advertised `inputSchema`, or `undefined` when the answer carries none
1766
+ *
1767
+ * @example
1768
+ * ```ts
1769
+ * extractToolSchema(answer, 'search')?.['properties']
1770
+ * ```
1771
+ */
1772
+ function extractToolSchema(response, name) {
1773
+ const result = (0, _orkestrel_contract.isRecord)(response) ? response["result"] : void 0;
1774
+ const tools = (0, _orkestrel_contract.isRecord)(result) ? result["tools"] : void 0;
1775
+ if (!(0, _orkestrel_contract.isArray)(tools)) return void 0;
1776
+ for (const tool of tools) {
1777
+ if (!(0, _orkestrel_contract.isRecord)(tool) || tool["name"] !== name) continue;
1778
+ const schema = tool["inputSchema"];
1779
+ return (0, _orkestrel_contract.isRecord)(schema) ? schema : void 0;
1780
+ }
1781
+ }
1782
+ /**
1358
1783
  * Reads the request id an inbound `notifications/cancelled` names — the inverse of
1359
1784
  * {@link buildCancelledNotification}.
1360
1785
  *
@@ -1670,6 +2095,48 @@ function isStandardBase64(value) {
1670
2095
  return (0, _orkestrel_contract.isString)(value) && /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value);
1671
2096
  }
1672
2097
  /**
2098
+ * Determines whether a value is one RFC 9110 field token.
2099
+ *
2100
+ * @remarks
2101
+ * A token is one or more `tchar`: the ASCII letters, the digits, and
2102
+ * ``!#$%&'*+-.^_`|~``. That set already excludes the empty string, whitespace, a colon, a
2103
+ * control character, and every non-ASCII code point, so it is the whole constraint an
2104
+ * `x-mcp-header` annotation's value must satisfy — the value is appended verbatim to
2105
+ * {@link MCP_PARAM_PREFIX} and must survive as an HTTP field name.
2106
+ *
2107
+ * @param value - The unknown value to inspect
2108
+ * @returns Whether the value is a non-empty RFC 9110 token
2109
+ *
2110
+ * @example
2111
+ * ```ts
2112
+ * isFieldToken('Region') // true
2113
+ * isFieldToken('My Region') // false
2114
+ * ```
2115
+ */
2116
+ function isFieldToken(value) {
2117
+ return (0, _orkestrel_contract.isString)(value) && /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(value);
2118
+ }
2119
+ /**
2120
+ * Determines whether a value is a JSON Schema type an `x-mcp-header` annotation may sit on.
2121
+ *
2122
+ * @remarks
2123
+ * `number` is refused deliberately: a JSON number has no interoperable decimal text form, so
2124
+ * a header carrying one could not be compared with the body byte for byte. `integer` renders
2125
+ * exactly, and the server compares it numerically.
2126
+ *
2127
+ * @param value - The unknown value to inspect
2128
+ * @returns Whether the value is one of `'string'`, `'integer'`, or `'boolean'`
2129
+ *
2130
+ * @example
2131
+ * ```ts
2132
+ * isMCPHeaderPrimitive('integer') // true
2133
+ * isMCPHeaderPrimitive('number') // false
2134
+ * ```
2135
+ */
2136
+ function isMCPHeaderPrimitive(value) {
2137
+ return value === "string" || value === "integer" || value === "boolean";
2138
+ }
2139
+ /**
1673
2140
  * Determines whether a value is one absolute URI under RFC 3986 syntax.
1674
2141
  *
1675
2142
  * @remarks
@@ -2793,7 +3260,7 @@ function isMCPElicitRequest(value) {
2793
3260
  * Determines whether a value is one legal embedded multi-round-trip request.
2794
3261
  *
2795
3262
  * @param value - The unknown value to inspect
2796
- * @returns `true` for elicitation, deprecated sampling, or deprecated roots requests
3263
+ * @returns `true` for an embedded elicitation, sampling, or roots request
2797
3264
  *
2798
3265
  * @example
2799
3266
  * ```ts
@@ -2814,7 +3281,7 @@ function isMCPInputRequest(value) {
2814
3281
  }
2815
3282
  }
2816
3283
  /**
2817
- * Determines whether a value is a server-keyed map of embedded input requests.
3284
+ * Determines whether a value is a consumer-keyed map of embedded input requests.
2818
3285
  *
2819
3286
  * @param value - The unknown value to inspect
2820
3287
  * @returns `true` when every own value is a legal {@link MCPInputRequest}
@@ -2968,6 +3435,187 @@ function isElicitContent(value, schema) {
2968
3435
  }
2969
3436
  }
2970
3437
  /**
3438
+ * Determines whether a value is one filesystem root a client exposes.
3439
+ *
3440
+ * @remarks
3441
+ * The dated schema declares `uri` with `format: uri`, so this applies the same RFC 3986
3442
+ * check {@link isAbsoluteURI} gives every other `format: uri` field the package validates,
3443
+ * including a URL-mode elicitation's `url`. Total over hostile input.
3444
+ *
3445
+ * @param value - The unknown value to inspect
3446
+ * @returns `true` when `value` carries an absolute `uri` and an optional string `name`
3447
+ *
3448
+ * @example
3449
+ * ```ts
3450
+ * isMCPRoot({ uri: 'file:///workspace', name: 'workspace' }) // true
3451
+ * isMCPRoot({ uri: 'workspace' }) // false — the schema declares `format: uri`
3452
+ * ```
3453
+ */
3454
+ function isMCPRoot(value) {
3455
+ const owned = (0, _orkestrel_contract.attempt)(() => (0, _orkestrel_contract.cloneJSONRecord)(value));
3456
+ if (!owned.success) return false;
3457
+ try {
3458
+ const root = owned.value;
3459
+ const name = root["name"];
3460
+ const metadata = root["_meta"];
3461
+ if (!isAbsoluteURI(root["uri"])) return false;
3462
+ if (!(0, _orkestrel_contract.isUndefined)(name) && !(0, _orkestrel_contract.isString)(name)) return false;
3463
+ return (0, _orkestrel_contract.isUndefined)(metadata) || isMCPMetaObject(metadata);
3464
+ } catch {
3465
+ return false;
3466
+ }
3467
+ }
3468
+ /**
3469
+ * Determines whether a value is one client answer to an embedded `roots/list` request.
3470
+ *
3471
+ * @remarks
3472
+ * The dated schema requires the `roots` array, and each root is checked by
3473
+ * {@link isMCPRoot}. Total over hostile input.
3474
+ *
3475
+ * @param value - The unknown value to inspect
3476
+ * @returns `true` when `value` carries an array of valid roots
3477
+ *
3478
+ * @example
3479
+ * ```ts
3480
+ * isMCPRootResult({ roots: [{ uri: 'file:///workspace' }] }) // true
3481
+ * isMCPRootResult({ roots: {} }) // false — the schema requires an array
3482
+ * ```
3483
+ */
3484
+ function isMCPRootResult(value) {
3485
+ const owned = (0, _orkestrel_contract.attempt)(() => (0, _orkestrel_contract.cloneJSONRecord)(value));
3486
+ if (!owned.success) return false;
3487
+ try {
3488
+ const result = owned.value;
3489
+ const roots = result["roots"];
3490
+ const metadata = result["_meta"];
3491
+ if (!Array.isArray(roots) || !roots.every((root) => isMCPRoot(root))) return false;
3492
+ return (0, _orkestrel_contract.isUndefined)(metadata) || isMCPMetaObject(metadata);
3493
+ } catch {
3494
+ return false;
3495
+ }
3496
+ }
3497
+ /**
3498
+ * Determines whether a value is one block a sampling completion may carry.
3499
+ *
3500
+ * @remarks
3501
+ * The schema's `SamplingMessageContentBlock`: the text, image, and audio blocks
3502
+ * {@link isMCPContent} also admits, plus `tool_use` and `tool_result`. The resource arms of
3503
+ * {@link isMCPContent} are refused, because the schema leaves them out of a sampling
3504
+ * completion. A `tool_result` carries ordinary {@link isMCPContent} blocks and an open
3505
+ * `structuredContent`, which the schema constrains to no shape at all. Total over hostile
3506
+ * input.
3507
+ *
3508
+ * @param value - The unknown value to inspect
3509
+ * @returns `true` when `value` is one legal sampling content block
3510
+ *
3511
+ * @example
3512
+ * ```ts
3513
+ * isMCPSampleContent({ type: 'text', text: 'Paris' }) // true
3514
+ * isMCPSampleContent({ type: 'tool_use', id: 'c1', name: 'lookup', input: {} }) // true
3515
+ * isMCPSampleContent({ type: 'resource_link', name: 'doc', uri: 'file:///doc' }) // false
3516
+ * ```
3517
+ */
3518
+ function isMCPSampleContent(value) {
3519
+ const owned = (0, _orkestrel_contract.attempt)(() => (0, _orkestrel_contract.cloneJSONRecord)(value));
3520
+ if (!owned.success) return false;
3521
+ try {
3522
+ const block = owned.value;
3523
+ const metadata = block["_meta"];
3524
+ if (!(0, _orkestrel_contract.isUndefined)(metadata) && !isMCPMetaObject(metadata)) return false;
3525
+ if (block["type"] === "tool_use") return (0, _orkestrel_contract.isString)(block["id"]) && (0, _orkestrel_contract.isString)(block["name"]) && (0, _orkestrel_contract.isRecord)(block["input"]);
3526
+ if (block["type"] === "tool_result") {
3527
+ const carried = block["content"];
3528
+ const failed = block["isError"];
3529
+ if (!Array.isArray(carried) || !carried.every((entry) => isMCPContent(entry))) return false;
3530
+ if (!(0, _orkestrel_contract.isUndefined)(failed) && !(0, _orkestrel_contract.isBoolean)(failed)) return false;
3531
+ return (0, _orkestrel_contract.isString)(block["toolUseId"]);
3532
+ }
3533
+ if (!isMCPContent(block)) return false;
3534
+ return block.type === "text" || block.type === "image" || block.type === "audio";
3535
+ } catch {
3536
+ return false;
3537
+ }
3538
+ }
3539
+ /**
3540
+ * Determines whether a value is one client answer to an embedded sampling request.
3541
+ *
3542
+ * @remarks
3543
+ * The schema's `CreateMessageResult` types `content` as an `anyOf` over one
3544
+ * {@link isMCPSampleContent} block or an ARRAY of them, so both are admitted here: a
3545
+ * tool-using model answers with `tool_use` and `tool_result` blocks, and a model answering in
3546
+ * several parts answers with the array. `stopReason` stays an open string because the schema
3547
+ * names four values and permits any other a provider reports. Total over hostile input.
3548
+ *
3549
+ * @param value - The unknown value to inspect
3550
+ * @returns `true` when `value` has the sampling-completion shape
3551
+ *
3552
+ * @example
3553
+ * ```ts
3554
+ * isMCPSampleResult({
3555
+ * role: 'assistant',
3556
+ * content: { type: 'text', text: 'Paris' },
3557
+ * model: 'test-model',
3558
+ * }) // true
3559
+ * isMCPSampleResult({
3560
+ * role: 'assistant',
3561
+ * content: [{ type: 'text', text: 'Paris' }],
3562
+ * model: 'test-model',
3563
+ * }) // true
3564
+ * ```
3565
+ */
3566
+ function isMCPSampleResult(value) {
3567
+ const owned = (0, _orkestrel_contract.attempt)(() => (0, _orkestrel_contract.cloneJSONRecord)(value));
3568
+ if (!owned.success) return false;
3569
+ try {
3570
+ const result = owned.value;
3571
+ const role = result["role"];
3572
+ const content = result["content"];
3573
+ const reason = result["stopReason"];
3574
+ const metadata = result["_meta"];
3575
+ if (role !== "user" && role !== "assistant") return false;
3576
+ if (!(0, _orkestrel_contract.isString)(result["model"])) return false;
3577
+ if (!(Array.isArray(content) ? content : [content]).every((block) => isMCPSampleContent(block))) return false;
3578
+ if (!(0, _orkestrel_contract.isUndefined)(reason) && !(0, _orkestrel_contract.isString)(reason)) return false;
3579
+ return (0, _orkestrel_contract.isUndefined)(metadata) || isMCPMetaObject(metadata);
3580
+ } catch {
3581
+ return false;
3582
+ }
3583
+ }
3584
+ /**
3585
+ * Determines whether a response answers the exact embedded request that was issued.
3586
+ *
3587
+ * @remarks
3588
+ * A response carries no `method` of its own, so the ISSUED request selects which arm applies
3589
+ * — the same way {@link isElicitContent} takes the issued schema rather than trusting the
3590
+ * content to describe itself. A form elicitation is checked twice: once for the response
3591
+ * shape and once, on `accept`, for the content against the schema that round issued. A
3592
+ * URL-mode elicitation issues no schema, so only the shape is checked. A request this
3593
+ * package cannot recognize admits NOTHING, because an unrecognized question has no correct
3594
+ * answer. Total over hostile responses and hostile requests alike.
3595
+ *
3596
+ * @param value - The client's answer to check
3597
+ * @param request - The exact {@link MCPInputRequest} that was issued under the same key
3598
+ * @returns `true` when the answer is legal for that request
3599
+ *
3600
+ * @example
3601
+ * ```ts
3602
+ * isMCPInputResponse({ roots: [] }, { method: 'roots/list' }) // true
3603
+ * isMCPInputResponse({ roots: [] }, { method: 'sampling/createMessage', params: {} }) // false
3604
+ * ```
3605
+ */
3606
+ function isMCPInputResponse(value, request) {
3607
+ if (!isMCPInputRequest(request)) return false;
3608
+ try {
3609
+ if (request.method === "roots/list") return isMCPRootResult(value);
3610
+ if (request.method === "sampling/createMessage") return isMCPSampleResult(value);
3611
+ if (!isMCPElicitResult(value)) return false;
3612
+ if (value.action !== "accept" || !isMCPElicitForm(request.params)) return true;
3613
+ return isElicitContent(value.content ?? {}, request.params.requestedSchema);
3614
+ } catch {
3615
+ return false;
3616
+ }
3617
+ }
3618
+ /**
2971
3619
  * Determines whether a value is an MCP input-required result.
2972
3620
  *
2973
3621
  * @remarks
@@ -4293,13 +4941,7 @@ var MCPServer = class {
4293
4941
  if (captured === void 0) return buildJSONRPCError(request.id, JSONRPC_INTERNAL_ERROR, "Server resource manager returned invalid or oversized contents");
4294
4942
  if (isMCPInputResult(captured[0])) {
4295
4943
  const input = captured[0];
4296
- return buildJSONRPCResult(request.id, {
4297
- ...input,
4298
- _meta: {
4299
- ...input["_meta"] ?? {},
4300
- [MCP_META_SERVER]: this.#options.identity
4301
- }
4302
- });
4944
+ return this.#forward(input, request);
4303
4945
  }
4304
4946
  if (!Array.isArray(captured[0]) || !captured[0].every((entry) => isMCPResourceContents(entry))) return buildJSONRPCError(request.id, JSONRPC_INTERNAL_ERROR, "Server resource manager returned invalid or oversized contents");
4305
4947
  const result = buildModernResult({ contents: captured[0] }, this.#options.identity, this.#options.cache?.ttl ?? 6e4, this.#options.cache?.scope);
@@ -4361,13 +5003,7 @@ var MCPServer = class {
4361
5003
  if (captured === void 0) return buildJSONRPCError(request.id, JSONRPC_INTERNAL_ERROR, "Server prompt manager returned an invalid or oversized result");
4362
5004
  if (isMCPInputResult(captured[0])) {
4363
5005
  const input = captured[0];
4364
- return buildJSONRPCResult(request.id, {
4365
- ...input,
4366
- _meta: {
4367
- ...input["_meta"] ?? {},
4368
- [MCP_META_SERVER]: this.#options.identity
4369
- }
4370
- });
5006
+ return this.#forward(input, request);
4371
5007
  }
4372
5008
  if (!isMCPPromptGetResult(captured[0])) return buildJSONRPCError(request.id, JSONRPC_INTERNAL_ERROR, "Server prompt manager returned an invalid or oversized result");
4373
5009
  const result = captured[0];
@@ -4521,21 +5157,46 @@ var MCPServer = class {
4521
5157
  });
4522
5158
  if (digest === void 0) return buildJSONRPCError(id, JSONRPC_INVALID_PARAMS, "Invalid params: tool arguments are too large or unsafe");
4523
5159
  if (params?.["requestState"] !== void 0 || params?.["inputResponses"] !== void 0) return this.#retry(request, name, digest, args, options);
4524
- const elicitation = await configured.elicit({
5160
+ const selected = await configured.selector({
4525
5161
  request,
4526
5162
  name,
4527
5163
  arguments: args
4528
5164
  }, options);
4529
- if (elicitation === void 0) return void 0;
4530
- const form = this.#form(elicitation);
5165
+ if (selected === void 0) return void 0;
5166
+ const round = this.#round(selected);
4531
5167
  const context = parseRequestContext(request, {
4532
5168
  bytes: this.#limits.message,
4533
5169
  depth: this.#limits.depth
4534
5170
  });
4535
- if (form === void 0) return buildJSONRPCError(id, JSONRPC_INVALID_PARAMS, "Invalid params: elicitation policy returned an invalid form or continuation context");
4536
- if (context === void 0 || !isFormElicitationSupported(context.capabilities)) return buildJSONRPCError(id, MCP_MISSING_CAPABILITY, "Server requires the elicitation capability for this request", { requiredCapabilities: { elicitation: {} } });
5171
+ if (round === void 0) return buildJSONRPCError(id, JSONRPC_INVALID_PARAMS, "Invalid params: input policy returned an invalid round or continuation context");
5172
+ const refusal = this.#gate(round, context, id);
5173
+ if (refusal !== void 0) return refusal;
4537
5174
  const principal = await configured.principal(request, options);
4538
- return this.#required(request, name, digest, form, principal, id, void 0);
5175
+ return this.#required(request, name, digest, round, principal, id, void 0);
5176
+ }
5177
+ #gate(round, context, id) {
5178
+ if (context === void 0) return buildJSONRPCError(id, JSONRPC_INVALID_PARAMS, "Invalid params: malformed modern request metadata");
5179
+ const missing = computeMissingCapabilities(round.requests, context.capabilities);
5180
+ if (missing === void 0) return void 0;
5181
+ return buildJSONRPCError(id, MCP_MISSING_CAPABILITY, "Server requires a client capability this request did not declare", { requiredCapabilities: missing });
5182
+ }
5183
+ #forward(input, request) {
5184
+ const requests = input.inputRequests;
5185
+ if (requests !== void 0) {
5186
+ const context = parseRequestContext(request, {
5187
+ bytes: this.#limits.message,
5188
+ depth: this.#limits.depth
5189
+ });
5190
+ const refusal = this.#gate({ requests }, context, request.id);
5191
+ if (refusal !== void 0) return refusal;
5192
+ }
5193
+ return buildJSONRPCResult(request.id, {
5194
+ ...input,
5195
+ _meta: {
5196
+ ...input["_meta"] ?? {},
5197
+ [MCP_META_SERVER]: this.#options.identity
5198
+ }
5199
+ });
4539
5200
  }
4540
5201
  async #retry(request, name, digest, args, options) {
4541
5202
  const configured = this.#options.input;
@@ -4548,55 +5209,66 @@ var MCPServer = class {
4548
5209
  bytes: this.#limits.message,
4549
5210
  depth: this.#limits.depth
4550
5211
  });
4551
- if (context === void 0 || !isFormElicitationSupported(context.capabilities)) return buildJSONRPCError(id, MCP_MISSING_CAPABILITY, "Server requires the elicitation capability for this request", { requiredCapabilities: { elicitation: {} } });
5212
+ if (context === void 0) return buildJSONRPCError(id, JSONRPC_INVALID_PARAMS, "Invalid params: malformed modern request metadata");
4552
5213
  const verified = await configured.continuation.open(requestState);
4553
5214
  if (verified === void 0) return buildJSONRPCError(id, JSONRPC_INVALID_PARAMS, "Invalid params: request state could not be recovered");
4554
5215
  if (!isBoundedString(verified, this.#limits.state) || verified.length === 0) return this.#contain(/* @__PURE__ */ new Error("Continuation port opened a value outside the configured state bound"), id);
4555
5216
  const state = parseMCPInputState(verified);
4556
5217
  if (state === void 0) return this.#contain(/* @__PURE__ */ new Error("Continuation port opened a malformed protected payload"), id);
4557
- const response = inputResponses[state.key];
4558
- if (state.expiry <= Date.now() || state.id === id || state.version !== context.version || state.method !== request.method || state.name !== name || state.digest !== digest || !Object.hasOwn(inputResponses, state.key)) return buildJSONRPCError(id, JSONRPC_INVALID_PARAMS, "Invalid params: request state could not be verified for this retry");
4559
- if (!isMCPElicitResult(response) || response.action === "accept" && !isElicitContent(response.content ?? {}, state.schema)) return buildJSONRPCError(id, JSONRPC_INVALID_PARAMS, "Invalid params: the elicitation response is missing or malformed");
5218
+ if (state.expiry <= Date.now() || state.id === id || state.version !== context.version || state.method !== request.method || state.name !== name || state.digest !== digest) return buildJSONRPCError(id, JSONRPC_INVALID_PARAMS, "Invalid params: request state could not be verified for this retry");
5219
+ const responses = this.#answers(state.requests, inputResponses);
5220
+ if (responses === void 0) return buildJSONRPCError(id, JSONRPC_INVALID_PARAMS, "Invalid params: an input response is missing or malformed");
4560
5221
  const principal = await configured.principal(request, options);
4561
5222
  if (!(0, _orkestrel_contract.isString)(principal) || principal.length === 0 || state.principal !== principal) return buildJSONRPCError(id, JSONRPC_INVALID_PARAMS, "Invalid params: request state could not be verified for this retry");
4562
- const elicitation = await configured.elicit({
5223
+ const selected = await configured.selector({
4563
5224
  request,
4564
5225
  name,
4565
5226
  arguments: args,
4566
- response,
5227
+ responses,
4567
5228
  ...state.state !== void 0 ? { state: state.state } : {}
4568
5229
  }, options);
4569
5230
  if (state.expiry <= Date.now()) return buildJSONRPCError(id, JSONRPC_INVALID_PARAMS, "Invalid params: request state could not be verified for this retry");
4570
- if (elicitation === void 0) return void 0;
4571
- const form = this.#form(elicitation);
4572
- if (form === void 0) return buildJSONRPCError(id, JSONRPC_INVALID_PARAMS, "Invalid params: elicitation policy returned an invalid form or continuation context");
4573
- return this.#required(request, name, digest, form, principal, state.id, state.expiry);
5231
+ if (selected === void 0) return void 0;
5232
+ const round = this.#round(selected);
5233
+ if (round === void 0) return buildJSONRPCError(id, JSONRPC_INVALID_PARAMS, "Invalid params: input policy returned an invalid round or continuation context");
5234
+ const refusal = this.#gate(round, context, id);
5235
+ if (refusal !== void 0) return refusal;
5236
+ return this.#required(request, name, digest, round, principal, state.id, state.expiry);
5237
+ }
5238
+ #answers(requests, responses) {
5239
+ const answered = {};
5240
+ for (const [key, issued] of Object.entries(requests)) {
5241
+ const response = responses[key];
5242
+ if (!Object.hasOwn(responses, key) || !isMCPInputResponse(response, issued)) return void 0;
5243
+ answered[key] = response;
5244
+ }
5245
+ return Object.freeze(answered);
4574
5246
  }
4575
- #form(elicitation) {
4576
- const owned = snapshotJSON(elicitation, {
5247
+ #round(round) {
5248
+ const owned = snapshotJSON(round, {
4577
5249
  bytes: this.#limits.content,
4578
5250
  keys: this.#limits.keys,
4579
5251
  depth: this.#limits.depth
4580
5252
  });
4581
5253
  if (owned === void 0 || !(0, _orkestrel_contract.isRecord)(owned[0])) return void 0;
4582
- const form = owned[0]["request"];
5254
+ const requests = owned[0]["requests"];
4583
5255
  const state = owned[0]["state"];
4584
- if (!isMCPElicitForm(form) || !(0, _orkestrel_contract.isUndefined)(state) && !(0, _orkestrel_contract.isJSONValue)(state)) return void 0;
4585
- return (0, _orkestrel_contract.isUndefined)(state) ? { request: form } : {
4586
- request: form,
5256
+ if (!isMCPInputRequestMap(requests) || Object.keys(requests).length === 0) return void 0;
5257
+ if (!(0, _orkestrel_contract.isUndefined)(state) && !(0, _orkestrel_contract.isJSONValue)(state)) return void 0;
5258
+ return (0, _orkestrel_contract.isUndefined)(state) ? { requests } : {
5259
+ requests,
4587
5260
  state
4588
5261
  };
4589
5262
  }
4590
- async #required(request, name, digest, form, principal, origin, previous) {
5263
+ async #required(request, name, digest, round, principal, origin, previous) {
4591
5264
  const id = request.id;
4592
5265
  const configured = this.#options.input;
4593
5266
  const context = parseRequestContext(request, {
4594
5267
  bytes: this.#limits.message,
4595
5268
  depth: this.#limits.depth
4596
5269
  });
4597
- if (configured === void 0 || context === void 0 || !(0, _orkestrel_contract.isString)(principal) || principal.length === 0 || !Number.isFinite(configured.ttl) || configured.ttl <= 0) return buildJSONRPCError(id, JSONRPC_INVALID_PARAMS, "Invalid params: elicitation policy returned an invalid form or continuation context");
5270
+ if (configured === void 0 || context === void 0 || !(0, _orkestrel_contract.isString)(principal) || principal.length === 0 || !Number.isFinite(configured.ttl) || configured.ttl <= 0) return buildJSONRPCError(id, JSONRPC_INVALID_PARAMS, "Invalid params: input policy returned an invalid round or continuation context");
4598
5271
  if (previous !== void 0 && previous <= Date.now()) return buildJSONRPCError(id, JSONRPC_INVALID_PARAMS, "Invalid params: request state could not be verified for this retry");
4599
- const key = crypto.randomUUID();
4600
5272
  const expiry = Date.now() + configured.ttl;
4601
5273
  const protectedState = {
4602
5274
  principal,
@@ -4604,11 +5276,10 @@ var MCPServer = class {
4604
5276
  id: origin,
4605
5277
  version: context.version,
4606
5278
  method: request.method,
4607
- key,
5279
+ requests: round.requests,
4608
5280
  name,
4609
5281
  digest,
4610
- schema: form.request.requestedSchema,
4611
- ...form.state !== void 0 ? { state: form.state } : {}
5282
+ ...round.state !== void 0 ? { state: round.state } : {}
4612
5283
  };
4613
5284
  if (!isBoundedJSON(protectedState, {
4614
5285
  bytes: this.#limits.state,
@@ -4624,13 +5295,7 @@ var MCPServer = class {
4624
5295
  if (expiry <= Date.now() || previous !== void 0 && previous <= Date.now()) return buildJSONRPCError(id, JSONRPC_INVALID_PARAMS, previous === void 0 ? "Invalid params: request state expired before it could be issued" : "Invalid params: request state could not be verified for this retry");
4625
5296
  return buildJSONRPCResult(id, {
4626
5297
  resultType: "input_required",
4627
- inputRequests: { [key]: {
4628
- method: "elicitation/create",
4629
- params: {
4630
- ...form.request,
4631
- mode: "form"
4632
- }
4633
- } },
5298
+ inputRequests: round.requests,
4634
5299
  requestState,
4635
5300
  _meta: { [MCP_META_SERVER]: this.#options.identity }
4636
5301
  });
@@ -5045,7 +5710,7 @@ var MCPClient = class {
5045
5710
  name,
5046
5711
  arguments: args,
5047
5712
  ...input === void 0 ? {} : {
5048
- requestState: input.state,
5713
+ ...input.state === void 0 ? {} : { requestState: input.state },
5049
5714
  inputResponses: input.responses
5050
5715
  }
5051
5716
  }, this.#timeout, void 0, options));
@@ -5595,7 +6260,9 @@ exports.MCPTextStreamController = MCPTextStreamController;
5595
6260
  exports.MCP_EXTENSION_TASKS = MCP_EXTENSION_TASKS;
5596
6261
  exports.MCP_FALLBACK_VERSION = MCP_FALLBACK_VERSION;
5597
6262
  exports.MCP_HANDSHAKE_VERSION = MCP_HANDSHAKE_VERSION;
6263
+ exports.MCP_HEADER_ANNOTATION = MCP_HEADER_ANNOTATION;
5598
6264
  exports.MCP_HEADER_MISMATCH = MCP_HEADER_MISMATCH;
6265
+ exports.MCP_LOOKUP_PAGES = MCP_LOOKUP_PAGES;
5599
6266
  exports.MCP_META_CAPABILITIES = MCP_META_CAPABILITIES;
5600
6267
  exports.MCP_META_CLIENT = MCP_META_CLIENT;
5601
6268
  exports.MCP_META_SERVER = MCP_META_SERVER;
@@ -5603,6 +6270,9 @@ exports.MCP_META_SUBSCRIPTION = MCP_META_SUBSCRIPTION;
5603
6270
  exports.MCP_META_VERSION = MCP_META_VERSION;
5604
6271
  exports.MCP_MISSING_CAPABILITY = MCP_MISSING_CAPABILITY;
5605
6272
  exports.MCP_MODERN_VERSION = MCP_MODERN_VERSION;
6273
+ exports.MCP_PARAM_PREFIX = MCP_PARAM_PREFIX;
6274
+ exports.MCP_SENTINEL_PREFIX = MCP_SENTINEL_PREFIX;
6275
+ exports.MCP_SENTINEL_SUFFIX = MCP_SENTINEL_SUFFIX;
5606
6276
  exports.MCP_UNSUPPORTED_VERSION = MCP_UNSUPPORTED_VERSION;
5607
6277
  exports.SUPPORTED_LEGACY_PROTOCOL_VERSIONS = SUPPORTED_LEGACY_PROTOCOL_VERSIONS;
5608
6278
  exports.SUPPORTED_MCP_VERSIONS = SUPPORTED_MCP_VERSIONS;
@@ -5612,6 +6282,8 @@ exports.bindServer = bindServer;
5612
6282
  exports.buildCallOutcome = buildCallOutcome;
5613
6283
  exports.buildCancelledNotification = buildCancelledNotification;
5614
6284
  exports.buildDiscoverResult = buildDiscoverResult;
6285
+ exports.buildHeaderParameters = buildHeaderParameters;
6286
+ exports.buildHeaderProjection = buildHeaderProjection;
5615
6287
  exports.buildInitializeResult = buildInitializeResult;
5616
6288
  exports.buildJSONRPCError = buildJSONRPCError;
5617
6289
  exports.buildJSONRPCResult = buildJSONRPCResult;
@@ -5623,14 +6295,20 @@ exports.buildSubscriptionFilter = buildSubscriptionFilter;
5623
6295
  exports.buildSubscriptionResult = buildSubscriptionResult;
5624
6296
  exports.buildToolCall = buildToolCall;
5625
6297
  exports.buildToolDescriptors = buildToolDescriptors;
6298
+ exports.computeMissingCapabilities = computeMissingCapabilities;
6299
+ exports.countHeaderAnnotations = countHeaderAnnotations;
5626
6300
  exports.createDuplexClientTransport = createDuplexClientTransport;
5627
6301
  exports.createMCPClient = createMCPClient;
5628
6302
  exports.createMCPLegacy = createMCPLegacy;
5629
6303
  exports.createMCPLegacyClientTransport = createMCPLegacyClientTransport;
5630
6304
  exports.createMCPServer = createMCPServer;
5631
6305
  exports.decodeBoundedMessage = decodeBoundedMessage;
6306
+ exports.decodeSentinel = decodeSentinel;
5632
6307
  exports.digestJSON = digestJSON;
6308
+ exports.encodeSentinel = encodeSentinel;
5633
6309
  exports.extractContentText = extractContentText;
6310
+ exports.extractHeaderAnnotations = extractHeaderAnnotations;
6311
+ exports.extractToolSchema = extractToolSchema;
5634
6312
  exports.inferEra = inferEra;
5635
6313
  exports.inferRequestVersion = inferRequestVersion;
5636
6314
  exports.inferVersion = inferVersion;
@@ -5638,6 +6316,7 @@ exports.isAbsoluteURI = isAbsoluteURI;
5638
6316
  exports.isBoundedJSON = isBoundedJSON;
5639
6317
  exports.isBoundedString = isBoundedString;
5640
6318
  exports.isElicitContent = isElicitContent;
6319
+ exports.isFieldToken = isFieldToken;
5641
6320
  exports.isFormElicitationSupported = isFormElicitationSupported;
5642
6321
  exports.isInitializeRequest = isInitializeRequest;
5643
6322
  exports.isJSONObject = isJSONObject;
@@ -5666,10 +6345,12 @@ exports.isMCPElicitResult = isMCPElicitResult;
5666
6345
  exports.isMCPElicitSchema = isMCPElicitSchema;
5667
6346
  exports.isMCPElicitURL = isMCPElicitURL;
5668
6347
  exports.isMCPError = isMCPError;
6348
+ exports.isMCPHeaderPrimitive = isMCPHeaderPrimitive;
5669
6349
  exports.isMCPIcon = isMCPIcon;
5670
6350
  exports.isMCPIdentity = isMCPIdentity;
5671
6351
  exports.isMCPInputRequest = isMCPInputRequest;
5672
6352
  exports.isMCPInputRequestMap = isMCPInputRequestMap;
6353
+ exports.isMCPInputResponse = isMCPInputResponse;
5673
6354
  exports.isMCPInputResult = isMCPInputResult;
5674
6355
  exports.isMCPLegacyResult = isMCPLegacyResult;
5675
6356
  exports.isMCPLegacyVersion = isMCPLegacyVersion;
@@ -5692,6 +6373,10 @@ exports.isMCPResourceTemplate = isMCPResourceTemplate;
5692
6373
  exports.isMCPResourceTemplatePage = isMCPResourceTemplatePage;
5693
6374
  exports.isMCPResult = isMCPResult;
5694
6375
  exports.isMCPResultMetaObject = isMCPResultMetaObject;
6376
+ exports.isMCPRoot = isMCPRoot;
6377
+ exports.isMCPRootResult = isMCPRootResult;
6378
+ exports.isMCPSampleContent = isMCPSampleContent;
6379
+ exports.isMCPSampleResult = isMCPSampleResult;
5695
6380
  exports.isMCPServerCapabilities = isMCPServerCapabilities;
5696
6381
  exports.isMCPStringArguments = isMCPStringArguments;
5697
6382
  exports.isMCPSubscriptionFilter = isMCPSubscriptionFilter;
@@ -5718,6 +6403,7 @@ exports.parseJSONRPCMessage = parseJSONRPCMessage;
5718
6403
  exports.parseMCPInputState = parseMCPInputState;
5719
6404
  exports.parseRequestContext = parseRequestContext;
5720
6405
  exports.readCancelledId = readCancelledId;
6406
+ exports.renderHeaderValue = renderHeaderValue;
5721
6407
  exports.sendStream = sendStream;
5722
6408
  exports.serializeJSON = serializeJSON;
5723
6409
  exports.snapshotJSON = snapshotJSON;