@orkestrel/mcp 0.0.19 → 0.0.21

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.
@@ -9,6 +9,7 @@ import { request as request$1 } from "node:https";
9
9
  import { WEBSOCKET_VERSION, computeWebSocketAccept, createNodeWebSocket } from "@orkestrel/websocket";
10
10
  import { Process } from "@orkestrel/process/server";
11
11
  import { PROCESS_GRACE } from "@orkestrel/process";
12
+ import { Readable } from "node:stream";
12
13
  //#region src/server/constants.ts
13
14
  /**
14
15
  * The Streamable-HTTP transport header that carries the MCP session id. When a {@link
@@ -86,7 +87,7 @@ var DEFAULT_MCP_SESSION_TTL = 3e5;
86
87
  //#endregion
87
88
  //#region src/server/helpers.ts
88
89
  /**
89
- * Create a readable stream from its pull and cancellation behaviours.
90
+ * Creates a readable stream from its pull and cancellation behaviours.
90
91
  *
91
92
  * @param pull - The behaviour that supplies the stream's next chunk
92
93
  * @param cancel - The behaviour that releases the stream after consumer cancellation
@@ -99,12 +100,12 @@ function createReadableStream(pull, cancel) {
99
100
  });
100
101
  }
101
102
  /**
102
- * Pump a controlled held-open exchange onto an open SSE stream — one `data:` event per
103
+ * Pumps a controlled held-open exchange onto an open SSE stream — one `data:` event per
103
104
  * notification in order, then the terminating response — and END the exchange however the
104
105
  * pump leaves.
105
106
  *
106
107
  * @remarks
107
- * The Streamable-HTTP twin of {@link import('@src/core').sendStream}, and it owns exactly what
108
+ * The Streamable-HTTP twin of {@link import('@orkestrel/mcp').sendStream}, and it owns exactly what
108
109
  * that owns. The `finally` releases the exchange on EVERY exit — the normal terminal, a
109
110
  * producer that threw, a `write` that threw, and an abort alike — because nothing else will:
110
111
  * a request whose client vanished cancels nothing by itself, so an exchange this pump walks
@@ -112,7 +113,7 @@ function createReadableStream(pull, cancel) {
112
113
  * The exchange is released BEFORE the body ends, so the slot is already back when the response
113
114
  * completes.
114
115
  *
115
- * Total (§14) — never throws and never rejects. A held-open SSE response has already sent its
116
+ * Total — never throws and never rejects. A held-open SSE response has already sent its
116
117
  * headers and part of its body, so there is no failure the transport could still convert into
117
118
  * a different answer; the honest end of a broken stream is a closed one, and the fault itself
118
119
  * is already legible on `server.emitter`'s `error` event, which is where a contained fault
@@ -194,7 +195,7 @@ function allowsOrigin(request, options) {
194
195
  return options?.origins?.includes(parsed.origin) ?? false;
195
196
  }
196
197
  /**
197
- * Read the request's `mcp-session-id` header — the session id a stateful transport
198
+ * Reads the request's `mcp-session-id` header — the session id a stateful transport
198
199
  * validates, or `undefined` when absent.
199
200
  *
200
201
  * @remarks
@@ -212,7 +213,7 @@ function readSessionHeader(request) {
212
213
  return id === null ? void 0 : id;
213
214
  }
214
215
  /**
215
- * Read the request's `Last-Event-ID` header — the SSE resume cursor a client sends when it
216
+ * Reads the request's `Last-Event-ID` header — the SSE resume cursor a client sends when it
216
217
  * reconnects to the resumable `GET {path}` stream, or `undefined` when absent.
217
218
  *
218
219
  * @remarks
@@ -230,7 +231,7 @@ function readLastEventId(request) {
230
231
  return id === null ? void 0 : id;
231
232
  }
232
233
  /**
233
- * Build the stateful transport's "unknown session" rejection — an HTTP `404` carrying a
234
+ * Builds the stateful transport's "unknown session" rejection — an HTTP `404` carrying a
234
235
  * JSON-RPC error body.
235
236
  *
236
237
  * @remarks
@@ -248,16 +249,16 @@ function rejectUnknownSession() {
248
249
  return Response.json(buildJSONRPCError(void 0, JSONRPC_INVALID_REQUEST, "Session not found"), { status: 404 });
249
250
  }
250
251
  /**
251
- * Decode a `fetch` Response's Server-Sent-Events body into the JSON-RPC messages it
252
+ * Decodes a `fetch` Response's Server-Sent-Events body into the JSON-RPC messages it
252
253
  * carried — the CLIENT-side inverse of the server's Streamable-HTTP SSE response.
253
254
  *
254
255
  * @remarks
255
256
  * Reads the whole `response.body` stream chunk-by-chunk through a `TextDecoder({
256
257
  * stream: true })` (handling a multi-byte char split across reads) and `@orkestrel/sse`'s
257
258
  * {@link SSEParserInterface} (handling a partial line / in-progress event split across
258
- * reads), then narrows each dispatched event's `data` to a {@link JSONRPCMessage} via
259
+ * reads), then narrows each dispatched event's `data` to a {@link JSONRPCMessage} with
259
260
  * `parseJSONRPCMessage` (so a non-message / non-JSON `data:` event is DROPPED, never
260
- * thrown — total, §14). It reuses the SAME `SSEParser` the server's `openStream` seam
261
+ * thrown — total). It reuses the SAME `SSEParser` the server's `openStream` seam
261
262
  * serializes against, so the wire round-trips. A `null` body (no stream) yields no
262
263
  * messages; the {@link import('./transports/HTTPClientTransport.js').HTTPClientTransport}
263
264
  * reads a request/response SSE reply (the server sends one `data:` event then ends), so
@@ -288,13 +289,13 @@ async function readEventStream(response) {
288
289
  return messages;
289
290
  }
290
291
  /**
291
- * Decode one SSE event's `data` string into a {@link JSONRPCMessage}, or `undefined`
292
+ * Decodes one SSE event's `data` string into a {@link JSONRPCMessage}, or `undefined`
292
293
  * when it is not one — the per-event step {@link readEventStream} folds over.
293
294
  *
294
295
  * @remarks
295
296
  * `JSON.parse`s the `data` (the server serializes the JSON-RPC envelope as the event's
296
297
  * `data`) inside a try/catch and narrows the parsed value with `parseJSONRPCMessage`.
297
- * Total (§14): malformed JSON or a non-message value yields `undefined`, never throws.
298
+ * Total: malformed JSON or a non-message value yields `undefined`, never throws.
298
299
  *
299
300
  * @param data - One SSE event's `data` payload
300
301
  * @returns The decoded {@link JSONRPCMessage}, or `undefined`
@@ -307,13 +308,13 @@ function decodeEvent(data) {
307
308
  }
308
309
  }
309
310
  /**
310
- * Read the path (without the query string) of a raw `node:http` protocol-upgrade request —
311
+ * Reads the path (without the query string) of a raw `node:http` protocol-upgrade request —
311
312
  * the `createWebSocketServer` upgrade-path match.
312
313
  *
313
314
  * @remarks
314
315
  * A `node:http` {@link import('node:http').IncomingMessage}'s `url` is the request TARGET
315
- * (`'/mcp?x=1'`), narrowed with `isString` (§14, never `as`) and defaulting to `'/'` for an
316
- * absent target; it is parsed against a dummy base (only the pathname matters for the upgrade
316
+ * (`'/mcp?x=1'`), narrowed with `isString` (never `as`) and defaulting to `'/'` for an
317
+ * absent target; it is parsed against a placeholder base (only the pathname matters for the upgrade
317
318
  * decision) and the `pathname` returned. The upgrade handler compares this against its
318
319
  * configured `path` to decide whether to claim the socket. Total — never throws on an
319
320
  * adversarial / absent target.
@@ -326,7 +327,7 @@ function upgradeRequestPath(request) {
326
327
  return new URL(target, "http://localhost").pathname;
327
328
  }
328
329
  /**
329
- * Fold one more chunk of raw stdio bytes into a newline-framed buffer — the shared
330
+ * Folds one more chunk of raw stdio bytes into a newline-framed buffer — the shared
330
331
  * line-framing step both stdio transports (client and server) read their inbound
331
332
  * newline-delimited JSON-RPC messages through.
332
333
  *
@@ -351,19 +352,20 @@ function extractLines(buffer, chunk) {
351
352
  };
352
353
  }
353
354
  /**
354
- * Decode and deliver each complete newline-framed line onto a {@link
355
+ * Decodes and delivers each complete newline-framed line onto a {@link
355
356
  * MCPClientTransportEventMap} emitter — the shared per-chunk dispatch step both stdio
356
- * transports (client and server) run their {@link extractLines} output through.
357
+ * transports run their framed lines through: the server transport frames with {@link
358
+ * extractLines}, the client transport takes its lines from the process supervisor.
357
359
  *
358
360
  * @remarks
359
361
  * A blank line is skipped (a stray trailing newline). Every other line is decoded
360
362
  * with {@link decodeEvent} (`JSON.parse` + `parseJSONRPCMessage`, guarded); a
361
363
  * well-formed {@link JSONRPCMessage} emits `message`, a malformed / non-message line
362
- * emits `error` (§14 — total, never throws). Pure w.r.t. its own state — the emit is
364
+ * emits `error` (total, never throws). Pure w.r.t. its own state — the emit is
363
365
  * the caller-owned side effect.
364
366
  *
365
367
  * @param emitter - The transport's {@link EmitterInterface} to emit `message` / `error` onto
366
- * @param lines - The complete lines (from {@link extractLines}) to decode and deliver
368
+ * @param lines - The complete lines to decode and deliver
367
369
  */
