@orkestrel/mcp 0.0.4 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/browser/index.d.ts +564 -0
- package/dist/src/browser/index.js +690 -0
- package/dist/src/browser/index.js.map +1 -0
- package/dist/src/core/index.cjs +277 -39
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +211 -17
- package/dist/src/core/index.d.ts +211 -17
- package/dist/src/core/index.js +273 -40
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +225 -139
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +138 -51
- package/dist/src/server/index.d.ts +138 -51
- package/dist/src/server/index.js +226 -142
- package/dist/src/server/index.js.map +1 -1
- package/package.json +38 -25
|
@@ -2,13 +2,13 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
2
2
|
let _orkestrel_sse = require("@orkestrel/sse");
|
|
3
3
|
let _src_core = require("../core/index.cjs");
|
|
4
4
|
let _orkestrel_contract = require("@orkestrel/contract");
|
|
5
|
+
let _orkestrel_server = require("@orkestrel/server");
|
|
5
6
|
let _orkestrel_emitter = require("@orkestrel/emitter");
|
|
6
7
|
let node_crypto = require("node:crypto");
|
|
7
8
|
let node_http = require("node:http");
|
|
8
9
|
let node_https = require("node:https");
|
|
9
10
|
let _orkestrel_websocket = require("@orkestrel/websocket");
|
|
10
11
|
let node_child_process = require("node:child_process");
|
|
11
|
-
let _orkestrel_server = require("@orkestrel/server");
|
|
12
12
|
//#region src/server/constants.ts
|
|
13
13
|
/**
|
|
14
14
|
* The Streamable-HTTP transport header that carries the MCP session id. When a {@link
|
|
@@ -18,10 +18,13 @@ let _orkestrel_server = require("@orkestrel/server");
|
|
|
18
18
|
*/
|
|
19
19
|
var MCP_SESSION_HEADER = "mcp-session-id";
|
|
20
20
|
/**
|
|
21
|
-
* The Streamable-HTTP transport header
|
|
22
|
-
* on
|
|
23
|
-
*
|
|
24
|
-
*
|
|
21
|
+
* The Streamable-HTTP transport header carrying the negotiated MCP protocol version
|
|
22
|
+
* on every post-initialize client request.
|
|
23
|
+
*
|
|
24
|
+
* @remarks
|
|
25
|
+
* Required by MCP 2025-06-18 after initialization. Both HTTP client transports
|
|
26
|
+
* capture the initialize result's `protocolVersion` and send it on subsequent
|
|
27
|
+
* requests; `createMCPRoutes` rejects a present unsupported value before dispatch.
|
|
25
28
|
*/
|
|
26
29
|
var MCP_PROTOCOL_VERSION_HEADER = "mcp-protocol-version";
|
|
27
30
|
/** The default request path `createMCPRoutes` mounts the transport's `POST` route at. */
|
|
@@ -264,6 +267,131 @@ function dispatchLines(emitter, lines) {
|
|
|
264
267
|
emitter.emit("message", message);
|
|
265
268
|
}
|
|
266
269
|
}
|
|
270
|
+
/**
|
|
271
|
+
* Bridge a message-channel {@link ClientTransportInterface} (the shape the stdio and
|
|
272
|
+
* WebSocket SERVER transports already implement) into the environment-agnostic
|
|
273
|
+
* {@link import('@src/core').MCPTransportInterface} port — the adapter
|
|
274
|
+
* {@link import('./factories.js').createStdioServer} and {@link
|
|
275
|
+
* import('./factories.js').createWebSocketServer} pipe through `bindServer`, so the
|
|
276
|
+
* request/reply/error pump those two factories used to hand-roll identically now
|
|
277
|
+
* lives ONCE in the core binder.
|
|
278
|
+
*
|
|
279
|
+
* @remarks
|
|
280
|
+
* `send` decodes the already-serialized reply string back to a {@link JSONRPCMessage}
|
|
281
|
+
* and writes it via `transport.send` (the SAME `JSON.stringify` the underlying
|
|
282
|
+
* transport already performs, so the wire bytes are unchanged). `listen` filters
|
|
283
|
+
* `transport`'s `message` event to REQUESTS ONLY — a stray response is ignored,
|
|
284
|
+
* exactly as the prior hand-rolled pumps did — and re-serializes each one back to a
|
|
285
|
+
* string for `bindServer`. `closed` bridges `transport`'s `close` event. `close`
|
|
286
|
+
* closes the underlying `transport`.
|
|
287
|
+
*
|
|
288
|
+
* @remarks Per {@link import('@src/core').MCPTransportInterface}, `listen`/`closed`
|
|
289
|
+
* each hold THE SINGLE current handler (a second call REPLACES the first, never adds).
|
|
290
|
+
* Since the underlying `transport.emitter` is ADD-based (`on` subscribes, never
|
|
291
|
+
* replaces), this bridge installs ONE stable emitter listener per event on first use
|
|
292
|
+
* and re-routes it to whichever handler is CURRENTLY registered (`undefined` while
|
|
293
|
+
* none is), so rebinding never double-dispatches.
|
|
294
|
+
*
|
|
295
|
+
* @remarks A response whose `result` serializes away (e.g. `undefined`) is dropped by
|
|
296
|
+
* the message validators on the wire's decode side — an asymmetry the stdio/WS carrier
|
|
297
|
+
* shares with the streamable-HTTP face, since both round-trip through `JSON.stringify`
|
|
298
|
+
* / `JSON.parse` before re-validation.
|
|
299
|
+
*
|
|
300
|
+
* @param transport - The message-channel transport to bridge (stdio or WebSocket)
|
|
301
|
+
* @returns An {@link import('@src/core').MCPTransportInterface} `bindServer` can drive
|
|
302
|
+
*
|
|
303
|
+
* @example
|
|
304
|
+
* ```ts
|
|
305
|
+
* import { bindServer } from '@src/core'
|
|
306
|
+
*
|
|
307
|
+
* const transport = new StdioServerTransport(process.stdin, process.stdout)
|
|
308
|
+
* bindServer(mcp, bridgeMessageTransport(transport))
|
|
309
|
+
* ```
|
|
310
|
+
*/
|
|
311
|
+
function bridgeMessageTransport(transport) {
|
|
312
|
+
let onMessage;
|
|
313
|
+
let onClosed;
|
|
314
|
+
transport.emitter.on("message", (message) => {
|
|
315
|
+
if (!(0, _src_core.isJSONRPCRequest)(message)) return;
|
|
316
|
+
onMessage?.(JSON.stringify(message));
|
|
317
|
+
});
|
|
318
|
+
transport.emitter.on("close", () => {
|
|
319
|
+
onClosed?.();
|
|
320
|
+
});
|
|
321
|
+
return {
|
|
322
|
+
async send(message) {
|
|
323
|
+
const decoded = decodeEvent(message);
|
|
324
|
+
if (decoded === void 0) return;
|
|
325
|
+
await transport.send(decoded);
|
|
326
|
+
},
|
|
327
|
+
listen(handler) {
|
|
328
|
+
onMessage = handler;
|
|
329
|
+
},
|
|
330
|
+
closed(handler) {
|
|
331
|
+
onClosed = handler;
|
|
332
|
+
},
|
|
333
|
+
async close() {
|
|
334
|
+
await transport.close();
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
//#endregion
|
|
339
|
+
//#region src/server/handlers.ts
|
|
340
|
+
/**
|
|
341
|
+
* Create the Streamable-HTTP POST handler used by `createMCPRoutes`.
|
|
342
|
+
*
|
|
343
|
+
* @remarks
|
|
344
|
+
* A present `mcp-protocol-version` header must name a supported revision; an
|
|
345
|
+
* unsupported value returns an HTTP `400` JSON-RPC invalid-request error without
|
|
346
|
+
* dispatching. An absent header is accepted for the initialize/bootstrap request.
|
|
347
|
+
*
|
|
348
|
+
* @param mcp - The transport-agnostic MCP server to dispatch through
|
|
349
|
+
* @param streaming - Whether an event-stream response may be negotiated
|
|
350
|
+
* @returns A request handler for the stateless MCP POST route
|
|
351
|
+
*
|
|
352
|
+
* @example
|
|
353
|
+
* ```ts
|
|
354
|
+
* import { createMCPServer } from '@orkestrel/mcp'
|
|
355
|
+
* import { createMCPPostHandler } from '@orkestrel/mcp/server'
|
|
356
|
+
* import { createToolManager } from '@orkestrel/agent'
|
|
357
|
+
*
|
|
358
|
+
* const mcp = createMCPServer({ name: 'docs', version: '1.0.0', tools: createToolManager() })
|
|
359
|
+
* const handler = createMCPPostHandler(mcp, true)
|
|
360
|
+
* await handler(new Request('http://localhost/mcp', {
|
|
361
|
+
* method: 'POST',
|
|
362
|
+
* body: '{"jsonrpc":"2.0","method":"ping","id":1}',
|
|
363
|
+
* }))
|
|
364
|
+
* ```
|
|
365
|
+
*/
|
|
366
|
+
function createMCPPostHandler(mcp, streaming) {
|
|
367
|
+
return async (request) => {
|
|
368
|
+
const protocol = request.headers.get(MCP_PROTOCOL_VERSION_HEADER);
|
|
369
|
+
if (protocol !== null && !_src_core.SUPPORTED_PROTOCOL_VERSIONS.includes(protocol)) return Response.json((0, _src_core.jsonRPCError)(null, _src_core.JSONRPC_INVALID_REQUEST, `Unsupported MCP protocol version '${protocol}'`), { status: 400 });
|
|
370
|
+
let text;
|
|
371
|
+
try {
|
|
372
|
+
text = await request.text();
|
|
373
|
+
} catch {
|
|
374
|
+
return Response.json((0, _src_core.jsonRPCError)(null, _src_core.JSONRPC_PARSE_ERROR, "Parse error"), { status: 400 });
|
|
375
|
+
}
|
|
376
|
+
let parsed;
|
|
377
|
+
try {
|
|
378
|
+
parsed = JSON.parse(text);
|
|
379
|
+
} catch {
|
|
380
|
+
return Response.json((0, _src_core.jsonRPCError)(null, _src_core.JSONRPC_PARSE_ERROR, "Parse error"), { status: 400 });
|
|
381
|
+
}
|
|
382
|
+
const rpcRequest = (0, _src_core.parseJSONRPCMessage)(parsed);
|
|
383
|
+
if (rpcRequest === void 0 || !("method" in rpcRequest)) return Response.json((0, _src_core.jsonRPCError)(null, _src_core.JSONRPC_INVALID_REQUEST, "Invalid Request"), { status: 400 });
|
|
384
|
+
const response = await mcp.dispatch(rpcRequest);
|
|
385
|
+
if (response === void 0) return new Response(null, { status: 202 });
|
|
386
|
+
if (streaming && acceptsEventStream(request)) {
|
|
387
|
+
const stream = (0, _orkestrel_server.openStream)();
|
|
388
|
+
stream.write({ data: JSON.stringify(response) });
|
|
389
|
+
stream.end();
|
|
390
|
+
return stream.response;
|
|
391
|
+
}
|
|
392
|
+
return Response.json(response);
|
|
393
|
+
};
|
|
394
|
+
}
|
|
267
395
|
//#endregion
|
|
268
396
|
//#region src/server/transports/HTTPClientTransport.ts
|
|
269
397
|
/**
|
|
@@ -273,7 +401,7 @@ function dispatchLines(emitter, lines) {
|
|
|
273
401
|
*
|
|
274
402
|
* @remarks
|
|
275
403
|
* - **Request/response over `fetch`.** `send(message)` POSTs the JSON-serialized
|
|
276
|
-
* message
|
|
404
|
+
* message to `options.url` with `content-type: application/json` and an
|
|
277
405
|
* `Accept` of BOTH `application/json` and `text/event-stream` (so the server may
|
|
278
406
|
* answer with either framing) — plus any `options.headers` (e.g. an `Authorization`
|
|
279
407
|
* bearer). It then decodes the reply and emits each decoded {@link JSONRPCMessage} on
|
|
@@ -285,13 +413,18 @@ function dispatchLines(emitter, lines) {
|
|
|
285
413
|
* readEventStream}) — the inverse of the server's `openStream` seam, so the wire
|
|
286
414
|
* round-trips. A `202`
|
|
287
415
|
* Accepted (a notification) carries no body and emits nothing.
|
|
288
|
-
* - **Session echo.** `start()`
|
|
289
|
-
*
|
|
290
|
-
* STATEFUL server sends one (on
|
|
291
|
-
*
|
|
292
|
-
* `
|
|
293
|
-
*
|
|
294
|
-
*
|
|
416
|
+
* - **Session and protocol echo.** `start()` is a no-op (a
|
|
417
|
+
* request/response transport opens no long-lived connection). The
|
|
418
|
+
* `mcp-session-id` response header, when a STATEFUL server sends one (on
|
|
419
|
+
* `initialize`), is captured into `session` and then ECHOED as the
|
|
420
|
+
* `mcp-session-id` request header on every SUBSEQUENT request — so an
|
|
421
|
+
* `MCPClient` passes a stateful server's session validation. The
|
|
422
|
+
* initialize result's `protocolVersion` is likewise captured, but only
|
|
423
|
+
* when it is a SUPPORTED value, and echoed as `mcp-protocol-version` on
|
|
424
|
+
* every subsequent request, as required by the 2025-06-18 Streamable-HTTP
|
|
425
|
+
* transport. Before initialize returns, neither captured header is sent.
|
|
426
|
+
* `close()` clears the captured protocol so a reconnect's `initialize`
|
|
427
|
+
* POST is headerless; the captured `session` persists across `close()`.
|
|
295
428
|
* - **Total at the boundary (§14).** Every reply is narrowed (`parseJSONRPCMessage`,
|
|
296
429
|
* the SSE decoder) — a non-message reply is dropped, never asserted; a `fetch` /
|
|
297
430
|
* decode failure surfaces on the `error` event rather than escaping `send`.
|
|
@@ -312,6 +445,7 @@ var HTTPClientTransport = class {
|
|
|
312
445
|
#fetch;
|
|
313
446
|
#timeout;
|
|
314
447
|
#session = void 0;
|
|
448
|
+
#protocol = void 0;
|
|
315
449
|
constructor(options) {
|
|
316
450
|
this.#emitter = new _orkestrel_emitter.Emitter();
|
|
317
451
|
this.#url = options.url;
|
|
@@ -335,6 +469,7 @@ var HTTPClientTransport = class {
|
|
|
335
469
|
"content-type": "application/json",
|
|
336
470
|
accept: "application/json, text/event-stream",
|
|
337
471
|
...this.#session === void 0 ? {} : { [MCP_SESSION_HEADER]: this.#session },
|
|
472
|
+
...this.#protocol === void 0 ? {} : { [MCP_PROTOCOL_VERSION_HEADER]: this.#protocol },
|
|
338
473
|
...this.#headers
|
|
339
474
|
},
|
|
340
475
|
body: JSON.stringify(message),
|
|
@@ -349,6 +484,7 @@ var HTTPClientTransport = class {
|
|
|
349
484
|
await this.#deliver(response);
|
|
350
485
|
}
|
|
351
486
|
async close() {
|
|
487
|
+
this.#protocol = void 0;
|
|
352
488
|
this.#emitter.emit("close");
|
|
353
489
|
}
|
|
354
490
|
async #deliver(response) {
|
|
@@ -356,17 +492,21 @@ var HTTPClientTransport = class {
|
|
|
356
492
|
const type = response.headers.get("content-type") ?? "";
|
|
357
493
|
try {
|
|
358
494
|
if (type.includes("text/event-stream")) {
|
|
359
|
-
for (const message of await readEventStream(response)) this.#
|
|
495
|
+
for (const message of await readEventStream(response)) this.#capture(message);
|
|
360
496
|
return;
|
|
361
497
|
}
|
|
362
498
|
if (type.includes("application/json")) {
|
|
363
499
|
const message = (0, _src_core.parseJSONRPCMessage)(await response.json());
|
|
364
|
-
if (message !== void 0) this.#
|
|
500
|
+
if (message !== void 0) this.#capture(message);
|
|
365
501
|
}
|
|
366
502
|
} catch (error) {
|
|
367
503
|
this.#emitter.emit("error", error);
|
|
368
504
|
}
|
|
369
505
|
}
|
|
506
|
+
#capture(message) {
|
|
507
|
+
if ((0, _src_core.isJSONRPCResponse)(message) && (0, _orkestrel_contract.isRecord)(message.result) && (0, _orkestrel_contract.isString)(message.result["protocolVersion"]) && _src_core.SUPPORTED_PROTOCOL_VERSIONS.includes(message.result["protocolVersion"])) this.#protocol = message.result["protocolVersion"];
|
|
508
|
+
this.#emitter.emit("message", message);
|
|
509
|
+
}
|
|
370
510
|
};
|
|
371
511
|
//#endregion
|
|
372
512
|
//#region src/server/MCPSession.ts
|
|
@@ -508,8 +648,8 @@ var MCPSession = class {
|
|
|
508
648
|
* parsed envelope the {@link import('@src/core').MCPServerInterface} pump dispatches), while
|
|
509
649
|
* a non-JSON or non-message frame is surfaced on `error` and DROPPED, never thrown (§14). It
|
|
510
650
|
* also bridges the socket's `close` → this transport's `close`, and the socket's `error`.
|
|
511
|
-
* - **Outbound (`send`).** `send(message
|
|
512
|
-
* (`nodeWs.send(JSON.stringify(
|
|
651
|
+
* - **Outbound (`send`).** `send(message)` writes one text frame
|
|
652
|
+
* (`nodeWs.send(JSON.stringify(message))`); the underlying wrapper no-ops a write on a
|
|
513
653
|
* non-open socket, so a closed connection drops silently rather than throwing.
|
|
514
654
|
* - **`close()`** closes the underlying socket (the RFC 6455 close handshake) and fires the
|
|
515
655
|
* transport's `close` event (idempotent — a second `close`, or a socket-driven close, emits
|
|
@@ -539,8 +679,7 @@ var WebSocketServerTransport = class {
|
|
|
539
679
|
this.#socket.emitter.on("error", (error) => this.#emitter.emit("error", error));
|
|
540
680
|
}
|
|
541
681
|
async send(message) {
|
|
542
|
-
|
|
543
|
-
for (const one of messages) this.#socket.send(JSON.stringify(one));
|
|
682
|
+
this.#socket.send(JSON.stringify(message));
|
|
544
683
|
}
|
|
545
684
|
async close() {
|
|
546
685
|
if (this.#closed) return;
|
|
@@ -591,7 +730,7 @@ var WebSocketServerTransport = class {
|
|
|
591
730
|
* event (the reply the {@link import('@src/core').MCPClientInterface} correlates by `id`); a
|
|
592
731
|
* non-JSON / non-message frame surfaces on `error` and is dropped (§14). The socket's `close`
|
|
593
732
|
* / `error` bridge to this transport's events.
|
|
594
|
-
* - **Outbound (`send`).** `send(message
|
|
733
|
+
* - **Outbound (`send`).** `send(message)` writes one masked text frame.
|
|
595
734
|
* - **`close()`** closes the underlying socket and fires `close` (idempotent).
|
|
596
735
|
* - **URL scheme.** `options.url` accepts a `ws://` / `wss://` URL or an `http://` / `https://`
|
|
597
736
|
* one; a `ws(s)` scheme is converted to `http(s)` for the underlying upgrade request (`wss`
|
|
@@ -630,12 +769,6 @@ var WebSocketClientTransport = class {
|
|
|
630
769
|
const secure = url.protocol === "https:";
|
|
631
770
|
const send = secure ? node_https.request : node_http.request;
|
|
632
771
|
await new Promise((resolve, reject) => {
|
|
633
|
-
let settled = false;
|
|
634
|
-
const fail = (error) => {
|
|
635
|
-
if (settled) return;
|
|
636
|
-
settled = true;
|
|
637
|
-
reject(error);
|
|
638
|
-
};
|
|
639
772
|
const request = send({
|
|
640
773
|
hostname: url.hostname,
|
|
641
774
|
port: url.port.length > 0 ? Number(url.port) : secure ? 443 : 80,
|
|
@@ -653,7 +786,7 @@ var WebSocketClientTransport = class {
|
|
|
653
786
|
const accept = response.headers["sec-websocket-accept"];
|
|
654
787
|
if (!(0, _orkestrel_contract.isString)(accept) || accept !== (0, _orkestrel_websocket.computeWebSocketAccept)(key)) {
|
|
655
788
|
socket.destroy();
|
|
656
|
-
|
|
789
|
+
reject(/* @__PURE__ */ new Error("WebSocket handshake failed: Sec-WebSocket-Accept mismatch"));
|
|
657
790
|
return;
|
|
658
791
|
}
|
|
659
792
|
const ws = (0, _orkestrel_websocket.createNodeWebSocket)({
|
|
@@ -662,24 +795,20 @@ var WebSocketClientTransport = class {
|
|
|
662
795
|
});
|
|
663
796
|
this.#socket = ws;
|
|
664
797
|
this.#bind(ws);
|
|
665
|
-
|
|
666
|
-
settled = true;
|
|
667
|
-
resolve();
|
|
668
|
-
}
|
|
798
|
+
resolve();
|
|
669
799
|
});
|
|
670
800
|
request.on("response", (response) => {
|
|
671
801
|
response.resume();
|
|
672
|
-
|
|
802
|
+
reject(/* @__PURE__ */ new Error(`WebSocket upgrade declined with status ${response.statusCode ?? 0}`));
|
|
673
803
|
});
|
|
674
|
-
request.on("error", (error) =>
|
|
804
|
+
request.on("error", (error) => reject(error instanceof Error ? error : new Error(String(error))));
|
|
675
805
|
request.end();
|
|
676
806
|
});
|
|
677
807
|
}
|
|
678
808
|
async send(message) {
|
|
679
809
|
const socket = this.#socket;
|
|
680
810
|
if (socket === void 0) throw new Error("WebSocket transport is not connected");
|
|
681
|
-
|
|
682
|
-
for (const one of messages) socket.send(JSON.stringify(one));
|
|
811
|
+
socket.send(JSON.stringify(message));
|
|
683
812
|
}
|
|
684
813
|
async close() {
|
|
685
814
|
if (this.#closed) return;
|
|
@@ -743,8 +872,8 @@ var WebSocketClientTransport = class {
|
|
|
743
872
|
* {@link dispatchLines} helper — a well-formed {@link JSONRPCMessage} emits
|
|
744
873
|
* `message`, a malformed line emits `error` (§14, never throws). The child's
|
|
745
874
|
* `close` bridges to this transport's `close`.
|
|
746
|
-
* - **Outbound (`send`).** `send(message
|
|
747
|
-
* `JSON.stringify`d line
|
|
875
|
+
* - **Outbound (`send`).** `send(message)` writes one newline-terminated
|
|
876
|
+
* `JSON.stringify`d line to the child's `stdin`.
|
|
748
877
|
* - **`close()`** kills the child process and fires `close` (idempotent).
|
|
749
878
|
* - **Observable (§13).** Owns the `emitter` ({@link ClientTransportEventMap}); the
|
|
750
879
|
* emitter isolates a listener throw; `error` is a DOMAIN event (a transport-level
|
|
@@ -795,8 +924,7 @@ var StdioClientTransport = class {
|
|
|
795
924
|
async send(message) {
|
|
796
925
|
const child = this.#child;
|
|
797
926
|
if (child === void 0) throw new Error("stdio transport is not connected");
|
|
798
|
-
|
|
799
|
-
for (const one of messages) child.stdin.write(`${JSON.stringify(one)}\n`);
|
|
927
|
+
child.stdin.write(`${JSON.stringify(message)}\n`);
|
|
800
928
|
}
|
|
801
929
|
async close() {
|
|
802
930
|
if (this.#closed) return;
|
|
@@ -839,8 +967,8 @@ var StdioClientTransport = class {
|
|
|
839
967
|
* well-formed {@link JSONRPCMessage} re-emits on `message`, a malformed line
|
|
840
968
|
* emits `error` (§14, never throws). `input`'s `close` bridges to this
|
|
841
969
|
* transport's `close`.
|
|
842
|
-
* - **Outbound (`send`).** `send(message
|
|
843
|
-
* `JSON.stringify`d line
|
|
970
|
+
* - **Outbound (`send`).** `send(message)` writes one newline-terminated
|
|
971
|
+
* `JSON.stringify`d line to `output`.
|
|
844
972
|
* - **`close()`** fires this transport's `close` (idempotent) — the injected streams
|
|
845
973
|
* are owned by the caller (typically `process.stdin`/`process.stdout`, which must
|
|
846
974
|
* never be closed out from under the process) and are not torn down here.
|
|
@@ -872,8 +1000,7 @@ var StdioServerTransport = class {
|
|
|
872
1000
|
this.#input.on("error", (error) => this.#emitter.emit("error", error));
|
|
873
1001
|
}
|
|
874
1002
|
async send(message) {
|
|
875
|
-
|
|
876
|
-
for (const one of messages) this.#output.write(`${JSON.stringify(one)}\n`);
|
|
1003
|
+
this.#output.write(`${JSON.stringify(message)}\n`);
|
|
877
1004
|
}
|
|
878
1005
|
async close() {
|
|
879
1006
|
if (this.#closed) return;
|
|
@@ -908,6 +1035,9 @@ var StdioServerTransport = class {
|
|
|
908
1035
|
* - A **transport** failure — a malformed JSON body, or a parsed value that is not a
|
|
909
1036
|
* JSON-RPC REQUEST — is an HTTP `400` carrying a JSON-RPC error BODY (`-32700` Parse
|
|
910
1037
|
* error / `-32600` Invalid Request, id `null`).
|
|
1038
|
+
* - A present `mcp-protocol-version` header is validated before dispatch: a supported
|
|
1039
|
+
* value proceeds, while an unsupported value returns HTTP `400` with a JSON-RPC
|
|
1040
|
+
* `-32600` body. An absent value proceeds for initialize/bootstrap compatibility.
|
|
911
1041
|
* - A **dispatch** result — a success OR an IN-BAND JSON-RPC error from `mcp.dispatch`
|
|
912
1042
|
* (e.g. `-32601` method-not-found) — is an HTTP `200` carrying the JSON-RPC response
|
|
913
1043
|
* envelope (the error is in-band per JSON-RPC, NOT an HTTP error).
|
|
@@ -944,37 +1074,11 @@ var StdioServerTransport = class {
|
|
|
944
1074
|
* ```
|
|
945
1075
|
*/
|
|
946
1076
|
function createMCPRoutes(mcp, options) {
|
|
947
|
-
const path = options?.path ?? "/mcp";
|
|
948
|
-
const streaming = options?.streaming ?? true;
|
|
949
1077
|
return [{
|
|
950
1078
|
method: "POST",
|
|
951
|
-
path,
|
|
1079
|
+
path: options?.path ?? "/mcp",
|
|
952
1080
|
name: "mcp",
|
|
953
|
-
handler:
|
|
954
|
-
let text;
|
|
955
|
-
try {
|
|
956
|
-
text = await request.text();
|
|
957
|
-
} catch {
|
|
958
|
-
return Response.json((0, _src_core.jsonRPCError)(null, _src_core.JSONRPC_PARSE_ERROR, "Parse error"), { status: 400 });
|
|
959
|
-
}
|
|
960
|
-
let parsed;
|
|
961
|
-
try {
|
|
962
|
-
parsed = JSON.parse(text);
|
|
963
|
-
} catch {
|
|
964
|
-
return Response.json((0, _src_core.jsonRPCError)(null, _src_core.JSONRPC_PARSE_ERROR, "Parse error"), { status: 400 });
|
|
965
|
-
}
|
|
966
|
-
const rpcRequest = (0, _src_core.parseJSONRPCMessage)(parsed);
|
|
967
|
-
if (rpcRequest === void 0 || !("method" in rpcRequest)) return Response.json((0, _src_core.jsonRPCError)(null, _src_core.JSONRPC_INVALID_REQUEST, "Invalid Request"), { status: 400 });
|
|
968
|
-
const response = await mcp.dispatch(rpcRequest);
|
|
969
|
-
if (response === void 0) return new Response(null, { status: 202 });
|
|
970
|
-
if (streaming && acceptsEventStream(request)) {
|
|
971
|
-
const s = (0, _orkestrel_server.openStream)();
|
|
972
|
-
s.write({ data: JSON.stringify(response) });
|
|
973
|
-
s.end();
|
|
974
|
-
return s.response;
|
|
975
|
-
}
|
|
976
|
-
return Response.json(response);
|
|
977
|
-
}
|
|
1081
|
+
handler: createMCPPostHandler(mcp, options?.streaming ?? true)
|
|
978
1082
|
}];
|
|
979
1083
|
}
|
|
980
1084
|
/**
|
|
@@ -990,8 +1094,10 @@ function createMCPRoutes(mcp, options) {
|
|
|
990
1094
|
* and the reply is surfaced on the transport's `message` event for the client's id
|
|
991
1095
|
* correlation. Add `options.headers` (e.g. an `Authorization` bearer) to reach a guarded
|
|
992
1096
|
* server. `start` / `close` hold no connection; against a STATEFUL server it captures the
|
|
993
|
-
* `mcp-session-id` from `initialize` and echoes it on later requests
|
|
994
|
-
*
|
|
1097
|
+
* `mcp-session-id` from `initialize` and echoes it on later requests. It also captures
|
|
1098
|
+
* the initialize result's `protocolVersion` and sends `mcp-protocol-version` on every
|
|
1099
|
+
* subsequent request, so the same `MCPClient` passes the session and 2025-06-18
|
|
1100
|
+
* protocol gates without caller wiring.
|
|
995
1101
|
*
|
|
996
1102
|
* @param options - `url` (the remote endpoint; REQUIRED), optional `headers` merged onto
|
|
997
1103
|
* every request, optional `fetch` (default `globalThis.fetch`), and optional `timeout`
|
|
@@ -1031,12 +1137,13 @@ function createHTTPClientTransport(options) {
|
|
|
1031
1137
|
* - **Claims (returns `true`)** otherwise: it builds `createNodeWebSocket({ socket, key, head,
|
|
1032
1138
|
* protocol })` (SERVER mode → writes the `101` handshake, echoing the `subprotocol`, default
|
|
1033
1139
|
* {@link MCP_WEBSOCKET_SUBPROTOCOL} `'mcp'`, and sends UNMASKED frames), wraps it in a
|
|
1034
|
-
* {@link WebSocketServerTransport}, and
|
|
1035
|
-
* import('@src/core').
|
|
1036
|
-
*
|
|
1037
|
-
*
|
|
1038
|
-
*
|
|
1039
|
-
*
|
|
1140
|
+
* {@link WebSocketServerTransport}, and pipes it through the core {@link
|
|
1141
|
+
* import('@src/core').MCPTransportInterface} port via {@link
|
|
1142
|
+
* import('./helpers.js').bridgeMessageTransport} + {@link import('@src/core').bindServer}:
|
|
1143
|
+
* each inbound REQUEST runs through `mcp.dispatch`, and a defined response is written back
|
|
1144
|
+
* as a frame — a NOTIFICATION sends nothing, and a non-request message (a stray response) is
|
|
1145
|
+
* ignored. A `dispatch` / `send` fault surfaces on `mcp.emitter`'s `error` event rather than
|
|
1146
|
+
* escaping the (async) message pump.
|
|
1040
1147
|
*
|
|
1041
1148
|
* It is MECHANISM, not policy: compose an auth guard IN FRONT by registering an upgrade
|
|
1042
1149
|
* handler BEFORE this one — that handler can claim (decline + destroy) an unauthenticated
|
|
@@ -1073,19 +1180,7 @@ function createWebSocketServer(mcp, options) {
|
|
|
1073
1180
|
head,
|
|
1074
1181
|
protocol: subprotocol
|
|
1075
1182
|
}));
|
|
1076
|
-
|
|
1077
|
-
if (!(0, _src_core.isJSONRPCRequest)(message)) return;
|
|
1078
|
-
(async () => {
|
|
1079
|
-
try {
|
|
1080
|
-
const response = await mcp.dispatch(message);
|
|
1081
|
-
if (response !== void 0) await transport.send(response);
|
|
1082
|
-
} catch (error) {
|
|
1083
|
-
try {
|
|
1084
|
-
transport.emitter.emit("error", error);
|
|
1085
|
-
} catch {}
|
|
1086
|
-
}
|
|
1087
|
-
})();
|
|
1088
|
-
});
|
|
1183
|
+
(0, _src_core.bindServer)(mcp, bridgeMessageTransport(transport));
|
|
1089
1184
|
transport.start();
|
|
1090
1185
|
return true;
|
|
1091
1186
|
};
|
|
@@ -1167,12 +1262,13 @@ function createStdioClientTransport(options) {
|
|
|
1167
1262
|
* @remarks
|
|
1168
1263
|
* Wraps `options.input` (default `process.stdin`) / `options.output` (default
|
|
1169
1264
|
* `process.stdout`) in a {@link import('./transports/StdioServerTransport.js').StdioServerTransport}
|
|
1170
|
-
* and
|
|
1171
|
-
*
|
|
1172
|
-
*
|
|
1173
|
-
*
|
|
1174
|
-
*
|
|
1175
|
-
* escaping the (async) message
|
|
1265
|
+
* and pipes it through the core {@link import('@src/core').MCPTransportInterface} port
|
|
1266
|
+
* via {@link import('./helpers.js').bridgeMessageTransport} + {@link
|
|
1267
|
+
* import('@src/core').bindServer}: each inbound REQUEST runs through `mcp.dispatch`, and
|
|
1268
|
+
* a defined response is written back as a newline-terminated line — a NOTIFICATION
|
|
1269
|
+
* writes nothing, and a non-request message is ignored. A `dispatch` / `send` fault
|
|
1270
|
+
* surfaces on `mcp.emitter`'s `error` event rather than escaping the (async) message
|
|
1271
|
+
* pump.
|
|
1176
1272
|
*
|
|
1177
1273
|
* @param mcp - The transport-agnostic {@link MCPServerInterface} to expose over stdio
|
|
1178
1274
|
* @param options - Optional injectable `input` / `output` streams; see
|
|
@@ -1190,19 +1286,7 @@ function createStdioClientTransport(options) {
|
|
|
1190
1286
|
*/
|
|
1191
1287
|
function createStdioServer(mcp, options) {
|
|
1192
1288
|
const transport = new StdioServerTransport(options?.input ?? process.stdin, options?.output ?? process.stdout);
|
|
1193
|
-
|
|
1194
|
-
if (!(0, _src_core.isJSONRPCRequest)(message)) return;
|
|
1195
|
-
(async () => {
|
|
1196
|
-
try {
|
|
1197
|
-
const response = await mcp.dispatch(message);
|
|
1198
|
-
if (response !== void 0) await transport.send(response);
|
|
1199
|
-
} catch (error) {
|
|
1200
|
-
try {
|
|
1201
|
-
transport.emitter.emit("error", error);
|
|
1202
|
-
} catch {}
|
|
1203
|
-
}
|
|
1204
|
-
})();
|
|
1205
|
-
});
|
|
1289
|
+
(0, _src_core.bindServer)(mcp, bridgeMessageTransport(transport));
|
|
1206
1290
|
return {
|
|
1207
1291
|
start() {
|
|
1208
1292
|
transport.start();
|
|
@@ -1276,30 +1360,43 @@ function createMCPSession(options) {
|
|
|
1276
1360
|
const store = /* @__PURE__ */ new Map();
|
|
1277
1361
|
return async (request, context, next) => {
|
|
1278
1362
|
if (context.url.pathname !== path) return next();
|
|
1279
|
-
|
|
1363
|
+
if (ttl !== void 0) {
|
|
1364
|
+
const cutoff = clock() - ttl;
|
|
1365
|
+
for (const [id, entry] of store) if (entry.touched <= cutoff) store.delete(id);
|
|
1366
|
+
}
|
|
1367
|
+
if (context.method === "DELETE") {
|
|
1368
|
+
const id = readSessionHeader(request);
|
|
1369
|
+
if (id === void 0 || !store.delete(id)) return rejectUnknownSession();
|
|
1370
|
+
return new Response(null, { status: 204 });
|
|
1371
|
+
}
|
|
1372
|
+
let entry;
|
|
1373
|
+
const id = readSessionHeader(request);
|
|
1374
|
+
if (id !== void 0) {
|
|
1375
|
+
const current = store.get(id);
|
|
1376
|
+
if (current !== void 0) {
|
|
1377
|
+
entry = {
|
|
1378
|
+
session: current.session,
|
|
1379
|
+
touched: clock()
|
|
1380
|
+
};
|
|
1381
|
+
store.set(id, entry);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1280
1384
|
if (context.method === "GET") {
|
|
1281
|
-
const entry = resolve(request);
|
|
1282
1385
|
if (entry === void 0) return rejectUnknownSession();
|
|
1386
|
+
const session = entry.session;
|
|
1283
1387
|
const stream = (0, _orkestrel_server.openStream)();
|
|
1284
1388
|
stream.comment("open");
|
|
1285
1389
|
const lastEventId = readLastEventId(request);
|
|
1286
|
-
if (lastEventId !== void 0) for (const e of
|
|
1390
|
+
if (lastEventId !== void 0) for (const e of session.replay(lastEventId)) stream.write({
|
|
1287
1391
|
id: e.id,
|
|
1288
1392
|
data: JSON.stringify(e.message)
|
|
1289
1393
|
});
|
|
1290
|
-
|
|
1291
|
-
if (request.signal.aborted)
|
|
1292
|
-
else request.signal.addEventListener("abort", () =>
|
|
1394
|
+
session.attach(stream);
|
|
1395
|
+
if (request.signal.aborted) session.detach(stream);
|
|
1396
|
+
else request.signal.addEventListener("abort", () => session.detach(stream), { once: true });
|
|
1293
1397
|
return stream.response;
|
|
1294
1398
|
}
|
|
1295
|
-
if (context.method === "DELETE") {
|
|
1296
|
-
const id = readSessionHeader(request);
|
|
1297
|
-
if (id === void 0 || !store.has(id)) return rejectUnknownSession();
|
|
1298
|
-
store.delete(id);
|
|
1299
|
-
return new Response(null, { status: 204 });
|
|
1300
|
-
}
|
|
1301
1399
|
const text = await request.text();
|
|
1302
|
-
let entry = resolve(request);
|
|
1303
1400
|
if (entry === void 0) {
|
|
1304
1401
|
let parsed;
|
|
1305
1402
|
try {
|
|
@@ -1308,7 +1405,7 @@ function createMCPSession(options) {
|
|
|
1308
1405
|
parsed = void 0;
|
|
1309
1406
|
}
|
|
1310
1407
|
if (parsed !== void 0 && (0, _src_core.isInitializeRequest)(parsed)) {
|
|
1311
|
-
const session = new MCPSession(crypto.randomUUID(), { capacity });
|
|
1408
|
+
const session = new MCPSession(crypto.randomUUID(), capacity !== void 0 ? { capacity } : {});
|
|
1312
1409
|
entry = {
|
|
1313
1410
|
session,
|
|
1314
1411
|
touched: clock()
|
|
@@ -1316,7 +1413,7 @@ function createMCPSession(options) {
|
|
|
1316
1413
|
store.set(session.id, entry);
|
|
1317
1414
|
} else return rejectUnknownSession();
|
|
1318
1415
|
}
|
|
1319
|
-
context.state
|
|
1416
|
+
if (!Reflect.set(context.state, "session", entry.session)) throw new Error("MCP session state is not writable");
|
|
1320
1417
|
const response = await next(new Request(context.url, {
|
|
1321
1418
|
method: "POST",
|
|
1322
1419
|
headers: request.headers,
|
|
@@ -1325,19 +1422,6 @@ function createMCPSession(options) {
|
|
|
1325
1422
|
response.headers.set(MCP_SESSION_HEADER, entry.session.id);
|
|
1326
1423
|
return response;
|
|
1327
1424
|
};
|
|
1328
|
-
function resolve(request) {
|
|
1329
|
-
const id = readSessionHeader(request);
|
|
1330
|
-
if (id === void 0) return void 0;
|
|
1331
|
-
const entry = store.get(id);
|
|
1332
|
-
if (entry === void 0) return void 0;
|
|
1333
|
-
entry.touched = clock();
|
|
1334
|
-
return entry;
|
|
1335
|
-
}
|
|
1336
|
-
function sweep() {
|
|
1337
|
-
if (ttl === void 0) return;
|
|
1338
|
-
const cutoff = clock() - ttl;
|
|
1339
|
-
for (const [id, entry] of store) if (entry.touched <= cutoff) store.delete(id);
|
|
1340
|
-
}
|
|
1341
1425
|
}
|
|
1342
1426
|
//#endregion
|
|
1343
1427
|
exports.DEFAULT_MCP_PATH = DEFAULT_MCP_PATH;
|
|
@@ -1353,7 +1437,9 @@ exports.StdioServerTransport = StdioServerTransport;
|
|
|
1353
1437
|
exports.WebSocketClientTransport = WebSocketClientTransport;
|
|
1354
1438
|
exports.WebSocketServerTransport = WebSocketServerTransport;
|
|
1355
1439
|
exports.acceptsEventStream = acceptsEventStream;
|
|
1440
|
+
exports.bridgeMessageTransport = bridgeMessageTransport;
|
|
1356
1441
|
exports.createHTTPClientTransport = createHTTPClientTransport;
|
|
1442
|
+
exports.createMCPPostHandler = createMCPPostHandler;
|
|
1357
1443
|
exports.createMCPRoutes = createMCPRoutes;
|
|
1358
1444
|
exports.createMCPSession = createMCPSession;
|
|
1359
1445
|
exports.createStdioClientTransport = createStdioClientTransport;
|