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