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