@orkestrel/mcp 0.0.24 → 0.0.26

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