@orkestrel/mcp 0.0.23 → 0.0.25

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.
@@ -11,25 +11,24 @@ let _orkestrel_tool = require("@orkestrel/tool");
11
11
  * and defines no `initialize`, so it can never be the handshake's version — a client that offers
12
12
  * it is asking to negotiate a revision with no negotiation.
13
13
  */
14
- var MCP_PROTOCOL_VERSION = "2025-11-25";
15
- /** The legacy fallback anchor used when an initialize request cannot be accepted as modern. */
16
- var MCP_LEGACY_VERSION = "2025-06-18";
14
+ var MCP_HANDSHAKE_VERSION = "2025-11-25";
15
+ /** The older legacy revision the optional legacy decorator accepts and an adapter can pin. */
16
+ var MCP_FALLBACK_VERSION = "2025-06-18";
17
17
  /** The modern revision offered by an unpinned client during discovery. */
18
18
  var MCP_MODERN_VERSION = "2026-07-28";
19
19
  /**
20
- * The MCP protocol revisions this server can negotiate.
20
+ * The modern MCP protocol revisions a bare server accepts and advertises.
21
21
  *
22
22
  * @remarks
23
- * `initialize` echoes the client's requested `protocolVersion` when it appears in
24
- * this list. Frozen in client-preference and discovery-advertisement order. The
25
- * package does not advertise `2025-03-26` because that revision mandates JSON-RPC
26
- * batching, while this package accepts only individual JSON-RPC messages.
23
+ * Frozen in discovery-advertisement order. Legacy revisions are absent because
24
+ * only {@link SUPPORTED_LEGACY_PROTOCOL_VERSIONS} and the optional legacy
25
+ * decorator own them.
27
26
  */
28
- var SUPPORTED_PROTOCOL_VERSIONS = Object.freeze([
29
- "2026-07-28",
30
- "2025-11-25",
31
- "2025-06-18"
32
- ]);
27
+ var SUPPORTED_MODERN_PROTOCOL_VERSIONS = Object.freeze([MCP_MODERN_VERSION]);
28
+ /** The protocol revisions accepted by the optional legacy decorator. */
29
+ var SUPPORTED_LEGACY_PROTOCOL_VERSIONS = Object.freeze([MCP_HANDSHAKE_VERSION, MCP_FALLBACK_VERSION]);
30
+ /** The protocol revisions the `isMCPVersion` guard admits, spanning the modern and legacy eras. */
31
+ var SUPPORTED_MCP_VERSIONS = Object.freeze([...SUPPORTED_MODERN_PROTOCOL_VERSIONS, ...SUPPORTED_LEGACY_PROTOCOL_VERSIONS]);
33
32
  /** Reserved modern `_meta` key carrying the request's protocol revision. */
34
33
  var MCP_META_VERSION = "io.modelcontextprotocol/protocolVersion";
35
34
  /** Reserved modern `_meta` key carrying the client's open capability record. */
@@ -41,10 +40,11 @@ var MCP_META_SERVER = "io.modelcontextprotocol/serverInfo";
41
40
  /** Reserved modern `_meta` key carrying a `subscriptions/listen` request id. */
42
41
  var MCP_META_SUBSCRIPTION = "io.modelcontextprotocol/subscriptionId";
43
42
  /**
44
- * The reserved extension key identifying the draft Tasks extension.
43
+ * The reserved extension key identifying the stable Tasks extension.
45
44
  *
46
45
  * @remarks
47
- * The ONE spelling of it in this package. A client declares it per REQUEST, under
46
+ * The ONE spelling of it in this package, and the identity of the immutable snapshot dated
47
+ * 2026-07-28 this package implements. A client declares it per REQUEST, under
48
48
  * `_meta['io.modelcontextprotocol/clientCapabilities'].extensions`; a server advertises it
49
49
  * under `server/discover`'s `capabilities.extensions`. Both sides carry an empty object —
50
50
  * the extension defines no options, so presence is the entire declaration.
@@ -62,8 +62,8 @@ var MCP_HEADER_MISMATCH = -32020;
62
62
  * `error.data.requiredCapabilities` alone (`{ elicitation: {} }` against
63
63
  * `{ extensions: { 'io.modelcontextprotocol/tasks': {} } }`). They are instances of the same
64
64
  * condition, so a separate numeral would describe the same fact twice. The Tasks extension's
65
- * own draft prose still shows `-32003` in examples; the dated core schema fixes this code,
66
- * and the dated schema is what a peer implements against.
65
+ * own prose examples show `-32003`; the dated core schema fixes this code, and the dated
66
+ * schema is what a peer implements against.
67
67
  */
68
68
  var MCP_MISSING_CAPABILITY = -32021;
69
69
  /** MCP reserved error: a request names an unsupported protocol revision. */
@@ -148,6 +148,8 @@ var DEFAULT_MCP_CLIENT_VERSION = "1.0.0";
148
148
  * is unset — a request the remote server does not answer within it rejects.
149
149
  */
150
150
  var DEFAULT_MCP_REQUEST_TIMEOUT = 3e4;
151
+ /** The default number of subscription frames retained while no client read is parked. */
152
+ var DEFAULT_MCP_SUBSCRIPTION_CAPACITY = 64;
151
153
  //#endregion
152
154
  //#region src/core/errors.ts
153
155
  /**
@@ -210,66 +212,6 @@ function isMCPError(value) {
210
212
  }
211
213
  }
212
214
  //#endregion
213
- //#region src/core/inferers.ts
214
- /**
215
- * Infers the wire era for an MCP protocol revision.
216
- *
217
- * @param version - The protocol revision to classify
218
- * @returns `'modern'` for `2026-07-28`, `'legacy'` for either supported legacy
219
- * revision, or `undefined` when the revision is unsupported
220
- */
221
- function inferEra(version) {
222
- switch (version) {
223
- case "2026-07-28": return "modern";
224
- case "2025-11-25":
225
- case "2025-06-18": return "legacy";
226
- default: return;
227
- }
228
- }
229
- /**
230
- * Infers the newest supported protocol revision present in a peer's offer.
231
- *
232
- * @param offered - The protocol revisions offered by the peer
233
- * @returns The newest locally supported offered revision, or `undefined`
234
- */
235
- function inferVersion(offered) {
236
- for (const version of SUPPORTED_PROTOCOL_VERSIONS) if (offered.includes(version)) return version;
237
- }
238
- /**
239
- * Infers the protocol version an outbound message announces itself with — the ONE
240
- * projection every HTTP client transport stamps `mcp-protocol-version` from.
241
- *
242
- * @remarks
243
- * This is deliberately the SAME read the server's own expectation performs
244
- * ({@link import('@orkestrel/mcp/server').inferHeaderIssue}): a modern request's reserved
245
- * `_meta` version, accepted whenever it is a string. It is NOT
246
- * {@link import('./parsers.js').parseRequestContext}, and the difference is the whole
247
- * point. That parser answers a different question — is the modern metadata WELL FORMED —
248
- * and refuses a request whose capability declaration or logging level is malformed. Such a
249
- * request is still modern (era is fixed by key presence) and the server still demands the
250
- * header for it, so projecting through the parser withholds a header the peer requires and
251
- * earns `-32602` instead of the `-32602` the malformed metadata itself deserves.
252
- *
253
- * A non-modern message projects nothing: a legacy request's version comes from the
254
- * `initialize` handshake the transport captured, not from the message.
255
- *
256
- * Header NAMES stay with the transports that own the wire (see `constants.ts`); core owns
257
- * the value this projection derives, which is the part the browser and Node faces disagreed about.
258
- *
259
- * @param message - The outbound message about to be written
260
- * @returns The version to announce, or `undefined` when the message announces none
261
- *
262
- * @example
263
- * ```ts
264
- * inferRequestVersion({ jsonrpc: '2.0', id: 1, method: 'tools/list', params: { _meta: meta } })
265
- * ```
266
- */
267
- function inferRequestVersion(message) {
268
- if (!isModernRequest(message)) return void 0;
269
- const version = ((0, _orkestrel_contract.isRecord)(message.params?.["_meta"]) ? message.params["_meta"] : void 0)?.[MCP_META_VERSION];
270
- return (0, _orkestrel_contract.isString)(version) ? version : void 0;
271
- }
272
- //#endregion
273
215
  //#region src/core/cloners.ts
274
216
  /**
275
217
  * Snapshots one bounded exact JSON value together with its canonical wire serialization.
@@ -571,26 +513,29 @@ function isFormElicitationSupported(value) {
571
513
  }
572
514
  }
573
515
  /**
574
- * Determines whether a client capability record declares the draft Tasks extension.
516
+ * Determines whether a client capability record declares the stable Tasks extension.
575
517
  *
576
518
  * @remarks
577
- * The declaration lives at `extensions['io.modelcontextprotocol/tasks']` and its value is
578
- * an empty object, so this is a PRESENCE check: the extension defines no options, and a
579
- * server that read one would be reading a field no client can meaningfully set. The value
580
- * must still be a record, because that is the shape the capability record declares a
581
- * `true` or a string there is a client speaking a different protocol, not a shorthand.
519
+ * The declaration lives at `extensions['io.modelcontextprotocol/tasks']` and the schema
520
+ * types its value EXACTLY EMPTY `Record<string, never>`, an object with no additional
521
+ * properties. So the key's presence is the whole declaration, and the value carries the
522
+ * whole of the check: a `true` or a string there is a client speaking a different protocol
523
+ * rather than a shorthand, and a member inside the object is a client declaring an option
524
+ * this extension does not define. Both are refused, because a server that accepted either
525
+ * would be reading a shape no peer can produce from the snapshot's own schema.
582
526
  *
583
527
  * A client declares this PER REQUEST. Nothing here consults a session, because the modern
584
528
  * revision is stateless and a capability declared once at connect time says nothing about
585
529
  * the request in hand. Total over hostile input.
586
530
  *
587
531
  * @param value - The client capability record to inspect
588
- * @returns `true` when the tasks extension is declared
532
+ * @returns `true` when the tasks extension is declared as the schema's empty object
589
533
  *
590
534
  * @example
591
535
  * ```ts
592
536
  * isTaskSupported({ extensions: { 'io.modelcontextprotocol/tasks': {} } }) // true
593
537
  * isTaskSupported({ extensions: {} }) // false — the key is the declaration
538
+ * isTaskSupported({ extensions: { 'io.modelcontextprotocol/tasks': { on: true } } }) // false
594
539
  * ```
595
540
  */
