@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.
@@ -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 that carries the negotiated MCP protocol version
22
- * on a subsequent request. The version is negotiated in the `initialize` JSON-RPC result
23
- * body; a stateful transport MAY additionally read this header to pin the per-request
24
- * protocol version (optional — the result body remains the source of truth).
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 (or batch) to `options.url` with `content-type: application/json` and an
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()` / `close()` are no-ops (a request/response transport
289
- * holds no long-lived connection). The `mcp-session-id` response header, when a
290
- * STATEFUL server sends one (on `initialize`), is captured into `session` and then
291
- * ECHOED as the `mcp-session-id` request header on every SUBSEQUENT request — so an
292
- * `MCPClient` passes a stateful server's session validation. Before initialize returns
293
- * an id, `session` is `undefined` and no header is sent (safe against a stateless
294
- * server, which neither sends nor expects one).
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.#emitter.emit("message", message);
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.#emitter.emit("message", message);
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 | messages)` writes ONE text frame per message
512
- * (`nodeWs.send(JSON.stringify(...))`); the underlying wrapper no-ops a write on a
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
- const messages = Array.isArray(message) ? message : [message];
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 | messages)` writes ONE masked text frame per 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
- fail(/* @__PURE__ */ new Error("WebSocket handshake failed: Sec-WebSocket-Accept mismatch"));
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
- if (!settled) {
666
- settled = true;
667
- resolve();
668
- }
798
+ resolve();
669
799
  });
670
800
  request.on("response", (response) => {
671
801
  response.resume();
672
- fail(/* @__PURE__ */ new Error(`WebSocket upgrade declined with status ${response.statusCode ?? 0}`));
802
+ reject(/* @__PURE__ */ new Error(`WebSocket upgrade declined with status ${response.statusCode ?? 0}`));
673
803
  });
674
- request.on("error", (error) => fail(error instanceof Error ? error : new Error(String(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
- const messages = Array.isArray(message) ? message : [message];
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 | messages)` writes ONE newline-terminated
747
- * `JSON.stringify`d line per message to the child's `stdin`.
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
- const messages = Array.isArray(message) ? message : [message];
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 | messages)` writes ONE newline-terminated
843
- * `JSON.stringify`d line per message to `output`.
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
- const messages = Array.isArray(message) ? message : [message];
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: async (request) => {
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, so the same
994
- * `MCPClient` passes session validation (a stateless server sends none).
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 PUMPS: each inbound {@link
1035
- * import('@src/core').JSONRPCMessage} that is a REQUEST runs through `mcp.dispatch`, and a
1036
- * defined response is written back as a frame — a NOTIFICATION (`dispatch` → `undefined`)
1037
- * sends nothing. A non-request message (a stray response) is ignored. The dispatch is
1038
- * guarded so a `dispatch` / `send` fault surfaces on the transport's `error` event rather
1039
- * than escaping the (async) message listener.
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
- transport.emitter.on("message", (message) => {
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 PUMPS: each inbound {@link import('@src/core').JSONRPCMessage} that is a
1171
- * REQUEST runs through `mcp.dispatch`, and a defined response is written back as a
1172
- * newline-terminated line a NOTIFICATION (`dispatch` → `undefined`) writes
1173
- * nothing. A non-request message is ignored. The dispatch is guarded so a
1174
- * `dispatch` / `send` fault surfaces on the transport's `error` event rather than
1175
- * escaping the (async) message listener.
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
- transport.emitter.on("message", (message) => {
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
- sweep();
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 entry.session.replay(lastEventId)) stream.write({
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
- entry.session.attach(stream);
1291
- if (request.signal.aborted) entry.session.detach(stream);
1292
- else request.signal.addEventListener("abort", () => entry.session.detach(stream), { once: true });
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.session = entry.session;
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;