@orkestrel/mcp 0.0.28 → 0.0.29
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/README.md +11 -14
- package/dist/src/browser/index.d.ts +63 -65
- package/dist/src/browser/index.js +44 -44
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/core/index.cjs +233 -176
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +442 -366
- package/dist/src/core/index.d.ts +442 -366
- package/dist/src/core/index.js +233 -176
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +109 -109
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +209 -203
- package/dist/src/server/index.d.ts +209 -203
- package/dist/src/server/index.js +109 -109
- package/dist/src/server/index.js.map +1 -1
- package/package.json +24 -25
package/dist/src/core/index.cjs
CHANGED
|
@@ -6,7 +6,8 @@ let _orkestrel_emitter = require("@orkestrel/emitter");
|
|
|
6
6
|
let _orkestrel_tool = require("@orkestrel/tool");
|
|
7
7
|
//#region src/core/constants.ts
|
|
8
8
|
/**
|
|
9
|
-
* Names the revision offered and defaulted to in the legacy `initialize` handshake
|
|
9
|
+
* Names the revision offered and defaulted to in the legacy `initialize` handshake,
|
|
10
|
+
* `'2025-11-25'`.
|
|
10
11
|
*
|
|
11
12
|
* @remarks
|
|
12
13
|
* This is deliberately a legacy revision, and the newest one supported. 2026-07-28 is stateless
|
|
@@ -14,12 +15,15 @@ let _orkestrel_tool = require("@orkestrel/tool");
|
|
|
14
15
|
* it is asking to negotiate a revision with no negotiation.
|
|
15
16
|
*/
|
|
16
17
|
var MCP_HANDSHAKE_VERSION = "2025-11-25";
|
|
17
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* Names the older legacy revision the optional legacy decorator accepts and an adapter can pin,
|
|
20
|
+
* `'2025-06-18'`.
|
|
21
|
+
*/
|
|
18
22
|
var MCP_FALLBACK_VERSION = "2025-06-18";
|
|
19
|
-
/** Names the modern revision offered by an unpinned client during discovery
|
|
23
|
+
/** Names the modern revision offered by an unpinned client during discovery, `'2026-07-28'`. */
|
|
20
24
|
var MCP_MODERN_VERSION = "2026-07-28";
|
|
21
25
|
/**
|
|
22
|
-
* Lists the modern MCP protocol revisions a bare server accepts and advertises
|
|
26
|
+
* Lists the modern MCP protocol revisions a bare server accepts and advertises, `2026-07-28`.
|
|
23
27
|
*
|
|
24
28
|
* @remarks
|
|
25
29
|
* Frozen in discovery-advertisement order. Legacy revisions are absent because
|
|
@@ -27,7 +31,10 @@ var MCP_MODERN_VERSION = "2026-07-28";
|
|
|
27
31
|
* decorator own them.
|
|
28
32
|
*/
|
|
29
33
|
var SUPPORTED_MODERN_PROTOCOL_VERSIONS = Object.freeze([MCP_MODERN_VERSION]);
|
|
30
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* Lists the protocol revisions accepted by the optional legacy decorator, `2025-11-25` and
|
|
36
|
+
* `2025-06-18`.
|
|
37
|
+
*/
|
|
31
38
|
var SUPPORTED_LEGACY_PROTOCOL_VERSIONS = Object.freeze([MCP_HANDSHAKE_VERSION, MCP_FALLBACK_VERSION]);
|
|
32
39
|
/**
|
|
33
40
|
* Lists the protocol revisions the `isMCPVersion` guard admits, spanning the modern and legacy
|
|
@@ -48,8 +55,8 @@ var MCP_META_SUBSCRIPTION = "io.modelcontextprotocol/subscriptionId";
|
|
|
48
55
|
* Names the reserved extension key identifying the stable Tasks extension.
|
|
49
56
|
*
|
|
50
57
|
* @remarks
|
|
51
|
-
* The
|
|
52
|
-
* 2026-07-28 this package implements. A client declares it per
|
|
58
|
+
* The one spelling of it in this package, and the identity of the immutable snapshot dated
|
|
59
|
+
* 2026-07-28 this package implements. A client declares it per request, under
|
|
53
60
|
* `_meta['io.modelcontextprotocol/clientCapabilities'].extensions`; a server advertises it
|
|
54
61
|
* under `server/discover`'s `capabilities.extensions`. Both sides carry an empty object —
|
|
55
62
|
* the extension defines no options, so presence is the entire declaration.
|
|
@@ -59,10 +66,10 @@ var MCP_EXTENSION_TASKS = "io.modelcontextprotocol/tasks";
|
|
|
59
66
|
* Names the opening marker of the Base64 sentinel a standard MCP header value travels in.
|
|
60
67
|
*
|
|
61
68
|
* @remarks
|
|
62
|
-
* The markers are
|
|
63
|
-
* their
|
|
69
|
+
* The markers are lowercase and exact, and this constant with {@link MCP_SENTINEL_SUFFIX} is
|
|
70
|
+
* their one spelling in this package: {@link import('@orkestrel/mcp').encodeSentinel} builds a
|
|
64
71
|
* sentinel from them and {@link import('@orkestrel/mcp').decodeSentinel} recognizes one by
|
|
65
|
-
* them, so the
|
|
72
|
+
* them, so the directions cannot drift apart.
|
|
66
73
|
*/
|
|
67
74
|
var MCP_SENTINEL_PREFIX = "=?base64?";
|
|
68
75
|
/** Names the closing marker of the Base64 sentinel a standard MCP header value travels in. */
|
|
@@ -81,7 +88,7 @@ var MCP_PARAM_PREFIX = "Mcp-Param-";
|
|
|
81
88
|
* Names the Streamable-HTTP transport header that carries the MCP session id.
|
|
82
89
|
*
|
|
83
90
|
* @remarks
|
|
84
|
-
* A
|
|
91
|
+
* A stateful server sends it on the `initialize` reply, and
|
|
85
92
|
* {@link import('./transports/HTTPClientTransport.js').HTTPClientTransport} echoes it as a
|
|
86
93
|
* request header on every subsequent request, so a client passes that server's session
|
|
87
94
|
* validation unchanged.
|
|
@@ -114,7 +121,7 @@ var MCP_NAME_HEADER = "mcp-name";
|
|
|
114
121
|
* Identifies the tool-schema annotation key naming the header one parameter projects into.
|
|
115
122
|
*
|
|
116
123
|
* @remarks
|
|
117
|
-
* It is valid
|
|
124
|
+
* It is valid only on a primitive property schema statically reachable from the `inputSchema`
|
|
118
125
|
* root through `properties` keys alone. An occurrence anywhere else — under `items`, a
|
|
119
126
|
* composition or conditional keyword, or a `$ref` target — makes the whole tool definition
|
|
120
127
|
* invalid, which is what {@link import('@orkestrel/mcp').buildHeaderParameters} decides.
|
|
@@ -158,7 +165,7 @@ var MCP_HEADER_MISMATCH = -32020;
|
|
|
158
165
|
* declared.
|
|
159
166
|
*
|
|
160
167
|
* @remarks
|
|
161
|
-
* The
|
|
168
|
+
* The generic code for the whole condition, not one capability's code. This server answers
|
|
162
169
|
* it in more than one place — a `tools/call` that needs `elicitation`, and a `tasks/*` request
|
|
163
170
|
* whose client never declared `io.modelcontextprotocol/tasks` — and they are told apart by
|
|
164
171
|
* `error.data.requiredCapabilities` alone (`{ elicitation: {} }` against
|
|
@@ -205,7 +212,7 @@ var DEFAULT_MCP_LIMITS = Object.freeze({
|
|
|
205
212
|
* Holds the one empty argument record every argument-less modern `tools/call` runs with.
|
|
206
213
|
*
|
|
207
214
|
* @remarks
|
|
208
|
-
* Frozen and null-prototype, and
|
|
215
|
+
* Frozen and null-prototype, and shared: two calls that name no `arguments` receive the same
|
|
209
216
|
* reference, so nothing a tool writes into its own `arguments` can survive into the next
|
|
210
217
|
* call — the write fails instead. That failure is a tool-domain failure like any other: the
|
|
211
218
|
* registry isolates it into a `success: false` result, which reaches the peer as an
|
|
@@ -229,7 +236,7 @@ var JSONRPC_INVALID_PARAMS = -32602;
|
|
|
229
236
|
* valid request.
|
|
230
237
|
*
|
|
231
238
|
* @remarks
|
|
232
|
-
* The code every
|
|
239
|
+
* The code every modern internal fault answers with — a provider, handler, continuation,
|
|
233
240
|
* capacity, stream-source, normalization, or serialization failure the server contained.
|
|
234
241
|
* It is detail-free on the wire: the caught value reaches the application through the
|
|
235
242
|
* server's `error` event and never through the response.
|
|
@@ -239,7 +246,7 @@ var JSONRPC_INTERNAL_ERROR = -32603;
|
|
|
239
246
|
* Names the JSON-RPC 2.0 implementation-defined server error (the `-32000` to `-32099` range).
|
|
240
247
|
*
|
|
241
248
|
* @remarks
|
|
242
|
-
* Retained for the
|
|
249
|
+
* Retained for the legacy branch alone. A modern fault answers
|
|
243
250
|
* {@link JSONRPC_INTERNAL_ERROR}; this code survives only where an old-wire peer was
|
|
244
251
|
* already characterized against it.
|
|
245
252
|
*/
|
|
@@ -438,19 +445,19 @@ function snapshotToolResult(value, limits) {
|
|
|
438
445
|
*
|
|
439
446
|
* @remarks
|
|
440
447
|
* Total — a non-message returns `undefined`, never throws. The input must
|
|
441
|
-
*
|
|
448
|
+
* already be `JSON.parse`d: the raw-string parse (which can throw on malformed
|
|
442
449
|
* JSON) happens in `MCPServer.handle` inside a try/catch that maps a parse failure
|
|
443
450
|
* to a `-32700` response.
|
|
444
451
|
*
|
|
445
|
-
* A defined result is an
|
|
452
|
+
* A defined result is an owned canonical snapshot, never the input reference: it is
|
|
446
453
|
* rebuilt from the canonical text and deeply frozen, so `-0` arrives as `0`. Every record
|
|
447
|
-
* was
|
|
454
|
+
* was serialized with its keys sorted, but the rebuilt object enumerates its own keys the
|
|
448
455
|
* way JavaScript does, so an integer-like `'9'` still precedes `'10'`: the result's key
|
|
449
456
|
* order is neither promised nor generally the canonical one. A caller who needs canonical
|
|
450
|
-
*
|
|
457
|
+
* bytes takes them from `serializeJSON`/`snapshotJSON` rather than re-stringifying this
|
|
451
458
|
* result. Identity is not preserved and is not promised.
|
|
452
459
|
*
|
|
453
|
-
* The parser's sound partner is the
|
|
460
|
+
* The parser's sound partner is the composite `isJSONRPCMessage(value) &&
|
|
454
461
|
* isBoundedJSON(value, limits)`, and against it both halves of the soundness law
|
|
455
462
|
* hold by construction:
|
|
456
463
|
*
|
|
@@ -458,12 +465,12 @@ function snapshotToolResult(value, limits) {
|
|
|
458
465
|
* is applied to the exact frozen reference returned.
|
|
459
466
|
* - Every input satisfying the composite is admitted rather than rejected, because
|
|
460
467
|
* `isBoundedJSON` is this parser's own admission test — the same canonical
|
|
461
|
-
* serializer under the same `limits` — so
|
|
468
|
+
* serializer under the same `limits` — so they cannot disagree about the bound.
|
|
462
469
|
*
|
|
463
|
-
* {@link isJSONRPCMessage}
|
|
470
|
+
* {@link isJSONRPCMessage} Alone is not that partner. It is clone-backed and so already
|
|
464
471
|
* exact about shape, but it carries no size or depth bound — so guard-valid values
|
|
465
472
|
* exist that this parser rejects: a message nested deeper than `limits.depth`, and one
|
|
466
|
-
* whose canonical text exceeds `limits.bytes`. Those are named causes,
|
|
473
|
+
* whose canonical text exceeds `limits.bytes`. Those are named causes, not a complete
|
|
467
474
|
* boundary. Among values `isJSONRPCMessage` already admits, the admitted set is exactly
|
|
468
475
|
* what canonical serialization accepts under `limits`, so a caller who needs that line
|
|
469
476
|
* tests it with `isBoundedJSON` rather than inferring it from this list.
|
|
@@ -493,7 +500,7 @@ function parseJSONRPCMessage(value, limits = {
|
|
|
493
500
|
* This is the validity step after {@link isModernRequest}: a defined result can
|
|
494
501
|
* only come from a guard-positive request, while a guard-positive request returns
|
|
495
502
|
* `undefined` when its required modern metadata is malformed — and also when the
|
|
496
|
-
* request falls outside the bound this parser
|
|
503
|
+
* request falls outside the bound this parser inherits by routing through
|
|
497
504
|
* {@link parseJSONRPCMessage} under the same `limits`. The version
|
|
498
505
|
* must be a string but need not be supported; unsupported strings belong to the
|
|
499
506
|
* dedicated protocol-version error path. Client identity is optional, but when
|
|
@@ -542,11 +549,11 @@ function parseRequestContext(value, limits = {
|
|
|
542
549
|
* @remarks
|
|
543
550
|
* This parser does not open the opaque continuation carrier; the configured
|
|
544
551
|
* continuation port performs that boundary first. The protected
|
|
545
|
-
* payload binds the authenticated principal, absolute expiry,
|
|
552
|
+
* payload binds the authenticated principal, absolute expiry, original request id, version,
|
|
546
553
|
* method, the exact round that was issued, tool name, argument digest, and optional
|
|
547
554
|
* application state. Every member is required except application state: a payload missing its
|
|
548
555
|
* round cannot have the client's answers enforced, so it is refused rather than admitted
|
|
549
|
-
* unenforced. An
|
|
556
|
+
* unenforced. An empty round is refused for the same reason — a retry against it would answer
|
|
550
557
|
* no question at all. Total over malformed or hostile input.
|
|
551
558
|
*
|
|
552
559
|
* @param value - The opened canonical continuation value to parse
|
|
@@ -626,7 +633,7 @@ function supportsFormElicitation(value) {
|
|
|
626
633
|
* Computes the capabilities one round of input requests needs and the client did not declare.
|
|
627
634
|
*
|
|
628
635
|
* @remarks
|
|
629
|
-
* The protocol's rule is about
|
|
636
|
+
* The protocol's rule is about sending: a server never issues a request kind the client's
|
|
630
637
|
* declared capabilities exclude. So this reads the round rather than the method, and it
|
|
631
638
|
* answers with the refusal's own payload — the `requiredCapabilities` record a
|
|
632
639
|
* `MissingRequiredClientCapability` error carries, keyed by each missing capability, in the
|
|
@@ -638,7 +645,7 @@ function supportsFormElicitation(value) {
|
|
|
638
645
|
* recognize needs nothing, because {@link import('./validators.js').isMCPInputRequestMap}
|
|
639
646
|
* has already refused the round it would have travelled in. Total over hostile input.
|
|
640
647
|
*
|
|
641
|
-
* The `elicitation` value names the
|
|
648
|
+
* The `elicitation` value names the arm the round needs, so a client can act on the refusal
|
|
642
649
|
* by declaring exactly what the payload asks for. A missing URL arm answers `{ url: {} }`, a
|
|
643
650
|
* missing form arm answers the empty record this package reads as form-only, and a round
|
|
644
651
|
* needing both answers `{ form: {}, url: {} }`. An empty record for a URL round would name
|
|
@@ -689,14 +696,14 @@ function computeMissingCapabilities(requests, capabilities) {
|
|
|
689
696
|
*
|
|
690
697
|
* @remarks
|
|
691
698
|
* The declaration lives at `extensions['io.modelcontextprotocol/tasks']` and the schema
|
|
692
|
-
* types its value
|
|
699
|
+
* types its value exactly empty — `Record<string, never>`, an object with no additional
|
|
693
700
|
* properties. So the key's presence is the whole declaration, and the value carries the
|
|
694
701
|
* whole of the check: a `true` or a string there is a client speaking a different protocol
|
|
695
702
|
* rather than a shorthand, and a member inside the object is a client declaring an option
|
|
696
703
|
* this extension does not define. Both are refused, because a server that accepted either
|
|
697
704
|
* would be reading a shape no peer can produce from the snapshot's own schema.
|
|
698
705
|
*
|
|
699
|
-
* A client declares this
|
|
706
|
+
* A client declares this per request. Nothing here consults a session, because the modern
|
|
700
707
|
* revision is stateless and a capability declared once at connect time says nothing about
|
|
701
708
|
* the request in hand. Total over hostile input.
|
|
702
709
|
*
|
|
@@ -977,7 +984,7 @@ function buildProgressNotification(token, progress) {
|
|
|
977
984
|
* Builds one official cancellation notification for a request already sent.
|
|
978
985
|
*
|
|
979
986
|
* @remarks
|
|
980
|
-
* `requestId` and `reason` are
|
|
987
|
+
* `requestId` and `reason` are wire spellings carried verbatim from the dated schema's
|
|
981
988
|
* `CancelledNotificationParams`, and so is the `cancelled` in the method name — this
|
|
982
989
|
* package's own vocabulary says `abort`, but the method is the protocol's and does not
|
|
983
990
|
* change. The notification is FIRE-AND-FORGET in the strongest sense: it carries no id,
|
|
@@ -1015,7 +1022,7 @@ function buildCancelledNotification(id, reason) {
|
|
|
1015
1022
|
* Determines whether one method may answer with a given modern `resultType`.
|
|
1016
1023
|
*
|
|
1017
1024
|
* @remarks
|
|
1018
|
-
* The dated protocol lets a `tools/call` answer in more than one way — it
|
|
1025
|
+
* The dated protocol lets a `tools/call` answer in more than one way — it completed, it became a
|
|
1019
1026
|
* durable task, or it needs another round trip — while every other method this client
|
|
1020
1027
|
* issues has exactly one legal answer. So the arm a peer chose is only meaningful beside
|
|
1021
1028
|
* the method it answers, and this is the one place that pairing is decided.
|
|
@@ -1044,9 +1051,9 @@ function matchesResultType(method, resultType) {
|
|
|
1044
1051
|
* Concatenates an MCP tool-call result's text content blocks into one string.
|
|
1045
1052
|
*
|
|
1046
1053
|
* @remarks
|
|
1047
|
-
* The inverse of a server splitting a value into text block(s), and
|
|
1054
|
+
* The inverse of a server splitting a value into text block(s), and total: a non-record
|
|
1048
1055
|
* result, a non-array `content`, or a non-string `text` contributes nothing rather than
|
|
1049
|
-
* throwing. What it returns is a
|
|
1056
|
+
* throwing. What it returns is a rendering — the prose a model reads — and not the tool's
|
|
1050
1057
|
* value, which travels as `structuredContent` whenever the peer sent one.
|
|
1051
1058
|
*
|
|
1052
1059
|
* @param result - The unknown result payload to read content blocks from
|
|
@@ -1073,12 +1080,12 @@ function extractContentText(result) {
|
|
|
1073
1080
|
* the arms the protocol gives a shape to, and deriving the tool's value from the one it
|
|
1074
1081
|
* does not:
|
|
1075
1082
|
*
|
|
1076
|
-
* - A peer's `structuredContent` is
|
|
1083
|
+
* - A peer's `structuredContent` is preferred over the content blocks, because it is the
|
|
1077
1084
|
* tool's value in its original structure while the blocks are a rendering beside it. Its
|
|
1078
1085
|
* mere presence decides — an explicit `null` is a value the tool returned, not an absence.
|
|
1079
1086
|
* - With no structured value the legacy shape applies: the value was JSON-serialized into
|
|
1080
1087
|
* the text block(s), so parse them and fall back to the raw string when they are not JSON.
|
|
1081
|
-
* - A remote tool
|
|
1088
|
+
* - A remote tool failure (`isError: true`) throws the error text, so an agent's tool
|
|
1082
1089
|
* registry isolates it into a failure result exactly as it would a local throw.
|
|
1083
1090
|
*
|
|
1084
1091
|
* @param name - The tool's name, used only to describe a failure that carried no text
|
|
@@ -1160,14 +1167,14 @@ function buildJSONRPCResult(id, result) {
|
|
|
1160
1167
|
* as an `error` object.
|
|
1161
1168
|
*
|
|
1162
1169
|
* @remarks
|
|
1163
|
-
* An `undefined` `id` is
|
|
1170
|
+
* An `undefined` `id` is omitted from the envelope rather than serialized as `null`:
|
|
1164
1171
|
* MCP overrides the base specification here, so a peer that could not have its id
|
|
1165
1172
|
* read receives a response with no `id` member at all.
|
|
1166
1173
|
*
|
|
1167
1174
|
* @param id - The failed request's id, or `undefined` when none could be read
|
|
1168
1175
|
* @param code - One of the reserved JSON-RPC codes (see `./constants.js`)
|
|
1169
1176
|
* @param message - A short human description of the failure
|
|
1170
|
-
* @param data - An
|
|
1177
|
+
* @param data - An optional machine-readable payload (omitted from the envelope when absent)
|
|
1171
1178
|
* @returns The error response envelope
|
|
1172
1179
|
*/
|
|
1173
1180
|
function buildJSONRPCError(id, code, message, data) {
|
|
@@ -1189,11 +1196,11 @@ function buildJSONRPCError(id, code, message, data) {
|
|
|
1189
1196
|
* receives.
|
|
1190
1197
|
*
|
|
1191
1198
|
* @remarks
|
|
1192
|
-
* The
|
|
1199
|
+
* The one place a cancellation signal is resolved. A caller may have no signal to
|
|
1193
1200
|
* offer; a dispatched method always has one to observe, so a missing signal becomes
|
|
1194
1201
|
* a real signal rather than an absence every downstream handler would have to case on.
|
|
1195
1202
|
*
|
|
1196
|
-
* The resolved signal is the request's
|
|
1203
|
+
* The resolved signal is the request's lifetime, which is strictly wider than the
|
|
1197
1204
|
* caller's: it composes the caller's signal, when there is one, with the `lifetime`
|
|
1198
1205
|
* dispatch owns and aborts once the answer this request produced is finished. That is
|
|
1199
1206
|
* what wakes a stream producer parked on an event that will never arrive after its
|
|
@@ -1446,7 +1453,7 @@ function buildSubscriptionResult(id, identity) {
|
|
|
1446
1453
|
* `capabilities.resources` and `capabilities.prompts` appear only for servers with their
|
|
1447
1454
|
* respective managers and derive notification flags from the configured subscription filter.
|
|
1448
1455
|
* `capabilities.completions` is independent and appears only with a completion provider.
|
|
1449
|
-
* `capabilities.extensions` appears only for a server that
|
|
1456
|
+
* `capabilities.extensions` appears only for a server that configured the extension it
|
|
1450
1457
|
* would name. An advertisement is a promise a client is entitled to act on, so a server
|
|
1451
1458
|
* with no `task` policy omits the member entirely rather than advertising an empty
|
|
1452
1459
|
* record — and its discovery answer stays byte-for-byte what it was before the extension
|
|
@@ -1502,7 +1509,7 @@ function buildInitializeResult(name, version, requested) {
|
|
|
1502
1509
|
* before it hands the string on.
|
|
1503
1510
|
*
|
|
1504
1511
|
* @remarks
|
|
1505
|
-
* The bound is checked
|
|
1512
|
+
* The bound is checked first, against the raw string, so an oversized message is never
|
|
1506
1513
|
* parsed at all: a decoder that parses before it measures has already spent the work
|
|
1507
1514
|
* the bound exists to refuse. A message over the bound, malformed JSON, and a well-formed
|
|
1508
1515
|
* value that is not a JSON-RPC message are one answer — `undefined` — because a binder does
|
|
@@ -1529,12 +1536,12 @@ function decodeBoundedMessage(message, limits) {
|
|
|
1529
1536
|
* Decodes one inbound frame and delivers it onto a transport emitter as `message` or `error`.
|
|
1530
1537
|
*
|
|
1531
1538
|
* @remarks
|
|
1532
|
-
* The
|
|
1539
|
+
* The one inbound fold every message-carrying transport in this package runs: parse the frame,
|
|
1533
1540
|
* narrow it with `parseJSONRPCMessage`, emit `message` for a well-formed
|
|
1534
1541
|
* {@link JSONRPCMessage}, and emit `error` for anything else. Total — an adversarial frame
|
|
1535
1542
|
* produces an `error` emission and never a throw.
|
|
1536
1543
|
*
|
|
1537
|
-
* The
|
|
1544
|
+
* The failures report differently on purpose. Unparsable text emits the caught parse
|
|
1538
1545
|
* error, which names the offending position; well-formed JSON that is not a JSON-RPC message
|
|
1539
1546
|
* has no caught value to report, so it emits `fault` — the carrier's own wording, passed in
|
|
1540
1547
|
* rather than forked into a second copy of this body.
|
|
@@ -1586,15 +1593,15 @@ function decodeEvent(data) {
|
|
|
1586
1593
|
}
|
|
1587
1594
|
/**
|
|
1588
1595
|
* Decodes a `fetch` Response's Server-Sent-Events body into the JSON-RPC messages it
|
|
1589
|
-
* carried — the
|
|
1596
|
+
* carried — the client-side inverse of a server's Streamable-HTTP SSE response.
|
|
1590
1597
|
*
|
|
1591
1598
|
* @remarks
|
|
1592
1599
|
* Reads the whole `response.body` stream chunk-by-chunk through a `TextDecoder({ stream: true
|
|
1593
1600
|
* })` (handling a multi-byte character split across reads) and `@orkestrel/sse`'s
|
|
1594
1601
|
* {@link SSEParserInterface} (handling a partial line or in-progress event split across
|
|
1595
1602
|
* reads), then narrows each dispatched event's `data` to a {@link JSONRPCMessage} through
|
|
1596
|
-
* {@link decodeEvent} (so a non-message or non-JSON `data:` event is
|
|
1597
|
-
* total). It reuses the
|
|
1603
|
+
* {@link decodeEvent} (so a non-message or non-JSON `data:` event is dropped, never thrown —
|
|
1604
|
+
* total). It reuses the same `SSEParser` a server's `createStream` seam serializes against, so
|
|
1598
1605
|
* the wire round-trips. A `null` body (no stream) yields no messages;
|
|
1599
1606
|
* {@link import('./transports/HTTPClientTransport.js').HTTPClientTransport} reads a
|
|
1600
1607
|
* request/response SSE reply (the server sends one `data:` event then ends), so this drains to
|
|
@@ -1661,7 +1668,7 @@ function buildResponseError(response, type) {
|
|
|
1661
1668
|
* second spelling of a byte, so it is refused: `=?base64?QR==?=` reaches for the byte
|
|
1662
1669
|
* `=?base64?QQ==?=` spells canonically, and only the canonical spelling decodes. A malformed
|
|
1663
1670
|
* payload answers `undefined` rather than falling back to the literal, because the protocol
|
|
1664
|
-
* requires a server to
|
|
1671
|
+
* requires a server to reject invalid characters, and a fallback would admit the very value
|
|
1665
1672
|
* the rule exists to refuse. A value missing either marker is a literal and comes back
|
|
1666
1673
|
* unchanged.
|
|
1667
1674
|
*
|
|
@@ -1707,7 +1714,7 @@ function decodeSentinel(value) {
|
|
|
1707
1714
|
*
|
|
1708
1715
|
* @remarks
|
|
1709
1716
|
* The exact inverse of {@link decodeSentinel}, and its membership rule is stated as that
|
|
1710
|
-
* inverse rather than as a second list that could drift: a value travels
|
|
1717
|
+
* inverse rather than as a second list that could drift: a value travels literally when it is
|
|
1711
1718
|
* plain printable ASCII — every code point in `U+0020`–`U+007E`, the RFC 9110 field-value
|
|
1712
1719
|
* range this package admits — and {@link decodeSentinel} gives it back unchanged. Every other
|
|
1713
1720
|
* value travels wrapped in {@link MCP_SENTINEL_PREFIX} and {@link MCP_SENTINEL_SUFFIX}, the
|
|
@@ -1747,7 +1754,7 @@ function encodeSentinel(value) {
|
|
|
1747
1754
|
*
|
|
1748
1755
|
* @remarks
|
|
1749
1756
|
* The companion of {@link extractHeaderAnnotations}, which reads only the annotations a
|
|
1750
|
-
* `properties` chain reaches. Comparing the
|
|
1757
|
+
* `properties` chain reaches. Comparing the answers is how
|
|
1751
1758
|
* {@link buildHeaderParameters} decides reachability without a second walk that would have
|
|
1752
1759
|
* to re-state which JSON Schema keywords are traversable: an annotation the reachable walk
|
|
1753
1760
|
* did not read is one sitting under `items`, a composition or conditional keyword, a `$ref`
|
|
@@ -1791,12 +1798,12 @@ function countHeaderAnnotations(value) {
|
|
|
1791
1798
|
* Reachability is the protocol's own rule: an annotation counts only where a chain of
|
|
1792
1799
|
* `properties` keys leads to it from the `inputSchema` root, so `path` is both the schema
|
|
1793
1800
|
* position and the position the call's `arguments` carry the value at. A property named
|
|
1794
|
-
* `items` is reachable like any other, because the chain is read by key
|
|
1801
|
+
* `items` is reachable like any other, because the chain is read by key position rather than
|
|
1795
1802
|
* by key name.
|
|
1796
1803
|
*
|
|
1797
1804
|
* `undefined` means the definition is invalid rather than empty: a reachable annotation whose
|
|
1798
1805
|
* value is not an {@link import('./validators.js').isFieldToken} token, one sitting on the
|
|
1799
|
-
* schema
|
|
1806
|
+
* schema root (which is no property), one on a leaf whose declared type is not an
|
|
1800
1807
|
* {@link import('./validators.js').isMCPHeaderPrimitive} primitive, or a chain deeper than
|
|
1801
1808
|
* `DEFAULT_MCP_LIMITS.depth` — which is also what makes a self-referential schema terminate.
|
|
1802
1809
|
* A node that is not a record carries nothing and answers an empty list, because a leaf the
|
|
@@ -1840,8 +1847,8 @@ function extractHeaderAnnotations(schema, path) {
|
|
|
1840
1847
|
*
|
|
1841
1848
|
* @remarks
|
|
1842
1849
|
* The single decision both sides of the protocol make about an annotated tool: an HTTP
|
|
1843
|
-
*
|
|
1844
|
-
*
|
|
1850
|
+
* client excludes a definition this refuses from the `tools/list` result it delivers, and a
|
|
1851
|
+
* server recognizes exactly the `Mcp-Param-*` names this returns for its own definitions.
|
|
1845
1852
|
*
|
|
1846
1853
|
* `undefined` means the definition is invalid, and every rule the protocol states produces
|
|
1847
1854
|
* it: a value that is not an RFC 9110 token, a non-primitive or untyped annotated leaf, a
|
|
@@ -1878,11 +1885,11 @@ function buildHeaderParameters(schema) {
|
|
|
1878
1885
|
* Renders one projected argument as the text its `Mcp-Param-*` header carries.
|
|
1879
1886
|
*
|
|
1880
1887
|
* @remarks
|
|
1881
|
-
* The protocol's conversion table, and the
|
|
1888
|
+
* The protocol's conversion table, and the one place it is stated: a string travels as
|
|
1882
1889
|
* itself, an integer in decimal, and a boolean as lowercase `true` or `false`. The value's
|
|
1883
1890
|
* runtime shape must match the leaf's declared type, so a schema that declares `integer` and
|
|
1884
1891
|
* an argument that supplies a string, a fraction, or a magnitude outside the IEEE 754 safe
|
|
1885
|
-
* range carries
|
|
1892
|
+
* range carries nothing — a header that cannot round-trip the body value is worse than an
|
|
1886
1893
|
* absent one, and the tool's own argument validation owns the disagreement.
|
|
1887
1894
|
*
|
|
1888
1895
|
* @param value - The argument value read at the parameter's path
|
|
@@ -1967,7 +1974,7 @@ function extractToolSchema(response, name) {
|
|
|
1967
1974
|
* {@link buildCancelledNotification}.
|
|
1968
1975
|
*
|
|
1969
1976
|
* @remarks
|
|
1970
|
-
* `requestId` is the
|
|
1977
|
+
* `requestId` is the wire spelling carried verbatim from the dated schema, and it must be a
|
|
1971
1978
|
* real {@link JSONRPCId}: `null` is not one, and neither is an absent member, so a
|
|
1972
1979
|
* malformed frame reads as "cancels nothing" rather than as an error. Anything that is not a
|
|
1973
1980
|
* `notifications/cancelled` notification — a response, a request that happens to use the
|
|
@@ -1988,14 +1995,14 @@ function readCancelledId(message) {
|
|
|
1988
1995
|
}
|
|
1989
1996
|
/**
|
|
1990
1997
|
* Pumps a controlled serialized exchange onto a transport — every notification in order, then
|
|
1991
|
-
* the terminating response — and
|
|
1998
|
+
* the terminating response — and end the exchange however the pump leaves.
|
|
1992
1999
|
*
|
|
1993
2000
|
* @remarks
|
|
1994
2001
|
* The generator's `return` value is a message like any other on the wire: it is sent
|
|
1995
|
-
*
|
|
2002
|
+
* last and closes the exchange. Sends are awaited one at a time so the transport
|
|
1996
2003
|
* receives the sequence in the order the method produced it.
|
|
1997
2004
|
*
|
|
1998
|
-
* The first parameter is the
|
|
2005
|
+
* The first parameter is the controlled arm rather than a bare
|
|
1999
2006
|
* {@link import('./types.js').MCPTextStream}, and that is the whole point of it: this pump is
|
|
2000
2007
|
* an owner, and an owner needs a lifecycle member to discharge its obligation with. A bare
|
|
2001
2008
|
* generator has none, so an exit where nothing was cancelled — a `send` that threw two
|
|
@@ -2005,7 +2012,7 @@ function readCancelledId(message) {
|
|
|
2005
2012
|
* is a no-op for an exchange that already ended on its terminal.
|
|
2006
2013
|
*
|
|
2007
2014
|
* The `finally` is spelled explicitly rather than with `await using` because this package's
|
|
2008
|
-
* declared Node floor cannot
|
|
2015
|
+
* declared Node floor cannot parse `await using` — `target: ESNext` emits the declaration
|
|
2009
2016
|
* verbatim, and a floor engine rejects the whole module at load. The obligation discharged is
|
|
2010
2017
|
* identical either way.
|
|
2011
2018
|
*
|
|
@@ -2040,36 +2047,36 @@ async function sendStream(stream, transport) {
|
|
|
2040
2047
|
* `server.handle` already turns a malformed message into a serialized `-32700` /
|
|
2041
2048
|
* `-32600` reply and a notification into `undefined` (no reply), so this binder parses
|
|
2042
2049
|
* nothing the server would parse differently: it decodes each inbound message through
|
|
2043
|
-
* {@link decodeBoundedMessage} under `server.limit`, the
|
|
2050
|
+
* {@link decodeBoundedMessage} under `server.limit`, the server's own bound, so a message
|
|
2044
2051
|
* the server would refuse is never parsed here either and still receives its `-32700` from
|
|
2045
|
-
* the one place that words it. A
|
|
2052
|
+
* the one place that words it. A held-open reply arrives as an
|
|
2046
2053
|
* {@link import('./types.js').MCPTextStreamControllerInterface} instead of a string: this is
|
|
2047
2054
|
* the one place that pumps it, writing each notification in order and then the generator's
|
|
2048
2055
|
* returned terminating response ({@link sendStream}). A `transport.send` throw or rejection —
|
|
2049
2056
|
* mid-stream included — is caught and routed
|
|
2050
2057
|
* to `server.emitter`'s `error` event (never rethrown, never an unhandled rejection);
|
|
2051
2058
|
* a listener on that event that itself throws is swallowed (the end of the line —
|
|
2052
|
-
* the caller's own bug, never this binder's). A fault raised
|
|
2059
|
+
* the caller's own bug, never this binder's). A fault raised after its own request was
|
|
2053
2060
|
* cancelled reports nothing, because a cancellation is not a fault.
|
|
2054
2061
|
*
|
|
2055
|
-
* **This binder
|
|
2062
|
+
* **This binder owns every exchange it starts, and ends each one on every exit.** It holds one
|
|
2056
2063
|
* `AbortController` per live request, keyed by the request's id and deleted whenever that
|
|
2057
2064
|
* request leaves — normally, by a throw, or by cancellation — and it supplies that signal to
|
|
2058
2065
|
* `handle` as {@link import('./types.js').MCPDispatchOptions}. These consequences follow.
|
|
2059
|
-
* An inbound `notifications/cancelled`
|
|
2066
|
+
* An inbound `notifications/cancelled` aborts the request it names, which is how the message-
|
|
2060
2067
|
* based cancellation path reaches a tool on the carriers that have one (stdio, WebSocket,
|
|
2061
|
-
* `MessagePort`); a cancelled request writes
|
|
2068
|
+
* `MessagePort`); a cancelled request writes no response, because a peer that asked for a call
|
|
2062
2069
|
* to stop is not answered by it; and the transport's `closed` signal aborts every request
|
|
2063
2070
|
* still in flight, so an exchange being pumped when the carrier dies ends with it instead of
|
|
2064
2071
|
* writing into a socket nobody is holding.
|
|
2065
2072
|
*
|
|
2066
|
-
* `listen`/`closed` are
|
|
2067
|
-
*
|
|
2068
|
-
* `bindServer` call on the
|
|
2073
|
+
* `listen`/`closed` are replace semantics (§ port contract): the returned unbind
|
|
2074
|
+
* detaches by replacing this binder's own handlers with no-ops, so a subsequent
|
|
2075
|
+
* `bindServer` call on the same transport is never double-dispatched by a stale
|
|
2069
2076
|
* subscription left behind — an unbind→rebind cycle yields exactly one reply per
|
|
2070
2077
|
* request. Unbinding is itself an owner exit: it aborts and retires every request still in
|
|
2071
2078
|
* flight before detaching, so `unbind()` then `close()` and `close()` then `unbind()` end the
|
|
2072
|
-
* same exchanges. It does
|
|
2079
|
+
* same exchanges. It does not close the transport; that remains the caller's decision.
|
|
2073
2080
|
*
|
|
2074
2081
|
* @param server - The transport-agnostic server to dispatch inbound messages over
|
|
2075
2082
|
* @param transport - The duplex channel to pipe the server over
|
|
@@ -2129,7 +2136,7 @@ function bindServer(server, transport) {
|
|
|
2129
2136
|
}
|
|
2130
2137
|
/**
|
|
2131
2138
|
* Pipes an {@link MCPTransportInterface} into an {@link MCPClientInterface} — every
|
|
2132
|
-
* inbound message is decoded and delivered onto the client's
|
|
2139
|
+
* inbound message is decoded and delivered onto the client's own transport
|
|
2133
2140
|
* (`client.transport.emitter`'s `message` / `close` events), resolving/rejecting the
|
|
2134
2141
|
* client's correlated pending requests exactly as a direct reply would.
|
|
2135
2142
|
*
|
|
@@ -2137,30 +2144,30 @@ function bindServer(server, transport) {
|
|
|
2137
2144
|
* The client's outbound writes flow through `client.transport.send` — its existing,
|
|
2138
2145
|
* unmodified request/response correlation — so `client` must have been constructed
|
|
2139
2146
|
* with a {@link import('./types.js').MCPMessageTransportInterface} that itself carries
|
|
2140
|
-
* the
|
|
2147
|
+
* the same `transport` (see {@link import('./factories.js').createDuplexClientTransport},
|
|
2141
2148
|
* the additive factory that adapts an {@link MCPTransportInterface} into that shape);
|
|
2142
2149
|
* this binder then completes the inbound half by decoding each message and pushing it
|
|
2143
2150
|
* onto `client.transport.emitter` (an {@link import('@orkestrel/emitter').EmitterInterface}
|
|
2144
2151
|
* exposes `emit`, so no client modification is needed). A malformed / non-JSON-RPC
|
|
2145
|
-
* inbound message is
|
|
2152
|
+
* inbound message is dropped (total — never throws); a delivery fault is routed to
|
|
2146
2153
|
* `client.transport.emitter`'s `error` event (never rethrown). The returned unbind
|
|
2147
|
-
*
|
|
2148
|
-
* ignored)
|
|
2154
|
+
* detaches this binder (further inbound messages and the transport's `closed` signal are
|
|
2155
|
+
* ignored) without closing the transport.
|
|
2149
2156
|
*
|
|
2150
|
-
* `listen`/`closed` are
|
|
2151
|
-
*
|
|
2152
|
-
* `bindClient` call on the
|
|
2157
|
+
* `listen`/`closed` are replace semantics (§ port contract): the returned unbind
|
|
2158
|
+
* detaches by replacing this binder's own handlers with no-ops, so a subsequent
|
|
2159
|
+
* `bindClient` call on the same transport is never double-dispatched by a stale
|
|
2153
2160
|
* subscription left behind — an unbind→rebind cycle delivers exactly one `message`
|
|
2154
2161
|
* emit per inbound reply.
|
|
2155
2162
|
*
|
|
2156
2163
|
* **This binder needs no live-request registry, and the asymmetry with {@link bindServer} is
|
|
2157
|
-
* real rather than an omission.** A server binder holds the lifetime of work it
|
|
2164
|
+
* real rather than an omission.** A server binder holds the lifetime of work it started, so an
|
|
2158
2165
|
* inbound `notifications/cancelled` has something to reach; a client binder starts no work —
|
|
2159
2166
|
* `MCPClient` already owns its pending entries and already writes the cancellation frame
|
|
2160
2167
|
* itself when a caller's `signal` aborts, on a carrier declaring `duplex`. Adding a registry
|
|
2161
|
-
* here would be a second correlation table for ids the client is already correlating, and
|
|
2162
|
-
* tables for one fact drift. The one obligation this binder does carry is delivery: a
|
|
2163
|
-
* malformed / non-JSON-RPC inbound message is
|
|
2168
|
+
* here would be a second correlation table for ids the client is already correlating, and a
|
|
2169
|
+
* pair of tables for one fact drift. The one obligation this binder does carry is delivery: a
|
|
2170
|
+
* malformed / non-JSON-RPC inbound message is dropped (total — never throws).
|
|
2164
2171
|
*
|
|
2165
2172
|
* @param client - The transport-agnostic client whose transport to deliver messages onto
|
|
2166
2173
|
* @param transport - The duplex channel to pipe the client over
|
|
@@ -2244,8 +2251,8 @@ function isMCPResultMetaObject(value) {
|
|
|
2244
2251
|
* subscription id.
|
|
2245
2252
|
*
|
|
2246
2253
|
* @remarks
|
|
2247
|
-
* The reserved key is
|
|
2248
|
-
* stream passes with no stamp at all. When the key
|
|
2254
|
+
* The reserved key is optional, so a frame delivered outside a `subscriptions/listen`
|
|
2255
|
+
* stream passes with no stamp at all. When the key is present its value must be a valid
|
|
2249
2256
|
* {@link JSONRPCId}, because a stamp naming nothing addressable is worse than no stamp.
|
|
2250
2257
|
*
|
|
2251
2258
|
* @param value - The unknown value to inspect
|
|
@@ -2412,7 +2419,7 @@ function isAbsoluteURI(value) {
|
|
|
2412
2419
|
* Determines whether a value is one RFC 3339 `full-date` naming a real calendar day.
|
|
2413
2420
|
*
|
|
2414
2421
|
* @remarks
|
|
2415
|
-
* RFC 3339 §5.6 defines `date-mday` as `01-28`, `29`, `30`, or `31`
|
|
2422
|
+
* RFC 3339 §5.6 defines `date-mday` as `01-28`, `29`, `30`, or `31` based on the month and
|
|
2416
2423
|
* year, so the grammar is not satisfied by shape alone: `2026-02-30` and `2025-02-29` are
|
|
2417
2424
|
* well-formed triples that name no day, and a downstream `new Date` rolls each of them
|
|
2418
2425
|
* silently onto a different date rather than refusing it. February's length follows the
|
|
@@ -2420,7 +2427,7 @@ function isAbsoluteURI(value) {
|
|
|
2420
2427
|
*
|
|
2421
2428
|
* The check is pure integer arithmetic on the matched fields and never constructs a `Date`,
|
|
2422
2429
|
* because `Date` is exactly the component that performs the rollover this guard exists to
|
|
2423
|
-
* refuse. It is a
|
|
2430
|
+
* refuse. It is a syntax guard: no time zone, locale, calendar era, or leap second applies.
|
|
2424
2431
|
*
|
|
2425
2432
|
* @param value - The unknown value to inspect
|
|
2426
2433
|
* @returns True if the value is an RFC 3339 `full-date` for a day that exists; false otherwise
|
|
@@ -2894,7 +2901,7 @@ function isMCPContent(value) {
|
|
|
2894
2901
|
*
|
|
2895
2902
|
* @remarks
|
|
2896
2903
|
* The open contract's guard: a record carrying a string `resultType` and, when
|
|
2897
|
-
* present, exact result metadata. It deliberately does
|
|
2904
|
+
* present, exact result metadata. It deliberately does not narrow `resultType` to a
|
|
2898
2905
|
* known value, because the dated schema keeps adding them — a caller that needs a
|
|
2899
2906
|
* specific result uses that result's own guard, which narrows to its literal.
|
|
2900
2907
|
* Mutually exclusive with {@link isMCPLegacyResult} on every input: this one needs
|
|
@@ -2969,7 +2976,7 @@ function isMCPCallResult(value) {
|
|
|
2969
2976
|
* proof: this is what stands between a manager that answers a numeric `taskId` and a
|
|
2970
2977
|
* client that would receive one. `ttlMs` accepts `null` because the schema uses it to
|
|
2971
2978
|
* mean "no expiry", which is distinct from an absent field, and both durations must be
|
|
2972
|
-
*
|
|
2979
|
+
* integer milliseconds because the schema formats them `int`.
|
|
2973
2980
|
*
|
|
2974
2981
|
* @param value - The unknown value to inspect
|
|
2975
2982
|
* @returns True if the value is a well-formed `resultType: 'task'` result; false otherwise
|
|
@@ -3020,7 +3027,7 @@ function isMCPTaskStatus(value) {
|
|
|
3020
3027
|
* the requests to answer, `completed` owns the deferred call's result, `failed` owns the
|
|
3021
3028
|
* JSON-RPC error that ended it, and `working` / `cancelled` own nothing further.
|
|
3022
3029
|
*
|
|
3023
|
-
* A `completed` task's `result` is checked as an
|
|
3030
|
+
* A `completed` task's `result` is checked as an object and no further. The schema declares
|
|
3024
3031
|
* it an open record, so its contents belong to whichever method was deferred; a guard that
|
|
3025
3032
|
* demanded a protocol result here would refuse payloads the extension permits.
|
|
3026
3033
|
* `ttlMs` and `pollIntervalMs` are integer milliseconds, per the schema's `int` formats.
|
|
@@ -3062,11 +3069,11 @@ function isMCPTaskDetail(value) {
|
|
|
3062
3069
|
* Determines whether a value is the wire answer to `tasks/get`.
|
|
3063
3070
|
*
|
|
3064
3071
|
* @remarks
|
|
3065
|
-
* {@link isMCPTaskDetail} plus the stamp the
|
|
3072
|
+
* {@link isMCPTaskDetail} plus the stamp the method owes. The schema types a `tasks/get`
|
|
3066
3073
|
* reply as the detail intersected with the standard result, so `resultType: 'complete'` is
|
|
3067
3074
|
* part of the answer rather than decoration on it — and an unstamped payload, or one
|
|
3068
3075
|
* carrying the creation answer's `resultType: 'task'`, is a peer answering some other
|
|
3069
|
-
* shape. Use this guard wherever a `tasks/get`
|
|
3076
|
+
* shape. Use this guard wherever a `tasks/get` reply is read; use
|
|
3070
3077
|
* {@link isMCPTaskDetail} wherever a consumer's manager answers directly.
|
|
3071
3078
|
*
|
|
3072
3079
|
* `_meta` is checked only when present, and only as result metadata: the server identity a
|
|
@@ -3096,14 +3103,14 @@ function isMCPTaskDetailResult(value) {
|
|
|
3096
3103
|
* Determines whether a value is a `notifications/tasks` frame carrying a task snapshot.
|
|
3097
3104
|
*
|
|
3098
3105
|
* @remarks
|
|
3099
|
-
* The
|
|
3106
|
+
* The admission guard for a task transition: a subscription producer is consumer-written,
|
|
3100
3107
|
* so the frame it hands over is foreign input, and this is what stands between a mutated
|
|
3101
3108
|
* or half-built snapshot and a subscribed client. Both halves are checked — the method
|
|
3102
3109
|
* literal the extension fixes, and params that hold together as an
|
|
3103
3110
|
* {@link MCPTaskDetail} — because either alone admits a frame the other rejects.
|
|
3104
3111
|
*
|
|
3105
|
-
* `_meta` is checked for
|
|
3106
|
-
* stamp is the
|
|
3112
|
+
* `_meta` is checked for shape when present and nothing more. The reserved subscription
|
|
3113
|
+
* stamp is the server's to write, after this guard admits the frame and the matcher agrees
|
|
3107
3114
|
* to it, so a guard that demanded the stamp would refuse every frame a producer emits.
|
|
3108
3115
|
*
|
|
3109
3116
|
* @param value - The unknown value to inspect
|
|
@@ -3514,13 +3521,13 @@ function isMCPElicitResult(value) {
|
|
|
3514
3521
|
* Determines whether accepted elicitation content satisfies the exact schema that was issued.
|
|
3515
3522
|
*
|
|
3516
3523
|
* @remarks
|
|
3517
|
-
* {@link isMCPElicitResult} says a response has the
|
|
3518
|
-
* response answers the
|
|
3524
|
+
* {@link isMCPElicitResult} says a response has the shape of a response; this says the
|
|
3525
|
+
* response answers the question that was asked. A server that protects the schema it issued
|
|
3519
3526
|
* and then never enforces it has bought nothing, so this guard closes that gap: it is what
|
|
3520
3527
|
* turns a bound schema into a checked one.
|
|
3521
3528
|
*
|
|
3522
3529
|
* Every own value must be one {@link MCPElicitValue} — a string, a finite number, a boolean,
|
|
3523
|
-
* or an array of strings. A value whose name is
|
|
3530
|
+
* or an array of strings. A value whose name is declared in `schema.properties` must in
|
|
3524
3531
|
* addition satisfy that field's schema: `integer` rejects a fraction, `minimum` / `maximum`
|
|
3525
3532
|
* bound a number, `minLength` / `maxLength` bound a string by code points, `enum` and `oneOf`
|
|
3526
3533
|
* bound it to a declared member, `format` is enforced (`uri` by {@link isAbsoluteURI}, `email`
|
|
@@ -3529,9 +3536,9 @@ function isMCPElicitResult(value) {
|
|
|
3529
3536
|
* `maxItems` with every entry drawn from its `items.enum` or `items.anyOf`. Every name listed
|
|
3530
3537
|
* in `schema.required` must be present.
|
|
3531
3538
|
*
|
|
3532
|
-
* An
|
|
3539
|
+
* An undeclared property remains valid: the restricted schema is open by default, so a client
|
|
3533
3540
|
* that answers more than it was asked is not refused for it. A `schema` that is not itself a
|
|
3534
|
-
* valid {@link MCPElicitSchema} admits
|
|
3541
|
+
* valid {@link MCPElicitSchema} admits nothing — an unenforceable schema is never a permissive
|
|
3535
3542
|
* one — which is why `schema` is accepted as `unknown` and checked rather than trusted. Total
|
|
3536
3543
|
* over hostile content and hostile schemas alike.
|
|
3537
3544
|
*
|
|
@@ -3724,10 +3731,11 @@ function isMCPSampleContent(value) {
|
|
|
3724
3731
|
*
|
|
3725
3732
|
* @remarks
|
|
3726
3733
|
* The schema's `CreateMessageResult` types `content` as an `anyOf` over one
|
|
3727
|
-
* {@link isMCPSampleContent} block or an
|
|
3734
|
+
* {@link isMCPSampleContent} block or an array of them, so both are admitted here: a
|
|
3728
3735
|
* tool-using model answers with `tool_use` and `tool_result` blocks, and a model answering in
|
|
3729
3736
|
* several parts answers with the array. `stopReason` stays an open string because the schema
|
|
3730
|
-
* names
|
|
3737
|
+
* names `endTurn`, `stopSequence`, `maxTokens`, and `toolUse` and permits any other a provider
|
|
3738
|
+
* reports. Total over hostile input.
|
|
3731
3739
|
*
|
|
3732
3740
|
* @param value - The unknown value to inspect
|
|
3733
3741
|
* @returns True if `value` has the sampling-completion shape; false otherwise
|
|
@@ -3768,12 +3776,12 @@ function isMCPSampleResult(value) {
|
|
|
3768
3776
|
* Determines whether a response answers the exact embedded request that was issued.
|
|
3769
3777
|
*
|
|
3770
3778
|
* @remarks
|
|
3771
|
-
* A response carries no `method` of its own, so the
|
|
3779
|
+
* A response carries no `method` of its own, so the issued request selects which arm applies
|
|
3772
3780
|
* — the same way {@link isElicitContent} takes the issued schema rather than trusting the
|
|
3773
3781
|
* content to describe itself. A form elicitation is checked twice: once for the response
|
|
3774
3782
|
* shape and once, on `accept`, for the content against the schema that round issued. A
|
|
3775
3783
|
* URL-mode elicitation issues no schema, so only the shape is checked. A request this
|
|
3776
|
-
* package cannot recognize admits
|
|
3784
|
+
* package cannot recognize admits nothing, because an unrecognized question has no correct
|
|
3777
3785
|
* answer. Total over hostile responses and hostile requests alike.
|
|
3778
3786
|
*
|
|
3779
3787
|
* @param value - The client's answer to check
|
|
@@ -3836,7 +3844,7 @@ function isMCPInputResult(value) {
|
|
|
3836
3844
|
*
|
|
3837
3845
|
* @remarks
|
|
3838
3846
|
* A request is a record with `jsonrpc === '2.0'`, a string `method`, and an `id`
|
|
3839
|
-
* that {@link isJSONRPCId} accepts. An id-less call is
|
|
3847
|
+
* that {@link isJSONRPCId} accepts. An id-less call is not a request — it is a
|
|
3840
3848
|
* {@link JSONRPCNotification}, which {@link isJSONRPCNotification} answers for. The
|
|
3841
3849
|
* guards are mutually exclusive on every input: this one requires a valid `id`
|
|
3842
3850
|
* value, that one requires no own `id` member at all. `params`, when present, must
|
|
@@ -3865,7 +3873,7 @@ function isJSONRPCRequest(value) {
|
|
|
3865
3873
|
* Determines whether a parsed value is a {@link JSONRPCNotification}.
|
|
3866
3874
|
*
|
|
3867
3875
|
* @remarks
|
|
3868
|
-
* A notification is a request-shaped call carrying
|
|
3876
|
+
* A notification is a request-shaped call carrying no `id` member — the protocol
|
|
3869
3877
|
* forbids one, because nothing answers a notification. `params`, when present, must
|
|
3870
3878
|
* be a record. Total: any other input returns `false`.
|
|
3871
3879
|
*
|
|
@@ -3906,8 +3914,8 @@ function isJSONRPCInvocation(value) {
|
|
|
3906
3914
|
* arm of a response.
|
|
3907
3915
|
*
|
|
3908
3916
|
* @remarks
|
|
3909
|
-
* A result answers a request, so `id` is
|
|
3910
|
-
* {@link isJSONRPCId}. The envelope must own a `result` and must
|
|
3917
|
+
* A result answers a request, so `id` is required and must be a valid
|
|
3918
|
+
* {@link isJSONRPCId}. The envelope must own a `result` and must not own an `error`,
|
|
3911
3919
|
* which is what makes this guard and {@link isJSONRPCErrorResponse} mutually
|
|
3912
3920
|
* exclusive on every input. `result` itself must be an object: either a modern
|
|
3913
3921
|
* {@link isMCPResult} or a legacy {@link isMCPLegacyResult}, never a bare primitive.
|
|
@@ -3935,17 +3943,17 @@ function isJSONRPCResultResponse(value) {
|
|
|
3935
3943
|
* Determines whether a value is one JSON-RPC `error` member.
|
|
3936
3944
|
*
|
|
3937
3945
|
* @remarks
|
|
3938
|
-
* The failure
|
|
3946
|
+
* The failure object, not the envelope carrying it — the shape a failed response owns
|
|
3939
3947
|
* under `error`, and the shape a `failed` {@link MCPTaskDetail} owns under the same name,
|
|
3940
3948
|
* which is why it is one guard rather than the same checks written twice.
|
|
3941
3949
|
*
|
|
3942
|
-
* It is deliberately
|
|
3950
|
+
* It is deliberately structural rather than exact-JSON: `data` is declared `unknown`, so
|
|
3943
3951
|
* requiring the whole object to survive a JSON clone would refuse a legal error that
|
|
3944
3952
|
* carried a non-JSON payload. Both callers here hand it an already-owned value.
|
|
3945
3953
|
*
|
|
3946
3954
|
* That choice is why the key reads are guarded. Every sibling guard clones first, and a
|
|
3947
3955
|
* clone reads each key once behind a boundary that already owns totality; this one is the
|
|
3948
|
-
* family's only
|
|
3956
|
+
* family's only direct reader, so it meets `code` and `message` exactly as the value defines
|
|
3949
3957
|
* them — including as accessors that throw. Reading a named key off an unowned value is
|
|
3950
3958
|
* itself the hostile step, and it is bounded here rather than allowed to escape. Total.
|
|
3951
3959
|
*
|
|
@@ -3968,9 +3976,9 @@ function isJSONRPCError(value) {
|
|
|
3968
3976
|
* arm of a response.
|
|
3969
3977
|
*
|
|
3970
3978
|
* @remarks
|
|
3971
|
-
* `id` is
|
|
3972
|
-
* request's id
|
|
3973
|
-
* valid and a `null` one is not. The envelope must own an `error` and must
|
|
3979
|
+
* `id` is optional here and only here: a peer that could not read the failed
|
|
3980
|
+
* request's id omits the member rather than sending `null`, so an absent `id` is
|
|
3981
|
+
* valid and a `null` one is not. The envelope must own an `error` and must not own a
|
|
3974
3982
|
* `result`. `error` carries an integer `code` and a string `message`. Total.
|
|
3975
3983
|
*
|
|
3976
3984
|
* @param value - The already-parsed value to test
|
|
@@ -4060,9 +4068,9 @@ function isModernRequest(value) {
|
|
|
4060
4068
|
* Infers the wire era for an MCP protocol revision.
|
|
4061
4069
|
*
|
|
4062
4070
|
* @remarks
|
|
4063
|
-
* The era is
|
|
4071
|
+
* The era is read from the era guards rather than restated here, so a revision added
|
|
4064
4072
|
* to {@link SUPPORTED_MODERN_PROTOCOL_VERSIONS} or {@link SUPPORTED_LEGACY_PROTOCOL_VERSIONS}
|
|
4065
|
-
* carries its era with it and no
|
|
4073
|
+
* carries its era with it and no further list can disagree with them.
|
|
4066
4074
|
*
|
|
4067
4075
|
* @param version - The protocol revision to classify
|
|
4068
4076
|
* @returns `'modern'` for a revision a bare server accepts, `'legacy'` for a revision the
|
|
@@ -4076,10 +4084,10 @@ function inferEra(version) {
|
|
|
4076
4084
|
* Infers the wire era one invocation's own structure selects.
|
|
4077
4085
|
*
|
|
4078
4086
|
* @remarks
|
|
4079
|
-
* The
|
|
4087
|
+
* The structural read, distinct from {@link inferEra}'s read of a revision string: era is fixed
|
|
4080
4088
|
* by the reserved modern metadata a request carries, so this answers for a message whose
|
|
4081
4089
|
* revision has not been read and cannot answer `undefined` — every invocation took one of the
|
|
4082
|
-
*
|
|
4090
|
+
* published wire shapes. It is what an observation surface reports and what an ingress
|
|
4083
4091
|
* routes on, so both derive it here rather than each spelling the ternary out.
|
|
4084
4092
|
*
|
|
4085
4093
|
* @param invocation - The invocation whose structure selects the era
|
|
@@ -4103,15 +4111,15 @@ function inferVersion(offered) {
|
|
|
4103
4111
|
for (const version of SUPPORTED_MODERN_PROTOCOL_VERSIONS) if (offered.includes(version)) return version;
|
|
4104
4112
|
}
|
|
4105
4113
|
/**
|
|
4106
|
-
* Infers the protocol version an outbound message announces itself with — the
|
|
4114
|
+
* Infers the protocol version an outbound message announces itself with — the one
|
|
4107
4115
|
* projection every HTTP client transport stamps `mcp-protocol-version` from.
|
|
4108
4116
|
*
|
|
4109
4117
|
* @remarks
|
|
4110
|
-
* This is deliberately the
|
|
4118
|
+
* This is deliberately the same read the server's own expectation performs
|
|
4111
4119
|
* ({@link import('@orkestrel/mcp/server').inferHeaderIssue}): a modern request's reserved
|
|
4112
|
-
* `_meta` version, accepted whenever it is a string. It is
|
|
4120
|
+
* `_meta` version, accepted whenever it is a string. It is not
|
|
4113
4121
|
* {@link import('./parsers.js').parseRequestContext}, and the difference is the whole
|
|
4114
|
-
* point. That parser answers a different question — is the modern metadata
|
|
4122
|
+
* point. That parser answers a different question — is the modern metadata well formed —
|
|
4115
4123
|
* and refuses a request whose capability declaration or logging level is malformed. Such a
|
|
4116
4124
|
* request is still modern (era is fixed by key presence) and the server still demands the
|
|
4117
4125
|
* header for it, so projecting through the parser withholds a header the peer requires and
|
|
@@ -4120,7 +4128,7 @@ function inferVersion(offered) {
|
|
|
4120
4128
|
* A non-modern message projects nothing: a legacy request's version comes from the
|
|
4121
4129
|
* `initialize` handshake the transport captured, not from the message.
|
|
4122
4130
|
*
|
|
4123
|
-
* Header
|
|
4131
|
+
* Header names stay with the transports that own the wire (see `constants.ts`); core owns
|
|
4124
4132
|
* the value this projection derives, which is the part the browser and Node faces disagreed about.
|
|
4125
4133
|
*
|
|
4126
4134
|
* @param message - The outbound message about to be written
|
|
@@ -4145,10 +4153,10 @@ function inferRequestVersion(message) {
|
|
|
4145
4153
|
*
|
|
4146
4154
|
* @remarks
|
|
4147
4155
|
* - **One seam.** The server registers its built-in modern methods here at construction
|
|
4148
|
-
* and resolves
|
|
4156
|
+
* and resolves every modern method from here, so a consumer's method and a built-in
|
|
4149
4157
|
* are the same kind of thing on the same path.
|
|
4150
4158
|
* - **Registration is a write, not a merge.** `add` under a name already present
|
|
4151
|
-
*
|
|
4159
|
+
* replaces it, which is how a consumer overrides a built-in; there is no precedence
|
|
4152
4160
|
* rule to remember.
|
|
4153
4161
|
* - **A narrower contract than a `Map`.** Callers register and resolve; they cannot
|
|
4154
4162
|
* iterate, clear, or otherwise reach the server's internal state through it.
|
|
@@ -4301,16 +4309,16 @@ var MCPProgressReporter = class {
|
|
|
4301
4309
|
* through.
|
|
4302
4310
|
*
|
|
4303
4311
|
* @remarks
|
|
4304
|
-
* A native async generator decides cancellation with a
|
|
4312
|
+
* A native async generator decides cancellation with a queue: `return()` and `throw()` wait
|
|
4305
4313
|
* behind a `next()` the producer has not answered, so a consumer abandoning a source parked
|
|
4306
4314
|
* on an event that will never arrive waits forever for its own cancellation. This class
|
|
4307
|
-
* arbitrates instead of queueing. It keeps at most
|
|
4308
|
-
* settles the consumer's read itself, aborts the request's lifetime
|
|
4315
|
+
* arbitrates instead of queueing. It keeps at most one read outstanding against the source,
|
|
4316
|
+
* settles the consumer's read itself, aborts the request's lifetime before it delegates
|
|
4309
4317
|
* cleanup to the producer — so a cooperating producer is woken rather than waited on —
|
|
4310
4318
|
* contains every promise the producer settles late, and makes every closure path idempotent.
|
|
4311
4319
|
*
|
|
4312
4320
|
* The closures are deliberately different answers: the source's own return is the
|
|
4313
|
-
* terminal
|
|
4321
|
+
* terminal response, `return(value)` is the consumer saying it has the answer already, and
|
|
4314
4322
|
* {@link stop} is an owner saying there will be no answer at all. Only the source's own
|
|
4315
4323
|
* return is a message a peer ever sees.
|
|
4316
4324
|
*
|
|
@@ -4318,7 +4326,7 @@ var MCPProgressReporter = class {
|
|
|
4318
4326
|
* generator is suspended inside, so the signal is how an uncooperative producer is asked to
|
|
4319
4327
|
* finish, and this controller never blocks its consumer on the answer.
|
|
4320
4328
|
*
|
|
4321
|
-
* **What this class does
|
|
4329
|
+
* **What this class does not have is an owner of last resort.** No finalizer, no timer, no
|
|
4322
4330
|
* timeout ends an exchange nobody released. That absence is the design: an exchange holds a
|
|
4323
4331
|
* producer, a request lifetime and a live server slot, so a silent background release would
|
|
4324
4332
|
* turn "a pump forgot its obligation" from a reproducible defect into a nondeterministic one,
|
|
@@ -4499,15 +4507,15 @@ var MCPStreamController = class {
|
|
|
4499
4507
|
* serialized.
|
|
4500
4508
|
*
|
|
4501
4509
|
* @remarks
|
|
4502
|
-
* A
|
|
4510
|
+
* A translation boundary and deliberately nothing else. It serializes each message and the
|
|
4503
4511
|
* terminating response, and every lifecycle decision — return, throw, dispose, stop — ends
|
|
4504
4512
|
* the typed exchange beneath it rather than this face. That is the whole design constraint: a
|
|
4505
|
-
* serialized face implemented as its own async generator would add a
|
|
4513
|
+
* serialized face implemented as its own async generator would add a second operation queue,
|
|
4506
4514
|
* and the queue is exactly the defect the typed controller exists to remove — a `return()`
|
|
4507
4515
|
* promptly settled at the text face and left queued at the typed one cancels nothing.
|
|
4508
4516
|
*
|
|
4509
4517
|
* One member is a narrowing rather than a pass-through, and it is worth knowing before it
|
|
4510
|
-
* surprises a producer. `return` receives a
|
|
4518
|
+
* surprises a producer. `return` receives a string; it cannot rebuild the typed
|
|
4511
4519
|
* `JSONRPCResponse` the typed face would close on, and inventing one by parsing the
|
|
4512
4520
|
* argument back would make this face decide what the exchange ended with. So it ends the
|
|
4513
4521
|
* typed exchange with {@link MCPStreamControllerInterface.stop} — no terminal — and answers
|
|
@@ -4516,11 +4524,11 @@ var MCPStreamController = class {
|
|
|
4516
4524
|
* the honest translation of "the consumer already has its answer" when the answer is opaque
|
|
4517
4525
|
* text, not a downgrade to work around.
|
|
4518
4526
|
*
|
|
4519
|
-
* It accepts only a
|
|
4527
|
+
* It accepts only a controlled typed stream. A raw generator would have no lifecycle to
|
|
4520
4528
|
* delegate to, and this class refuses to grow one of its own.
|
|
4521
4529
|
*
|
|
4522
4530
|
* Delegation is total and it is what makes the ownership obligation transitive: `return`,
|
|
4523
|
-
* `throw`, `stop`, and dispose each end the
|
|
4531
|
+
* `throw`, `stop`, and dispose each end the typed exchange, so a pump holding only this
|
|
4524
4532
|
* serialized face still releases the producer, the request lifetime, and the live server slot
|
|
4525
4533
|
* behind it. There is no owner of last resort here either, for the same reason there is none
|
|
4526
4534
|
* on the typed face.
|
|
@@ -4567,7 +4575,7 @@ var MCPTextStreamController = class {
|
|
|
4567
4575
|
* @remarks
|
|
4568
4576
|
* The typed exchange ends with no terminal, because a string is not a
|
|
4569
4577
|
* `JSONRPCResponse` and this face never parses one back out of its argument. The
|
|
4570
|
-
* supplied text is the answer to
|
|
4578
|
+
* supplied text is the answer to this consumer, and a cooperating producer sees its
|
|
4571
4579
|
* cancellation path rather than its normal return.
|
|
4572
4580
|
*
|
|
4573
4581
|
* @param value - The serialized terminal the consumer is ending on
|
|
@@ -4604,7 +4612,7 @@ var MCPTextStreamController = class {
|
|
|
4604
4612
|
*
|
|
4605
4613
|
* @remarks
|
|
4606
4614
|
* Delegates downward exactly as {@link stop} does — disposing the serialized arm is
|
|
4607
|
-
* disposing the exchange, never
|
|
4615
|
+
* disposing the exchange, never this adapter alone.
|
|
4608
4616
|
*
|
|
4609
4617
|
* @returns Resolves once the typed exchange has ended
|
|
4610
4618
|
*/
|
|
@@ -4660,7 +4668,8 @@ var MCPLegacy = class {
|
|
|
4660
4668
|
const parsed = (0, _orkestrel_contract.parseJSON)(message);
|
|
4661
4669
|
if (isModernRequest(parsed) || !isJSONRPCInvocation(parsed)) return this.#options.dispatcher.handle(message, options);
|
|
4662
4670
|
const answer = await this.#legacy(parsed, options);
|
|
4663
|
-
|
|
4671
|
+
if (answer === void 0) return void 0;
|
|
4672
|
+
return Symbol.asyncIterator in answer ? new MCPTextStreamController(answer) : JSON.stringify(answer);
|
|
4664
4673
|
}
|
|
4665
4674
|
async #legacy(invocation, options) {
|
|
4666
4675
|
if (invocation.id === void 0) return void 0;
|
|
@@ -4684,13 +4693,42 @@ var MCPLegacy = class {
|
|
|
4684
4693
|
}
|
|
4685
4694
|
async #forward(request, options) {
|
|
4686
4695
|
const translated = legacyInvocationToModern(request);
|
|
4687
|
-
const
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
4696
|
+
const metadata = request.method === "tools/call" ? request.params?.["_meta"] : void 0;
|
|
4697
|
+
const candidate = (0, _orkestrel_contract.isRecord)(metadata) ? metadata["progressToken"] : void 0;
|
|
4698
|
+
const token = (0, _orkestrel_contract.isString)(candidate) || (0, _orkestrel_contract.isInteger)(candidate) ? candidate : void 0;
|
|
4699
|
+
if (token === void 0) {
|
|
4700
|
+
const answer = await this.#options.dispatcher.dispatch(translated, options);
|
|
4701
|
+
if (Symbol.asyncIterator in answer) {
|
|
4702
|
+
answer.stop();
|
|
4703
|
+
await answer[Symbol.asyncDispose]();
|
|
4704
|
+
return this.#unsupported(request.id, "stream");
|
|
4705
|
+
}
|
|
4706
|
+
return this.#project(answer, request.id);
|
|
4707
|
+
}
|
|
4708
|
+
const closure = new AbortController();
|
|
4709
|
+
const resolved = buildMethodOptions(options ?? {}, closure.signal);
|
|
4710
|
+
try {
|
|
4711
|
+
const answer = await this.#options.dispatcher.dispatch(translated, resolved);
|
|
4712
|
+
if (Symbol.asyncIterator in answer) return new MCPStreamController(this.#progress(answer, request.id, token), resolved.signal, closure);
|
|
4713
|
+
closure.abort();
|
|
4714
|
+
return this.#project(answer, request.id);
|
|
4715
|
+
} catch (error) {
|
|
4716
|
+
closure.abort(error);
|
|
4717
|
+
throw error;
|
|
4718
|
+
}
|
|
4719
|
+
}
|
|
4720
|
+
async *#progress(stream, id, token) {
|
|
4721
|
+
try {
|
|
4722
|
+
while (true) {
|
|
4723
|
+
const frame = await stream.next();
|
|
4724
|
+
if (frame.done === true) return this.#project(frame.value, id);
|
|
4725
|
+
const params = frame.value.params;
|
|
4726
|
+
if (frame.value.method !== "notifications/progress" || !(0, _orkestrel_contract.isRecord)(params) || params["progressToken"] !== token || !isMCPProgress(params)) return this.#unsupported(id, "stream");
|
|
4727
|
+
yield frame.value;
|
|
4728
|
+
}
|
|
4729
|
+
} finally {
|
|
4730
|
+
await stream[Symbol.asyncDispose]();
|
|
4692
4731
|
}
|
|
4693
|
-
return this.#project(answer, request.id);
|
|
4694
4732
|
}
|
|
4695
4733
|
#project(answer, id) {
|
|
4696
4734
|
if (answer.error !== void 0) return answer.error.code === -32021 ? this.#unsupported(id, this.#capability(answer)) : answer;
|
|
@@ -4936,7 +4974,7 @@ var MCPLegacyClientTransport = class {
|
|
|
4936
4974
|
//#endregion
|
|
4937
4975
|
//#region src/core/MCPServer.ts
|
|
4938
4976
|
/**
|
|
4939
|
-
* Dispatches JSON-RPC 2.0 requests over a live {@link ToolManagerInterface}, with
|
|
4977
|
+
* Dispatches JSON-RPC 2.0 requests over a live {@link ToolManagerInterface}, with no
|
|
4940
4978
|
* transport coupling.
|
|
4941
4979
|
*
|
|
4942
4980
|
* @remarks
|
|
@@ -4946,7 +4984,7 @@ var MCPLegacyClientTransport = class {
|
|
|
4946
4984
|
* `handle(message)` is the string boundary: it
|
|
4947
4985
|
* `JSON.parse`s the raw message (a failure → a `-32700` response), narrows it to
|
|
4948
4986
|
* an invocation (a non-invocation → a `-32600` response, with the unreadable `id`
|
|
4949
|
-
*
|
|
4987
|
+
* omitted rather than nulled), dispatches, and serializes the
|
|
4950
4988
|
* response back to a string (`undefined` for a notification).
|
|
4951
4989
|
* - **One modern seam.** `server/discover`, `tools/list`, `tools/call`, and
|
|
4952
4990
|
* `subscriptions/listen` are always registered; `resources/*`, `prompts/*`, and
|
|
@@ -5637,19 +5675,19 @@ var MCPServer = class {
|
|
|
5637
5675
|
//#endregion
|
|
5638
5676
|
//#region src/core/MCPTaskClient.ts
|
|
5639
5677
|
/**
|
|
5640
|
-
* Issues the `tasks/*` methods over one correlated-request door — the
|
|
5678
|
+
* Issues the `tasks/*` methods over one correlated-request door — the client half of the
|
|
5641
5679
|
* stable Tasks extension, exposed as an {@link import('./types.js').MCPClientInterface}'s
|
|
5642
5680
|
* `tasks`.
|
|
5643
5681
|
*
|
|
5644
5682
|
* @remarks
|
|
5645
5683
|
* - **The mirror of the server-side port, minus `start`.** An
|
|
5646
5684
|
* {@link import('./types.js').MCPTaskManagerInterface} is the consumer's durable store the
|
|
5647
|
-
*
|
|
5685
|
+
* server creates tasks in; this is the client's read/answer/stop access to the tasks a peer
|
|
5648
5686
|
* already created. Creation is missing on purpose: the extension gives a client no flag and
|
|
5649
5687
|
* no parameter to ask for a task, so `start` has no wire method to be.
|
|
5650
5688
|
* - **No plural accessor, no loop, no cache.** MCP defines no `tasks/list`, so nothing here
|
|
5651
5689
|
* enumerates. A task snapshot's `pollIntervalMs` is carried untouched and a one-shot read
|
|
5652
|
-
* sits beside it; the
|
|
5690
|
+
* sits beside it; the schedule is the consumer's, because this package has no durable place
|
|
5653
5691
|
* to keep a task, no idea when the application still cares, and no lifetime to hang a timer
|
|
5654
5692
|
* on that outlives the request it was born from. An instance left alone writes nothing.
|
|
5655
5693
|
* - **One channel.** Every request goes through the injected
|
|
@@ -5697,20 +5735,20 @@ var MCPTaskClient = class {
|
|
|
5697
5735
|
//#endregion
|
|
5698
5736
|
//#region src/core/MCPClient.ts
|
|
5699
5737
|
/**
|
|
5700
|
-
* Connects to a
|
|
5738
|
+
* Connects to a remote MCP server over any injected {@link MCPMessageTransportInterface},
|
|
5701
5739
|
* negotiates the modern revision, and exposes the server's tools as local
|
|
5702
5740
|
* {@link ToolInterface}s an agent can run.
|
|
5703
5741
|
*
|
|
5704
5742
|
* @remarks
|
|
5705
|
-
* - **The mirror of `MCPServer`.** The server
|
|
5706
|
-
* this client
|
|
5743
|
+
* - **The mirror of `MCPServer`.** The server dispatches requests over a tool registry;
|
|
5744
|
+
* this client issues them over a transport. `connect` probes `server/discover` and exposes
|
|
5707
5745
|
* the negotiated `version`; a legacy peer requires an explicit transport adapter.
|
|
5708
5746
|
* `tools()` lists the remote tools and wraps each as a
|
|
5709
5747
|
* local {@link ToolInterface} whose `execute` calls back through `call`; `call` runs a
|
|
5710
5748
|
* remote `tools/call` and reports the arm the peer answered with — a value, a durable
|
|
5711
5749
|
* task, or a request for more input (a remote `isError: true` throws locally, so an
|
|
5712
5750
|
* agent's {@link import('@orkestrel/tool').ToolManagerInterface} isolates it into a
|
|
5713
|
-
* `success: false` result
|
|
5751
|
+
* `success: false` result exactly like a local throw). A wrapped tool cannot hand an agent
|
|
5714
5752
|
* a deferred answer, so a non-`'complete'` arm throws there.
|
|
5715
5753
|
* - **Request↔response correlation.** Each request is tagged with a monotonic numeric
|
|
5716
5754
|
* `id` ({@link #nextId}); a single transport `message` subscription resolves / rejects
|
|
@@ -5718,9 +5756,9 @@ var MCPTaskClient = class {
|
|
|
5718
5756
|
* every pending request because the peer could not identify which request failed. A
|
|
5719
5757
|
* server-initiated message is re-surfaced on the `notification` event, except a progress
|
|
5720
5758
|
* frame claimed by the request that asked
|
|
5721
|
-
* for it; a
|
|
5759
|
+
* for it; a response correlating to nothing pending is discarded, because the request it
|
|
5722
5760
|
* answers has already settled.
|
|
5723
|
-
* - **Per-request cancellation.** `call`'s `options.signal` withdraws
|
|
5761
|
+
* - **Per-request cancellation.** `call`'s `options.signal` withdraws one caller from one
|
|
5724
5762
|
* request: the pending entry rejects on every carrier, and `notifications/cancelled` goes
|
|
5725
5763
|
* out only where the transport declares itself duplex — the dated revision defines no
|
|
5726
5764
|
* client-to-server notification over Streamable HTTP, where closing the response stream
|
|
@@ -5737,7 +5775,7 @@ var MCPTaskClient = class {
|
|
|
5737
5775
|
* discovery probe uses that same configured deadline, so a silent peer cannot hold
|
|
5738
5776
|
* negotiation indefinitely.
|
|
5739
5777
|
* `AbortSignal.timeout` (never a raw `setTimeout`) rejects only that pending request, and the
|
|
5740
|
-
* same deadline bounds the
|
|
5778
|
+
* same deadline bounds the wait on the transport's `close`, the one wait no drain and no signal
|
|
5741
5779
|
* can reach. It bounds the wait rather than the close, which keeps running, so a retry joins it
|
|
5742
5780
|
* instead of shutting one connection down twice.
|
|
5743
5781
|
* - **Transport-agnostic.** Imports only core siblings (JSON-RPC + the tool vocabulary);
|
|
@@ -6271,7 +6309,7 @@ var MCPClient = class {
|
|
|
6271
6309
|
//#endregion
|
|
6272
6310
|
//#region src/core/transports/HTTPClientTransport.ts
|
|
6273
6311
|
/**
|
|
6274
|
-
* Drives a
|
|
6312
|
+
* Drives a remote Streamable-HTTP MCP server over `fetch` — a client
|
|
6275
6313
|
* {@link MCPMessageTransportInterface} for the Model Context Protocol, the egress mirror of
|
|
6276
6314
|
* the server's `createMCPRoutes`.
|
|
6277
6315
|
*
|
|
@@ -6283,7 +6321,7 @@ var MCPClient = class {
|
|
|
6283
6321
|
* class, so a reply reaches a page and a Node process through the same decode.
|
|
6284
6322
|
* - **Request/response over `fetch`.** `send(message)` POSTs the JSON-serialized
|
|
6285
6323
|
* message to `options.url` with `content-type: application/json` and an
|
|
6286
|
-
* `Accept` of
|
|
6324
|
+
* `Accept` of both `application/json` and `text/event-stream` (so the server may
|
|
6287
6325
|
* answer with either framing) — plus any `options.headers` (for example, an `Authorization`
|
|
6288
6326
|
* bearer). It then decodes the reply and emits each decoded {@link JSONRPCMessage} on
|
|
6289
6327
|
* the `message` event the {@link import('@orkestrel/mcp').MCPClientInterface} subscribes
|
|
@@ -6296,12 +6334,12 @@ var MCPClient = class {
|
|
|
6296
6334
|
* Accepted (a notification) carries no body and emits nothing.
|
|
6297
6335
|
* - **Session and protocol headers.** `start()` is a no-op (a
|
|
6298
6336
|
* request/response transport opens no long-lived connection). The
|
|
6299
|
-
* `mcp-session-id` response header, when a
|
|
6300
|
-
* `initialize`), is captured into `session` and then
|
|
6301
|
-
* `mcp-session-id` request header on every
|
|
6337
|
+
* `mcp-session-id` response header, when a stateful server sends one (on
|
|
6338
|
+
* `initialize`), is captured into `session` and then echoed as the
|
|
6339
|
+
* `mcp-session-id` request header on every subsequent request — so an
|
|
6302
6340
|
* `MCPClient` passes a stateful server's session validation. The
|
|
6303
6341
|
* initialize result's `protocolVersion` is likewise captured, but only
|
|
6304
|
-
* when it is a
|
|
6342
|
+
* when it is a supported value, and echoed as `mcp-protocol-version` alone on
|
|
6305
6343
|
* subsequent legacy requests. Modern requests instead derive protocol and method
|
|
6306
6344
|
* headers from the message, plus the name header only for `tools/call` — carried in the
|
|
6307
6345
|
* protocol's Base64 sentinel form whenever the tool name cannot ride as plain ASCII.
|
|
@@ -6309,11 +6347,11 @@ var MCPClient = class {
|
|
|
6309
6347
|
* `close()` clears the captured protocol so a reconnect's `initialize`
|
|
6310
6348
|
* POST is headerless; the captured `session` persists across `close()`.
|
|
6311
6349
|
* - **`close()` releases what is in flight.** Every `fetch` this transport still has open is
|
|
6312
|
-
*
|
|
6350
|
+
* aborted, which cancels the response body a `send` is reading — an SSE reply the server
|
|
6313
6351
|
* never ends would otherwise outlive the transport, with nothing left able to reach it. The
|
|
6314
6352
|
* aborted read surfaces on `error` and the `send` reporting it resolves. `close()` is
|
|
6315
6353
|
* idempotent (one `close` event per connected lifetime), and `start()` opens the next one.
|
|
6316
|
-
* - **Total at the boundary, and a non-success reply
|
|
6354
|
+
* - **Total at the boundary, and a non-success reply rejects.** Every reply is narrowed
|
|
6317
6355
|
* (`parseJSONRPCMessage`, the SSE decoder). A non-message success reply is dropped, never
|
|
6318
6356
|
* asserted. A non-success reply that carries no valid JSON-RPC message rejects `send` with
|
|
6319
6357
|
* an error naming its HTTP status and body shape — the peer answered, and answering the
|
|
@@ -6503,20 +6541,35 @@ var HTTPClientTransport = class {
|
|
|
6503
6541
|
* {@link import('@orkestrel/emitter').EmitterHooks} (see {@link MCPServerOptions})
|
|
6504
6542
|
* @returns A working {@link MCPServerInterface}
|
|
6505
6543
|
*
|
|
6506
|
-
* @example
|
|
6544
|
+
* @example Expose a tool registry over MCP
|
|
6507
6545
|
* ```ts
|
|
6508
6546
|
* import { createMCPServer } from '@orkestrel/mcp'
|
|
6509
6547
|
* import { createTool, createToolManager } from '@orkestrel/tool'
|
|
6510
6548
|
*
|
|
6511
6549
|
* const tools = createToolManager()
|
|
6550
|
+
* tools.add(
|
|
6551
|
+
* createTool({
|
|
6552
|
+
* name: 'search',
|
|
6553
|
+
* description: 'Search the docs',
|
|
6554
|
+
* execute: (a) => find(String(a.query)),
|
|
6555
|
+
* }),
|
|
6556
|
+
* )
|
|
6512
6557
|
* tools.add(createTool({ name: 'add', execute: (a) => Number(a.x) + Number(a.y) }))
|
|
6513
6558
|
*
|
|
6514
|
-
* const server = createMCPServer({ identity: { name: '
|
|
6559
|
+
* const server = createMCPServer({ identity: { name: 'docs', version: '1.0.0' }, tools })
|
|
6515
6560
|
* server.emitter.on('request', (method, id) => log(method, id))
|
|
6516
6561
|
*
|
|
6517
|
-
* // A transport
|
|
6518
|
-
* const
|
|
6519
|
-
*
|
|
6562
|
+
* // A transport reads a framed message string and writes the reply:
|
|
6563
|
+
* for await (const message of transport) {
|
|
6564
|
+
* const reply = await server.handle(message)
|
|
6565
|
+
* if (reply !== undefined) await transport.send(reply) // a notification has no reply
|
|
6566
|
+
* }
|
|
6567
|
+
*
|
|
6568
|
+
* // `handle` also answers one message string on its own:
|
|
6569
|
+
* const listed = await server.handle(
|
|
6570
|
+
* '{"jsonrpc":"2.0","method":"tools/list","id":1,"params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}',
|
|
6571
|
+
* )
|
|
6572
|
+
* // listed → '{"jsonrpc":"2.0","id":1,"result":{"tools":[{"name":"search","inputSchema":{"type":"object"},"description":"Search the docs"},{"name":"add","inputSchema":{"type":"object"}}],"resultType":"complete","ttlMs":60000,"cacheScope":"private","_meta":{"io.modelcontextprotocol/serverInfo":{"name":"docs","version":"1.0.0"}}}}'
|
|
6520
6573
|
* ```
|
|
6521
6574
|
*/
|
|
6522
6575
|
function createMCPServer(options) {
|
|
@@ -6525,6 +6578,10 @@ function createMCPServer(options) {
|
|
|
6525
6578
|
/**
|
|
6526
6579
|
* Decorates one MCP server with the fixed legacy method translation.
|
|
6527
6580
|
*
|
|
6581
|
+
* @remarks
|
|
6582
|
+
* Adds support for the `2025-11-25` and `2025-06-18` legacy revisions. Removing this one
|
|
6583
|
+
* decorator removes that legacy surface while leaving the modern dispatcher unchanged.
|
|
6584
|
+
*
|
|
6528
6585
|
* @param server - The sole modern dispatcher and handshake identity source
|
|
6529
6586
|
* @returns A dispatcher accepting both modern and legacy invocations
|
|
6530
6587
|
*/
|
|
@@ -6535,14 +6592,14 @@ function createMCPLegacy(server) {
|
|
|
6535
6592
|
});
|
|
6536
6593
|
}
|
|
6537
6594
|
/**
|
|
6538
|
-
* Creates a transport-agnostic Model Context Protocol
|
|
6595
|
+
* Creates a transport-agnostic Model Context Protocol client — connects to a remote
|
|
6539
6596
|
* MCP server over an injected {@link import('./types.js').MCPMessageTransportInterface},
|
|
6540
6597
|
* negotiates the modern revision through `server/discover`, and exposes the server's tools as local
|
|
6541
6598
|
* {@link import('@orkestrel/tool').ToolInterface}s an agent can run.
|
|
6542
6599
|
*
|
|
6543
6600
|
* @remarks
|
|
6544
6601
|
* The egress mirror of {@link createMCPServer}: where the server exposes a local tool
|
|
6545
|
-
* registry over MCP, the client
|
|
6602
|
+
* registry over MCP, the client uses a remote server's tools. `connect()` discovers,
|
|
6546
6603
|
* validates, and exposes the negotiated modern protocol; a legacy peer requires
|
|
6547
6604
|
* {@link createMCPLegacyClientTransport}. `tools()` lists + wraps the remote
|
|
6548
6605
|
* tools (each `execute` calls back over the wire),
|
|
@@ -6552,7 +6609,7 @@ function createMCPLegacy(server) {
|
|
|
6552
6609
|
* `fetch`) lives in the published server environment; the client itself is provider-agnostic. Subscribe
|
|
6553
6610
|
* to `connect` / `disconnect` / `notification` through `client.emitter.on(...)`.
|
|
6554
6611
|
*
|
|
6555
|
-
* @param options - `transport` (the carrier;
|
|
6612
|
+
* @param options - `transport` (the carrier; required), an optional `identity`
|
|
6556
6613
|
* (the client identity), `timeout` (the per-request deadline), and the reserved `on`
|
|
6557
6614
|
* {@link import('@orkestrel/emitter').EmitterHooks} (see {@link MCPClientOptions})
|
|
6558
6615
|
* @returns A working {@link MCPClientInterface}
|
|
@@ -6598,7 +6655,7 @@ function createMCPLegacyClientTransport(transport, options) {
|
|
|
6598
6655
|
* existing shape.
|
|
6599
6656
|
*
|
|
6600
6657
|
* @remarks
|
|
6601
|
-
* Hand the
|
|
6658
|
+
* Hand the result to `createMCPClient({ transport })`, then pass the same
|
|
6602
6659
|
* `transport` to {@link import('./helpers.js').bindClient} to complete the inbound
|
|
6603
6660
|
* wiring: `send` serializes each outbound {@link JSONRPCMessage} and writes it through
|
|
6604
6661
|
* `transport.send`; `close` closes the underlying
|
|
@@ -6606,10 +6663,10 @@ function createMCPLegacyClientTransport(transport, options) {
|
|
|
6606
6663
|
* it is handed in — there is no separate connect step at this layer); `session` is
|
|
6607
6664
|
* always `undefined` (session correlation is a higher-level concern the duplex port
|
|
6608
6665
|
* does not carry); and `duplex` is always `true`, because carrying frames in both
|
|
6609
|
-
* directions at any moment is exactly what the adapted port is — a claim
|
|
6666
|
+
* directions at any moment is exactly what the adapted port is — a claim driven over a real
|
|
6610
6667
|
* `MessageChannel` and a real scope pair (a client-initiated `notifications/cancelled`
|
|
6611
6668
|
* observed arriving at the peer) rather than read back off this literal. The literal is
|
|
6612
|
-
* true of the
|
|
6669
|
+
* true of the port, and stays true only while the port has a peer: close the far half and
|
|
6613
6670
|
* this transport still declares `true` while carrying nothing, which is the one thing a
|
|
6614
6671
|
* per-carrier declaration cannot express. Inbound delivery (`emitter`'s `message` / `close` events) is
|
|
6615
6672
|
* `bindClient`'s job, not this factory's — the returned object exposes a `message`-
|