@orkestrel/mcp 0.0.27 → 0.0.28
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 +1 -1
- package/dist/src/browser/index.d.ts +135 -273
- package/dist/src/browser/index.js +128 -431
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/core/index.cjs +617 -200
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +881 -547
- package/dist/src/core/index.d.ts +881 -547
- package/dist/src/core/index.js +606 -200
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +275 -591
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +216 -343
- package/dist/src/server/index.d.ts +216 -343
- package/dist/src/server/index.js +269 -576
- package/dist/src/server/index.js.map +1 -1
- package/package.json +22 -22
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
let _orkestrel_sse = require("@orkestrel/sse");
|
|
3
2
|
let _src_core = require("../core/index.cjs");
|
|
4
3
|
let _orkestrel_contract = require("@orkestrel/contract");
|
|
5
4
|
let _orkestrel_server = require("@orkestrel/server");
|
|
@@ -12,35 +11,14 @@ let _orkestrel_process_server = require("@orkestrel/process/server");
|
|
|
12
11
|
let _orkestrel_process = require("@orkestrel/process");
|
|
13
12
|
let node_stream = require("node:stream");
|
|
14
13
|
//#region src/server/constants.ts
|
|
15
|
-
/**
|
|
16
|
-
* The Streamable-HTTP transport header that carries the MCP session id. When a {@link
|
|
17
|
-
* import('./middlewares.js').createMCPSession} middleware is mounted, it SETS this header on
|
|
18
|
-
* the `initialize` response (the minted id) and READS it on every subsequent request
|
|
19
|
-
* (validating the session); the stateless `createMCPRoutes` default neither sets nor reads it.
|
|
20
|
-
*/
|
|
21
|
-
var MCP_SESSION_HEADER = "mcp-session-id";
|
|
22
|
-
/**
|
|
23
|
-
* The Streamable-HTTP transport header carrying the negotiated MCP protocol version
|
|
24
|
-
* on every post-initialize client request.
|
|
25
|
-
*
|
|
26
|
-
* @remarks
|
|
27
|
-
* Required by MCP 2025-06-18 after initialization. Both HTTP client transports
|
|
28
|
-
* capture the initialize result's `protocolVersion` and send it on subsequent
|
|
29
|
-
* requests; `createMCPRoutes` rejects a present unsupported value before dispatch.
|
|
30
|
-
*/
|
|
31
|
-
var MCP_PROTOCOL_VERSION_HEADER = "mcp-protocol-version";
|
|
32
|
-
/** The modern Streamable-HTTP request header carrying the JSON-RPC method name. */
|
|
33
|
-
var MCP_METHOD_HEADER = "mcp-method";
|
|
34
|
-
/** The modern Streamable-HTTP request header carrying a named method's target. */
|
|
35
|
-
var MCP_NAME_HEADER = "mcp-name";
|
|
36
|
-
/** The reverse-proxy response header controlling buffering of an SSE response. */
|
|
14
|
+
/** Names the reverse-proxy response header controlling buffering of an SSE response. */
|
|
37
15
|
var SSE_BUFFERING_HEADER = "x-accel-buffering";
|
|
38
|
-
/**
|
|
16
|
+
/** Names the `X-Accel-Buffering` value that disables reverse-proxy buffering. */
|
|
39
17
|
var SSE_BUFFERING_DISABLED = "no";
|
|
40
|
-
/**
|
|
18
|
+
/** Names the default request path `createMCPRoutes` mounts the transport's `POST` route at. */
|
|
41
19
|
var DEFAULT_MCP_PATH = "/mcp";
|
|
42
20
|
/**
|
|
43
|
-
*
|
|
21
|
+
* Sets the default interval in milliseconds between SSE keepalive comments on held-open MCP
|
|
44
22
|
* responses.
|
|
45
23
|
*
|
|
46
24
|
* @remarks
|
|
@@ -48,34 +26,22 @@ var DEFAULT_MCP_PATH = "/mcp";
|
|
|
48
26
|
* client detection and staying comfortably inside common intermediary idle windows.
|
|
49
27
|
*/
|
|
50
28
|
var DEFAULT_MCP_KEEPALIVE_INTERVAL = 15e3;
|
|
51
|
-
/**
|
|
29
|
+
/** Names the comment text written by the held-open MCP response keepalive. */
|
|
52
30
|
var SSE_KEEPALIVE_COMMENT = "keepalive";
|
|
53
31
|
/**
|
|
54
|
-
*
|
|
55
|
-
* client in `Sec-WebSocket-Protocol`, echoed by the server in its `101` handshake.
|
|
56
|
-
*
|
|
57
|
-
* @remarks
|
|
58
|
-
* `createWebSocketServer` echoes it in the upgrade response and `createWebSocketClientTransport`
|
|
59
|
-
* requests it, so an MCP WebSocket endpoint is distinguishable from any other WebSocket on the
|
|
60
|
-
* same path. The default WebSocket upgrade path is {@link DEFAULT_MCP_PATH} (the same `'/mcp'`
|
|
61
|
-
* the HTTP transport mounts at) — the upgrade is selected by the `Upgrade: websocket` header,
|
|
62
|
-
* not a separate path.
|
|
63
|
-
*/
|
|
64
|
-
var MCP_WEBSOCKET_SUBPROTOCOL = "mcp";
|
|
65
|
-
/**
|
|
66
|
-
* The default capacity of a session's FOLDED resumable event log (the per-{@link
|
|
32
|
+
* Sets the default capacity of a session's FOLDED resumable event log (the per-{@link
|
|
67
33
|
* import('./MCPSession.js').MCPSession} replay log) — the maximum number of pushed
|
|
68
34
|
* server→client messages retained for replay before the OLDEST is evicted.
|
|
69
35
|
*
|
|
70
36
|
* @remarks
|
|
71
37
|
* Bounds the replay log's memory: only the most-recent {@link DEFAULT_MCP_SESSION_CAPACITY}
|
|
72
38
|
* pushes are retained, so a client reconnecting with a `Last-Event-ID` older than that window
|
|
73
|
-
* replays nothing (its cursor fell off the back). Override
|
|
74
|
-
* for a deeper / shallower window.
|
|
39
|
+
* replays nothing (its cursor fell off the back). Override through the `session` group of
|
|
40
|
+
* `createMCPSession`'s options (`session.capacity`) for a deeper / shallower window.
|
|
75
41
|
*/
|
|
76
42
|
var DEFAULT_MCP_SESSION_CAPACITY = 1024;
|
|
77
43
|
/**
|
|
78
|
-
*
|
|
44
|
+
* Sets the default per-event idle lifetime (ms) of a session's folded resumable event log — an
|
|
79
45
|
* entry older than this is lazily evicted on the next access (no background timer), bounding
|
|
80
46
|
* how far back a reconnecting client may replay.
|
|
81
47
|
*
|
|
@@ -86,7 +52,7 @@ var DEFAULT_MCP_SESSION_CAPACITY = 1024;
|
|
|
86
52
|
*/
|
|
87
53
|
var DEFAULT_MCP_SESSION_TTL = 3e5;
|
|
88
54
|
/**
|
|
89
|
-
*
|
|
55
|
+
* Sets the default bound in milliseconds on one unconfirmed write to a stdio client transport's
|
|
90
56
|
* child `stdin` — the `delivery` a `createStdioClientTransport` caller who supplies none gets.
|
|
91
57
|
*
|
|
92
58
|
* @remarks
|
|
@@ -100,37 +66,6 @@ var DEFAULT_MCP_DELIVERY = 1e4;
|
|
|
100
66
|
//#endregion
|
|
101
67
|
//#region src/server/helpers.ts
|
|
102
68
|
/**
|
|
103
|
-
* Builds the error for a non-success HTTP response that carried no JSON-RPC message.
|
|
104
|
-
*
|
|
105
|
-
* @param response - The response whose status is reported
|
|
106
|
-
* @param type - The response's content type, or an empty string when absent
|
|
107
|
-
* @returns An error naming the HTTP status and unsupported response shape
|
|
108
|
-
*
|
|
109
|
-
* @example
|
|
110
|
-
* ```ts
|
|
111
|
-
* const error = buildResponseError(new Response('', { status: 500 }), '')
|
|
112
|
-
* ```
|
|
113
|
-
*/
|
|
114
|
-
function buildResponseError(response, type) {
|
|
115
|
-
if (type.includes("application/json")) return /* @__PURE__ */ new Error(`HTTP ${response.status} response contained an application/json body that was not a JSON-RPC message`);
|
|
116
|
-
if (type.includes("text/event-stream")) return /* @__PURE__ */ new Error(`HTTP ${response.status} response contained a text/event-stream body without a JSON-RPC message`);
|
|
117
|
-
const shape = type === "" ? "a body without a content type" : `an unsupported '${type}' body`;
|
|
118
|
-
return /* @__PURE__ */ new Error(`HTTP ${response.status} response contained ${shape}`);
|
|
119
|
-
}
|
|
120
|
-
/**
|
|
121
|
-
* Creates a readable stream from its pull and cancellation behaviours.
|
|
122
|
-
*
|
|
123
|
-
* @param pull - The behaviour that supplies the stream's next chunk
|
|
124
|
-
* @param cancel - The behaviour that releases the stream after consumer cancellation
|
|
125
|
-
* @returns A readable stream backed by the supplied behaviours
|
|
126
|
-
*/
|
|
127
|
-
function createReadableStream(pull, cancel) {
|
|
128
|
-
return new ReadableStream({
|
|
129
|
-
pull,
|
|
130
|
-
cancel
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
/**
|
|
134
69
|
* Pumps a controlled held-open exchange onto an open SSE stream — one `data:` event per
|
|
135
70
|
* notification in order, then the terminating response — and END the exchange however the
|
|
136
71
|
* pump leaves.
|
|
@@ -158,7 +93,7 @@ function createReadableStream(pull, cancel) {
|
|
|
158
93
|
* ```ts
|
|
159
94
|
* const answer = await mcp.dispatch(invocation, { signal: disconnect.signal })
|
|
160
95
|
* if (answer !== undefined && Symbol.asyncIterator in answer) {
|
|
161
|
-
* const sse =
|
|
96
|
+
* const sse = createStream()
|
|
162
97
|
* queueMicrotask(() => void sendEventStream(answer, sse))
|
|
163
98
|
* }
|
|
164
99
|
* ```
|
|
@@ -180,7 +115,7 @@ async function sendEventStream(stream, sse) {
|
|
|
180
115
|
}
|
|
181
116
|
}
|
|
182
117
|
/**
|
|
183
|
-
*
|
|
118
|
+
* Checks whether the request's `Accept` header opts into a Server-Sent-Events response.
|
|
184
119
|
*
|
|
185
120
|
* @remarks
|
|
186
121
|
* Reads the fetch-standard `Request.headers.get('accept')` and returns `true` when it
|
|
@@ -190,7 +125,7 @@ async function sendEventStream(stream, sse) {
|
|
|
190
125
|
* — an absent / unmatched header returns `false`.
|
|
191
126
|
*
|
|
192
127
|
* @param request - The fetch-standard `Request`
|
|
193
|
-
* @returns
|
|
128
|
+
* @returns True if the client `Accept`s `text/event-stream`; false otherwise
|
|
194
129
|
*/
|
|
195
130
|
function acceptsEventStream(request) {
|
|
196
131
|
const accept = request.headers.get("accept");
|
|
@@ -198,7 +133,7 @@ function acceptsEventStream(request) {
|
|
|
198
133
|
return accept.toLowerCase().includes("text/event-stream");
|
|
199
134
|
}
|
|
200
135
|
/**
|
|
201
|
-
*
|
|
136
|
+
* Checks whether an HTTP request satisfies the endpoint's origin gate.
|
|
202
137
|
*
|
|
203
138
|
* @remarks
|
|
204
139
|
* Validation is enabled by default. A request without `Origin` is allowed. A canonical origin
|
|
@@ -209,7 +144,7 @@ function acceptsEventStream(request) {
|
|
|
209
144
|
*
|
|
210
145
|
* @param request - The fetch-standard request to validate
|
|
211
146
|
* @param options - Shared origin validation and delegation options
|
|
212
|
-
* @returns
|
|
147
|
+
* @returns True if the request may reach MCP dispatch; false otherwise
|
|
213
148
|
*/
|
|
214
149
|
function allowsOrigin(request, options) {
|
|
215
150
|
if (options?.enabled === false) return true;
|
|
@@ -240,7 +175,7 @@ function allowsOrigin(request, options) {
|
|
|
240
175
|
* @returns The session id, or `undefined` when the header is absent
|
|
241
176
|
*/
|
|
242
177
|
function readSessionHeader(request) {
|
|
243
|
-
const id = request.headers.get(MCP_SESSION_HEADER);
|
|
178
|
+
const id = request.headers.get(_src_core.MCP_SESSION_HEADER);
|
|
244
179
|
return id === null ? void 0 : id;
|
|
245
180
|
}
|
|
246
181
|
/**
|
|
@@ -280,65 +215,6 @@ function rejectUnknownSession() {
|
|
|
280
215
|
return Response.json((0, _src_core.buildJSONRPCError)(void 0, _src_core.JSONRPC_INVALID_REQUEST, "Session not found"), { status: 404 });
|
|
281
216
|
}
|
|
282
217
|
/**
|
|
283
|
-
* Decodes a `fetch` Response's Server-Sent-Events body into the JSON-RPC messages it
|
|
284
|
-
* carried — the CLIENT-side inverse of the server's Streamable-HTTP SSE response.
|
|
285
|
-
*
|
|
286
|
-
* @remarks
|
|
287
|
-
* Reads the whole `response.body` stream chunk-by-chunk through a `TextDecoder({
|
|
288
|
-
* stream: true })` (handling a multi-byte char split across reads) and `@orkestrel/sse`'s
|
|
289
|
-
* {@link SSEParserInterface} (handling a partial line / in-progress event split across
|
|
290
|
-
* reads), then narrows each dispatched event's `data` to a {@link JSONRPCMessage} with
|
|
291
|
-
* `parseJSONRPCMessage` (so a non-message / non-JSON `data:` event is DROPPED, never
|
|
292
|
-
* thrown — total). It reuses the SAME `SSEParser` the server's `openStream` seam
|
|
293
|
-
* serializes against, so the wire round-trips. A `null` body (no stream) yields no
|
|
294
|
-
* messages; the {@link import('./transports/HTTPClientTransport.js').HTTPClientTransport}
|
|
295
|
-
* reads a request/response SSE reply (the server sends one `data:` event then ends), so
|
|
296
|
-
* this drains to completion.
|
|
297
|
-
*
|
|
298
|
-
* @param response - The SSE `fetch` Response to decode (its `body` is read to completion)
|
|
299
|
-
* @returns Every {@link JSONRPCMessage} the stream carried, in order
|
|
300
|
-
*/
|
|
301
|
-
async function readEventStream(response) {
|
|
302
|
-
const body = response.body;
|
|
303
|
-
if (body === null) return [];
|
|
304
|
-
const reader = body.getReader();
|
|
305
|
-
const decoder = new TextDecoder();
|
|
306
|
-
const parser = (0, _orkestrel_sse.createSSEParser)();
|
|
307
|
-
const messages = [];
|
|
308
|
-
try {
|
|
309
|
-
for (;;) {
|
|
310
|
-
const { done, value } = await reader.read();
|
|
311
|
-
if (done) break;
|
|
312
|
-
for (const event of parser.parse(decoder.decode(value, { stream: true }))) {
|
|
313
|
-
const message = decodeEvent(event.data);
|
|
314
|
-
if (message !== void 0) messages.push(message);
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
} finally {
|
|
318
|
-
reader.releaseLock();
|
|
319
|
-
}
|
|
320
|
-
return messages;
|
|
321
|
-
}
|
|
322
|
-
/**
|
|
323
|
-
* Decodes one SSE event's `data` string into a {@link JSONRPCMessage}, or `undefined`
|
|
324
|
-
* when it is not one — the per-event step {@link readEventStream} folds over.
|
|
325
|
-
*
|
|
326
|
-
* @remarks
|
|
327
|
-
* `JSON.parse`s the `data` (the server serializes the JSON-RPC envelope as the event's
|
|
328
|
-
* `data`) inside a try/catch and narrows the parsed value with `parseJSONRPCMessage`.
|
|
329
|
-
* Total: malformed JSON or a non-message value yields `undefined`, never throws.
|
|
330
|
-
*
|
|
331
|
-
* @param data - One SSE event's `data` payload
|
|
332
|
-
* @returns The decoded {@link JSONRPCMessage}, or `undefined`
|
|
333
|
-
*/
|
|
334
|
-
function decodeEvent(data) {
|
|
335
|
-
try {
|
|
336
|
-
return (0, _src_core.parseJSONRPCMessage)(JSON.parse(data));
|
|
337
|
-
} catch {
|
|
338
|
-
return;
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
/**
|
|
342
218
|
* Reads the path (without the query string) of a raw `node:http` protocol-upgrade request —
|
|
343
219
|
* the `createWebSocketServer` upgrade-path match.
|
|
344
220
|
*
|
|
@@ -420,16 +296,17 @@ function writeLine(output, line) {
|
|
|
420
296
|
}
|
|
421
297
|
/**
|
|
422
298
|
* Decodes and delivers each complete newline-framed line onto a {@link
|
|
423
|
-
*
|
|
299
|
+
* MCPMessageTransportEventMap} emitter — the shared per-chunk dispatch step both stdio
|
|
424
300
|
* transports run their framed lines through: the server transport frames with {@link
|
|
425
301
|
* extractLines}, the client transport takes its lines from the process supervisor.
|
|
426
302
|
*
|
|
427
303
|
* @remarks
|
|
428
|
-
* A blank line is skipped (a stray trailing newline). Every other line
|
|
429
|
-
*
|
|
430
|
-
* well-formed {@link JSONRPCMessage} emits `message`,
|
|
431
|
-
*
|
|
432
|
-
*
|
|
304
|
+
* A blank line is skipped (a stray trailing newline). Every other line runs through the
|
|
305
|
+
* shared {@link import('@orkestrel/mcp').deliverMessage} fold, the one inbound decode every
|
|
306
|
+
* transport in this package shares: a well-formed {@link JSONRPCMessage} emits `message`,
|
|
307
|
+
* unparsable text emits the caught parse error, and a well-formed non-message line emits
|
|
308
|
+
* `error` naming a non-JSON-RPC stdio line (total, never throws). Pure w.r.t. its own state
|
|
309
|
+
* — the emit is the caller-owned side effect.
|
|
433
310
|
*
|
|
434
311
|
* @param emitter - The transport's {@link EmitterInterface} to emit `message` / `error` onto
|
|
435
312
|
* @param lines - The complete lines to decode and deliver
|
|
@@ -437,93 +314,9 @@ function writeLine(output, line) {
|
|
|
437
314
|
function dispatchLines(emitter, lines) {
|
|
438
315
|
for (const line of lines) {
|
|
439
316
|
if (line.length === 0) continue;
|
|
440
|
-
|
|
441
|
-
if (message === void 0) {
|
|
442
|
-
emitter.emit("error", /* @__PURE__ */ new Error("non-JSON-RPC stdio line"));
|
|
443
|
-
continue;
|
|
444
|
-
}
|
|
445
|
-
emitter.emit("message", message);
|
|
317
|
+
(0, _src_core.deliverMessage)(emitter, line, "non-JSON-RPC stdio line");
|
|
446
318
|
}
|
|
447
319
|
}
|
|
448
|
-
/**
|
|
449
|
-
* Bridges a message-channel {@link MCPClientTransportInterface} (the shape the stdio and
|
|
450
|
-
* WebSocket SERVER transports already implement) into the environment-agnostic
|
|
451
|
-
* {@link import('@orkestrel/mcp').MCPTransportInterface} port — the adapter
|
|
452
|
-
* {@link import('./factories.js').createStdioServer} and {@link
|
|
453
|
-
* import('./factories.js').createWebSocketServer} pipe through `bindServer`, so the
|
|
454
|
-
* request/reply/error pump those factories used to hand-roll identically now
|
|
455
|
-
* lives ONCE in the core binder.
|
|
456
|
-
*
|
|
457
|
-
* @remarks
|
|
458
|
-
* `send` decodes the already-serialized reply string back to a {@link JSONRPCMessage}
|
|
459
|
-
* and writes it through `transport.send` (the same `JSON.stringify` the underlying
|
|
460
|
-
* transport already performs, so the wire bytes are unchanged). `listen` filters
|
|
461
|
-
* `transport`'s `message` event to INVOCATIONS ONLY — requests and notifications, never a
|
|
462
|
-
* stray response, exactly as the prior hand-rolled pumps did — and re-serializes each one
|
|
463
|
-
* back to a string for `bindServer`. `closed` bridges `transport`'s `close` event. `close`
|
|
464
|
-
* closes the underlying `transport`.
|
|
465
|
-
*
|
|
466
|
-
* @remarks A message crossing this bridge is decoded and re-encoded TWICE, and that is
|
|
467
|
-
* ACCEPTED rather than accidental. Inbound: the carrier already parsed the frame into a
|
|
468
|
-
* {@link JSONRPCMessage}, and `listen` re-serializes it so `bindServer` can decode it again
|
|
469
|
-
* under the server's own `limit`. Outbound: `bindServer` serialized the reply, `send` parses
|
|
470
|
-
* it back, and the carrier stringifies it once more. The cost is two extra `JSON.parse` /
|
|
471
|
-
* `JSON.stringify` round trips per message, paid to keep ONE pump in the core binder instead
|
|
472
|
-
* of a hand-rolled one per carrier. It is BOUNDED rather than unbounded because the binder
|
|
473
|
-
* decodes within `server.limit.message`, so an oversized frame is refused before the second
|
|
474
|
-
* decode rather than after it. Removing the cost means giving `MCPTransportInterface` a
|
|
475
|
-
* message-shaped face beside its string one, which every transport would then carry.
|
|
476
|
-
*
|
|
477
|
-
* @remarks Per {@link import('@orkestrel/mcp').MCPTransportInterface}, `listen`/`closed`
|
|
478
|
-
* each hold THE SINGLE current handler (a second call REPLACES the first, never adds).
|
|
479
|
-
* Because the underlying `transport.emitter` is ADD-based (`on` subscribes, never
|
|
480
|
-
* replaces), this bridge installs ONE stable emitter listener per event on first use
|
|
481
|
-
* and re-routes it to whichever handler is active (`undefined` while
|
|
482
|
-
* none is), so rebinding never double-dispatches.
|
|
483
|
-
*
|
|
484
|
-
* @remarks A response whose `result` serializes away (for example, `undefined`) is dropped by
|
|
485
|
-
* the message validators on the wire's decode side — an asymmetry the stdio/WS carrier
|
|
486
|
-
* shares with the streamable-HTTP face, because both round-trip through `JSON.stringify`
|
|
487
|
-
* / `JSON.parse` before re-validation.
|
|
488
|
-
*
|
|
489
|
-
* @param transport - The message-channel transport to bridge (stdio or WebSocket)
|
|
490
|
-
* @returns An {@link import('@orkestrel/mcp').MCPTransportInterface} `bindServer` can drive
|
|
491
|
-
*
|
|
492
|
-
* @example
|
|
493
|
-
* ```ts
|
|
494
|
-
* import { bindServer } from '@orkestrel/mcp'
|
|
495
|
-
*
|
|
496
|
-
* const transport = new StdioServerTransport(process.stdin, process.stdout)
|
|
497
|
-
* bindServer(mcp, bridgeMessageTransport(transport))
|
|
498
|
-
* ```
|
|
499
|
-
*/
|
|
500
|
-
function bridgeMessageTransport(transport) {
|
|
501
|
-
let onMessage;
|
|
502
|
-
let onClosed;
|
|
503
|
-
transport.emitter.on("message", (message) => {
|
|
504
|
-
if (!(0, _src_core.isJSONRPCInvocation)(message)) return;
|
|
505
|
-
onMessage?.(JSON.stringify(message));
|
|
506
|
-
});
|
|
507
|
-
transport.emitter.on("close", () => {
|
|
508
|
-
onClosed?.();
|
|
509
|
-
});
|
|
510
|
-
return {
|
|
511
|
-
async send(message) {
|
|
512
|
-
const decoded = decodeEvent(message);
|
|
513
|
-
if (decoded === void 0) return;
|
|
514
|
-
await transport.send(decoded);
|
|
515
|
-
},
|
|
516
|
-
listen(handler) {
|
|
517
|
-
onMessage = handler;
|
|
518
|
-
},
|
|
519
|
-
closed(handler) {
|
|
520
|
-
onClosed = handler;
|
|
521
|
-
},
|
|
522
|
-
async close() {
|
|
523
|
-
await transport.close();
|
|
524
|
-
}
|
|
525
|
-
};
|
|
526
|
-
}
|
|
527
320
|
//#endregion
|
|
528
321
|
//#region src/server/inferers.ts
|
|
529
322
|
/**
|
|
@@ -560,7 +353,7 @@ function inferHeaderTarget(request) {
|
|
|
560
353
|
}
|
|
561
354
|
}
|
|
562
355
|
/**
|
|
563
|
-
* Infers the first required MCP HTTP header
|
|
356
|
+
* Infers the first required MCP HTTP header a request's own body contradicts.
|
|
564
357
|
*
|
|
565
358
|
* @remarks
|
|
566
359
|
* A modern request derives its protocol, method, and name expectations from the JSON-RPC body,
|
|
@@ -569,12 +362,14 @@ function inferHeaderTarget(request) {
|
|
|
569
362
|
* {@link import('@orkestrel/mcp').decodeSentinel} before the comparison, so a peer that had
|
|
570
363
|
* to encode its value still matches; a sentinel whose payload is invalid decodes to nothing
|
|
571
364
|
* and therefore mismatches, which is how an invalid header value is refused. A legacy request
|
|
572
|
-
* body requires a protocol header after initialization
|
|
573
|
-
*
|
|
574
|
-
*
|
|
365
|
+
* body requires a protocol header after initialization. Messages name the expected value but
|
|
366
|
+
* never echo the client-supplied one.
|
|
367
|
+
*
|
|
368
|
+
* The expectation a LIVE SESSION supplies is a different rule over a different input, so it
|
|
369
|
+
* is {@link inferSessionHeaderIssue} rather than a second arm of this one.
|
|
575
370
|
*
|
|
576
371
|
* @param request - The HTTP request carrying the headers
|
|
577
|
-
* @param
|
|
372
|
+
* @param invocation - The parsed invocation body the expectations are derived from
|
|
578
373
|
* @returns The first header issue, or `undefined` when the applicable headers agree
|
|
579
374
|
*
|
|
580
375
|
* @example
|
|
@@ -583,30 +378,17 @@ function inferHeaderTarget(request) {
|
|
|
583
378
|
* issue?.header // 'Mcp-Method' when that field is absent or mismatched
|
|
584
379
|
* ```
|
|
585
380
|
*/
|
|
586
|
-
function inferHeaderIssue(request,
|
|
587
|
-
const protocol = request.headers.get(MCP_PROTOCOL_VERSION_HEADER);
|
|
588
|
-
if ((0,
|
|
589
|
-
if (protocol
|
|
590
|
-
header: "MCP-Protocol-Version",
|
|
591
|
-
reason: "missing",
|
|
592
|
-
message: `Required MCP-Protocol-Version header is missing; the active session uses '${reference}'.`
|
|
593
|
-
};
|
|
594
|
-
if (protocol !== reference) return {
|
|
595
|
-
header: "MCP-Protocol-Version",
|
|
596
|
-
reason: "mismatched",
|
|
597
|
-
message: `MCP-Protocol-Version header does not match the active session version '${reference}'.`
|
|
598
|
-
};
|
|
599
|
-
return;
|
|
600
|
-
}
|
|
601
|
-
if (!(0, _src_core.isModernRequest)(reference)) {
|
|
602
|
-
if ((0, _src_core.isInitializeRequest)(reference) || protocol !== null) return void 0;
|
|
381
|
+
function inferHeaderIssue(request, invocation) {
|
|
382
|
+
const protocol = request.headers.get(_src_core.MCP_PROTOCOL_VERSION_HEADER);
|
|
383
|
+
if (!(0, _src_core.isModernRequest)(invocation)) {
|
|
384
|
+
if ((0, _src_core.isInitializeRequest)(invocation) || protocol !== null) return void 0;
|
|
603
385
|
return {
|
|
604
386
|
header: "MCP-Protocol-Version",
|
|
605
387
|
reason: "missing",
|
|
606
388
|
message: `Required MCP-Protocol-Version header is missing; this server offers '${_src_core.MCP_HANDSHAKE_VERSION}'.`
|
|
607
389
|
};
|
|
608
390
|
}
|
|
609
|
-
const message =
|
|
391
|
+
const message = invocation;
|
|
610
392
|
const version = ((0, _orkestrel_contract.isRecord)(message.params?.["_meta"]) ? message.params["_meta"] : void 0)?.[_src_core.MCP_META_VERSION];
|
|
611
393
|
if (!(0, _orkestrel_contract.isString)(version)) return void 0;
|
|
612
394
|
if (protocol === null) return {
|
|
@@ -619,7 +401,7 @@ function inferHeaderIssue(request, reference) {
|
|
|
619
401
|
reason: "mismatched",
|
|
620
402
|
message: `MCP-Protocol-Version header does not match the request body version '${version}'.`
|
|
621
403
|
};
|
|
622
|
-
const method = request.headers.get(MCP_METHOD_HEADER);
|
|
404
|
+
const method = request.headers.get(_src_core.MCP_METHOD_HEADER);
|
|
623
405
|
if (method === null) return {
|
|
624
406
|
header: "Mcp-Method",
|
|
625
407
|
reason: "missing",
|
|
@@ -632,7 +414,7 @@ function inferHeaderIssue(request, reference) {
|
|
|
632
414
|
};
|
|
633
415
|
const target = inferHeaderTarget(message);
|
|
634
416
|
if (target === void 0) return void 0;
|
|
635
|
-
const header = request.headers.get(MCP_NAME_HEADER);
|
|
417
|
+
const header = request.headers.get(_src_core.MCP_NAME_HEADER);
|
|
636
418
|
if (header === null) return {
|
|
637
419
|
header: "Mcp-Name",
|
|
638
420
|
reason: "missing",
|
|
@@ -645,6 +427,40 @@ function inferHeaderIssue(request, reference) {
|
|
|
645
427
|
};
|
|
646
428
|
}
|
|
647
429
|
/**
|
|
430
|
+
* Infers the protocol header issue an active legacy session's pinned revision diagnoses.
|
|
431
|
+
*
|
|
432
|
+
* @remarks
|
|
433
|
+
* The session layer's rule, distinct from the body-derived one {@link inferHeaderIssue} owns:
|
|
434
|
+
* a live legacy session pinned its revision at `initialize`, so every later request on that
|
|
435
|
+
* session must name the same one. An absent header reads as `missing`, which the session
|
|
436
|
+
* middleware answers by SUPPLYING the pinned revision rather than refusing; a present header
|
|
437
|
+
* naming another revision reads as `mismatched` and is refused. The message names the session's
|
|
438
|
+
* revision and never echoes the client-supplied value.
|
|
439
|
+
*
|
|
440
|
+
* @param request - The HTTP request carrying the headers
|
|
441
|
+
* @param version - The legacy revision the active session pinned at `initialize`
|
|
442
|
+
* @returns The protocol header issue, or `undefined` when the header agrees
|
|
443
|
+
*
|
|
444
|
+
* @example
|
|
445
|
+
* ```ts
|
|
446
|
+
* const issue = inferSessionHeaderIssue(request, '2025-06-18')
|
|
447
|
+
* issue?.reason // 'missing' when the request carries no protocol header
|
|
448
|
+
* ```
|
|
449
|
+
*/
|
|
450
|
+
function inferSessionHeaderIssue(request, version) {
|
|
451
|
+
const protocol = request.headers.get(_src_core.MCP_PROTOCOL_VERSION_HEADER);
|
|
452
|
+
if (protocol === null) return {
|
|
453
|
+
header: "MCP-Protocol-Version",
|
|
454
|
+
reason: "missing",
|
|
455
|
+
message: `Required MCP-Protocol-Version header is missing; the active session uses '${version}'.`
|
|
456
|
+
};
|
|
457
|
+
if (protocol !== version) return {
|
|
458
|
+
header: "MCP-Protocol-Version",
|
|
459
|
+
reason: "mismatched",
|
|
460
|
+
message: `MCP-Protocol-Version header does not match the active session version '${version}'.`
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
648
464
|
* Infers the refusal one `tools/call` earns for a `Mcp-Param-*` header the body contradicts.
|
|
649
465
|
*
|
|
650
466
|
* @remarks
|
|
@@ -737,7 +553,7 @@ function inferStatus(response, era) {
|
|
|
737
553
|
return 200;
|
|
738
554
|
}
|
|
739
555
|
//#endregion
|
|
740
|
-
//#region src/server/
|
|
556
|
+
//#region src/server/HTTPDisconnect.ts
|
|
741
557
|
/**
|
|
742
558
|
* Composes one incoming HTTP request lifetime with one MCP-owned SSE response lifetime.
|
|
743
559
|
*
|
|
@@ -766,10 +582,10 @@ function inferStatus(response, era) {
|
|
|
766
582
|
* @example
|
|
767
583
|
* ```ts
|
|
768
584
|
* import { HTTPDisconnect } from '@orkestrel/mcp/server'
|
|
769
|
-
* import {
|
|
585
|
+
* import { createStream } from '@orkestrel/server'
|
|
770
586
|
*
|
|
771
587
|
* const disconnect = new HTTPDisconnect(request.signal, { interval: 15_000 })
|
|
772
|
-
* const stream =
|
|
588
|
+
* const stream = createStream()
|
|
773
589
|
* const response = disconnect.bridge(stream)
|
|
774
590
|
* ```
|
|
775
591
|
*/
|
|
@@ -778,6 +594,9 @@ var HTTPDisconnect = class {
|
|
|
778
594
|
#lifecycle = new AbortController();
|
|
779
595
|
#interval;
|
|
780
596
|
#signal;
|
|
597
|
+
#pull = (controller) => this.#pump(controller);
|
|
598
|
+
#cancel = (reason) => this.#discard(reason);
|
|
599
|
+
#reader;
|
|
781
600
|
#timer;
|
|
782
601
|
#bridged = false;
|
|
783
602
|
#pulling = false;
|
|
@@ -794,8 +613,8 @@ var HTTPDisconnect = class {
|
|
|
794
613
|
this.#signal = AbortSignal.any([signal, this.#response.signal]);
|
|
795
614
|
}
|
|
796
615
|
/**
|
|
797
|
-
*
|
|
798
|
-
* its graceful completion.
|
|
616
|
+
* Returns the signal aborted by the incoming request, or by any end of this response that
|
|
617
|
+
* is not its graceful completion.
|
|
799
618
|
*
|
|
800
619
|
* @returns The composed lifecycle signal
|
|
801
620
|
*/
|
|
@@ -821,7 +640,7 @@ var HTTPDisconnect = class {
|
|
|
821
640
|
const response = stream.response;
|
|
822
641
|
const body = response.body;
|
|
823
642
|
if (body === null) throw new Error("MCP SSE response has no body");
|
|
824
|
-
|
|
643
|
+
this.#reader = body.getReader();
|
|
825
644
|
this.#timer = setInterval(() => {
|
|
826
645
|
if (stream.closed) {
|
|
827
646
|
if (!this.#pulling) this.#abort();
|
|
@@ -833,29 +652,39 @@ var HTTPDisconnect = class {
|
|
|
833
652
|
});
|
|
834
653
|
if (this.#signal.aborted) this.#release();
|
|
835
654
|
else if (stream.closed) this.#abort();
|
|
836
|
-
return new Response(
|
|
837
|
-
this.#
|
|
838
|
-
|
|
839
|
-
const chunk = await reader.read();
|
|
840
|
-
if (chunk.done) {
|
|
841
|
-
this.#release();
|
|
842
|
-
controller.close();
|
|
843
|
-
} else controller.enqueue(chunk.value);
|
|
844
|
-
} catch (error) {
|
|
845
|
-
this.#abort();
|
|
846
|
-
controller.error(error);
|
|
847
|
-
} finally {
|
|
848
|
-
this.#pulling = false;
|
|
849
|
-
}
|
|
850
|
-
}, async (reason) => {
|
|
851
|
-
this.#abort();
|
|
852
|
-
await reader.cancel(reason);
|
|
655
|
+
return new Response(new ReadableStream({
|
|
656
|
+
pull: this.#pull,
|
|
657
|
+
cancel: this.#cancel
|
|
853
658
|
}), {
|
|
854
659
|
status: response.status,
|
|
855
660
|
statusText: response.statusText,
|
|
856
661
|
headers: response.headers
|
|
857
662
|
});
|
|
858
663
|
}
|
|
664
|
+
async #pump(controller) {
|
|
665
|
+
const reader = this.#reader;
|
|
666
|
+
if (reader === void 0) {
|
|
667
|
+
controller.close();
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
this.#pulling = true;
|
|
671
|
+
try {
|
|
672
|
+
const chunk = await reader.read();
|
|
673
|
+
if (chunk.done) {
|
|
674
|
+
this.#release();
|
|
675
|
+
controller.close();
|
|
676
|
+
} else controller.enqueue(chunk.value);
|
|
677
|
+
} catch (error) {
|
|
678
|
+
this.#abort();
|
|
679
|
+
controller.error(error);
|
|
680
|
+
} finally {
|
|
681
|
+
this.#pulling = false;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
async #discard(reason) {
|
|
685
|
+
this.#abort();
|
|
686
|
+
await this.#reader?.cancel(reason);
|
|
687
|
+
}
|
|
859
688
|
#release() {
|
|
860
689
|
if (this.#timer !== void 0) {
|
|
861
690
|
clearInterval(this.#timer);
|
|
@@ -922,17 +751,13 @@ function createMCPPostHandler(mcp, options) {
|
|
|
922
751
|
} catch {
|
|
923
752
|
return Response.json((0, _src_core.buildJSONRPCError)(void 0, _src_core.JSONRPC_PARSE_ERROR, "Parse error"), { status: 400 });
|
|
924
753
|
}
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
parsed = JSON.parse(text);
|
|
928
|
-
} catch {
|
|
929
|
-
return Response.json((0, _src_core.buildJSONRPCError)(void 0, _src_core.JSONRPC_PARSE_ERROR, "Parse error"), { status: 400 });
|
|
930
|
-
}
|
|
754
|
+
const parsed = (0, _orkestrel_contract.parseJSON)(text);
|
|
755
|
+
if (parsed === void 0) return Response.json((0, _src_core.buildJSONRPCError)(void 0, _src_core.JSONRPC_PARSE_ERROR, "Parse error"), { status: 400 });
|
|
931
756
|
const invocation = (0, _src_core.parseJSONRPCMessage)(parsed);
|
|
932
757
|
if (invocation === void 0 || !("method" in invocation)) return Response.json((0, _src_core.buildJSONRPCError)(void 0, _src_core.JSONRPC_INVALID_REQUEST, "Invalid Request"), { status: 400 });
|
|
933
|
-
const era = (0, _src_core.
|
|
758
|
+
const era = (0, _src_core.inferRequestEra)(invocation);
|
|
934
759
|
const id = invocation.id;
|
|
935
|
-
const protocol = request.headers.get(MCP_PROTOCOL_VERSION_HEADER);
|
|
760
|
+
const protocol = request.headers.get(_src_core.MCP_PROTOCOL_VERSION_HEADER);
|
|
936
761
|
if (era === "modern" || (0, _src_core.isMCPModernVersion)(protocol)) {
|
|
937
762
|
if ((0, _src_core.parseRequestContext)(invocation) === void 0) return Response.json((0, _src_core.buildJSONRPCError)(id, _src_core.JSONRPC_INVALID_PARAMS, "Invalid params: malformed modern request metadata"), { status: 400 });
|
|
938
763
|
}
|
|
@@ -982,7 +807,7 @@ function createMCPPostHandler(mcp, options) {
|
|
|
982
807
|
...caller === void 0 ? {} : { caller }
|
|
983
808
|
});
|
|
984
809
|
if (response !== void 0 && Symbol.asyncIterator in response) {
|
|
985
|
-
const stream = (0, _orkestrel_server.
|
|
810
|
+
const stream = (0, _orkestrel_server.createStream)();
|
|
986
811
|
stream.response.headers.set(SSE_BUFFERING_HEADER, "no");
|
|
987
812
|
queueMicrotask(() => void sendEventStream(response, stream));
|
|
988
813
|
return disconnect.bridge(stream);
|
|
@@ -990,7 +815,7 @@ function createMCPPostHandler(mcp, options) {
|
|
|
990
815
|
const status = inferStatus(response, era);
|
|
991
816
|
if (response === void 0) return new Response(null, { status });
|
|
992
817
|
if (status === 200 && streaming && acceptsEventStream(request)) {
|
|
993
|
-
const stream = (0, _orkestrel_server.
|
|
818
|
+
const stream = (0, _orkestrel_server.createStream)();
|
|
994
819
|
stream.response.headers.set(SSE_BUFFERING_HEADER, "no");
|
|
995
820
|
stream.write({ data: JSON.stringify(response) });
|
|
996
821
|
stream.end();
|
|
@@ -1000,215 +825,14 @@ function createMCPPostHandler(mcp, options) {
|
|
|
1000
825
|
};
|
|
1001
826
|
}
|
|
1002
827
|
//#endregion
|
|
1003
|
-
//#region src/server/transports/HTTPClientTransport.ts
|
|
1004
|
-
/**
|
|
1005
|
-
* The HTTP CLIENT transport for the Model Context Protocol — a
|
|
1006
|
-
* {@link MCPClientTransportInterface} that drives a REMOTE Streamable-HTTP MCP server over
|
|
1007
|
-
* `fetch`, the egress mirror of the server's `createMCPRoutes`.
|
|
1008
|
-
*
|
|
1009
|
-
* @remarks
|
|
1010
|
-
* - **Request/response over `fetch`.** `send(message)` POSTs the JSON-serialized
|
|
1011
|
-
* message to `options.url` with `content-type: application/json` and an
|
|
1012
|
-
* `Accept` of BOTH `application/json` and `text/event-stream` (so the server may
|
|
1013
|
-
* answer with either framing) — plus any `options.headers` (for example, an `Authorization`
|
|
1014
|
-
* bearer). It then decodes the reply and emits each decoded {@link JSONRPCMessage} on
|
|
1015
|
-
* the `message` event the {@link import('@orkestrel/mcp').MCPClientInterface} subscribes
|
|
1016
|
-
* to.
|
|
1017
|
-
* - **Both reply framings.** A `200` with an `application/json` body is parsed with
|
|
1018
|
-
* `parseJSONRPCMessage`; a `200` with a `text/event-stream` body is decoded with the
|
|
1019
|
-
* `@orkestrel/sse` {@link import('@orkestrel/sse').SSEParserInterface} ({@link
|
|
1020
|
-
* readEventStream}) — the inverse of the server's `openStream` seam, so the wire
|
|
1021
|
-
* round-trips. A `202`
|
|
1022
|
-
* Accepted (a notification) carries no body and emits nothing.
|
|
1023
|
-
* - **Session and protocol headers.** `start()` is a no-op (a
|
|
1024
|
-
* request/response transport opens no long-lived connection). The
|
|
1025
|
-
* `mcp-session-id` response header, when a STATEFUL server sends one (on
|
|
1026
|
-
* `initialize`), is captured into `session` and then ECHOED as the
|
|
1027
|
-
* `mcp-session-id` request header on every SUBSEQUENT request — so an
|
|
1028
|
-
* `MCPClient` passes a stateful server's session validation. The
|
|
1029
|
-
* initialize result's `protocolVersion` is likewise captured, but only
|
|
1030
|
-
* when it is a SUPPORTED value, and echoed as `mcp-protocol-version` alone on
|
|
1031
|
-
* subsequent legacy requests. Modern requests instead derive protocol and method
|
|
1032
|
-
* headers from the message, plus the name header only for `tools/call` — carried in the
|
|
1033
|
-
* protocol's Base64 sentinel form whenever the tool name cannot ride as plain ASCII.
|
|
1034
|
-
* Before initialize returns, neither captured legacy header is sent.
|
|
1035
|
-
* `close()` clears the captured protocol so a reconnect's `initialize`
|
|
1036
|
-
* POST is headerless; the captured `session` persists across `close()`.
|
|
1037
|
-
* - **`close()` releases what is in flight.** Every `fetch` this transport still has open is
|
|
1038
|
-
* ABORTED, which cancels the response body a `send` is reading — an SSE reply the server
|
|
1039
|
-
* never ends would otherwise outlive the transport, with nothing left able to reach it. The
|
|
1040
|
-
* aborted read surfaces on `error` and the `send` reporting it resolves. `close()` is
|
|
1041
|
-
* idempotent (one `close` event per connected lifetime), and `start()` opens the next one.
|
|
1042
|
-
* - **Total at the boundary.** Every reply is narrowed (`parseJSONRPCMessage`,
|
|
1043
|
-
* the SSE decoder). A non-message success reply is dropped, never asserted. A non-success
|
|
1044
|
-
* reply that carries no valid JSON-RPC message rejects `send` with its HTTP status and body
|
|
1045
|
-
* shape. A valid JSON-RPC error body is emitted at any HTTP status. A `fetch` / decode failure
|
|
1046
|
-
* on a success response surfaces on the `error` event rather than escaping `send`.
|
|
1047
|
-
* - **Observable.** Owns the `emitter` ({@link MCPClientTransportEventMap}); fires
|
|
1048
|
-
* `message` per decoded reply, `error` on a fault, and `close` on `close()`.
|
|
1049
|
-
*
|
|
1050
|
-
* @example
|
|
1051
|
-
* ```ts
|
|
1052
|
-
* const transport = new HTTPClientTransport({ url: 'http://localhost:3000/mcp' })
|
|
1053
|
-
* const client = new MCPClient({ transport })
|
|
1054
|
-
* await client.connect()
|
|
1055
|
-
* ```
|
|
1056
|
-
*/
|
|
1057
|
-
var HTTPClientTransport = class {
|
|
1058
|
-
#emitter;
|
|
1059
|
-
#url;
|
|
1060
|
-
#headers;
|
|
1061
|
-
#fetch;
|
|
1062
|
-
#timeout;
|
|
1063
|
-
#pending = /* @__PURE__ */ new Set();
|
|
1064
|
-
#parameters = /* @__PURE__ */ new Map();
|
|
1065
|
-
#stamps = /* @__PURE__ */ new WeakMap();
|
|
1066
|
-
#session = void 0;
|
|
1067
|
-
#protocol = void 0;
|
|
1068
|
-
#generation = 0;
|
|
1069
|
-
#closed = false;
|
|
1070
|
-
constructor(options) {
|
|
1071
|
-
this.#emitter = new _orkestrel_emitter.Emitter();
|
|
1072
|
-
this.#url = options.url;
|
|
1073
|
-
this.#headers = options.headers ?? {};
|
|
1074
|
-
this.#fetch = options.fetch ?? globalThis.fetch;
|
|
1075
|
-
this.#timeout = options.timeout;
|
|
1076
|
-
}
|
|
1077
|
-
get emitter() {
|
|
1078
|
-
return this.#emitter;
|
|
1079
|
-
}
|
|
1080
|
-
get session() {
|
|
1081
|
-
return this.#session;
|
|
1082
|
-
}
|
|
1083
|
-
get duplex() {
|
|
1084
|
-
return false;
|
|
1085
|
-
}
|
|
1086
|
-
async start() {
|
|
1087
|
-
this.#closed = false;
|
|
1088
|
-
}
|
|
1089
|
-
async send(message) {
|
|
1090
|
-
this.#stamp(message);
|
|
1091
|
-
const request = new AbortController();
|
|
1092
|
-
this.#pending.add(request);
|
|
1093
|
-
try {
|
|
1094
|
-
await this.#exchange(message, request.signal);
|
|
1095
|
-
} finally {
|
|
1096
|
-
this.#pending.delete(request);
|
|
1097
|
-
}
|
|
1098
|
-
}
|
|
1099
|
-
#stamp(message) {
|
|
1100
|
-
if (!(0, _src_core.isModernRequest)(message) || message.method !== "tools/list") return;
|
|
1101
|
-
if (message.params?.["cursor"] === void 0) this.#generation += 1;
|
|
1102
|
-
this.#stamps.set(message, this.#generation);
|
|
1103
|
-
}
|
|
1104
|
-
async #exchange(message, signal) {
|
|
1105
|
-
let response;
|
|
1106
|
-
try {
|
|
1107
|
-
response = await this.#fetch(this.#url, {
|
|
1108
|
-
method: "POST",
|
|
1109
|
-
headers: {
|
|
1110
|
-
"content-type": "application/json",
|
|
1111
|
-
accept: "application/json, text/event-stream",
|
|
1112
|
-
...this.#session === void 0 ? {} : { [MCP_SESSION_HEADER]: this.#session },
|
|
1113
|
-
...this.#buildHeaders(message),
|
|
1114
|
-
...this.#headers
|
|
1115
|
-
},
|
|
1116
|
-
body: JSON.stringify(message),
|
|
1117
|
-
signal: this.#timeout === void 0 ? signal : AbortSignal.any([signal, AbortSignal.timeout(this.#timeout)])
|
|
1118
|
-
});
|
|
1119
|
-
} catch (error) {
|
|
1120
|
-
this.#emitter.emit("error", error);
|
|
1121
|
-
return;
|
|
1122
|
-
}
|
|
1123
|
-
const session = response.headers.get(MCP_SESSION_HEADER);
|
|
1124
|
-
if (session !== null) this.#session = session;
|
|
1125
|
-
await this.#deliver(response, message);
|
|
1126
|
-
}
|
|
1127
|
-
async close() {
|
|
1128
|
-
if (this.#closed) return;
|
|
1129
|
-
this.#closed = true;
|
|
1130
|
-
for (const request of this.#pending) request.abort();
|
|
1131
|
-
this.#pending.clear();
|
|
1132
|
-
this.#protocol = void 0;
|
|
1133
|
-
this.#emitter.emit("close");
|
|
1134
|
-
}
|
|
1135
|
-
#buildHeaders(message) {
|
|
1136
|
-
if ((0, _src_core.isModernRequest)(message)) {
|
|
1137
|
-
const version = (0, _src_core.inferRequestVersion)(message);
|
|
1138
|
-
const name = message.params?.["name"];
|
|
1139
|
-
return {
|
|
1140
|
-
...version === void 0 ? {} : { [MCP_PROTOCOL_VERSION_HEADER]: version },
|
|
1141
|
-
[MCP_METHOD_HEADER]: message.method,
|
|
1142
|
-
...message.method === "tools/call" && (0, _orkestrel_contract.isString)(name) ? {
|
|
1143
|
-
[MCP_NAME_HEADER]: (0, _src_core.encodeSentinel)(name),
|
|
1144
|
-
...(0, _src_core.buildHeaderProjection)(this.#parameters.get(name) ?? [], message.params?.["arguments"])
|
|
1145
|
-
} : {}
|
|
1146
|
-
};
|
|
1147
|
-
}
|
|
1148
|
-
return this.#protocol === void 0 ? {} : { [MCP_PROTOCOL_VERSION_HEADER]: this.#protocol };
|
|
1149
|
-
}
|
|
1150
|
-
async #deliver(response, sent) {
|
|
1151
|
-
if (response.status === 202) return;
|
|
1152
|
-
const type = response.headers.get("content-type") ?? "";
|
|
1153
|
-
let messages = [];
|
|
1154
|
-
let failure;
|
|
1155
|
-
try {
|
|
1156
|
-
if (type.includes("text/event-stream")) messages = await readEventStream(response);
|
|
1157
|
-
else if (type.includes("application/json")) {
|
|
1158
|
-
const message = (0, _src_core.parseJSONRPCMessage)(await response.json());
|
|
1159
|
-
if (message !== void 0) messages = [message];
|
|
1160
|
-
}
|
|
1161
|
-
} catch (error) {
|
|
1162
|
-
failure = { error };
|
|
1163
|
-
}
|
|
1164
|
-
for (const message of messages) this.#capture(message, sent);
|
|
1165
|
-
if (!response.ok && messages.length === 0) throw buildResponseError(response, type);
|
|
1166
|
-
if (failure !== void 0) this.#emitter.emit("error", failure.error);
|
|
1167
|
-
}
|
|
1168
|
-
#capture(message, sent) {
|
|
1169
|
-
if ((0, _src_core.isJSONRPCResponse)(message) && (0, _orkestrel_contract.isRecord)(message.result) && (0, _src_core.isMCPVersion)(message.result["protocolVersion"])) this.#protocol = message.result["protocolVersion"];
|
|
1170
|
-
this.#emitter.emit("message", this.#select(message, sent));
|
|
1171
|
-
}
|
|
1172
|
-
#select(message, sent) {
|
|
1173
|
-
if (!(0, _src_core.isModernRequest)(sent) || sent.method !== "tools/list") return message;
|
|
1174
|
-
if (!(0, _src_core.isJSONRPCResponse)(message) || message.error !== void 0) return message;
|
|
1175
|
-
const result = message.result;
|
|
1176
|
-
const listed = (0, _orkestrel_contract.isRecord)(result) ? result["tools"] : void 0;
|
|
1177
|
-
if (!(0, _orkestrel_contract.isRecord)(result) || !(0, _orkestrel_contract.isArray)(listed)) return message;
|
|
1178
|
-
const current = this.#stamps.get(sent) === this.#generation;
|
|
1179
|
-
if (current && sent.params?.["cursor"] === void 0) this.#parameters.clear();
|
|
1180
|
-
const kept = [];
|
|
1181
|
-
for (const tool of listed) {
|
|
1182
|
-
if (!(0, _orkestrel_contract.isRecord)(tool) || !(0, _orkestrel_contract.isString)(tool["name"])) {
|
|
1183
|
-
kept.push(tool);
|
|
1184
|
-
continue;
|
|
1185
|
-
}
|
|
1186
|
-
const parameters = (0, _src_core.buildHeaderParameters)(tool["inputSchema"]);
|
|
1187
|
-
if (parameters === void 0) {
|
|
1188
|
-
this.#emitter.emit("error", /* @__PURE__ */ new Error(`MCP tool '${tool["name"]}' is excluded from tools/list: its inputSchema carries an invalid x-mcp-header annotation`));
|
|
1189
|
-
continue;
|
|
1190
|
-
}
|
|
1191
|
-
if (current) this.#parameters.set(tool["name"], parameters);
|
|
1192
|
-
kept.push(tool);
|
|
1193
|
-
}
|
|
1194
|
-
return {
|
|
1195
|
-
...message,
|
|
1196
|
-
result: {
|
|
1197
|
-
...result,
|
|
1198
|
-
tools: kept
|
|
1199
|
-
}
|
|
1200
|
-
};
|
|
1201
|
-
}
|
|
1202
|
-
};
|
|
1203
|
-
//#endregion
|
|
1204
828
|
//#region src/server/MCPSession.ts
|
|
1205
829
|
/**
|
|
1206
|
-
*
|
|
830
|
+
* Represents one MCP transport session — the per-session entity a {@link
|
|
1207
831
|
* import('./middlewares.js').createMCPSession} middleware owns, keyed by its `id`, carrying the
|
|
1208
832
|
* resumable server→client push channel with its bounded replay log FOLDED IN.
|
|
1209
833
|
*
|
|
1210
834
|
* @remarks
|
|
1211
|
-
*
|
|
835
|
+
* One entity carries the whole session: it holds the
|
|
1212
836
|
* session `id`, its OWN bounded, replayable log of pushed server→client messages (the
|
|
1213
837
|
* resumable GET-SSE channel — a private `#events` `Map` + a monotone `#counter`, with
|
|
1214
838
|
* `capacity` / `ttl` eviction, not a separate store), and the set of open
|
|
@@ -1220,7 +844,7 @@ var HTTPClientTransport = class {
|
|
|
1220
844
|
* SSE event (`stream.write({ id, data })`). A push with NO attached stream is still logged,
|
|
1221
845
|
* so a client that connects (or reconnects with a `Last-Event-ID`) LATER replays it from the
|
|
1222
846
|
* log. A `write` to a closed stream is a safe no-op (the {@link
|
|
1223
|
-
* `@orkestrel/server`'s `
|
|
847
|
+
* `@orkestrel/server`'s `createStream` contract), so a just-disconnected stream that
|
|
1224
848
|
* has not yet been `detach`ed never throws. A replayed event and the live one carry the
|
|
1225
849
|
* IDENTICAL id (the log assigns it once).
|
|
1226
850
|
*
|
|
@@ -1245,9 +869,10 @@ var HTTPClientTransport = class {
|
|
|
1245
869
|
* The middleware opens the stream (the spine seam) and registers it here; this class only
|
|
1246
870
|
* serializes a message onto the already-open streams.
|
|
1247
871
|
*
|
|
1248
|
-
* - **Injected clock.**
|
|
1249
|
-
* `Date.now
|
|
1250
|
-
* timer
|
|
872
|
+
* - **Injected clock.** {@link import('./types.js').MCPSessionOptions.clock} supplies the
|
|
873
|
+
* epoch-ms clock the lazy TTL sweep reads, defaulting to `Date.now` — so a test drives TTL
|
|
874
|
+
* eviction with an elapsed clock rather than a real timer, and the middleware that mints a
|
|
875
|
+
* session hands its own clock down instead of leaving the log on wall-clock time.
|
|
1251
876
|
*
|
|
1252
877
|
* @example
|
|
1253
878
|
* ```ts
|
|
@@ -1263,11 +888,13 @@ var MCPSession = class {
|
|
|
1263
888
|
#streams = /* @__PURE__ */ new Set();
|
|
1264
889
|
#capacity;
|
|
1265
890
|
#ttl;
|
|
891
|
+
#clock;
|
|
1266
892
|
#counter = 0;
|
|
1267
893
|
constructor(id, options) {
|
|
1268
894
|
this.#id = id;
|
|
1269
895
|
this.#capacity = options?.capacity ?? 1024;
|
|
1270
896
|
this.#ttl = options?.ttl ?? 3e5;
|
|
897
|
+
this.#clock = options?.clock ?? Date.now;
|
|
1271
898
|
}
|
|
1272
899
|
get id() {
|
|
1273
900
|
return this.#id;
|
|
@@ -1278,8 +905,8 @@ var MCPSession = class {
|
|
|
1278
905
|
detach(stream) {
|
|
1279
906
|
this.#streams.delete(stream);
|
|
1280
907
|
}
|
|
1281
|
-
push(message
|
|
1282
|
-
const id = this.#append(message
|
|
908
|
+
push(message) {
|
|
909
|
+
const id = this.#append(message);
|
|
1283
910
|
const data = JSON.stringify(message);
|
|
1284
911
|
for (const stream of this.#streams) stream.write({
|
|
1285
912
|
id,
|
|
@@ -1287,15 +914,16 @@ var MCPSession = class {
|
|
|
1287
914
|
});
|
|
1288
915
|
return id;
|
|
1289
916
|
}
|
|
1290
|
-
replay(afterId
|
|
1291
|
-
this.#evict(
|
|
917
|
+
replay(afterId) {
|
|
918
|
+
this.#evict(this.#clock());
|
|
1292
919
|
const out = [];
|
|
1293
920
|
let found = false;
|
|
1294
921
|
for (const entry of this.#events.values()) if (found) out.push(entry);
|
|
1295
922
|
else if (entry.id === afterId) found = true;
|
|
1296
923
|
return found ? out : [];
|
|
1297
924
|
}
|
|
1298
|
-
#append(message
|
|
925
|
+
#append(message) {
|
|
926
|
+
const now = this.#clock();
|
|
1299
927
|
this.#evict(now);
|
|
1300
928
|
this.#counter += 1;
|
|
1301
929
|
const id = this.#counter.toString(36);
|
|
@@ -1321,21 +949,21 @@ var MCPSession = class {
|
|
|
1321
949
|
//#endregion
|
|
1322
950
|
//#region src/server/transports/WebSocketServerTransport.ts
|
|
1323
951
|
/**
|
|
1324
|
-
*
|
|
1325
|
-
* {@link
|
|
1326
|
-
*
|
|
952
|
+
* Wraps a {@link NodeWebSocketInterface} (the RFC 6455 wire wrapper) as a
|
|
953
|
+
* {@link MCPMessageTransportInterface} — the per-connection JSON-RPC-over-WebSocket SERVER
|
|
954
|
+
* bridge, the bidirectional JSON-RPC message channel
|
|
1327
955
|
* `createWebSocketServer` pumps `mcp.dispatch` over and the egress mirror's
|
|
1328
956
|
* {@link import('./WebSocketClientTransport.js').WebSocketClientTransport} reuses.
|
|
1329
957
|
*
|
|
1330
958
|
* @remarks
|
|
1331
|
-
* - **Reuses `
|
|
959
|
+
* - **Reuses `MCPMessageTransportInterface`.** It IS the same generic carrier the HTTP
|
|
1332
960
|
* client transport implements — `emitter` (`message` / `close` / `error`), `start`,
|
|
1333
961
|
* `send`, `close` — so the WebSocket server and client both speak ONE transport contract,
|
|
1334
962
|
* no near-duplicate sibling interface. `session` is `undefined` (the stateless v1; a
|
|
1335
963
|
* session id is the deferred sessions tier). The name keeps the role explicit even though
|
|
1336
964
|
* the shape is shared.
|
|
1337
965
|
* - **Inbound (`message`).** `start()` subscribes to the socket's `message` event; each text
|
|
1338
|
-
* frame
|
|
966
|
+
* frame runs through the shared `deliverMessage` fold (parse, then narrow) — a
|
|
1339
967
|
* well-formed {@link JSONRPCMessage} is re-emitted on this transport's `message` event (the
|
|
1340
968
|
* parsed envelope the {@link import('@orkestrel/mcp').MCPServerInterface} pump dispatches), while
|
|
1341
969
|
* a non-JSON or non-message frame is surfaced on `error` and DROPPED, never thrown. It
|
|
@@ -1353,7 +981,7 @@ var MCPSession = class {
|
|
|
1353
981
|
* (idempotent — a second `close`, or a socket-driven close, emits once). A frame that arrives
|
|
1354
982
|
* between that release and the peer's close echo reaches nothing: the socket-driven close path
|
|
1355
983
|
* releases the same way, so a closed transport is never subscribed to a live socket.
|
|
1356
|
-
* - **Observable.** Owns the `emitter` ({@link
|
|
984
|
+
* - **Observable.** Owns the `emitter` ({@link MCPMessageTransportEventMap}); the emitter
|
|
1357
985
|
* isolates a listener throw (a buggy observer never corrupts the bridge). `error` is a
|
|
1358
986
|
* DOMAIN event (a transport-level fault), distinct from the emitter's listener-error channel.
|
|
1359
987
|
*/
|
|
@@ -1395,19 +1023,7 @@ var WebSocketServerTransport = class {
|
|
|
1395
1023
|
this.#emitter.emit("close");
|
|
1396
1024
|
}
|
|
1397
1025
|
#receive(text) {
|
|
1398
|
-
|
|
1399
|
-
try {
|
|
1400
|
-
parsed = JSON.parse(text);
|
|
1401
|
-
} catch (error) {
|
|
1402
|
-
this.#emitter.emit("error", error);
|
|
1403
|
-
return;
|
|
1404
|
-
}
|
|
1405
|
-
const message = (0, _src_core.parseJSONRPCMessage)(parsed);
|
|
1406
|
-
if (message === void 0) {
|
|
1407
|
-
this.#emitter.emit("error", /* @__PURE__ */ new Error("non-JSON-RPC WebSocket frame"));
|
|
1408
|
-
return;
|
|
1409
|
-
}
|
|
1410
|
-
this.#emitter.emit("message", message);
|
|
1026
|
+
(0, _src_core.deliverMessage)(this.#emitter, text, "non-JSON-RPC WebSocket frame");
|
|
1411
1027
|
}
|
|
1412
1028
|
#onClose() {
|
|
1413
1029
|
if (this.#closed) return;
|
|
@@ -1424,10 +1040,10 @@ var WebSocketServerTransport = class {
|
|
|
1424
1040
|
//#endregion
|
|
1425
1041
|
//#region src/server/transports/WebSocketClientTransport.ts
|
|
1426
1042
|
/**
|
|
1427
|
-
*
|
|
1428
|
-
* {@link
|
|
1043
|
+
* Drives a REMOTE MCP server over a WebSocket — a CLIENT
|
|
1044
|
+
* {@link MCPMessageTransportInterface} for the Model Context Protocol, the
|
|
1429
1045
|
* egress mirror of {@link import('./factories.js').createWebSocketServer} and the WebSocket
|
|
1430
|
-
* sibling of {@link import('
|
|
1046
|
+
* sibling of {@link import('@orkestrel/mcp').HTTPClientTransport}.
|
|
1431
1047
|
*
|
|
1432
1048
|
* @remarks
|
|
1433
1049
|
* - **Persistent bidirectional channel (unlike the HTTP transport).** `start()` performs the
|
|
@@ -1445,8 +1061,8 @@ var WebSocketServerTransport = class {
|
|
|
1445
1061
|
* transport while the handshake was on the wire, both WIN — the socket that arrives late is
|
|
1446
1062
|
* DESTROYED and never bound, so no orphan is left re-emitting frames at nobody. Both
|
|
1447
1063
|
* `start()` calls still resolve; exactly one socket is ever bound.
|
|
1448
|
-
* - **Inbound (`message`).** Each decoded text frame
|
|
1449
|
-
*
|
|
1064
|
+
* - **Inbound (`message`).** Each decoded text frame runs through the shared `deliverMessage`
|
|
1065
|
+
* fold (parse, then narrow) — a {@link JSONRPCMessage} re-emits on this transport's `message`
|
|
1450
1066
|
* event (the reply the {@link import('@orkestrel/mcp').MCPClientInterface} correlates by `id`); a
|
|
1451
1067
|
* non-JSON / non-message frame surfaces on `error` and is dropped. The socket's `close`
|
|
1452
1068
|
* / `error` bridge to this transport's events.
|
|
@@ -1464,7 +1080,7 @@ var WebSocketServerTransport = class {
|
|
|
1464
1080
|
* - **URL scheme.** `options.url` accepts a `ws://` / `wss://` URL or an `http://` / `https://`
|
|
1465
1081
|
* one; a `ws(s)` scheme is converted to `http(s)` for the underlying upgrade request (`wss`
|
|
1466
1082
|
* → TLS through `node:https`). Either reaches the same endpoint.
|
|
1467
|
-
* - **Observable.** Owns the `emitter` ({@link
|
|
1083
|
+
* - **Observable.** Owns the `emitter` ({@link MCPMessageTransportEventMap}); every emit
|
|
1468
1084
|
* the emitter isolates a listener throw (a buggy observer never corrupts the transport);
|
|
1469
1085
|
* `error` is a DOMAIN event (a transport-level fault).
|
|
1470
1086
|
*
|
|
@@ -1501,7 +1117,7 @@ var WebSocketClientTransport = class {
|
|
|
1501
1117
|
if (this.#socket !== void 0) return;
|
|
1502
1118
|
this.#closed = false;
|
|
1503
1119
|
try {
|
|
1504
|
-
await this.#connect(this.#
|
|
1120
|
+
await this.#connect(this.#toHTTPURL(), (0, node_crypto.randomBytes)(16).toString("base64"));
|
|
1505
1121
|
} finally {
|
|
1506
1122
|
this.#request = void 0;
|
|
1507
1123
|
}
|
|
@@ -1534,7 +1150,7 @@ var WebSocketClientTransport = class {
|
|
|
1534
1150
|
Upgrade: "websocket",
|
|
1535
1151
|
"Sec-WebSocket-Key": key,
|
|
1536
1152
|
"Sec-WebSocket-Version": _orkestrel_websocket.WEBSOCKET_VERSION,
|
|
1537
|
-
"Sec-WebSocket-Protocol":
|
|
1153
|
+
"Sec-WebSocket-Protocol": _src_core.MCP_WEBSOCKET_SUBPROTOCOL,
|
|
1538
1154
|
...this.#headers
|
|
1539
1155
|
}
|
|
1540
1156
|
});
|
|
@@ -1586,19 +1202,7 @@ var WebSocketClientTransport = class {
|
|
|
1586
1202
|
socket.emitter.off("error", this.#failure);
|
|
1587
1203
|
}
|
|
1588
1204
|
#receive(text) {
|
|
1589
|
-
|
|
1590
|
-
try {
|
|
1591
|
-
parsed = JSON.parse(text);
|
|
1592
|
-
} catch (error) {
|
|
1593
|
-
this.#emitter.emit("error", error);
|
|
1594
|
-
return;
|
|
1595
|
-
}
|
|
1596
|
-
const message = (0, _src_core.parseJSONRPCMessage)(parsed);
|
|
1597
|
-
if (message === void 0) {
|
|
1598
|
-
this.#emitter.emit("error", /* @__PURE__ */ new Error("non-JSON-RPC WebSocket frame"));
|
|
1599
|
-
return;
|
|
1600
|
-
}
|
|
1601
|
-
this.#emitter.emit("message", message);
|
|
1205
|
+
(0, _src_core.deliverMessage)(this.#emitter, text, "non-JSON-RPC WebSocket frame");
|
|
1602
1206
|
}
|
|
1603
1207
|
#onClose() {
|
|
1604
1208
|
if (this.#closed) return;
|
|
@@ -1607,7 +1211,7 @@ var WebSocketClientTransport = class {
|
|
|
1607
1211
|
this.#socket = void 0;
|
|
1608
1212
|
this.#emitter.emit("close");
|
|
1609
1213
|
}
|
|
1610
|
-
#
|
|
1214
|
+
#toHTTPURL() {
|
|
1611
1215
|
const url = new URL(this.#url);
|
|
1612
1216
|
if (url.protocol === "ws:") url.protocol = "http:";
|
|
1613
1217
|
else if (url.protocol === "wss:") url.protocol = "https:";
|
|
@@ -1618,10 +1222,9 @@ var WebSocketClientTransport = class {
|
|
|
1618
1222
|
//#endregion
|
|
1619
1223
|
//#region src/server/transports/StdioClientTransport.ts
|
|
1620
1224
|
/**
|
|
1621
|
-
*
|
|
1622
|
-
* {@link StdioClientTransportInterface}
|
|
1623
|
-
*
|
|
1624
|
-
* import('./HTTPClientTransport.js').HTTPClientTransport} and {@link
|
|
1225
|
+
* Drives a CHILD PROCESS MCP server over newline-delimited JSON-RPC on `stdin`/`stdout` —
|
|
1226
|
+
* a {@link StdioClientTransportInterface}, the stdio sibling of {@link
|
|
1227
|
+
* import('@orkestrel/mcp').HTTPClientTransport} and {@link
|
|
1625
1228
|
* import('./WebSocketClientTransport.js').WebSocketClientTransport}.
|
|
1626
1229
|
*
|
|
1627
1230
|
* @remarks
|
|
@@ -1662,7 +1265,7 @@ var WebSocketClientTransport = class {
|
|
|
1662
1265
|
* never moves again, so a detached descendant writing to the inherited stderr after the cutoff
|
|
1663
1266
|
* cannot grow it. See {@link StdioClientTransportInterface.evidence} for the readings and the
|
|
1664
1267
|
* byte bound.
|
|
1665
|
-
* - **Observable.** Owns the `emitter` ({@link
|
|
1268
|
+
* - **Observable.** Owns the `emitter` ({@link MCPMessageTransportEventMap}); the
|
|
1666
1269
|
* emitter isolates a listener throw; `error` is a DOMAIN event (a transport-level
|
|
1667
1270
|
* fault, including the child spawn cause the supervisor surfaces and the notice that this
|
|
1668
1271
|
* lifetime's `evidence` was cut off at the `drain` bound), distinct from the emitter's own
|
|
@@ -1784,15 +1387,14 @@ var StdioClientTransport = class {
|
|
|
1784
1387
|
//#endregion
|
|
1785
1388
|
//#region src/server/transports/StdioServerTransport.ts
|
|
1786
1389
|
/**
|
|
1787
|
-
*
|
|
1788
|
-
*
|
|
1789
|
-
*
|
|
1790
|
-
*
|
|
1791
|
-
* `mcp.dispatch` over, the stdio mirror of {@link
|
|
1390
|
+
* Wraps an injectable readable/writable stream pair (`process.stdin`/`process.stdout` in
|
|
1391
|
+
* production, a test double in tests) as a {@link MCPMessageTransportInterface} — the
|
|
1392
|
+
* newline-delimited JSON-RPC channel {@link import('../factories.js').createStdioServer}
|
|
1393
|
+
* pumps `mcp.dispatch` over, the stdio mirror of {@link
|
|
1792
1394
|
* import('./WebSocketServerTransport.js').WebSocketServerTransport}.
|
|
1793
1395
|
*
|
|
1794
1396
|
* @remarks
|
|
1795
|
-
* - **Reuses `
|
|
1397
|
+
* - **Reuses `MCPMessageTransportInterface`.** The same generic carrier the HTTP
|
|
1796
1398
|
* and WebSocket server transports implement — `emitter` (`message` / `close` /
|
|
1797
1399
|
* `error`), `start`, `send`, `close`. `session` is `undefined` (the stateless v1).
|
|
1798
1400
|
* - **Inbound (`message`).** `start()` subscribes to `input`'s `data` event; each
|
|
@@ -1818,7 +1420,7 @@ var StdioClientTransport = class {
|
|
|
1818
1420
|
* listener receives data. The injected streams are owned by the caller (typically
|
|
1819
1421
|
* `process.stdin`/`process.stdout`), so the transport never destroys, ends, or blanket-clears
|
|
1820
1422
|
* them.
|
|
1821
|
-
* - **Observable.** Owns the `emitter` ({@link
|
|
1423
|
+
* - **Observable.** Owns the `emitter` ({@link MCPMessageTransportEventMap}); the
|
|
1822
1424
|
* emitter isolates a listener throw; `error` is a DOMAIN event (a transport-level
|
|
1823
1425
|
* fault), distinct from the emitter's own listener-error channel.
|
|
1824
1426
|
*/
|
|
@@ -1924,6 +1526,87 @@ function createMCPContinuation(secret) {
|
|
|
1924
1526
|
};
|
|
1925
1527
|
}
|
|
1926
1528
|
/**
|
|
1529
|
+
* Creates the server-side mirror of
|
|
1530
|
+
* {@link import('@orkestrel/mcp').createDuplexClientTransport}: the adapter that bridges a
|
|
1531
|
+
* message-channel {@link MCPMessageTransportInterface}
|
|
1532
|
+
* (the shape the stdio and WebSocket SERVER transports already implement) onto the
|
|
1533
|
+
* environment-agnostic {@link import('@orkestrel/mcp').MCPTransportInterface} port — what
|
|
1534
|
+
* {@link createStdioServer} and {@link createWebSocketServer} pipe through `bindServer`, so
|
|
1535
|
+
* the request/reply/error pump those factories used to hand-roll identically now lives ONCE
|
|
1536
|
+
* in the core binder. {@link import('@orkestrel/mcp').createDuplexClientTransport} adapts the
|
|
1537
|
+
* same two contracts the other way.
|
|
1538
|
+
*
|
|
1539
|
+
* @remarks
|
|
1540
|
+
* `send` decodes the already-serialized reply string back to a {@link JSONRPCMessage}
|
|
1541
|
+
* and writes it through `transport.send` (the same `JSON.stringify` the underlying
|
|
1542
|
+
* transport already performs, so the wire bytes are unchanged). `listen` filters
|
|
1543
|
+
* `transport`'s `message` event to INVOCATIONS ONLY — requests and notifications, never a
|
|
1544
|
+
* stray response, exactly as the prior hand-rolled pumps did — and re-serializes each one
|
|
1545
|
+
* back to a string for `bindServer`. `closed` bridges `transport`'s `close` event. `close`
|
|
1546
|
+
* closes the underlying `transport`.
|
|
1547
|
+
*
|
|
1548
|
+
* @remarks A message crossing this bridge is decoded and re-encoded TWICE, and that is
|
|
1549
|
+
* ACCEPTED rather than accidental. Inbound: the carrier already parsed the frame into a
|
|
1550
|
+
* {@link JSONRPCMessage}, and `listen` re-serializes it so `bindServer` can decode it again
|
|
1551
|
+
* under the server's own `limit`. Outbound: `bindServer` serialized the reply, `send` parses
|
|
1552
|
+
* it back, and the carrier stringifies it once more. The cost is two extra `JSON.parse` /
|
|
1553
|
+
* `JSON.stringify` round trips per message, paid to keep ONE pump in the core binder instead
|
|
1554
|
+
* of a hand-rolled one per carrier. It is BOUNDED rather than unbounded because the binder
|
|
1555
|
+
* decodes within `server.limit.message`, so an oversized frame is refused before the second
|
|
1556
|
+
* decode rather than after it. Removing the cost means giving `MCPTransportInterface` a
|
|
1557
|
+
* message-shaped face beside its string one, which every transport would then carry.
|
|
1558
|
+
*
|
|
1559
|
+
* @remarks Per {@link import('@orkestrel/mcp').MCPTransportInterface}, `listen`/`closed`
|
|
1560
|
+
* each hold THE SINGLE current handler (a second call REPLACES the first, never adds).
|
|
1561
|
+
* Because the underlying `transport.emitter` is ADD-based (`on` subscribes, never
|
|
1562
|
+
* replaces), this bridge installs ONE stable emitter listener per event on first use
|
|
1563
|
+
* and re-routes it to whichever handler is active (`undefined` while
|
|
1564
|
+
* none is), so rebinding never double-dispatches.
|
|
1565
|
+
*
|
|
1566
|
+
* @remarks A response whose `result` serializes away (for example, `undefined`) is dropped by
|
|
1567
|
+
* the message validators on the wire's decode side — an asymmetry the stdio/WS carrier
|
|
1568
|
+
* shares with the streamable-HTTP face, because both round-trip through `JSON.stringify`
|
|
1569
|
+
* / `JSON.parse` before re-validation.
|
|
1570
|
+
*
|
|
1571
|
+
* @param transport - The message-channel transport to bridge (stdio or WebSocket)
|
|
1572
|
+
* @returns An {@link import('@orkestrel/mcp').MCPTransportInterface} `bindServer` can drive
|
|
1573
|
+
*
|
|
1574
|
+
* @example
|
|
1575
|
+
* ```ts
|
|
1576
|
+
* import { bindServer } from '@orkestrel/mcp'
|
|
1577
|
+
*
|
|
1578
|
+
* const transport = new StdioServerTransport(process.stdin, process.stdout)
|
|
1579
|
+
* bindServer(mcp, createDuplexServerTransport(transport))
|
|
1580
|
+
* ```
|
|
1581
|
+
*/
|
|
1582
|
+
function createDuplexServerTransport(transport) {
|
|
1583
|
+
let onMessage;
|
|
1584
|
+
let onClosed;
|
|
1585
|
+
transport.emitter.on("message", (message) => {
|
|
1586
|
+
if (!(0, _src_core.isJSONRPCInvocation)(message)) return;
|
|
1587
|
+
onMessage?.(JSON.stringify(message));
|
|
1588
|
+
});
|
|
1589
|
+
transport.emitter.on("close", () => {
|
|
1590
|
+
onClosed?.();
|
|
1591
|
+
});
|
|
1592
|
+
return {
|
|
1593
|
+
async send(message) {
|
|
1594
|
+
const decoded = (0, _src_core.decodeEvent)(message);
|
|
1595
|
+
if (decoded === void 0) return;
|
|
1596
|
+
await transport.send(decoded);
|
|
1597
|
+
},
|
|
1598
|
+
listen(handler) {
|
|
1599
|
+
onMessage = handler;
|
|
1600
|
+
},
|
|
1601
|
+
closed(handler) {
|
|
1602
|
+
onClosed = handler;
|
|
1603
|
+
},
|
|
1604
|
+
async close() {
|
|
1605
|
+
await transport.close();
|
|
1606
|
+
}
|
|
1607
|
+
};
|
|
1608
|
+
}
|
|
1609
|
+
/**
|
|
1927
1610
|
* Creates the MCP Streamable-HTTP transport routes — mounts a transport-agnostic
|
|
1928
1611
|
* {@link MCPDispatcherInterface} (the `@orkestrel/mcp` dispatch boundary) on the fetch-standard router
|
|
1929
1612
|
* spine, pumping each `POST` body through `mcp.dispatch`. Returns the {@link RouteInput}s to
|
|
@@ -1949,7 +1632,7 @@ function createMCPContinuation(secret) {
|
|
|
1949
1632
|
* When `streaming` is enabled (the default) and the client `Accept`s `text/event-stream`,
|
|
1950
1633
|
* the `200` reply is framed as a Streamable-HTTP SSE response (one `data:` event carrying
|
|
1951
1634
|
* the JSON-RPC envelope, then the stream ends) through `@orkestrel/server`'s generic
|
|
1952
|
-
* {@link import('@orkestrel/server').
|
|
1635
|
+
* {@link import('@orkestrel/server').createStream} seam; otherwise it is a plain JSON body.
|
|
1953
1636
|
*
|
|
1954
1637
|
* **Sessions are a SEPARATE, plug-and-play middleware.** `createMCPRoutes` mints / reads no
|
|
1955
1638
|
* session id. To make the transport STATEFUL, mount {@link
|
|
@@ -1988,10 +1671,15 @@ function createMCPRoutes(mcp, options) {
|
|
|
1988
1671
|
}
|
|
1989
1672
|
/**
|
|
1990
1673
|
* Creates the HTTP CLIENT transport for an {@link import('@orkestrel/mcp').MCPClientInterface}
|
|
1991
|
-
* — a {@link
|
|
1674
|
+
* — a {@link MCPMessageTransportInterface} that drives a REMOTE Streamable-HTTP MCP server
|
|
1992
1675
|
* over `fetch`. The egress mirror of {@link createMCPRoutes}.
|
|
1993
1676
|
*
|
|
1994
1677
|
* @remarks
|
|
1678
|
+
* It returns the core {@link import('@orkestrel/mcp').HTTPClientTransport}, the same class the
|
|
1679
|
+
* browser face's `createHTTPClientTransport` returns, because the class touches `fetch`,
|
|
1680
|
+
* `Response`, `AbortController`, `AbortSignal`, and `WeakMap` alone.
|
|
1681
|
+
*
|
|
1682
|
+
* @remarks
|
|
1995
1683
|
* Hand it to `createMCPClient({ transport })`: each JSON-RPC message the client sends is
|
|
1996
1684
|
* `POST`ed to `options.url` with `content-type: application/json` and an `Accept` of
|
|
1997
1685
|
* both `application/json` and `text/event-stream` (the server answers with EITHER — a
|
|
@@ -2007,7 +1695,7 @@ function createMCPRoutes(mcp, options) {
|
|
|
2007
1695
|
* @param options - `url` (the remote endpoint; REQUIRED), optional `headers` merged onto
|
|
2008
1696
|
* every request, optional `fetch` (default `globalThis.fetch`), and optional `timeout`
|
|
2009
1697
|
* (ms, applied with `AbortSignal.timeout`); see {@link HTTPClientTransportOptions}
|
|
2010
|
-
* @returns A working {@link
|
|
1698
|
+
* @returns A working {@link MCPMessageTransportInterface} over `fetch`
|
|
2011
1699
|
*
|
|
2012
1700
|
* @example
|
|
2013
1701
|
* ```ts
|
|
@@ -2022,7 +1710,7 @@ function createMCPRoutes(mcp, options) {
|
|
|
2022
1710
|
* ```
|
|
2023
1711
|
*/
|
|
2024
1712
|
function createHTTPClientTransport(options) {
|
|
2025
|
-
return new HTTPClientTransport(options);
|
|
1713
|
+
return new _src_core.HTTPClientTransport(options);
|
|
2026
1714
|
}
|
|
2027
1715
|
/**
|
|
2028
1716
|
* Creates the MCP WebSocket transport INGRESS — an {@link UpgradeHandler} that exposes a
|
|
@@ -2044,7 +1732,7 @@ function createHTTPClientTransport(options) {
|
|
|
2044
1732
|
* only when the client's offer contains it, and sends UNMASKED frames), wraps it in a
|
|
2045
1733
|
* {@link WebSocketServerTransport}, and pipes it through the core {@link
|
|
2046
1734
|
* import('@orkestrel/mcp').MCPTransportInterface} port through {@link
|
|
2047
|
-
*
|
|
1735
|
+
* createDuplexServerTransport} + {@link import('@orkestrel/mcp').bindServer}:
|
|
2048
1736
|
* each inbound REQUEST runs through `mcp.dispatch`, and a defined response is written back
|
|
2049
1737
|
* as a frame — a NOTIFICATION sends nothing, and a non-request message (a stray response) is
|
|
2050
1738
|
* ignored. A `dispatch` / `send` fault surfaces on `mcp.emitter`'s `error` event rather than
|
|
@@ -2080,7 +1768,7 @@ function createHTTPClientTransport(options) {
|
|
|
2080
1768
|
*/
|
|
2081
1769
|
function createWebSocketServer(mcp, options) {
|
|
2082
1770
|
const path = options.path ?? "/mcp";
|
|
2083
|
-
const subprotocol = options.subprotocol ??
|
|
1771
|
+
const subprotocol = options.subprotocol ?? _src_core.MCP_WEBSOCKET_SUBPROTOCOL;
|
|
2084
1772
|
const live = /* @__PURE__ */ new Map();
|
|
2085
1773
|
options.emitter.on("stop", () => {
|
|
2086
1774
|
for (const [transport, unbind] of live) {
|
|
@@ -2104,7 +1792,7 @@ function createWebSocketServer(mcp, options) {
|
|
|
2104
1792
|
head,
|
|
2105
1793
|
...protocol === void 0 ? {} : { protocol }
|
|
2106
1794
|
}));
|
|
2107
|
-
const unbind = (0, _src_core.bindServer)(mcp,
|
|
1795
|
+
const unbind = (0, _src_core.bindServer)(mcp, createDuplexServerTransport(transport));
|
|
2108
1796
|
live.set(transport, unbind);
|
|
2109
1797
|
transport.emitter.on("close", () => {
|
|
2110
1798
|
live.delete(transport);
|
|
@@ -2116,7 +1804,7 @@ function createWebSocketServer(mcp, options) {
|
|
|
2116
1804
|
}
|
|
2117
1805
|
/**
|
|
2118
1806
|
* Creates the WebSocket CLIENT transport for an {@link import('@orkestrel/mcp').MCPClientInterface}
|
|
2119
|
-
* — a {@link
|
|
1807
|
+
* — a {@link MCPMessageTransportInterface} that drives a REMOTE MCP server over a WebSocket. The
|
|
2120
1808
|
* egress mirror of {@link createWebSocketServer} and the WebSocket sibling of {@link
|
|
2121
1809
|
* createHTTPClientTransport}.
|
|
2122
1810
|
*
|
|
@@ -2132,7 +1820,7 @@ function createWebSocketServer(mcp, options) {
|
|
|
2132
1820
|
*
|
|
2133
1821
|
* @param options - `url` (the remote WebSocket endpoint; REQUIRED) and optional `headers`
|
|
2134
1822
|
* merged onto the upgrade request; see {@link WebSocketClientTransportOptions}
|
|
2135
|
-
* @returns A working {@link
|
|
1823
|
+
* @returns A working {@link MCPMessageTransportInterface} over a WebSocket
|
|
2136
1824
|
*
|
|
2137
1825
|
* @example
|
|
2138
1826
|
* ```ts
|
|
@@ -2198,7 +1886,7 @@ function createStdioClientTransport(options) {
|
|
|
2198
1886
|
* Wraps `options.input` (default `process.stdin`) / `options.output` (default
|
|
2199
1887
|
* `process.stdout`) in a {@link import('./transports/StdioServerTransport.js').StdioServerTransport}
|
|
2200
1888
|
* and pipes it through the core {@link import('@orkestrel/mcp').MCPTransportInterface} port
|
|
2201
|
-
* through {@link
|
|
1889
|
+
* through {@link createDuplexServerTransport} + {@link
|
|
2202
1890
|
* import('@orkestrel/mcp').bindServer}: each inbound REQUEST runs through `mcp.dispatch`, and
|
|
2203
1891
|
* a defined response is written back as a newline-terminated line — a NOTIFICATION
|
|
2204
1892
|
* writes nothing, and a non-request message is ignored. A `dispatch` / `send` fault
|
|
@@ -2224,7 +1912,7 @@ function createStdioClientTransport(options) {
|
|
|
2224
1912
|
*/
|
|
2225
1913
|
function createStdioServer(mcp, options) {
|
|
2226
1914
|
const transport = new StdioServerTransport(options?.input ?? process.stdin, options?.output ?? process.stdout);
|
|
2227
|
-
const unbind = (0, _src_core.bindServer)(mcp,
|
|
1915
|
+
const unbind = (0, _src_core.bindServer)(mcp, createDuplexServerTransport(transport));
|
|
2228
1916
|
return {
|
|
2229
1917
|
start() {
|
|
2230
1918
|
transport.start();
|
|
@@ -2256,8 +1944,9 @@ function createStdioServer(mcp, options) {
|
|
|
2256
1944
|
* can re-read it from a freshly-built forwarded `Request`). Resolves a session through {@link
|
|
2257
1945
|
* readSessionHeader}: a VALID id touches the entry and sets `context.state.session`; an
|
|
2258
1946
|
* ABSENT / unknown id whose (guarded) body parses to an `initialize` request ({@link
|
|
2259
|
-
* isInitializeRequest}) MINTS a fresh {@link MCPSession} (`crypto.randomUUID()`, `
|
|
2260
|
-
* and sets `context.state.session`; neither → {@link rejectUnknownSession}
|
|
1947
|
+
* isInitializeRequest}) MINTS a fresh {@link MCPSession} (`crypto.randomUUID()`, the `session`
|
|
1948
|
+
* options group) and sets `context.state.session`; neither → {@link rejectUnknownSession}
|
|
1949
|
+
* (`404`). The
|
|
2261
1950
|
* minted entry pins the negotiated legacy revision, which is supplied to a later headerless
|
|
2262
1951
|
* live-session request. It then
|
|
2263
1952
|
* FORWARDS a fresh `Request` carrying the buffered `text` (`next(forwarded)`) — never the
|
|
@@ -2268,7 +1957,7 @@ function createStdioServer(mcp, options) {
|
|
|
2268
1957
|
* a `DELETE` arriving while the request was suspended is not undone.
|
|
2269
1958
|
* - **`GET {path}`.** Resolves the session the same way (no mint — only `initialize` mints);
|
|
2270
1959
|
* an invalid / unknown id is the same `404`. A valid session opens the resumable
|
|
2271
|
-
* server→client stream through `@orkestrel/server`'s {@link import('@orkestrel/server').
|
|
1960
|
+
* server→client stream through `@orkestrel/server`'s {@link import('@orkestrel/server').createStream}:
|
|
2272
1961
|
* replays every event after the client's `Last-Event-ID` ({@link readLastEventId}) BEFORE
|
|
2273
1962
|
* attaching the stream for live pushes, then attaches; cancellation of the streamed response
|
|
2274
1963
|
* body composes with `request.signal` and detaches it. Long-lived — never `end()`ed here.
|
|
@@ -2283,10 +1972,11 @@ function createStdioServer(mcp, options) {
|
|
|
2283
1972
|
* @typeParam TState - The consumer's `TState`, which MUST extend {@link MCPSessionState} so
|
|
2284
1973
|
* the resolved session can be threaded through `context.state.session`
|
|
2285
1974
|
* @param options - Optional `path` (default {@link DEFAULT_MCP_PATH}), `ttl` (idle-session
|
|
2286
|
-
* sweep window, ms — omit for sessions that live until an explicit `DELETE`), `
|
|
2287
|
-
* (the
|
|
2288
|
-
*
|
|
2289
|
-
*
|
|
1975
|
+
* sweep window, ms — omit for sessions that live until an explicit `DELETE`), `session`
|
|
1976
|
+
* (the knobs each minted {@link MCPSession} takes — `capacity`, the log's own `ttl`, and its
|
|
1977
|
+
* `clock`), and `clock` (the deterministic epoch-ms clock this middleware keeps its own
|
|
1978
|
+
* bookkeeping on and hands down to a session that names none; defaults to `Date.now`), plus
|
|
1979
|
+
* the shared `origin` validation options; see {@link MCPSessionMiddlewareOptions}
|
|
2290
1980
|
* @returns A {@link MiddlewareHandler} that mints / validates sessions + serves the resumable
|
|
2291
1981
|
* `GET` / `DELETE`
|
|
2292
1982
|
*
|
|
@@ -2304,7 +1994,7 @@ function createStdioServer(mcp, options) {
|
|
|
2304
1994
|
*/
|
|
2305
1995
|
function createMCPSession(options) {
|
|
2306
1996
|
const path = options?.path ?? "/mcp";
|
|
2307
|
-
const
|
|
1997
|
+
const sessionOptions = options?.session ?? {};
|
|
2308
1998
|
const ttl = options?.ttl;
|
|
2309
1999
|
const clock = options?.clock ?? Date.now;
|
|
2310
2000
|
const origin = options?.origin;
|
|
@@ -2317,10 +2007,10 @@ function createMCPSession(options) {
|
|
|
2317
2007
|
if (context.method === "POST") {
|
|
2318
2008
|
try {
|
|
2319
2009
|
text = await request.text();
|
|
2320
|
-
parsed = (0, _src_core.parseJSONRPCMessage)(JSON.parse(text));
|
|
2321
2010
|
} catch {
|
|
2322
|
-
|
|
2011
|
+
text = void 0;
|
|
2323
2012
|
}
|
|
2013
|
+
parsed = text === void 0 ? void 0 : (0, _src_core.parseJSONRPCMessage)((0, _orkestrel_contract.parseJSON)(text));
|
|
2324
2014
|
if (text !== void 0 && parsed !== void 0 && (0, _src_core.isModernRequest)(parsed)) return next(new Request(context.url, {
|
|
2325
2015
|
method: "POST",
|
|
2326
2016
|
headers: request.headers,
|
|
@@ -2353,7 +2043,7 @@ function createMCPSession(options) {
|
|
|
2353
2043
|
if (context.method === "GET") {
|
|
2354
2044
|
if (entry === void 0) return rejectUnknownSession();
|
|
2355
2045
|
const session = entry.session;
|
|
2356
|
-
const stream = (0, _orkestrel_server.
|
|
2046
|
+
const stream = (0, _orkestrel_server.createStream)();
|
|
2357
2047
|
const disconnect = new HTTPDisconnect(request.signal, options?.keepalive);
|
|
2358
2048
|
stream.response.headers.set(SSE_BUFFERING_HEADER, "no");
|
|
2359
2049
|
stream.comment("open");
|
|
@@ -2372,7 +2062,10 @@ function createMCPSession(options) {
|
|
|
2372
2062
|
if (entry === void 0) {
|
|
2373
2063
|
if (parsed !== void 0 && (0, _src_core.isInitializeRequest)(parsed)) {
|
|
2374
2064
|
created = {
|
|
2375
|
-
session: new MCPSession(crypto.randomUUID(),
|
|
2065
|
+
session: new MCPSession(crypto.randomUUID(), {
|
|
2066
|
+
...sessionOptions,
|
|
2067
|
+
clock: sessionOptions.clock ?? clock
|
|
2068
|
+
}),
|
|
2376
2069
|
touched: clock(),
|
|
2377
2070
|
version: inferLegacyVersion(parsed)
|
|
2378
2071
|
};
|
|
@@ -2382,8 +2075,8 @@ function createMCPSession(options) {
|
|
|
2382
2075
|
if (!Reflect.set(context.state, "session", entry.session)) throw new Error("MCP session state is not writable");
|
|
2383
2076
|
const headers = new Headers(request.headers);
|
|
2384
2077
|
if (parsed === void 0 || !(0, _src_core.isInitializeRequest)(parsed)) {
|
|
2385
|
-
const issue =
|
|
2386
|
-
if (issue?.reason === "missing") headers.set(MCP_PROTOCOL_VERSION_HEADER, entry.version);
|
|
2078
|
+
const issue = inferSessionHeaderIssue(request, entry.version);
|
|
2079
|
+
if (issue?.reason === "missing") headers.set(_src_core.MCP_PROTOCOL_VERSION_HEADER, entry.version);
|
|
2387
2080
|
else if (issue !== void 0) {
|
|
2388
2081
|
const requestId = parsed !== void 0 && "method" in parsed ? parsed.id : void 0;
|
|
2389
2082
|
return Response.json((0, _src_core.buildJSONRPCError)(requestId, _src_core.MCP_HEADER_MISMATCH, issue.message), { status: 400 });
|
|
@@ -2405,7 +2098,7 @@ function createMCPSession(options) {
|
|
|
2405
2098
|
...entry,
|
|
2406
2099
|
touched: clock()
|
|
2407
2100
|
});
|
|
2408
|
-
response.headers.set(MCP_SESSION_HEADER, entry.session.id);
|
|
2101
|
+
response.headers.set(_src_core.MCP_SESSION_HEADER, entry.session.id);
|
|
2409
2102
|
return response;
|
|
2410
2103
|
};
|
|
2411
2104
|
}
|
|
@@ -2415,14 +2108,8 @@ exports.DEFAULT_MCP_KEEPALIVE_INTERVAL = DEFAULT_MCP_KEEPALIVE_INTERVAL;
|
|
|
2415
2108
|
exports.DEFAULT_MCP_PATH = DEFAULT_MCP_PATH;
|
|
2416
2109
|
exports.DEFAULT_MCP_SESSION_CAPACITY = DEFAULT_MCP_SESSION_CAPACITY;
|
|
2417
2110
|
exports.DEFAULT_MCP_SESSION_TTL = DEFAULT_MCP_SESSION_TTL;
|
|
2418
|
-
exports.HTTPClientTransport = HTTPClientTransport;
|
|
2419
2111
|
exports.HTTPDisconnect = HTTPDisconnect;
|
|
2420
2112
|
exports.MCPSession = MCPSession;
|
|
2421
|
-
exports.MCP_METHOD_HEADER = MCP_METHOD_HEADER;
|
|
2422
|
-
exports.MCP_NAME_HEADER = MCP_NAME_HEADER;
|
|
2423
|
-
exports.MCP_PROTOCOL_VERSION_HEADER = MCP_PROTOCOL_VERSION_HEADER;
|
|
2424
|
-
exports.MCP_SESSION_HEADER = MCP_SESSION_HEADER;
|
|
2425
|
-
exports.MCP_WEBSOCKET_SUBPROTOCOL = MCP_WEBSOCKET_SUBPROTOCOL;
|
|
2426
2113
|
exports.SSE_BUFFERING_DISABLED = SSE_BUFFERING_DISABLED;
|
|
2427
2114
|
exports.SSE_BUFFERING_HEADER = SSE_BUFFERING_HEADER;
|
|
2428
2115
|
exports.SSE_KEEPALIVE_COMMENT = SSE_KEEPALIVE_COMMENT;
|
|
@@ -2432,27 +2119,24 @@ exports.WebSocketClientTransport = WebSocketClientTransport;
|
|
|
2432
2119
|
exports.WebSocketServerTransport = WebSocketServerTransport;
|
|
2433
2120
|
exports.acceptsEventStream = acceptsEventStream;
|
|
2434
2121
|
exports.allowsOrigin = allowsOrigin;
|
|
2435
|
-
exports.
|
|
2436
|
-
exports.buildResponseError = buildResponseError;
|
|
2122
|
+
exports.createDuplexServerTransport = createDuplexServerTransport;
|
|
2437
2123
|
exports.createHTTPClientTransport = createHTTPClientTransport;
|
|
2438
2124
|
exports.createMCPContinuation = createMCPContinuation;
|
|
2439
2125
|
exports.createMCPPostHandler = createMCPPostHandler;
|
|
2440
2126
|
exports.createMCPRoutes = createMCPRoutes;
|
|
2441
2127
|
exports.createMCPSession = createMCPSession;
|
|
2442
|
-
exports.createReadableStream = createReadableStream;
|
|
2443
2128
|
exports.createStdioClientTransport = createStdioClientTransport;
|
|
2444
2129
|
exports.createStdioServer = createStdioServer;
|
|
2445
2130
|
exports.createWebSocketClientTransport = createWebSocketClientTransport;
|
|
2446
2131
|
exports.createWebSocketServer = createWebSocketServer;
|
|
2447
|
-
exports.decodeEvent = decodeEvent;
|
|
2448
2132
|
exports.dispatchLines = dispatchLines;
|
|
2449
2133
|
exports.extractLines = extractLines;
|
|
2450
2134
|
exports.inferHeaderIssue = inferHeaderIssue;
|
|
2451
2135
|
exports.inferHeaderTarget = inferHeaderTarget;
|
|
2452
2136
|
exports.inferLegacyVersion = inferLegacyVersion;
|
|
2453
2137
|
exports.inferParameterRefusal = inferParameterRefusal;
|
|
2138
|
+
exports.inferSessionHeaderIssue = inferSessionHeaderIssue;
|
|
2454
2139
|
exports.inferStatus = inferStatus;
|
|
2455
|
-
exports.readEventStream = readEventStream;
|
|
2456
2140
|
exports.readLastEventId = readLastEventId;
|
|
2457
2141
|
exports.readSessionHeader = readSessionHeader;
|
|
2458
2142
|
exports.rejectUnknownSession = rejectUnknownSession;
|