@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.
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/core/index.cjs +797 -212
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +496 -111
- package/dist/src/core/index.d.ts +496 -111
- package/dist/src/core/index.js +780 -210
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +60 -19
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +44 -7
- package/dist/src/server/index.d.ts +44 -7
- package/dist/src/server/index.js +61 -21
- package/dist/src/server/index.js.map +1 -1
- package/package.json +4 -4
package/dist/src/core/index.cjs
CHANGED
|
@@ -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
|
|
15
|
-
/** The legacy
|
|
16
|
-
var
|
|
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
|
|
20
|
+
* The modern MCP protocol revisions a bare server accepts and advertises.
|
|
21
21
|
*
|
|
22
22
|
* @remarks
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
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
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
43
|
+
* The reserved extension key identifying the stable Tasks extension.
|
|
45
44
|
*
|
|
46
45
|
* @remarks
|
|
47
|
-
* The ONE spelling of it in this package
|
|
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
|
|
66
|
-
*
|
|
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
|
|
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
|
|
578
|
-
*
|
|
579
|
-
*
|
|
580
|
-
*
|
|
581
|
-
*
|
|
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
|
-
|
|
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
|
|
1177
|
-
|
|
1178
|
-
|
|
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:
|
|
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:
|
|
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.
|
|
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
|
-
*
|
|
2304
|
-
*
|
|
2305
|
-
*
|
|
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.
|
|
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
|
|
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
|
|
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) &&
|
|
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,
|
|
2427
|
-
* `resourceSubscriptions` accepts only an array of string URIs
|
|
2428
|
-
*
|
|
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
|
-
|
|
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
|
|
3533
|
-
*
|
|
3534
|
-
*
|
|
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
|
|
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
|
-
|
|
3610
|
-
|
|
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 ${
|
|
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
|
|
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":"
|
|
3676
|
-
* //
|
|
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 (
|
|
3798
|
-
supported:
|
|
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
|
|
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
|
|
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 (!
|
|
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
|
|
4382
|
-
*
|
|
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`
|
|
4387
|
-
*
|
|
4388
|
-
*
|
|
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
|
-
|
|
4463
|
-
|
|
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
|
-
...
|
|
4468
|
-
...
|
|
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
|
|
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
|
|
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 ??
|
|
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 (!
|
|
4739
|
-
|
|
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
|
-
|
|
4855
|
-
|
|
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
|
-
*
|
|
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()`
|
|
4924
|
-
* validates and exposes the negotiated protocol
|
|
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.
|
|
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;
|