368
370
  function dispatchLines(emitter, lines) {
369
371
  for (const line of lines) {
@@ -377,17 +379,17 @@ function dispatchLines(emitter, lines) {
377
379
  }
378
380
  }
379
381
  /**
380
- * Bridge a message-channel {@link MCPClientTransportInterface} (the shape the stdio and
382
+ * Bridges a message-channel {@link MCPClientTransportInterface} (the shape the stdio and
381
383
  * WebSocket SERVER transports already implement) into the environment-agnostic
382
- * {@link import('@src/core').MCPTransportInterface} port — the adapter
384
+ * {@link import('@orkestrel/mcp').MCPTransportInterface} port — the adapter
383
385
  * {@link import('./factories.js').createStdioServer} and {@link
384
386
  * import('./factories.js').createWebSocketServer} pipe through `bindServer`, so the
385
- * request/reply/error pump those two factories used to hand-roll identically now
387
+ * request/reply/error pump those factories used to hand-roll identically now
386
388
  * lives ONCE in the core binder.
387
389
  *
388
390
  * @remarks
389
391
  * `send` decodes the already-serialized reply string back to a {@link JSONRPCMessage}
390
- * and writes it via `transport.send` (the SAME `JSON.stringify` the underlying
392
+ * and writes it through `transport.send` (the same `JSON.stringify` the underlying
391
393
  * transport already performs, so the wire bytes are unchanged). `listen` filters
392
394
  * `transport`'s `message` event to INVOCATIONS ONLY — requests and notifications, never a
393
395
  * stray response, exactly as the prior hand-rolled pumps did — and re-serializes each one
@@ -405,24 +407,24 @@ function dispatchLines(emitter, lines) {
405
407
  * decode rather than after it. Removing the cost means giving `MCPTransportInterface` a
406
408
  * message-shaped face beside its string one, which every transport would then carry.
407
409
  *
408
- * @remarks Per {@link import('@src/core').MCPTransportInterface}, `listen`/`closed`
410
+ * @remarks Per {@link import('@orkestrel/mcp').MCPTransportInterface}, `listen`/`closed`
409
411
  * each hold THE SINGLE current handler (a second call REPLACES the first, never adds).
410
- * Since the underlying `transport.emitter` is ADD-based (`on` subscribes, never
412
+ * Because the underlying `transport.emitter` is ADD-based (`on` subscribes, never
411
413
  * replaces), this bridge installs ONE stable emitter listener per event on first use
412
- * and re-routes it to whichever handler is CURRENTLY registered (`undefined` while
414
+ * and re-routes it to whichever handler is active (`undefined` while
413
415
  * none is), so rebinding never double-dispatches.
414
416
  *
415
- * @remarks A response whose `result` serializes away (e.g. `undefined`) is dropped by
417
+ * @remarks A response whose `result` serializes away (for example, `undefined`) is dropped by
416
418
  * the message validators on the wire's decode side — an asymmetry the stdio/WS carrier
417
- * shares with the streamable-HTTP face, since both round-trip through `JSON.stringify`
419
+ * shares with the streamable-HTTP face, because both round-trip through `JSON.stringify`
418
420
  * / `JSON.parse` before re-validation.
419
421
  *
420
422
  * @param transport - The message-channel transport to bridge (stdio or WebSocket)
421
- * @returns An {@link import('@src/core').MCPTransportInterface} `bindServer` can drive
423
+ * @returns An {@link import('@orkestrel/mcp').MCPTransportInterface} `bindServer` can drive
422
424
  *
423
425
  * @example
424
426
  * ```ts
425
- * import { bindServer } from '@src/core'
427
+ * import { bindServer } from '@orkestrel/mcp'
426
428
  *
427
429
  * const transport = new StdioServerTransport(process.stdin, process.stdout)
428
430
  * bindServer(mcp, bridgeMessageTransport(transport))
@@ -458,7 +460,7 @@ function bridgeMessageTransport(transport) {
458
460
  //#endregion
459
461
  //#region src/server/inferers.ts
460
462
  /**
461
- * Infer the first required MCP HTTP header that is missing or mismatched.
463
+ * Infers the first required MCP HTTP header that is missing or mismatched.
462
464
  *
463
465
  * @remarks
464
466
  * A modern request derives its protocol, method, and tools/call-only name expectations from
@@ -539,7 +541,7 @@ function inferHeaderIssue(request, reference) {
539
541
  };
540
542
  }
541
543
  /**
542
- * Infer the legacy revision an `initialize` request negotiates.
544
+ * Infers the legacy revision an `initialize` request negotiates.
543
545
  *
544
546
  * @remarks
545
547
  * A supported legacy request is pinned exactly. A modern, malformed, absent, or unsupported
@@ -555,7 +557,7 @@ function inferLegacyVersion(request) {
555
557
  return MCP_PROTOCOL_VERSION;
556
558
  }
557
559
  /**
558
- * Infer the HTTP status for one MCP dispatch outcome without changing its JSON-RPC body.
560
+ * Infers the HTTP status for one MCP dispatch outcome without changing its JSON-RPC body.
559
561
  *
560
562
  * @remarks
561
563
  * Notifications are accepted with `202`. Legacy response envelopes retain uniform `200`
@@ -577,7 +579,7 @@ function inferStatus(response, era) {
577
579
  //#endregion
578
580
  //#region src/server/transports/HTTPDisconnect.ts
579
581
  /**
580
- * Compose one incoming HTTP request lifetime with one MCP-owned SSE response lifetime.
582
+ * Composes one incoming HTTP request lifetime with one MCP-owned SSE response lifetime.
581
583
  *
582
584
  * @remarks
583
585
  * The composed {@link signal} observes request abort and EVERY way this response can end
@@ -591,7 +593,8 @@ function inferStatus(response, era) {
591
593
  *
592
594
  * {@link bridge} preserves the source response status and headers, forwards its body bytes, and
593
595
  * owns keepalive comments plus listener/timer cleanup until upstream completion, request abort,
594
- * or consumer cancellation. This is a single-response lifecycle object, not a reusable bridge.
596
+ * or consumer cancellation. This is a single-response lifecycle object, not a reusable bridge:
597
+ * a second {@link bridge} call THROWS rather than arming a second keepalive over one lifecycle.
595
598
  * It supplies no handler or session policy.
596
599
  *
597
600
  * The keepalive interval is a BUDGET, sanitized like every other numeric knob in this package:
@@ -616,9 +619,10 @@ var HTTPDisconnect = class {
616
619
  #interval;
617
620
  #signal;
618
621
  #timer;
622
+ #bridged = false;
619
623
  #pulling = false;
620
624
  /**
621
- * Create the lifecycle composition for one request and its future SSE response.
625
+ * Creates the lifecycle composition for one request and its future SSE response.
622
626
  *
623
627
  * @param signal - The incoming request signal
624
628
  * @param options - Optional keepalive `interval` in milliseconds; an invalid value falls back
@@ -639,7 +643,7 @@ var HTTPDisconnect = class {
639
643
  return this.#signal;
640
644
  }
641
645
  /**
642
- * Bridge one open SSE response through cancellation-aware byte forwarding and keepalives.
646
+ * Bridges one open SSE response through cancellation-aware byte forwarding and keepalives.
643
647
  *
644
648
  * Consumer cancellation, a read failure while forwarding, and a keepalive tick that finds the
645
649
  * SSE stream already closed each abort {@link signal}; consumer cancellation also cancels the
@@ -648,9 +652,12 @@ var HTTPDisconnect = class {
648
652
  *
649
653
  * @param stream - The open SSE stream whose response will be consumed by the HTTP writer
650
654
  * @returns A one-use response preserving status, status text, headers, and SSE body bytes
651
- * @throws When the supplied SSE response has no body
655
+ * @throws When this disconnect has already bridged a stream, or the supplied SSE response
656
+ * has no body
652
657
  */
653
658
  bridge(stream) {
659
+ if (this.#bridged) throw new Error("MCP SSE response is already bridged");
660
+ this.#bridged = true;
654
661
  const response = stream.response;
655
662
  const body = response.body;
656
663
  if (body === null) throw new Error("MCP SSE response has no body");
@@ -704,7 +711,7 @@ var HTTPDisconnect = class {
704
711
  //#endregion
705
712
  //#region src/server/handlers.ts
706
713
  /**
707
- * Create the Streamable-HTTP POST handler used by `createMCPRoutes`.
714
+ * Creates the Streamable-HTTP POST handler used by `createMCPRoutes`.
708
715
  *
709
716
  * @remarks
710
717
  * Modern requests require matching protocol/method headers and a matching name header only
@@ -730,7 +737,7 @@ var HTTPDisconnect = class {
730
737
  * import { createToolManager } from '@orkestrel/tool'
731
738
  *
732
739
  * const mcp = createMCPServer({ identity: { name: 'docs', version: '1.0.0' }, tools: createToolManager() })
733
- * const handler = createMCPPostHandler(createMCPLegacy(mcp), { streaming: true })
740
+ * const handler = createMCPPostHandler(createMCPLegacy(mcp), { streaming: true }) // answers `initialize` too; pass `mcp` alone for modern-only
734
741
  * await handler(new Request('http://localhost/mcp', {
735
742
  * method: 'POST',
736
743
  * body: '{"jsonrpc":"2.0","method":"ping","id":1}',
@@ -805,12 +812,12 @@ function createMCPPostHandler(mcp, options) {
805
812
  * - **Request/response over `fetch`.** `send(message)` POSTs the JSON-serialized
806
813
  * message to `options.url` with `content-type: application/json` and an
807
814
  * `Accept` of BOTH `application/json` and `text/event-stream` (so the server may
808
- * answer with either framing) — plus any `options.headers` (e.g. an `Authorization`
815
+ * answer with either framing) — plus any `options.headers` (for example, an `Authorization`
809
816
  * bearer). It then decodes the reply and emits each decoded {@link JSONRPCMessage} on
810
- * the `message` event the {@link import('@src/core').MCPClientInterface} subscribes
817
+ * the `message` event the {@link import('@orkestrel/mcp').MCPClientInterface} subscribes
811
818
  * to.
812
819
  * - **Both reply framings.** A `200` with an `application/json` body is parsed with
813
- * `parseJSONRPCMessage`; a `200` with a `text/event-stream` body is decoded via the
820
+ * `parseJSONRPCMessage`; a `200` with a `text/event-stream` body is decoded with the
814
821
  * `@orkestrel/sse` {@link import('@orkestrel/sse').SSEParserInterface} ({@link
815
822
  * readEventStream}) — the inverse of the server's `openStream` seam, so the wire
816
823
  * round-trips. A `202`
@@ -828,10 +835,15 @@ function createMCPPostHandler(mcp, options) {
828
835
  * Before initialize returns, neither captured legacy header is sent.
829
836
  * `close()` clears the captured protocol so a reconnect's `initialize`
830
837
  * POST is headerless; the captured `session` persists across `close()`.
831
- * - **Total at the boundary (§14).** Every reply is narrowed (`parseJSONRPCMessage`,
838
+ * - **`close()` releases what is in flight.** Every `fetch` this transport still has open is
839
+ * ABORTED, which cancels the response body a `send` is reading — an SSE reply the server
840
+ * never ends would otherwise outlive the transport, with nothing left able to reach it. The
841
+ * aborted read surfaces on `error` and the `send` reporting it resolves. `close()` is
842
+ * idempotent (one `close` event per connected lifetime), and `start()` opens the next one.
843
+ * - **Total at the boundary.** Every reply is narrowed (`parseJSONRPCMessage`,
832
844
  * the SSE decoder) — a non-message reply is dropped, never asserted; a `fetch` /
833
845
  * decode failure surfaces on the `error` event rather than escaping `send`.
834
- * - **Observable (§13).** Owns the `emitter` ({@link MCPClientTransportEventMap}); fires
846
+ * - **Observable.** Owns the `emitter` ({@link MCPClientTransportEventMap}); fires
835
847
  * `message` per decoded reply, `error` on a fault, and `close` on `close()`.
836
848
  *
837
849
  * @example
@@ -847,8 +859,10 @@ var HTTPClientTransport = class {
847
859
  #headers;
848
860
  #fetch;
849
861
  #timeout;
862
+ #pending = /* @__PURE__ */ new Set();
850
863
  #session = void 0;
851
864
  #protocol = void 0;
865
+ #closed = false;
852
866
  constructor(options) {
853
867
  this.#emitter = new Emitter();
854
868
  this.#url = options.url;
@@ -865,8 +879,19 @@ var HTTPClientTransport = class {
865
879
  get duplex() {
866
880
  return false;
867
881
  }
868
- async start() {}
882
+ async start() {
883
+ this.#closed = false;
884
+ }
869
885
  async send(message) {
886
+ const request = new AbortController();
887
+ this.#pending.add(request);
888
+ try {
889
+ await this.#exchange(message, request.signal);
890
+ } finally {
891
+ this.#pending.delete(request);
892
+ }
893
+ }
894
+ async #exchange(message, signal) {
870
895
  let response;
871
896
  try {
872
897
  response = await this.#fetch(this.#url, {
@@ -879,7 +904,7 @@ var HTTPClientTransport = class {
879
904
  ...this.#headers
880
905
  },
881
906
  body: JSON.stringify(message),
882
- ...this.#timeout === void 0 ? {} : { signal: AbortSignal.timeout(this.#timeout) }
907
+ signal: this.#timeout === void 0 ? signal : AbortSignal.any([signal, AbortSignal.timeout(this.#timeout)])
883
908
  });
884
909
  } catch (error) {
885
910
  this.#emitter.emit("error", error);
@@ -890,6 +915,10 @@ var HTTPClientTransport = class {
890
915
  await this.#deliver(response);
891
916
  }
892
917
  async close() {
918
+ if (this.#closed) return;
919
+ this.#closed = true;
920
+ for (const request of this.#pending) request.abort();
921
+ this.#pending.clear();
893
922
  this.#protocol = void 0;
894
923
  this.#emitter.emit("close");
895
924
  }
@@ -937,8 +966,8 @@ var HTTPClientTransport = class {
937
966
  * The single session entity (the old `SessionState` + `EventStore` merged): it holds the
938
967
  * session `id`, its OWN bounded, replayable log of pushed server→client messages (the
939
968
  * resumable GET-SSE channel — a private `#events` `Map` + a monotone `#counter`, with
940
- * `capacity` / `ttl` eviction, NOT a separate store), and the set of currently OPEN
941
- * server→client SSE streams (a resumable `GET {path}` registers via `attach`, unregisters via
969
+ * `capacity` / `ttl` eviction, not a separate store), and the set of open
970
+ * server→client SSE streams (a resumable `GET {path}` registers through `attach`, unregisters through
942
971
  * `detach` on disconnect). Still a small entity (not a record), built minimal + extensible.
943
972
  *
944
973
  * - **`push` is the server-initiated primitive.** It APPENDS the message to the log (assigning
@@ -958,7 +987,7 @@ var HTTPClientTransport = class {
958
987
  * lost (its cursor is OLDER than everything retained); returning `[]` lets the handler then
959
988
  * stream only the fresh pushes that follow `attach` — the spec-sane resume.
960
989
  *
961
- * - **Bounded, append-ordered, plain `Map` (§21).** The log lives in ONE insertion-ordered
990
+ * - **Bounded, append-ordered, plain `Map`.** The log lives in ONE insertion-ordered
962
991
  * `Map<id, entry>` — insertion order IS append order IS id order, so `replay` and capacity
963
992
  * eviction both walk the map directly. NO database mirror — the log is process-local
964
993
  * transport mechanics, not durable state. `push` first drops every entry older than `ttl`
@@ -972,8 +1001,8 @@ var HTTPClientTransport = class {
972
1001
  * serializes a message onto the already-open streams.
973
1002
  *
974
1003
  * - **Injected clock.** `push` / `replay` accept an optional `now` (epoch ms), defaulting to
975
- * `Date.now()` — so a test drives TTL eviction with an elapsed clock rather than a real timer
976
- * (AGENTS §16).
1004
+ * `Date.now()` — so a test drives TTL eviction with an elapsed clock rather than a real
1005
+ * timer.
977
1006
  *
978
1007
  * @example
979
1008
  * ```ts
@@ -1054,7 +1083,7 @@ var MCPSession = class {
1054
1083
  * {@link import('./WebSocketClientTransport.js').WebSocketClientTransport} reuses.
1055
1084
  *
1056
1085
  * @remarks
1057
- * - **Reuses `MCPClientTransportInterface` (§21).** It IS the same generic carrier the HTTP
1086
+ * - **Reuses `MCPClientTransportInterface`.** It IS the same generic carrier the HTTP
1058
1087
  * client transport implements — `emitter` (`message` / `close` / `error`), `start`,
1059
1088
  * `send`, `close` — so the WebSocket server and client both speak ONE transport contract,
1060
1089
  * no near-duplicate sibling interface. `session` is `undefined` (the stateless v1; a
@@ -1063,22 +1092,27 @@ var MCPSession = class {
1063
1092
  * - **Inbound (`message`).** `start()` subscribes to the socket's `message` event; each text
1064
1093
  * frame is `JSON.parse`d inside a try/catch and narrowed with `parseJSONRPCMessage` — a
1065
1094
  * well-formed {@link JSONRPCMessage} is re-emitted on this transport's `message` event (the
1066
- * parsed envelope the {@link import('@src/core').MCPServerInterface} pump dispatches), while
1067
- * a non-JSON or non-message frame is surfaced on `error` and DROPPED, never thrown (§14). It
1095
+ * parsed envelope the {@link import('@orkestrel/mcp').MCPServerInterface} pump dispatches), while
1096
+ * a non-JSON or non-message frame is surfaced on `error` and DROPPED, never thrown. It
1068
1097
  * also bridges the socket's `close` → this transport's `close`, and the socket's `error`.
1069
1098
  * - **Outbound (`send`).** `send(message)` writes one text frame
1070
1099
  * (`nodeWs.send(JSON.stringify(message))`); the underlying wrapper no-ops a write on a
1071
1100
  * non-open socket, so a closed connection drops silently rather than throwing.
1072
- * - **`close()`** closes the underlying socket (the RFC 6455 close handshake) and fires the
1073
- * transport's `close` event (idempotent a second `close`, or a socket-driven close, emits
1074
- * once).
1075
- * - **Observable (§13).** Owns the `emitter` ({@link MCPClientTransportEventMap}); the emitter
1101
+ * - **`close()`** removes the subscriptions `start()` installed on the socket, closes the
1102
+ * underlying socket (the RFC 6455 close handshake), and fires the transport's `close` event
1103
+ * (idempotent — a second `close`, or a socket-driven close, emits once). A frame that arrives
1104
+ * between that release and the peer's close echo reaches nothing: the socket-driven close path
1105
+ * releases the same way, so a closed transport is never subscribed to a live socket.
1106
+ * - **Observable.** Owns the `emitter` ({@link MCPClientTransportEventMap}); the emitter
1076
1107
  * isolates a listener throw (a buggy observer never corrupts the bridge). `error` is a
1077
1108
  * DOMAIN event (a transport-level fault), distinct from the emitter's listener-error channel.
1078
1109
  */
1079
1110
  var WebSocketServerTransport = class {
1080
1111
  #emitter;
1081
1112
  #socket;
1113
+ #frame = (text) => this.#receive(text);
1114
+ #ending = () => this.#onClose();
1115
+ #failure = (error) => this.#emitter.emit("error", error);
1082
1116
  #started = false;
1083
1117
  #closed = false;
1084
1118
  constructor(socket) {
@@ -1095,9 +1129,9 @@ var WebSocketServerTransport = class {
1095
1129
  async start() {
1096
1130
  if (this.#started || this.#closed) return;
1097
1131
  this.#started = true;
1098
- this.#socket.emitter.on("message", (text) => this.#receive(text));
1099
- this.#socket.emitter.on("close", () => this.#onClose());
1100
- this.#socket.emitter.on("error", (error) => this.#emitter.emit("error", error));
1132
+ this.#socket.emitter.on("message", this.#frame);
1133
+ this.#socket.emitter.on("close", this.#ending);
1134
+ this.#socket.emitter.on("error", this.#failure);
1101
1135
  }
1102
1136
  async send(message) {
1103
1137
  this.#socket.send(JSON.stringify(message));
@@ -1105,6 +1139,7 @@ var WebSocketServerTransport = class {
1105
1139
  async close() {
1106
1140
  if (this.#closed) return;
1107
1141
  this.#closed = true;
1142
+ this.#release();
1108
1143
  this.#socket.close();
1109
1144
  this.#emitter.emit("close");
1110
1145
  }
@@ -1126,8 +1161,14 @@ var WebSocketServerTransport = class {
1126
1161
  #onClose() {
1127
1162
  if (this.#closed) return;
1128
1163
  this.#closed = true;
1164
+ this.#release();
1129
1165
  this.#emitter.emit("close");
1130
1166
  }
1167
+ #release() {
1168
+ this.#socket.emitter.off("message", this.#frame);
1169
+ this.#socket.emitter.off("close", this.#ending);
1170
+ this.#socket.emitter.off("error", this.#failure);
1171
+ }
1131
1172
  };
1132
1173
  //#endregion
1133
1174
  //#region src/server/transports/WebSocketClientTransport.ts
@@ -1145,7 +1186,8 @@ var WebSocketServerTransport = class {
1145
1186
  * event, and VALIDATES `Sec-WebSocket-Accept === computeWebSocketAccept(key)` (the D2 helper)
1146
1187
  * — a mismatch (or a non-`101` response, or a request error) REJECTS `start()` and the socket
1147
1188
  * is destroyed. On success it wraps the raw upgraded socket in `createNodeWebSocket({ socket,
1148
- * head })` (CLIENT mode — no key → frames are MASKED per §5.3) and bridges its `message`.
1189
+ * head })` (CLIENT mode — no key → frames are MASKED per RFC 6455 §5.3) and bridges its
1190
+ * `message`.
1149
1191
  * - **The arriving socket is RE-ASKED for, never assumed.** `start()` suspends across that
1150
1192
  * connect and upgrade, so it re-checks the transport's state before installing anything: a
1151
1193
  * concurrent `start()` that already installed a socket, or a {@link close} that ended the
@@ -1154,15 +1196,18 @@ var WebSocketServerTransport = class {
1154
1196
  * `start()` calls still resolve; exactly one socket is ever bound.
1155
1197
  * - **Inbound (`message`).** Each decoded text frame is `JSON.parse`d (guarded) and narrowed
1156
1198
  * with `parseJSONRPCMessage` — a {@link JSONRPCMessage} re-emits on this transport's `message`
1157
- * event (the reply the {@link import('@src/core').MCPClientInterface} correlates by `id`); a
1158
- * non-JSON / non-message frame surfaces on `error` and is dropped (§14). The socket's `close`
1199
+ * event (the reply the {@link import('@orkestrel/mcp').MCPClientInterface} correlates by `id`); a
1200
+ * non-JSON / non-message frame surfaces on `error` and is dropped. The socket's `close`
1159
1201
  * / `error` bridge to this transport's events.
1160
1202
  * - **Outbound (`send`).** `send(message)` writes one masked text frame.
1161
- * - **`close()`** closes the underlying socket and fires `close` (idempotent).
1203
+ * - **`close()`** unsubscribes from the socket, closes it, and fires `close` (idempotent). An
1204
+ * upgrade still on the wire is DESTROYED, so a `close()` during the handshake ends the
1205
+ * transport at once instead of waiting for a peer that may never answer — the suspended
1206
+ * `start()` resolves, because the close is the outcome its caller asked for.
1162
1207
  * - **URL scheme.** `options.url` accepts a `ws://` / `wss://` URL or an `http://` / `https://`
1163
1208
  * one; a `ws(s)` scheme is converted to `http(s)` for the underlying upgrade request (`wss`
1164
- * → TLS via `node:https`). Either reaches the same endpoint.
1165
- * - **Observable (§13).** Owns the `emitter` ({@link MCPClientTransportEventMap}); every emit
1209
+ * → TLS through `node:https`). Either reaches the same endpoint.
1210
+ * - **Observable.** Owns the `emitter` ({@link MCPClientTransportEventMap}); every emit
1166
1211
  * the emitter isolates a listener throw (a buggy observer never corrupts the transport);
1167
1212
  * `error` is a DOMAIN event (a transport-level fault).
1168
1213
  *
@@ -1177,7 +1222,11 @@ var WebSocketClientTransport = class {
1177
1222
  #emitter;
1178
1223
  #url;
1179
1224
  #headers;
1225
+ #frame = (text) => this.#receive(text);
1226
+ #ending = () => this.#onClose();
1227
+ #failure = (error) => this.#emitter.emit("error", error);
1180
1228
  #socket = void 0;
1229
+ #request = void 0;
1181
1230
  #closed = false;
1182
1231
  constructor(options) {
1183
1232
  this.#emitter = new Emitter();
@@ -1194,8 +1243,28 @@ var WebSocketClientTransport = class {
1194
1243
  async start() {
1195
1244
  if (this.#socket !== void 0) return;
1196
1245
  this.#closed = false;
1197
- const url = this.#httpURL();
1198
- const key = randomBytes(16).toString("base64");
1246
+ try {
1247
+ await this.#connect(this.#httpURL(), randomBytes(16).toString("base64"));
1248
+ } finally {
1249
+ this.#request = void 0;
1250
+ }
1251
+ }
1252
+ async send(message) {
1253
+ const socket = this.#socket;
1254
+ if (socket === void 0) throw new Error("WebSocket transport is not connected");
1255
+ socket.send(JSON.stringify(message));
1256
+ }
1257
+ async close() {
1258
+ if (this.#closed) return;
1259
+ this.#closed = true;
1260
+ this.#request?.destroy();
1261
+ const socket = this.#socket;
1262
+ this.#release();
1263
+ this.#socket = void 0;
1264
+ if (socket !== void 0) socket.close();
1265
+ this.#emitter.emit("close");
1266
+ }
1267
+ async #connect(url, key) {
1199
1268
  const secure = url.protocol === "https:";
1200
1269
  const send = secure ? request$1 : request;
1201
1270
  await new Promise((resolve, reject) => {
@@ -1212,6 +1281,7 @@ var WebSocketClientTransport = class {
1212
1281
  ...this.#headers
1213
1282
  }
1214
1283
  });
1284
+ this.#request = request;
1215
1285
  request.on("upgrade", (response, socket, head) => {
1216
1286
  const accept = response.headers["sec-websocket-accept"];
1217
1287
  if (!isString(accept) || accept !== computeWebSocketAccept(key)) {
@@ -1236,27 +1306,27 @@ var WebSocketClientTransport = class {
1236
1306
  response.resume();
1237
1307
  reject(/* @__PURE__ */ new Error(`WebSocket upgrade declined with status ${response.statusCode ?? 0}`));
1238
1308
  });
1239
- request.on("error", (error) => reject(error instanceof Error ? error : new Error(String(error))));
1309
+ request.on("error", (error) => {
1310
+ if (this.#closed) {
1311
+ resolve();
1312
+ return;
1313
+ }
1314
+ reject(error instanceof Error ? error : new Error(String(error)));
1315
+ });
1240
1316
  request.end();
1241
1317
  });
1242
1318
  }
1243
- async send(message) {
1244
- const socket = this.#socket;
1245
- if (socket === void 0) throw new Error("WebSocket transport is not connected");
1246
- socket.send(JSON.stringify(message));
1319
+ #bind(ws) {
1320
+ ws.emitter.on("message", this.#frame);
1321
+ ws.emitter.on("close", this.#ending);
1322
+ ws.emitter.on("error", this.#failure);
1247
1323
  }
1248
- async close() {
1249
- if (this.#closed) return;
1250
- this.#closed = true;
1324
+ #release() {
1251
1325
  const socket = this.#socket;
1252
- this.#socket = void 0;
1253
- if (socket !== void 0) socket.close();
1254
- this.#emitter.emit("close");
1255
- }
1256
- #bind(ws) {
1257
- ws.emitter.on("message", (text) => this.#receive(text));
1258
- ws.emitter.on("close", () => this.#onClose(ws));
1259
- ws.emitter.on("error", (error) => this.#emitter.emit("error", error));
1326
+ if (socket === void 0) return;
1327
+ socket.emitter.off("message", this.#frame);
1328
+ socket.emitter.off("close", this.#ending);
1329
+ socket.emitter.off("error", this.#failure);
1260
1330
  }
1261
1331
  #receive(text) {
1262
1332
  let parsed;
@@ -1273,9 +1343,10 @@ var WebSocketClientTransport = class {
1273
1343
  }
1274
1344
  this.#emitter.emit("message", message);
1275
1345
  }
1276
- #onClose(socket) {
1277
- if (this.#closed || this.#socket !== socket) return;
1346
+ #onClose() {
1347
+ if (this.#closed) return;
1278
1348
  this.#closed = true;
1349
+ this.#release();
1279
1350
  this.#socket = void 0;
1280
1351
  this.#emitter.emit("close");
1281
1352
  }
@@ -1291,7 +1362,7 @@ var WebSocketClientTransport = class {
1291
1362
  //#region src/server/transports/StdioClientTransport.ts
1292
1363
  /**
1293
1364
  * The stdio CLIENT transport for the Model Context Protocol — a
1294
- * {@link MCPClientTransportInterface} that drives a CHILD PROCESS MCP server over
1365
+ * {@link StdioClientTransportInterface} that drives a CHILD PROCESS MCP server over
1295
1366
  * newline-delimited JSON-RPC on `stdin`/`stdout`, the stdio sibling of {@link
1296
1367
  * import('./HTTPClientTransport.js').HTTPClientTransport} and {@link
1297
1368
  * import('./WebSocketClientTransport.js').WebSocketClientTransport}.
@@ -1304,22 +1375,40 @@ var WebSocketClientTransport = class {
1304
1375
  * - **Inbound (`message`).** Standard output is drained eagerly through the supervisor's
1305
1376
  * `readline`-framed `lines` iterable, so a multi-byte UTF-8 sequence split across two reads is
1306
1377
  * decoded whole and a final line written without a trailing newline still arrives. Each framed
1307
- * line is decoded and delivered via the shared {@link dispatchLines} helper — a well-formed
1308
- * {@link JSONRPCMessage} emits `message`, a malformed line emits `error` (§14, never throws).
1378
+ * line is decoded and delivered through the shared {@link dispatchLines} helper — a well-formed
1379
+ * {@link JSONRPCMessage} emits `message`, a malformed line emits `error` (never throws).
1309
1380
  * - **Outbound (`send`).** `send(message)` writes one newline-terminated `JSON.stringify`d line
1310
1381
  * through the supervisor's `send` and AWAITS its answer, so this promise settles only after the
1311
1382
  * host reports the line handled rather than the moment the write is queued. The supervisor never
1312
1383
  * rejects — it answers `false` for a channel that was closed, destroyed, or ended, and for a write
1313
1384
  * that failed — so a `false` answer REJECTS here with the same not-connected error a transport
1314
1385
  * that was never started raises. A dead peer surfaces at the caller instead of vanishing.
1315
- * - **`close()`** terminates the child through the supervisor's bounded `SIGTERM` → grace →
1316
- * `SIGKILL` group-kill, awaits its observed exit, tears down the supervisor, and fires `close`
1317
- * once (idempotent). On a POSIX host the child leads its own process group, so the group-kill
1318
- * reaches its grandchildren rather than orphaning them.
1319
- * - **Observable (§13).** Owns the `emitter` ({@link MCPClientTransportEventMap}); the
1386
+ * - **`close()`** runs the supervisor's bounded termination and teardown, then fires `close` once
1387
+ * (idempotent). That teardown reaches the child's TERMINAL MOMENT, where the supervisor freezes
1388
+ * `evidence`, ends `lines`, and settles `exit` together, so this transport needs no release of
1389
+ * its own to get its line pump back: the stream ends under the pump rather than throwing at it.
1390
+ * A line the supervisor had already framed behind the one being delivered is dropped rather than
1391
+ * emitted onto a transport whose teardown has begun. A `close()` issued while that teardown runs
1392
+ * joins it rather than opening a second one, so it resolves only after `close` has fired, and a
1393
+ * `start()` issued while it runs waits behind the same barrier, so lifetimes never overlap. A
1394
+ * descendant can retain an inherited stdout pipe after the child exits; the supervisor's `drain`
1395
+ * bound cuts that wait off, so this transport's `close()` settles within that bound rather than
1396
+ * on the descendant. The termination itself belongs to the host: a POSIX host signals the
1397
+ * child's own process group `SIGTERM`, waits the grace window, then `SIGKILL`s through the same
1398
+ * route, so the kill reaches grandchildren rather than orphaning them, while Windows ends the
1399
+ * tree with `taskkill /F /T`, which nothing in the child can intercept.
1400
+ * - **Evidence.** `evidence` reports that retained stderr tail off the HELD child — its live tail
1401
+ * while the child runs, and the value the supervisor froze at that child's terminal moment
1402
+ * afterwards. The reference is held past that moment and replaced only by the next `start()`,
1403
+ * which is what keeps a post-`close()` read stable without a private copy: the frozen value
1404
+ * never moves again, so a detached descendant writing to the inherited stderr after the cutoff
1405
+ * cannot grow it. See {@link StdioClientTransportInterface.evidence} for the readings and the
1406
+ * byte bound.
1407
+ * - **Observable.** Owns the `emitter` ({@link MCPClientTransportEventMap}); the
1320
1408
  * emitter isolates a listener throw; `error` is a DOMAIN event (a transport-level
1321
- * fault, including the child spawn cause the supervisor surfaces), distinct from the emitter's
1322
- * own listener-error channel.
1409
+ * fault, including the child spawn cause the supervisor surfaces and the notice that this
1410
+ * lifetime's `evidence` was cut off at the `drain` bound), distinct from the emitter's own
1411
+ * listener-error channel.
1323
1412
  *
1324
1413
  * @example
1325
1414
  * ```ts
@@ -1334,6 +1423,7 @@ var StdioClientTransport = class {
1334
1423
  #args;
1335
1424
  #env;
1336
1425
  #process = void 0;
1426
+ #closing = void 0;
1337
1427
  #closed = false;
1338
1428
  constructor(options) {
1339
1429
  this.#emitter = new Emitter();
@@ -1348,8 +1438,20 @@ var StdioClientTransport = class {
1348
1438
  get duplex() {
1349
1439
  return true;
1350
1440
  }
1441
+ get evidence() {
1442
+ return this.#process?.evidence;
1443
+ }
1351
1444
  async start() {
1352
- if (this.#process !== void 0) return;
1445
+ let closing = this.#closing;
1446
+ while (closing !== void 0) {
1447
+ await closing;
1448
+ if (this.#closing === closing) {
1449
+ this.#closing = void 0;
1450
+ break;
1451
+ }
1452
+ closing = this.#closing;
1453
+ }
1454
+ if (this.#process !== void 0 && !this.#closed) return;
1353
1455
  this.#closed = false;
1354
1456
  const child = new Process({
1355
1457
  command: {
@@ -1363,33 +1465,49 @@ var StdioClientTransport = class {
1363
1465
  });
1364
1466
  this.#process = child;
1365
1467
  child.emitter.on("error", (cause) => this.#emitter.emit("error", cause));
1366
- child.exit.then(() => this.#onExit(child));
1468
+ child.exit.then((exit) => this.#onExit(child, exit));
1367
1469
  this.#pump(child);
1368
1470
  }
1369
1471
  async send(message) {
1370
- const child = this.#process;
1472
+ const child = this.#closed ? void 0 : this.#process;
1371
1473
  if (!(child === void 0 ? false : await child.send(JSON.stringify(message)))) throw new Error("stdio transport is not connected");
1372
1474
  }
1373
1475
  async close() {
1476
+ if (this.#closed && this.#closing === void 0) return;
1477
+ this.#closing ??= this.#teardown();
1478
+ await this.#closing;
1479
+ }
1480
+ async #teardown() {
1374
1481
  if (this.#closed) return;
1375
1482
  this.#closed = true;
1376
1483
  const child = this.#process;
1377
- this.#process = void 0;
1378
- if (child !== void 0) await child.destroy();
1484
+ if (child !== void 0) {
1485
+ await child.destroy();
1486
+ this.#report(await child.exit);
1487
+ }
1379
1488
  this.#emitter.emit("close");
1380
1489
  }
1381
1490
  async #pump(child) {
1382
1491
  for await (const line of child.lines) {
1383
- if (this.#process !== child) return;
1492
+ if (this.#closed || this.#process !== child) return;
1384
1493
  dispatchLines(this.#emitter, [line]);
1385
1494
  }
1386
1495
  }
1387
- #onExit(child) {
1388
- if (this.#closed || this.#process !== child) return;
1496
+ #onExit(child, exit) {
1497
+ if (this.#process !== child) return;
1498
+ if (this.#closed) return;
1389
1499
  this.#closed = true;
1390
- this.#process = void 0;
1500
+ const barrier = Promise.withResolvers();
1501
+ this.#closing ??= barrier.promise;
1502
+ this.#report(exit);
1503
+ barrier.resolve();
1504
+ if (this.#closing === barrier.promise) this.#closing = void 0;
1391
1505
  this.#emitter.emit("close");
1392
1506
  }
1507
+ #report(exit) {
1508
+ if (exit.drained) return;
1509
+ this.#emitter.emit("error", /* @__PURE__ */ new Error("stdio transport evidence may be incomplete: the child streams stayed open past the supervisor drain bound"));
1510
+ }
1393
1511
  };
1394
1512
  //#endregion
1395
1513
  //#region src/server/transports/StdioServerTransport.ts
@@ -1402,22 +1520,31 @@ var StdioClientTransport = class {
1402
1520
  * import('./WebSocketServerTransport.js').WebSocketServerTransport}.
1403
1521
  *
1404
1522
  * @remarks
1405
- * - **Reuses `MCPClientTransportInterface` (§21).** The same generic carrier the HTTP
1523
+ * - **Reuses `MCPClientTransportInterface`.** The same generic carrier the HTTP
1406
1524
  * and WebSocket server transports implement — `emitter` (`message` / `close` /
1407
1525
  * `error`), `start`, `send`, `close`. `session` is `undefined` (the stateless v1).
1408
1526
  * - **Inbound (`message`).** `start()` subscribes to `input`'s `data` event; each
1409
1527
  * chunk is folded through the shared {@link extractLines} line-framing helper
1410
1528
  * (buffering a partial trailing line across reads), and every complete line is
1411
- * decoded and delivered via the shared {@link dispatchLines} helper — a
1529
+ * decoded and delivered through the shared {@link dispatchLines} helper — a
1412
1530
  * well-formed {@link JSONRPCMessage} re-emits on `message`, a malformed line
1413
- * emits `error` (§14, never throws). `input`'s `close` bridges to this
1531
+ * emits `error` (never throws). `input`'s `close` bridges to this
1414
1532
  * transport's `close`.
1415
1533
  * - **Outbound (`send`).** `send(message)` writes one newline-terminated
1416
1534
  * `JSON.stringify`d line to `output`.
1417
- * - **`close()`** fires this transport's `close` (idempotent) the injected streams
1418
- * are owned by the caller (typically `process.stdin`/`process.stdout`, which must
1419
- * never be closed out from under the process) and are not torn down here.
1420
- * - **Observable (§13).** Owns the `emitter` ({@link MCPClientTransportEventMap}); the
1535
+ * - **`close()`** removes this transport's input subscriptions and fires its `close`
1536
+ * event (idempotent). It pauses the input only when the caller was not already reading
1537
+ * it at `start` (`readableFlowing !== true`) AND no `data` listener remains once this
1538
+ * transport's own is removed so a process holding `process.stdin` can exit, and a
1539
+ * caller's own flow is never stopped underneath it. The transport preserves flowing versus
1540
+ * non-flowing state and restores every caller-owned listener. A Node stream that had never been
1541
+ * read starts with `readableFlowing === null` and is left non-flowing (`false`), because Node
1542
+ * exposes no public operation that restores `null` after data consumption starts. Attaching a
1543
+ * later `data` listener does not resume that stream; the caller must call `resume()` before the
1544
+ * listener receives data. The injected streams are owned by the caller (typically
1545
+ * `process.stdin`/`process.stdout`), so the transport never destroys, ends, or blanket-clears
1546
+ * them.
1547
+ * - **Observable.** Owns the `emitter` ({@link MCPClientTransportEventMap}); the
1421
1548
  * emitter isolates a listener throw; `error` is a DOMAIN event (a transport-level
1422
1549
  * fault), distinct from the emitter's own listener-error channel.
1423
1550
  */
@@ -1425,9 +1552,13 @@ var StdioServerTransport = class {
1425
1552
  #emitter;
1426
1553
  #input;
1427
1554
  #output;
1555
+ #data = (chunk) => this.#receive(chunk.toString());
1556
+ #ending = () => this.#onClose();
1557
+ #failure = (error) => this.#emitter.emit("error", error);
1428
1558
  #buffer = "";
1429
1559
  #started = false;
1430
1560
  #closed = false;
1561
+ #flowing = false;
1431
1562
  constructor(input, output) {
1432
1563
  this.#emitter = new Emitter();
1433
1564
  this.#input = input;
@@ -1443,9 +1574,10 @@ var StdioServerTransport = class {
1443
1574
  async start() {
1444
1575
  if (this.#started || this.#closed) return;
1445
1576
  this.#started = true;
1446
- this.#input.on("data", (chunk) => this.#receive(chunk.toString()));
1447
- this.#input.on("close", () => this.#onClose());
1448
- this.#input.on("error", (error) => this.#emitter.emit("error", error));
1577
+ this.#flowing = this.#input instanceof Readable && this.#input.readableFlowing === true;
1578
+ this.#input.on("data", this.#data);
1579
+ this.#input.on("close", this.#ending);
1580
+ this.#input.on("error", this.#failure);
1449
1581
  }
1450
1582
  async send(message) {
1451
1583
  this.#output.write(`${JSON.stringify(message)}\n`);
@@ -1453,6 +1585,7 @@ var StdioServerTransport = class {
1453
1585
  async close() {
1454
1586
  if (this.#closed) return;
1455
1587
  this.#closed = true;
1588
+ this.#release();
1456
1589
  this.#emitter.emit("close");
1457
1590
  }
1458
1591
  #receive(chunk) {
@@ -1463,13 +1596,20 @@ var StdioServerTransport = class {
1463
1596
  #onClose() {
1464
1597
  if (this.#closed) return;
1465
1598
  this.#closed = true;
1599
+ this.#release();
1466
1600
  this.#emitter.emit("close");
1467
1601
  }
1602
+ #release() {
1603
+ this.#input.removeListener("data", this.#data);
1604
+ this.#input.removeListener("close", this.#ending);
1605
+ this.#input.removeListener("error", this.#failure);
1606
+ if (!this.#flowing && this.#input.listenerCount("data") === 0) this.#input.pause();
1607
+ }
1468
1608
  };
1469
1609
  //#endregion
1470
1610
  //#region src/server/factories.ts
1471
1611
  /**
1472
- * Adapt the installed server token primitives to the host-neutral MCP continuation port.
1612
+ * Adapts the installed server token primitives to the host-neutral MCP continuation port.
1473
1613
  *
1474
1614
  * @param secret - Current signing secret or `[current, ...older]` rotation list
1475
1615
  * @returns A continuation port that seals and opens opaque canonical state strings
@@ -1485,8 +1625,8 @@ function createMCPContinuation(secret) {
1485
1625
  };
1486
1626
  }
1487
1627
  /**
1488
- * Create the MCP Streamable-HTTP transport routes — mounts a transport-agnostic
1489
- * {@link MCPDispatcherInterface} (the `@src/core` dispatch boundary) on the fetch-standard router
1628
+ * Creates the MCP Streamable-HTTP transport routes — mounts a transport-agnostic
1629
+ * {@link MCPDispatcherInterface} (the `@orkestrel/mcp` dispatch boundary) on the fetch-standard router
1490
1630
  * spine, pumping each `POST` body through `mcp.dispatch`. Returns the {@link RouteInput}s to
1491
1631
  * hand to `router.add(...)`.
1492
1632
  *
@@ -1509,7 +1649,7 @@ function createMCPContinuation(secret) {
1509
1649
  *
1510
1650
  * When `streaming` is enabled (the default) and the client `Accept`s `text/event-stream`,
1511
1651
  * the `200` reply is framed as a Streamable-HTTP SSE response (one `data:` event carrying
1512
- * the JSON-RPC envelope, then the stream ends) via `@orkestrel/server`'s generic
1652
+ * the JSON-RPC envelope, then the stream ends) through `@orkestrel/server`'s generic
1513
1653
  * {@link import('@orkestrel/server').openStream} seam; otherwise it is a plain JSON body.
1514
1654
  *
1515
1655
  * **Sessions are a SEPARATE, plug-and-play middleware.** `createMCPRoutes` mints / reads no
@@ -1531,11 +1671,12 @@ function createMCPContinuation(secret) {
1531
1671
  *
1532
1672
  * @example
1533
1673
  * ```ts
1534
- * import { createMCPLegacy, createMCPServer, createToolManager } from '@src/core'
1535
- * import { createMCPRoutes } from '@src/server'
1674
+ * import { createMCPLegacy, createMCPServer } from '@orkestrel/mcp'
1675
+ * import { createMCPRoutes } from '@orkestrel/mcp/server'
1676
+ * import { createToolManager } from '@orkestrel/tool'
1536
1677
  *
1537
1678
  * const mcp = createMCPServer({ identity: { name: 'docs', version: '1.0.0' }, tools: createToolManager() })
1538
- * const routes = createMCPRoutes(createMCPLegacy(mcp)) // both eras; pass `mcp` for modern only
1679
+ * const routes = createMCPRoutes(createMCPLegacy(mcp)) // answers `initialize` too; pass `mcp` alone for modern-only
1539
1680
  * ```
1540
1681
  */
1541
1682
  function createMCPRoutes(mcp, options) {
@@ -1547,7 +1688,7 @@ function createMCPRoutes(mcp, options) {
1547
1688
  }];
1548
1689
  }
1549
1690
  /**
1550
- * Create the HTTP CLIENT transport for an {@link import('@src/core').MCPClientInterface}
1691
+ * Creates the HTTP CLIENT transport for an {@link import('@orkestrel/mcp').MCPClientInterface}
1551
1692
  * — a {@link MCPClientTransportInterface} that drives a REMOTE Streamable-HTTP MCP server
1552
1693
  * over `fetch`. The egress mirror of {@link createMCPRoutes}.
1553
1694
  *
@@ -1555,9 +1696,9 @@ function createMCPRoutes(mcp, options) {
1555
1696
  * Hand it to `createMCPClient({ transport })`: each JSON-RPC message the client sends is
1556
1697
  * `POST`ed to `options.url` with `content-type: application/json` and an `Accept` of
1557
1698
  * both `application/json` and `text/event-stream` (the server answers with EITHER — a
1558
- * plain JSON envelope or a Streamable-HTTP SSE `data:` event, decoded via `@orkestrel/sse`),
1699
+ * plain JSON envelope or a Streamable-HTTP SSE `data:` event, decoded with `@orkestrel/sse`),
1559
1700
  * and the reply is surfaced on the transport's `message` event for the client's id
1560
- * correlation. Add `options.headers` (e.g. an `Authorization` bearer) to reach a guarded
1701
+ * correlation. Add `options.headers` (for example, an `Authorization` bearer) to reach a guarded
1561
1702
  * server. `start` / `close` hold no connection; against a STATEFUL server it captures the
1562
1703
  * `mcp-session-id` from `initialize` and echoes it on later requests. It also captures
1563
1704
  * the initialize result's `protocolVersion` and sends `mcp-protocol-version` alone on each
@@ -1566,13 +1707,13 @@ function createMCPRoutes(mcp, options) {
1566
1707
  *
1567
1708
  * @param options - `url` (the remote endpoint; REQUIRED), optional `headers` merged onto
1568
1709
  * every request, optional `fetch` (default `globalThis.fetch`), and optional `timeout`
1569
- * (ms, applied via `AbortSignal.timeout`); see {@link HTTPClientTransportOptions}
1710
+ * (ms, applied with `AbortSignal.timeout`); see {@link HTTPClientTransportOptions}
1570
1711
  * @returns A working {@link MCPClientTransportInterface} over `fetch`
1571
1712
  *
1572
1713
  * @example
1573
1714
  * ```ts
1574
- * import { createMCPClient } from '@src/core'
1575
- * import { createHTTPClientTransport } from '@src/server'
1715
+ * import { createMCPClient } from '@orkestrel/mcp'
1716
+ * import { createHTTPClientTransport } from '@orkestrel/mcp/server'
1576
1717
  *
1577
1718
  * const client = createMCPClient({
1578
1719
  * transport: createHTTPClientTransport({ url: 'http://localhost:3000/mcp' }),
@@ -1585,7 +1726,7 @@ function createHTTPClientTransport(options) {
1585
1726
  return new HTTPClientTransport(options);
1586
1727
  }
1587
1728
  /**
1588
- * Create the MCP WebSocket transport INGRESS — an {@link UpgradeHandler} that exposes a
1729
+ * Creates the MCP WebSocket transport INGRESS — an {@link UpgradeHandler} that exposes a
1589
1730
  * transport-agnostic {@link MCPDispatcherInterface} over a WebSocket, the WebSocket mirror of
1590
1731
  * {@link createMCPRoutes}. Register it on the spine's upgrade seam.
1591
1732
  *
@@ -1600,11 +1741,11 @@ function createHTTPClientTransport(options) {
1600
1741
  * A decline NEVER writes to the socket (it is not yet ours) — the spine owns the unclaimed
1601
1742
  * outcome.
1602
1743
  * - **Claims (returns `true`)** otherwise: it builds `createNodeWebSocket({ socket, key, head,
1603
- * protocol })` (SERVER mode → writes the `101` handshake, echoing the `subprotocol`, default
1604
- * {@link MCP_WEBSOCKET_SUBPROTOCOL} `'mcp'`, and sends UNMASKED frames), wraps it in a
1744
+ * protocol })` (SERVER mode → writes the `101` handshake, selects the configured subprotocol
1745
+ * only when the client's offer contains it, and sends UNMASKED frames), wraps it in a
1605
1746
  * {@link WebSocketServerTransport}, and pipes it through the core {@link
1606
- * import('@src/core').MCPTransportInterface} port via {@link
1607
- * import('./helpers.js').bridgeMessageTransport} + {@link import('@src/core').bindServer}:
1747
+ * import('@orkestrel/mcp').MCPTransportInterface} port through {@link
1748
+ * import('./helpers.js').bridgeMessageTransport} + {@link import('@orkestrel/mcp').bindServer}:
1608
1749
  * each inbound REQUEST runs through `mcp.dispatch`, and a defined response is written back
1609
1750
  * as a frame — a NOTIFICATION sends nothing, and a non-request message (a stray response) is
1610
1751
  * ignored. A `dispatch` / `send` fault surfaces on `mcp.emitter`'s `error` event rather than
@@ -1629,19 +1770,24 @@ function createHTTPClientTransport(options) {
1629
1770
  *
1630
1771
  * @example
1631
1772
  * ```ts
1632
- * import { createMCPServer, createToolManager } from '@src/core'
1633
- * import { createWebSocketServer } from '@src/server'
1773
+ * import { createMCPLegacy, createMCPServer } from '@orkestrel/mcp'
1774
+ * import { createWebSocketServer } from '@orkestrel/mcp/server'
1775
+ * import { createToolManager } from '@orkestrel/tool'
1634
1776
  *
1635
1777
  * const mcp = createMCPServer({ identity: { name: 'docs', version: '1.0.0' }, tools: createToolManager() })
1636
- * server.upgrade(createWebSocketServer(mcp, { emitter: server.emitter })) // ws://…/mcp
1778
+ * // Claims the MCP upgrade at ws://…/mcp:
1779
+ * server.upgrade(createWebSocketServer(createMCPLegacy(mcp), { emitter: server.emitter })) // answers `initialize` too; pass `mcp` alone for modern-only
1637
1780
  * ```
1638
1781
  */
1639
1782
  function createWebSocketServer(mcp, options) {
1640
1783
  const path = options.path ?? "/mcp";
1641
1784
  const subprotocol = options.subprotocol ?? "mcp";
1642
- const live = /* @__PURE__ */ new Set();
1785
+ const live = /* @__PURE__ */ new Map();
1643
1786
  options.emitter.on("stop", () => {
1644
- for (const transport of live) transport.close();
1787
+ for (const [transport, unbind] of live) {
1788
+ unbind();
1789
+ transport.close();
1790
+ }
1645
1791
  });
1646
1792
  return (request, socket, head) => {
1647
1793
  const upgrade = request.headers["upgrade"];
@@ -1651,21 +1797,26 @@ function createWebSocketServer(mcp, options) {
1651
1797
  if (!isString(key)) return false;
1652
1798
  const version = request.headers["sec-websocket-version"];
1653
1799
  if (!isString(version) || version !== WEBSOCKET_VERSION) return false;
1800
+ const offer = request.headers["sec-websocket-protocol"];
1801
+ const protocol = isString(offer) && offer.split(",").some((candidate) => candidate.trim() === subprotocol) ? subprotocol : void 0;
1654
1802
  const transport = new WebSocketServerTransport(createNodeWebSocket({
1655
1803
  socket,
1656
1804
  key,
1657
1805
  head,
1658
- protocol: subprotocol
1806
+ ...protocol === void 0 ? {} : { protocol }
1659
1807
  }));
1660
- live.add(transport);
1661
- transport.emitter.on("close", () => live.delete(transport));
1662
- bindServer(mcp, bridgeMessageTransport(transport));
1808
+ const unbind = bindServer(mcp, bridgeMessageTransport(transport));
1809
+ live.set(transport, unbind);
1810
+ transport.emitter.on("close", () => {
1811
+ live.delete(transport);
1812
+ unbind();
1813
+ });
1663
1814
  transport.start();
1664
1815
  return true;
1665
1816
  };
1666
1817
  }
1667
1818
  /**
1668
- * Create the WebSocket CLIENT transport for an {@link import('@src/core').MCPClientInterface}
1819
+ * Creates the WebSocket CLIENT transport for an {@link import('@orkestrel/mcp').MCPClientInterface}
1669
1820
  * — a {@link MCPClientTransportInterface} that drives a REMOTE MCP server over a WebSocket. The
1670
1821
  * egress mirror of {@link createWebSocketServer} and the WebSocket sibling of {@link
1671
1822
  * createHTTPClientTransport}.
@@ -1674,11 +1825,11 @@ function createWebSocketServer(mcp, options) {
1674
1825
  * Hand it to `createMCPClient({ transport })`: `start()` (run by `client.connect()`) performs
1675
1826
  * the RFC 6455 client handshake against `options.url` (accepting a `ws://` / `wss://` or an
1676
1827
  * `http://` / `https://` URL — a `ws(s)` scheme is converted to `http(s)` for the underlying
1677
- * upgrade request), validates the `Sec-WebSocket-Accept` (via `@orkestrel/websocket`'s
1828
+ * upgrade request), validates the `Sec-WebSocket-Accept` (with `@orkestrel/websocket`'s
1678
1829
  * `computeWebSocketAccept`), and opens a persistent bidirectional frame channel; each JSON-RPC
1679
1830
  * message the client `send`s is written as one masked text frame, and each decoded reply is
1680
1831
  * surfaced on the transport's `message` event for the client's id correlation. Add
1681
- * `options.headers` (e.g. an `Authorization` bearer) to reach a guarded server.
1832
+ * `options.headers` (for example, an `Authorization` bearer) to reach a guarded server.
1682
1833
  *
1683
1834
  * @param options - `url` (the remote WebSocket endpoint; REQUIRED) and optional `headers`
1684
1835
  * merged onto the upgrade request; see {@link WebSocketClientTransportOptions}
@@ -1686,8 +1837,8 @@ function createWebSocketServer(mcp, options) {
1686
1837
  *
1687
1838
  * @example
1688
1839
  * ```ts
1689
- * import { createMCPClient } from '@src/core'
1690
- * import { createWebSocketClientTransport } from '@src/server'
1840
+ * import { createMCPClient } from '@orkestrel/mcp'
1841
+ * import { createWebSocketClientTransport } from '@orkestrel/mcp/server'
1691
1842
  *
1692
1843
  * const client = createMCPClient({
1693
1844
  * transport: createWebSocketClientTransport({ url: 'ws://localhost:3000/mcp' }),
@@ -1700,28 +1851,30 @@ function createWebSocketClientTransport(options) {
1700
1851
  return new WebSocketClientTransport(options);
1701
1852
  }
1702
1853
  /**
1703
- * Create the stdio CLIENT transport for an {@link import('@src/core').MCPClientInterface}
1704
- * — a {@link MCPClientTransportInterface} that spawns and drives a CHILD PROCESS MCP server
1854
+ * Creates the stdio CLIENT transport for an {@link import('@orkestrel/mcp').MCPClientInterface}
1855
+ * — a {@link StdioClientTransportInterface} that spawns and drives a CHILD PROCESS MCP server
1705
1856
  * over newline-delimited JSON-RPC on `stdin`/`stdout`, the stdio sibling of {@link
1706
1857
  * createHTTPClientTransport} and {@link createWebSocketClientTransport}.
1707
1858
  *
1708
1859
  * @remarks
1709
1860
  * Hand it to `createMCPClient({ transport })`: `start()` (run by `client.connect()`)
1710
1861
  * spawns `options.command` with `options.args` and `options.env`, piping its
1711
- * `stdin`/`stdout` for the JSON-RPC channel (its `stderr` inherits the parent's for
1712
- * diagnostics). Each JSON-RPC message the client `send`s is written as one
1862
+ * `stdin`/`stdout` for the JSON-RPC channel. The child's `stderr` is piped too, and
1863
+ * retained as a bounded tail this transport reports as `evidence` the parent never
1864
+ * inherits it. Each JSON-RPC message the client `send`s is written as one
1713
1865
  * newline-terminated line to the child's `stdin`; each decoded reply line from the
1714
1866
  * child's `stdout` is surfaced on the transport's `message` event for the client's
1715
1867
  * id correlation.
1716
1868
  *
1717
1869
  * @param options - `command` (the executable to spawn; REQUIRED), optional `args`,
1718
1870
  * and optional `env`; see {@link StdioClientTransportOptions}
1719
- * @returns A working {@link MCPClientTransportInterface} over a child process's stdio
1871
+ * @returns A working {@link StdioClientTransportInterface} over a child process's stdio,
1872
+ * whose `evidence` carries the supervised child's bounded stderr tail
1720
1873
  *
1721
1874
  * @example
1722
1875
  * ```ts
1723
- * import { createMCPClient } from '@src/core'
1724
- * import { createStdioClientTransport } from '@src/server'
1876
+ * import { createMCPClient } from '@orkestrel/mcp'
1877
+ * import { createStdioClientTransport } from '@orkestrel/mcp/server'
1725
1878
  *
1726
1879
  * const client = createMCPClient({
1727
1880
  * transport: createStdioClientTransport({ command: 'node', args: ['./server.js'] }),
@@ -1734,16 +1887,16 @@ function createStdioClientTransport(options) {
1734
1887
  return new StdioClientTransport(options);
1735
1888
  }
1736
1889
  /**
1737
- * Create the MCP stdio transport INGRESS — pumps a transport-agnostic {@link
1890
+ * Creates the MCP stdio transport INGRESS — pumps a transport-agnostic {@link
1738
1891
  * MCPDispatcherInterface} over newline-delimited JSON-RPC on `stdin`/`stdout` (or an
1739
1892
  * injected stream pair), the stdio mirror of {@link createWebSocketServer}.
1740
1893
  *
1741
1894
  * @remarks
1742
1895
  * Wraps `options.input` (default `process.stdin`) / `options.output` (default
1743
1896
  * `process.stdout`) in a {@link import('./transports/StdioServerTransport.js').StdioServerTransport}
1744
- * and pipes it through the core {@link import('@src/core').MCPTransportInterface} port
1745
- * via {@link import('./helpers.js').bridgeMessageTransport} + {@link
1746
- * import('@src/core').bindServer}: each inbound REQUEST runs through `mcp.dispatch`, and
1897
+ * and pipes it through the core {@link import('@orkestrel/mcp').MCPTransportInterface} port
1898
+ * through {@link import('./helpers.js').bridgeMessageTransport} + {@link
1899
+ * import('@orkestrel/mcp').bindServer}: each inbound REQUEST runs through `mcp.dispatch`, and
1747
1900
  * a defined response is written back as a newline-terminated line — a NOTIFICATION
1748
1901
  * writes nothing, and a non-request message is ignored. A `dispatch` / `send` fault
1749
1902
  * surfaces on `mcp.emitter`'s `error` event rather than escaping the (async) message
@@ -1756,21 +1909,24 @@ function createStdioClientTransport(options) {
1756
1909
  *
1757
1910
  * @example
1758
1911
  * ```ts
1759
- * import { createMCPServer, createToolManager } from '@src/core'
1760
- * import { createStdioServer } from '@src/server'
1912
+ * import { createMCPLegacy, createMCPServer } from '@orkestrel/mcp'
1913
+ * import { createStdioServer } from '@orkestrel/mcp/server'
1914
+ * import { createToolManager } from '@orkestrel/tool'
1761
1915
  *
1762
1916
  * const mcp = createMCPServer({ identity: { name: 'docs', version: '1.0.0' }, tools: createToolManager() })
1763
- * createStdioServer(mcp).start() // an MCP client now connects over this process's stdio
1917
+ * // An MCP client now connects over this process's stdio:
1918
+ * createStdioServer(createMCPLegacy(mcp)).start() // answers `initialize` too; pass `mcp` alone for modern-only
1764
1919
  * ```
1765
1920
  */
1766
1921
  function createStdioServer(mcp, options) {
1767
1922
  const transport = new StdioServerTransport(options?.input ?? process.stdin, options?.output ?? process.stdout);
1768
- bindServer(mcp, bridgeMessageTransport(transport));
1923
+ const unbind = bindServer(mcp, bridgeMessageTransport(transport));
1769
1924
  return {
1770
1925
  start() {
1771
1926
  transport.start();
1772
1927
  },
1773
1928
  stop() {
1929
+ unbind();
1774
1930
  transport.close();
1775
1931
  }
1776
1932
  };
@@ -1778,9 +1934,9 @@ function createStdioServer(mcp, options) {
1778
1934
  //#endregion
1779
1935
  //#region src/server/middlewares.ts
1780
1936
  /**
1781
- * Create the native MCP session {@link MiddlewareHandler} — the plug-and-play stateful layer
1937
+ * Creates the native MCP session {@link MiddlewareHandler} — the plug-and-play stateful layer
1782
1938
  * that fronts a session-agnostic {@link import('./factories.js').createMCPRoutes}. Compose it
1783
- * via `router.use(createMCPSession())` (or the equivalent middleware seam), mirroring any
1939
+ * with `router.use(createMCPSession())` (or the equivalent middleware seam), mirroring any
1784
1940
  * other closure-scoped stateful middleware. Has NO dependency on `@orkestrel/middleware` — the
1785
1941
  * session store, mint-on-`initialize`, and resumable stream are all native to this package.
1786
1942
  *
@@ -1789,11 +1945,11 @@ function createStdioServer(mcp, options) {
1789
1945
  * `path` (default {@link DEFAULT_MCP_PATH}); a request to any other path passes straight
1790
1946
  * through (`next()`).
1791
1947
  *
1792
- * A modern-shaped POST also passes straight through via `next()`, ignoring any session id.
1948
+ * A modern-shaped POST also passes straight through with `next()`, ignoring any session id.
1793
1949
  * The remaining behavior is the legacy session layer:
1794
1950
  *
1795
1951
  * - **`POST {path}`.** Buffers `const text = await request.text()` (so the downstream route
1796
- * can re-read it via a freshly-built forwarded `Request`). Resolves a session via {@link
1952
+ * can re-read it from a freshly-built forwarded `Request`). Resolves a session through {@link
1797
1953
  * readSessionHeader}: a VALID id touches the entry and sets `context.state.session`; an
1798
1954
  * ABSENT / unknown id whose (guarded) body parses to an `initialize` request ({@link
1799
1955
  * isInitializeRequest}) MINTS a fresh {@link MCPSession} (`crypto.randomUUID()`, `capacity`)
@@ -1808,7 +1964,7 @@ function createStdioServer(mcp, options) {
1808
1964
  * a `DELETE` arriving while the request was suspended is not undone.
1809
1965
  * - **`GET {path}`.** Resolves the session the same way (no mint — only `initialize` mints);
1810
1966
  * an invalid / unknown id is the same `404`. A valid session opens the resumable
1811
- * server→client stream via `@orkestrel/server`'s {@link import('@orkestrel/server').openStream}:
1967
+ * server→client stream through `@orkestrel/server`'s {@link import('@orkestrel/server').openStream}:
1812
1968
  * replays every event after the client's `Last-Event-ID` ({@link readLastEventId}) BEFORE
1813
1969
  * attaching the stream for live pushes, then attaches; cancellation of the streamed response
1814
1970
  * body composes with `request.signal` and detaches it. Long-lived — never `end()`ed here.
@@ -1832,12 +1988,14 @@ function createStdioServer(mcp, options) {
1832
1988
  *
1833
1989
  * @example
1834
1990
  * ```ts
1835
- * import { createMCPServer, createToolManager } from '@src/core'
1836
- * import { createMCPRoutes, createMCPSession } from '@src/server'
1991
+ * import { createMCPLegacy, createMCPServer } from '@orkestrel/mcp'
1992
+ * import { createMCPRoutes, createMCPSession } from '@orkestrel/mcp/server'
1993
+ * import { createToolManager } from '@orkestrel/tool'
1837
1994
  *
1838
1995
  * const mcp = createMCPServer({ identity: { name: 'docs', version: '1.0.0' }, tools: createToolManager() })
1839
1996
  * router.use(createMCPSession({ ttl: 60_000 })) // stateful: mint + validate + resumable GET / DELETE
1840
- * router.add(createMCPRoutes(mcp)) // the route stays session-agnostic
1997
+ * // The route stays session-agnostic:
1998
+ * router.add(createMCPRoutes(createMCPLegacy(mcp))) // answers `initialize` too; pass `mcp` alone for modern-only
1841
1999
  * ```
1842
2000
  */
1843
2001
  function createMCPSession(options) {