@orkestrel/mcp 0.0.18 → 0.0.20

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