596
541
  function isTaskSupported(value) {
@@ -598,7 +543,9 @@ function isTaskSupported(value) {
598
543
  if (!owned.success) return false;
599
544
  try {
600
545
  const extensions = owned.value["extensions"];
601
- return (0, _orkestrel_contract.isRecord)(extensions) && (0, _orkestrel_contract.isRecord)(extensions["io.modelcontextprotocol/tasks"]);
546
+ if (!(0, _orkestrel_contract.isRecord)(extensions)) return false;
547
+ const declaration = extensions[MCP_EXTENSION_TASKS];
548
+ return (0, _orkestrel_contract.isRecord)(declaration) && Object.keys(declaration).length === 0;
602
549
  } catch {
603
550
  return false;
604
551
  }
@@ -1143,23 +1090,118 @@ function buildModernResult(result, identity, ttl, scope) {
1143
1090
  };
1144
1091
  }
1145
1092
  /**
1093
+ * Projects one complete modern result onto the legacy wire shape.
1094
+ *
1095
+ * @remarks
1096
+ * The projection removes the modern discriminator, cache fields, and reserved server identity.
1097
+ * A non-complete result has no legacy representation and returns `undefined`.
1098
+ *
1099
+ * @param result - The modern result to project
1100
+ * @returns The legacy result, or `undefined` when the modern arm cannot be represented
1101
+ */
1102
+ function modernResultToLegacy(result) {
1103
+ if (result.resultType !== "complete") return void 0;
1104
+ const projected = {};
1105
+ for (const [key, value] of Object.entries(result)) {
1106
+ if (key === "resultType" || key === "ttlMs" || key === "cacheScope") continue;
1107
+ if (key === "content" && Array.isArray(value)) {
1108
+ projected[key] = value.map((entry) => (0, _orkestrel_contract.isRecord)(entry) && entry["type"] === "text" && (0, _orkestrel_contract.isString)(entry["text"]) ? {
1109
+ type: "text",
1110
+ text: entry["text"]
1111
+ } : entry);
1112
+ continue;
1113
+ }
1114
+ if (key !== "_meta" || !(0, _orkestrel_contract.isRecord)(value)) {
1115
+ projected[key] = value;
1116
+ continue;
1117
+ }
1118
+ const metadata = {};
1119
+ for (const [name, entry] of Object.entries(value)) if (name !== "io.modelcontextprotocol/serverInfo") metadata[name] = entry;
1120
+ if (Object.keys(metadata).length > 0) projected["_meta"] = metadata;
1121
+ }
1122
+ return projected;
1123
+ }
1124
+ /**
1125
+ * Restores one legacy result to the modern complete-result shape.
1126
+ *
1127
+ * @remarks
1128
+ * Legacy `tools/list` results receive the required modern cache fields. Other legacy results are
1129
+ * non-cacheable. Every restored result receives the server identity learned during `initialize`.
1130
+ *
1131
+ * @param result - The unstamped legacy result
1132
+ * @param method - The request method whose result is being restored
1133
+ * @param identity - The server identity learned during the legacy handshake
1134
+ * @returns The modern complete result
1135
+ */
1136
+ function legacyResultToModern(result, method, identity) {
1137
+ return method === "tools/list" ? buildModernResult(result, identity, DEFAULT_MCP_CACHE_TTL) : buildModernResult(result, identity);
1138
+ }
1139
+ /**
1140
+ * Stamps one legacy request for the modern dispatcher.
1141
+ *
1142
+ * @param request - The legacy request to translate
1143
+ * @returns A modern request carrying the package revision and an empty capability set
1144
+ */
1145
+ function legacyInvocationToModern(request) {
1146
+ const params = request.params ?? {};
1147
+ const metadata = (0, _orkestrel_contract.isRecord)(params["_meta"]) ? params["_meta"] : {};
1148
+ return {
1149
+ ...request,
1150
+ params: {
1151
+ ...params,
1152
+ _meta: {
1153
+ ...metadata,
1154
+ [MCP_META_VERSION]: MCP_MODERN_VERSION,
1155
+ [MCP_META_CAPABILITIES]: {}
1156
+ }
1157
+ }
1158
+ };
1159
+ }
1160
+ /**
1161
+ * Removes modern request metadata before an invocation reaches a legacy peer.
1162
+ *
1163
+ * @remarks
1164
+ * Non-reserved metadata such as `progressToken` remains on the legacy wire. When no metadata
1165
+ * remains, the translated parameters omit `_meta`.
1166
+ *
1167
+ * @param invocation - The modern invocation to translate
1168
+ * @returns The legacy invocation with reserved modern metadata removed
1169
+ */
1170
+ function modernInvocationToLegacy(invocation) {
1171
+ const params = invocation.params;
1172
+ if (params === void 0 || !(0, _orkestrel_contract.isRecord)(params["_meta"])) return invocation;
1173
+ const translated = {};
1174
+ for (const [key, value] of Object.entries(params)) if (key !== "_meta") translated[key] = value;
1175
+ const metadata = {};
1176
+ for (const [key, value] of Object.entries(params["_meta"])) if (key !== "io.modelcontextprotocol/protocolVersion" && key !== "io.modelcontextprotocol/clientCapabilities" && key !== "io.modelcontextprotocol/clientInfo") metadata[key] = value;
1177
+ if (Object.keys(metadata).length > 0) translated["_meta"] = metadata;
1178
+ return {
1179
+ ...invocation,
1180
+ params: translated
1181
+ };
1182
+ }
1183
+ /**
1146
1184
  * Intersects a requested subscription filter with the notification families a server supports.
1147
1185
  *
1148
1186
  * @param requested - The notification families requested by the client
1149
1187
  * @param supported - The notification families the server can actually produce
1188
+ * @param enabled - If `true`, carries the requested task identifiers into the filter; if `false`,
1189
+ * omits them. Default: `false`
1150
1190
  * @returns The exact subset the server will honour
1151
1191
  */
1152
- function buildSubscriptionFilter(requested, supported) {
1192
+ function buildSubscriptionFilter(requested, supported, enabled = false) {
1153
1193
  const toolsListChanged = requested.toolsListChanged === true && supported.toolsListChanged === true;
1154
1194
  const promptsListChanged = requested.promptsListChanged === true && supported.promptsListChanged === true;
1155
1195
  const resourcesListChanged = requested.resourcesListChanged === true && supported.resourcesListChanged === true;
1156
1196
  const supportedResources = new Set(supported.resourceSubscriptions ?? []);
1157
1197
  const resourceSubscriptions = requested.resourceSubscriptions?.filter((uri) => supportedResources.has(uri));
1198
+ const taskIds = enabled ? requested.taskIds : void 0;
1158
1199
  return {
1159
1200
  ...toolsListChanged ? { toolsListChanged: true } : {},
1160
1201
  ...promptsListChanged ? { promptsListChanged: true } : {},
1161
1202
  ...resourcesListChanged ? { resourcesListChanged: true } : {},
1162
- ...resourceSubscriptions !== void 0 && resourceSubscriptions.length > 0 ? { resourceSubscriptions } : {}
1203
+ ...resourceSubscriptions !== void 0 && resourceSubscriptions.length > 0 ? { resourceSubscriptions } : {},
1204
+ ...taskIds !== void 0 && taskIds.length > 0 ? { taskIds } : {}
1163
1205
  };
1164
1206
  }
1165
1207
  /**
@@ -1173,9 +1215,12 @@ function matchesSubscriptionNotification(notification, filter) {
1173
1215
  if (notification.method === "notifications/tools/list_changed") return filter.toolsListChanged === true;
1174
1216
  if (notification.method === "notifications/prompts/list_changed") return filter.promptsListChanged === true;
1175
1217
  if (notification.method === "notifications/resources/list_changed") return filter.resourcesListChanged === true;
1176
- if (notification.method !== "notifications/resources/updated") return false;
1177
- const uri = notification.params?.["uri"];
1178
- return typeof uri === "string" && filter.resourceSubscriptions?.includes(uri) === true;
1218
+ if (notification.method === "notifications/resources/updated") {
1219
+ const uri = notification.params?.["uri"];
1220
+ return typeof uri === "string" && filter.resourceSubscriptions?.includes(uri) === true;
1221
+ }
1222
+ if (notification.method === "notifications/tasks") return isMCPTaskNotification(notification) && filter.taskIds?.includes(notification.params.taskId) === true;
1223
+ return false;
1179
1224
  }
1180
1225
  /**
1181
1226
  * Stamps a subscription notification with the request id reserved for its held-open stream.
@@ -1240,7 +1285,7 @@ function buildSubscriptionResult(id, identity) {
1240
1285
  */
1241
1286
  function buildDiscoverResult(options) {
1242
1287
  return buildModernResult({
1243
- supportedVersions: SUPPORTED_PROTOCOL_VERSIONS.filter(isMCPVersion),
1288
+ supportedVersions: SUPPORTED_MODERN_PROTOCOL_VERSIONS,
1244
1289
  capabilities: {
1245
1290
  tools: {},
1246
1291
  ...options.resources === void 0 ? {} : { resources: {
@@ -1271,9 +1316,8 @@ function buildDiscoverResult(options) {
1271
1316
  * @returns The `initialize` result payload
1272
1317
  */
1273
1318
  function buildInitializeResult(name, version, requested) {
1274
- const newestLegacy = SUPPORTED_PROTOCOL_VERSIONS.find((candidate) => inferEra(candidate) === "legacy") ?? "2025-06-18";
1275
1319
  return {
1276
- protocolVersion: isMCPVersion(requested) && inferEra(requested) === "legacy" ? requested : newestLegacy,
1320
+ protocolVersion: isMCPLegacyVersion(requested) ? requested : MCP_HANDSHAKE_VERSION,
1277
1321
  capabilities: { tools: {} },
1278
1322
  serverInfo: {
1279
1323
  name,
@@ -1587,6 +1631,31 @@ function isMCPResultMetaObject(value) {
1587
1631
  const identity = owned.value[MCP_META_SERVER];
1588
1632
  return (0, _orkestrel_contract.isUndefined)(identity) || isMCPIdentity(identity);
1589
1633
  }
1634
+ /**
1635
+ * Determines whether a value is exact notification metadata with a valid reserved
1636
+ * subscription id.
1637
+ *
1638
+ * @remarks
1639
+ * The reserved key is OPTIONAL, so a frame delivered outside a `subscriptions/listen`
1640
+ * stream passes with no stamp at all. When the key IS present its value must be a valid
1641
+ * {@link JSONRPCId}, because a stamp naming nothing addressable is worse than no stamp.
1642
+ *
1643
+ * @param value - The unknown value to inspect
1644
+ * @returns `true` when the value is exact metadata whose subscription stamp, if present, is valid
1645
+ *
1646
+ * @example
1647
+ * ```ts
1648
+ * isMCPNotificationMetaObject({}) // true — an unstamped frame carries no subscription
1649
+ * isMCPNotificationMetaObject({ 'io.modelcontextprotocol/subscriptionId': 7 }) // true
1650
+ * isMCPNotificationMetaObject({ 'io.modelcontextprotocol/subscriptionId': null }) // false
1651
+ * ```
1652
+ */
1653
+ function isMCPNotificationMetaObject(value) {
1654
+ const owned = (0, _orkestrel_contract.attempt)(() => (0, _orkestrel_contract.cloneJSONRecord)(value));
1655
+ if (!owned.success || !Object.keys(owned.value).every((key) => isMCPMetaKey(key))) return false;
1656
+ const subscription = owned.value[MCP_META_SUBSCRIPTION];
1657
+ return (0, _orkestrel_contract.isUndefined)(subscription) || isJSONRPCId(subscription);
1658
+ }
1590
1659
  /** Determines whether a value is one dated MCP logging level. */
1591
1660
  function isMCPLoggingLevel(value) {
1592
1661
  return value === "debug" || value === "info" || value === "notice" || value === "warning" || value === "error" || value === "critical" || value === "alert" || value === "emergency";
@@ -2249,7 +2318,8 @@ function isMCPCallResult(value) {
2249
2318
  * shape. The manager is consumer-supplied, so its types are a promise rather than a
2250
2319
  * proof: this is what stands between a manager that answers a numeric `taskId` and a
2251
2320
  * client that would receive one. `ttlMs` accepts `null` because the schema uses it to
2252
- * mean "no expiry", which is distinct from an absent field.
2321
+ * mean "no expiry", which is distinct from an absent field, and both durations must be
2322
+ * INTEGER milliseconds because the schema formats them `int`.
2253
2323
  *
2254
2324
  * @param value - The unknown value to inspect
2255
2325
  * @returns Whether the value is a well-formed `resultType: 'task'` result
@@ -2270,7 +2340,7 @@ function isMCPTaskResult(value) {
2270
2340
  const interval = result["pollIntervalMs"];
2271
2341
  const lifetime = result["ttlMs"];
2272
2342
  const metadata = result["_meta"];
2273
- return (0, _orkestrel_contract.isString)(result["taskId"]) && isMCPTaskStatus(result["status"]) && (0, _orkestrel_contract.isString)(result["createdAt"]) && (0, _orkestrel_contract.isString)(result["lastUpdatedAt"]) && (lifetime === null || (0, _orkestrel_contract.isFiniteNumber)(lifetime)) && ((0, _orkestrel_contract.isUndefined)(message) || (0, _orkestrel_contract.isString)(message)) && ((0, _orkestrel_contract.isUndefined)(interval) || (0, _orkestrel_contract.isFiniteNumber)(interval)) && ((0, _orkestrel_contract.isUndefined)(metadata) || isMCPResultMetaObject(metadata));
2343
+ return (0, _orkestrel_contract.isString)(result["taskId"]) && isMCPTaskStatus(result["status"]) && (0, _orkestrel_contract.isString)(result["createdAt"]) && (0, _orkestrel_contract.isString)(result["lastUpdatedAt"]) && (lifetime === null || (0, _orkestrel_contract.isInteger)(lifetime)) && ((0, _orkestrel_contract.isUndefined)(message) || (0, _orkestrel_contract.isString)(message)) && ((0, _orkestrel_contract.isUndefined)(interval) || (0, _orkestrel_contract.isInteger)(interval)) && ((0, _orkestrel_contract.isUndefined)(metadata) || isMCPResultMetaObject(metadata));
2274
2344
  } catch {
2275
2345
  return false;
2276
2346
  }
@@ -2300,9 +2370,14 @@ function isMCPTaskStatus(value) {
2300
2370
  * the requests to answer, `completed` owns the deferred call's result, `failed` owns the
2301
2371
  * JSON-RPC error that ended it, and `working` / `cancelled` own nothing further.
2302
2372
  *
2303
- * Unrecognized members stay valid, because the extension is DRAFT and a manager tracking a
2304
- * later revision must not be refused by this one. What is checked is what this package
2305
- * publishes as the contract.
2373
+ * A `completed` task's `result` is checked as an OBJECT and no further. The schema declares
2374
+ * it an open record, so its contents belong to whichever method was deferred; a guard that
2375
+ * demanded a protocol result here would refuse payloads the extension permits.
2376
+ * `ttlMs` and `pollIntervalMs` are integer milliseconds, per the schema's `int` formats.
2377
+ *
2378
+ * Unrecognized members stay valid, because this guard reads a value a consumer's manager
2379
+ * produced, and a guard over a foreign contract enforces the published contract and no more.
2380
+ * What is checked is what this package publishes as the contract.
2306
2381
  *
2307
2382
  * @param value - The unknown value to inspect
2308
2383
  * @returns Whether the value is a well-formed {@link MCPTaskDetail}
@@ -2324,9 +2399,9 @@ function isMCPTaskDetail(value) {
2324
2399
  const message = detail["statusMessage"];
2325
2400
  const interval = detail["pollIntervalMs"];
2326
2401
  const lifetime = detail["ttlMs"];
2327
- if (!(0, _orkestrel_contract.isString)(detail["taskId"]) || !isMCPTaskStatus(status) || !(0, _orkestrel_contract.isString)(detail["createdAt"]) || !(0, _orkestrel_contract.isString)(detail["lastUpdatedAt"]) || lifetime !== null && !(0, _orkestrel_contract.isFiniteNumber)(lifetime) || !(0, _orkestrel_contract.isUndefined)(message) && !(0, _orkestrel_contract.isString)(message) || !(0, _orkestrel_contract.isUndefined)(interval) && !(0, _orkestrel_contract.isFiniteNumber)(interval)) return false;
2402
+ if (!(0, _orkestrel_contract.isString)(detail["taskId"]) || !isMCPTaskStatus(status) || !(0, _orkestrel_contract.isString)(detail["createdAt"]) || !(0, _orkestrel_contract.isString)(detail["lastUpdatedAt"]) || lifetime !== null && !(0, _orkestrel_contract.isInteger)(lifetime) || !(0, _orkestrel_contract.isUndefined)(message) && !(0, _orkestrel_contract.isString)(message) || !(0, _orkestrel_contract.isUndefined)(interval) && !(0, _orkestrel_contract.isInteger)(interval)) return false;
2328
2403
  if (status === "input_required") return isMCPInputRequestMap(detail["inputRequests"]);
2329
- if (status === "completed") return isMCPResult(detail["result"]);
2404
+ if (status === "completed") return (0, _orkestrel_contract.isRecord)(detail["result"]);
2330
2405
  if (status === "failed") return isJSONRPCError(detail["error"]);
2331
2406
  return true;
2332
2407
  } catch {
@@ -2334,6 +2409,78 @@ function isMCPTaskDetail(value) {
2334
2409
  }
2335
2410
  }
2336
2411
  /**
2412
+ * Determines whether a value is the wire answer to `tasks/get`.
2413
+ *
2414
+ * @remarks
2415
+ * {@link isMCPTaskDetail} plus the stamp the METHOD owes. The schema types a `tasks/get`
2416
+ * reply as the detail intersected with the standard result, so `resultType: 'complete'` is
2417
+ * part of the answer rather than decoration on it — and an unstamped payload, or one
2418
+ * carrying the creation answer's `resultType: 'task'`, is a peer answering some other
2419
+ * shape. Use this guard wherever a `tasks/get` REPLY is read; use
2420
+ * {@link isMCPTaskDetail} wherever a consumer's manager answers directly.
2421
+ *
2422
+ * `_meta` is checked only when present, and only as result metadata: the server identity a
2423
+ * peer stamps there is the peer's to write.
2424
+ *
2425
+ * @param value - The unknown value to inspect
2426
+ * @returns Whether the value is a well-formed {@link MCPTaskDetailResult}
2427
+ *
2428
+ * @example
2429
+ * ```ts
2430
+ * isMCPTaskDetailResult({ resultType: 'complete', taskId: 'a', status: 'working',
2431
+ * createdAt: '', lastUpdatedAt: '', ttlMs: null }) // true
2432
+ * isMCPTaskDetailResult({ taskId: 'a', status: 'working', createdAt: '',
2433
+ * lastUpdatedAt: '', ttlMs: null }) // false — the reply owes its `resultType`
2434
+ * ```
2435
+ */
2436
+ function isMCPTaskDetailResult(value) {
2437
+ const owned = (0, _orkestrel_contract.attempt)(() => (0, _orkestrel_contract.cloneJSONRecord)(value));
2438
+ if (!owned.success) return false;
2439
+ const result = owned.value;
2440
+ if (result["resultType"] !== "complete") return false;
2441
+ const metadata = result["_meta"];
2442
+ if (!(0, _orkestrel_contract.isUndefined)(metadata) && !isMCPResultMetaObject(metadata)) return false;
2443
+ return isMCPTaskDetail(result);
2444
+ }
2445
+ /**
2446
+ * Determines whether a value is a `notifications/tasks` frame carrying a task snapshot.
2447
+ *
2448
+ * @remarks
2449
+ * The ADMISSION guard for a task transition: a subscription producer is consumer-written,
2450
+ * so the frame it hands over is foreign input, and this is what stands between a mutated
2451
+ * or half-built snapshot and a subscribed client. Both halves are checked — the method
2452
+ * literal the extension fixes, and params that hold together as an
2453
+ * {@link MCPTaskDetail} — because either alone admits a frame the other rejects.
2454
+ *
2455
+ * `_meta` is checked for SHAPE WHEN PRESENT and nothing more. The reserved subscription
2456
+ * stamp is the SERVER'S to write, after this guard admits the frame and the matcher agrees
2457
+ * to it, so a guard that demanded the stamp would refuse every frame a producer emits.
2458
+ *
2459
+ * @param value - The unknown value to inspect
2460
+ * @returns Whether the value is a well-formed `notifications/tasks` notification
2461
+ *
2462
+ * @example
2463
+ * ```ts
2464
+ * isMCPTaskNotification({ jsonrpc: '2.0', method: 'notifications/tasks',
2465
+ * params: { taskId: 'a', status: 'working', createdAt: '', lastUpdatedAt: '',
2466
+ * ttlMs: null } }) // true
2467
+ * isMCPTaskNotification({ jsonrpc: '2.0', method: 'notifications/tasks',
2468
+ * params: { taskId: 'a' } }) // false — the params owe a whole snapshot
2469
+ * ```
2470
+ */
2471
+ function isMCPTaskNotification(value) {
2472
+ const owned = (0, _orkestrel_contract.attempt)(() => (0, _orkestrel_contract.cloneJSONRecord)(value));
2473
+ if (!owned.success) return false;
2474
+ const notification = owned.value;
2475
+ if (!isJSONRPCNotification(notification)) return false;
2476
+ if (notification["method"] !== "notifications/tasks") return false;
2477
+ const params = notification["params"];
2478
+ if (!(0, _orkestrel_contract.isRecord)(params)) return false;
2479
+ const metadata = params["_meta"];
2480
+ if (!(0, _orkestrel_contract.isUndefined)(metadata) && !isMCPNotificationMetaObject(metadata)) return false;
2481
+ return isMCPTaskDetail(params);
2482
+ }
2483
+ /**
2337
2484
  * Determines whether a value is a string within a UTF-8 byte bound.
2338
2485
  *
2339
2486
  * @param value - The unknown value to inspect
@@ -2414,19 +2561,41 @@ function isJSONRPCId(value) {
2414
2561
  * Determines whether a value is a supported {@link MCPVersion}.
2415
2562
  *
2416
2563
  * @param value - The unknown value to inspect
2417
- * @returns `true` when the value is one of {@link SUPPORTED_PROTOCOL_VERSIONS}
2564
+ * @returns `true` when the value is one of {@link SUPPORTED_MCP_VERSIONS}
2418
2565
  */
2419
2566
  function isMCPVersion(value) {
2420
- return (0, _orkestrel_contract.isString)(value) && SUPPORTED_PROTOCOL_VERSIONS.some((version) => version === value);
2567
+ return (0, _orkestrel_contract.isString)(value) && SUPPORTED_MCP_VERSIONS.some((version) => version === value);
2568
+ }
2569
+ /**
2570
+ * Determines whether a value is a modern protocol revision accepted by a bare server.
2571
+ *
2572
+ * @param value - The unknown value to inspect
2573
+ * @returns `true` when the value is one of {@link SUPPORTED_MODERN_PROTOCOL_VERSIONS}
2574
+ */
2575
+ function isMCPModernVersion(value) {
2576
+ return (0, _orkestrel_contract.isString)(value) && SUPPORTED_MODERN_PROTOCOL_VERSIONS.some((version) => version === value);
2577
+ }
2578
+ /**
2579
+ * Determines whether a value is a revision accepted by the optional legacy decorator.
2580
+ *
2581
+ * @param value - The unknown value to inspect
2582
+ * @returns `true` when the value is one of {@link SUPPORTED_LEGACY_PROTOCOL_VERSIONS}
2583
+ */
2584
+ function isMCPLegacyVersion(value) {
2585
+ return (0, _orkestrel_contract.isString)(value) && SUPPORTED_LEGACY_PROTOCOL_VERSIONS.some((version) => version === value);
2421
2586
  }
2422
2587
  /**
2423
2588
  * Determines whether a value is an MCP {@link MCPSubscriptionFilter}.
2424
2589
  *
2425
2590
  * @remarks
2426
- * Every filter field is optional. Boolean notification families accept only booleans, and
2427
- * `resourceSubscriptions` accepts only an array of string URIs. Unknown fields remain open
2428
- * for protocol extensions and are ignored by the built-in subscription matcher. Total over
2429
- * hostile input.
2591
+ * Every filter field is optional. Boolean notification families accept only booleans,
2592
+ * `resourceSubscriptions` accepts only an array of string URIs, and `taskIds` accepts only
2593
+ * an array of string task identifiers. Unknown fields remain open for protocol extensions
2594
+ * and are ignored by the built-in subscription matcher. Total over hostile input.
2595
+ *
2596
+ * A malformed `taskIds` is refused here rather than dropped, so the listen request that
2597
+ * carried it fails outright instead of quietly agreeing to a narrower subscription than
2598
+ * the caller asked for.
2430
2599
  *
2431
2600
  * @param value - The unknown value to inspect
2432
2601
  * @returns `true` when every recognized filter field has its protocol shape
@@ -2442,7 +2611,21 @@ function isMCPSubscriptionFilter(value) {
2442
2611
  const resources = filter["resourcesListChanged"];
2443
2612
  if (!(0, _orkestrel_contract.isUndefined)(resources) && !(0, _orkestrel_contract.isBoolean)(resources)) return false;
2444
2613
  const subscriptions = filter["resourceSubscriptions"];
2445
- return (0, _orkestrel_contract.isUndefined)(subscriptions) || (0, _orkestrel_contract.arrayOf)(_orkestrel_contract.isString)(subscriptions);
2614
+ if (!(0, _orkestrel_contract.isUndefined)(subscriptions) && !(0, _orkestrel_contract.arrayOf)(_orkestrel_contract.isString)(subscriptions)) return false;
2615
+ const tasks = filter["taskIds"];
2616
+ return (0, _orkestrel_contract.isUndefined)(tasks) || (0, _orkestrel_contract.arrayOf)(_orkestrel_contract.isString)(tasks);
2617
+ }
2618
+ /**
2619
+ * Determines whether a value is a graceful `subscriptions/listen` result.
2620
+ *
2621
+ * @param value - The unknown value to inspect
2622
+ * @returns `true` when the result is complete and carries a valid subscription id
2623
+ */
2624
+ function isMCPSubscriptionResult(value) {
2625
+ const owned = (0, _orkestrel_contract.attempt)(() => (0, _orkestrel_contract.cloneJSONRecord)(value));
2626
+ if (!owned.success || owned.value["resultType"] !== "complete") return false;
2627
+ const metadata = owned.value["_meta"];
2628
+ return isMCPResultMetaObject(metadata) && (0, _orkestrel_contract.isRecord)(metadata) && isJSONRPCId(metadata["io.modelcontextprotocol/subscriptionId"]);
2446
2629
  }
2447
2630
  /**
2448
2631
  * Determines whether a value is one restricted primitive form-elicitation schema.
@@ -3041,6 +3224,67 @@ function isModernRequest(value) {
3041
3224
  return (0, _orkestrel_contract.isRecord)(metadata) && Object.hasOwn(metadata, "io.modelcontextprotocol/protocolVersion");
3042
3225
  }
3043
3226
  //#endregion
3227
+ //#region src/core/inferers.ts
3228
+ /**
3229
+ * Infers the wire era for an MCP protocol revision.
3230
+ *
3231
+ * @remarks
3232
+ * The era is READ from the two era guards rather than restated here, so a revision added
3233
+ * to {@link SUPPORTED_MODERN_PROTOCOL_VERSIONS} or {@link SUPPORTED_LEGACY_PROTOCOL_VERSIONS}
3234
+ * carries its era with it and no third list can disagree with those two.
3235
+ *
3236
+ * @param version - The protocol revision to classify
3237
+ * @returns `'modern'` for a revision a bare server accepts, `'legacy'` for a revision the
3238
+ * optional decorator accepts, or `undefined` when the revision is unsupported
3239
+ */
3240
+ function inferEra(version) {
3241
+ if (isMCPModernVersion(version)) return "modern";
3242
+ if (isMCPLegacyVersion(version)) return "legacy";
3243
+ }
3244
+ /**
3245
+ * Infers the newest supported modern protocol revision present in a peer's offer.
3246
+ *
3247
+ * @param offered - The protocol revisions offered by the peer
3248
+ * @returns The newest locally supported modern revision, or `undefined`
3249
+ */
3250
+ function inferVersion(offered) {
3251
+ for (const version of SUPPORTED_MODERN_PROTOCOL_VERSIONS) if (offered.includes(version)) return version;
3252
+ }
3253
+ /**
3254
+ * Infers the protocol version an outbound message announces itself with — the ONE
3255
+ * projection every HTTP client transport stamps `mcp-protocol-version` from.
3256
+ *
3257
+ * @remarks
3258
+ * This is deliberately the SAME read the server's own expectation performs
3259
+ * ({@link import('@orkestrel/mcp/server').inferHeaderIssue}): a modern request's reserved
3260
+ * `_meta` version, accepted whenever it is a string. It is NOT
3261
+ * {@link import('./parsers.js').parseRequestContext}, and the difference is the whole
3262
+ * point. That parser answers a different question — is the modern metadata WELL FORMED —
3263
+ * and refuses a request whose capability declaration or logging level is malformed. Such a
3264
+ * request is still modern (era is fixed by key presence) and the server still demands the
3265
+ * header for it, so projecting through the parser withholds a header the peer requires and
3266
+ * earns `-32602` instead of the `-32602` the malformed metadata itself deserves.
3267
+ *
3268
+ * A non-modern message projects nothing: a legacy request's version comes from the
3269
+ * `initialize` handshake the transport captured, not from the message.
3270
+ *
3271
+ * Header NAMES stay with the transports that own the wire (see `constants.ts`); core owns
3272
+ * the value this projection derives, which is the part the browser and Node faces disagreed about.
3273
+ *
3274
+ * @param message - The outbound message about to be written
3275
+ * @returns The version to announce, or `undefined` when the message announces none
3276
+ *
3277
+ * @example
3278
+ * ```ts
3279
+ * inferRequestVersion({ jsonrpc: '2.0', id: 1, method: 'tools/list', params: { _meta: meta } })
3280
+ * ```
3281
+ */
3282
+ function inferRequestVersion(message) {
3283
+ if (!isModernRequest(message)) return void 0;
3284
+ const version = ((0, _orkestrel_contract.isRecord)(message.params?.["_meta"]) ? message.params["_meta"] : void 0)?.[MCP_META_VERSION];
3285
+ return (0, _orkestrel_contract.isString)(version) ? version : void 0;
3286
+ }
3287
+ //#endregion
3044
3288
  //#region src/core/MCPMethodManager.ts
3045
3289
  /**
3046
3290
  * The modern method registry an {@link import('./types.js').MCPServerInterface}
@@ -3529,9 +3773,12 @@ var MCPTextStreamController = class {
3529
3773
  * Translates the fixed legacy method set onto one modern dispatcher.
3530
3774
  *
3531
3775
  * @remarks
3532
- * This decorator owns no execution engine or result normalizer. Modern invocations
3533
- * pass through untouched. Legacy tool methods acquire modern request metadata, run
3534
- * through the configured dispatcher, and lose only fields their dated result shape
3776
+ * This decorator answers `initialize` and `ping` itself, under the limits the configured
3777
+ * dispatcher advertises through {@link MCPLegacy.limit}: an invocation outside the message
3778
+ * bound earns the same id-less `-32600` refusal the dispatcher produces, whether this
3779
+ * decorator would have answered it or forwarded it. It owns no result normalizer. Modern
3780
+ * invocations pass through untouched. Legacy tool methods acquire modern request metadata,
3781
+ * run through the configured dispatcher, and lose only fields their dated result shape
3535
3782
  * cannot represent.
3536
3783
  */
3537
3784
  var MCPLegacy = class {
@@ -3556,6 +3803,7 @@ var MCPLegacy = class {
3556
3803
  return this.#legacy(invocation, options);
3557
3804
  }
3558
3805
  async handle(message, options) {
3806
+ if (!isBoundedString(message, this.limit.message)) return JSON.stringify(buildJSONRPCError(void 0, JSONRPC_INVALID_REQUEST, "Invalid Request"));
3559
3807
  let parsed;
3560
3808
  try {
3561
3809
  parsed = JSON.parse(message);
@@ -3569,12 +3817,16 @@ var MCPLegacy = class {
3569
3817
  async #legacy(invocation, options) {
3570
3818
  if (invocation.id === void 0) return void 0;
3571
3819
  const id = invocation.id;
3820
+ if (parseJSONRPCMessage(invocation, {
3821
+ bytes: this.limit.message,
3822
+ depth: this.limit.depth
3823
+ }) === void 0) return buildJSONRPCError(void 0, JSONRPC_INVALID_REQUEST, "Invalid Request");
3572
3824
  switch (invocation.method) {
3573
3825
  case "initialize": {
3574
3826
  const requested = invocation.params?.["protocolVersion"];
3575
3827
  return buildJSONRPCResult(id, buildInitializeResult(this.#options.identity.name, this.#options.identity.version, (0, _orkestrel_contract.isString)(requested) ? requested : void 0));
3576
3828
  }
3577
- case "ping":
3829
+ case "ping": return buildJSONRPCResult(id, {});
3578
3830
  case "tools/list": return this.#forward(invocation, options);
3579
3831
  case "tools/call":
3580
3832
  if (invocation.params !== void 0 && (Object.hasOwn(invocation.params, "requestState") || Object.hasOwn(invocation.params, "inputResponses"))) return buildJSONRPCError(id, JSONRPC_INVALID_PARAMS, "Invalid params: legacy requests cannot continue an input-required result");
@@ -3583,19 +3835,7 @@ var MCPLegacy = class {
3583
3835
  }
3584
3836
  }
3585
3837
  async #forward(request, options) {
3586
- const params = request.params ?? {};
3587
- const metadata = (0, _orkestrel_contract.isRecord)(params["_meta"]) ? params["_meta"] : {};
3588
- const translated = {
3589
- ...request,
3590
- params: {
3591
- ...params,
3592
- _meta: {
3593
- ...metadata,
3594
- [MCP_META_VERSION]: MCP_MODERN_VERSION,
3595
- [MCP_META_CAPABILITIES]: {}
3596
- }
3597
- }
3598
- };
3838
+ const translated = legacyInvocationToModern(request);
3599
3839
  const answer = await this.#options.dispatcher.dispatch(translated, options);
3600
3840
  if (Symbol.asyncIterator in answer) {
3601
3841
  answer.stop();
@@ -3606,25 +3846,8 @@ var MCPLegacy = class {
3606
3846
  }
3607
3847
  #project(answer, id) {
3608
3848
  if (answer.error !== void 0) return answer.error.code === -32021 ? this.#unsupported(id, this.#capability(answer)) : answer;
3609
- if (answer.result.resultType !== "complete") return this.#unsupported(id, answer.result.resultType ?? "unstamped");
3610
- const projected = {};
3611
- for (const [key, value] of Object.entries(answer.result)) {
3612
- if (key === "resultType" || key === "ttlMs" || key === "cacheScope") continue;
3613
- if (key === "content" && Array.isArray(value)) {
3614
- projected[key] = value.map((entry) => (0, _orkestrel_contract.isRecord)(entry) && entry["type"] === "text" && (0, _orkestrel_contract.isString)(entry["text"]) ? {
3615
- type: "text",
3616
- text: entry["text"]
3617
- } : entry);
3618
- continue;
3619
- }
3620
- if (key !== "_meta" || !(0, _orkestrel_contract.isRecord)(value)) {
3621
- projected[key] = value;
3622
- continue;
3623
- }
3624
- const metadata = {};
3625
- for (const [name, entry] of Object.entries(value)) if (name !== "io.modelcontextprotocol/serverInfo") metadata[name] = entry;
3626
- if (Object.keys(metadata).length > 0) projected["_meta"] = metadata;
3627
- }
3849
+ const projected = modernResultToLegacy(answer.result);
3850
+ if (projected === void 0) return this.#unsupported(id, answer.result.resultType ?? "unstamped");
3628
3851
  return buildJSONRPCResult(id, projected);
3629
3852
  }
3630
3853
  #capability(answer) {
@@ -3636,7 +3859,230 @@ var MCPLegacy = class {
3636
3859
  return (0, _orkestrel_contract.isRecord)(extensions) && Object.hasOwn(extensions, "io.modelcontextprotocol/tasks") ? "task" : "input-required";
3637
3860
  }
3638
3861
  #unsupported(id, result) {
3639
- return buildJSONRPCError(id, JSONRPC_SERVER_ERROR, `Legacy protocol ${MCP_PROTOCOL_VERSION} cannot represent ${result === "input-required" ? "an" : "a"} ${result} result`);
3862
+ return buildJSONRPCError(id, JSONRPC_SERVER_ERROR, `Legacy protocol ${MCP_HANDSHAKE_VERSION} cannot represent ${result === "input-required" ? "an" : "a"} ${result} result`);
3863
+ }
3864
+ };
3865
+ //#endregion
3866
+ //#region src/core/MCPLegacyClientTransport.ts
3867
+ /**
3868
+ * Adapts a legacy MCP peer to the modern client transport boundary.
3869
+ *
3870
+ * @remarks
3871
+ * `start` performs the legacy `initialize` handshake. The adapter answers
3872
+ * `server/discover` locally from that handshake, removes modern request metadata before writes,
3873
+ * restores legacy results to modern complete-result shapes before delivery, and bounds retained
3874
+ * request correlations with the configured deadline.
3875
+ */
3876
+ var MCPLegacyClientTransport = class {
3877
+ #emitter = new _orkestrel_emitter.Emitter();
3878
+ #transport;
3879
+ #client;
3880
+ #capabilities;
3881
+ #pin;
3882
+ #timeout;
3883
+ #correlations = /* @__PURE__ */ new Map();
3884
+ #handshake = void 0;
3885
+ #instructions = void 0;
3886
+ #server = void 0;
3887
+ #supported = void 0;
3888
+ /**
3889
+ * Creates a legacy client transport adapter.
3890
+ *
3891
+ * @param transport - The legacy peer transport
3892
+ * @param options - The legacy handshake identity, capabilities, revision, and deadline
3893
+ */
3894
+ constructor(transport, options) {
3895
+ const requested = options?.version;
3896
+ if (requested !== void 0 && !isMCPLegacyVersion(requested)) throw new MCPError("Unsupported legacy protocol version", MCP_UNSUPPORTED_VERSION, { requested });
3897
+ this.#transport = transport;
3898
+ this.#client = options?.identity ?? {
3899
+ name: "taverna",
3900
+ version: "1.0.0"
3901
+ };
3902
+ this.#capabilities = options?.capabilities ?? {};
3903
+ this.#pin = requested;
3904
+ this.#timeout = options?.timeout ?? 3e4;
3905
+ transport.emitter.on("message", (message) => this.#receive(message));
3906
+ transport.emitter.on("close", () => this.#emitter.emit("close"));
3907
+ transport.emitter.on("error", (error) => this.#emitter.emit("error", error));
3908
+ }
3909
+ get emitter() {
3910
+ return this.#emitter;
3911
+ }
3912
+ get session() {
3913
+ return this.#transport.session;
3914
+ }
3915
+ get duplex() {
3916
+ return this.#transport.duplex;
3917
+ }
3918
+ async start() {
3919
+ await this.#transport.start();
3920
+ try {
3921
+ await this.#initialize();
3922
+ } catch (error) {
3923
+ try {
3924
+ await this.#transport.close();
3925
+ } catch (fault) {
3926
+ this.#emitter.emit("error", fault);
3927
+ }
3928
+ throw error;
3929
+ }
3930
+ }
3931
+ async send(message) {
3932
+ if (!("method" in message)) {
3933
+ await this.#transport.send(message);
3934
+ return;
3935
+ }
3936
+ if (message.method === "server/discover" && message.id !== void 0) {
3937
+ this.#discover(message.id);
3938
+ return;
3939
+ }
3940
+ const id = message.id;
3941
+ let correlation;
3942
+ if (id !== void 0) {
3943
+ correlation = { method: message.method };
3944
+ this.#correlations.set(id, correlation);
3945
+ AbortSignal.timeout(this.#timeout).addEventListener("abort", () => {
3946
+ if (this.#correlations.get(id) !== correlation) return;
3947
+ this.#reject(id, new MCPError(`Legacy MCP request timed out after ${this.#timeout}ms`, JSONRPC_INTERNAL_ERROR));
3948
+ }, { once: true });
3949
+ }
3950
+ try {
3951
+ await this.#transport.send(modernInvocationToLegacy(message));
3952
+ } catch (error) {
3953
+ if (id !== void 0 && this.#correlations.get(id) === correlation) this.#correlations.delete(id);
3954
+ throw error;
3955
+ }
3956
+ }
3957
+ /**
3958
+ * Closes the wrapped transport and clears retained adapter state.
3959
+ *
3960
+ * @remarks
3961
+ * The cleared handshake state — the server identity, the supported reading, and the retained
3962
+ * `instructions` value — is unobservable between `close()` and the next accepted handshake.
3963
+ * Discovery answers the pre-handshake refusal in that window, and the accepted handshake
3964
+ * reassigns the state unconditionally.
3965
+ *
3966
+ * @returns Resolves after the wrapped transport closes
3967
+ */
3968
+ async close() {
3969
+ this.#instructions = void 0;
3970
+ this.#server = void 0;
3971
+ this.#supported = void 0;
3972
+ this.#correlations.clear();
3973
+ await this.#transport.close();
3974
+ }
3975
+ async #initialize() {
3976
+ const handshake = Promise.withResolvers();
3977
+ this.#handshake = handshake;
3978
+ try {
3979
+ await this.#write({
3980
+ jsonrpc: "2.0",
3981
+ id: 0,
3982
+ method: "initialize",
3983
+ params: {
3984
+ protocolVersion: this.#pin ?? "2025-11-25",
3985
+ capabilities: this.#capabilities,
3986
+ clientInfo: this.#client
3987
+ }
3988
+ });
3989
+ AbortSignal.timeout(this.#timeout).addEventListener("abort", () => handshake.reject(new MCPError(`Legacy MCP handshake timed out after ${this.#timeout}ms`, MCP_UNSUPPORTED_VERSION)), { once: true });
3990
+ const response = await handshake.promise;
3991
+ this.#accept(response);
3992
+ await this.#write({
3993
+ jsonrpc: "2.0",
3994
+ method: "notifications/initialized"
3995
+ });
3996
+ } finally {
3997
+ if (this.#handshake === handshake) this.#handshake = void 0;
3998
+ }
3999
+ }
4000
+ #accept(response) {
4001
+ if (response.error !== void 0) throw new MCPError(response.error.message, response.error.code, response.error.data);
4002
+ const owned = (0, _orkestrel_contract.attempt)(() => (0, _orkestrel_contract.cloneJSONRecord)(response.result));
4003
+ if (!owned.success || !isMCPLegacyResult(owned.value)) throw new MCPError("Legacy MCP handshake returned a malformed result", JSONRPC_INVALID_PARAMS);
4004
+ const result = owned.value;
4005
+ const protocol = result["protocolVersion"];
4006
+ const capabilities = result["capabilities"];
4007
+ const identity = result["serverInfo"];
4008
+ const instructions = result["instructions"];
4009
+ if (protocol === void 0) throw new MCPError("Legacy MCP handshake returned no protocol version", JSONRPC_INVALID_PARAMS, result);
4010
+ if (!(0, _orkestrel_contract.isString)(protocol)) throw new MCPError("Legacy MCP handshake returned a malformed protocol version", JSONRPC_INVALID_PARAMS, result);
4011
+ if (!isMCPLegacyVersion(protocol)) throw new MCPError(`Legacy MCP peer negotiated unsupported protocol version '${protocol}'`, MCP_UNSUPPORTED_VERSION, {
4012
+ supported: SUPPORTED_LEGACY_PROTOCOL_VERSIONS,
4013
+ negotiated: protocol
4014
+ });
4015
+ if (!isMCPServerCapabilities(capabilities) || !isMCPIdentity(identity) || instructions !== void 0 && !(0, _orkestrel_contract.isString)(instructions)) throw new MCPError("Legacy MCP handshake returned a malformed result", JSONRPC_INVALID_PARAMS, result);
4016
+ if (this.#pin !== void 0 && protocol !== this.#pin) throw new MCPError("Legacy MCP peer negotiated a different protocol version than the adapter requested", MCP_UNSUPPORTED_VERSION, {
4017
+ requested: this.#pin,
4018
+ negotiated: protocol
4019
+ });
4020
+ this.#instructions = instructions;
4021
+ this.#server = identity;
4022
+ this.#supported = capabilities;
4023
+ }
4024
+ async #write(message) {
4025
+ const deadline = AbortSignal.timeout(this.#timeout);
4026
+ await Promise.race([this.#transport.send(message), new Promise((_resolve, reject) => deadline.addEventListener("abort", () => reject(new MCPError(`Legacy MCP handshake write timed out after ${this.#timeout}ms`, MCP_UNSUPPORTED_VERSION)), { once: true }))]);
4027
+ }
4028
+ #discover(id) {
4029
+ const identity = this.#server;
4030
+ const capabilities = this.#supported;
4031
+ if (identity === void 0 || capabilities === void 0) {
4032
+ this.#reject(id, new MCPError("Legacy MCP transport has not completed its handshake", JSONRPC_INTERNAL_ERROR));
4033
+ return;
4034
+ }
4035
+ this.#emitter.emit("message", {
4036
+ jsonrpc: "2.0",
4037
+ id,
4038
+ result: buildModernResult({
4039
+ supportedVersions: [MCP_MODERN_VERSION],
4040
+ capabilities,
4041
+ ...this.#instructions === void 0 ? {} : { instructions: this.#instructions }
4042
+ }, identity, 0)
4043
+ });
4044
+ }
4045
+ #receive(message) {
4046
+ const owned = parseJSONRPCMessage(message);
4047
+ if (owned === void 0) {
4048
+ this.#emitter.emit("error", new MCPError("Legacy MCP peer returned a malformed message", JSONRPC_INVALID_PARAMS));
4049
+ return;
4050
+ }
4051
+ const handshake = this.#handshake;
4052
+ if (handshake !== void 0 && isJSONRPCResponse(owned) && owned.id === 0) {
4053
+ handshake.resolve(owned);
4054
+ return;
4055
+ }
4056
+ if (!isJSONRPCResponse(owned) || owned.id === void 0) {
4057
+ this.#emitter.emit("message", owned);
4058
+ return;
4059
+ }
4060
+ const correlation = this.#correlations.get(owned.id);
4061
+ if (correlation === void 0) {
4062
+ this.#emitter.emit("message", owned);
4063
+ return;
4064
+ }
4065
+ this.#correlations.delete(owned.id);
4066
+ const method = correlation.method;
4067
+ if (owned.error !== void 0) {
4068
+ this.#emitter.emit("message", owned);
4069
+ return;
4070
+ }
4071
+ const identity = this.#server;
4072
+ if (identity === void 0 || !isMCPLegacyResult(owned.result)) {
4073
+ this.#reject(owned.id, new MCPError("Legacy MCP peer returned a malformed result", JSONRPC_INTERNAL_ERROR));
4074
+ return;
4075
+ }
4076
+ this.#emitter.emit("message", {
4077
+ jsonrpc: "2.0",
4078
+ id: owned.id,
4079
+ result: legacyResultToModern(owned.result, method, identity)
4080
+ });
4081
+ }
4082
+ #reject(id, error) {
4083
+ this.#correlations.delete(id);
4084
+ this.#emitter.emit("error", error);
4085
+ this.#emitter.emit("message", buildJSONRPCError(id, error.code, error.message));
3640
4086
  }
3641
4087
  };
3642
4088
  //#endregion
@@ -3657,7 +4103,7 @@ var MCPLegacy = class {
3657
4103
  * - **One modern seam.** `server/discover`, `tools/list`, `tools/call`, and
3658
4104
  * `subscriptions/listen` are always registered; `resources/*`, `prompts/*`, and
3659
4105
  * `completion/complete` register independently when their respective host ports are
3660
- * configured — plus `tasks/get`, `tasks/update`, and `tasks/cancel` when the draft Tasks
4106
+ * configured — plus `tasks/get`, `tasks/update`, and `tasks/cancel` when the stable Tasks
3661
4107
  * extension is configured — and every method is resolved from the registry on
3662
4108
  * every dispatch: the same path a later method or a consumer's own takes, with an
3663
4109
  * unregistered method still answering `-32601`.
@@ -3672,8 +4118,8 @@ var MCPLegacy = class {
3672
4118
  * const tools = createToolManager()
3673
4119
  * tools.add(createTool({ name: 'add', execute: (a) => Number(a.x) + Number(a.y) }))
3674
4120
  * const server = new MCPServer({ identity: { name: 'demo', version: '1.0.0' }, tools })
3675
- * await server.handle('{"jsonrpc":"2.0","method":"ping","id":1,"params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}')
3676
- * // '{"jsonrpc":"2.0","id":1,"result":{"resultType":"complete","_meta":{"io.modelcontextprotocol/serverInfo":{"name":"demo","version":"1.0.0"}}}}'
4121
+ * await server.handle('{"jsonrpc":"2.0","method":"server/discover","id":1,"params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}')
4122
+ * // The result advertises only `2026-07-28`; wrap with `createMCPLegacy` to serve initialize or ping.
3677
4123
  * ```
3678
4124
  */
3679
4125
  var MCPServer = class {
@@ -3757,7 +4203,6 @@ var MCPServer = class {
3757
4203
  return Symbol.asyncIterator in answer ? new MCPTextStreamController(answer) : JSON.stringify(answer);
3758
4204
  }
3759
4205
  #register() {
3760
- this.#methods.add("ping", async (request, _options) => buildJSONRPCResult(request.id, buildModernResult({}, this.#options.identity)));
3761
4206
  this.#methods.add("server/discover", async (request, _options) => this.#discover(request));
3762
4207
  this.#methods.add("tools/list", async (request, _options) => this.#list(request));
3763
4208
  this.#methods.add("tools/call", async (request, options) => this.#call(request, options));
@@ -3794,8 +4239,8 @@ var MCPServer = class {
3794
4239
  depth: this.#limits.depth
3795
4240
  });
3796
4241
  if (context === void 0) return buildJSONRPCError(id, JSONRPC_INVALID_PARAMS, "Invalid params: malformed modern request metadata");
3797
- if (inferEra(context.version) === void 0) return buildJSONRPCError(id, MCP_UNSUPPORTED_VERSION, `Unsupported protocol version: ${context.version}`, {
3798
- supported: SUPPORTED_PROTOCOL_VERSIONS,
4242
+ if (!isMCPModernVersion(context.version)) return buildJSONRPCError(id, MCP_UNSUPPORTED_VERSION, `Unsupported protocol version: ${context.version}`, {
4243
+ supported: SUPPORTED_MODERN_PROTOCOL_VERSIONS,
3799
4244
  requested: context.version
3800
4245
  });
3801
4246
  const handler = this.#methods.method(request.method);
@@ -4204,8 +4649,22 @@ var MCPServer = class {
4204
4649
  if (options.signal.aborted) slot.abort();
4205
4650
  else options.signal.addEventListener("abort", () => slot.abort(), { once: true });
4206
4651
  try {
4652
+ const task = this.#options.task;
4207
4653
  const configured = this.#options.subscription;
4208
- const notifications = buildSubscriptionFilter(requested, configured?.notifications ?? {});
4654
+ const tasks = task !== void 0 && configured !== void 0;
4655
+ let notifications = buildSubscriptionFilter(requested, configured?.notifications ?? {}, tasks);
4656
+ const requestedTaskIds = notifications.taskIds;
4657
+ if (requestedTaskIds !== void 0) {
4658
+ const resolved = [];
4659
+ if (task !== void 0) {
4660
+ for (const taskId of requestedTaskIds) if (await task.tasks.task(taskId, options) !== void 0) resolved.push(taskId);
4661
+ }
4662
+ const { taskIds: _dropped, ...rest } = notifications;
4663
+ notifications = resolved.length > 0 ? {
4664
+ ...rest,
4665
+ taskIds: resolved
4666
+ } : rest;
4667
+ }
4209
4668
  yield buildSubscriptionAcknowledgement(notifications, id);
4210
4669
  if (configured !== void 0) {
4211
4670
  const iterator = (await configured.listen(notifications, options))[Symbol.asyncIterator]();
@@ -4317,7 +4776,7 @@ var MCPServer = class {
4317
4776
  //#endregion
4318
4777
  //#region src/core/MCPTaskClient.ts
4319
4778
  /**
4320
- * The CLIENT half of the draft Tasks extension — the `tasks/*` methods over one
4779
+ * The CLIENT half of the stable Tasks extension — the `tasks/*` methods over one
4321
4780
  * correlated-request door, exposed as an {@link import('./types.js').MCPClientInterface}'s
4322
4781
  * `tasks`.
4323
4782
  *
@@ -4361,7 +4820,7 @@ var MCPTaskClient = class {
4361
4820
  }
4362
4821
  async task(id) {
4363
4822
  const result = await this.#request("tasks/get", { taskId: id }, this.#timeout);
4364
- if (!isMCPTaskDetail(result)) throw new MCPError("MCP server returned an invalid task", JSONRPC_INVALID_PARAMS);
4823
+ if (!isMCPTaskDetailResult(result)) throw new MCPError("MCP server returned an invalid task", JSONRPC_INVALID_PARAMS);
4365
4824
  return result;
4366
4825
  }
4367
4826
  async update(id, responses) {
@@ -4378,14 +4837,14 @@ var MCPTaskClient = class {
4378
4837
  //#region src/core/MCPClient.ts
4379
4838
  /**
4380
4839
  * A transport-agnostic Model Context Protocol CLIENT — connects to a REMOTE MCP server
4381
- * over an injected {@link MCPClientTransportInterface}, negotiates the modern or legacy
4382
- * wire era, and exposes the server's tools as local {@link ToolInterface}s an agent can run.
4840
+ * over an injected {@link MCPClientTransportInterface}, negotiates the modern revision, and
4841
+ * exposes the server's tools as local {@link ToolInterface}s an agent can run.
4383
4842
  *
4384
4843
  * @remarks
4385
4844
  * - **The mirror of `MCPServer`.** The server DISPATCHES requests over a tool registry;
4386
- * this client ISSUES them over a transport. `connect` probes `server/discover` unless
4387
- * pinned legacy, falls back to `initialize` only for a legacy peer, and exposes the
4388
- * negotiated `version`; `tools()` lists the remote tools and wraps each as a
4845
+ * this client ISSUES them over a transport. `connect` probes `server/discover` and exposes
4846
+ * the negotiated `version`; a legacy peer requires an explicit transport adapter.
4847
+ * `tools()` lists the remote tools and wraps each as a
4389
4848
  * local {@link ToolInterface} whose `execute` calls back through `call`; `call` runs a
4390
4849
  * remote `tools/call` and reports the arm the peer answered with — a value, a durable
4391
4850
  * task, or a request for more input (a remote `isError: true` throws locally, so an
@@ -4455,17 +4914,18 @@ var MCPClient = class {
4455
4914
  #generation = 0;
4456
4915
  #supersession = Promise.withResolvers();
4457
4916
  #version = void 0;
4458
- #era = void 0;
4459
4917
  #offer;
4460
4918
  constructor(options) {
4461
4919
  const requested = options.version;
4462
- if (requested !== void 0 && !isMCPVersion(requested)) throw new MCPError("Unsupported protocol version", MCP_UNSUPPORTED_VERSION, {
4463
- supported: SUPPORTED_PROTOCOL_VERSIONS,
4920
+ const on = options.on;
4921
+ const error = options.error;
4922
+ if (requested !== void 0 && !isMCPModernVersion(requested)) throw new MCPError("Unsupported protocol version", MCP_UNSUPPORTED_VERSION, {
4923
+ supported: SUPPORTED_MODERN_PROTOCOL_VERSIONS,
4464
4924
  requested
4465
4925
  });
4466
4926
  this.#emitter = new _orkestrel_emitter.Emitter({
4467
- ...options.on !== void 0 ? { on: options.on } : {},
4468
- ...options.error !== void 0 ? { error: options.error } : {}
4927
+ ...on === void 0 ? {} : { on },
4928
+ ...error === void 0 ? {} : { error }
4469
4929
  });
4470
4930
  this.#transport = options.transport;
4471
4931
  this.#identity = options.identity ?? {
@@ -4481,6 +4941,7 @@ var MCPClient = class {
4481
4941
  timeout: this.#timeout
4482
4942
  });
4483
4943
  this.#transport.emitter.on("message", (message) => this.#receive(message));
4944
+ this.#transport.emitter.on("close", () => this.#loseTransport());
4484
4945
  }
4485
4946
  get emitter() {
4486
4947
  return this.#emitter;
@@ -4529,7 +4990,9 @@ var MCPClient = class {
4529
4990
  }
4530
4991
  }
4531
4992
  async discover() {
4532
- const received = await this.#request("server/discover", void 0, this.#timeout, this.#version ?? this.#offer);
4993
+ const negotiated = this.#version;
4994
+ const version = isMCPModernVersion(negotiated) ? negotiated : this.#offer;
4995
+ const received = await this.#request("server/discover", void 0, this.#timeout, version);
4533
4996
  const owned = (0, _orkestrel_contract.attempt)(() => (0, _orkestrel_contract.cloneJSONRecord)(received));
4534
4997
  if (!owned.success) throw new MCPError("MCP server returned a malformed discovery result", JSONRPC_INVALID_PARAMS);
4535
4998
  const result = owned.value;
@@ -4542,7 +5005,7 @@ var MCPClient = class {
4542
5005
  const resultType = result["resultType"];
4543
5006
  if (!(0, _orkestrel_contract.isArray)(advertised) || !advertised.every(_orkestrel_contract.isString) || !isMCPServerCapabilities(capabilities) || !(0, _orkestrel_contract.isInteger)(ttl) || ttl < 0 || scope !== "public" && scope !== "private" || resultType !== "complete" || instructions !== void 0 && !(0, _orkestrel_contract.isString)(instructions) || metadata !== void 0 && !isMCPResultMetaObject(metadata)) throw new MCPError("MCP server returned a malformed discovery result", JSONRPC_INVALID_PARAMS, result);
4544
5007
  const supportedVersions = [];
4545
- for (const version of advertised) if (isMCPVersion(version)) supportedVersions.push(version);
5008
+ for (const candidate of advertised) if (isMCPModernVersion(candidate)) supportedVersions.push(candidate);
4546
5009
  const retained = Object.freeze(supportedVersions);
4547
5010
  return Object.freeze({
4548
5011
  supportedVersions: retained,
@@ -4571,19 +5034,81 @@ var MCPClient = class {
4571
5034
  }
4572
5035
  return tools;
4573
5036
  }
5037
+ listen(notifications, options) {
5038
+ const signal = options.signal;
5039
+ const capacity = options.capacity ?? 64;
5040
+ return this.#openSubscription(notifications, signal, capacity);
5041
+ }
4574
5042
  async call(name, args, options) {
5043
+ const input = options?.input;
4575
5044
  return buildCallOutcome(name, await this.#request("tools/call", {
4576
5045
  name,
4577
- arguments: args
5046
+ arguments: args,
5047
+ ...input === void 0 ? {} : {
5048
+ requestState: input.state,
5049
+ inputResponses: input.responses
5050
+ }
4578
5051
  }, this.#timeout, void 0, options));
4579
5052
  }
5053
+ async *#openSubscription(notifications, signal, capacity) {
5054
+ signal.throwIfAborted();
5055
+ if (!(0, _orkestrel_contract.isInteger)(capacity) || capacity < 1) throw new MCPError("MCP subscription capacity must be a positive integer", JSONRPC_INVALID_PARAMS);
5056
+ this.#nextId += 1;
5057
+ const id = this.#nextId;
5058
+ const method = "subscriptions/listen";
5059
+ const modern = this.#version;
5060
+ const request = {
5061
+ jsonrpc: "2.0",
5062
+ id,
5063
+ method,
5064
+ params: {
5065
+ notifications: notifications ?? {},
5066
+ ...modern === void 0 ? {} : { _meta: {
5067
+ [MCP_META_VERSION]: modern,
5068
+ [MCP_META_CAPABILITIES]: this.#capabilities,
5069
+ [MCP_META_CLIENT]: this.#identity
5070
+ } }
5071
+ }
5072
+ };
5073
+ const subscription = {
5074
+ queue: [],
5075
+ capacity
5076
+ };
5077
+ const abort = this.#abortSubscription.bind(this, id, signal);
5078
+ signal.addEventListener("abort", abort, { once: true });
5079
+ this.#pending.set(id, {
5080
+ method,
5081
+ signal,
5082
+ abort,
5083
+ subscription
5084
+ });
5085
+ this.#transport.send(request).catch((error) => this.#settle(id, error, true));
5086
+ try {
5087
+ for (;;) {
5088
+ if (subscription.failure !== void 0) throw subscription.failure.reason;
5089
+ const queued = subscription.queue.shift();
5090
+ if (queued !== void 0) {
5091
+ yield queued;
5092
+ continue;
5093
+ }
5094
+ if (subscription.terminal !== void 0) return subscription.terminal;
5095
+ const waiter = Promise.withResolvers();
5096
+ subscription.waiter = waiter;
5097
+ const frame = await waiter.promise;
5098
+ if ("method" in frame) yield frame;
5099
+ else return frame;
5100
+ }
5101
+ } finally {
5102
+ this.#cancelSubscription(id, /* @__PURE__ */ new Error("MCP subscription closed by its consumer"));
5103
+ }
5104
+ }
4580
5105
  #request(method, params, deadline, version, options) {
4581
5106
  this.#nextId += 1;
4582
5107
  const id = this.#nextId;
4583
5108
  const timeout = deadline;
4584
5109
  const caller = options?.signal;
4585
5110
  const report = options?.progress;
4586
- const modern = version ?? (this.#era === "modern" ? this.#version : void 0);
5111
+ const modern = version ?? this.#version;
4587
5112
  const metadata = {
4588
5113
  ...modern === void 0 ? {} : {
4589
5114
  [MCP_META_VERSION]: modern,
@@ -4647,7 +5172,6 @@ var MCPClient = class {
4647
5172
  });
4648
5173
  if (correlated.success && correlated.value !== void 0) {
4649
5174
  if (this.#pending.get(correlated.value)?.method === "server/discover") {
4650
- this.#era = "modern";
4651
5175
  this.#settle(correlated.value, new MCPError("MCP server returned a malformed discovery result", JSONRPC_INVALID_PARAMS), true);
4652
5176
  return;
4653
5177
  }
@@ -4663,9 +5187,11 @@ var MCPClient = class {
4663
5187
  const correlation = owned.id;
4664
5188
  const pending = this.#pending.get(correlation);
4665
5189
  if (pending !== void 0) {
4666
- if (pending.method === "server/discover" && (owned.error === void 0 || owned.error.code !== -32601 && owned.error.code !== -32600)) this.#era = "modern";
4667
5190
  if (owned.error !== void 0) this.#settle(correlation, new MCPError(owned.error.message, owned.error.code, owned.error.data), true);
4668
- else {
5191
+ else if (pending.subscription !== void 0) {
5192
+ if (!isMCPSubscriptionResult(owned.result) || owned.result["_meta"]["io.modelcontextprotocol/subscriptionId"] !== correlation) this.#settle(correlation, new MCPError("MCP server returned a malformed subscription result", JSONRPC_INVALID_PARAMS, owned.result), true);
5193
+ else this.#settle(correlation, owned.result, false);
5194
+ } else {
4669
5195
  const resultType = (0, _orkestrel_contract.isRecord)(owned.result) ? owned.result["resultType"] : void 0;
4670
5196
  const metadata = (0, _orkestrel_contract.isRecord)(owned.result) ? owned.result["_meta"] : void 0;
4671
5197
  if (metadata !== void 0 && !isMCPResultMetaObject(metadata)) {
@@ -4680,8 +5206,30 @@ var MCPClient = class {
4680
5206
  return;
4681
5207
  }
4682
5208
  if ("method" in owned && this.#reportProgress(owned)) return;
5209
+ if ("method" in owned && this.#routeSubscription(owned)) return;
4683
5210
  this.#emitter.emit("notification", owned);
4684
5211
  }
5212
+ #routeSubscription(message) {
5213
+ if (!isJSONRPCNotification(message)) return false;
5214
+ const metadata = message.params?.["_meta"];
5215
+ if (!(0, _orkestrel_contract.isRecord)(metadata)) return false;
5216
+ const id = metadata[MCP_META_SUBSCRIPTION];
5217
+ if (!isJSONRPCId(id)) return false;
5218
+ const subscription = this.#pending.get(id)?.subscription;
5219
+ if (subscription === void 0) return true;
5220
+ const waiter = subscription.waiter;
5221
+ if (waiter !== void 0) {
5222
+ delete subscription.waiter;
5223
+ waiter.resolve(message);
5224
+ return true;
5225
+ }
5226
+ if (subscription.queue.length >= subscription.capacity) {
5227
+ this.#cancelSubscription(id, new MCPError("MCP subscription frame queue overflow", JSONRPC_INTERNAL_ERROR));
5228
+ return true;
5229
+ }
5230
+ subscription.queue.push(message);
5231
+ return true;
5232
+ }
4685
5233
  #reportProgress(message) {
4686
5234
  if (message.method !== "notifications/progress") return false;
4687
5235
  const params = message.params;
@@ -4715,10 +5263,6 @@ var MCPClient = class {
4715
5263
  this.#owner = generation;
4716
5264
  try {
4717
5265
  if (generation !== this.#generation) throw new Error("MCP client disconnected");
4718
- if (this.#era === "legacy" || this.#pin !== void 0 && inferEra(this.#pin) === "legacy") {
4719
- await this.#initialize(generation, this.#pin ?? "2025-11-25");
4720
- return;
4721
- }
4722
5266
  let discovery;
4723
5267
  try {
4724
5268
  try {
@@ -4735,13 +5279,12 @@ var MCPClient = class {
4735
5279
  }
4736
5280
  } catch (error) {
4737
5281
  if (generation !== this.#generation) throw error;
4738
- if (!(this.#pin !== "2026-07-28" && this.#era === void 0 && (!isMCPError(error) || error.code !== -32022))) throw error;
4739
- await this.#initialize(generation, MCP_PROTOCOL_VERSION);
4740
- return;
5282
+ if (!isMCPError(error) || error.code !== -32601) throw error;
5283
+ throw new MCPError("MCP server does not support modern negotiation; wrap the transport with createMCPLegacyClientTransport to connect to a legacy peer", error.code, error.context);
4741
5284
  }
4742
5285
  let version;
4743
5286
  if (this.#pin === void 0) version = inferVersion(discovery.supportedVersions);
4744
- else if (discovery.supportedVersions.includes(this.#pin)) version = this.#pin;
5287
+ else if (isMCPModernVersion(this.#pin) && discovery.supportedVersions.includes(this.#pin)) version = this.#pin;
4745
5288
  else throw new MCPError("MCP server does not support the pinned protocol version", MCP_UNSUPPORTED_VERSION, {
4746
5289
  supported: discovery.supportedVersions,
4747
5290
  requested: this.#pin
@@ -4749,7 +5292,6 @@ var MCPClient = class {
4749
5292
  if (version === void 0) throw new MCPError("MCP server supports no compatible protocol version", MCP_UNSUPPORTED_VERSION, { supported: discovery.supportedVersions });
4750
5293
  if (generation !== this.#generation) throw new Error("MCP client disconnected");
4751
5294
  this.#version = version;
4752
- this.#era = "modern";
4753
5295
  this.#connected = true;
4754
5296
  this.#emitter.emit("connect");
4755
5297
  } catch (error) {
@@ -4811,31 +5353,6 @@ var MCPClient = class {
4811
5353
  this.#closing = void 0;
4812
5354
  if (closed) this.#owner = void 0;
4813
5355
  }
4814
- async #initialize(generation, version) {
4815
- const result = await this.#request("initialize", {
4816
- protocolVersion: version,
4817
- capabilities: {},
4818
- clientInfo: this.#identity
4819
- }, this.#timeout);
4820
- const protocol = (0, _orkestrel_contract.isRecord)(result) ? result["protocolVersion"] : void 0;
4821
- if (protocol === void 0) throw new Error("MCP server returned no protocol version");
4822
- if (!(0, _orkestrel_contract.isString)(protocol)) throw new Error("MCP server returned a malformed protocol version");
4823
- if (!isMCPVersion(protocol) || inferEra(protocol) !== "legacy") throw new Error(`MCP server negotiated unsupported protocol version '${protocol}'`);
4824
- if (this.#pin !== void 0 && protocol !== this.#pin) throw new MCPError("MCP server negotiated a different protocol version than the client pinned", MCP_UNSUPPORTED_VERSION, {
4825
- requested: this.#pin,
4826
- negotiated: protocol
4827
- });
4828
- if (generation !== this.#generation) throw new Error("MCP client disconnected");
4829
- await Promise.race([this.#transport.send({
4830
- jsonrpc: "2.0",
4831
- method: "notifications/initialized"
4832
- }), this.#supersession.promise]);
4833
- if (generation !== this.#generation) throw new Error("MCP client disconnected");
4834
- this.#version = protocol;
4835
- this.#era = "legacy";
4836
- this.#connected = true;
4837
- this.#emitter.emit("connect");
4838
- }
4839
5356
  #timeoutRequest(id, method, timeout) {
4840
5357
  this.#settle(id, /* @__PURE__ */ new Error(`MCP request '${method}' timed out after ${timeout}ms`), true);
4841
5358
  }
@@ -4845,14 +5362,48 @@ var MCPClient = class {
4845
5362
  if (this.#transport.duplex) this.#transport.send(buildCancelledNotification(id, (0, _orkestrel_contract.isString)(reason) ? reason : void 0)).catch((error) => this.#emitter.emit("error", error));
4846
5363
  this.#settle(id, new Error(`MCP request '${method}' was aborted`, { cause: reason }), true);
4847
5364
  }
5365
+ #abortSubscription(id, signal) {
5366
+ this.#cancelSubscription(id, signal.reason);
5367
+ }
5368
+ #cancelSubscription(id, reason) {
5369
+ if (this.#pending.get(id)?.subscription === void 0) return;
5370
+ if (this.#transport.duplex) this.#transport.send(buildCancelledNotification(id, (0, _orkestrel_contract.isString)(reason) ? reason : void 0)).catch((error) => this.#emitter.emit("error", error));
5371
+ this.#settle(id, reason, true);
5372
+ }
5373
+ #loseTransport() {
5374
+ const announced = this.#connected;
5375
+ this.#generation += 1;
5376
+ this.#connected = false;
5377
+ this.#version = void 0;
5378
+ this.#owner = void 0;
5379
+ const supersession = this.#supersession;
5380
+ this.#supersession = Promise.withResolvers();
5381
+ supersession.resolve();
5382
+ for (const id of this.#pending.keys()) this.#settle(id, /* @__PURE__ */ new Error("MCP transport closed"), true);
5383
+ if (announced) this.#emitter.emit("disconnect");
5384
+ }
4848
5385
  #settle(id, value, failed) {
4849
5386
  const pending = this.#pending.get(id);
4850
5387
  if (pending === void 0) return;
4851
5388
  this.#pending.delete(id);
4852
5389
  if (pending.deadline !== void 0 && pending.timeout !== void 0) pending.deadline.removeEventListener("abort", pending.timeout);
4853
5390
  if (pending.signal !== void 0 && pending.abort !== void 0) pending.signal.removeEventListener("abort", pending.abort);
4854
- if (failed) pending.reject(value);
4855
- else pending.resolve(value);
5391
+ const subscription = pending.subscription;
5392
+ if (subscription !== void 0) {
5393
+ const waiter = subscription.waiter;
5394
+ delete subscription.waiter;
5395
+ if (failed) {
5396
+ subscription.queue.length = 0;
5397
+ if (waiter === void 0) subscription.failure = { reason: value };
5398
+ else waiter.reject(value);
5399
+ } else if (isMCPSubscriptionResult(value)) {
5400
+ if (waiter === void 0) subscription.terminal = value;
5401
+ else waiter.resolve(value);
5402
+ }
5403
+ return;
5404
+ }
5405
+ if (failed) pending.reject?.(value);
5406
+ else pending.resolve?.(value);
4856
5407
  }
4857
5408
  };
4858
5409
  //#endregion
@@ -4915,13 +5466,14 @@ function createMCPLegacy(server) {
4915
5466
  /**
4916
5467
  * Creates a transport-agnostic Model Context Protocol CLIENT — connects to a REMOTE
4917
5468
  * MCP server over an injected {@link import('./types.js').MCPClientTransportInterface},
4918
- * runs the `initialize` handshake, and exposes the server's tools as local
5469
+ * negotiates the modern revision through `server/discover`, and exposes the server's tools as local
4919
5470
  * {@link import('@orkestrel/tool').ToolInterface}s an agent can run.
4920
5471
  *
4921
5472
  * @remarks
4922
5473
  * The egress mirror of {@link createMCPServer}: where the server exposes a local tool
4923
- * registry over MCP, the client USES a remote server's tools. `connect()` handshakes,
4924
- * validates and exposes the negotiated protocol, `tools()` lists + wraps the remote
5474
+ * registry over MCP, the client USES a remote server's tools. `connect()` discovers,
5475
+ * validates, and exposes the negotiated modern protocol; a legacy peer requires
5476
+ * {@link createMCPLegacyClientTransport}. `tools()` lists + wraps the remote
4925
5477
  * tools (each `execute` calls back over the wire),
4926
5478
  * and `call(name, args)` runs a remote `tools/call` (a remote tool failure throws
4927
5479
  * locally, so an agent's {@link import('@orkestrel/tool').ToolManagerInterface}
@@ -4951,6 +5503,24 @@ function createMCPClient(options) {
4951
5503
  return new MCPClient(options);
4952
5504
  }
4953
5505
  /**
5506
+ * Decorates one client transport with explicit legacy handshake and era translation.
5507
+ *
5508
+ * @param transport - The transport connected to a legacy MCP peer
5509
+ * @param options - Optional legacy handshake identity, capabilities, revision, and deadline
5510
+ * @returns A modern-facing client transport over the legacy peer
5511
+ *
5512
+ * @example
5513
+ * ```ts
5514
+ * const transport = createMCPLegacyClientTransport(legacyTransport)
5515
+ * const client = createMCPClient({ transport })
5516
+ * await client.connect()
5517
+ * client.version // '2026-07-28'
5518
+ * ```
5519
+ */
5520
+ function createMCPLegacyClientTransport(transport, options) {
5521
+ return new MCPLegacyClientTransport(transport, options);
5522
+ }
5523
+ /**
4954
5524
  * Adapts an {@link MCPTransportInterface} (the environment-agnostic duplex message
4955
5525
  * channel) into a {@link MCPClientTransportInterface} — the additive bridge that lets
4956
5526
  * `createMCPClient` run over the new port without any change to `MCPClient`'s
@@ -5004,6 +5574,7 @@ exports.DEFAULT_MCP_CLIENT_NAME = DEFAULT_MCP_CLIENT_NAME;
5004
5574
  exports.DEFAULT_MCP_CLIENT_VERSION = DEFAULT_MCP_CLIENT_VERSION;
5005
5575
  exports.DEFAULT_MCP_LIMITS = DEFAULT_MCP_LIMITS;
5006
5576
  exports.DEFAULT_MCP_REQUEST_TIMEOUT = DEFAULT_MCP_REQUEST_TIMEOUT;
5577
+ exports.DEFAULT_MCP_SUBSCRIPTION_CAPACITY = DEFAULT_MCP_SUBSCRIPTION_CAPACITY;
5007
5578
  exports.EMPTY_MCP_ARGUMENTS = EMPTY_MCP_ARGUMENTS;
5008
5579
  exports.JSONRPC_INTERNAL_ERROR = JSONRPC_INTERNAL_ERROR;
5009
5580
  exports.JSONRPC_INVALID_PARAMS = JSONRPC_INVALID_PARAMS;
@@ -5014,6 +5585,7 @@ exports.JSONRPC_SERVER_ERROR = JSONRPC_SERVER_ERROR;
5014
5585
  exports.MCPClient = MCPClient;
5015
5586
  exports.MCPError = MCPError;
5016
5587
  exports.MCPLegacy = MCPLegacy;
5588
+ exports.MCPLegacyClientTransport = MCPLegacyClientTransport;
5017
5589
  exports.MCPMethodManager = MCPMethodManager;
5018
5590
  exports.MCPProgressReporter = MCPProgressReporter;
5019
5591
  exports.MCPServer = MCPServer;
@@ -5021,8 +5593,9 @@ exports.MCPStreamController = MCPStreamController;
5021
5593
  exports.MCPTaskClient = MCPTaskClient;
5022
5594
  exports.MCPTextStreamController = MCPTextStreamController;
5023
5595
  exports.MCP_EXTENSION_TASKS = MCP_EXTENSION_TASKS;
5596
+ exports.MCP_FALLBACK_VERSION = MCP_FALLBACK_VERSION;
5597
+ exports.MCP_HANDSHAKE_VERSION = MCP_HANDSHAKE_VERSION;
5024
5598
  exports.MCP_HEADER_MISMATCH = MCP_HEADER_MISMATCH;
5025
- exports.MCP_LEGACY_VERSION = MCP_LEGACY_VERSION;
5026
5599
  exports.MCP_META_CAPABILITIES = MCP_META_CAPABILITIES;
5027
5600
  exports.MCP_META_CLIENT = MCP_META_CLIENT;
5028
5601
  exports.MCP_META_SERVER = MCP_META_SERVER;
@@ -5030,9 +5603,10 @@ exports.MCP_META_SUBSCRIPTION = MCP_META_SUBSCRIPTION;
5030
5603
  exports.MCP_META_VERSION = MCP_META_VERSION;
5031
5604
  exports.MCP_MISSING_CAPABILITY = MCP_MISSING_CAPABILITY;
5032
5605
  exports.MCP_MODERN_VERSION = MCP_MODERN_VERSION;
5033
- exports.MCP_PROTOCOL_VERSION = MCP_PROTOCOL_VERSION;
5034
5606
  exports.MCP_UNSUPPORTED_VERSION = MCP_UNSUPPORTED_VERSION;
5035
- exports.SUPPORTED_PROTOCOL_VERSIONS = SUPPORTED_PROTOCOL_VERSIONS;
5607
+ exports.SUPPORTED_LEGACY_PROTOCOL_VERSIONS = SUPPORTED_LEGACY_PROTOCOL_VERSIONS;
5608
+ exports.SUPPORTED_MCP_VERSIONS = SUPPORTED_MCP_VERSIONS;
5609
+ exports.SUPPORTED_MODERN_PROTOCOL_VERSIONS = SUPPORTED_MODERN_PROTOCOL_VERSIONS;
5036
5610
  exports.bindClient = bindClient;
5037
5611
  exports.bindServer = bindServer;
5038
5612
  exports.buildCallOutcome = buildCallOutcome;
@@ -5052,6 +5626,7 @@ exports.buildToolDescriptors = buildToolDescriptors;
5052
5626
  exports.createDuplexClientTransport = createDuplexClientTransport;
5053
5627
  exports.createMCPClient = createMCPClient;
5054
5628
  exports.createMCPLegacy = createMCPLegacy;
5629
+ exports.createMCPLegacyClientTransport = createMCPLegacyClientTransport;
5055
5630
  exports.createMCPServer = createMCPServer;
5056
5631
  exports.decodeBoundedMessage = decodeBoundedMessage;
5057
5632
  exports.digestJSON = digestJSON;
@@ -5097,9 +5672,12 @@ exports.isMCPInputRequest = isMCPInputRequest;
5097
5672
  exports.isMCPInputRequestMap = isMCPInputRequestMap;
5098
5673
  exports.isMCPInputResult = isMCPInputResult;
5099
5674
  exports.isMCPLegacyResult = isMCPLegacyResult;
5675
+ exports.isMCPLegacyVersion = isMCPLegacyVersion;
5100
5676
  exports.isMCPLoggingLevel = isMCPLoggingLevel;
5101
5677
  exports.isMCPMetaKey = isMCPMetaKey;
5102
5678
  exports.isMCPMetaObject = isMCPMetaObject;
5679
+ exports.isMCPModernVersion = isMCPModernVersion;
5680
+ exports.isMCPNotificationMetaObject = isMCPNotificationMetaObject;
5103
5681
  exports.isMCPPaginationParams = isMCPPaginationParams;
5104
5682
  exports.isMCPProgress = isMCPProgress;
5105
5683
  exports.isMCPPrompt = isMCPPrompt;
@@ -5117,7 +5695,10 @@ exports.isMCPResultMetaObject = isMCPResultMetaObject;
5117
5695
  exports.isMCPServerCapabilities = isMCPServerCapabilities;
5118
5696
  exports.isMCPStringArguments = isMCPStringArguments;
5119
5697
  exports.isMCPSubscriptionFilter = isMCPSubscriptionFilter;
5698
+ exports.isMCPSubscriptionResult = isMCPSubscriptionResult;
5120
5699
  exports.isMCPTaskDetail = isMCPTaskDetail;
5700
+ exports.isMCPTaskDetailResult = isMCPTaskDetailResult;
5701
+ exports.isMCPTaskNotification = isMCPTaskNotification;
5121
5702
  exports.isMCPTaskResult = isMCPTaskResult;
5122
5703
  exports.isMCPTaskStatus = isMCPTaskStatus;
5123
5704
  exports.isMCPTextResource = isMCPTextResource;
@@ -5127,8 +5708,12 @@ exports.isRFC3339Date = isRFC3339Date;
5127
5708
  exports.isRFC3339DateTime = isRFC3339DateTime;
5128
5709
  exports.isStandardBase64 = isStandardBase64;
5129
5710
  exports.isTaskSupported = isTaskSupported;
5711
+ exports.legacyInvocationToModern = legacyInvocationToModern;
5712
+ exports.legacyResultToModern = legacyResultToModern;
5130
5713
  exports.matchesResultType = matchesResultType;
5131
5714
  exports.matchesSubscriptionNotification = matchesSubscriptionNotification;
5715
+ exports.modernInvocationToLegacy = modernInvocationToLegacy;
5716
+ exports.modernResultToLegacy = modernResultToLegacy;
5132
5717
  exports.parseJSONRPCMessage = parseJSONRPCMessage;
5133
5718
  exports.parseMCPInputState = parseMCPInputState;
5134
5719
  exports.parseRequestContext = parseRequestContext;