@orkestrel/mcp 0.0.2 → 0.0.4

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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["#emitter","#url","#headers","#fetch","#timeout","#session","#deliver","#id","#events","#streams","#capacity","#ttl","#append","#evict","#counter","#emitter","#socket","#started","#closed","#receive","#onClose","#emitter","#url","#headers","#socket","#closed","#httpURL","#bind","#receive","#onClose","#emitter","#command","#args","#env","#child","#closed","#buffer","#receive","#onClose","#emitter","#input","#output","#started","#closed","#receive","#onClose","#buffer"],"sources":["../../../src/server/constants.ts","../../../src/server/helpers.ts","../../../src/server/transports/HTTPClientTransport.ts","../../../src/server/MCPSession.ts","../../../src/server/transports/WebSocketServerTransport.ts","../../../src/server/transports/WebSocketClientTransport.ts","../../../src/server/transports/StdioClientTransport.ts","../../../src/server/transports/StdioServerTransport.ts","../../../src/server/factories.ts","../../../src/server/middlewares.ts"],"sourcesContent":["// The MCP HTTP-transport constants (AGENTS §5 constants file) — the wire-level header\n// names, the default mount path, and the folded event-log bounds. The HEADER names are\n// the Streamable-HTTP transport's session /\n// protocol-version headers: they go LIVE when a `createMCPSession` middleware is mounted\n// (it mints the session id into `MCP_SESSION_HEADER` on `initialize` and reads it back on\n// subsequent requests); the stateless `createMCPRoutes` default neither sets nor reads\n// them. The transport-agnostic dispatch core (`src/core/mcp`) deliberately does NOT carry\n// these — header names belong to the HTTP transport, here.\n\n/**\n * The Streamable-HTTP transport header that carries the MCP session id. When a {@link\n * import('./middlewares.js').createMCPSession} middleware is mounted, it SETS this header on\n * the `initialize` response (the minted id) and READS it on every subsequent request\n * (validating the session); the stateless `createMCPRoutes` default neither sets nor reads it.\n */\nexport const MCP_SESSION_HEADER = 'mcp-session-id'\n\n/**\n * The Streamable-HTTP transport header that carries the negotiated MCP protocol version\n * on a subsequent request. The version is negotiated in the `initialize` JSON-RPC result\n * body; a stateful transport MAY additionally read this header to pin the per-request\n * protocol version (optional — the result body remains the source of truth).\n */\nexport const MCP_PROTOCOL_VERSION_HEADER = 'mcp-protocol-version'\n\n/** The default request path `createMCPRoutes` mounts the transport's `POST` route at. */\nexport const DEFAULT_MCP_PATH = '/mcp'\n\n/**\n * The WebSocket subprotocol the MCP-over-WebSocket transports negotiate — sent by the\n * client in `Sec-WebSocket-Protocol`, echoed by the server in its `101` handshake.\n *\n * @remarks\n * `createWebSocketServer` echoes it in the upgrade response and `createWebSocketClientTransport`\n * requests it, so an MCP WebSocket endpoint is distinguishable from any other WebSocket on the\n * same path. The default WebSocket upgrade path is {@link DEFAULT_MCP_PATH} (the same `'/mcp'`\n * the HTTP transport mounts at) — the upgrade is selected by the `Upgrade: websocket` header,\n * not a separate path.\n */\nexport const MCP_WEBSOCKET_SUBPROTOCOL = 'mcp'\n\n/**\n * The default capacity of a session's FOLDED resumable event log (the per-{@link\n * import('./MCPSession.js').MCPSession} replay log) — the maximum number of pushed\n * server→client messages retained for replay before the OLDEST is evicted.\n *\n * @remarks\n * Bounds the replay log's memory: only the most-recent {@link DEFAULT_MCP_SESSION_CAPACITY}\n * pushes are retained, so a client reconnecting with a `Last-Event-ID` older than that window\n * replays nothing (its cursor fell off the back). Override per `createMCPSession`'s `capacity`\n * for a deeper / shallower window.\n */\nexport const DEFAULT_MCP_SESSION_CAPACITY = 1024\n\n/**\n * The default per-event idle lifetime (ms) of a session's folded resumable event log — an\n * entry older than this is lazily evicted on the next access (no background timer), bounding\n * how far back a reconnecting client may replay.\n *\n * @remarks\n * Five minutes — a generous reconnection window for a dropped SSE stream without retaining\n * stale pushes indefinitely. The session's own idle TTL is the `createMCPSession` `ttl` knob;\n * this bounds the replay log paired with it.\n */\nexport const DEFAULT_MCP_SESSION_TTL = 300_000\n","import type { ClientTransportEventMap, JSONRPCMessage } from '@src/core'\nimport type { EmitterInterface } from '@orkestrel/emitter'\nimport type { SSEParserInterface } from '@orkestrel/sse'\nimport type { IncomingMessage } from 'node:http'\nimport type { LineExtraction } from './types.js'\nimport { createSSEParser } from '@orkestrel/sse'\nimport { JSONRPC_INVALID_REQUEST, jsonRPCError, parseJSONRPCMessage } from '@src/core'\nimport { isString } from '@orkestrel/contract'\nimport { MCP_SESSION_HEADER } from './constants.js'\n\n// The MCP server-transport helpers (AGENTS §4.3 module-scope names — no entity context).\n// The server-side reader `acceptsEventStream` reads the request's `Accept` header to\n// decide whether a Streamable-HTTP SSE response is allowed; `readSessionHeader` reads the\n// request's `mcp-session-id` header (the stateful transport's session validation);\n// `readLastEventId` reads the request's `Last-Event-ID` header (the resumable GET-SSE\n// replay cursor); the CLIENT-side reader `readEventStream` decodes a `fetch` Response's SSE\n// body back into JSON-RPC messages (the egress mirror, reusing `@orkestrel/sse`'s\n// `SSEParser`); `upgradeRequestPath` reads a raw `node:http` upgrade request's path (the\n// WebSocket transport's upgrade-path match). All are total and narrow at the boundary,\n// never `as` (AGENTS §14) — a missing / non-string Accept reads as \"no\", a missing session /\n// last-event header reads as `undefined`, a non-message SSE `data:` event is dropped, an\n// absent `url` reads as `'/'`.\n\n/**\n * Whether the request's `Accept` header opts into a Server-Sent-Events response.\n *\n * @remarks\n * Reads the fetch-standard `Request.headers.get('accept')` and returns `true` when it\n * contains `text/event-stream` (case-insensitive). The MCP `POST` handler uses it\n * (together with the `streaming` option) to pick the Streamable-HTTP SSE response\n * framing over a plain JSON body; the JSON-RPC envelope is identical either way. Total\n * — an absent / unmatched header returns `false`.\n *\n * @param request - The fetch-standard `Request`\n * @returns `true` when the client `Accept`s `text/event-stream`, else `false`\n */\nexport function acceptsEventStream(request: Request): boolean {\n\tconst accept = request.headers.get('accept')\n\tif (accept === null) return false\n\treturn accept.toLowerCase().includes('text/event-stream')\n}\n\n/**\n * Read the request's `mcp-session-id` header — the session id a stateful transport\n * validates, or `undefined` when absent.\n *\n * @remarks\n * Reads `request.headers.get(MCP_SESSION_HEADER)` — a fetch-standard `Headers` lookup\n * (single-valued by construction, never an array) — so a missing header reads as\n * `undefined` (no session). {@link import('./middlewares.js').createMCPSession} uses it on\n * every `POST` / `GET` / `DELETE` to look the session up in its closure store; an\n * `undefined` id is treated exactly like an unknown one (a `404`). Total — never throws.\n *\n * @param request - The fetch-standard `Request`\n * @returns The session id, or `undefined` when the header is absent\n */\nexport function readSessionHeader(request: Request): string | undefined {\n\tconst id = request.headers.get(MCP_SESSION_HEADER)\n\treturn id === null ? undefined : id\n}\n\n/**\n * Read the request's `Last-Event-ID` header — the SSE resume cursor a client sends when it\n * reconnects to the resumable `GET {path}` stream, or `undefined` when absent.\n *\n * @remarks\n * Reads `request.headers.get('last-event-id')` — a fetch-standard `Headers` lookup — so a\n * missing header reads as `undefined` (no resume, the stream starts fresh). The resumable\n * `GET` handler in {@link import('./middlewares.js').createMCPSession} passes a present value\n * to the session's {@link import('./types.js').MCPSessionInterface.replay} to re-deliver the\n * missed events before attaching the stream for live pushes. Total — never throws.\n *\n * @param request - The fetch-standard `Request`\n * @returns The last-event-id, or `undefined` when the header is absent\n */\nexport function readLastEventId(request: Request): string | undefined {\n\tconst id = request.headers.get('last-event-id')\n\treturn id === null ? undefined : id\n}\n\n/**\n * Build the stateful transport's \"unknown session\" rejection — an HTTP `404` carrying a\n * JSON-RPC error body.\n *\n * @remarks\n * Returns `Response.json(jsonRPCError(null, JSONRPC_INVALID_REQUEST, 'Session not found'),\n * { status: 404 })`, mirroring `createMCPRoutes`'s `400` transport-failure shape (a\n * JSON-RPC error BODY with a `null` id) but at the session-not-found status. Shared by\n * every {@link import('./middlewares.js').createMCPSession} validation site — the\n * non-`initialize` `POST` path, the resumable `GET {path}` open, and the `DELETE {path}`\n * session-end (each a missing / unknown / TTL-evicted id) — so the single `404` envelope\n * is defined once. Total — never throws.\n *\n * @returns The `404` JSON-RPC error `Response`\n */\nexport function rejectUnknownSession(): Response {\n\treturn Response.json(jsonRPCError(null, JSONRPC_INVALID_REQUEST, 'Session not found'), {\n\t\tstatus: 404,\n\t})\n}\n\n/**\n * Decode a `fetch` Response's Server-Sent-Events body into the JSON-RPC messages it\n * carried — the CLIENT-side inverse of the server's Streamable-HTTP SSE response.\n *\n * @remarks\n * Reads the whole `response.body` stream chunk-by-chunk through a `TextDecoder({\n * stream: true })` (handling a multi-byte char split across reads) and `@orkestrel/sse`'s\n * {@link SSEParserInterface} (handling a partial line / in-progress event split across\n * reads), then narrows each dispatched event's `data` to a {@link JSONRPCMessage} via\n * `parseJSONRPCMessage` (so a non-message / non-JSON `data:` event is DROPPED, never\n * thrown — total, §14). It reuses the SAME `SSEParser` the server's `openStream` seam\n * serializes against, so the wire round-trips. A `null` body (no stream) yields no\n * messages; the {@link import('./transports/HTTPClientTransport.js').HTTPClientTransport}\n * reads a request/response SSE reply (the server sends one `data:` event then ends), so\n * this drains to completion.\n *\n * @param response - The SSE `fetch` Response to decode (its `body` is read to completion)\n * @returns Every {@link JSONRPCMessage} the stream carried, in order\n */\nexport async function readEventStream(response: Response): Promise<readonly JSONRPCMessage[]> {\n\tconst body = response.body\n\tif (body === null) return []\n\tconst reader = body.getReader()\n\tconst decoder = new TextDecoder()\n\tconst parser: SSEParserInterface = createSSEParser()\n\tconst messages: JSONRPCMessage[] = []\n\ttry {\n\t\tfor (;;) {\n\t\t\tconst { done, value } = await reader.read()\n\t\t\tif (done) break\n\t\t\tfor (const event of parser.parse(decoder.decode(value, { stream: true }))) {\n\t\t\t\t// JSON-parse the event's `data` (the JSON-RPC envelope the server wrote), then\n\t\t\t\t// narrow it — a malformed / non-message payload is dropped, never thrown (§14).\n\t\t\t\tconst message = decodeEvent(event.data)\n\t\t\t\tif (message !== undefined) messages.push(message)\n\t\t\t}\n\t\t}\n\t} finally {\n\t\treader.releaseLock()\n\t}\n\treturn messages\n}\n\n/**\n * Decode one SSE event's `data` string into a {@link JSONRPCMessage}, or `undefined`\n * when it is not one — the per-event step {@link readEventStream} folds over.\n *\n * @remarks\n * `JSON.parse`s the `data` (the server serializes the JSON-RPC envelope as the event's\n * `data`) inside a try/catch and narrows the parsed value with `parseJSONRPCMessage`.\n * Total (§14): malformed JSON or a non-message value yields `undefined`, never throws.\n *\n * @param data - One SSE event's `data` payload\n * @returns The decoded {@link JSONRPCMessage}, or `undefined`\n */\nexport function decodeEvent(data: string): JSONRPCMessage | undefined {\n\ttry {\n\t\treturn parseJSONRPCMessage(JSON.parse(data))\n\t} catch {\n\t\treturn undefined\n\t}\n}\n\n/**\n * Read the path (without the query string) of a raw `node:http` protocol-upgrade request —\n * the `createWebSocketServer` upgrade-path match.\n *\n * @remarks\n * A `node:http` {@link import('node:http').IncomingMessage}'s `url` is the request TARGET\n * (`'/mcp?x=1'`), narrowed with `isString` (§14, never `as`) and defaulting to `'/'` for an\n * absent target; it is parsed against a dummy base (only the pathname matters for the upgrade\n * decision) and the `pathname` returned. The upgrade handler compares this against its\n * configured `path` to decide whether to claim the socket. Total — never throws on an\n * adversarial / absent target.\n *\n * @param request - The raw upgrade {@link import('node:http').IncomingMessage}\n * @returns The request's path (the `pathname`, no query), or `'/'` when the target is absent\n */\nexport function upgradeRequestPath(request: IncomingMessage): string {\n\tconst target = isString(request.url) ? request.url : '/'\n\treturn new URL(target, 'http://localhost').pathname\n}\n\n/**\n * Fold one more chunk of raw stdio bytes into a newline-framed buffer — the shared\n * line-framing step both stdio transports (client and server) read their inbound\n * newline-delimited JSON-RPC messages through.\n *\n * @remarks\n * Concatenates `buffer` (the carried-forward partial line from the previous call)\n * with `chunk`, splits on `'\\n'`, and returns every COMPLETE line (a `'\\r'` trailing\n * a line, from a CRLF-framed peer, is trimmed) plus the final, possibly-empty\n * fragment as the new `remainder` — the caller threads it back in as the next call's\n * `buffer`. A chunk containing no `'\\n'` yields no lines and the whole (buffer +\n * chunk) as `remainder`. Pure — no I/O, no instance state.\n *\n * @param buffer - The partial line carried forward from the previous chunk (`''` initially)\n * @param chunk - The newly-read raw bytes (already decoded to a string)\n * @returns The complete `lines` extracted (in order) and the trailing `remainder`\n */\nexport function extractLines(buffer: string, chunk: string): LineExtraction {\n\tconst combined = buffer + chunk\n\tconst parts = combined.split('\\n')\n\tconst remainder = parts[parts.length - 1] ?? ''\n\tconst lines = parts.slice(0, -1).map((line) => (line.endsWith('\\r') ? line.slice(0, -1) : line))\n\treturn { lines, remainder }\n}\n\n/**\n * Decode and deliver each complete newline-framed line onto a {@link\n * ClientTransportEventMap} emitter — the shared per-chunk dispatch step both stdio\n * transports (client and server) run their {@link extractLines} output through.\n *\n * @remarks\n * A blank line is skipped (a stray trailing newline). Every other line is decoded\n * with {@link decodeEvent} (`JSON.parse` + `parseJSONRPCMessage`, guarded); a\n * well-formed {@link JSONRPCMessage} emits `message`, a malformed / non-message line\n * emits `error` (§14 — total, never throws). Pure w.r.t. its own state — the emit is\n * the caller-owned side effect.\n *\n * @param emitter - The transport's {@link EmitterInterface} to emit `message` / `error` onto\n * @param lines - The complete lines (from {@link extractLines}) to decode and deliver\n */\nexport function dispatchLines(\n\temitter: EmitterInterface<ClientTransportEventMap>,\n\tlines: readonly string[],\n): void {\n\tfor (const line of lines) {\n\t\tif (line.length === 0) continue\n\t\tconst message = decodeEvent(line)\n\t\tif (message === undefined) {\n\t\t\temitter.emit('error', new Error('non-JSON-RPC stdio line'))\n\t\t\tcontinue\n\t\t}\n\t\temitter.emit('message', message)\n\t}\n}\n","import type { ClientTransportEventMap, ClientTransportInterface, JSONRPCMessage } from '@src/core'\nimport type { EmitterInterface } from '@orkestrel/emitter'\nimport type { HTTPClientTransportOptions } from '../types.js'\nimport { parseJSONRPCMessage } from '@src/core'\nimport { Emitter } from '@orkestrel/emitter'\nimport { MCP_SESSION_HEADER } from '../constants.js'\nimport { readEventStream } from '../helpers.js'\n\n/**\n * The HTTP CLIENT transport for the Model Context Protocol — a\n * {@link ClientTransportInterface} that drives a REMOTE Streamable-HTTP MCP server over\n * `fetch`, the egress mirror of the server's `createMCPRoutes`.\n *\n * @remarks\n * - **Request/response over `fetch`.** `send(message)` POSTs the JSON-serialized\n * message (or batch) to `options.url` with `content-type: application/json` and an\n * `Accept` of BOTH `application/json` and `text/event-stream` (so the server may\n * answer with either framing) — plus any `options.headers` (e.g. an `Authorization`\n * bearer). It then decodes the reply and emits each decoded {@link JSONRPCMessage} on\n * the `message` event the {@link import('@src/core').MCPClientInterface} subscribes\n * to.\n * - **Both reply framings.** A `200` with an `application/json` body is parsed with\n * `parseJSONRPCMessage`; a `200` with a `text/event-stream` body is decoded via the\n * `@orkestrel/sse` {@link import('@orkestrel/sse').SSEParserInterface} ({@link\n * readEventStream}) — the inverse of the server's `openStream` seam, so the wire\n * round-trips. A `202`\n * Accepted (a notification) carries no body and emits nothing.\n * - **Session echo.** `start()` / `close()` are no-ops (a request/response transport\n * holds no long-lived connection). The `mcp-session-id` response header, when a\n * STATEFUL server sends one (on `initialize`), is captured into `session` and then\n * ECHOED as the `mcp-session-id` request header on every SUBSEQUENT request — so an\n * `MCPClient` passes a stateful server's session validation. Before initialize returns\n * an id, `session` is `undefined` and no header is sent (safe against a stateless\n * server, which neither sends nor expects one).\n * - **Total at the boundary (§14).** Every reply is narrowed (`parseJSONRPCMessage`,\n * the SSE decoder) — a non-message reply is dropped, never asserted; a `fetch` /\n * decode failure surfaces on the `error` event rather than escaping `send`.\n * - **Observable (§13).** Owns the `emitter` ({@link ClientTransportEventMap}); fires\n * `message` per decoded reply, `error` on a fault, and `close` on `close()`.\n *\n * @example\n * ```ts\n * const transport = new HTTPClientTransport({ url: 'http://localhost:3000/mcp' })\n * const client = new MCPClient({ transport })\n * await client.connect()\n * ```\n */\nexport class HTTPClientTransport implements ClientTransportInterface {\n\treadonly #emitter: Emitter<ClientTransportEventMap>\n\treadonly #url: string\n\treadonly #headers: Readonly<Record<string, string>>\n\treadonly #fetch: typeof fetch\n\treadonly #timeout: number | undefined\n\t#session: string | undefined = undefined\n\n\tconstructor(options: HTTPClientTransportOptions) {\n\t\tthis.#emitter = new Emitter<ClientTransportEventMap>()\n\t\tthis.#url = options.url\n\t\tthis.#headers = options.headers ?? {}\n\t\tthis.#fetch = options.fetch ?? globalThis.fetch\n\t\tthis.#timeout = options.timeout\n\t}\n\n\tget emitter(): EmitterInterface<ClientTransportEventMap> {\n\t\treturn this.#emitter\n\t}\n\n\tget session(): string | undefined {\n\t\treturn this.#session\n\t}\n\n\tasync start(): Promise<void> {\n\t\t// A request/response transport opens no long-lived connection — `send` issues each\n\t\t// `fetch` on demand. Nothing to arm.\n\t}\n\n\tasync send(message: JSONRPCMessage | readonly JSONRPCMessage[]): Promise<void> {\n\t\tlet response: Response\n\t\ttry {\n\t\t\tresponse = await this.#fetch(this.#url, {\n\t\t\t\tmethod: 'POST',\n\t\t\t\theaders: {\n\t\t\t\t\t'content-type': 'application/json',\n\t\t\t\t\taccept: 'application/json, text/event-stream',\n\t\t\t\t\t// Echo a captured session id so a STATEFUL server validates the request; before\n\t\t\t\t\t// `initialize` returns one `#session` is undefined → no header (safe for a\n\t\t\t\t\t// stateless server). A caller `headers` key still wins (merged last).\n\t\t\t\t\t...(this.#session === undefined ? {} : { [MCP_SESSION_HEADER]: this.#session }),\n\t\t\t\t\t...this.#headers,\n\t\t\t\t},\n\t\t\t\tbody: JSON.stringify(message),\n\t\t\t\t...(this.#timeout === undefined ? {} : { signal: AbortSignal.timeout(this.#timeout) }),\n\t\t\t})\n\t\t} catch (error) {\n\t\t\t// A network-level failure (connection refused, DNS) — surface it for observation;\n\t\t\t// the client's per-request deadline still rejects the pending request.\n\t\t\tthis.#emitter.emit('error', error)\n\t\t\treturn\n\t\t}\n\t\t// Capture a server-assigned session id (a stateless server sends none) so it is echoed\n\t\t// on subsequent requests; a missing header leaves `session` unchanged.\n\t\tconst session = response.headers.get(MCP_SESSION_HEADER)\n\t\tif (session !== null) this.#session = session\n\t\tawait this.#deliver(response)\n\t}\n\n\tasync close(): Promise<void> {\n\t\tthis.#emitter.emit('close')\n\t}\n\n\t// Decode a reply and emit each carried message. A 202 (notification accepted) has no\n\t// body — emit nothing. An `application/json` body is one envelope; a `text/event-stream`\n\t// body is decoded via the core SSEParser (one or more `data:` events). A decode failure\n\t// surfaces on `error` rather than escaping.\n\tasync #deliver(response: Response): Promise<void> {\n\t\tif (response.status === 202) return\n\t\tconst type = response.headers.get('content-type') ?? ''\n\t\ttry {\n\t\t\tif (type.includes('text/event-stream')) {\n\t\t\t\tfor (const message of await readEventStream(response))\n\t\t\t\t\tthis.#emitter.emit('message', message)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (type.includes('application/json')) {\n\t\t\t\tconst message = parseJSONRPCMessage(await response.json())\n\t\t\t\tif (message !== undefined) this.#emitter.emit('message', message)\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tthis.#emitter.emit('error', error)\n\t\t}\n\t}\n}\n","import type { JSONRPCMessage } from '@src/core'\nimport type { StreamInterface } from '@orkestrel/server'\nimport type { EventStoreEntry, MCPSessionInterface, MCPSessionOptions } from './types.js'\nimport { DEFAULT_MCP_SESSION_CAPACITY, DEFAULT_MCP_SESSION_TTL } from './constants.js'\n\n/**\n * One MCP transport session — the per-session entity a {@link\n * import('./middlewares.js').createMCPSession} middleware owns, keyed by its `id`, carrying the\n * resumable server→client push channel with its bounded replay log FOLDED IN.\n *\n * @remarks\n * The single session entity (the old `SessionState` + `EventStore` merged): it holds the\n * session `id`, its OWN bounded, replayable log of pushed server→client messages (the\n * resumable GET-SSE channel — a private `#events` `Map` + a monotone `#counter`, with\n * `capacity` / `ttl` eviction, NOT a separate store), and the set of currently OPEN\n * server→client SSE streams (a resumable `GET {path}` registers via `attach`, unregisters via\n * `detach` on disconnect). Still a small entity (not a record), built minimal + extensible.\n *\n * - **`push` is the server-initiated primitive.** It APPENDS the message to the log (assigning\n * a monotone base36 event id) and FANS it out to every attached stream as one `id:`-tagged\n * SSE event (`stream.write({ id, data })`). A push with NO attached stream is still logged,\n * so a client that connects (or reconnects with a `Last-Event-ID`) LATER replays it from the\n * log. A `write` to a closed stream is a safe no-op (the {@link\n * `@orkestrel/server`'s `openStream` contract), so a just-disconnected stream that\n * has not yet been `detach`ed never throws. A replayed event and the live one carry the\n * IDENTICAL id (the log assigns it once).\n *\n * - **`replay(afterId)` is strictly-after.** It returns every retained log entry whose id sorts\n * AFTER `afterId` in append order — the missed-events list the `GET {path}` handler writes\n * before attaching the stream for live pushes. The decision for an UNKNOWN / already-evicted\n * `afterId` (the client's cursor fell off the back of the capacity window, or never existed):\n * replay NOTHING. Replaying the whole retained log would re-deliver events the client never\n * lost (its cursor is OLDER than everything retained); returning `[]` lets the handler then\n * stream only the fresh pushes that follow `attach` — the spec-sane resume.\n *\n * - **Bounded, append-ordered, plain `Map` (§21).** The log lives in ONE insertion-ordered\n * `Map<id, entry>` — insertion order IS append order IS id order, so `replay` and capacity\n * eviction both walk the map directly. NO database mirror — the log is process-local\n * transport mechanics, not durable state. `push` first drops every entry older than `ttl`\n * (lazy TTL — no background timer, the middleware's lazy-window idiom), appends, then evicts\n * the OLDEST entries until at most `capacity` remain; `replay` also runs the lazy TTL sweep\n * first, so a stale entry is never replayed.\n *\n * - **No transport coupling beyond the SSE seam.** It holds session state + the generic {@link\n * StreamInterface} handles `attach` was handed — never a raw socket, request, or response.\n * The middleware opens the stream (the spine seam) and registers it here; this class only\n * serializes a message onto the already-open streams.\n *\n * - **Injected clock.** `push` / `replay` accept an optional `now` (epoch ms), defaulting to\n * `Date.now()` — so a test drives TTL eviction with an elapsed clock rather than a real timer\n * (AGENTS §16).\n *\n * @example\n * ```ts\n * const session = new MCPSession(crypto.randomUUID())\n * session.attach(stream) // an open resumable GET-SSE stream\n * session.push({ jsonrpc: '2.0', method: 'notifications/message', params: { text: 'hi' } })\n * // → logged AND written to `stream` as an `id:`-tagged event; a reconnect replays it\n * ```\n */\nexport class MCPSession implements MCPSessionInterface {\n\treadonly #id: string\n\treadonly #events = new Map<string, EventStoreEntry>()\n\treadonly #streams = new Set<StreamInterface>()\n\treadonly #capacity: number\n\treadonly #ttl: number\n\t#counter = 0\n\n\tconstructor(id: string, options?: MCPSessionOptions) {\n\t\tthis.#id = id\n\t\tthis.#capacity = options?.capacity ?? DEFAULT_MCP_SESSION_CAPACITY\n\t\tthis.#ttl = options?.ttl ?? DEFAULT_MCP_SESSION_TTL\n\t}\n\n\tget id(): string {\n\t\treturn this.#id\n\t}\n\n\tattach(stream: StreamInterface): void {\n\t\tthis.#streams.add(stream)\n\t}\n\n\tdetach(stream: StreamInterface): void {\n\t\tthis.#streams.delete(stream)\n\t}\n\n\tpush(message: JSONRPCMessage, now = Date.now()): string {\n\t\t// Append to the log first (assigning the monotone event id), then fan the SAME id out to\n\t\t// every open stream — so a replayed event and a live one carry the identical id.\n\t\tconst id = this.#append(message, now)\n\t\tconst data = JSON.stringify(message)\n\t\tfor (const stream of this.#streams) stream.write({ id, data })\n\t\treturn id\n\t}\n\n\treplay(afterId: string, now = Date.now()): readonly EventStoreEntry[] {\n\t\tthis.#evict(now)\n\t\tconst out: EventStoreEntry[] = []\n\t\tlet found = false\n\t\tfor (const entry of this.#events.values()) {\n\t\t\t// Collect every entry STRICTLY AFTER `afterId`, in append order.\n\t\t\tif (found) out.push(entry)\n\t\t\telse if (entry.id === afterId) found = true\n\t\t}\n\t\t// An unknown / evicted `afterId` was never matched → `found` stays false → replay nothing\n\t\t// (the documented spec-sane choice; never re-deliver un-lost events).\n\t\treturn found ? out : []\n\t}\n\n\t// Append a message to the bounded replay log under a fresh monotone id, evicting stale +\n\t// over-capacity entries — the folded EventStore.append, now private to the session.\n\t#append(message: JSONRPCMessage, now: number): string {\n\t\t// Lazy TTL sweep BEFORE appending so an idle log shrinks as it is written.\n\t\tthis.#evict(now)\n\t\tthis.#counter += 1\n\t\tconst id = this.#counter.toString(36)\n\t\tthis.#events.set(id, { id, message, timestamp: now })\n\t\t// Capacity bound: drop the OLDEST entries (front of the insertion-ordered map) until at\n\t\t// most `capacity` remain — so the log is the most-recent `capacity` pushes.\n\t\twhile (this.#events.size > this.#capacity) {\n\t\t\tconst oldest = this.#events.keys().next().value\n\t\t\tif (oldest === undefined) break\n\t\t\tthis.#events.delete(oldest)\n\t\t}\n\t\treturn id\n\t}\n\n\t// Drop every entry older than the TTL. Entries are append-ordered (oldest first) and the\n\t// timestamp is monotone with insertion, so the stale run is a PREFIX — stop at the first live\n\t// entry. A non-positive ttl is treated as no expiry (nothing ever ages out by time).\n\t#evict(now: number): void {\n\t\tif (this.#ttl <= 0) return\n\t\tconst cutoff = now - this.#ttl\n\t\tfor (const [id, entry] of this.#events) {\n\t\t\tif (entry.timestamp <= cutoff) this.#events.delete(id)\n\t\t\telse break\n\t\t}\n\t}\n}\n","import type { ClientTransportEventMap, ClientTransportInterface, JSONRPCMessage } from '@src/core'\nimport type { EmitterInterface } from '@orkestrel/emitter'\nimport type { NodeWebSocketInterface } from '@orkestrel/websocket'\nimport { parseJSONRPCMessage } from '@src/core'\nimport { Emitter } from '@orkestrel/emitter'\n\n/**\n * The per-connection JSON-RPC-over-WebSocket SERVER bridge — wraps a\n * {@link NodeWebSocketInterface} (the RFC 6455 wire wrapper) as a\n * {@link ClientTransportInterface}, the bidirectional JSON-RPC message channel\n * `createWebSocketServer` pumps `mcp.dispatch` over and the egress mirror's\n * {@link import('./WebSocketClientTransport.js').WebSocketClientTransport} reuses.\n *\n * @remarks\n * - **Reuses `ClientTransportInterface` (§21).** It IS the same generic carrier the HTTP\n * client transport implements — `emitter` (`message` / `close` / `error`), `start`,\n * `send`, `close` — so the WebSocket server and client both speak ONE transport contract,\n * no near-duplicate sibling interface. `session` is `undefined` (the stateless v1; a\n * session id is the deferred sessions tier). The name keeps the role explicit even though\n * the shape is shared.\n * - **Inbound (`message`).** `start()` subscribes to the socket's `message` event; each text\n * frame is `JSON.parse`d inside a try/catch and narrowed with `parseJSONRPCMessage` — a\n * well-formed {@link JSONRPCMessage} is re-emitted on this transport's `message` event (the\n * parsed envelope the {@link import('@src/core').MCPServerInterface} pump dispatches), while\n * a non-JSON or non-message frame is surfaced on `error` and DROPPED, never thrown (§14). It\n * also bridges the socket's `close` → this transport's `close`, and the socket's `error`.\n * - **Outbound (`send`).** `send(message | messages)` writes ONE text frame per message\n * (`nodeWs.send(JSON.stringify(...))`); the underlying wrapper no-ops a write on a\n * non-open socket, so a closed connection drops silently rather than throwing.\n * - **`close()`** closes the underlying socket (the RFC 6455 close handshake) and fires the\n * transport's `close` event (idempotent — a second `close`, or a socket-driven close, emits\n * once).\n * - **Observable (§13).** Owns the `emitter` ({@link ClientTransportEventMap}); the emitter\n * isolates a listener throw (a buggy observer never corrupts the bridge). `error` is a\n * DOMAIN event (a transport-level fault), distinct from the emitter's listener-error channel.\n */\nexport class WebSocketServerTransport implements ClientTransportInterface {\n\treadonly #emitter: Emitter<ClientTransportEventMap>\n\treadonly #socket: NodeWebSocketInterface\n\t#started = false\n\t#closed = false\n\n\tconstructor(socket: NodeWebSocketInterface) {\n\t\tthis.#emitter = new Emitter<ClientTransportEventMap>()\n\t\tthis.#socket = socket\n\t}\n\n\tget emitter(): EmitterInterface<ClientTransportEventMap> {\n\t\treturn this.#emitter\n\t}\n\n\tget session(): string | undefined {\n\t\t// The stateless v1 holds no session — a server-assigned id is the deferred tier.\n\t\treturn undefined\n\t}\n\n\tasync start(): Promise<void> {\n\t\t// Arm the socket subscriptions once: a text frame becomes a `message`, the socket's\n\t\t// close / error bridge to this transport's events. Idempotent — a second `start` is a\n\t\t// no-op (the single MCPServer pump subscribes once).\n\t\tif (this.#started || this.#closed) return\n\t\tthis.#started = true\n\t\tthis.#socket.emitter.on('message', (text) => this.#receive(text))\n\t\tthis.#socket.emitter.on('close', () => this.#onClose())\n\t\tthis.#socket.emitter.on('error', (error) => this.#emitter.emit('error', error))\n\t}\n\n\tasync send(message: JSONRPCMessage | readonly JSONRPCMessage[]): Promise<void> {\n\t\t// One text frame per message (a batch is unrolled). The wrapper drops a write on a\n\t\t// non-open socket, so a closed connection is a silent no-op rather than a throw.\n\t\tconst messages = Array.isArray(message) ? message : [message]\n\t\tfor (const one of messages) this.#socket.send(JSON.stringify(one))\n\t}\n\n\tasync close(): Promise<void> {\n\t\tif (this.#closed) return\n\t\tthis.#closed = true\n\t\tthis.#socket.close()\n\t\tthis.#emitter.emit('close')\n\t}\n\n\t// Decode one inbound text frame: `JSON.parse` → `parseJSONRPCMessage`. A well-formed\n\t// message re-emits on `message`; a malformed / non-message frame surfaces on `error` and\n\t// is dropped (§14 — the bridge never throws on adversarial wire input).\n\t#receive(text: string): void {\n\t\tlet parsed: unknown\n\t\ttry {\n\t\t\tparsed = JSON.parse(text)\n\t\t} catch (error) {\n\t\t\tthis.#emitter.emit('error', error)\n\t\t\treturn\n\t\t}\n\t\tconst message = parseJSONRPCMessage(parsed)\n\t\tif (message === undefined) {\n\t\t\tthis.#emitter.emit('error', new Error('non-JSON-RPC WebSocket frame'))\n\t\t\treturn\n\t\t}\n\t\tthis.#emitter.emit('message', message)\n\t}\n\n\t// The socket closed (peer close frame, transport teardown) — fire this transport's\n\t// `close` once. A `close()` call already flipped `#closed`, so a socket-driven close\n\t// after an explicit one does not double-emit.\n\t#onClose(): void {\n\t\tif (this.#closed) return\n\t\tthis.#closed = true\n\t\tthis.#emitter.emit('close')\n\t}\n}\n","import type { ClientTransportEventMap, ClientTransportInterface, JSONRPCMessage } from '@src/core'\nimport type { EmitterInterface } from '@orkestrel/emitter'\nimport type { NodeWebSocketInterface } from '@orkestrel/websocket'\nimport type { WebSocketClientTransportOptions } from '../types.js'\nimport type { IncomingMessage } from 'node:http'\nimport type { Duplex } from 'node:stream'\nimport { randomBytes } from 'node:crypto'\nimport { request as httpRequest } from 'node:http'\nimport { request as httpsRequest } from 'node:https'\nimport { parseJSONRPCMessage } from '@src/core'\nimport { isString } from '@orkestrel/contract'\nimport { Emitter } from '@orkestrel/emitter'\nimport {\n\tcomputeWebSocketAccept,\n\tcreateNodeWebSocket,\n\tWEBSOCKET_VERSION,\n} from '@orkestrel/websocket'\nimport { MCP_WEBSOCKET_SUBPROTOCOL } from '../constants.js'\n\n/**\n * The WebSocket CLIENT transport for the Model Context Protocol — a\n * {@link ClientTransportInterface} that drives a REMOTE MCP server over a WebSocket, the\n * egress mirror of {@link import('./factories.js').createWebSocketServer} and the WebSocket\n * sibling of {@link import('./HTTPClientTransport.js').HTTPClientTransport}.\n *\n * @remarks\n * - **Persistent bidirectional channel (unlike the HTTP transport).** `start()` performs the\n * RFC 6455 client handshake: it opens a `node:http`(`s`) `GET` carrying `Connection: Upgrade`\n * / `Upgrade: websocket` / a random `Sec-WebSocket-Key` / `Sec-WebSocket-Version: 13` /\n * `Sec-WebSocket-Protocol: mcp` (plus any `options.headers`), awaits the client `'upgrade'`\n * event, and VALIDATES `Sec-WebSocket-Accept === computeWebSocketAccept(key)` (the D2 helper)\n * — a mismatch (or a non-`101` response, or a request error) REJECTS `start()` and the socket\n * is destroyed. On success it wraps the raw upgraded socket in `createNodeWebSocket({ socket,\n * head })` (CLIENT mode — no key → frames are MASKED per §5.3) and bridges its `message`.\n * - **Inbound (`message`).** Each decoded text frame is `JSON.parse`d (guarded) and narrowed\n * with `parseJSONRPCMessage` — a {@link JSONRPCMessage} re-emits on this transport's `message`\n * event (the reply the {@link import('@src/core').MCPClientInterface} correlates by `id`); a\n * non-JSON / non-message frame surfaces on `error` and is dropped (§14). The socket's `close`\n * / `error` bridge to this transport's events.\n * - **Outbound (`send`).** `send(message | messages)` writes ONE masked text frame per message.\n * - **`close()`** closes the underlying socket and fires `close` (idempotent).\n * - **URL scheme.** `options.url` accepts a `ws://` / `wss://` URL or an `http://` / `https://`\n * one; a `ws(s)` scheme is converted to `http(s)` for the underlying upgrade request (`wss`\n * → TLS via `node:https`). Either reaches the same endpoint.\n * - **Observable (§13).** Owns the `emitter` ({@link ClientTransportEventMap}); every emit\n * the emitter isolates a listener throw (a buggy observer never corrupts the transport);\n * `error` is a DOMAIN event (a transport-level fault).\n *\n * @example\n * ```ts\n * const transport = new WebSocketClientTransport({ url: 'ws://localhost:3000/mcp' })\n * const client = new MCPClient({ transport })\n * await client.connect() // start() handshakes, then the MCP initialize runs over WS frames\n * ```\n */\nexport class WebSocketClientTransport implements ClientTransportInterface {\n\treadonly #emitter: Emitter<ClientTransportEventMap>\n\treadonly #url: string\n\treadonly #headers: Readonly<Record<string, string>>\n\t#socket: NodeWebSocketInterface | undefined = undefined\n\t#closed = false\n\n\tconstructor(options: WebSocketClientTransportOptions) {\n\t\tthis.#emitter = new Emitter<ClientTransportEventMap>()\n\t\tthis.#url = options.url\n\t\tthis.#headers = options.headers ?? {}\n\t}\n\n\tget emitter(): EmitterInterface<ClientTransportEventMap> {\n\t\treturn this.#emitter\n\t}\n\n\tget session(): string | undefined {\n\t\treturn undefined\n\t}\n\n\tasync start(): Promise<void> {\n\t\t// Already connected — a second `connect()` short-circuits in the client, but guard here\n\t\t// too (idempotent open).\n\t\tif (this.#socket !== undefined) return\n\t\tthis.#closed = false\n\t\tconst url = this.#httpURL()\n\t\tconst key = randomBytes(16).toString('base64')\n\t\tconst secure = url.protocol === 'https:'\n\t\tconst send = secure ? httpsRequest : httpRequest\n\n\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\tlet settled = false\n\t\t\tconst fail = (error: Error): void => {\n\t\t\t\tif (settled) return\n\t\t\t\tsettled = true\n\t\t\t\treject(error)\n\t\t\t}\n\t\t\tconst request = send({\n\t\t\t\thostname: url.hostname,\n\t\t\t\tport: url.port.length > 0 ? Number(url.port) : secure ? 443 : 80,\n\t\t\t\tpath: `${url.pathname}${url.search}`,\n\t\t\t\theaders: {\n\t\t\t\t\tConnection: 'Upgrade',\n\t\t\t\t\tUpgrade: 'websocket',\n\t\t\t\t\t'Sec-WebSocket-Key': key,\n\t\t\t\t\t'Sec-WebSocket-Version': WEBSOCKET_VERSION,\n\t\t\t\t\t'Sec-WebSocket-Protocol': MCP_WEBSOCKET_SUBPROTOCOL,\n\t\t\t\t\t...this.#headers,\n\t\t\t\t},\n\t\t\t})\n\n\t\t\t// The server accepted the upgrade: validate the handshake accept, then wrap the\n\t\t\t// raw socket in a CLIENT-mode NodeWebSocket (masks its frames).\n\t\t\trequest.on('upgrade', (response: IncomingMessage, socket: Duplex, head: Buffer) => {\n\t\t\t\tconst accept = response.headers['sec-websocket-accept']\n\t\t\t\tif (!isString(accept) || accept !== computeWebSocketAccept(key)) {\n\t\t\t\t\tsocket.destroy()\n\t\t\t\t\tfail(new Error('WebSocket handshake failed: Sec-WebSocket-Accept mismatch'))\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst ws = createNodeWebSocket({ socket, head })\n\t\t\t\tthis.#socket = ws\n\t\t\t\tthis.#bind(ws)\n\t\t\t\tif (!settled) {\n\t\t\t\t\tsettled = true\n\t\t\t\t\tresolve()\n\t\t\t\t}\n\t\t\t})\n\n\t\t\t// A plain (non-101) response means the server declined the upgrade.\n\t\t\trequest.on('response', (response) => {\n\t\t\t\tresponse.resume()\n\t\t\t\tfail(new Error(`WebSocket upgrade declined with status ${response.statusCode ?? 0}`))\n\t\t\t})\n\t\t\t// A connection-level failure (refused, DNS, reset).\n\t\t\trequest.on('error', (error) =>\n\t\t\t\tfail(error instanceof Error ? error : new Error(String(error))),\n\t\t\t)\n\t\t\trequest.end()\n\t\t})\n\t}\n\n\tasync send(message: JSONRPCMessage | readonly JSONRPCMessage[]): Promise<void> {\n\t\tconst socket = this.#socket\n\t\tif (socket === undefined) throw new Error('WebSocket transport is not connected')\n\t\tconst messages = Array.isArray(message) ? message : [message]\n\t\tfor (const one of messages) socket.send(JSON.stringify(one))\n\t}\n\n\tasync close(): Promise<void> {\n\t\tif (this.#closed) return\n\t\tthis.#closed = true\n\t\tconst socket = this.#socket\n\t\tthis.#socket = undefined\n\t\tif (socket !== undefined) socket.close()\n\t\tthis.#emitter.emit('close')\n\t}\n\n\t// Bridge the upgraded socket's events onto the transport: a text frame → `message`\n\t// (decoded + narrowed), the socket close → `close`, a socket fault → `error`.\n\t#bind(ws: NodeWebSocketInterface): void {\n\t\tws.emitter.on('message', (text) => this.#receive(text))\n\t\tws.emitter.on('close', () => this.#onClose())\n\t\tws.emitter.on('error', (error) => this.#emitter.emit('error', error))\n\t}\n\n\t// Decode one inbound text frame: `JSON.parse` → `parseJSONRPCMessage`. A well-formed\n\t// message re-emits on `message`; a malformed / non-message frame surfaces on `error` and\n\t// is dropped (§14 — never throws on adversarial wire input).\n\t#receive(text: string): void {\n\t\tlet parsed: unknown\n\t\ttry {\n\t\t\tparsed = JSON.parse(text)\n\t\t} catch (error) {\n\t\t\tthis.#emitter.emit('error', error)\n\t\t\treturn\n\t\t}\n\t\tconst message = parseJSONRPCMessage(parsed)\n\t\tif (message === undefined) {\n\t\t\tthis.#emitter.emit('error', new Error('non-JSON-RPC WebSocket frame'))\n\t\t\treturn\n\t\t}\n\t\tthis.#emitter.emit('message', message)\n\t}\n\n\t// The socket closed underneath us — fire `close` once (a `close()` call already flipped\n\t// `#closed`, so it does not double-emit).\n\t#onClose(): void {\n\t\tif (this.#closed) return\n\t\tthis.#closed = true\n\t\tthis.#socket = undefined\n\t\tthis.#emitter.emit('close')\n\t}\n\n\t// Normalize `options.url` to the `http(s)` URL the underlying upgrade request uses: a\n\t// `ws://` → `http://`, a `wss://` → `https://`; an `http(s)://` URL passes through. Any\n\t// other scheme throws (a clear boundary error, not a silent mis-dial).\n\t#httpURL(): URL {\n\t\tconst url = new URL(this.#url)\n\t\tif (url.protocol === 'ws:') url.protocol = 'http:'\n\t\telse if (url.protocol === 'wss:') url.protocol = 'https:'\n\t\telse if (url.protocol !== 'http:' && url.protocol !== 'https:') {\n\t\t\tthrow new Error(`unsupported WebSocket URL scheme '${url.protocol}'`)\n\t\t}\n\t\treturn url\n\t}\n}\n","import type { ClientTransportEventMap, ClientTransportInterface, JSONRPCMessage } from '@src/core'\nimport type { EmitterInterface } from '@orkestrel/emitter'\nimport type { StdioClientTransportOptions } from '../types.js'\nimport type { ChildProcessByStdio } from 'node:child_process'\nimport type { Readable, Writable } from 'node:stream'\nimport { spawn } from 'node:child_process'\nimport { Emitter } from '@orkestrel/emitter'\nimport { dispatchLines, extractLines } from '../helpers.js'\n\n/**\n * The stdio CLIENT transport for the Model Context Protocol — a\n * {@link ClientTransportInterface} that drives a CHILD PROCESS MCP server over\n * newline-delimited JSON-RPC on `stdin`/`stdout`, the stdio sibling of {@link\n * import('./HTTPClientTransport.js').HTTPClientTransport} and {@link\n * import('./WebSocketClientTransport.js').WebSocketClientTransport}.\n *\n * @remarks\n * - **Spawns the server.** `start()` runs `node:child_process`'s `spawn(options.command,\n * options.args, { env: options.env, stdio: ['pipe', 'pipe', 'inherit'] })` — the\n * child's `stdin`/`stdout` are piped for the JSON-RPC channel, its `stderr` inherits\n * the parent's (diagnostics pass through, never parsed as protocol).\n * - **Inbound (`message`).** Each `stdout` chunk is folded through the shared\n * {@link extractLines} line-framing helper (buffering a partial trailing line\n * across reads); every complete line is decoded and delivered via the shared\n * {@link dispatchLines} helper — a well-formed {@link JSONRPCMessage} emits\n * `message`, a malformed line emits `error` (§14, never throws). The child's\n * `close` bridges to this transport's `close`.\n * - **Outbound (`send`).** `send(message | messages)` writes ONE newline-terminated\n * `JSON.stringify`d line per message to the child's `stdin`.\n * - **`close()`** kills the child process and fires `close` (idempotent).\n * - **Observable (§13).** Owns the `emitter` ({@link ClientTransportEventMap}); the\n * emitter isolates a listener throw; `error` is a DOMAIN event (a transport-level\n * fault), distinct from the emitter's own listener-error channel.\n *\n * @example\n * ```ts\n * const transport = new StdioClientTransport({ command: 'node', args: ['./server.js'] })\n * const client = new MCPClient({ transport })\n * await client.connect() // start() spawns the child, then the MCP initialize runs over stdio\n * ```\n */\nexport class StdioClientTransport implements ClientTransportInterface {\n\treadonly #emitter: Emitter<ClientTransportEventMap>\n\treadonly #command: string\n\treadonly #args: readonly string[]\n\treadonly #env: Readonly<Record<string, string>> | undefined\n\t#child: ChildProcessByStdio<Writable, Readable, null> | undefined = undefined\n\t#buffer = ''\n\t#closed = false\n\n\tconstructor(options: StdioClientTransportOptions) {\n\t\tthis.#emitter = new Emitter<ClientTransportEventMap>()\n\t\tthis.#command = options.command\n\t\tthis.#args = options.args ?? []\n\t\tthis.#env = options.env\n\t}\n\n\tget emitter(): EmitterInterface<ClientTransportEventMap> {\n\t\treturn this.#emitter\n\t}\n\n\tget session(): string | undefined {\n\t\treturn undefined\n\t}\n\n\tasync start(): Promise<void> {\n\t\t// Already spawned — a second `start()` (e.g. via `connect()`) short-circuits (idempotent).\n\t\tif (this.#child !== undefined) return\n\t\tthis.#closed = false\n\t\tthis.#buffer = ''\n\t\tconst child = spawn(this.#command, [...this.#args], {\n\t\t\tenv: this.#env,\n\t\t\tstdio: ['pipe', 'pipe', 'inherit'],\n\t\t})\n\t\tthis.#child = child\n\t\tchild.stdout.on('data', (chunk: Buffer | string) => this.#receive(chunk.toString()))\n\t\tchild.on('close', () => this.#onClose())\n\t\tchild.on('error', (error) => this.#emitter.emit('error', error))\n\t}\n\n\tasync send(message: JSONRPCMessage | readonly JSONRPCMessage[]): Promise<void> {\n\t\tconst child = this.#child\n\t\tif (child === undefined) throw new Error('stdio transport is not connected')\n\t\tconst messages = Array.isArray(message) ? message : [message]\n\t\tfor (const one of messages) child.stdin.write(`${JSON.stringify(one)}\\n`)\n\t}\n\n\tasync close(): Promise<void> {\n\t\tif (this.#closed) return\n\t\tthis.#closed = true\n\t\tconst child = this.#child\n\t\tthis.#child = undefined\n\t\tif (child !== undefined) child.kill()\n\t\tthis.#emitter.emit('close')\n\t}\n\n\t// Buffer a raw stdout chunk through the shared line-framing helper, then decode + deliver\n\t// every complete line onto this transport's emitter (a partial trailing line carries\n\t// forward to the next chunk).\n\t#receive(chunk: string): void {\n\t\tconst { lines, remainder } = extractLines(this.#buffer, chunk)\n\t\tthis.#buffer = remainder\n\t\tdispatchLines(this.#emitter, lines)\n\t}\n\n\t// The child process closed — fire this transport's `close` once. A `close()` call already\n\t// flipped `#closed`, so a child-driven close after an explicit one does not double-emit.\n\t#onClose(): void {\n\t\tif (this.#closed) return\n\t\tthis.#closed = true\n\t\tthis.#child = undefined\n\t\tthis.#emitter.emit('close')\n\t}\n}\n","import type { ClientTransportEventMap, ClientTransportInterface, JSONRPCMessage } from '@src/core'\nimport type { EmitterInterface } from '@orkestrel/emitter'\nimport { Emitter } from '@orkestrel/emitter'\nimport { dispatchLines, extractLines } from '../helpers.js'\n\n/**\n * The stdio SERVER transport for the Model Context Protocol — wraps an injectable\n * readable/writable stream pair (`process.stdin`/`process.stdout` in production, a\n * test double in tests) as a {@link ClientTransportInterface}, the newline-delimited\n * JSON-RPC channel {@link import('../factories.js').createStdioServer} pumps\n * `mcp.dispatch` over, the stdio mirror of {@link\n * import('./WebSocketServerTransport.js').WebSocketServerTransport}.\n *\n * @remarks\n * - **Reuses `ClientTransportInterface` (§21).** The same generic carrier the HTTP\n * and WebSocket server transports implement — `emitter` (`message` / `close` /\n * `error`), `start`, `send`, `close`. `session` is `undefined` (the stateless v1).\n * - **Inbound (`message`).** `start()` subscribes to `input`'s `data` event; each\n * chunk is folded through the shared {@link extractLines} line-framing helper\n * (buffering a partial trailing line across reads), and every complete line is\n * decoded and delivered via the shared {@link dispatchLines} helper — a\n * well-formed {@link JSONRPCMessage} re-emits on `message`, a malformed line\n * emits `error` (§14, never throws). `input`'s `close` bridges to this\n * transport's `close`.\n * - **Outbound (`send`).** `send(message | messages)` writes ONE newline-terminated\n * `JSON.stringify`d line per message to `output`.\n * - **`close()`** fires this transport's `close` (idempotent) — the injected streams\n * are owned by the caller (typically `process.stdin`/`process.stdout`, which must\n * never be closed out from under the process) and are not torn down here.\n * - **Observable (§13).** Owns the `emitter` ({@link ClientTransportEventMap}); the\n * emitter isolates a listener throw; `error` is a DOMAIN event (a transport-level\n * fault), distinct from the emitter's own listener-error channel.\n */\nexport class StdioServerTransport implements ClientTransportInterface {\n\treadonly #emitter: Emitter<ClientTransportEventMap>\n\treadonly #input: NodeJS.ReadableStream\n\treadonly #output: NodeJS.WritableStream\n\t#buffer = ''\n\t#started = false\n\t#closed = false\n\n\tconstructor(input: NodeJS.ReadableStream, output: NodeJS.WritableStream) {\n\t\tthis.#emitter = new Emitter<ClientTransportEventMap>()\n\t\tthis.#input = input\n\t\tthis.#output = output\n\t}\n\n\tget emitter(): EmitterInterface<ClientTransportEventMap> {\n\t\treturn this.#emitter\n\t}\n\n\tget session(): string | undefined {\n\t\t// The stateless v1 holds no session — a server-assigned id is the deferred tier.\n\t\treturn undefined\n\t}\n\n\tasync start(): Promise<void> {\n\t\t// Arm the stream subscriptions once: an input chunk decodes to `message`, the input's\n\t\t// close bridges to this transport's `close`. Idempotent — a second `start` is a no-op\n\t\t// (the single MCPServer pump subscribes once).\n\t\tif (this.#started || this.#closed) return\n\t\tthis.#started = true\n\t\tthis.#input.on('data', (chunk: Buffer | string) => this.#receive(chunk.toString()))\n\t\tthis.#input.on('close', () => this.#onClose())\n\t\tthis.#input.on('error', (error) => this.#emitter.emit('error', error))\n\t}\n\n\tasync send(message: JSONRPCMessage | readonly JSONRPCMessage[]): Promise<void> {\n\t\tconst messages = Array.isArray(message) ? message : [message]\n\t\tfor (const one of messages) this.#output.write(`${JSON.stringify(one)}\\n`)\n\t}\n\n\tasync close(): Promise<void> {\n\t\tif (this.#closed) return\n\t\tthis.#closed = true\n\t\tthis.#emitter.emit('close')\n\t}\n\n\t// Buffer a raw input chunk through the shared line-framing helper, then decode + deliver\n\t// every complete line onto this transport's emitter (a partial trailing line carries\n\t// forward to the next chunk).\n\t#receive(chunk: string): void {\n\t\tconst { lines, remainder } = extractLines(this.#buffer, chunk)\n\t\tthis.#buffer = remainder\n\t\tdispatchLines(this.#emitter, lines)\n\t}\n\n\t// The input stream closed (EOF, peer teardown) — fire this transport's `close` once. A\n\t// `close()` call already flipped `#closed`, so a stream-driven close after an explicit one\n\t// does not double-emit.\n\t#onClose(): void {\n\t\tif (this.#closed) return\n\t\tthis.#closed = true\n\t\tthis.#emitter.emit('close')\n\t}\n}\n","import type { ClientTransportInterface, MCPServerInterface } from '@src/core'\nimport type { RouteInput } from '@orkestrel/router'\nimport type { UpgradeHandler } from '@orkestrel/server/server'\nimport type {\n\tHTTPClientTransportOptions,\n\tHTTPTransportOptions,\n\tStdioClientTransportOptions,\n\tStdioServerOptions,\n\tWebSocketClientTransportOptions,\n\tWebSocketServerOptions,\n} from './types.js'\nimport type { IncomingMessage } from 'node:http'\nimport type { Duplex } from 'node:stream'\nimport {\n\tisJSONRPCRequest,\n\tJSONRPC_INVALID_REQUEST,\n\tJSONRPC_PARSE_ERROR,\n\tjsonRPCError,\n\tparseJSONRPCMessage,\n} from '@src/core'\nimport { isString } from '@orkestrel/contract'\nimport { openStream } from '@orkestrel/server'\nimport { createNodeWebSocket, WEBSOCKET_VERSION } from '@orkestrel/websocket'\nimport { DEFAULT_MCP_PATH, MCP_WEBSOCKET_SUBPROTOCOL } from './constants.js'\nimport { acceptsEventStream, upgradeRequestPath } from './helpers.js'\nimport { HTTPClientTransport } from './transports/HTTPClientTransport.js'\nimport { StdioClientTransport } from './transports/StdioClientTransport.js'\nimport { StdioServerTransport } from './transports/StdioServerTransport.js'\nimport { WebSocketClientTransport } from './transports/WebSocketClientTransport.js'\nimport { WebSocketServerTransport } from './transports/WebSocketServerTransport.js'\n\n/**\n * Create the MCP Streamable-HTTP transport routes — mounts a transport-agnostic\n * {@link MCPServerInterface} (the `@src/core` dispatch core) on the fetch-standard router\n * spine, pumping each `POST` body through `mcp.dispatch`. Returns the {@link RouteInput}s to\n * hand to `router.add(...)`.\n *\n * @remarks\n * A SINGLE `POST {path}` route — `createMCPRoutes` is STATELESS. The handler reads its own\n * request body (its own JSON parse try/catch), so it works with or without a session\n * middleware mounted in front. It draws a sharp line between TRANSPORT-level and\n * DISPATCH-level outcomes:\n *\n * - A **transport** failure — a malformed JSON body, or a parsed value that is not a\n * JSON-RPC REQUEST — is an HTTP `400` carrying a JSON-RPC error BODY (`-32700` Parse\n * error / `-32600` Invalid Request, id `null`).\n * - A **dispatch** result — a success OR an IN-BAND JSON-RPC error from `mcp.dispatch`\n * (e.g. `-32601` method-not-found) — is an HTTP `200` carrying the JSON-RPC response\n * envelope (the error is in-band per JSON-RPC, NOT an HTTP error).\n * - A **notification** (a request with no `id`, which `dispatch` resolves to\n * `undefined`) is a `202 Accepted` with no body.\n *\n * When `streaming` is enabled (the default) and the client `Accept`s `text/event-stream`,\n * the `200` reply is framed as a Streamable-HTTP SSE response (one `data:` event carrying\n * the JSON-RPC envelope, then the stream ends) via `@orkestrel/server`'s generic\n * {@link import('@orkestrel/server').openStream} seam; otherwise it is a plain JSON body.\n *\n * **Sessions are a SEPARATE, plug-and-play middleware.** `createMCPRoutes` mints / reads no\n * session id. To make the transport STATEFUL, mount {@link\n * import('./middlewares.js').createMCPSession} IN FRONT — it owns the same `path`, mints +\n * validates the `mcp-session-id`, and serves the resumable `GET {path}` + `DELETE {path}`,\n * leaving this route to dispatch the validated `POST`.\n *\n * This is MECHANISM, not policy: compose auth / CORS / rate-limiting (and the session\n * middleware) IN FRONT as ordinary middleware — the transport route adds none.\n *\n * @typeParam TState - The consumer's opaque per-request state type\n * @param mcp - The transport-agnostic {@link MCPServerInterface} to expose over HTTP\n * @param options - Optional `path` (default {@link DEFAULT_MCP_PATH}) and `streaming`\n * (default `true`); see {@link HTTPTransportOptions}\n * @returns The {@link RouteInput}s to register with the router\n *\n * @example\n * ```ts\n * import { createMCPServer, createToolManager } from '@src/core'\n * import { createMCPRoutes } from '@src/server'\n *\n * const mcp = createMCPServer({ name: 'docs', version: '1.0.0', tools: createToolManager() })\n * const routes = createMCPRoutes(mcp) // POST /mcp dispatches JSON-RPC (JSON or SSE per Accept)\n * ```\n */\nexport function createMCPRoutes<TState = unknown>(\n\tmcp: MCPServerInterface,\n\toptions?: HTTPTransportOptions,\n): readonly RouteInput<string, TState>[] {\n\tconst path = options?.path ?? DEFAULT_MCP_PATH\n\tconst streaming = options?.streaming ?? true\n\tconst post: RouteInput<string, TState> = {\n\t\tmethod: 'POST',\n\t\tpath,\n\t\tname: 'mcp',\n\t\thandler: async (request) => {\n\t\t\tlet text: string\n\t\t\ttry {\n\t\t\t\ttext = await request.text()\n\t\t\t} catch {\n\t\t\t\t// A malformed JSON body is a TRANSPORT failure — HTTP 400 + a JSON-RPC -32700.\n\t\t\t\treturn Response.json(jsonRPCError(null, JSONRPC_PARSE_ERROR, 'Parse error'), {\n\t\t\t\t\tstatus: 400,\n\t\t\t\t})\n\t\t\t}\n\t\t\tlet parsed: unknown\n\t\t\ttry {\n\t\t\t\tparsed = JSON.parse(text)\n\t\t\t} catch {\n\t\t\t\treturn Response.json(jsonRPCError(null, JSONRPC_PARSE_ERROR, 'Parse error'), {\n\t\t\t\t\tstatus: 400,\n\t\t\t\t})\n\t\t\t}\n\t\t\tconst rpcRequest = parseJSONRPCMessage(parsed)\n\t\t\tif (rpcRequest === undefined || !('method' in rpcRequest)) {\n\t\t\t\t// Not a JSON-RPC request (a response, or any non-message) — HTTP 400 + -32600.\n\t\t\t\t// `'method' in rpcRequest` narrows the message union to `JSONRPCRequest` (no `as`).\n\t\t\t\treturn Response.json(jsonRPCError(null, JSONRPC_INVALID_REQUEST, 'Invalid Request'), {\n\t\t\t\t\tstatus: 400,\n\t\t\t\t})\n\t\t\t}\n\t\t\tconst response = await mcp.dispatch(rpcRequest)\n\t\t\tif (response === undefined) {\n\t\t\t\t// A notification (no `id`) yields no response — 202 Accepted, no body.\n\t\t\t\treturn new Response(null, { status: 202 })\n\t\t\t}\n\t\t\tif (streaming && acceptsEventStream(request)) {\n\t\t\t\t// Streamable-HTTP SSE response: one `data:` event with the JSON-RPC envelope, then end.\n\t\t\t\tconst s = openStream()\n\t\t\t\ts.write({ data: JSON.stringify(response) })\n\t\t\t\ts.end()\n\t\t\t\treturn s.response\n\t\t\t}\n\t\t\t// A dispatch result — success OR an in-band JSON-RPC error — is HTTP 200 + the envelope.\n\t\t\treturn Response.json(response)\n\t\t},\n\t}\n\treturn [post]\n}\n\n/**\n * Create the HTTP CLIENT transport for an {@link import('@src/core').MCPClientInterface}\n * — a {@link ClientTransportInterface} that drives a REMOTE Streamable-HTTP MCP server\n * over `fetch`. The egress mirror of {@link createMCPRoutes}.\n *\n * @remarks\n * Hand it to `createMCPClient({ transport })`: each JSON-RPC message the client sends is\n * `POST`ed to `options.url` with `content-type: application/json` and an `Accept` of\n * both `application/json` and `text/event-stream` (the server answers with EITHER — a\n * plain JSON envelope or a Streamable-HTTP SSE `data:` event, decoded via `@orkestrel/sse`),\n * and the reply is surfaced on the transport's `message` event for the client's id\n * correlation. Add `options.headers` (e.g. an `Authorization` bearer) to reach a guarded\n * server. `start` / `close` hold no connection; against a STATEFUL server it captures the\n * `mcp-session-id` from `initialize` and echoes it on later requests, so the same\n * `MCPClient` passes session validation (a stateless server sends none).\n *\n * @param options - `url` (the remote endpoint; REQUIRED), optional `headers` merged onto\n * every request, optional `fetch` (default `globalThis.fetch`), and optional `timeout`\n * (ms, applied via `AbortSignal.timeout`); see {@link HTTPClientTransportOptions}\n * @returns A working {@link ClientTransportInterface} over `fetch`\n *\n * @example\n * ```ts\n * import { createMCPClient } from '@src/core'\n * import { createHTTPClientTransport } from '@src/server'\n *\n * const client = createMCPClient({\n * \ttransport: createHTTPClientTransport({ url: 'http://localhost:3000/mcp' }),\n * })\n * await client.connect()\n * const tools = await client.tools()\n * ```\n */\nexport function createHTTPClientTransport(\n\toptions: HTTPClientTransportOptions,\n): ClientTransportInterface {\n\treturn new HTTPClientTransport(options)\n}\n\n/**\n * Create the MCP WebSocket transport INGRESS — an {@link UpgradeHandler} that exposes a\n * transport-agnostic {@link MCPServerInterface} over a WebSocket, the WebSocket mirror of\n * {@link createMCPRoutes}. Register it on the spine's upgrade seam.\n *\n * @remarks\n * It composes the lean RFC 6455 `@orkestrel/websocket` wrapper over `@orkestrel/server`'s\n * generic upgrade seam — the spine speaks no WebSocket, this handler does.\n *\n * - **Declines (returns `false`)** when the upgrade is not for it, so the spine fans the\n * socket to the next handler (or destroys an unclaimed one): the `Upgrade` header is not\n * `websocket`, the request path is not `options.path` (default {@link DEFAULT_MCP_PATH},\n * `'/mcp'`), the `Sec-WebSocket-Key` is absent, or the `Sec-WebSocket-Version` is not `13`.\n * A decline NEVER writes to the socket (it is not yet ours) — the spine owns the unclaimed\n * outcome.\n * - **Claims (returns `true`)** otherwise: it builds `createNodeWebSocket({ socket, key, head,\n * protocol })` (SERVER mode → writes the `101` handshake, echoing the `subprotocol`, default\n * {@link MCP_WEBSOCKET_SUBPROTOCOL} `'mcp'`, and sends UNMASKED frames), wraps it in a\n * {@link WebSocketServerTransport}, and PUMPS: each inbound {@link\n * import('@src/core').JSONRPCMessage} that is a REQUEST runs through `mcp.dispatch`, and a\n * defined response is written back as a frame — a NOTIFICATION (`dispatch` → `undefined`)\n * sends nothing. A non-request message (a stray response) is ignored. The dispatch is\n * guarded so a `dispatch` / `send` fault surfaces on the transport's `error` event rather\n * than escaping the (async) message listener.\n *\n * It is MECHANISM, not policy: compose an auth guard IN FRONT by registering an upgrade\n * handler BEFORE this one — that handler can claim (decline + destroy) an unauthenticated\n * upgrade so it never reaches this pump.\n *\n * @param mcp - The transport-agnostic {@link MCPServerInterface} to expose over WebSocket\n * @param options - Optional `path` (default {@link DEFAULT_MCP_PATH}) and `subprotocol`\n * (default {@link MCP_WEBSOCKET_SUBPROTOCOL}); see {@link WebSocketServerOptions}\n * @returns An {@link UpgradeHandler} to register with the spine's `upgrade` seam\n *\n * @example\n * ```ts\n * import { createMCPServer, createToolManager } from '@src/core'\n * import { createWebSocketServer } from '@src/server'\n *\n * const mcp = createMCPServer({ name: 'docs', version: '1.0.0', tools: createToolManager() })\n * server.upgrade(createWebSocketServer(mcp)) // an MCP client now connects over ws://…/mcp\n * ```\n */\nexport function createWebSocketServer(\n\tmcp: MCPServerInterface,\n\toptions?: WebSocketServerOptions,\n): UpgradeHandler {\n\tconst path = options?.path ?? DEFAULT_MCP_PATH\n\tconst subprotocol = options?.subprotocol ?? MCP_WEBSOCKET_SUBPROTOCOL\n\treturn (request: IncomingMessage, socket: Duplex, head: Buffer): boolean => {\n\t\t// DECLINE anything that is not our MCP WebSocket upgrade — the spine fans it onward or\n\t\t// destroys it. Never touch the socket on a decline (it is not ours yet).\n\t\tconst upgrade = request.headers['upgrade']\n\t\tif (!isString(upgrade) || upgrade.toLowerCase() !== 'websocket') return false\n\t\tif (upgradeRequestPath(request) !== path) return false\n\t\tconst key = request.headers['sec-websocket-key']\n\t\tif (!isString(key)) return false\n\t\tconst version = request.headers['sec-websocket-version']\n\t\tif (!isString(version) || version !== WEBSOCKET_VERSION) return false\n\n\t\t// CLAIM: the wrapper writes the `101` handshake (server mode) and the transport pumps\n\t\t// each request through `mcp.dispatch`, writing back a defined response (a notification\n\t\t// sends nothing). A dispatch / send fault surfaces on the transport `error` event.\n\t\tconst ws = createNodeWebSocket({ socket, key, head, protocol: subprotocol })\n\t\tconst transport = new WebSocketServerTransport(ws)\n\t\ttransport.emitter.on('message', (message) => {\n\t\t\tif (!isJSONRPCRequest(message)) return\n\t\t\tvoid (async () => {\n\t\t\t\ttry {\n\t\t\t\t\tconst response = await mcp.dispatch(message)\n\t\t\t\t\tif (response !== undefined) await transport.send(response)\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// Surface a dispatch / send fault on the transport's `error` event — but a\n\t\t\t\t\t// user `'error'` listener that itself throws would (Emitter.emit rethrows the\n\t\t\t\t\t// first listener throw) escape this `void` async listener as an UNHANDLED\n\t\t\t\t\t// rejection and, on Node ≥15, can terminate the process. Swallow that here:\n\t\t\t\t\t// a buggy observer must never crash the server (§13).\n\t\t\t\t\ttry {\n\t\t\t\t\t\ttransport.emitter.emit('error', error)\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// A throwing `error` listener is the caller's own bug — the end of the line.\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t})()\n\t\t})\n\t\tvoid transport.start()\n\t\treturn true\n\t}\n}\n\n/**\n * Create the WebSocket CLIENT transport for an {@link import('@src/core').MCPClientInterface}\n * — a {@link ClientTransportInterface} that drives a REMOTE MCP server over a WebSocket. The\n * egress mirror of {@link createWebSocketServer} and the WebSocket sibling of {@link\n * createHTTPClientTransport}.\n *\n * @remarks\n * Hand it to `createMCPClient({ transport })`: `start()` (run by `client.connect()`) performs\n * the RFC 6455 client handshake against `options.url` (accepting a `ws://` / `wss://` or an\n * `http://` / `https://` URL — a `ws(s)` scheme is converted to `http(s)` for the underlying\n * upgrade request), validates the `Sec-WebSocket-Accept` (via `@orkestrel/websocket`'s\n * `computeWebSocketAccept`), and opens a persistent bidirectional frame channel; each JSON-RPC\n * message the client `send`s is written as one masked text frame, and each decoded reply is\n * surfaced on the transport's `message` event for the client's id correlation. Add\n * `options.headers` (e.g. an `Authorization` bearer) to reach a guarded server.\n *\n * @param options - `url` (the remote WebSocket endpoint; REQUIRED) and optional `headers`\n * merged onto the upgrade request; see {@link WebSocketClientTransportOptions}\n * @returns A working {@link ClientTransportInterface} over a WebSocket\n *\n * @example\n * ```ts\n * import { createMCPClient } from '@src/core'\n * import { createWebSocketClientTransport } from '@src/server'\n *\n * const client = createMCPClient({\n * \ttransport: createWebSocketClientTransport({ url: 'ws://localhost:3000/mcp' }),\n * })\n * await client.connect()\n * const tools = await client.tools()\n * ```\n */\nexport function createWebSocketClientTransport(\n\toptions: WebSocketClientTransportOptions,\n): ClientTransportInterface {\n\treturn new WebSocketClientTransport(options)\n}\n\n/**\n * Create the stdio CLIENT transport for an {@link import('@src/core').MCPClientInterface}\n * — a {@link ClientTransportInterface} that spawns and drives a CHILD PROCESS MCP server\n * over newline-delimited JSON-RPC on `stdin`/`stdout`, the stdio sibling of {@link\n * createHTTPClientTransport} and {@link createWebSocketClientTransport}.\n *\n * @remarks\n * Hand it to `createMCPClient({ transport })`: `start()` (run by `client.connect()`)\n * spawns `options.command` with `options.args` and `options.env`, piping its\n * `stdin`/`stdout` for the JSON-RPC channel (its `stderr` inherits the parent's for\n * diagnostics). Each JSON-RPC message the client `send`s is written as one\n * newline-terminated line to the child's `stdin`; each decoded reply line from the\n * child's `stdout` is surfaced on the transport's `message` event for the client's\n * id correlation.\n *\n * @param options - `command` (the executable to spawn; REQUIRED), optional `args`,\n * and optional `env`; see {@link StdioClientTransportOptions}\n * @returns A working {@link ClientTransportInterface} over a child process's stdio\n *\n * @example\n * ```ts\n * import { createMCPClient } from '@src/core'\n * import { createStdioClientTransport } from '@src/server'\n *\n * const client = createMCPClient({\n * \ttransport: createStdioClientTransport({ command: 'node', args: ['./server.js'] }),\n * })\n * await client.connect()\n * const tools = await client.tools()\n * ```\n */\nexport function createStdioClientTransport(\n\toptions: StdioClientTransportOptions,\n): ClientTransportInterface {\n\treturn new StdioClientTransport(options)\n}\n\n/**\n * Create the MCP stdio transport INGRESS — pumps a transport-agnostic {@link\n * MCPServerInterface} over newline-delimited JSON-RPC on `stdin`/`stdout` (or an\n * injected stream pair), the stdio mirror of {@link createWebSocketServer}.\n *\n * @remarks\n * Wraps `options.input` (default `process.stdin`) / `options.output` (default\n * `process.stdout`) in a {@link import('./transports/StdioServerTransport.js').StdioServerTransport}\n * and PUMPS: each inbound {@link import('@src/core').JSONRPCMessage} that is a\n * REQUEST runs through `mcp.dispatch`, and a defined response is written back as a\n * newline-terminated line — a NOTIFICATION (`dispatch` → `undefined`) writes\n * nothing. A non-request message is ignored. The dispatch is guarded so a\n * `dispatch` / `send` fault surfaces on the transport's `error` event rather than\n * escaping the (async) message listener.\n *\n * @param mcp - The transport-agnostic {@link MCPServerInterface} to expose over stdio\n * @param options - Optional injectable `input` / `output` streams; see\n * {@link StdioServerOptions}\n * @returns A `{ start(): void; stop(): void }` handle to arm / tear down the pump\n *\n * @example\n * ```ts\n * import { createMCPServer, createToolManager } from '@src/core'\n * import { createStdioServer } from '@src/server'\n *\n * const mcp = createMCPServer({ name: 'docs', version: '1.0.0', tools: createToolManager() })\n * createStdioServer(mcp).start() // an MCP client now connects over this process's stdio\n * ```\n */\nexport function createStdioServer(\n\tmcp: MCPServerInterface,\n\toptions?: StdioServerOptions,\n): { start(): void; stop(): void } {\n\tconst input = options?.input ?? process.stdin\n\tconst output = options?.output ?? process.stdout\n\tconst transport = new StdioServerTransport(input, output)\n\ttransport.emitter.on('message', (message) => {\n\t\tif (!isJSONRPCRequest(message)) return\n\t\tvoid (async () => {\n\t\t\ttry {\n\t\t\t\tconst response = await mcp.dispatch(message)\n\t\t\t\tif (response !== undefined) await transport.send(response)\n\t\t\t} catch (error) {\n\t\t\t\t// Surface a dispatch / send fault on the transport's `error` event — but a user\n\t\t\t\t// `'error'` listener that itself throws would (Emitter.emit rethrows the first\n\t\t\t\t// listener throw) escape this `void` async listener as an unhandled rejection.\n\t\t\t\t// Swallow that here: a buggy observer must never crash the server (§13).\n\t\t\t\ttry {\n\t\t\t\t\ttransport.emitter.emit('error', error)\n\t\t\t\t} catch {\n\t\t\t\t\t// A throwing `error` listener is the caller's own bug — the end of the line.\n\t\t\t\t}\n\t\t\t}\n\t\t})()\n\t})\n\treturn {\n\t\tstart(): void {\n\t\t\tvoid transport.start()\n\t\t},\n\t\tstop(): void {\n\t\t\tvoid transport.close()\n\t\t},\n\t}\n}\n","import type { MiddlewareHandler } from '@orkestrel/server'\nimport type { MCPSessionEntry, MCPSessionOptions, MCPSessionState } from './types.js'\nimport { isInitializeRequest, parseJSONRPCMessage } from '@src/core'\nimport { openStream } from '@orkestrel/server'\nimport { DEFAULT_MCP_PATH, MCP_SESSION_HEADER } from './constants.js'\nimport { readLastEventId, readSessionHeader, rejectUnknownSession } from './helpers.js'\nimport { MCPSession } from './MCPSession.js'\n\n/**\n * Create the native MCP session {@link MiddlewareHandler} — the plug-and-play stateful layer\n * that fronts a session-agnostic {@link import('./factories.js').createMCPRoutes}. Compose it\n * via `router.use(createMCPSession())` (or the equivalent middleware seam), mirroring any\n * other closure-scoped stateful middleware. Has NO dependency on `@orkestrel/middleware` — the\n * session store, mint-on-`initialize`, and resumable stream are all native to this package.\n *\n * @remarks\n * Owns a closure `Map<string, MCPSessionEntry>` keyed by session id, and a single request\n * `path` (default {@link DEFAULT_MCP_PATH}); a request to any other path passes straight\n * through (`next()`).\n *\n * - **`POST {path}`.** Buffers `const text = await request.text()` (so the downstream route\n * can re-read it via a freshly-built forwarded `Request`). Resolves a session via {@link\n * readSessionHeader}: a VALID id touches the entry and sets `context.state.session`; an\n * ABSENT / unknown id whose (guarded) body parses to an `initialize` request ({@link\n * isInitializeRequest}) MINTS a fresh {@link MCPSession} (`crypto.randomUUID()`, `capacity`)\n * and sets `context.state.session`; neither → {@link rejectUnknownSession} (`404`). It then\n * FORWARDS a fresh `Request` carrying the buffered `text` (`next(forwarded)`) — never the\n * already-consumed original — so the route re-reads the same body, and stamps the response\n * with {@link MCP_SESSION_HEADER}.\n * - **`GET {path}`.** Resolves the session the same way (no mint — only `initialize` mints);\n * an invalid / unknown id is the same `404`. A valid session opens the resumable\n * server→client stream via `@orkestrel/server`'s {@link import('@orkestrel/server').openStream}:\n * replays every event after the client's `Last-Event-ID` ({@link readLastEventId}) BEFORE\n * attaching the stream for live pushes, then attaches; a client disconnect (`request.signal`)\n * detaches it. Long-lived — never `end()`ed here.\n * - **`DELETE {path}`.** Resolves the session; a valid id deletes it from the store and answers\n * `204`; an invalid / unknown id is the same `404`.\n *\n * It is MECHANISM, not policy, and ADDITIVE: omit it entirely for the stateless default\n * ({@link import('./factories.js').createMCPRoutes}'s only behavior). The `path` MUST match the\n * `createMCPRoutes` `path` it fronts. The WebSocket transport is inherently one session per\n * connection (the socket IS the session), so this middleware does not apply to it.\n *\n * @typeParam TState - The consumer's `TState`, which MUST extend {@link MCPSessionState} so\n * the resolved session can be threaded through `context.state.session`\n * @param options - Optional `path` (default {@link DEFAULT_MCP_PATH}), `ttl` (idle-session\n * sweep window, ms — omit for sessions that live until an explicit `DELETE`), `capacity`\n * (the folded per-session replay-log bound), and `clock` (the deterministic epoch-ms clock;\n * defaults to `Date.now`); see {@link MCPSessionOptions}\n * @returns A {@link MiddlewareHandler} that mints / validates sessions + serves the resumable\n * `GET` / `DELETE`\n *\n * @example\n * ```ts\n * import { createMCPServer, createToolManager } from '@src/core'\n * import { createMCPRoutes, createMCPSession } from '@src/server'\n *\n * const mcp = createMCPServer({ name: 'docs', version: '1.0.0', tools: createToolManager() })\n * router.use(createMCPSession({ ttl: 60_000 })) // stateful: mint + validate + resumable GET / DELETE\n * router.add(createMCPRoutes(mcp)) // the route stays session-agnostic\n * ```\n */\nexport function createMCPSession<TState extends MCPSessionState>(\n\toptions?: MCPSessionOptions,\n): MiddlewareHandler<TState> {\n\tconst path = options?.path ?? DEFAULT_MCP_PATH\n\tconst capacity = options?.capacity\n\tconst ttl = options?.ttl\n\tconst clock = options?.clock ?? Date.now\n\tconst store = new Map<string, MCPSessionEntry>()\n\n\treturn async (request, context, next) => {\n\t\tif (context.url.pathname !== path) return next()\n\t\tsweep()\n\n\t\tif (context.method === 'GET') {\n\t\t\tconst entry = resolve(request)\n\t\t\tif (entry === undefined) return rejectUnknownSession()\n\t\t\tconst stream = openStream()\n\t\t\t// A comment write flushes the response headers immediately (the underlying node:http\n\t\t\t// response only sends headers on its first `write`/`end`) — without it a client's fetch\n\t\t\t// hangs waiting for headers until the first replay/push write, which may never come.\n\t\t\tstream.comment('open')\n\t\t\tconst lastEventId = readLastEventId(request)\n\t\t\tif (lastEventId !== undefined) {\n\t\t\t\t// Replay every event STRICTLY AFTER the client's last-seen id BEFORE attaching, so the\n\t\t\t\t// missed events arrive in order ahead of any live push.\n\t\t\t\tfor (const e of entry.session.replay(lastEventId)) {\n\t\t\t\t\tstream.write({ id: e.id, data: JSON.stringify(e.message) })\n\t\t\t\t}\n\t\t\t}\n\t\t\tentry.session.attach(stream)\n\t\t\tif (request.signal.aborted) entry.session.detach(stream)\n\t\t\telse\n\t\t\t\trequest.signal.addEventListener('abort', () => entry.session.detach(stream), { once: true })\n\t\t\treturn stream.response\n\t\t}\n\n\t\tif (context.method === 'DELETE') {\n\t\t\tconst id = readSessionHeader(request)\n\t\t\tif (id === undefined || !store.has(id)) return rejectUnknownSession()\n\t\t\tstore.delete(id)\n\t\t\treturn new Response(null, { status: 204 })\n\t\t}\n\n\t\t// POST — buffer the body once so the downstream route can re-read it via a forwarded\n\t\t// Request; only `initialize` mints a fresh session when no valid id is present.\n\t\tconst text = await request.text()\n\t\tlet entry = resolve(request)\n\t\tif (entry === undefined) {\n\t\t\tlet parsed: unknown\n\t\t\ttry {\n\t\t\t\tparsed = parseJSONRPCMessage(JSON.parse(text))\n\t\t\t} catch {\n\t\t\t\tparsed = undefined\n\t\t\t}\n\t\t\tif (parsed !== undefined && isInitializeRequest(parsed)) {\n\t\t\t\tconst session = new MCPSession(crypto.randomUUID(), { capacity })\n\t\t\t\tentry = { session, touched: clock() }\n\t\t\t\tstore.set(session.id, entry)\n\t\t\t} else {\n\t\t\t\treturn rejectUnknownSession()\n\t\t\t}\n\t\t}\n\t\tcontext.state.session = entry.session\n\t\tconst forwarded = new Request(context.url, {\n\t\t\tmethod: 'POST',\n\t\t\theaders: request.headers,\n\t\t\tbody: text,\n\t\t})\n\t\tconst response = await next(forwarded)\n\t\tresponse.headers.set(MCP_SESSION_HEADER, entry.session.id)\n\t\treturn response\n\t}\n\n\t// Resolve + touch the session named by the request's `mcp-session-id` header, or\n\t// `undefined` when the header is absent or names an unknown / evicted session.\n\tfunction resolve(request: Request): MCPSessionEntry | undefined {\n\t\tconst id = readSessionHeader(request)\n\t\tif (id === undefined) return undefined\n\t\tconst entry = store.get(id)\n\t\tif (entry === undefined) return undefined\n\t\tentry.touched = clock()\n\t\treturn entry\n\t}\n\n\t// Lazy idle-TTL sweep — no background timer (the rate-limiter lazy-window idiom): drop\n\t// every session not touched within `ttl` on the next access. Omitted entirely (a no-op)\n\t// when `ttl` is unset — sessions then live until an explicit `DELETE`.\n\tfunction sweep(): void {\n\t\tif (ttl === undefined) return\n\t\tconst cutoff = clock() - ttl\n\t\tfor (const [id, entry] of store) {\n\t\t\tif (entry.touched <= cutoff) store.delete(id)\n\t\t}\n\t}\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":["#emitter","#url","#headers","#fetch","#timeout","#session","#deliver","#id","#events","#streams","#capacity","#ttl","#append","#evict","#counter","#emitter","#socket","#started","#closed","#receive","#onClose","#emitter","#url","#headers","#socket","#closed","#httpURL","#bind","#receive","#onClose","#emitter","#command","#args","#env","#child","#closed","#buffer","#receive","#onClose","#emitter","#input","#output","#started","#closed","#receive","#onClose","#buffer"],"sources":["../../../src/server/constants.ts","../../../src/server/helpers.ts","../../../src/server/transports/HTTPClientTransport.ts","../../../src/server/MCPSession.ts","../../../src/server/transports/WebSocketServerTransport.ts","../../../src/server/transports/WebSocketClientTransport.ts","../../../src/server/transports/StdioClientTransport.ts","../../../src/server/transports/StdioServerTransport.ts","../../../src/server/factories.ts","../../../src/server/middlewares.ts"],"sourcesContent":["// The MCP HTTP-transport constants (AGENTS §5 constants file) — the wire-level header\n// names, the default mount path, and the folded event-log bounds. The HEADER names are\n// the Streamable-HTTP transport's session /\n// protocol-version headers: they go LIVE when a `createMCPSession` middleware is mounted\n// (it mints the session id into `MCP_SESSION_HEADER` on `initialize` and reads it back on\n// subsequent requests); the stateless `createMCPRoutes` default neither sets nor reads\n// them. The transport-agnostic dispatch core (`src/core/mcp`) deliberately does NOT carry\n// these — header names belong to the HTTP transport, here.\n\n/**\n * The Streamable-HTTP transport header that carries the MCP session id. When a {@link\n * import('./middlewares.js').createMCPSession} middleware is mounted, it SETS this header on\n * the `initialize` response (the minted id) and READS it on every subsequent request\n * (validating the session); the stateless `createMCPRoutes` default neither sets nor reads it.\n */\nexport const MCP_SESSION_HEADER = 'mcp-session-id'\n\n/**\n * The Streamable-HTTP transport header that carries the negotiated MCP protocol version\n * on a subsequent request. The version is negotiated in the `initialize` JSON-RPC result\n * body; a stateful transport MAY additionally read this header to pin the per-request\n * protocol version (optional — the result body remains the source of truth).\n */\nexport const MCP_PROTOCOL_VERSION_HEADER = 'mcp-protocol-version'\n\n/** The default request path `createMCPRoutes` mounts the transport's `POST` route at. */\nexport const DEFAULT_MCP_PATH = '/mcp'\n\n/**\n * The WebSocket subprotocol the MCP-over-WebSocket transports negotiate — sent by the\n * client in `Sec-WebSocket-Protocol`, echoed by the server in its `101` handshake.\n *\n * @remarks\n * `createWebSocketServer` echoes it in the upgrade response and `createWebSocketClientTransport`\n * requests it, so an MCP WebSocket endpoint is distinguishable from any other WebSocket on the\n * same path. The default WebSocket upgrade path is {@link DEFAULT_MCP_PATH} (the same `'/mcp'`\n * the HTTP transport mounts at) — the upgrade is selected by the `Upgrade: websocket` header,\n * not a separate path.\n */\nexport const MCP_WEBSOCKET_SUBPROTOCOL = 'mcp'\n\n/**\n * The default capacity of a session's FOLDED resumable event log (the per-{@link\n * import('./MCPSession.js').MCPSession} replay log) — the maximum number of pushed\n * server→client messages retained for replay before the OLDEST is evicted.\n *\n * @remarks\n * Bounds the replay log's memory: only the most-recent {@link DEFAULT_MCP_SESSION_CAPACITY}\n * pushes are retained, so a client reconnecting with a `Last-Event-ID` older than that window\n * replays nothing (its cursor fell off the back). Override per `createMCPSession`'s `capacity`\n * for a deeper / shallower window.\n */\nexport const DEFAULT_MCP_SESSION_CAPACITY = 1024\n\n/**\n * The default per-event idle lifetime (ms) of a session's folded resumable event log — an\n * entry older than this is lazily evicted on the next access (no background timer), bounding\n * how far back a reconnecting client may replay.\n *\n * @remarks\n * Five minutes — a generous reconnection window for a dropped SSE stream without retaining\n * stale pushes indefinitely. The session's own idle TTL is the `createMCPSession` `ttl` knob;\n * this bounds the replay log paired with it.\n */\nexport const DEFAULT_MCP_SESSION_TTL = 300_000\n","import type { ClientTransportEventMap, JSONRPCMessage } from '@src/core'\nimport type { EmitterInterface } from '@orkestrel/emitter'\nimport type { SSEParserInterface } from '@orkestrel/sse'\nimport type { IncomingMessage } from 'node:http'\nimport type { LineExtraction } from './types.js'\nimport { createSSEParser } from '@orkestrel/sse'\nimport { JSONRPC_INVALID_REQUEST, jsonRPCError, parseJSONRPCMessage } from '@src/core'\nimport { isString } from '@orkestrel/contract'\nimport { MCP_SESSION_HEADER } from './constants.js'\n\n// The MCP server-transport helpers (AGENTS §4.3 module-scope names — no entity context).\n// The server-side reader `acceptsEventStream` reads the request's `Accept` header to\n// decide whether a Streamable-HTTP SSE response is allowed; `readSessionHeader` reads the\n// request's `mcp-session-id` header (the stateful transport's session validation);\n// `readLastEventId` reads the request's `Last-Event-ID` header (the resumable GET-SSE\n// replay cursor); the CLIENT-side reader `readEventStream` decodes a `fetch` Response's SSE\n// body back into JSON-RPC messages (the egress mirror, reusing `@orkestrel/sse`'s\n// `SSEParser`); `upgradeRequestPath` reads a raw `node:http` upgrade request's path (the\n// WebSocket transport's upgrade-path match). All are total and narrow at the boundary,\n// never `as` (AGENTS §14) — a missing / non-string Accept reads as \"no\", a missing session /\n// last-event header reads as `undefined`, a non-message SSE `data:` event is dropped, an\n// absent `url` reads as `'/'`.\n\n/**\n * Whether the request's `Accept` header opts into a Server-Sent-Events response.\n *\n * @remarks\n * Reads the fetch-standard `Request.headers.get('accept')` and returns `true` when it\n * contains `text/event-stream` (case-insensitive). The MCP `POST` handler uses it\n * (together with the `streaming` option) to pick the Streamable-HTTP SSE response\n * framing over a plain JSON body; the JSON-RPC envelope is identical either way. Total\n * — an absent / unmatched header returns `false`.\n *\n * @param request - The fetch-standard `Request`\n * @returns `true` when the client `Accept`s `text/event-stream`, else `false`\n */\nexport function acceptsEventStream(request: Request): boolean {\n\tconst accept = request.headers.get('accept')\n\tif (accept === null) return false\n\treturn accept.toLowerCase().includes('text/event-stream')\n}\n\n/**\n * Read the request's `mcp-session-id` header — the session id a stateful transport\n * validates, or `undefined` when absent.\n *\n * @remarks\n * Reads `request.headers.get(MCP_SESSION_HEADER)` — a fetch-standard `Headers` lookup\n * (single-valued by construction, never an array) — so a missing header reads as\n * `undefined` (no session). {@link import('./middlewares.js').createMCPSession} uses it on\n * every `POST` / `GET` / `DELETE` to look the session up in its closure store; an\n * `undefined` id is treated exactly like an unknown one (a `404`). Total — never throws.\n *\n * @param request - The fetch-standard `Request`\n * @returns The session id, or `undefined` when the header is absent\n */\nexport function readSessionHeader(request: Request): string | undefined {\n\tconst id = request.headers.get(MCP_SESSION_HEADER)\n\treturn id === null ? undefined : id\n}\n\n/**\n * Read the request's `Last-Event-ID` header — the SSE resume cursor a client sends when it\n * reconnects to the resumable `GET {path}` stream, or `undefined` when absent.\n *\n * @remarks\n * Reads `request.headers.get('last-event-id')` — a fetch-standard `Headers` lookup — so a\n * missing header reads as `undefined` (no resume, the stream starts fresh). The resumable\n * `GET` handler in {@link import('./middlewares.js').createMCPSession} passes a present value\n * to the session's {@link import('./types.js').MCPSessionInterface.replay} to re-deliver the\n * missed events before attaching the stream for live pushes. Total — never throws.\n *\n * @param request - The fetch-standard `Request`\n * @returns The last-event-id, or `undefined` when the header is absent\n */\nexport function readLastEventId(request: Request): string | undefined {\n\tconst id = request.headers.get('last-event-id')\n\treturn id === null ? undefined : id\n}\n\n/**\n * Build the stateful transport's \"unknown session\" rejection — an HTTP `404` carrying a\n * JSON-RPC error body.\n *\n * @remarks\n * Returns `Response.json(jsonRPCError(null, JSONRPC_INVALID_REQUEST, 'Session not found'),\n * { status: 404 })`, mirroring `createMCPRoutes`'s `400` transport-failure shape (a\n * JSON-RPC error BODY with a `null` id) but at the session-not-found status. Shared by\n * every {@link import('./middlewares.js').createMCPSession} validation site — the\n * non-`initialize` `POST` path, the resumable `GET {path}` open, and the `DELETE {path}`\n * session-end (each a missing / unknown / TTL-evicted id) — so the single `404` envelope\n * is defined once. Total — never throws.\n *\n * @returns The `404` JSON-RPC error `Response`\n */\nexport function rejectUnknownSession(): Response {\n\treturn Response.json(jsonRPCError(null, JSONRPC_INVALID_REQUEST, 'Session not found'), {\n\t\tstatus: 404,\n\t})\n}\n\n/**\n * Decode a `fetch` Response's Server-Sent-Events body into the JSON-RPC messages it\n * carried — the CLIENT-side inverse of the server's Streamable-HTTP SSE response.\n *\n * @remarks\n * Reads the whole `response.body` stream chunk-by-chunk through a `TextDecoder({\n * stream: true })` (handling a multi-byte char split across reads) and `@orkestrel/sse`'s\n * {@link SSEParserInterface} (handling a partial line / in-progress event split across\n * reads), then narrows each dispatched event's `data` to a {@link JSONRPCMessage} via\n * `parseJSONRPCMessage` (so a non-message / non-JSON `data:` event is DROPPED, never\n * thrown — total, §14). It reuses the SAME `SSEParser` the server's `openStream` seam\n * serializes against, so the wire round-trips. A `null` body (no stream) yields no\n * messages; the {@link import('./transports/HTTPClientTransport.js').HTTPClientTransport}\n * reads a request/response SSE reply (the server sends one `data:` event then ends), so\n * this drains to completion.\n *\n * @param response - The SSE `fetch` Response to decode (its `body` is read to completion)\n * @returns Every {@link JSONRPCMessage} the stream carried, in order\n */\nexport async function readEventStream(response: Response): Promise<readonly JSONRPCMessage[]> {\n\tconst body = response.body\n\tif (body === null) return []\n\tconst reader = body.getReader()\n\tconst decoder = new TextDecoder()\n\tconst parser: SSEParserInterface = createSSEParser()\n\tconst messages: JSONRPCMessage[] = []\n\ttry {\n\t\tfor (;;) {\n\t\t\tconst { done, value } = await reader.read()\n\t\t\tif (done) break\n\t\t\tfor (const event of parser.parse(decoder.decode(value, { stream: true }))) {\n\t\t\t\t// JSON-parse the event's `data` (the JSON-RPC envelope the server wrote), then\n\t\t\t\t// narrow it — a malformed / non-message payload is dropped, never thrown (§14).\n\t\t\t\tconst message = decodeEvent(event.data)\n\t\t\t\tif (message !== undefined) messages.push(message)\n\t\t\t}\n\t\t}\n\t} finally {\n\t\treader.releaseLock()\n\t}\n\treturn messages\n}\n\n/**\n * Decode one SSE event's `data` string into a {@link JSONRPCMessage}, or `undefined`\n * when it is not one — the per-event step {@link readEventStream} folds over.\n *\n * @remarks\n * `JSON.parse`s the `data` (the server serializes the JSON-RPC envelope as the event's\n * `data`) inside a try/catch and narrows the parsed value with `parseJSONRPCMessage`.\n * Total (§14): malformed JSON or a non-message value yields `undefined`, never throws.\n *\n * @param data - One SSE event's `data` payload\n * @returns The decoded {@link JSONRPCMessage}, or `undefined`\n */\nexport function decodeEvent(data: string): JSONRPCMessage | undefined {\n\ttry {\n\t\treturn parseJSONRPCMessage(JSON.parse(data))\n\t} catch {\n\t\treturn undefined\n\t}\n}\n\n/**\n * Read the path (without the query string) of a raw `node:http` protocol-upgrade request —\n * the `createWebSocketServer` upgrade-path match.\n *\n * @remarks\n * A `node:http` {@link import('node:http').IncomingMessage}'s `url` is the request TARGET\n * (`'/mcp?x=1'`), narrowed with `isString` (§14, never `as`) and defaulting to `'/'` for an\n * absent target; it is parsed against a dummy base (only the pathname matters for the upgrade\n * decision) and the `pathname` returned. The upgrade handler compares this against its\n * configured `path` to decide whether to claim the socket. Total — never throws on an\n * adversarial / absent target.\n *\n * @param request - The raw upgrade {@link import('node:http').IncomingMessage}\n * @returns The request's path (the `pathname`, no query), or `'/'` when the target is absent\n */\nexport function upgradeRequestPath(request: IncomingMessage): string {\n\tconst target = isString(request.url) ? request.url : '/'\n\treturn new URL(target, 'http://localhost').pathname\n}\n\n/**\n * Fold one more chunk of raw stdio bytes into a newline-framed buffer — the shared\n * line-framing step both stdio transports (client and server) read their inbound\n * newline-delimited JSON-RPC messages through.\n *\n * @remarks\n * Concatenates `buffer` (the carried-forward partial line from the previous call)\n * with `chunk`, splits on `'\\n'`, and returns every COMPLETE line (a `'\\r'` trailing\n * a line, from a CRLF-framed peer, is trimmed) plus the final, possibly-empty\n * fragment as the new `remainder` — the caller threads it back in as the next call's\n * `buffer`. A chunk containing no `'\\n'` yields no lines and the whole (buffer +\n * chunk) as `remainder`. Pure — no I/O, no instance state.\n *\n * @param buffer - The partial line carried forward from the previous chunk (`''` initially)\n * @param chunk - The newly-read raw bytes (already decoded to a string)\n * @returns The complete `lines` extracted (in order) and the trailing `remainder`\n */\nexport function extractLines(buffer: string, chunk: string): LineExtraction {\n\tconst combined = buffer + chunk\n\tconst parts = combined.split('\\n')\n\tconst remainder = parts[parts.length - 1] ?? ''\n\tconst lines = parts.slice(0, -1).map((line) => (line.endsWith('\\r') ? line.slice(0, -1) : line))\n\treturn { lines, remainder }\n}\n\n/**\n * Decode and deliver each complete newline-framed line onto a {@link\n * ClientTransportEventMap} emitter — the shared per-chunk dispatch step both stdio\n * transports (client and server) run their {@link extractLines} output through.\n *\n * @remarks\n * A blank line is skipped (a stray trailing newline). Every other line is decoded\n * with {@link decodeEvent} (`JSON.parse` + `parseJSONRPCMessage`, guarded); a\n * well-formed {@link JSONRPCMessage} emits `message`, a malformed / non-message line\n * emits `error` (§14 — total, never throws). Pure w.r.t. its own state — the emit is\n * the caller-owned side effect.\n *\n * @param emitter - The transport's {@link EmitterInterface} to emit `message` / `error` onto\n * @param lines - The complete lines (from {@link extractLines}) to decode and deliver\n */\nexport function dispatchLines(\n\temitter: EmitterInterface<ClientTransportEventMap>,\n\tlines: readonly string[],\n): void {\n\tfor (const line of lines) {\n\t\tif (line.length === 0) continue\n\t\tconst message = decodeEvent(line)\n\t\tif (message === undefined) {\n\t\t\temitter.emit('error', new Error('non-JSON-RPC stdio line'))\n\t\t\tcontinue\n\t\t}\n\t\temitter.emit('message', message)\n\t}\n}\n","import type { ClientTransportEventMap, ClientTransportInterface, JSONRPCMessage } from '@src/core'\nimport type { EmitterInterface } from '@orkestrel/emitter'\nimport type { HTTPClientTransportOptions } from '../types.js'\nimport { parseJSONRPCMessage } from '@src/core'\nimport { Emitter } from '@orkestrel/emitter'\nimport { MCP_SESSION_HEADER } from '../constants.js'\nimport { readEventStream } from '../helpers.js'\n\n/**\n * The HTTP CLIENT transport for the Model Context Protocol — a\n * {@link ClientTransportInterface} that drives a REMOTE Streamable-HTTP MCP server over\n * `fetch`, the egress mirror of the server's `createMCPRoutes`.\n *\n * @remarks\n * - **Request/response over `fetch`.** `send(message)` POSTs the JSON-serialized\n * message (or batch) to `options.url` with `content-type: application/json` and an\n * `Accept` of BOTH `application/json` and `text/event-stream` (so the server may\n * answer with either framing) — plus any `options.headers` (e.g. an `Authorization`\n * bearer). It then decodes the reply and emits each decoded {@link JSONRPCMessage} on\n * the `message` event the {@link import('@src/core').MCPClientInterface} subscribes\n * to.\n * - **Both reply framings.** A `200` with an `application/json` body is parsed with\n * `parseJSONRPCMessage`; a `200` with a `text/event-stream` body is decoded via the\n * `@orkestrel/sse` {@link import('@orkestrel/sse').SSEParserInterface} ({@link\n * readEventStream}) — the inverse of the server's `openStream` seam, so the wire\n * round-trips. A `202`\n * Accepted (a notification) carries no body and emits nothing.\n * - **Session echo.** `start()` / `close()` are no-ops (a request/response transport\n * holds no long-lived connection). The `mcp-session-id` response header, when a\n * STATEFUL server sends one (on `initialize`), is captured into `session` and then\n * ECHOED as the `mcp-session-id` request header on every SUBSEQUENT request — so an\n * `MCPClient` passes a stateful server's session validation. Before initialize returns\n * an id, `session` is `undefined` and no header is sent (safe against a stateless\n * server, which neither sends nor expects one).\n * - **Total at the boundary (§14).** Every reply is narrowed (`parseJSONRPCMessage`,\n * the SSE decoder) — a non-message reply is dropped, never asserted; a `fetch` /\n * decode failure surfaces on the `error` event rather than escaping `send`.\n * - **Observable (§13).** Owns the `emitter` ({@link ClientTransportEventMap}); fires\n * `message` per decoded reply, `error` on a fault, and `close` on `close()`.\n *\n * @example\n * ```ts\n * const transport = new HTTPClientTransport({ url: 'http://localhost:3000/mcp' })\n * const client = new MCPClient({ transport })\n * await client.connect()\n * ```\n */\nexport class HTTPClientTransport implements ClientTransportInterface {\n\treadonly #emitter: Emitter<ClientTransportEventMap>\n\treadonly #url: string\n\treadonly #headers: Readonly<Record<string, string>>\n\treadonly #fetch: typeof fetch\n\treadonly #timeout: number | undefined\n\t#session: string | undefined = undefined\n\n\tconstructor(options: HTTPClientTransportOptions) {\n\t\tthis.#emitter = new Emitter<ClientTransportEventMap>()\n\t\tthis.#url = options.url\n\t\tthis.#headers = options.headers ?? {}\n\t\tthis.#fetch = options.fetch ?? globalThis.fetch\n\t\tthis.#timeout = options.timeout\n\t}\n\n\tget emitter(): EmitterInterface<ClientTransportEventMap> {\n\t\treturn this.#emitter\n\t}\n\n\tget session(): string | undefined {\n\t\treturn this.#session\n\t}\n\n\tasync start(): Promise<void> {\n\t\t// A request/response transport opens no long-lived connection — `send` issues each\n\t\t// `fetch` on demand. Nothing to arm.\n\t}\n\n\tasync send(message: JSONRPCMessage | readonly JSONRPCMessage[]): Promise<void> {\n\t\tlet response: Response\n\t\ttry {\n\t\t\tresponse = await this.#fetch(this.#url, {\n\t\t\t\tmethod: 'POST',\n\t\t\t\theaders: {\n\t\t\t\t\t'content-type': 'application/json',\n\t\t\t\t\taccept: 'application/json, text/event-stream',\n\t\t\t\t\t// Echo a captured session id so a STATEFUL server validates the request; before\n\t\t\t\t\t// `initialize` returns one `#session` is undefined → no header (safe for a\n\t\t\t\t\t// stateless server). A caller `headers` key still wins (merged last).\n\t\t\t\t\t...(this.#session === undefined ? {} : { [MCP_SESSION_HEADER]: this.#session }),\n\t\t\t\t\t...this.#headers,\n\t\t\t\t},\n\t\t\t\tbody: JSON.stringify(message),\n\t\t\t\t...(this.#timeout === undefined ? {} : { signal: AbortSignal.timeout(this.#timeout) }),\n\t\t\t})\n\t\t} catch (error) {\n\t\t\t// A network-level failure (connection refused, DNS) — surface it for observation;\n\t\t\t// the client's per-request deadline still rejects the pending request.\n\t\t\tthis.#emitter.emit('error', error)\n\t\t\treturn\n\t\t}\n\t\t// Capture a server-assigned session id (a stateless server sends none) so it is echoed\n\t\t// on subsequent requests; a missing header leaves `session` unchanged.\n\t\tconst session = response.headers.get(MCP_SESSION_HEADER)\n\t\tif (session !== null) this.#session = session\n\t\tawait this.#deliver(response)\n\t}\n\n\tasync close(): Promise<void> {\n\t\tthis.#emitter.emit('close')\n\t}\n\n\t// Decode a reply and emit each carried message. A 202 (notification accepted) has no\n\t// body — emit nothing. An `application/json` body is one envelope; a `text/event-stream`\n\t// body is decoded via the core SSEParser (one or more `data:` events). A decode failure\n\t// surfaces on `error` rather than escaping.\n\tasync #deliver(response: Response): Promise<void> {\n\t\tif (response.status === 202) return\n\t\tconst type = response.headers.get('content-type') ?? ''\n\t\ttry {\n\t\t\tif (type.includes('text/event-stream')) {\n\t\t\t\tfor (const message of await readEventStream(response))\n\t\t\t\t\tthis.#emitter.emit('message', message)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (type.includes('application/json')) {\n\t\t\t\tconst message = parseJSONRPCMessage(await response.json())\n\t\t\t\tif (message !== undefined) this.#emitter.emit('message', message)\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tthis.#emitter.emit('error', error)\n\t\t}\n\t}\n}\n","import type { JSONRPCMessage } from '@src/core'\nimport type { StreamInterface } from '@orkestrel/server'\nimport type { EventStoreEntry, MCPSessionInterface, MCPSessionOptions } from './types.js'\nimport { DEFAULT_MCP_SESSION_CAPACITY, DEFAULT_MCP_SESSION_TTL } from './constants.js'\n\n/**\n * One MCP transport session — the per-session entity a {@link\n * import('./middlewares.js').createMCPSession} middleware owns, keyed by its `id`, carrying the\n * resumable server→client push channel with its bounded replay log FOLDED IN.\n *\n * @remarks\n * The single session entity (the old `SessionState` + `EventStore` merged): it holds the\n * session `id`, its OWN bounded, replayable log of pushed server→client messages (the\n * resumable GET-SSE channel — a private `#events` `Map` + a monotone `#counter`, with\n * `capacity` / `ttl` eviction, NOT a separate store), and the set of currently OPEN\n * server→client SSE streams (a resumable `GET {path}` registers via `attach`, unregisters via\n * `detach` on disconnect). Still a small entity (not a record), built minimal + extensible.\n *\n * - **`push` is the server-initiated primitive.** It APPENDS the message to the log (assigning\n * a monotone base36 event id) and FANS it out to every attached stream as one `id:`-tagged\n * SSE event (`stream.write({ id, data })`). A push with NO attached stream is still logged,\n * so a client that connects (or reconnects with a `Last-Event-ID`) LATER replays it from the\n * log. A `write` to a closed stream is a safe no-op (the {@link\n * `@orkestrel/server`'s `openStream` contract), so a just-disconnected stream that\n * has not yet been `detach`ed never throws. A replayed event and the live one carry the\n * IDENTICAL id (the log assigns it once).\n *\n * - **`replay(afterId)` is strictly-after.** It returns every retained log entry whose id sorts\n * AFTER `afterId` in append order — the missed-events list the `GET {path}` handler writes\n * before attaching the stream for live pushes. The decision for an UNKNOWN / already-evicted\n * `afterId` (the client's cursor fell off the back of the capacity window, or never existed):\n * replay NOTHING. Replaying the whole retained log would re-deliver events the client never\n * lost (its cursor is OLDER than everything retained); returning `[]` lets the handler then\n * stream only the fresh pushes that follow `attach` — the spec-sane resume.\n *\n * - **Bounded, append-ordered, plain `Map` (§21).** The log lives in ONE insertion-ordered\n * `Map<id, entry>` — insertion order IS append order IS id order, so `replay` and capacity\n * eviction both walk the map directly. NO database mirror — the log is process-local\n * transport mechanics, not durable state. `push` first drops every entry older than `ttl`\n * (lazy TTL — no background timer, the middleware's lazy-window idiom), appends, then evicts\n * the OLDEST entries until at most `capacity` remain; `replay` also runs the lazy TTL sweep\n * first, so a stale entry is never replayed.\n *\n * - **No transport coupling beyond the SSE seam.** It holds session state + the generic {@link\n * StreamInterface} handles `attach` was handed — never a raw socket, request, or response.\n * The middleware opens the stream (the spine seam) and registers it here; this class only\n * serializes a message onto the already-open streams.\n *\n * - **Injected clock.** `push` / `replay` accept an optional `now` (epoch ms), defaulting to\n * `Date.now()` — so a test drives TTL eviction with an elapsed clock rather than a real timer\n * (AGENTS §16).\n *\n * @example\n * ```ts\n * const session = new MCPSession(crypto.randomUUID())\n * session.attach(stream) // an open resumable GET-SSE stream\n * session.push({ jsonrpc: '2.0', method: 'notifications/message', params: { text: 'hi' } })\n * // → logged AND written to `stream` as an `id:`-tagged event; a reconnect replays it\n * ```\n */\nexport class MCPSession implements MCPSessionInterface {\n\treadonly #id: string\n\treadonly #events = new Map<string, EventStoreEntry>()\n\treadonly #streams = new Set<StreamInterface>()\n\treadonly #capacity: number\n\treadonly #ttl: number\n\t#counter = 0\n\n\tconstructor(id: string, options?: MCPSessionOptions) {\n\t\tthis.#id = id\n\t\tthis.#capacity = options?.capacity ?? DEFAULT_MCP_SESSION_CAPACITY\n\t\tthis.#ttl = options?.ttl ?? DEFAULT_MCP_SESSION_TTL\n\t}\n\n\tget id(): string {\n\t\treturn this.#id\n\t}\n\n\tattach(stream: StreamInterface): void {\n\t\tthis.#streams.add(stream)\n\t}\n\n\tdetach(stream: StreamInterface): void {\n\t\tthis.#streams.delete(stream)\n\t}\n\n\tpush(message: JSONRPCMessage, now = Date.now()): string {\n\t\t// Append to the log first (assigning the monotone event id), then fan the SAME id out to\n\t\t// every open stream — so a replayed event and a live one carry the identical id.\n\t\tconst id = this.#append(message, now)\n\t\tconst data = JSON.stringify(message)\n\t\tfor (const stream of this.#streams) stream.write({ id, data })\n\t\treturn id\n\t}\n\n\treplay(afterId: string, now = Date.now()): readonly EventStoreEntry[] {\n\t\tthis.#evict(now)\n\t\tconst out: EventStoreEntry[] = []\n\t\tlet found = false\n\t\tfor (const entry of this.#events.values()) {\n\t\t\t// Collect every entry STRICTLY AFTER `afterId`, in append order.\n\t\t\tif (found) out.push(entry)\n\t\t\telse if (entry.id === afterId) found = true\n\t\t}\n\t\t// An unknown / evicted `afterId` was never matched → `found` stays false → replay nothing\n\t\t// (the documented spec-sane choice; never re-deliver un-lost events).\n\t\treturn found ? out : []\n\t}\n\n\t// Append a message to the bounded replay log under a fresh monotone id, evicting stale +\n\t// over-capacity entries — the folded EventStore.append, now private to the session.\n\t#append(message: JSONRPCMessage, now: number): string {\n\t\t// Lazy TTL sweep BEFORE appending so an idle log shrinks as it is written.\n\t\tthis.#evict(now)\n\t\tthis.#counter += 1\n\t\tconst id = this.#counter.toString(36)\n\t\tthis.#events.set(id, { id, message, timestamp: now })\n\t\t// Capacity bound: drop the OLDEST entries (front of the insertion-ordered map) until at\n\t\t// most `capacity` remain — so the log is the most-recent `capacity` pushes.\n\t\twhile (this.#events.size > this.#capacity) {\n\t\t\tconst oldest = this.#events.keys().next().value\n\t\t\tif (oldest === undefined) break\n\t\t\tthis.#events.delete(oldest)\n\t\t}\n\t\treturn id\n\t}\n\n\t// Drop every entry older than the TTL. Entries are append-ordered (oldest first) and the\n\t// timestamp is monotone with insertion, so the stale run is a PREFIX — stop at the first live\n\t// entry. A non-positive ttl is treated as no expiry (nothing ever ages out by time).\n\t#evict(now: number): void {\n\t\tif (this.#ttl <= 0) return\n\t\tconst cutoff = now - this.#ttl\n\t\tfor (const [id, entry] of this.#events) {\n\t\t\tif (entry.timestamp <= cutoff) this.#events.delete(id)\n\t\t\telse break\n\t\t}\n\t}\n}\n","import type { ClientTransportEventMap, ClientTransportInterface, JSONRPCMessage } from '@src/core'\nimport type { EmitterInterface } from '@orkestrel/emitter'\nimport type { NodeWebSocketInterface } from '@orkestrel/websocket'\nimport { parseJSONRPCMessage } from '@src/core'\nimport { Emitter } from '@orkestrel/emitter'\n\n/**\n * The per-connection JSON-RPC-over-WebSocket SERVER bridge — wraps a\n * {@link NodeWebSocketInterface} (the RFC 6455 wire wrapper) as a\n * {@link ClientTransportInterface}, the bidirectional JSON-RPC message channel\n * `createWebSocketServer` pumps `mcp.dispatch` over and the egress mirror's\n * {@link import('./WebSocketClientTransport.js').WebSocketClientTransport} reuses.\n *\n * @remarks\n * - **Reuses `ClientTransportInterface` (§21).** It IS the same generic carrier the HTTP\n * client transport implements — `emitter` (`message` / `close` / `error`), `start`,\n * `send`, `close` — so the WebSocket server and client both speak ONE transport contract,\n * no near-duplicate sibling interface. `session` is `undefined` (the stateless v1; a\n * session id is the deferred sessions tier). The name keeps the role explicit even though\n * the shape is shared.\n * - **Inbound (`message`).** `start()` subscribes to the socket's `message` event; each text\n * frame is `JSON.parse`d inside a try/catch and narrowed with `parseJSONRPCMessage` — a\n * well-formed {@link JSONRPCMessage} is re-emitted on this transport's `message` event (the\n * parsed envelope the {@link import('@src/core').MCPServerInterface} pump dispatches), while\n * a non-JSON or non-message frame is surfaced on `error` and DROPPED, never thrown (§14). It\n * also bridges the socket's `close` → this transport's `close`, and the socket's `error`.\n * - **Outbound (`send`).** `send(message | messages)` writes ONE text frame per message\n * (`nodeWs.send(JSON.stringify(...))`); the underlying wrapper no-ops a write on a\n * non-open socket, so a closed connection drops silently rather than throwing.\n * - **`close()`** closes the underlying socket (the RFC 6455 close handshake) and fires the\n * transport's `close` event (idempotent — a second `close`, or a socket-driven close, emits\n * once).\n * - **Observable (§13).** Owns the `emitter` ({@link ClientTransportEventMap}); the emitter\n * isolates a listener throw (a buggy observer never corrupts the bridge). `error` is a\n * DOMAIN event (a transport-level fault), distinct from the emitter's listener-error channel.\n */\nexport class WebSocketServerTransport implements ClientTransportInterface {\n\treadonly #emitter: Emitter<ClientTransportEventMap>\n\treadonly #socket: NodeWebSocketInterface\n\t#started = false\n\t#closed = false\n\n\tconstructor(socket: NodeWebSocketInterface) {\n\t\tthis.#emitter = new Emitter<ClientTransportEventMap>()\n\t\tthis.#socket = socket\n\t}\n\n\tget emitter(): EmitterInterface<ClientTransportEventMap> {\n\t\treturn this.#emitter\n\t}\n\n\tget session(): string | undefined {\n\t\t// The stateless v1 holds no session — a server-assigned id is the deferred tier.\n\t\treturn undefined\n\t}\n\n\tasync start(): Promise<void> {\n\t\t// Arm the socket subscriptions once: a text frame becomes a `message`, the socket's\n\t\t// close / error bridge to this transport's events. Idempotent — a second `start` is a\n\t\t// no-op (the single MCPServer pump subscribes once).\n\t\tif (this.#started || this.#closed) return\n\t\tthis.#started = true\n\t\tthis.#socket.emitter.on('message', (text) => this.#receive(text))\n\t\tthis.#socket.emitter.on('close', () => this.#onClose())\n\t\tthis.#socket.emitter.on('error', (error) => this.#emitter.emit('error', error))\n\t}\n\n\tasync send(message: JSONRPCMessage | readonly JSONRPCMessage[]): Promise<void> {\n\t\t// One text frame per message (a batch is unrolled). The wrapper drops a write on a\n\t\t// non-open socket, so a closed connection is a silent no-op rather than a throw.\n\t\tconst messages = Array.isArray(message) ? message : [message]\n\t\tfor (const one of messages) this.#socket.send(JSON.stringify(one))\n\t}\n\n\tasync close(): Promise<void> {\n\t\tif (this.#closed) return\n\t\tthis.#closed = true\n\t\tthis.#socket.close()\n\t\tthis.#emitter.emit('close')\n\t}\n\n\t// Decode one inbound text frame: `JSON.parse` → `parseJSONRPCMessage`. A well-formed\n\t// message re-emits on `message`; a malformed / non-message frame surfaces on `error` and\n\t// is dropped (§14 — the bridge never throws on adversarial wire input).\n\t#receive(text: string): void {\n\t\tlet parsed: unknown\n\t\ttry {\n\t\t\tparsed = JSON.parse(text)\n\t\t} catch (error) {\n\t\t\tthis.#emitter.emit('error', error)\n\t\t\treturn\n\t\t}\n\t\tconst message = parseJSONRPCMessage(parsed)\n\t\tif (message === undefined) {\n\t\t\tthis.#emitter.emit('error', new Error('non-JSON-RPC WebSocket frame'))\n\t\t\treturn\n\t\t}\n\t\tthis.#emitter.emit('message', message)\n\t}\n\n\t// The socket closed (peer close frame, transport teardown) — fire this transport's\n\t// `close` once. A `close()` call already flipped `#closed`, so a socket-driven close\n\t// after an explicit one does not double-emit.\n\t#onClose(): void {\n\t\tif (this.#closed) return\n\t\tthis.#closed = true\n\t\tthis.#emitter.emit('close')\n\t}\n}\n","import type { ClientTransportEventMap, ClientTransportInterface, JSONRPCMessage } from '@src/core'\nimport type { EmitterInterface } from '@orkestrel/emitter'\nimport type { NodeWebSocketInterface } from '@orkestrel/websocket'\nimport type { WebSocketClientTransportOptions } from '../types.js'\nimport type { IncomingMessage } from 'node:http'\nimport type { Duplex } from 'node:stream'\nimport { randomBytes } from 'node:crypto'\nimport { request as httpRequest } from 'node:http'\nimport { request as httpsRequest } from 'node:https'\nimport { parseJSONRPCMessage } from '@src/core'\nimport { isString } from '@orkestrel/contract'\nimport { Emitter } from '@orkestrel/emitter'\nimport {\n\tcomputeWebSocketAccept,\n\tcreateNodeWebSocket,\n\tWEBSOCKET_VERSION,\n} from '@orkestrel/websocket'\nimport { MCP_WEBSOCKET_SUBPROTOCOL } from '../constants.js'\n\n/**\n * The WebSocket CLIENT transport for the Model Context Protocol — a\n * {@link ClientTransportInterface} that drives a REMOTE MCP server over a WebSocket, the\n * egress mirror of {@link import('./factories.js').createWebSocketServer} and the WebSocket\n * sibling of {@link import('./HTTPClientTransport.js').HTTPClientTransport}.\n *\n * @remarks\n * - **Persistent bidirectional channel (unlike the HTTP transport).** `start()` performs the\n * RFC 6455 client handshake: it opens a `node:http`(`s`) `GET` carrying `Connection: Upgrade`\n * / `Upgrade: websocket` / a random `Sec-WebSocket-Key` / `Sec-WebSocket-Version: 13` /\n * `Sec-WebSocket-Protocol: mcp` (plus any `options.headers`), awaits the client `'upgrade'`\n * event, and VALIDATES `Sec-WebSocket-Accept === computeWebSocketAccept(key)` (the D2 helper)\n * — a mismatch (or a non-`101` response, or a request error) REJECTS `start()` and the socket\n * is destroyed. On success it wraps the raw upgraded socket in `createNodeWebSocket({ socket,\n * head })` (CLIENT mode — no key → frames are MASKED per §5.3) and bridges its `message`.\n * - **Inbound (`message`).** Each decoded text frame is `JSON.parse`d (guarded) and narrowed\n * with `parseJSONRPCMessage` — a {@link JSONRPCMessage} re-emits on this transport's `message`\n * event (the reply the {@link import('@src/core').MCPClientInterface} correlates by `id`); a\n * non-JSON / non-message frame surfaces on `error` and is dropped (§14). The socket's `close`\n * / `error` bridge to this transport's events.\n * - **Outbound (`send`).** `send(message | messages)` writes ONE masked text frame per message.\n * - **`close()`** closes the underlying socket and fires `close` (idempotent).\n * - **URL scheme.** `options.url` accepts a `ws://` / `wss://` URL or an `http://` / `https://`\n * one; a `ws(s)` scheme is converted to `http(s)` for the underlying upgrade request (`wss`\n * → TLS via `node:https`). Either reaches the same endpoint.\n * - **Observable (§13).** Owns the `emitter` ({@link ClientTransportEventMap}); every emit\n * the emitter isolates a listener throw (a buggy observer never corrupts the transport);\n * `error` is a DOMAIN event (a transport-level fault).\n *\n * @example\n * ```ts\n * const transport = new WebSocketClientTransport({ url: 'ws://localhost:3000/mcp' })\n * const client = new MCPClient({ transport })\n * await client.connect() // start() handshakes, then the MCP initialize runs over WS frames\n * ```\n */\nexport class WebSocketClientTransport implements ClientTransportInterface {\n\treadonly #emitter: Emitter<ClientTransportEventMap>\n\treadonly #url: string\n\treadonly #headers: Readonly<Record<string, string>>\n\t#socket: NodeWebSocketInterface | undefined = undefined\n\t#closed = false\n\n\tconstructor(options: WebSocketClientTransportOptions) {\n\t\tthis.#emitter = new Emitter<ClientTransportEventMap>()\n\t\tthis.#url = options.url\n\t\tthis.#headers = options.headers ?? {}\n\t}\n\n\tget emitter(): EmitterInterface<ClientTransportEventMap> {\n\t\treturn this.#emitter\n\t}\n\n\tget session(): string | undefined {\n\t\treturn undefined\n\t}\n\n\tasync start(): Promise<void> {\n\t\t// Already connected — a second `connect()` short-circuits in the client, but guard here\n\t\t// too (idempotent open).\n\t\tif (this.#socket !== undefined) return\n\t\tthis.#closed = false\n\t\tconst url = this.#httpURL()\n\t\tconst key = randomBytes(16).toString('base64')\n\t\tconst secure = url.protocol === 'https:'\n\t\tconst send = secure ? httpsRequest : httpRequest\n\n\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\tlet settled = false\n\t\t\tconst fail = (error: Error): void => {\n\t\t\t\tif (settled) return\n\t\t\t\tsettled = true\n\t\t\t\treject(error)\n\t\t\t}\n\t\t\tconst request = send({\n\t\t\t\thostname: url.hostname,\n\t\t\t\tport: url.port.length > 0 ? Number(url.port) : secure ? 443 : 80,\n\t\t\t\tpath: `${url.pathname}${url.search}`,\n\t\t\t\theaders: {\n\t\t\t\t\tConnection: 'Upgrade',\n\t\t\t\t\tUpgrade: 'websocket',\n\t\t\t\t\t'Sec-WebSocket-Key': key,\n\t\t\t\t\t'Sec-WebSocket-Version': WEBSOCKET_VERSION,\n\t\t\t\t\t'Sec-WebSocket-Protocol': MCP_WEBSOCKET_SUBPROTOCOL,\n\t\t\t\t\t...this.#headers,\n\t\t\t\t},\n\t\t\t})\n\n\t\t\t// The server accepted the upgrade: validate the handshake accept, then wrap the\n\t\t\t// raw socket in a CLIENT-mode NodeWebSocket (masks its frames).\n\t\t\trequest.on('upgrade', (response: IncomingMessage, socket: Duplex, head: Buffer) => {\n\t\t\t\tconst accept = response.headers['sec-websocket-accept']\n\t\t\t\tif (!isString(accept) || accept !== computeWebSocketAccept(key)) {\n\t\t\t\t\tsocket.destroy()\n\t\t\t\t\tfail(new Error('WebSocket handshake failed: Sec-WebSocket-Accept mismatch'))\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst ws = createNodeWebSocket({ socket, head })\n\t\t\t\tthis.#socket = ws\n\t\t\t\tthis.#bind(ws)\n\t\t\t\tif (!settled) {\n\t\t\t\t\tsettled = true\n\t\t\t\t\tresolve()\n\t\t\t\t}\n\t\t\t})\n\n\t\t\t// A plain (non-101) response means the server declined the upgrade.\n\t\t\trequest.on('response', (response) => {\n\t\t\t\tresponse.resume()\n\t\t\t\tfail(new Error(`WebSocket upgrade declined with status ${response.statusCode ?? 0}`))\n\t\t\t})\n\t\t\t// A connection-level failure (refused, DNS, reset).\n\t\t\trequest.on('error', (error) =>\n\t\t\t\tfail(error instanceof Error ? error : new Error(String(error))),\n\t\t\t)\n\t\t\trequest.end()\n\t\t})\n\t}\n\n\tasync send(message: JSONRPCMessage | readonly JSONRPCMessage[]): Promise<void> {\n\t\tconst socket = this.#socket\n\t\tif (socket === undefined) throw new Error('WebSocket transport is not connected')\n\t\tconst messages = Array.isArray(message) ? message : [message]\n\t\tfor (const one of messages) socket.send(JSON.stringify(one))\n\t}\n\n\tasync close(): Promise<void> {\n\t\tif (this.#closed) return\n\t\tthis.#closed = true\n\t\tconst socket = this.#socket\n\t\tthis.#socket = undefined\n\t\tif (socket !== undefined) socket.close()\n\t\tthis.#emitter.emit('close')\n\t}\n\n\t// Bridge the upgraded socket's events onto the transport: a text frame → `message`\n\t// (decoded + narrowed), the socket close → `close`, a socket fault → `error`.\n\t#bind(ws: NodeWebSocketInterface): void {\n\t\tws.emitter.on('message', (text) => this.#receive(text))\n\t\tws.emitter.on('close', () => this.#onClose())\n\t\tws.emitter.on('error', (error) => this.#emitter.emit('error', error))\n\t}\n\n\t// Decode one inbound text frame: `JSON.parse` → `parseJSONRPCMessage`. A well-formed\n\t// message re-emits on `message`; a malformed / non-message frame surfaces on `error` and\n\t// is dropped (§14 — never throws on adversarial wire input).\n\t#receive(text: string): void {\n\t\tlet parsed: unknown\n\t\ttry {\n\t\t\tparsed = JSON.parse(text)\n\t\t} catch (error) {\n\t\t\tthis.#emitter.emit('error', error)\n\t\t\treturn\n\t\t}\n\t\tconst message = parseJSONRPCMessage(parsed)\n\t\tif (message === undefined) {\n\t\t\tthis.#emitter.emit('error', new Error('non-JSON-RPC WebSocket frame'))\n\t\t\treturn\n\t\t}\n\t\tthis.#emitter.emit('message', message)\n\t}\n\n\t// The socket closed underneath us — fire `close` once (a `close()` call already flipped\n\t// `#closed`, so it does not double-emit).\n\t#onClose(): void {\n\t\tif (this.#closed) return\n\t\tthis.#closed = true\n\t\tthis.#socket = undefined\n\t\tthis.#emitter.emit('close')\n\t}\n\n\t// Normalize `options.url` to the `http(s)` URL the underlying upgrade request uses: a\n\t// `ws://` → `http://`, a `wss://` → `https://`; an `http(s)://` URL passes through. Any\n\t// other scheme throws (a clear boundary error, not a silent mis-dial).\n\t#httpURL(): URL {\n\t\tconst url = new URL(this.#url)\n\t\tif (url.protocol === 'ws:') url.protocol = 'http:'\n\t\telse if (url.protocol === 'wss:') url.protocol = 'https:'\n\t\telse if (url.protocol !== 'http:' && url.protocol !== 'https:') {\n\t\t\tthrow new Error(`unsupported WebSocket URL scheme '${url.protocol}'`)\n\t\t}\n\t\treturn url\n\t}\n}\n","import type { ClientTransportEventMap, ClientTransportInterface, JSONRPCMessage } from '@src/core'\nimport type { EmitterInterface } from '@orkestrel/emitter'\nimport type { StdioClientTransportOptions } from '../types.js'\nimport type { ChildProcessByStdio } from 'node:child_process'\nimport type { Readable, Writable } from 'node:stream'\nimport { spawn } from 'node:child_process'\nimport { Emitter } from '@orkestrel/emitter'\nimport { dispatchLines, extractLines } from '../helpers.js'\n\n/**\n * The stdio CLIENT transport for the Model Context Protocol — a\n * {@link ClientTransportInterface} that drives a CHILD PROCESS MCP server over\n * newline-delimited JSON-RPC on `stdin`/`stdout`, the stdio sibling of {@link\n * import('./HTTPClientTransport.js').HTTPClientTransport} and {@link\n * import('./WebSocketClientTransport.js').WebSocketClientTransport}.\n *\n * @remarks\n * - **Spawns the server.** `start()` runs `node:child_process`'s `spawn(options.command,\n * options.args, { env: options.env, stdio: ['pipe', 'pipe', 'inherit'] })` — the\n * child's `stdin`/`stdout` are piped for the JSON-RPC channel, its `stderr` inherits\n * the parent's (diagnostics pass through, never parsed as protocol).\n * - **Inbound (`message`).** Each `stdout` chunk is folded through the shared\n * {@link extractLines} line-framing helper (buffering a partial trailing line\n * across reads); every complete line is decoded and delivered via the shared\n * {@link dispatchLines} helper — a well-formed {@link JSONRPCMessage} emits\n * `message`, a malformed line emits `error` (§14, never throws). The child's\n * `close` bridges to this transport's `close`.\n * - **Outbound (`send`).** `send(message | messages)` writes ONE newline-terminated\n * `JSON.stringify`d line per message to the child's `stdin`.\n * - **`close()`** kills the child process and fires `close` (idempotent).\n * - **Observable (§13).** Owns the `emitter` ({@link ClientTransportEventMap}); the\n * emitter isolates a listener throw; `error` is a DOMAIN event (a transport-level\n * fault), distinct from the emitter's own listener-error channel.\n *\n * @example\n * ```ts\n * const transport = new StdioClientTransport({ command: 'node', args: ['./server.js'] })\n * const client = new MCPClient({ transport })\n * await client.connect() // start() spawns the child, then the MCP initialize runs over stdio\n * ```\n */\nexport class StdioClientTransport implements ClientTransportInterface {\n\treadonly #emitter: Emitter<ClientTransportEventMap>\n\treadonly #command: string\n\treadonly #args: readonly string[]\n\treadonly #env: Readonly<Record<string, string>> | undefined\n\t#child: ChildProcessByStdio<Writable, Readable, null> | undefined = undefined\n\t#buffer = ''\n\t#closed = false\n\n\tconstructor(options: StdioClientTransportOptions) {\n\t\tthis.#emitter = new Emitter<ClientTransportEventMap>()\n\t\tthis.#command = options.command\n\t\tthis.#args = options.args ?? []\n\t\tthis.#env = options.env\n\t}\n\n\tget emitter(): EmitterInterface<ClientTransportEventMap> {\n\t\treturn this.#emitter\n\t}\n\n\tget session(): string | undefined {\n\t\treturn undefined\n\t}\n\n\tasync start(): Promise<void> {\n\t\t// Already spawned — a second `start()` (e.g. via `connect()`) short-circuits (idempotent).\n\t\tif (this.#child !== undefined) return\n\t\tthis.#closed = false\n\t\tthis.#buffer = ''\n\t\tconst child = spawn(this.#command, [...this.#args], {\n\t\t\tenv: this.#env,\n\t\t\tstdio: ['pipe', 'pipe', 'inherit'],\n\t\t})\n\t\tthis.#child = child\n\t\tchild.stdout.on('data', (chunk: Buffer | string) => this.#receive(chunk.toString()))\n\t\tchild.on('close', () => this.#onClose())\n\t\tchild.on('error', (error) => this.#emitter.emit('error', error))\n\t}\n\n\tasync send(message: JSONRPCMessage | readonly JSONRPCMessage[]): Promise<void> {\n\t\tconst child = this.#child\n\t\tif (child === undefined) throw new Error('stdio transport is not connected')\n\t\tconst messages = Array.isArray(message) ? message : [message]\n\t\tfor (const one of messages) child.stdin.write(`${JSON.stringify(one)}\\n`)\n\t}\n\n\tasync close(): Promise<void> {\n\t\tif (this.#closed) return\n\t\tthis.#closed = true\n\t\tconst child = this.#child\n\t\tthis.#child = undefined\n\t\tif (child !== undefined) child.kill()\n\t\tthis.#emitter.emit('close')\n\t}\n\n\t// Buffer a raw stdout chunk through the shared line-framing helper, then decode + deliver\n\t// every complete line onto this transport's emitter (a partial trailing line carries\n\t// forward to the next chunk).\n\t#receive(chunk: string): void {\n\t\tconst { lines, remainder } = extractLines(this.#buffer, chunk)\n\t\tthis.#buffer = remainder\n\t\tdispatchLines(this.#emitter, lines)\n\t}\n\n\t// The child process closed — fire this transport's `close` once. A `close()` call already\n\t// flipped `#closed`, so a child-driven close after an explicit one does not double-emit.\n\t#onClose(): void {\n\t\tif (this.#closed) return\n\t\tthis.#closed = true\n\t\tthis.#child = undefined\n\t\tthis.#emitter.emit('close')\n\t}\n}\n","import type { ClientTransportEventMap, ClientTransportInterface, JSONRPCMessage } from '@src/core'\nimport type { EmitterInterface } from '@orkestrel/emitter'\nimport { Emitter } from '@orkestrel/emitter'\nimport { dispatchLines, extractLines } from '../helpers.js'\n\n/**\n * The stdio SERVER transport for the Model Context Protocol — wraps an injectable\n * readable/writable stream pair (`process.stdin`/`process.stdout` in production, a\n * test double in tests) as a {@link ClientTransportInterface}, the newline-delimited\n * JSON-RPC channel {@link import('../factories.js').createStdioServer} pumps\n * `mcp.dispatch` over, the stdio mirror of {@link\n * import('./WebSocketServerTransport.js').WebSocketServerTransport}.\n *\n * @remarks\n * - **Reuses `ClientTransportInterface` (§21).** The same generic carrier the HTTP\n * and WebSocket server transports implement — `emitter` (`message` / `close` /\n * `error`), `start`, `send`, `close`. `session` is `undefined` (the stateless v1).\n * - **Inbound (`message`).** `start()` subscribes to `input`'s `data` event; each\n * chunk is folded through the shared {@link extractLines} line-framing helper\n * (buffering a partial trailing line across reads), and every complete line is\n * decoded and delivered via the shared {@link dispatchLines} helper — a\n * well-formed {@link JSONRPCMessage} re-emits on `message`, a malformed line\n * emits `error` (§14, never throws). `input`'s `close` bridges to this\n * transport's `close`.\n * - **Outbound (`send`).** `send(message | messages)` writes ONE newline-terminated\n * `JSON.stringify`d line per message to `output`.\n * - **`close()`** fires this transport's `close` (idempotent) — the injected streams\n * are owned by the caller (typically `process.stdin`/`process.stdout`, which must\n * never be closed out from under the process) and are not torn down here.\n * - **Observable (§13).** Owns the `emitter` ({@link ClientTransportEventMap}); the\n * emitter isolates a listener throw; `error` is a DOMAIN event (a transport-level\n * fault), distinct from the emitter's own listener-error channel.\n */\nexport class StdioServerTransport implements ClientTransportInterface {\n\treadonly #emitter: Emitter<ClientTransportEventMap>\n\treadonly #input: NodeJS.ReadableStream\n\treadonly #output: NodeJS.WritableStream\n\t#buffer = ''\n\t#started = false\n\t#closed = false\n\n\tconstructor(input: NodeJS.ReadableStream, output: NodeJS.WritableStream) {\n\t\tthis.#emitter = new Emitter<ClientTransportEventMap>()\n\t\tthis.#input = input\n\t\tthis.#output = output\n\t}\n\n\tget emitter(): EmitterInterface<ClientTransportEventMap> {\n\t\treturn this.#emitter\n\t}\n\n\tget session(): string | undefined {\n\t\t// The stateless v1 holds no session — a server-assigned id is the deferred tier.\n\t\treturn undefined\n\t}\n\n\tasync start(): Promise<void> {\n\t\t// Arm the stream subscriptions once: an input chunk decodes to `message`, the input's\n\t\t// close bridges to this transport's `close`. Idempotent — a second `start` is a no-op\n\t\t// (the single MCPServer pump subscribes once).\n\t\tif (this.#started || this.#closed) return\n\t\tthis.#started = true\n\t\tthis.#input.on('data', (chunk: Buffer | string) => this.#receive(chunk.toString()))\n\t\tthis.#input.on('close', () => this.#onClose())\n\t\tthis.#input.on('error', (error) => this.#emitter.emit('error', error))\n\t}\n\n\tasync send(message: JSONRPCMessage | readonly JSONRPCMessage[]): Promise<void> {\n\t\tconst messages = Array.isArray(message) ? message : [message]\n\t\tfor (const one of messages) this.#output.write(`${JSON.stringify(one)}\\n`)\n\t}\n\n\tasync close(): Promise<void> {\n\t\tif (this.#closed) return\n\t\tthis.#closed = true\n\t\tthis.#emitter.emit('close')\n\t}\n\n\t// Buffer a raw input chunk through the shared line-framing helper, then decode + deliver\n\t// every complete line onto this transport's emitter (a partial trailing line carries\n\t// forward to the next chunk).\n\t#receive(chunk: string): void {\n\t\tconst { lines, remainder } = extractLines(this.#buffer, chunk)\n\t\tthis.#buffer = remainder\n\t\tdispatchLines(this.#emitter, lines)\n\t}\n\n\t// The input stream closed (EOF, peer teardown) — fire this transport's `close` once. A\n\t// `close()` call already flipped `#closed`, so a stream-driven close after an explicit one\n\t// does not double-emit.\n\t#onClose(): void {\n\t\tif (this.#closed) return\n\t\tthis.#closed = true\n\t\tthis.#emitter.emit('close')\n\t}\n}\n","import type { ClientTransportInterface, MCPServerInterface } from '@src/core'\nimport type { RouteInput } from '@orkestrel/router'\nimport type { UpgradeHandler } from '@orkestrel/server'\nimport type {\n\tHTTPClientTransportOptions,\n\tHTTPTransportOptions,\n\tStdioClientTransportOptions,\n\tStdioServerOptions,\n\tWebSocketClientTransportOptions,\n\tWebSocketServerOptions,\n} from './types.js'\nimport type { IncomingMessage } from 'node:http'\nimport type { Duplex } from 'node:stream'\nimport {\n\tisJSONRPCRequest,\n\tJSONRPC_INVALID_REQUEST,\n\tJSONRPC_PARSE_ERROR,\n\tjsonRPCError,\n\tparseJSONRPCMessage,\n} from '@src/core'\nimport { isString } from '@orkestrel/contract'\nimport { openStream } from '@orkestrel/server'\nimport { createNodeWebSocket, WEBSOCKET_VERSION } from '@orkestrel/websocket'\nimport { DEFAULT_MCP_PATH, MCP_WEBSOCKET_SUBPROTOCOL } from './constants.js'\nimport { acceptsEventStream, upgradeRequestPath } from './helpers.js'\nimport { HTTPClientTransport } from './transports/HTTPClientTransport.js'\nimport { StdioClientTransport } from './transports/StdioClientTransport.js'\nimport { StdioServerTransport } from './transports/StdioServerTransport.js'\nimport { WebSocketClientTransport } from './transports/WebSocketClientTransport.js'\nimport { WebSocketServerTransport } from './transports/WebSocketServerTransport.js'\n\n/**\n * Create the MCP Streamable-HTTP transport routes — mounts a transport-agnostic\n * {@link MCPServerInterface} (the `@src/core` dispatch core) on the fetch-standard router\n * spine, pumping each `POST` body through `mcp.dispatch`. Returns the {@link RouteInput}s to\n * hand to `router.add(...)`.\n *\n * @remarks\n * A SINGLE `POST {path}` route — `createMCPRoutes` is STATELESS. The handler reads its own\n * request body (its own JSON parse try/catch), so it works with or without a session\n * middleware mounted in front. It draws a sharp line between TRANSPORT-level and\n * DISPATCH-level outcomes:\n *\n * - A **transport** failure — a malformed JSON body, or a parsed value that is not a\n * JSON-RPC REQUEST — is an HTTP `400` carrying a JSON-RPC error BODY (`-32700` Parse\n * error / `-32600` Invalid Request, id `null`).\n * - A **dispatch** result — a success OR an IN-BAND JSON-RPC error from `mcp.dispatch`\n * (e.g. `-32601` method-not-found) — is an HTTP `200` carrying the JSON-RPC response\n * envelope (the error is in-band per JSON-RPC, NOT an HTTP error).\n * - A **notification** (a request with no `id`, which `dispatch` resolves to\n * `undefined`) is a `202 Accepted` with no body.\n *\n * When `streaming` is enabled (the default) and the client `Accept`s `text/event-stream`,\n * the `200` reply is framed as a Streamable-HTTP SSE response (one `data:` event carrying\n * the JSON-RPC envelope, then the stream ends) via `@orkestrel/server`'s generic\n * {@link import('@orkestrel/server').openStream} seam; otherwise it is a plain JSON body.\n *\n * **Sessions are a SEPARATE, plug-and-play middleware.** `createMCPRoutes` mints / reads no\n * session id. To make the transport STATEFUL, mount {@link\n * import('./middlewares.js').createMCPSession} IN FRONT — it owns the same `path`, mints +\n * validates the `mcp-session-id`, and serves the resumable `GET {path}` + `DELETE {path}`,\n * leaving this route to dispatch the validated `POST`.\n *\n * This is MECHANISM, not policy: compose auth / CORS / rate-limiting (and the session\n * middleware) IN FRONT as ordinary middleware — the transport route adds none.\n *\n * @typeParam TState - The consumer's opaque per-request state type\n * @param mcp - The transport-agnostic {@link MCPServerInterface} to expose over HTTP\n * @param options - Optional `path` (default {@link DEFAULT_MCP_PATH}) and `streaming`\n * (default `true`); see {@link HTTPTransportOptions}\n * @returns The {@link RouteInput}s to register with the router\n *\n * @example\n * ```ts\n * import { createMCPServer, createToolManager } from '@src/core'\n * import { createMCPRoutes } from '@src/server'\n *\n * const mcp = createMCPServer({ name: 'docs', version: '1.0.0', tools: createToolManager() })\n * const routes = createMCPRoutes(mcp) // POST /mcp dispatches JSON-RPC (JSON or SSE per Accept)\n * ```\n */\nexport function createMCPRoutes<TState = unknown>(\n\tmcp: MCPServerInterface,\n\toptions?: HTTPTransportOptions,\n): readonly RouteInput<string, TState>[] {\n\tconst path = options?.path ?? DEFAULT_MCP_PATH\n\tconst streaming = options?.streaming ?? true\n\tconst post: RouteInput<string, TState> = {\n\t\tmethod: 'POST',\n\t\tpath,\n\t\tname: 'mcp',\n\t\thandler: async (request) => {\n\t\t\tlet text: string\n\t\t\ttry {\n\t\t\t\ttext = await request.text()\n\t\t\t} catch {\n\t\t\t\t// A malformed JSON body is a TRANSPORT failure — HTTP 400 + a JSON-RPC -32700.\n\t\t\t\treturn Response.json(jsonRPCError(null, JSONRPC_PARSE_ERROR, 'Parse error'), {\n\t\t\t\t\tstatus: 400,\n\t\t\t\t})\n\t\t\t}\n\t\t\tlet parsed: unknown\n\t\t\ttry {\n\t\t\t\tparsed = JSON.parse(text)\n\t\t\t} catch {\n\t\t\t\treturn Response.json(jsonRPCError(null, JSONRPC_PARSE_ERROR, 'Parse error'), {\n\t\t\t\t\tstatus: 400,\n\t\t\t\t})\n\t\t\t}\n\t\t\tconst rpcRequest = parseJSONRPCMessage(parsed)\n\t\t\tif (rpcRequest === undefined || !('method' in rpcRequest)) {\n\t\t\t\t// Not a JSON-RPC request (a response, or any non-message) — HTTP 400 + -32600.\n\t\t\t\t// `'method' in rpcRequest` narrows the message union to `JSONRPCRequest` (no `as`).\n\t\t\t\treturn Response.json(jsonRPCError(null, JSONRPC_INVALID_REQUEST, 'Invalid Request'), {\n\t\t\t\t\tstatus: 400,\n\t\t\t\t})\n\t\t\t}\n\t\t\tconst response = await mcp.dispatch(rpcRequest)\n\t\t\tif (response === undefined) {\n\t\t\t\t// A notification (no `id`) yields no response — 202 Accepted, no body.\n\t\t\t\treturn new Response(null, { status: 202 })\n\t\t\t}\n\t\t\tif (streaming && acceptsEventStream(request)) {\n\t\t\t\t// Streamable-HTTP SSE response: one `data:` event with the JSON-RPC envelope, then end.\n\t\t\t\tconst s = openStream()\n\t\t\t\ts.write({ data: JSON.stringify(response) })\n\t\t\t\ts.end()\n\t\t\t\treturn s.response\n\t\t\t}\n\t\t\t// A dispatch result — success OR an in-band JSON-RPC error — is HTTP 200 + the envelope.\n\t\t\treturn Response.json(response)\n\t\t},\n\t}\n\treturn [post]\n}\n\n/**\n * Create the HTTP CLIENT transport for an {@link import('@src/core').MCPClientInterface}\n * — a {@link ClientTransportInterface} that drives a REMOTE Streamable-HTTP MCP server\n * over `fetch`. The egress mirror of {@link createMCPRoutes}.\n *\n * @remarks\n * Hand it to `createMCPClient({ transport })`: each JSON-RPC message the client sends is\n * `POST`ed to `options.url` with `content-type: application/json` and an `Accept` of\n * both `application/json` and `text/event-stream` (the server answers with EITHER — a\n * plain JSON envelope or a Streamable-HTTP SSE `data:` event, decoded via `@orkestrel/sse`),\n * and the reply is surfaced on the transport's `message` event for the client's id\n * correlation. Add `options.headers` (e.g. an `Authorization` bearer) to reach a guarded\n * server. `start` / `close` hold no connection; against a STATEFUL server it captures the\n * `mcp-session-id` from `initialize` and echoes it on later requests, so the same\n * `MCPClient` passes session validation (a stateless server sends none).\n *\n * @param options - `url` (the remote endpoint; REQUIRED), optional `headers` merged onto\n * every request, optional `fetch` (default `globalThis.fetch`), and optional `timeout`\n * (ms, applied via `AbortSignal.timeout`); see {@link HTTPClientTransportOptions}\n * @returns A working {@link ClientTransportInterface} over `fetch`\n *\n * @example\n * ```ts\n * import { createMCPClient } from '@src/core'\n * import { createHTTPClientTransport } from '@src/server'\n *\n * const client = createMCPClient({\n * \ttransport: createHTTPClientTransport({ url: 'http://localhost:3000/mcp' }),\n * })\n * await client.connect()\n * const tools = await client.tools()\n * ```\n */\nexport function createHTTPClientTransport(\n\toptions: HTTPClientTransportOptions,\n): ClientTransportInterface {\n\treturn new HTTPClientTransport(options)\n}\n\n/**\n * Create the MCP WebSocket transport INGRESS — an {@link UpgradeHandler} that exposes a\n * transport-agnostic {@link MCPServerInterface} over a WebSocket, the WebSocket mirror of\n * {@link createMCPRoutes}. Register it on the spine's upgrade seam.\n *\n * @remarks\n * It composes the lean RFC 6455 `@orkestrel/websocket` wrapper over `@orkestrel/server`'s\n * generic upgrade seam — the spine speaks no WebSocket, this handler does.\n *\n * - **Declines (returns `false`)** when the upgrade is not for it, so the spine fans the\n * socket to the next handler (or destroys an unclaimed one): the `Upgrade` header is not\n * `websocket`, the request path is not `options.path` (default {@link DEFAULT_MCP_PATH},\n * `'/mcp'`), the `Sec-WebSocket-Key` is absent, or the `Sec-WebSocket-Version` is not `13`.\n * A decline NEVER writes to the socket (it is not yet ours) — the spine owns the unclaimed\n * outcome.\n * - **Claims (returns `true`)** otherwise: it builds `createNodeWebSocket({ socket, key, head,\n * protocol })` (SERVER mode → writes the `101` handshake, echoing the `subprotocol`, default\n * {@link MCP_WEBSOCKET_SUBPROTOCOL} `'mcp'`, and sends UNMASKED frames), wraps it in a\n * {@link WebSocketServerTransport}, and PUMPS: each inbound {@link\n * import('@src/core').JSONRPCMessage} that is a REQUEST runs through `mcp.dispatch`, and a\n * defined response is written back as a frame — a NOTIFICATION (`dispatch` → `undefined`)\n * sends nothing. A non-request message (a stray response) is ignored. The dispatch is\n * guarded so a `dispatch` / `send` fault surfaces on the transport's `error` event rather\n * than escaping the (async) message listener.\n *\n * It is MECHANISM, not policy: compose an auth guard IN FRONT by registering an upgrade\n * handler BEFORE this one — that handler can claim (decline + destroy) an unauthenticated\n * upgrade so it never reaches this pump.\n *\n * @param mcp - The transport-agnostic {@link MCPServerInterface} to expose over WebSocket\n * @param options - Optional `path` (default {@link DEFAULT_MCP_PATH}) and `subprotocol`\n * (default {@link MCP_WEBSOCKET_SUBPROTOCOL}); see {@link WebSocketServerOptions}\n * @returns An {@link UpgradeHandler} to register with the spine's `upgrade` seam\n *\n * @example\n * ```ts\n * import { createMCPServer, createToolManager } from '@src/core'\n * import { createWebSocketServer } from '@src/server'\n *\n * const mcp = createMCPServer({ name: 'docs', version: '1.0.0', tools: createToolManager() })\n * server.upgrade(createWebSocketServer(mcp)) // an MCP client now connects over ws://…/mcp\n * ```\n */\nexport function createWebSocketServer(\n\tmcp: MCPServerInterface,\n\toptions?: WebSocketServerOptions,\n): UpgradeHandler {\n\tconst path = options?.path ?? DEFAULT_MCP_PATH\n\tconst subprotocol = options?.subprotocol ?? MCP_WEBSOCKET_SUBPROTOCOL\n\treturn (request: IncomingMessage, socket: Duplex, head: Buffer): boolean => {\n\t\t// DECLINE anything that is not our MCP WebSocket upgrade — the spine fans it onward or\n\t\t// destroys it. Never touch the socket on a decline (it is not ours yet).\n\t\tconst upgrade = request.headers['upgrade']\n\t\tif (!isString(upgrade) || upgrade.toLowerCase() !== 'websocket') return false\n\t\tif (upgradeRequestPath(request) !== path) return false\n\t\tconst key = request.headers['sec-websocket-key']\n\t\tif (!isString(key)) return false\n\t\tconst version = request.headers['sec-websocket-version']\n\t\tif (!isString(version) || version !== WEBSOCKET_VERSION) return false\n\n\t\t// CLAIM: the wrapper writes the `101` handshake (server mode) and the transport pumps\n\t\t// each request through `mcp.dispatch`, writing back a defined response (a notification\n\t\t// sends nothing). A dispatch / send fault surfaces on the transport `error` event.\n\t\tconst ws = createNodeWebSocket({ socket, key, head, protocol: subprotocol })\n\t\tconst transport = new WebSocketServerTransport(ws)\n\t\ttransport.emitter.on('message', (message) => {\n\t\t\tif (!isJSONRPCRequest(message)) return\n\t\t\tvoid (async () => {\n\t\t\t\ttry {\n\t\t\t\t\tconst response = await mcp.dispatch(message)\n\t\t\t\t\tif (response !== undefined) await transport.send(response)\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// Surface a dispatch / send fault on the transport's `error` event — but a\n\t\t\t\t\t// user `'error'` listener that itself throws would (Emitter.emit rethrows the\n\t\t\t\t\t// first listener throw) escape this `void` async listener as an UNHANDLED\n\t\t\t\t\t// rejection and, on Node ≥15, can terminate the process. Swallow that here:\n\t\t\t\t\t// a buggy observer must never crash the server (§13).\n\t\t\t\t\ttry {\n\t\t\t\t\t\ttransport.emitter.emit('error', error)\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// A throwing `error` listener is the caller's own bug — the end of the line.\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t})()\n\t\t})\n\t\tvoid transport.start()\n\t\treturn true\n\t}\n}\n\n/**\n * Create the WebSocket CLIENT transport for an {@link import('@src/core').MCPClientInterface}\n * — a {@link ClientTransportInterface} that drives a REMOTE MCP server over a WebSocket. The\n * egress mirror of {@link createWebSocketServer} and the WebSocket sibling of {@link\n * createHTTPClientTransport}.\n *\n * @remarks\n * Hand it to `createMCPClient({ transport })`: `start()` (run by `client.connect()`) performs\n * the RFC 6455 client handshake against `options.url` (accepting a `ws://` / `wss://` or an\n * `http://` / `https://` URL — a `ws(s)` scheme is converted to `http(s)` for the underlying\n * upgrade request), validates the `Sec-WebSocket-Accept` (via `@orkestrel/websocket`'s\n * `computeWebSocketAccept`), and opens a persistent bidirectional frame channel; each JSON-RPC\n * message the client `send`s is written as one masked text frame, and each decoded reply is\n * surfaced on the transport's `message` event for the client's id correlation. Add\n * `options.headers` (e.g. an `Authorization` bearer) to reach a guarded server.\n *\n * @param options - `url` (the remote WebSocket endpoint; REQUIRED) and optional `headers`\n * merged onto the upgrade request; see {@link WebSocketClientTransportOptions}\n * @returns A working {@link ClientTransportInterface} over a WebSocket\n *\n * @example\n * ```ts\n * import { createMCPClient } from '@src/core'\n * import { createWebSocketClientTransport } from '@src/server'\n *\n * const client = createMCPClient({\n * \ttransport: createWebSocketClientTransport({ url: 'ws://localhost:3000/mcp' }),\n * })\n * await client.connect()\n * const tools = await client.tools()\n * ```\n */\nexport function createWebSocketClientTransport(\n\toptions: WebSocketClientTransportOptions,\n): ClientTransportInterface {\n\treturn new WebSocketClientTransport(options)\n}\n\n/**\n * Create the stdio CLIENT transport for an {@link import('@src/core').MCPClientInterface}\n * — a {@link ClientTransportInterface} that spawns and drives a CHILD PROCESS MCP server\n * over newline-delimited JSON-RPC on `stdin`/`stdout`, the stdio sibling of {@link\n * createHTTPClientTransport} and {@link createWebSocketClientTransport}.\n *\n * @remarks\n * Hand it to `createMCPClient({ transport })`: `start()` (run by `client.connect()`)\n * spawns `options.command` with `options.args` and `options.env`, piping its\n * `stdin`/`stdout` for the JSON-RPC channel (its `stderr` inherits the parent's for\n * diagnostics). Each JSON-RPC message the client `send`s is written as one\n * newline-terminated line to the child's `stdin`; each decoded reply line from the\n * child's `stdout` is surfaced on the transport's `message` event for the client's\n * id correlation.\n *\n * @param options - `command` (the executable to spawn; REQUIRED), optional `args`,\n * and optional `env`; see {@link StdioClientTransportOptions}\n * @returns A working {@link ClientTransportInterface} over a child process's stdio\n *\n * @example\n * ```ts\n * import { createMCPClient } from '@src/core'\n * import { createStdioClientTransport } from '@src/server'\n *\n * const client = createMCPClient({\n * \ttransport: createStdioClientTransport({ command: 'node', args: ['./server.js'] }),\n * })\n * await client.connect()\n * const tools = await client.tools()\n * ```\n */\nexport function createStdioClientTransport(\n\toptions: StdioClientTransportOptions,\n): ClientTransportInterface {\n\treturn new StdioClientTransport(options)\n}\n\n/**\n * Create the MCP stdio transport INGRESS — pumps a transport-agnostic {@link\n * MCPServerInterface} over newline-delimited JSON-RPC on `stdin`/`stdout` (or an\n * injected stream pair), the stdio mirror of {@link createWebSocketServer}.\n *\n * @remarks\n * Wraps `options.input` (default `process.stdin`) / `options.output` (default\n * `process.stdout`) in a {@link import('./transports/StdioServerTransport.js').StdioServerTransport}\n * and PUMPS: each inbound {@link import('@src/core').JSONRPCMessage} that is a\n * REQUEST runs through `mcp.dispatch`, and a defined response is written back as a\n * newline-terminated line — a NOTIFICATION (`dispatch` → `undefined`) writes\n * nothing. A non-request message is ignored. The dispatch is guarded so a\n * `dispatch` / `send` fault surfaces on the transport's `error` event rather than\n * escaping the (async) message listener.\n *\n * @param mcp - The transport-agnostic {@link MCPServerInterface} to expose over stdio\n * @param options - Optional injectable `input` / `output` streams; see\n * {@link StdioServerOptions}\n * @returns A `{ start(): void; stop(): void }` handle to arm / tear down the pump\n *\n * @example\n * ```ts\n * import { createMCPServer, createToolManager } from '@src/core'\n * import { createStdioServer } from '@src/server'\n *\n * const mcp = createMCPServer({ name: 'docs', version: '1.0.0', tools: createToolManager() })\n * createStdioServer(mcp).start() // an MCP client now connects over this process's stdio\n * ```\n */\nexport function createStdioServer(\n\tmcp: MCPServerInterface,\n\toptions?: StdioServerOptions,\n): { start(): void; stop(): void } {\n\tconst input = options?.input ?? process.stdin\n\tconst output = options?.output ?? process.stdout\n\tconst transport = new StdioServerTransport(input, output)\n\ttransport.emitter.on('message', (message) => {\n\t\tif (!isJSONRPCRequest(message)) return\n\t\tvoid (async () => {\n\t\t\ttry {\n\t\t\t\tconst response = await mcp.dispatch(message)\n\t\t\t\tif (response !== undefined) await transport.send(response)\n\t\t\t} catch (error) {\n\t\t\t\t// Surface a dispatch / send fault on the transport's `error` event — but a user\n\t\t\t\t// `'error'` listener that itself throws would (Emitter.emit rethrows the first\n\t\t\t\t// listener throw) escape this `void` async listener as an unhandled rejection.\n\t\t\t\t// Swallow that here: a buggy observer must never crash the server (§13).\n\t\t\t\ttry {\n\t\t\t\t\ttransport.emitter.emit('error', error)\n\t\t\t\t} catch {\n\t\t\t\t\t// A throwing `error` listener is the caller's own bug — the end of the line.\n\t\t\t\t}\n\t\t\t}\n\t\t})()\n\t})\n\treturn {\n\t\tstart(): void {\n\t\t\tvoid transport.start()\n\t\t},\n\t\tstop(): void {\n\t\t\tvoid transport.close()\n\t\t},\n\t}\n}\n","import type { MiddlewareHandler } from '@orkestrel/server'\nimport type { MCPSessionEntry, MCPSessionOptions, MCPSessionState } from './types.js'\nimport { isInitializeRequest, parseJSONRPCMessage } from '@src/core'\nimport { openStream } from '@orkestrel/server'\nimport { DEFAULT_MCP_PATH, MCP_SESSION_HEADER } from './constants.js'\nimport { readLastEventId, readSessionHeader, rejectUnknownSession } from './helpers.js'\nimport { MCPSession } from './MCPSession.js'\n\n/**\n * Create the native MCP session {@link MiddlewareHandler} — the plug-and-play stateful layer\n * that fronts a session-agnostic {@link import('./factories.js').createMCPRoutes}. Compose it\n * via `router.use(createMCPSession())` (or the equivalent middleware seam), mirroring any\n * other closure-scoped stateful middleware. Has NO dependency on `@orkestrel/middleware` — the\n * session store, mint-on-`initialize`, and resumable stream are all native to this package.\n *\n * @remarks\n * Owns a closure `Map<string, MCPSessionEntry>` keyed by session id, and a single request\n * `path` (default {@link DEFAULT_MCP_PATH}); a request to any other path passes straight\n * through (`next()`).\n *\n * - **`POST {path}`.** Buffers `const text = await request.text()` (so the downstream route\n * can re-read it via a freshly-built forwarded `Request`). Resolves a session via {@link\n * readSessionHeader}: a VALID id touches the entry and sets `context.state.session`; an\n * ABSENT / unknown id whose (guarded) body parses to an `initialize` request ({@link\n * isInitializeRequest}) MINTS a fresh {@link MCPSession} (`crypto.randomUUID()`, `capacity`)\n * and sets `context.state.session`; neither → {@link rejectUnknownSession} (`404`). It then\n * FORWARDS a fresh `Request` carrying the buffered `text` (`next(forwarded)`) — never the\n * already-consumed original — so the route re-reads the same body, and stamps the response\n * with {@link MCP_SESSION_HEADER}.\n * - **`GET {path}`.** Resolves the session the same way (no mint — only `initialize` mints);\n * an invalid / unknown id is the same `404`. A valid session opens the resumable\n * server→client stream via `@orkestrel/server`'s {@link import('@orkestrel/server').openStream}:\n * replays every event after the client's `Last-Event-ID` ({@link readLastEventId}) BEFORE\n * attaching the stream for live pushes, then attaches; a client disconnect (`request.signal`)\n * detaches it. Long-lived — never `end()`ed here.\n * - **`DELETE {path}`.** Resolves the session; a valid id deletes it from the store and answers\n * `204`; an invalid / unknown id is the same `404`.\n *\n * It is MECHANISM, not policy, and ADDITIVE: omit it entirely for the stateless default\n * ({@link import('./factories.js').createMCPRoutes}'s only behavior). The `path` MUST match the\n * `createMCPRoutes` `path` it fronts. The WebSocket transport is inherently one session per\n * connection (the socket IS the session), so this middleware does not apply to it.\n *\n * @typeParam TState - The consumer's `TState`, which MUST extend {@link MCPSessionState} so\n * the resolved session can be threaded through `context.state.session`\n * @param options - Optional `path` (default {@link DEFAULT_MCP_PATH}), `ttl` (idle-session\n * sweep window, ms — omit for sessions that live until an explicit `DELETE`), `capacity`\n * (the folded per-session replay-log bound), and `clock` (the deterministic epoch-ms clock;\n * defaults to `Date.now`); see {@link MCPSessionOptions}\n * @returns A {@link MiddlewareHandler} that mints / validates sessions + serves the resumable\n * `GET` / `DELETE`\n *\n * @example\n * ```ts\n * import { createMCPServer, createToolManager } from '@src/core'\n * import { createMCPRoutes, createMCPSession } from '@src/server'\n *\n * const mcp = createMCPServer({ name: 'docs', version: '1.0.0', tools: createToolManager() })\n * router.use(createMCPSession({ ttl: 60_000 })) // stateful: mint + validate + resumable GET / DELETE\n * router.add(createMCPRoutes(mcp)) // the route stays session-agnostic\n * ```\n */\nexport function createMCPSession<TState extends MCPSessionState>(\n\toptions?: MCPSessionOptions,\n): MiddlewareHandler<TState> {\n\tconst path = options?.path ?? DEFAULT_MCP_PATH\n\tconst capacity = options?.capacity\n\tconst ttl = options?.ttl\n\tconst clock = options?.clock ?? Date.now\n\tconst store = new Map<string, MCPSessionEntry>()\n\n\treturn async (request, context, next) => {\n\t\tif (context.url.pathname !== path) return next()\n\t\tsweep()\n\n\t\tif (context.method === 'GET') {\n\t\t\tconst entry = resolve(request)\n\t\t\tif (entry === undefined) return rejectUnknownSession()\n\t\t\tconst stream = openStream()\n\t\t\t// A comment write flushes the response headers immediately (the underlying node:http\n\t\t\t// response only sends headers on its first `write`/`end`) — without it a client's fetch\n\t\t\t// hangs waiting for headers until the first replay/push write, which may never come.\n\t\t\tstream.comment('open')\n\t\t\tconst lastEventId = readLastEventId(request)\n\t\t\tif (lastEventId !== undefined) {\n\t\t\t\t// Replay every event STRICTLY AFTER the client's last-seen id BEFORE attaching, so the\n\t\t\t\t// missed events arrive in order ahead of any live push.\n\t\t\t\tfor (const e of entry.session.replay(lastEventId)) {\n\t\t\t\t\tstream.write({ id: e.id, data: JSON.stringify(e.message) })\n\t\t\t\t}\n\t\t\t}\n\t\t\tentry.session.attach(stream)\n\t\t\tif (request.signal.aborted) entry.session.detach(stream)\n\t\t\telse\n\t\t\t\trequest.signal.addEventListener('abort', () => entry.session.detach(stream), { once: true })\n\t\t\treturn stream.response\n\t\t}\n\n\t\tif (context.method === 'DELETE') {\n\t\t\tconst id = readSessionHeader(request)\n\t\t\tif (id === undefined || !store.has(id)) return rejectUnknownSession()\n\t\t\tstore.delete(id)\n\t\t\treturn new Response(null, { status: 204 })\n\t\t}\n\n\t\t// POST — buffer the body once so the downstream route can re-read it via a forwarded\n\t\t// Request; only `initialize` mints a fresh session when no valid id is present.\n\t\tconst text = await request.text()\n\t\tlet entry = resolve(request)\n\t\tif (entry === undefined) {\n\t\t\tlet parsed: unknown\n\t\t\ttry {\n\t\t\t\tparsed = parseJSONRPCMessage(JSON.parse(text))\n\t\t\t} catch {\n\t\t\t\tparsed = undefined\n\t\t\t}\n\t\t\tif (parsed !== undefined && isInitializeRequest(parsed)) {\n\t\t\t\tconst session = new MCPSession(crypto.randomUUID(), { capacity })\n\t\t\t\tentry = { session, touched: clock() }\n\t\t\t\tstore.set(session.id, entry)\n\t\t\t} else {\n\t\t\t\treturn rejectUnknownSession()\n\t\t\t}\n\t\t}\n\t\tcontext.state.session = entry.session\n\t\tconst forwarded = new Request(context.url, {\n\t\t\tmethod: 'POST',\n\t\t\theaders: request.headers,\n\t\t\tbody: text,\n\t\t})\n\t\tconst response = await next(forwarded)\n\t\tresponse.headers.set(MCP_SESSION_HEADER, entry.session.id)\n\t\treturn response\n\t}\n\n\t// Resolve + touch the session named by the request's `mcp-session-id` header, or\n\t// `undefined` when the header is absent or names an unknown / evicted session.\n\tfunction resolve(request: Request): MCPSessionEntry | undefined {\n\t\tconst id = readSessionHeader(request)\n\t\tif (id === undefined) return undefined\n\t\tconst entry = store.get(id)\n\t\tif (entry === undefined) return undefined\n\t\tentry.touched = clock()\n\t\treturn entry\n\t}\n\n\t// Lazy idle-TTL sweep — no background timer (the rate-limiter lazy-window idiom): drop\n\t// every session not touched within `ttl` on the next access. Omitted entirely (a no-op)\n\t// when `ttl` is unset — sessions then live until an explicit `DELETE`.\n\tfunction sweep(): void {\n\t\tif (ttl === undefined) return\n\t\tconst cutoff = clock() - ttl\n\t\tfor (const [id, entry] of store) {\n\t\t\tif (entry.touched <= cutoff) store.delete(id)\n\t\t}\n\t}\n}\n"],"mappings":"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"}
@@ -11,7 +11,7 @@ import { MiddlewareHandler } from '@orkestrel/server';
11
11
  import { NodeWebSocketInterface } from '@orkestrel/websocket';
12
12
  import { RouteInput } from '@orkestrel/router';
13
13
  import { StreamInterface } from '@orkestrel/server';
14
- import { UpgradeHandler } from '@orkestrel/server/server';
14
+ import { UpgradeHandler } from '@orkestrel/server';
15
15
 
16
16
  /**
17
17
  * Whether the request's `Accept` header opts into a Server-Sent-Events response.
@@ -11,7 +11,7 @@ import { MiddlewareHandler } from '@orkestrel/server';
11
11
  import { NodeWebSocketInterface } from '@orkestrel/websocket';
12
12
  import { RouteInput } from '@orkestrel/router';
13
13
  import { StreamInterface } from '@orkestrel/server';
14
- import { UpgradeHandler } from '@orkestrel/server/server';
14
+ import { UpgradeHandler } from '@orkestrel/server';
15
15
 
16
16
  /**
17
17
  * Whether the request's `Accept` header opts into a Server-Sent-Events response.
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["#emitter","#url","#headers","#fetch","#timeout","#session","#deliver","#id","#events","#streams","#capacity","#ttl","#append","#evict","#counter","#emitter","#socket","#started","#closed","#receive","#onClose","#emitter","#url","#headers","#socket","#closed","#httpURL","#bind","#receive","#onClose","#emitter","#command","#args","#env","#child","#closed","#buffer","#receive","#onClose","#emitter","#input","#output","#started","#closed","#receive","#onClose","#buffer"],"sources":["../../../src/server/constants.ts","../../../src/server/helpers.ts","../../../src/server/transports/HTTPClientTransport.ts","../../../src/server/MCPSession.ts","../../../src/server/transports/WebSocketServerTransport.ts","../../../src/server/transports/WebSocketClientTransport.ts","../../../src/server/transports/StdioClientTransport.ts","../../../src/server/transports/StdioServerTransport.ts","../../../src/server/factories.ts","../../../src/server/middlewares.ts"],"sourcesContent":["// The MCP HTTP-transport constants (AGENTS §5 constants file) — the wire-level header\n// names, the default mount path, and the folded event-log bounds. The HEADER names are\n// the Streamable-HTTP transport's session /\n// protocol-version headers: they go LIVE when a `createMCPSession` middleware is mounted\n// (it mints the session id into `MCP_SESSION_HEADER` on `initialize` and reads it back on\n// subsequent requests); the stateless `createMCPRoutes` default neither sets nor reads\n// them. The transport-agnostic dispatch core (`src/core/mcp`) deliberately does NOT carry\n// these — header names belong to the HTTP transport, here.\n\n/**\n * The Streamable-HTTP transport header that carries the MCP session id. When a {@link\n * import('./middlewares.js').createMCPSession} middleware is mounted, it SETS this header on\n * the `initialize` response (the minted id) and READS it on every subsequent request\n * (validating the session); the stateless `createMCPRoutes` default neither sets nor reads it.\n */\nexport const MCP_SESSION_HEADER = 'mcp-session-id'\n\n/**\n * The Streamable-HTTP transport header that carries the negotiated MCP protocol version\n * on a subsequent request. The version is negotiated in the `initialize` JSON-RPC result\n * body; a stateful transport MAY additionally read this header to pin the per-request\n * protocol version (optional — the result body remains the source of truth).\n */\nexport const MCP_PROTOCOL_VERSION_HEADER = 'mcp-protocol-version'\n\n/** The default request path `createMCPRoutes` mounts the transport's `POST` route at. */\nexport const DEFAULT_MCP_PATH = '/mcp'\n\n/**\n * The WebSocket subprotocol the MCP-over-WebSocket transports negotiate — sent by the\n * client in `Sec-WebSocket-Protocol`, echoed by the server in its `101` handshake.\n *\n * @remarks\n * `createWebSocketServer` echoes it in the upgrade response and `createWebSocketClientTransport`\n * requests it, so an MCP WebSocket endpoint is distinguishable from any other WebSocket on the\n * same path. The default WebSocket upgrade path is {@link DEFAULT_MCP_PATH} (the same `'/mcp'`\n * the HTTP transport mounts at) — the upgrade is selected by the `Upgrade: websocket` header,\n * not a separate path.\n */\nexport const MCP_WEBSOCKET_SUBPROTOCOL = 'mcp'\n\n/**\n * The default capacity of a session's FOLDED resumable event log (the per-{@link\n * import('./MCPSession.js').MCPSession} replay log) — the maximum number of pushed\n * server→client messages retained for replay before the OLDEST is evicted.\n *\n * @remarks\n * Bounds the replay log's memory: only the most-recent {@link DEFAULT_MCP_SESSION_CAPACITY}\n * pushes are retained, so a client reconnecting with a `Last-Event-ID` older than that window\n * replays nothing (its cursor fell off the back). Override per `createMCPSession`'s `capacity`\n * for a deeper / shallower window.\n */\nexport const DEFAULT_MCP_SESSION_CAPACITY = 1024\n\n/**\n * The default per-event idle lifetime (ms) of a session's folded resumable event log — an\n * entry older than this is lazily evicted on the next access (no background timer), bounding\n * how far back a reconnecting client may replay.\n *\n * @remarks\n * Five minutes — a generous reconnection window for a dropped SSE stream without retaining\n * stale pushes indefinitely. The session's own idle TTL is the `createMCPSession` `ttl` knob;\n * this bounds the replay log paired with it.\n */\nexport const DEFAULT_MCP_SESSION_TTL = 300_000\n","import type { ClientTransportEventMap, JSONRPCMessage } from '@src/core'\nimport type { EmitterInterface } from '@orkestrel/emitter'\nimport type { SSEParserInterface } from '@orkestrel/sse'\nimport type { IncomingMessage } from 'node:http'\nimport type { LineExtraction } from './types.js'\nimport { createSSEParser } from '@orkestrel/sse'\nimport { JSONRPC_INVALID_REQUEST, jsonRPCError, parseJSONRPCMessage } from '@src/core'\nimport { isString } from '@orkestrel/contract'\nimport { MCP_SESSION_HEADER } from './constants.js'\n\n// The MCP server-transport helpers (AGENTS §4.3 module-scope names — no entity context).\n// The server-side reader `acceptsEventStream` reads the request's `Accept` header to\n// decide whether a Streamable-HTTP SSE response is allowed; `readSessionHeader` reads the\n// request's `mcp-session-id` header (the stateful transport's session validation);\n// `readLastEventId` reads the request's `Last-Event-ID` header (the resumable GET-SSE\n// replay cursor); the CLIENT-side reader `readEventStream` decodes a `fetch` Response's SSE\n// body back into JSON-RPC messages (the egress mirror, reusing `@orkestrel/sse`'s\n// `SSEParser`); `upgradeRequestPath` reads a raw `node:http` upgrade request's path (the\n// WebSocket transport's upgrade-path match). All are total and narrow at the boundary,\n// never `as` (AGENTS §14) — a missing / non-string Accept reads as \"no\", a missing session /\n// last-event header reads as `undefined`, a non-message SSE `data:` event is dropped, an\n// absent `url` reads as `'/'`.\n\n/**\n * Whether the request's `Accept` header opts into a Server-Sent-Events response.\n *\n * @remarks\n * Reads the fetch-standard `Request.headers.get('accept')` and returns `true` when it\n * contains `text/event-stream` (case-insensitive). The MCP `POST` handler uses it\n * (together with the `streaming` option) to pick the Streamable-HTTP SSE response\n * framing over a plain JSON body; the JSON-RPC envelope is identical either way. Total\n * — an absent / unmatched header returns `false`.\n *\n * @param request - The fetch-standard `Request`\n * @returns `true` when the client `Accept`s `text/event-stream`, else `false`\n */\nexport function acceptsEventStream(request: Request): boolean {\n\tconst accept = request.headers.get('accept')\n\tif (accept === null) return false\n\treturn accept.toLowerCase().includes('text/event-stream')\n}\n\n/**\n * Read the request's `mcp-session-id` header — the session id a stateful transport\n * validates, or `undefined` when absent.\n *\n * @remarks\n * Reads `request.headers.get(MCP_SESSION_HEADER)` — a fetch-standard `Headers` lookup\n * (single-valued by construction, never an array) — so a missing header reads as\n * `undefined` (no session). {@link import('./middlewares.js').createMCPSession} uses it on\n * every `POST` / `GET` / `DELETE` to look the session up in its closure store; an\n * `undefined` id is treated exactly like an unknown one (a `404`). Total — never throws.\n *\n * @param request - The fetch-standard `Request`\n * @returns The session id, or `undefined` when the header is absent\n */\nexport function readSessionHeader(request: Request): string | undefined {\n\tconst id = request.headers.get(MCP_SESSION_HEADER)\n\treturn id === null ? undefined : id\n}\n\n/**\n * Read the request's `Last-Event-ID` header — the SSE resume cursor a client sends when it\n * reconnects to the resumable `GET {path}` stream, or `undefined` when absent.\n *\n * @remarks\n * Reads `request.headers.get('last-event-id')` — a fetch-standard `Headers` lookup — so a\n * missing header reads as `undefined` (no resume, the stream starts fresh). The resumable\n * `GET` handler in {@link import('./middlewares.js').createMCPSession} passes a present value\n * to the session's {@link import('./types.js').MCPSessionInterface.replay} to re-deliver the\n * missed events before attaching the stream for live pushes. Total — never throws.\n *\n * @param request - The fetch-standard `Request`\n * @returns The last-event-id, or `undefined` when the header is absent\n */\nexport function readLastEventId(request: Request): string | undefined {\n\tconst id = request.headers.get('last-event-id')\n\treturn id === null ? undefined : id\n}\n\n/**\n * Build the stateful transport's \"unknown session\" rejection — an HTTP `404` carrying a\n * JSON-RPC error body.\n *\n * @remarks\n * Returns `Response.json(jsonRPCError(null, JSONRPC_INVALID_REQUEST, 'Session not found'),\n * { status: 404 })`, mirroring `createMCPRoutes`'s `400` transport-failure shape (a\n * JSON-RPC error BODY with a `null` id) but at the session-not-found status. Shared by\n * every {@link import('./middlewares.js').createMCPSession} validation site — the\n * non-`initialize` `POST` path, the resumable `GET {path}` open, and the `DELETE {path}`\n * session-end (each a missing / unknown / TTL-evicted id) — so the single `404` envelope\n * is defined once. Total — never throws.\n *\n * @returns The `404` JSON-RPC error `Response`\n */\nexport function rejectUnknownSession(): Response {\n\treturn Response.json(jsonRPCError(null, JSONRPC_INVALID_REQUEST, 'Session not found'), {\n\t\tstatus: 404,\n\t})\n}\n\n/**\n * Decode a `fetch` Response's Server-Sent-Events body into the JSON-RPC messages it\n * carried — the CLIENT-side inverse of the server's Streamable-HTTP SSE response.\n *\n * @remarks\n * Reads the whole `response.body` stream chunk-by-chunk through a `TextDecoder({\n * stream: true })` (handling a multi-byte char split across reads) and `@orkestrel/sse`'s\n * {@link SSEParserInterface} (handling a partial line / in-progress event split across\n * reads), then narrows each dispatched event's `data` to a {@link JSONRPCMessage} via\n * `parseJSONRPCMessage` (so a non-message / non-JSON `data:` event is DROPPED, never\n * thrown — total, §14). It reuses the SAME `SSEParser` the server's `openStream` seam\n * serializes against, so the wire round-trips. A `null` body (no stream) yields no\n * messages; the {@link import('./transports/HTTPClientTransport.js').HTTPClientTransport}\n * reads a request/response SSE reply (the server sends one `data:` event then ends), so\n * this drains to completion.\n *\n * @param response - The SSE `fetch` Response to decode (its `body` is read to completion)\n * @returns Every {@link JSONRPCMessage} the stream carried, in order\n */\nexport async function readEventStream(response: Response): Promise<readonly JSONRPCMessage[]> {\n\tconst body = response.body\n\tif (body === null) return []\n\tconst reader = body.getReader()\n\tconst decoder = new TextDecoder()\n\tconst parser: SSEParserInterface = createSSEParser()\n\tconst messages: JSONRPCMessage[] = []\n\ttry {\n\t\tfor (;;) {\n\t\t\tconst { done, value } = await reader.read()\n\t\t\tif (done) break\n\t\t\tfor (const event of parser.parse(decoder.decode(value, { stream: true }))) {\n\t\t\t\t// JSON-parse the event's `data` (the JSON-RPC envelope the server wrote), then\n\t\t\t\t// narrow it — a malformed / non-message payload is dropped, never thrown (§14).\n\t\t\t\tconst message = decodeEvent(event.data)\n\t\t\t\tif (message !== undefined) messages.push(message)\n\t\t\t}\n\t\t}\n\t} finally {\n\t\treader.releaseLock()\n\t}\n\treturn messages\n}\n\n/**\n * Decode one SSE event's `data` string into a {@link JSONRPCMessage}, or `undefined`\n * when it is not one — the per-event step {@link readEventStream} folds over.\n *\n * @remarks\n * `JSON.parse`s the `data` (the server serializes the JSON-RPC envelope as the event's\n * `data`) inside a try/catch and narrows the parsed value with `parseJSONRPCMessage`.\n * Total (§14): malformed JSON or a non-message value yields `undefined`, never throws.\n *\n * @param data - One SSE event's `data` payload\n * @returns The decoded {@link JSONRPCMessage}, or `undefined`\n */\nexport function decodeEvent(data: string): JSONRPCMessage | undefined {\n\ttry {\n\t\treturn parseJSONRPCMessage(JSON.parse(data))\n\t} catch {\n\t\treturn undefined\n\t}\n}\n\n/**\n * Read the path (without the query string) of a raw `node:http` protocol-upgrade request —\n * the `createWebSocketServer` upgrade-path match.\n *\n * @remarks\n * A `node:http` {@link import('node:http').IncomingMessage}'s `url` is the request TARGET\n * (`'/mcp?x=1'`), narrowed with `isString` (§14, never `as`) and defaulting to `'/'` for an\n * absent target; it is parsed against a dummy base (only the pathname matters for the upgrade\n * decision) and the `pathname` returned. The upgrade handler compares this against its\n * configured `path` to decide whether to claim the socket. Total — never throws on an\n * adversarial / absent target.\n *\n * @param request - The raw upgrade {@link import('node:http').IncomingMessage}\n * @returns The request's path (the `pathname`, no query), or `'/'` when the target is absent\n */\nexport function upgradeRequestPath(request: IncomingMessage): string {\n\tconst target = isString(request.url) ? request.url : '/'\n\treturn new URL(target, 'http://localhost').pathname\n}\n\n/**\n * Fold one more chunk of raw stdio bytes into a newline-framed buffer — the shared\n * line-framing step both stdio transports (client and server) read their inbound\n * newline-delimited JSON-RPC messages through.\n *\n * @remarks\n * Concatenates `buffer` (the carried-forward partial line from the previous call)\n * with `chunk`, splits on `'\\n'`, and returns every COMPLETE line (a `'\\r'` trailing\n * a line, from a CRLF-framed peer, is trimmed) plus the final, possibly-empty\n * fragment as the new `remainder` — the caller threads it back in as the next call's\n * `buffer`. A chunk containing no `'\\n'` yields no lines and the whole (buffer +\n * chunk) as `remainder`. Pure — no I/O, no instance state.\n *\n * @param buffer - The partial line carried forward from the previous chunk (`''` initially)\n * @param chunk - The newly-read raw bytes (already decoded to a string)\n * @returns The complete `lines` extracted (in order) and the trailing `remainder`\n */\nexport function extractLines(buffer: string, chunk: string): LineExtraction {\n\tconst combined = buffer + chunk\n\tconst parts = combined.split('\\n')\n\tconst remainder = parts[parts.length - 1] ?? ''\n\tconst lines = parts.slice(0, -1).map((line) => (line.endsWith('\\r') ? line.slice(0, -1) : line))\n\treturn { lines, remainder }\n}\n\n/**\n * Decode and deliver each complete newline-framed line onto a {@link\n * ClientTransportEventMap} emitter — the shared per-chunk dispatch step both stdio\n * transports (client and server) run their {@link extractLines} output through.\n *\n * @remarks\n * A blank line is skipped (a stray trailing newline). Every other line is decoded\n * with {@link decodeEvent} (`JSON.parse` + `parseJSONRPCMessage`, guarded); a\n * well-formed {@link JSONRPCMessage} emits `message`, a malformed / non-message line\n * emits `error` (§14 — total, never throws). Pure w.r.t. its own state — the emit is\n * the caller-owned side effect.\n *\n * @param emitter - The transport's {@link EmitterInterface} to emit `message` / `error` onto\n * @param lines - The complete lines (from {@link extractLines}) to decode and deliver\n */\nexport function dispatchLines(\n\temitter: EmitterInterface<ClientTransportEventMap>,\n\tlines: readonly string[],\n): void {\n\tfor (const line of lines) {\n\t\tif (line.length === 0) continue\n\t\tconst message = decodeEvent(line)\n\t\tif (message === undefined) {\n\t\t\temitter.emit('error', new Error('non-JSON-RPC stdio line'))\n\t\t\tcontinue\n\t\t}\n\t\temitter.emit('message', message)\n\t}\n}\n","import type { ClientTransportEventMap, ClientTransportInterface, JSONRPCMessage } from '@src/core'\nimport type { EmitterInterface } from '@orkestrel/emitter'\nimport type { HTTPClientTransportOptions } from '../types.js'\nimport { parseJSONRPCMessage } from '@src/core'\nimport { Emitter } from '@orkestrel/emitter'\nimport { MCP_SESSION_HEADER } from '../constants.js'\nimport { readEventStream } from '../helpers.js'\n\n/**\n * The HTTP CLIENT transport for the Model Context Protocol — a\n * {@link ClientTransportInterface} that drives a REMOTE Streamable-HTTP MCP server over\n * `fetch`, the egress mirror of the server's `createMCPRoutes`.\n *\n * @remarks\n * - **Request/response over `fetch`.** `send(message)` POSTs the JSON-serialized\n * message (or batch) to `options.url` with `content-type: application/json` and an\n * `Accept` of BOTH `application/json` and `text/event-stream` (so the server may\n * answer with either framing) — plus any `options.headers` (e.g. an `Authorization`\n * bearer). It then decodes the reply and emits each decoded {@link JSONRPCMessage} on\n * the `message` event the {@link import('@src/core').MCPClientInterface} subscribes\n * to.\n * - **Both reply framings.** A `200` with an `application/json` body is parsed with\n * `parseJSONRPCMessage`; a `200` with a `text/event-stream` body is decoded via the\n * `@orkestrel/sse` {@link import('@orkestrel/sse').SSEParserInterface} ({@link\n * readEventStream}) — the inverse of the server's `openStream` seam, so the wire\n * round-trips. A `202`\n * Accepted (a notification) carries no body and emits nothing.\n * - **Session echo.** `start()` / `close()` are no-ops (a request/response transport\n * holds no long-lived connection). The `mcp-session-id` response header, when a\n * STATEFUL server sends one (on `initialize`), is captured into `session` and then\n * ECHOED as the `mcp-session-id` request header on every SUBSEQUENT request — so an\n * `MCPClient` passes a stateful server's session validation. Before initialize returns\n * an id, `session` is `undefined` and no header is sent (safe against a stateless\n * server, which neither sends nor expects one).\n * - **Total at the boundary (§14).** Every reply is narrowed (`parseJSONRPCMessage`,\n * the SSE decoder) — a non-message reply is dropped, never asserted; a `fetch` /\n * decode failure surfaces on the `error` event rather than escaping `send`.\n * - **Observable (§13).** Owns the `emitter` ({@link ClientTransportEventMap}); fires\n * `message` per decoded reply, `error` on a fault, and `close` on `close()`.\n *\n * @example\n * ```ts\n * const transport = new HTTPClientTransport({ url: 'http://localhost:3000/mcp' })\n * const client = new MCPClient({ transport })\n * await client.connect()\n * ```\n */\nexport class HTTPClientTransport implements ClientTransportInterface {\n\treadonly #emitter: Emitter<ClientTransportEventMap>\n\treadonly #url: string\n\treadonly #headers: Readonly<Record<string, string>>\n\treadonly #fetch: typeof fetch\n\treadonly #timeout: number | undefined\n\t#session: string | undefined = undefined\n\n\tconstructor(options: HTTPClientTransportOptions) {\n\t\tthis.#emitter = new Emitter<ClientTransportEventMap>()\n\t\tthis.#url = options.url\n\t\tthis.#headers = options.headers ?? {}\n\t\tthis.#fetch = options.fetch ?? globalThis.fetch\n\t\tthis.#timeout = options.timeout\n\t}\n\n\tget emitter(): EmitterInterface<ClientTransportEventMap> {\n\t\treturn this.#emitter\n\t}\n\n\tget session(): string | undefined {\n\t\treturn this.#session\n\t}\n\n\tasync start(): Promise<void> {\n\t\t// A request/response transport opens no long-lived connection — `send` issues each\n\t\t// `fetch` on demand. Nothing to arm.\n\t}\n\n\tasync send(message: JSONRPCMessage | readonly JSONRPCMessage[]): Promise<void> {\n\t\tlet response: Response\n\t\ttry {\n\t\t\tresponse = await this.#fetch(this.#url, {\n\t\t\t\tmethod: 'POST',\n\t\t\t\theaders: {\n\t\t\t\t\t'content-type': 'application/json',\n\t\t\t\t\taccept: 'application/json, text/event-stream',\n\t\t\t\t\t// Echo a captured session id so a STATEFUL server validates the request; before\n\t\t\t\t\t// `initialize` returns one `#session` is undefined → no header (safe for a\n\t\t\t\t\t// stateless server). A caller `headers` key still wins (merged last).\n\t\t\t\t\t...(this.#session === undefined ? {} : { [MCP_SESSION_HEADER]: this.#session }),\n\t\t\t\t\t...this.#headers,\n\t\t\t\t},\n\t\t\t\tbody: JSON.stringify(message),\n\t\t\t\t...(this.#timeout === undefined ? {} : { signal: AbortSignal.timeout(this.#timeout) }),\n\t\t\t})\n\t\t} catch (error) {\n\t\t\t// A network-level failure (connection refused, DNS) — surface it for observation;\n\t\t\t// the client's per-request deadline still rejects the pending request.\n\t\t\tthis.#emitter.emit('error', error)\n\t\t\treturn\n\t\t}\n\t\t// Capture a server-assigned session id (a stateless server sends none) so it is echoed\n\t\t// on subsequent requests; a missing header leaves `session` unchanged.\n\t\tconst session = response.headers.get(MCP_SESSION_HEADER)\n\t\tif (session !== null) this.#session = session\n\t\tawait this.#deliver(response)\n\t}\n\n\tasync close(): Promise<void> {\n\t\tthis.#emitter.emit('close')\n\t}\n\n\t// Decode a reply and emit each carried message. A 202 (notification accepted) has no\n\t// body — emit nothing. An `application/json` body is one envelope; a `text/event-stream`\n\t// body is decoded via the core SSEParser (one or more `data:` events). A decode failure\n\t// surfaces on `error` rather than escaping.\n\tasync #deliver(response: Response): Promise<void> {\n\t\tif (response.status === 202) return\n\t\tconst type = response.headers.get('content-type') ?? ''\n\t\ttry {\n\t\t\tif (type.includes('text/event-stream')) {\n\t\t\t\tfor (const message of await readEventStream(response))\n\t\t\t\t\tthis.#emitter.emit('message', message)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (type.includes('application/json')) {\n\t\t\t\tconst message = parseJSONRPCMessage(await response.json())\n\t\t\t\tif (message !== undefined) this.#emitter.emit('message', message)\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tthis.#emitter.emit('error', error)\n\t\t}\n\t}\n}\n","import type { JSONRPCMessage } from '@src/core'\nimport type { StreamInterface } from '@orkestrel/server'\nimport type { EventStoreEntry, MCPSessionInterface, MCPSessionOptions } from './types.js'\nimport { DEFAULT_MCP_SESSION_CAPACITY, DEFAULT_MCP_SESSION_TTL } from './constants.js'\n\n/**\n * One MCP transport session — the per-session entity a {@link\n * import('./middlewares.js').createMCPSession} middleware owns, keyed by its `id`, carrying the\n * resumable server→client push channel with its bounded replay log FOLDED IN.\n *\n * @remarks\n * The single session entity (the old `SessionState` + `EventStore` merged): it holds the\n * session `id`, its OWN bounded, replayable log of pushed server→client messages (the\n * resumable GET-SSE channel — a private `#events` `Map` + a monotone `#counter`, with\n * `capacity` / `ttl` eviction, NOT a separate store), and the set of currently OPEN\n * server→client SSE streams (a resumable `GET {path}` registers via `attach`, unregisters via\n * `detach` on disconnect). Still a small entity (not a record), built minimal + extensible.\n *\n * - **`push` is the server-initiated primitive.** It APPENDS the message to the log (assigning\n * a monotone base36 event id) and FANS it out to every attached stream as one `id:`-tagged\n * SSE event (`stream.write({ id, data })`). A push with NO attached stream is still logged,\n * so a client that connects (or reconnects with a `Last-Event-ID`) LATER replays it from the\n * log. A `write` to a closed stream is a safe no-op (the {@link\n * `@orkestrel/server`'s `openStream` contract), so a just-disconnected stream that\n * has not yet been `detach`ed never throws. A replayed event and the live one carry the\n * IDENTICAL id (the log assigns it once).\n *\n * - **`replay(afterId)` is strictly-after.** It returns every retained log entry whose id sorts\n * AFTER `afterId` in append order — the missed-events list the `GET {path}` handler writes\n * before attaching the stream for live pushes. The decision for an UNKNOWN / already-evicted\n * `afterId` (the client's cursor fell off the back of the capacity window, or never existed):\n * replay NOTHING. Replaying the whole retained log would re-deliver events the client never\n * lost (its cursor is OLDER than everything retained); returning `[]` lets the handler then\n * stream only the fresh pushes that follow `attach` — the spec-sane resume.\n *\n * - **Bounded, append-ordered, plain `Map` (§21).** The log lives in ONE insertion-ordered\n * `Map<id, entry>` — insertion order IS append order IS id order, so `replay` and capacity\n * eviction both walk the map directly. NO database mirror — the log is process-local\n * transport mechanics, not durable state. `push` first drops every entry older than `ttl`\n * (lazy TTL — no background timer, the middleware's lazy-window idiom), appends, then evicts\n * the OLDEST entries until at most `capacity` remain; `replay` also runs the lazy TTL sweep\n * first, so a stale entry is never replayed.\n *\n * - **No transport coupling beyond the SSE seam.** It holds session state + the generic {@link\n * StreamInterface} handles `attach` was handed — never a raw socket, request, or response.\n * The middleware opens the stream (the spine seam) and registers it here; this class only\n * serializes a message onto the already-open streams.\n *\n * - **Injected clock.** `push` / `replay` accept an optional `now` (epoch ms), defaulting to\n * `Date.now()` — so a test drives TTL eviction with an elapsed clock rather than a real timer\n * (AGENTS §16).\n *\n * @example\n * ```ts\n * const session = new MCPSession(crypto.randomUUID())\n * session.attach(stream) // an open resumable GET-SSE stream\n * session.push({ jsonrpc: '2.0', method: 'notifications/message', params: { text: 'hi' } })\n * // → logged AND written to `stream` as an `id:`-tagged event; a reconnect replays it\n * ```\n */\nexport class MCPSession implements MCPSessionInterface {\n\treadonly #id: string\n\treadonly #events = new Map<string, EventStoreEntry>()\n\treadonly #streams = new Set<StreamInterface>()\n\treadonly #capacity: number\n\treadonly #ttl: number\n\t#counter = 0\n\n\tconstructor(id: string, options?: MCPSessionOptions) {\n\t\tthis.#id = id\n\t\tthis.#capacity = options?.capacity ?? DEFAULT_MCP_SESSION_CAPACITY\n\t\tthis.#ttl = options?.ttl ?? DEFAULT_MCP_SESSION_TTL\n\t}\n\n\tget id(): string {\n\t\treturn this.#id\n\t}\n\n\tattach(stream: StreamInterface): void {\n\t\tthis.#streams.add(stream)\n\t}\n\n\tdetach(stream: StreamInterface): void {\n\t\tthis.#streams.delete(stream)\n\t}\n\n\tpush(message: JSONRPCMessage, now = Date.now()): string {\n\t\t// Append to the log first (assigning the monotone event id), then fan the SAME id out to\n\t\t// every open stream — so a replayed event and a live one carry the identical id.\n\t\tconst id = this.#append(message, now)\n\t\tconst data = JSON.stringify(message)\n\t\tfor (const stream of this.#streams) stream.write({ id, data })\n\t\treturn id\n\t}\n\n\treplay(afterId: string, now = Date.now()): readonly EventStoreEntry[] {\n\t\tthis.#evict(now)\n\t\tconst out: EventStoreEntry[] = []\n\t\tlet found = false\n\t\tfor (const entry of this.#events.values()) {\n\t\t\t// Collect every entry STRICTLY AFTER `afterId`, in append order.\n\t\t\tif (found) out.push(entry)\n\t\t\telse if (entry.id === afterId) found = true\n\t\t}\n\t\t// An unknown / evicted `afterId` was never matched → `found` stays false → replay nothing\n\t\t// (the documented spec-sane choice; never re-deliver un-lost events).\n\t\treturn found ? out : []\n\t}\n\n\t// Append a message to the bounded replay log under a fresh monotone id, evicting stale +\n\t// over-capacity entries — the folded EventStore.append, now private to the session.\n\t#append(message: JSONRPCMessage, now: number): string {\n\t\t// Lazy TTL sweep BEFORE appending so an idle log shrinks as it is written.\n\t\tthis.#evict(now)\n\t\tthis.#counter += 1\n\t\tconst id = this.#counter.toString(36)\n\t\tthis.#events.set(id, { id, message, timestamp: now })\n\t\t// Capacity bound: drop the OLDEST entries (front of the insertion-ordered map) until at\n\t\t// most `capacity` remain — so the log is the most-recent `capacity` pushes.\n\t\twhile (this.#events.size > this.#capacity) {\n\t\t\tconst oldest = this.#events.keys().next().value\n\t\t\tif (oldest === undefined) break\n\t\t\tthis.#events.delete(oldest)\n\t\t}\n\t\treturn id\n\t}\n\n\t// Drop every entry older than the TTL. Entries are append-ordered (oldest first) and the\n\t// timestamp is monotone with insertion, so the stale run is a PREFIX — stop at the first live\n\t// entry. A non-positive ttl is treated as no expiry (nothing ever ages out by time).\n\t#evict(now: number): void {\n\t\tif (this.#ttl <= 0) return\n\t\tconst cutoff = now - this.#ttl\n\t\tfor (const [id, entry] of this.#events) {\n\t\t\tif (entry.timestamp <= cutoff) this.#events.delete(id)\n\t\t\telse break\n\t\t}\n\t}\n}\n","import type { ClientTransportEventMap, ClientTransportInterface, JSONRPCMessage } from '@src/core'\nimport type { EmitterInterface } from '@orkestrel/emitter'\nimport type { NodeWebSocketInterface } from '@orkestrel/websocket'\nimport { parseJSONRPCMessage } from '@src/core'\nimport { Emitter } from '@orkestrel/emitter'\n\n/**\n * The per-connection JSON-RPC-over-WebSocket SERVER bridge — wraps a\n * {@link NodeWebSocketInterface} (the RFC 6455 wire wrapper) as a\n * {@link ClientTransportInterface}, the bidirectional JSON-RPC message channel\n * `createWebSocketServer` pumps `mcp.dispatch` over and the egress mirror's\n * {@link import('./WebSocketClientTransport.js').WebSocketClientTransport} reuses.\n *\n * @remarks\n * - **Reuses `ClientTransportInterface` (§21).** It IS the same generic carrier the HTTP\n * client transport implements — `emitter` (`message` / `close` / `error`), `start`,\n * `send`, `close` — so the WebSocket server and client both speak ONE transport contract,\n * no near-duplicate sibling interface. `session` is `undefined` (the stateless v1; a\n * session id is the deferred sessions tier). The name keeps the role explicit even though\n * the shape is shared.\n * - **Inbound (`message`).** `start()` subscribes to the socket's `message` event; each text\n * frame is `JSON.parse`d inside a try/catch and narrowed with `parseJSONRPCMessage` — a\n * well-formed {@link JSONRPCMessage} is re-emitted on this transport's `message` event (the\n * parsed envelope the {@link import('@src/core').MCPServerInterface} pump dispatches), while\n * a non-JSON or non-message frame is surfaced on `error` and DROPPED, never thrown (§14). It\n * also bridges the socket's `close` → this transport's `close`, and the socket's `error`.\n * - **Outbound (`send`).** `send(message | messages)` writes ONE text frame per message\n * (`nodeWs.send(JSON.stringify(...))`); the underlying wrapper no-ops a write on a\n * non-open socket, so a closed connection drops silently rather than throwing.\n * - **`close()`** closes the underlying socket (the RFC 6455 close handshake) and fires the\n * transport's `close` event (idempotent — a second `close`, or a socket-driven close, emits\n * once).\n * - **Observable (§13).** Owns the `emitter` ({@link ClientTransportEventMap}); the emitter\n * isolates a listener throw (a buggy observer never corrupts the bridge). `error` is a\n * DOMAIN event (a transport-level fault), distinct from the emitter's listener-error channel.\n */\nexport class WebSocketServerTransport implements ClientTransportInterface {\n\treadonly #emitter: Emitter<ClientTransportEventMap>\n\treadonly #socket: NodeWebSocketInterface\n\t#started = false\n\t#closed = false\n\n\tconstructor(socket: NodeWebSocketInterface) {\n\t\tthis.#emitter = new Emitter<ClientTransportEventMap>()\n\t\tthis.#socket = socket\n\t}\n\n\tget emitter(): EmitterInterface<ClientTransportEventMap> {\n\t\treturn this.#emitter\n\t}\n\n\tget session(): string | undefined {\n\t\t// The stateless v1 holds no session — a server-assigned id is the deferred tier.\n\t\treturn undefined\n\t}\n\n\tasync start(): Promise<void> {\n\t\t// Arm the socket subscriptions once: a text frame becomes a `message`, the socket's\n\t\t// close / error bridge to this transport's events. Idempotent — a second `start` is a\n\t\t// no-op (the single MCPServer pump subscribes once).\n\t\tif (this.#started || this.#closed) return\n\t\tthis.#started = true\n\t\tthis.#socket.emitter.on('message', (text) => this.#receive(text))\n\t\tthis.#socket.emitter.on('close', () => this.#onClose())\n\t\tthis.#socket.emitter.on('error', (error) => this.#emitter.emit('error', error))\n\t}\n\n\tasync send(message: JSONRPCMessage | readonly JSONRPCMessage[]): Promise<void> {\n\t\t// One text frame per message (a batch is unrolled). The wrapper drops a write on a\n\t\t// non-open socket, so a closed connection is a silent no-op rather than a throw.\n\t\tconst messages = Array.isArray(message) ? message : [message]\n\t\tfor (const one of messages) this.#socket.send(JSON.stringify(one))\n\t}\n\n\tasync close(): Promise<void> {\n\t\tif (this.#closed) return\n\t\tthis.#closed = true\n\t\tthis.#socket.close()\n\t\tthis.#emitter.emit('close')\n\t}\n\n\t// Decode one inbound text frame: `JSON.parse` → `parseJSONRPCMessage`. A well-formed\n\t// message re-emits on `message`; a malformed / non-message frame surfaces on `error` and\n\t// is dropped (§14 — the bridge never throws on adversarial wire input).\n\t#receive(text: string): void {\n\t\tlet parsed: unknown\n\t\ttry {\n\t\t\tparsed = JSON.parse(text)\n\t\t} catch (error) {\n\t\t\tthis.#emitter.emit('error', error)\n\t\t\treturn\n\t\t}\n\t\tconst message = parseJSONRPCMessage(parsed)\n\t\tif (message === undefined) {\n\t\t\tthis.#emitter.emit('error', new Error('non-JSON-RPC WebSocket frame'))\n\t\t\treturn\n\t\t}\n\t\tthis.#emitter.emit('message', message)\n\t}\n\n\t// The socket closed (peer close frame, transport teardown) — fire this transport's\n\t// `close` once. A `close()` call already flipped `#closed`, so a socket-driven close\n\t// after an explicit one does not double-emit.\n\t#onClose(): void {\n\t\tif (this.#closed) return\n\t\tthis.#closed = true\n\t\tthis.#emitter.emit('close')\n\t}\n}\n","import type { ClientTransportEventMap, ClientTransportInterface, JSONRPCMessage } from '@src/core'\nimport type { EmitterInterface } from '@orkestrel/emitter'\nimport type { NodeWebSocketInterface } from '@orkestrel/websocket'\nimport type { WebSocketClientTransportOptions } from '../types.js'\nimport type { IncomingMessage } from 'node:http'\nimport type { Duplex } from 'node:stream'\nimport { randomBytes } from 'node:crypto'\nimport { request as httpRequest } from 'node:http'\nimport { request as httpsRequest } from 'node:https'\nimport { parseJSONRPCMessage } from '@src/core'\nimport { isString } from '@orkestrel/contract'\nimport { Emitter } from '@orkestrel/emitter'\nimport {\n\tcomputeWebSocketAccept,\n\tcreateNodeWebSocket,\n\tWEBSOCKET_VERSION,\n} from '@orkestrel/websocket'\nimport { MCP_WEBSOCKET_SUBPROTOCOL } from '../constants.js'\n\n/**\n * The WebSocket CLIENT transport for the Model Context Protocol — a\n * {@link ClientTransportInterface} that drives a REMOTE MCP server over a WebSocket, the\n * egress mirror of {@link import('./factories.js').createWebSocketServer} and the WebSocket\n * sibling of {@link import('./HTTPClientTransport.js').HTTPClientTransport}.\n *\n * @remarks\n * - **Persistent bidirectional channel (unlike the HTTP transport).** `start()` performs the\n * RFC 6455 client handshake: it opens a `node:http`(`s`) `GET` carrying `Connection: Upgrade`\n * / `Upgrade: websocket` / a random `Sec-WebSocket-Key` / `Sec-WebSocket-Version: 13` /\n * `Sec-WebSocket-Protocol: mcp` (plus any `options.headers`), awaits the client `'upgrade'`\n * event, and VALIDATES `Sec-WebSocket-Accept === computeWebSocketAccept(key)` (the D2 helper)\n * — a mismatch (or a non-`101` response, or a request error) REJECTS `start()` and the socket\n * is destroyed. On success it wraps the raw upgraded socket in `createNodeWebSocket({ socket,\n * head })` (CLIENT mode — no key → frames are MASKED per §5.3) and bridges its `message`.\n * - **Inbound (`message`).** Each decoded text frame is `JSON.parse`d (guarded) and narrowed\n * with `parseJSONRPCMessage` — a {@link JSONRPCMessage} re-emits on this transport's `message`\n * event (the reply the {@link import('@src/core').MCPClientInterface} correlates by `id`); a\n * non-JSON / non-message frame surfaces on `error` and is dropped (§14). The socket's `close`\n * / `error` bridge to this transport's events.\n * - **Outbound (`send`).** `send(message | messages)` writes ONE masked text frame per message.\n * - **`close()`** closes the underlying socket and fires `close` (idempotent).\n * - **URL scheme.** `options.url` accepts a `ws://` / `wss://` URL or an `http://` / `https://`\n * one; a `ws(s)` scheme is converted to `http(s)` for the underlying upgrade request (`wss`\n * → TLS via `node:https`). Either reaches the same endpoint.\n * - **Observable (§13).** Owns the `emitter` ({@link ClientTransportEventMap}); every emit\n * the emitter isolates a listener throw (a buggy observer never corrupts the transport);\n * `error` is a DOMAIN event (a transport-level fault).\n *\n * @example\n * ```ts\n * const transport = new WebSocketClientTransport({ url: 'ws://localhost:3000/mcp' })\n * const client = new MCPClient({ transport })\n * await client.connect() // start() handshakes, then the MCP initialize runs over WS frames\n * ```\n */\nexport class WebSocketClientTransport implements ClientTransportInterface {\n\treadonly #emitter: Emitter<ClientTransportEventMap>\n\treadonly #url: string\n\treadonly #headers: Readonly<Record<string, string>>\n\t#socket: NodeWebSocketInterface | undefined = undefined\n\t#closed = false\n\n\tconstructor(options: WebSocketClientTransportOptions) {\n\t\tthis.#emitter = new Emitter<ClientTransportEventMap>()\n\t\tthis.#url = options.url\n\t\tthis.#headers = options.headers ?? {}\n\t}\n\n\tget emitter(): EmitterInterface<ClientTransportEventMap> {\n\t\treturn this.#emitter\n\t}\n\n\tget session(): string | undefined {\n\t\treturn undefined\n\t}\n\n\tasync start(): Promise<void> {\n\t\t// Already connected — a second `connect()` short-circuits in the client, but guard here\n\t\t// too (idempotent open).\n\t\tif (this.#socket !== undefined) return\n\t\tthis.#closed = false\n\t\tconst url = this.#httpURL()\n\t\tconst key = randomBytes(16).toString('base64')\n\t\tconst secure = url.protocol === 'https:'\n\t\tconst send = secure ? httpsRequest : httpRequest\n\n\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\tlet settled = false\n\t\t\tconst fail = (error: Error): void => {\n\t\t\t\tif (settled) return\n\t\t\t\tsettled = true\n\t\t\t\treject(error)\n\t\t\t}\n\t\t\tconst request = send({\n\t\t\t\thostname: url.hostname,\n\t\t\t\tport: url.port.length > 0 ? Number(url.port) : secure ? 443 : 80,\n\t\t\t\tpath: `${url.pathname}${url.search}`,\n\t\t\t\theaders: {\n\t\t\t\t\tConnection: 'Upgrade',\n\t\t\t\t\tUpgrade: 'websocket',\n\t\t\t\t\t'Sec-WebSocket-Key': key,\n\t\t\t\t\t'Sec-WebSocket-Version': WEBSOCKET_VERSION,\n\t\t\t\t\t'Sec-WebSocket-Protocol': MCP_WEBSOCKET_SUBPROTOCOL,\n\t\t\t\t\t...this.#headers,\n\t\t\t\t},\n\t\t\t})\n\n\t\t\t// The server accepted the upgrade: validate the handshake accept, then wrap the\n\t\t\t// raw socket in a CLIENT-mode NodeWebSocket (masks its frames).\n\t\t\trequest.on('upgrade', (response: IncomingMessage, socket: Duplex, head: Buffer) => {\n\t\t\t\tconst accept = response.headers['sec-websocket-accept']\n\t\t\t\tif (!isString(accept) || accept !== computeWebSocketAccept(key)) {\n\t\t\t\t\tsocket.destroy()\n\t\t\t\t\tfail(new Error('WebSocket handshake failed: Sec-WebSocket-Accept mismatch'))\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst ws = createNodeWebSocket({ socket, head })\n\t\t\t\tthis.#socket = ws\n\t\t\t\tthis.#bind(ws)\n\t\t\t\tif (!settled) {\n\t\t\t\t\tsettled = true\n\t\t\t\t\tresolve()\n\t\t\t\t}\n\t\t\t})\n\n\t\t\t// A plain (non-101) response means the server declined the upgrade.\n\t\t\trequest.on('response', (response) => {\n\t\t\t\tresponse.resume()\n\t\t\t\tfail(new Error(`WebSocket upgrade declined with status ${response.statusCode ?? 0}`))\n\t\t\t})\n\t\t\t// A connection-level failure (refused, DNS, reset).\n\t\t\trequest.on('error', (error) =>\n\t\t\t\tfail(error instanceof Error ? error : new Error(String(error))),\n\t\t\t)\n\t\t\trequest.end()\n\t\t})\n\t}\n\n\tasync send(message: JSONRPCMessage | readonly JSONRPCMessage[]): Promise<void> {\n\t\tconst socket = this.#socket\n\t\tif (socket === undefined) throw new Error('WebSocket transport is not connected')\n\t\tconst messages = Array.isArray(message) ? message : [message]\n\t\tfor (const one of messages) socket.send(JSON.stringify(one))\n\t}\n\n\tasync close(): Promise<void> {\n\t\tif (this.#closed) return\n\t\tthis.#closed = true\n\t\tconst socket = this.#socket\n\t\tthis.#socket = undefined\n\t\tif (socket !== undefined) socket.close()\n\t\tthis.#emitter.emit('close')\n\t}\n\n\t// Bridge the upgraded socket's events onto the transport: a text frame → `message`\n\t// (decoded + narrowed), the socket close → `close`, a socket fault → `error`.\n\t#bind(ws: NodeWebSocketInterface): void {\n\t\tws.emitter.on('message', (text) => this.#receive(text))\n\t\tws.emitter.on('close', () => this.#onClose())\n\t\tws.emitter.on('error', (error) => this.#emitter.emit('error', error))\n\t}\n\n\t// Decode one inbound text frame: `JSON.parse` → `parseJSONRPCMessage`. A well-formed\n\t// message re-emits on `message`; a malformed / non-message frame surfaces on `error` and\n\t// is dropped (§14 — never throws on adversarial wire input).\n\t#receive(text: string): void {\n\t\tlet parsed: unknown\n\t\ttry {\n\t\t\tparsed = JSON.parse(text)\n\t\t} catch (error) {\n\t\t\tthis.#emitter.emit('error', error)\n\t\t\treturn\n\t\t}\n\t\tconst message = parseJSONRPCMessage(parsed)\n\t\tif (message === undefined) {\n\t\t\tthis.#emitter.emit('error', new Error('non-JSON-RPC WebSocket frame'))\n\t\t\treturn\n\t\t}\n\t\tthis.#emitter.emit('message', message)\n\t}\n\n\t// The socket closed underneath us — fire `close` once (a `close()` call already flipped\n\t// `#closed`, so it does not double-emit).\n\t#onClose(): void {\n\t\tif (this.#closed) return\n\t\tthis.#closed = true\n\t\tthis.#socket = undefined\n\t\tthis.#emitter.emit('close')\n\t}\n\n\t// Normalize `options.url` to the `http(s)` URL the underlying upgrade request uses: a\n\t// `ws://` → `http://`, a `wss://` → `https://`; an `http(s)://` URL passes through. Any\n\t// other scheme throws (a clear boundary error, not a silent mis-dial).\n\t#httpURL(): URL {\n\t\tconst url = new URL(this.#url)\n\t\tif (url.protocol === 'ws:') url.protocol = 'http:'\n\t\telse if (url.protocol === 'wss:') url.protocol = 'https:'\n\t\telse if (url.protocol !== 'http:' && url.protocol !== 'https:') {\n\t\t\tthrow new Error(`unsupported WebSocket URL scheme '${url.protocol}'`)\n\t\t}\n\t\treturn url\n\t}\n}\n","import type { ClientTransportEventMap, ClientTransportInterface, JSONRPCMessage } from '@src/core'\nimport type { EmitterInterface } from '@orkestrel/emitter'\nimport type { StdioClientTransportOptions } from '../types.js'\nimport type { ChildProcessByStdio } from 'node:child_process'\nimport type { Readable, Writable } from 'node:stream'\nimport { spawn } from 'node:child_process'\nimport { Emitter } from '@orkestrel/emitter'\nimport { dispatchLines, extractLines } from '../helpers.js'\n\n/**\n * The stdio CLIENT transport for the Model Context Protocol — a\n * {@link ClientTransportInterface} that drives a CHILD PROCESS MCP server over\n * newline-delimited JSON-RPC on `stdin`/`stdout`, the stdio sibling of {@link\n * import('./HTTPClientTransport.js').HTTPClientTransport} and {@link\n * import('./WebSocketClientTransport.js').WebSocketClientTransport}.\n *\n * @remarks\n * - **Spawns the server.** `start()` runs `node:child_process`'s `spawn(options.command,\n * options.args, { env: options.env, stdio: ['pipe', 'pipe', 'inherit'] })` — the\n * child's `stdin`/`stdout` are piped for the JSON-RPC channel, its `stderr` inherits\n * the parent's (diagnostics pass through, never parsed as protocol).\n * - **Inbound (`message`).** Each `stdout` chunk is folded through the shared\n * {@link extractLines} line-framing helper (buffering a partial trailing line\n * across reads); every complete line is decoded and delivered via the shared\n * {@link dispatchLines} helper — a well-formed {@link JSONRPCMessage} emits\n * `message`, a malformed line emits `error` (§14, never throws). The child's\n * `close` bridges to this transport's `close`.\n * - **Outbound (`send`).** `send(message | messages)` writes ONE newline-terminated\n * `JSON.stringify`d line per message to the child's `stdin`.\n * - **`close()`** kills the child process and fires `close` (idempotent).\n * - **Observable (§13).** Owns the `emitter` ({@link ClientTransportEventMap}); the\n * emitter isolates a listener throw; `error` is a DOMAIN event (a transport-level\n * fault), distinct from the emitter's own listener-error channel.\n *\n * @example\n * ```ts\n * const transport = new StdioClientTransport({ command: 'node', args: ['./server.js'] })\n * const client = new MCPClient({ transport })\n * await client.connect() // start() spawns the child, then the MCP initialize runs over stdio\n * ```\n */\nexport class StdioClientTransport implements ClientTransportInterface {\n\treadonly #emitter: Emitter<ClientTransportEventMap>\n\treadonly #command: string\n\treadonly #args: readonly string[]\n\treadonly #env: Readonly<Record<string, string>> | undefined\n\t#child: ChildProcessByStdio<Writable, Readable, null> | undefined = undefined\n\t#buffer = ''\n\t#closed = false\n\n\tconstructor(options: StdioClientTransportOptions) {\n\t\tthis.#emitter = new Emitter<ClientTransportEventMap>()\n\t\tthis.#command = options.command\n\t\tthis.#args = options.args ?? []\n\t\tthis.#env = options.env\n\t}\n\n\tget emitter(): EmitterInterface<ClientTransportEventMap> {\n\t\treturn this.#emitter\n\t}\n\n\tget session(): string | undefined {\n\t\treturn undefined\n\t}\n\n\tasync start(): Promise<void> {\n\t\t// Already spawned — a second `start()` (e.g. via `connect()`) short-circuits (idempotent).\n\t\tif (this.#child !== undefined) return\n\t\tthis.#closed = false\n\t\tthis.#buffer = ''\n\t\tconst child = spawn(this.#command, [...this.#args], {\n\t\t\tenv: this.#env,\n\t\t\tstdio: ['pipe', 'pipe', 'inherit'],\n\t\t})\n\t\tthis.#child = child\n\t\tchild.stdout.on('data', (chunk: Buffer | string) => this.#receive(chunk.toString()))\n\t\tchild.on('close', () => this.#onClose())\n\t\tchild.on('error', (error) => this.#emitter.emit('error', error))\n\t}\n\n\tasync send(message: JSONRPCMessage | readonly JSONRPCMessage[]): Promise<void> {\n\t\tconst child = this.#child\n\t\tif (child === undefined) throw new Error('stdio transport is not connected')\n\t\tconst messages = Array.isArray(message) ? message : [message]\n\t\tfor (const one of messages) child.stdin.write(`${JSON.stringify(one)}\\n`)\n\t}\n\n\tasync close(): Promise<void> {\n\t\tif (this.#closed) return\n\t\tthis.#closed = true\n\t\tconst child = this.#child\n\t\tthis.#child = undefined\n\t\tif (child !== undefined) child.kill()\n\t\tthis.#emitter.emit('close')\n\t}\n\n\t// Buffer a raw stdout chunk through the shared line-framing helper, then decode + deliver\n\t// every complete line onto this transport's emitter (a partial trailing line carries\n\t// forward to the next chunk).\n\t#receive(chunk: string): void {\n\t\tconst { lines, remainder } = extractLines(this.#buffer, chunk)\n\t\tthis.#buffer = remainder\n\t\tdispatchLines(this.#emitter, lines)\n\t}\n\n\t// The child process closed — fire this transport's `close` once. A `close()` call already\n\t// flipped `#closed`, so a child-driven close after an explicit one does not double-emit.\n\t#onClose(): void {\n\t\tif (this.#closed) return\n\t\tthis.#closed = true\n\t\tthis.#child = undefined\n\t\tthis.#emitter.emit('close')\n\t}\n}\n","import type { ClientTransportEventMap, ClientTransportInterface, JSONRPCMessage } from '@src/core'\nimport type { EmitterInterface } from '@orkestrel/emitter'\nimport { Emitter } from '@orkestrel/emitter'\nimport { dispatchLines, extractLines } from '../helpers.js'\n\n/**\n * The stdio SERVER transport for the Model Context Protocol — wraps an injectable\n * readable/writable stream pair (`process.stdin`/`process.stdout` in production, a\n * test double in tests) as a {@link ClientTransportInterface}, the newline-delimited\n * JSON-RPC channel {@link import('../factories.js').createStdioServer} pumps\n * `mcp.dispatch` over, the stdio mirror of {@link\n * import('./WebSocketServerTransport.js').WebSocketServerTransport}.\n *\n * @remarks\n * - **Reuses `ClientTransportInterface` (§21).** The same generic carrier the HTTP\n * and WebSocket server transports implement — `emitter` (`message` / `close` /\n * `error`), `start`, `send`, `close`. `session` is `undefined` (the stateless v1).\n * - **Inbound (`message`).** `start()` subscribes to `input`'s `data` event; each\n * chunk is folded through the shared {@link extractLines} line-framing helper\n * (buffering a partial trailing line across reads), and every complete line is\n * decoded and delivered via the shared {@link dispatchLines} helper — a\n * well-formed {@link JSONRPCMessage} re-emits on `message`, a malformed line\n * emits `error` (§14, never throws). `input`'s `close` bridges to this\n * transport's `close`.\n * - **Outbound (`send`).** `send(message | messages)` writes ONE newline-terminated\n * `JSON.stringify`d line per message to `output`.\n * - **`close()`** fires this transport's `close` (idempotent) — the injected streams\n * are owned by the caller (typically `process.stdin`/`process.stdout`, which must\n * never be closed out from under the process) and are not torn down here.\n * - **Observable (§13).** Owns the `emitter` ({@link ClientTransportEventMap}); the\n * emitter isolates a listener throw; `error` is a DOMAIN event (a transport-level\n * fault), distinct from the emitter's own listener-error channel.\n */\nexport class StdioServerTransport implements ClientTransportInterface {\n\treadonly #emitter: Emitter<ClientTransportEventMap>\n\treadonly #input: NodeJS.ReadableStream\n\treadonly #output: NodeJS.WritableStream\n\t#buffer = ''\n\t#started = false\n\t#closed = false\n\n\tconstructor(input: NodeJS.ReadableStream, output: NodeJS.WritableStream) {\n\t\tthis.#emitter = new Emitter<ClientTransportEventMap>()\n\t\tthis.#input = input\n\t\tthis.#output = output\n\t}\n\n\tget emitter(): EmitterInterface<ClientTransportEventMap> {\n\t\treturn this.#emitter\n\t}\n\n\tget session(): string | undefined {\n\t\t// The stateless v1 holds no session — a server-assigned id is the deferred tier.\n\t\treturn undefined\n\t}\n\n\tasync start(): Promise<void> {\n\t\t// Arm the stream subscriptions once: an input chunk decodes to `message`, the input's\n\t\t// close bridges to this transport's `close`. Idempotent — a second `start` is a no-op\n\t\t// (the single MCPServer pump subscribes once).\n\t\tif (this.#started || this.#closed) return\n\t\tthis.#started = true\n\t\tthis.#input.on('data', (chunk: Buffer | string) => this.#receive(chunk.toString()))\n\t\tthis.#input.on('close', () => this.#onClose())\n\t\tthis.#input.on('error', (error) => this.#emitter.emit('error', error))\n\t}\n\n\tasync send(message: JSONRPCMessage | readonly JSONRPCMessage[]): Promise<void> {\n\t\tconst messages = Array.isArray(message) ? message : [message]\n\t\tfor (const one of messages) this.#output.write(`${JSON.stringify(one)}\\n`)\n\t}\n\n\tasync close(): Promise<void> {\n\t\tif (this.#closed) return\n\t\tthis.#closed = true\n\t\tthis.#emitter.emit('close')\n\t}\n\n\t// Buffer a raw input chunk through the shared line-framing helper, then decode + deliver\n\t// every complete line onto this transport's emitter (a partial trailing line carries\n\t// forward to the next chunk).\n\t#receive(chunk: string): void {\n\t\tconst { lines, remainder } = extractLines(this.#buffer, chunk)\n\t\tthis.#buffer = remainder\n\t\tdispatchLines(this.#emitter, lines)\n\t}\n\n\t// The input stream closed (EOF, peer teardown) — fire this transport's `close` once. A\n\t// `close()` call already flipped `#closed`, so a stream-driven close after an explicit one\n\t// does not double-emit.\n\t#onClose(): void {\n\t\tif (this.#closed) return\n\t\tthis.#closed = true\n\t\tthis.#emitter.emit('close')\n\t}\n}\n","import type { ClientTransportInterface, MCPServerInterface } from '@src/core'\nimport type { RouteInput } from '@orkestrel/router'\nimport type { UpgradeHandler } from '@orkestrel/server/server'\nimport type {\n\tHTTPClientTransportOptions,\n\tHTTPTransportOptions,\n\tStdioClientTransportOptions,\n\tStdioServerOptions,\n\tWebSocketClientTransportOptions,\n\tWebSocketServerOptions,\n} from './types.js'\nimport type { IncomingMessage } from 'node:http'\nimport type { Duplex } from 'node:stream'\nimport {\n\tisJSONRPCRequest,\n\tJSONRPC_INVALID_REQUEST,\n\tJSONRPC_PARSE_ERROR,\n\tjsonRPCError,\n\tparseJSONRPCMessage,\n} from '@src/core'\nimport { isString } from '@orkestrel/contract'\nimport { openStream } from '@orkestrel/server'\nimport { createNodeWebSocket, WEBSOCKET_VERSION } from '@orkestrel/websocket'\nimport { DEFAULT_MCP_PATH, MCP_WEBSOCKET_SUBPROTOCOL } from './constants.js'\nimport { acceptsEventStream, upgradeRequestPath } from './helpers.js'\nimport { HTTPClientTransport } from './transports/HTTPClientTransport.js'\nimport { StdioClientTransport } from './transports/StdioClientTransport.js'\nimport { StdioServerTransport } from './transports/StdioServerTransport.js'\nimport { WebSocketClientTransport } from './transports/WebSocketClientTransport.js'\nimport { WebSocketServerTransport } from './transports/WebSocketServerTransport.js'\n\n/**\n * Create the MCP Streamable-HTTP transport routes — mounts a transport-agnostic\n * {@link MCPServerInterface} (the `@src/core` dispatch core) on the fetch-standard router\n * spine, pumping each `POST` body through `mcp.dispatch`. Returns the {@link RouteInput}s to\n * hand to `router.add(...)`.\n *\n * @remarks\n * A SINGLE `POST {path}` route — `createMCPRoutes` is STATELESS. The handler reads its own\n * request body (its own JSON parse try/catch), so it works with or without a session\n * middleware mounted in front. It draws a sharp line between TRANSPORT-level and\n * DISPATCH-level outcomes:\n *\n * - A **transport** failure — a malformed JSON body, or a parsed value that is not a\n * JSON-RPC REQUEST — is an HTTP `400` carrying a JSON-RPC error BODY (`-32700` Parse\n * error / `-32600` Invalid Request, id `null`).\n * - A **dispatch** result — a success OR an IN-BAND JSON-RPC error from `mcp.dispatch`\n * (e.g. `-32601` method-not-found) — is an HTTP `200` carrying the JSON-RPC response\n * envelope (the error is in-band per JSON-RPC, NOT an HTTP error).\n * - A **notification** (a request with no `id`, which `dispatch` resolves to\n * `undefined`) is a `202 Accepted` with no body.\n *\n * When `streaming` is enabled (the default) and the client `Accept`s `text/event-stream`,\n * the `200` reply is framed as a Streamable-HTTP SSE response (one `data:` event carrying\n * the JSON-RPC envelope, then the stream ends) via `@orkestrel/server`'s generic\n * {@link import('@orkestrel/server').openStream} seam; otherwise it is a plain JSON body.\n *\n * **Sessions are a SEPARATE, plug-and-play middleware.** `createMCPRoutes` mints / reads no\n * session id. To make the transport STATEFUL, mount {@link\n * import('./middlewares.js').createMCPSession} IN FRONT — it owns the same `path`, mints +\n * validates the `mcp-session-id`, and serves the resumable `GET {path}` + `DELETE {path}`,\n * leaving this route to dispatch the validated `POST`.\n *\n * This is MECHANISM, not policy: compose auth / CORS / rate-limiting (and the session\n * middleware) IN FRONT as ordinary middleware — the transport route adds none.\n *\n * @typeParam TState - The consumer's opaque per-request state type\n * @param mcp - The transport-agnostic {@link MCPServerInterface} to expose over HTTP\n * @param options - Optional `path` (default {@link DEFAULT_MCP_PATH}) and `streaming`\n * (default `true`); see {@link HTTPTransportOptions}\n * @returns The {@link RouteInput}s to register with the router\n *\n * @example\n * ```ts\n * import { createMCPServer, createToolManager } from '@src/core'\n * import { createMCPRoutes } from '@src/server'\n *\n * const mcp = createMCPServer({ name: 'docs', version: '1.0.0', tools: createToolManager() })\n * const routes = createMCPRoutes(mcp) // POST /mcp dispatches JSON-RPC (JSON or SSE per Accept)\n * ```\n */\nexport function createMCPRoutes<TState = unknown>(\n\tmcp: MCPServerInterface,\n\toptions?: HTTPTransportOptions,\n): readonly RouteInput<string, TState>[] {\n\tconst path = options?.path ?? DEFAULT_MCP_PATH\n\tconst streaming = options?.streaming ?? true\n\tconst post: RouteInput<string, TState> = {\n\t\tmethod: 'POST',\n\t\tpath,\n\t\tname: 'mcp',\n\t\thandler: async (request) => {\n\t\t\tlet text: string\n\t\t\ttry {\n\t\t\t\ttext = await request.text()\n\t\t\t} catch {\n\t\t\t\t// A malformed JSON body is a TRANSPORT failure — HTTP 400 + a JSON-RPC -32700.\n\t\t\t\treturn Response.json(jsonRPCError(null, JSONRPC_PARSE_ERROR, 'Parse error'), {\n\t\t\t\t\tstatus: 400,\n\t\t\t\t})\n\t\t\t}\n\t\t\tlet parsed: unknown\n\t\t\ttry {\n\t\t\t\tparsed = JSON.parse(text)\n\t\t\t} catch {\n\t\t\t\treturn Response.json(jsonRPCError(null, JSONRPC_PARSE_ERROR, 'Parse error'), {\n\t\t\t\t\tstatus: 400,\n\t\t\t\t})\n\t\t\t}\n\t\t\tconst rpcRequest = parseJSONRPCMessage(parsed)\n\t\t\tif (rpcRequest === undefined || !('method' in rpcRequest)) {\n\t\t\t\t// Not a JSON-RPC request (a response, or any non-message) — HTTP 400 + -32600.\n\t\t\t\t// `'method' in rpcRequest` narrows the message union to `JSONRPCRequest` (no `as`).\n\t\t\t\treturn Response.json(jsonRPCError(null, JSONRPC_INVALID_REQUEST, 'Invalid Request'), {\n\t\t\t\t\tstatus: 400,\n\t\t\t\t})\n\t\t\t}\n\t\t\tconst response = await mcp.dispatch(rpcRequest)\n\t\t\tif (response === undefined) {\n\t\t\t\t// A notification (no `id`) yields no response — 202 Accepted, no body.\n\t\t\t\treturn new Response(null, { status: 202 })\n\t\t\t}\n\t\t\tif (streaming && acceptsEventStream(request)) {\n\t\t\t\t// Streamable-HTTP SSE response: one `data:` event with the JSON-RPC envelope, then end.\n\t\t\t\tconst s = openStream()\n\t\t\t\ts.write({ data: JSON.stringify(response) })\n\t\t\t\ts.end()\n\t\t\t\treturn s.response\n\t\t\t}\n\t\t\t// A dispatch result — success OR an in-band JSON-RPC error — is HTTP 200 + the envelope.\n\t\t\treturn Response.json(response)\n\t\t},\n\t}\n\treturn [post]\n}\n\n/**\n * Create the HTTP CLIENT transport for an {@link import('@src/core').MCPClientInterface}\n * — a {@link ClientTransportInterface} that drives a REMOTE Streamable-HTTP MCP server\n * over `fetch`. The egress mirror of {@link createMCPRoutes}.\n *\n * @remarks\n * Hand it to `createMCPClient({ transport })`: each JSON-RPC message the client sends is\n * `POST`ed to `options.url` with `content-type: application/json` and an `Accept` of\n * both `application/json` and `text/event-stream` (the server answers with EITHER — a\n * plain JSON envelope or a Streamable-HTTP SSE `data:` event, decoded via `@orkestrel/sse`),\n * and the reply is surfaced on the transport's `message` event for the client's id\n * correlation. Add `options.headers` (e.g. an `Authorization` bearer) to reach a guarded\n * server. `start` / `close` hold no connection; against a STATEFUL server it captures the\n * `mcp-session-id` from `initialize` and echoes it on later requests, so the same\n * `MCPClient` passes session validation (a stateless server sends none).\n *\n * @param options - `url` (the remote endpoint; REQUIRED), optional `headers` merged onto\n * every request, optional `fetch` (default `globalThis.fetch`), and optional `timeout`\n * (ms, applied via `AbortSignal.timeout`); see {@link HTTPClientTransportOptions}\n * @returns A working {@link ClientTransportInterface} over `fetch`\n *\n * @example\n * ```ts\n * import { createMCPClient } from '@src/core'\n * import { createHTTPClientTransport } from '@src/server'\n *\n * const client = createMCPClient({\n * \ttransport: createHTTPClientTransport({ url: 'http://localhost:3000/mcp' }),\n * })\n * await client.connect()\n * const tools = await client.tools()\n * ```\n */\nexport function createHTTPClientTransport(\n\toptions: HTTPClientTransportOptions,\n): ClientTransportInterface {\n\treturn new HTTPClientTransport(options)\n}\n\n/**\n * Create the MCP WebSocket transport INGRESS — an {@link UpgradeHandler} that exposes a\n * transport-agnostic {@link MCPServerInterface} over a WebSocket, the WebSocket mirror of\n * {@link createMCPRoutes}. Register it on the spine's upgrade seam.\n *\n * @remarks\n * It composes the lean RFC 6455 `@orkestrel/websocket` wrapper over `@orkestrel/server`'s\n * generic upgrade seam — the spine speaks no WebSocket, this handler does.\n *\n * - **Declines (returns `false`)** when the upgrade is not for it, so the spine fans the\n * socket to the next handler (or destroys an unclaimed one): the `Upgrade` header is not\n * `websocket`, the request path is not `options.path` (default {@link DEFAULT_MCP_PATH},\n * `'/mcp'`), the `Sec-WebSocket-Key` is absent, or the `Sec-WebSocket-Version` is not `13`.\n * A decline NEVER writes to the socket (it is not yet ours) — the spine owns the unclaimed\n * outcome.\n * - **Claims (returns `true`)** otherwise: it builds `createNodeWebSocket({ socket, key, head,\n * protocol })` (SERVER mode → writes the `101` handshake, echoing the `subprotocol`, default\n * {@link MCP_WEBSOCKET_SUBPROTOCOL} `'mcp'`, and sends UNMASKED frames), wraps it in a\n * {@link WebSocketServerTransport}, and PUMPS: each inbound {@link\n * import('@src/core').JSONRPCMessage} that is a REQUEST runs through `mcp.dispatch`, and a\n * defined response is written back as a frame — a NOTIFICATION (`dispatch` → `undefined`)\n * sends nothing. A non-request message (a stray response) is ignored. The dispatch is\n * guarded so a `dispatch` / `send` fault surfaces on the transport's `error` event rather\n * than escaping the (async) message listener.\n *\n * It is MECHANISM, not policy: compose an auth guard IN FRONT by registering an upgrade\n * handler BEFORE this one — that handler can claim (decline + destroy) an unauthenticated\n * upgrade so it never reaches this pump.\n *\n * @param mcp - The transport-agnostic {@link MCPServerInterface} to expose over WebSocket\n * @param options - Optional `path` (default {@link DEFAULT_MCP_PATH}) and `subprotocol`\n * (default {@link MCP_WEBSOCKET_SUBPROTOCOL}); see {@link WebSocketServerOptions}\n * @returns An {@link UpgradeHandler} to register with the spine's `upgrade` seam\n *\n * @example\n * ```ts\n * import { createMCPServer, createToolManager } from '@src/core'\n * import { createWebSocketServer } from '@src/server'\n *\n * const mcp = createMCPServer({ name: 'docs', version: '1.0.0', tools: createToolManager() })\n * server.upgrade(createWebSocketServer(mcp)) // an MCP client now connects over ws://…/mcp\n * ```\n */\nexport function createWebSocketServer(\n\tmcp: MCPServerInterface,\n\toptions?: WebSocketServerOptions,\n): UpgradeHandler {\n\tconst path = options?.path ?? DEFAULT_MCP_PATH\n\tconst subprotocol = options?.subprotocol ?? MCP_WEBSOCKET_SUBPROTOCOL\n\treturn (request: IncomingMessage, socket: Duplex, head: Buffer): boolean => {\n\t\t// DECLINE anything that is not our MCP WebSocket upgrade — the spine fans it onward or\n\t\t// destroys it. Never touch the socket on a decline (it is not ours yet).\n\t\tconst upgrade = request.headers['upgrade']\n\t\tif (!isString(upgrade) || upgrade.toLowerCase() !== 'websocket') return false\n\t\tif (upgradeRequestPath(request) !== path) return false\n\t\tconst key = request.headers['sec-websocket-key']\n\t\tif (!isString(key)) return false\n\t\tconst version = request.headers['sec-websocket-version']\n\t\tif (!isString(version) || version !== WEBSOCKET_VERSION) return false\n\n\t\t// CLAIM: the wrapper writes the `101` handshake (server mode) and the transport pumps\n\t\t// each request through `mcp.dispatch`, writing back a defined response (a notification\n\t\t// sends nothing). A dispatch / send fault surfaces on the transport `error` event.\n\t\tconst ws = createNodeWebSocket({ socket, key, head, protocol: subprotocol })\n\t\tconst transport = new WebSocketServerTransport(ws)\n\t\ttransport.emitter.on('message', (message) => {\n\t\t\tif (!isJSONRPCRequest(message)) return\n\t\t\tvoid (async () => {\n\t\t\t\ttry {\n\t\t\t\t\tconst response = await mcp.dispatch(message)\n\t\t\t\t\tif (response !== undefined) await transport.send(response)\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// Surface a dispatch / send fault on the transport's `error` event — but a\n\t\t\t\t\t// user `'error'` listener that itself throws would (Emitter.emit rethrows the\n\t\t\t\t\t// first listener throw) escape this `void` async listener as an UNHANDLED\n\t\t\t\t\t// rejection and, on Node ≥15, can terminate the process. Swallow that here:\n\t\t\t\t\t// a buggy observer must never crash the server (§13).\n\t\t\t\t\ttry {\n\t\t\t\t\t\ttransport.emitter.emit('error', error)\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// A throwing `error` listener is the caller's own bug — the end of the line.\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t})()\n\t\t})\n\t\tvoid transport.start()\n\t\treturn true\n\t}\n}\n\n/**\n * Create the WebSocket CLIENT transport for an {@link import('@src/core').MCPClientInterface}\n * — a {@link ClientTransportInterface} that drives a REMOTE MCP server over a WebSocket. The\n * egress mirror of {@link createWebSocketServer} and the WebSocket sibling of {@link\n * createHTTPClientTransport}.\n *\n * @remarks\n * Hand it to `createMCPClient({ transport })`: `start()` (run by `client.connect()`) performs\n * the RFC 6455 client handshake against `options.url` (accepting a `ws://` / `wss://` or an\n * `http://` / `https://` URL — a `ws(s)` scheme is converted to `http(s)` for the underlying\n * upgrade request), validates the `Sec-WebSocket-Accept` (via `@orkestrel/websocket`'s\n * `computeWebSocketAccept`), and opens a persistent bidirectional frame channel; each JSON-RPC\n * message the client `send`s is written as one masked text frame, and each decoded reply is\n * surfaced on the transport's `message` event for the client's id correlation. Add\n * `options.headers` (e.g. an `Authorization` bearer) to reach a guarded server.\n *\n * @param options - `url` (the remote WebSocket endpoint; REQUIRED) and optional `headers`\n * merged onto the upgrade request; see {@link WebSocketClientTransportOptions}\n * @returns A working {@link ClientTransportInterface} over a WebSocket\n *\n * @example\n * ```ts\n * import { createMCPClient } from '@src/core'\n * import { createWebSocketClientTransport } from '@src/server'\n *\n * const client = createMCPClient({\n * \ttransport: createWebSocketClientTransport({ url: 'ws://localhost:3000/mcp' }),\n * })\n * await client.connect()\n * const tools = await client.tools()\n * ```\n */\nexport function createWebSocketClientTransport(\n\toptions: WebSocketClientTransportOptions,\n): ClientTransportInterface {\n\treturn new WebSocketClientTransport(options)\n}\n\n/**\n * Create the stdio CLIENT transport for an {@link import('@src/core').MCPClientInterface}\n * — a {@link ClientTransportInterface} that spawns and drives a CHILD PROCESS MCP server\n * over newline-delimited JSON-RPC on `stdin`/`stdout`, the stdio sibling of {@link\n * createHTTPClientTransport} and {@link createWebSocketClientTransport}.\n *\n * @remarks\n * Hand it to `createMCPClient({ transport })`: `start()` (run by `client.connect()`)\n * spawns `options.command` with `options.args` and `options.env`, piping its\n * `stdin`/`stdout` for the JSON-RPC channel (its `stderr` inherits the parent's for\n * diagnostics). Each JSON-RPC message the client `send`s is written as one\n * newline-terminated line to the child's `stdin`; each decoded reply line from the\n * child's `stdout` is surfaced on the transport's `message` event for the client's\n * id correlation.\n *\n * @param options - `command` (the executable to spawn; REQUIRED), optional `args`,\n * and optional `env`; see {@link StdioClientTransportOptions}\n * @returns A working {@link ClientTransportInterface} over a child process's stdio\n *\n * @example\n * ```ts\n * import { createMCPClient } from '@src/core'\n * import { createStdioClientTransport } from '@src/server'\n *\n * const client = createMCPClient({\n * \ttransport: createStdioClientTransport({ command: 'node', args: ['./server.js'] }),\n * })\n * await client.connect()\n * const tools = await client.tools()\n * ```\n */\nexport function createStdioClientTransport(\n\toptions: StdioClientTransportOptions,\n): ClientTransportInterface {\n\treturn new StdioClientTransport(options)\n}\n\n/**\n * Create the MCP stdio transport INGRESS — pumps a transport-agnostic {@link\n * MCPServerInterface} over newline-delimited JSON-RPC on `stdin`/`stdout` (or an\n * injected stream pair), the stdio mirror of {@link createWebSocketServer}.\n *\n * @remarks\n * Wraps `options.input` (default `process.stdin`) / `options.output` (default\n * `process.stdout`) in a {@link import('./transports/StdioServerTransport.js').StdioServerTransport}\n * and PUMPS: each inbound {@link import('@src/core').JSONRPCMessage} that is a\n * REQUEST runs through `mcp.dispatch`, and a defined response is written back as a\n * newline-terminated line — a NOTIFICATION (`dispatch` → `undefined`) writes\n * nothing. A non-request message is ignored. The dispatch is guarded so a\n * `dispatch` / `send` fault surfaces on the transport's `error` event rather than\n * escaping the (async) message listener.\n *\n * @param mcp - The transport-agnostic {@link MCPServerInterface} to expose over stdio\n * @param options - Optional injectable `input` / `output` streams; see\n * {@link StdioServerOptions}\n * @returns A `{ start(): void; stop(): void }` handle to arm / tear down the pump\n *\n * @example\n * ```ts\n * import { createMCPServer, createToolManager } from '@src/core'\n * import { createStdioServer } from '@src/server'\n *\n * const mcp = createMCPServer({ name: 'docs', version: '1.0.0', tools: createToolManager() })\n * createStdioServer(mcp).start() // an MCP client now connects over this process's stdio\n * ```\n */\nexport function createStdioServer(\n\tmcp: MCPServerInterface,\n\toptions?: StdioServerOptions,\n): { start(): void; stop(): void } {\n\tconst input = options?.input ?? process.stdin\n\tconst output = options?.output ?? process.stdout\n\tconst transport = new StdioServerTransport(input, output)\n\ttransport.emitter.on('message', (message) => {\n\t\tif (!isJSONRPCRequest(message)) return\n\t\tvoid (async () => {\n\t\t\ttry {\n\t\t\t\tconst response = await mcp.dispatch(message)\n\t\t\t\tif (response !== undefined) await transport.send(response)\n\t\t\t} catch (error) {\n\t\t\t\t// Surface a dispatch / send fault on the transport's `error` event — but a user\n\t\t\t\t// `'error'` listener that itself throws would (Emitter.emit rethrows the first\n\t\t\t\t// listener throw) escape this `void` async listener as an unhandled rejection.\n\t\t\t\t// Swallow that here: a buggy observer must never crash the server (§13).\n\t\t\t\ttry {\n\t\t\t\t\ttransport.emitter.emit('error', error)\n\t\t\t\t} catch {\n\t\t\t\t\t// A throwing `error` listener is the caller's own bug — the end of the line.\n\t\t\t\t}\n\t\t\t}\n\t\t})()\n\t})\n\treturn {\n\t\tstart(): void {\n\t\t\tvoid transport.start()\n\t\t},\n\t\tstop(): void {\n\t\t\tvoid transport.close()\n\t\t},\n\t}\n}\n","import type { MiddlewareHandler } from '@orkestrel/server'\nimport type { MCPSessionEntry, MCPSessionOptions, MCPSessionState } from './types.js'\nimport { isInitializeRequest, parseJSONRPCMessage } from '@src/core'\nimport { openStream } from '@orkestrel/server'\nimport { DEFAULT_MCP_PATH, MCP_SESSION_HEADER } from './constants.js'\nimport { readLastEventId, readSessionHeader, rejectUnknownSession } from './helpers.js'\nimport { MCPSession } from './MCPSession.js'\n\n/**\n * Create the native MCP session {@link MiddlewareHandler} — the plug-and-play stateful layer\n * that fronts a session-agnostic {@link import('./factories.js').createMCPRoutes}. Compose it\n * via `router.use(createMCPSession())` (or the equivalent middleware seam), mirroring any\n * other closure-scoped stateful middleware. Has NO dependency on `@orkestrel/middleware` — the\n * session store, mint-on-`initialize`, and resumable stream are all native to this package.\n *\n * @remarks\n * Owns a closure `Map<string, MCPSessionEntry>` keyed by session id, and a single request\n * `path` (default {@link DEFAULT_MCP_PATH}); a request to any other path passes straight\n * through (`next()`).\n *\n * - **`POST {path}`.** Buffers `const text = await request.text()` (so the downstream route\n * can re-read it via a freshly-built forwarded `Request`). Resolves a session via {@link\n * readSessionHeader}: a VALID id touches the entry and sets `context.state.session`; an\n * ABSENT / unknown id whose (guarded) body parses to an `initialize` request ({@link\n * isInitializeRequest}) MINTS a fresh {@link MCPSession} (`crypto.randomUUID()`, `capacity`)\n * and sets `context.state.session`; neither → {@link rejectUnknownSession} (`404`). It then\n * FORWARDS a fresh `Request` carrying the buffered `text` (`next(forwarded)`) — never the\n * already-consumed original — so the route re-reads the same body, and stamps the response\n * with {@link MCP_SESSION_HEADER}.\n * - **`GET {path}`.** Resolves the session the same way (no mint — only `initialize` mints);\n * an invalid / unknown id is the same `404`. A valid session opens the resumable\n * server→client stream via `@orkestrel/server`'s {@link import('@orkestrel/server').openStream}:\n * replays every event after the client's `Last-Event-ID` ({@link readLastEventId}) BEFORE\n * attaching the stream for live pushes, then attaches; a client disconnect (`request.signal`)\n * detaches it. Long-lived — never `end()`ed here.\n * - **`DELETE {path}`.** Resolves the session; a valid id deletes it from the store and answers\n * `204`; an invalid / unknown id is the same `404`.\n *\n * It is MECHANISM, not policy, and ADDITIVE: omit it entirely for the stateless default\n * ({@link import('./factories.js').createMCPRoutes}'s only behavior). The `path` MUST match the\n * `createMCPRoutes` `path` it fronts. The WebSocket transport is inherently one session per\n * connection (the socket IS the session), so this middleware does not apply to it.\n *\n * @typeParam TState - The consumer's `TState`, which MUST extend {@link MCPSessionState} so\n * the resolved session can be threaded through `context.state.session`\n * @param options - Optional `path` (default {@link DEFAULT_MCP_PATH}), `ttl` (idle-session\n * sweep window, ms — omit for sessions that live until an explicit `DELETE`), `capacity`\n * (the folded per-session replay-log bound), and `clock` (the deterministic epoch-ms clock;\n * defaults to `Date.now`); see {@link MCPSessionOptions}\n * @returns A {@link MiddlewareHandler} that mints / validates sessions + serves the resumable\n * `GET` / `DELETE`\n *\n * @example\n * ```ts\n * import { createMCPServer, createToolManager } from '@src/core'\n * import { createMCPRoutes, createMCPSession } from '@src/server'\n *\n * const mcp = createMCPServer({ name: 'docs', version: '1.0.0', tools: createToolManager() })\n * router.use(createMCPSession({ ttl: 60_000 })) // stateful: mint + validate + resumable GET / DELETE\n * router.add(createMCPRoutes(mcp)) // the route stays session-agnostic\n * ```\n */\nexport function createMCPSession<TState extends MCPSessionState>(\n\toptions?: MCPSessionOptions,\n): MiddlewareHandler<TState> {\n\tconst path = options?.path ?? DEFAULT_MCP_PATH\n\tconst capacity = options?.capacity\n\tconst ttl = options?.ttl\n\tconst clock = options?.clock ?? Date.now\n\tconst store = new Map<string, MCPSessionEntry>()\n\n\treturn async (request, context, next) => {\n\t\tif (context.url.pathname !== path) return next()\n\t\tsweep()\n\n\t\tif (context.method === 'GET') {\n\t\t\tconst entry = resolve(request)\n\t\t\tif (entry === undefined) return rejectUnknownSession()\n\t\t\tconst stream = openStream()\n\t\t\t// A comment write flushes the response headers immediately (the underlying node:http\n\t\t\t// response only sends headers on its first `write`/`end`) — without it a client's fetch\n\t\t\t// hangs waiting for headers until the first replay/push write, which may never come.\n\t\t\tstream.comment('open')\n\t\t\tconst lastEventId = readLastEventId(request)\n\t\t\tif (lastEventId !== undefined) {\n\t\t\t\t// Replay every event STRICTLY AFTER the client's last-seen id BEFORE attaching, so the\n\t\t\t\t// missed events arrive in order ahead of any live push.\n\t\t\t\tfor (const e of entry.session.replay(lastEventId)) {\n\t\t\t\t\tstream.write({ id: e.id, data: JSON.stringify(e.message) })\n\t\t\t\t}\n\t\t\t}\n\t\t\tentry.session.attach(stream)\n\t\t\tif (request.signal.aborted) entry.session.detach(stream)\n\t\t\telse\n\t\t\t\trequest.signal.addEventListener('abort', () => entry.session.detach(stream), { once: true })\n\t\t\treturn stream.response\n\t\t}\n\n\t\tif (context.method === 'DELETE') {\n\t\t\tconst id = readSessionHeader(request)\n\t\t\tif (id === undefined || !store.has(id)) return rejectUnknownSession()\n\t\t\tstore.delete(id)\n\t\t\treturn new Response(null, { status: 204 })\n\t\t}\n\n\t\t// POST — buffer the body once so the downstream route can re-read it via a forwarded\n\t\t// Request; only `initialize` mints a fresh session when no valid id is present.\n\t\tconst text = await request.text()\n\t\tlet entry = resolve(request)\n\t\tif (entry === undefined) {\n\t\t\tlet parsed: unknown\n\t\t\ttry {\n\t\t\t\tparsed = parseJSONRPCMessage(JSON.parse(text))\n\t\t\t} catch {\n\t\t\t\tparsed = undefined\n\t\t\t}\n\t\t\tif (parsed !== undefined && isInitializeRequest(parsed)) {\n\t\t\t\tconst session = new MCPSession(crypto.randomUUID(), { capacity })\n\t\t\t\tentry = { session, touched: clock() }\n\t\t\t\tstore.set(session.id, entry)\n\t\t\t} else {\n\t\t\t\treturn rejectUnknownSession()\n\t\t\t}\n\t\t}\n\t\tcontext.state.session = entry.session\n\t\tconst forwarded = new Request(context.url, {\n\t\t\tmethod: 'POST',\n\t\t\theaders: request.headers,\n\t\t\tbody: text,\n\t\t})\n\t\tconst response = await next(forwarded)\n\t\tresponse.headers.set(MCP_SESSION_HEADER, entry.session.id)\n\t\treturn response\n\t}\n\n\t// Resolve + touch the session named by the request's `mcp-session-id` header, or\n\t// `undefined` when the header is absent or names an unknown / evicted session.\n\tfunction resolve(request: Request): MCPSessionEntry | undefined {\n\t\tconst id = readSessionHeader(request)\n\t\tif (id === undefined) return undefined\n\t\tconst entry = store.get(id)\n\t\tif (entry === undefined) return undefined\n\t\tentry.touched = clock()\n\t\treturn entry\n\t}\n\n\t// Lazy idle-TTL sweep — no background timer (the rate-limiter lazy-window idiom): drop\n\t// every session not touched within `ttl` on the next access. Omitted entirely (a no-op)\n\t// when `ttl` is unset — sessions then live until an explicit `DELETE`.\n\tfunction sweep(): void {\n\t\tif (ttl === undefined) return\n\t\tconst cutoff = clock() - ttl\n\t\tfor (const [id, entry] of store) {\n\t\t\tif (entry.touched <= cutoff) store.delete(id)\n\t\t}\n\t}\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["#emitter","#url","#headers","#fetch","#timeout","#session","#deliver","#id","#events","#streams","#capacity","#ttl","#append","#evict","#counter","#emitter","#socket","#started","#closed","#receive","#onClose","#emitter","#url","#headers","#socket","#closed","#httpURL","#bind","#receive","#onClose","#emitter","#command","#args","#env","#child","#closed","#buffer","#receive","#onClose","#emitter","#input","#output","#started","#closed","#receive","#onClose","#buffer"],"sources":["../../../src/server/constants.ts","../../../src/server/helpers.ts","../../../src/server/transports/HTTPClientTransport.ts","../../../src/server/MCPSession.ts","../../../src/server/transports/WebSocketServerTransport.ts","../../../src/server/transports/WebSocketClientTransport.ts","../../../src/server/transports/StdioClientTransport.ts","../../../src/server/transports/StdioServerTransport.ts","../../../src/server/factories.ts","../../../src/server/middlewares.ts"],"sourcesContent":["// The MCP HTTP-transport constants (AGENTS §5 constants file) — the wire-level header\n// names, the default mount path, and the folded event-log bounds. The HEADER names are\n// the Streamable-HTTP transport's session /\n// protocol-version headers: they go LIVE when a `createMCPSession` middleware is mounted\n// (it mints the session id into `MCP_SESSION_HEADER` on `initialize` and reads it back on\n// subsequent requests); the stateless `createMCPRoutes` default neither sets nor reads\n// them. The transport-agnostic dispatch core (`src/core/mcp`) deliberately does NOT carry\n// these — header names belong to the HTTP transport, here.\n\n/**\n * The Streamable-HTTP transport header that carries the MCP session id. When a {@link\n * import('./middlewares.js').createMCPSession} middleware is mounted, it SETS this header on\n * the `initialize` response (the minted id) and READS it on every subsequent request\n * (validating the session); the stateless `createMCPRoutes` default neither sets nor reads it.\n */\nexport const MCP_SESSION_HEADER = 'mcp-session-id'\n\n/**\n * The Streamable-HTTP transport header that carries the negotiated MCP protocol version\n * on a subsequent request. The version is negotiated in the `initialize` JSON-RPC result\n * body; a stateful transport MAY additionally read this header to pin the per-request\n * protocol version (optional — the result body remains the source of truth).\n */\nexport const MCP_PROTOCOL_VERSION_HEADER = 'mcp-protocol-version'\n\n/** The default request path `createMCPRoutes` mounts the transport's `POST` route at. */\nexport const DEFAULT_MCP_PATH = '/mcp'\n\n/**\n * The WebSocket subprotocol the MCP-over-WebSocket transports negotiate — sent by the\n * client in `Sec-WebSocket-Protocol`, echoed by the server in its `101` handshake.\n *\n * @remarks\n * `createWebSocketServer` echoes it in the upgrade response and `createWebSocketClientTransport`\n * requests it, so an MCP WebSocket endpoint is distinguishable from any other WebSocket on the\n * same path. The default WebSocket upgrade path is {@link DEFAULT_MCP_PATH} (the same `'/mcp'`\n * the HTTP transport mounts at) — the upgrade is selected by the `Upgrade: websocket` header,\n * not a separate path.\n */\nexport const MCP_WEBSOCKET_SUBPROTOCOL = 'mcp'\n\n/**\n * The default capacity of a session's FOLDED resumable event log (the per-{@link\n * import('./MCPSession.js').MCPSession} replay log) — the maximum number of pushed\n * server→client messages retained for replay before the OLDEST is evicted.\n *\n * @remarks\n * Bounds the replay log's memory: only the most-recent {@link DEFAULT_MCP_SESSION_CAPACITY}\n * pushes are retained, so a client reconnecting with a `Last-Event-ID` older than that window\n * replays nothing (its cursor fell off the back). Override per `createMCPSession`'s `capacity`\n * for a deeper / shallower window.\n */\nexport const DEFAULT_MCP_SESSION_CAPACITY = 1024\n\n/**\n * The default per-event idle lifetime (ms) of a session's folded resumable event log — an\n * entry older than this is lazily evicted on the next access (no background timer), bounding\n * how far back a reconnecting client may replay.\n *\n * @remarks\n * Five minutes — a generous reconnection window for a dropped SSE stream without retaining\n * stale pushes indefinitely. The session's own idle TTL is the `createMCPSession` `ttl` knob;\n * this bounds the replay log paired with it.\n */\nexport const DEFAULT_MCP_SESSION_TTL = 300_000\n","import type { ClientTransportEventMap, JSONRPCMessage } from '@src/core'\nimport type { EmitterInterface } from '@orkestrel/emitter'\nimport type { SSEParserInterface } from '@orkestrel/sse'\nimport type { IncomingMessage } from 'node:http'\nimport type { LineExtraction } from './types.js'\nimport { createSSEParser } from '@orkestrel/sse'\nimport { JSONRPC_INVALID_REQUEST, jsonRPCError, parseJSONRPCMessage } from '@src/core'\nimport { isString } from '@orkestrel/contract'\nimport { MCP_SESSION_HEADER } from './constants.js'\n\n// The MCP server-transport helpers (AGENTS §4.3 module-scope names — no entity context).\n// The server-side reader `acceptsEventStream` reads the request's `Accept` header to\n// decide whether a Streamable-HTTP SSE response is allowed; `readSessionHeader` reads the\n// request's `mcp-session-id` header (the stateful transport's session validation);\n// `readLastEventId` reads the request's `Last-Event-ID` header (the resumable GET-SSE\n// replay cursor); the CLIENT-side reader `readEventStream` decodes a `fetch` Response's SSE\n// body back into JSON-RPC messages (the egress mirror, reusing `@orkestrel/sse`'s\n// `SSEParser`); `upgradeRequestPath` reads a raw `node:http` upgrade request's path (the\n// WebSocket transport's upgrade-path match). All are total and narrow at the boundary,\n// never `as` (AGENTS §14) — a missing / non-string Accept reads as \"no\", a missing session /\n// last-event header reads as `undefined`, a non-message SSE `data:` event is dropped, an\n// absent `url` reads as `'/'`.\n\n/**\n * Whether the request's `Accept` header opts into a Server-Sent-Events response.\n *\n * @remarks\n * Reads the fetch-standard `Request.headers.get('accept')` and returns `true` when it\n * contains `text/event-stream` (case-insensitive). The MCP `POST` handler uses it\n * (together with the `streaming` option) to pick the Streamable-HTTP SSE response\n * framing over a plain JSON body; the JSON-RPC envelope is identical either way. Total\n * — an absent / unmatched header returns `false`.\n *\n * @param request - The fetch-standard `Request`\n * @returns `true` when the client `Accept`s `text/event-stream`, else `false`\n */\nexport function acceptsEventStream(request: Request): boolean {\n\tconst accept = request.headers.get('accept')\n\tif (accept === null) return false\n\treturn accept.toLowerCase().includes('text/event-stream')\n}\n\n/**\n * Read the request's `mcp-session-id` header — the session id a stateful transport\n * validates, or `undefined` when absent.\n *\n * @remarks\n * Reads `request.headers.get(MCP_SESSION_HEADER)` — a fetch-standard `Headers` lookup\n * (single-valued by construction, never an array) — so a missing header reads as\n * `undefined` (no session). {@link import('./middlewares.js').createMCPSession} uses it on\n * every `POST` / `GET` / `DELETE` to look the session up in its closure store; an\n * `undefined` id is treated exactly like an unknown one (a `404`). Total — never throws.\n *\n * @param request - The fetch-standard `Request`\n * @returns The session id, or `undefined` when the header is absent\n */\nexport function readSessionHeader(request: Request): string | undefined {\n\tconst id = request.headers.get(MCP_SESSION_HEADER)\n\treturn id === null ? undefined : id\n}\n\n/**\n * Read the request's `Last-Event-ID` header — the SSE resume cursor a client sends when it\n * reconnects to the resumable `GET {path}` stream, or `undefined` when absent.\n *\n * @remarks\n * Reads `request.headers.get('last-event-id')` — a fetch-standard `Headers` lookup — so a\n * missing header reads as `undefined` (no resume, the stream starts fresh). The resumable\n * `GET` handler in {@link import('./middlewares.js').createMCPSession} passes a present value\n * to the session's {@link import('./types.js').MCPSessionInterface.replay} to re-deliver the\n * missed events before attaching the stream for live pushes. Total — never throws.\n *\n * @param request - The fetch-standard `Request`\n * @returns The last-event-id, or `undefined` when the header is absent\n */\nexport function readLastEventId(request: Request): string | undefined {\n\tconst id = request.headers.get('last-event-id')\n\treturn id === null ? undefined : id\n}\n\n/**\n * Build the stateful transport's \"unknown session\" rejection — an HTTP `404` carrying a\n * JSON-RPC error body.\n *\n * @remarks\n * Returns `Response.json(jsonRPCError(null, JSONRPC_INVALID_REQUEST, 'Session not found'),\n * { status: 404 })`, mirroring `createMCPRoutes`'s `400` transport-failure shape (a\n * JSON-RPC error BODY with a `null` id) but at the session-not-found status. Shared by\n * every {@link import('./middlewares.js').createMCPSession} validation site — the\n * non-`initialize` `POST` path, the resumable `GET {path}` open, and the `DELETE {path}`\n * session-end (each a missing / unknown / TTL-evicted id) — so the single `404` envelope\n * is defined once. Total — never throws.\n *\n * @returns The `404` JSON-RPC error `Response`\n */\nexport function rejectUnknownSession(): Response {\n\treturn Response.json(jsonRPCError(null, JSONRPC_INVALID_REQUEST, 'Session not found'), {\n\t\tstatus: 404,\n\t})\n}\n\n/**\n * Decode a `fetch` Response's Server-Sent-Events body into the JSON-RPC messages it\n * carried — the CLIENT-side inverse of the server's Streamable-HTTP SSE response.\n *\n * @remarks\n * Reads the whole `response.body` stream chunk-by-chunk through a `TextDecoder({\n * stream: true })` (handling a multi-byte char split across reads) and `@orkestrel/sse`'s\n * {@link SSEParserInterface} (handling a partial line / in-progress event split across\n * reads), then narrows each dispatched event's `data` to a {@link JSONRPCMessage} via\n * `parseJSONRPCMessage` (so a non-message / non-JSON `data:` event is DROPPED, never\n * thrown — total, §14). It reuses the SAME `SSEParser` the server's `openStream` seam\n * serializes against, so the wire round-trips. A `null` body (no stream) yields no\n * messages; the {@link import('./transports/HTTPClientTransport.js').HTTPClientTransport}\n * reads a request/response SSE reply (the server sends one `data:` event then ends), so\n * this drains to completion.\n *\n * @param response - The SSE `fetch` Response to decode (its `body` is read to completion)\n * @returns Every {@link JSONRPCMessage} the stream carried, in order\n */\nexport async function readEventStream(response: Response): Promise<readonly JSONRPCMessage[]> {\n\tconst body = response.body\n\tif (body === null) return []\n\tconst reader = body.getReader()\n\tconst decoder = new TextDecoder()\n\tconst parser: SSEParserInterface = createSSEParser()\n\tconst messages: JSONRPCMessage[] = []\n\ttry {\n\t\tfor (;;) {\n\t\t\tconst { done, value } = await reader.read()\n\t\t\tif (done) break\n\t\t\tfor (const event of parser.parse(decoder.decode(value, { stream: true }))) {\n\t\t\t\t// JSON-parse the event's `data` (the JSON-RPC envelope the server wrote), then\n\t\t\t\t// narrow it — a malformed / non-message payload is dropped, never thrown (§14).\n\t\t\t\tconst message = decodeEvent(event.data)\n\t\t\t\tif (message !== undefined) messages.push(message)\n\t\t\t}\n\t\t}\n\t} finally {\n\t\treader.releaseLock()\n\t}\n\treturn messages\n}\n\n/**\n * Decode one SSE event's `data` string into a {@link JSONRPCMessage}, or `undefined`\n * when it is not one — the per-event step {@link readEventStream} folds over.\n *\n * @remarks\n * `JSON.parse`s the `data` (the server serializes the JSON-RPC envelope as the event's\n * `data`) inside a try/catch and narrows the parsed value with `parseJSONRPCMessage`.\n * Total (§14): malformed JSON or a non-message value yields `undefined`, never throws.\n *\n * @param data - One SSE event's `data` payload\n * @returns The decoded {@link JSONRPCMessage}, or `undefined`\n */\nexport function decodeEvent(data: string): JSONRPCMessage | undefined {\n\ttry {\n\t\treturn parseJSONRPCMessage(JSON.parse(data))\n\t} catch {\n\t\treturn undefined\n\t}\n}\n\n/**\n * Read the path (without the query string) of a raw `node:http` protocol-upgrade request —\n * the `createWebSocketServer` upgrade-path match.\n *\n * @remarks\n * A `node:http` {@link import('node:http').IncomingMessage}'s `url` is the request TARGET\n * (`'/mcp?x=1'`), narrowed with `isString` (§14, never `as`) and defaulting to `'/'` for an\n * absent target; it is parsed against a dummy base (only the pathname matters for the upgrade\n * decision) and the `pathname` returned. The upgrade handler compares this against its\n * configured `path` to decide whether to claim the socket. Total — never throws on an\n * adversarial / absent target.\n *\n * @param request - The raw upgrade {@link import('node:http').IncomingMessage}\n * @returns The request's path (the `pathname`, no query), or `'/'` when the target is absent\n */\nexport function upgradeRequestPath(request: IncomingMessage): string {\n\tconst target = isString(request.url) ? request.url : '/'\n\treturn new URL(target, 'http://localhost').pathname\n}\n\n/**\n * Fold one more chunk of raw stdio bytes into a newline-framed buffer — the shared\n * line-framing step both stdio transports (client and server) read their inbound\n * newline-delimited JSON-RPC messages through.\n *\n * @remarks\n * Concatenates `buffer` (the carried-forward partial line from the previous call)\n * with `chunk`, splits on `'\\n'`, and returns every COMPLETE line (a `'\\r'` trailing\n * a line, from a CRLF-framed peer, is trimmed) plus the final, possibly-empty\n * fragment as the new `remainder` — the caller threads it back in as the next call's\n * `buffer`. A chunk containing no `'\\n'` yields no lines and the whole (buffer +\n * chunk) as `remainder`. Pure — no I/O, no instance state.\n *\n * @param buffer - The partial line carried forward from the previous chunk (`''` initially)\n * @param chunk - The newly-read raw bytes (already decoded to a string)\n * @returns The complete `lines` extracted (in order) and the trailing `remainder`\n */\nexport function extractLines(buffer: string, chunk: string): LineExtraction {\n\tconst combined = buffer + chunk\n\tconst parts = combined.split('\\n')\n\tconst remainder = parts[parts.length - 1] ?? ''\n\tconst lines = parts.slice(0, -1).map((line) => (line.endsWith('\\r') ? line.slice(0, -1) : line))\n\treturn { lines, remainder }\n}\n\n/**\n * Decode and deliver each complete newline-framed line onto a {@link\n * ClientTransportEventMap} emitter — the shared per-chunk dispatch step both stdio\n * transports (client and server) run their {@link extractLines} output through.\n *\n * @remarks\n * A blank line is skipped (a stray trailing newline). Every other line is decoded\n * with {@link decodeEvent} (`JSON.parse` + `parseJSONRPCMessage`, guarded); a\n * well-formed {@link JSONRPCMessage} emits `message`, a malformed / non-message line\n * emits `error` (§14 — total, never throws). Pure w.r.t. its own state — the emit is\n * the caller-owned side effect.\n *\n * @param emitter - The transport's {@link EmitterInterface} to emit `message` / `error` onto\n * @param lines - The complete lines (from {@link extractLines}) to decode and deliver\n */\nexport function dispatchLines(\n\temitter: EmitterInterface<ClientTransportEventMap>,\n\tlines: readonly string[],\n): void {\n\tfor (const line of lines) {\n\t\tif (line.length === 0) continue\n\t\tconst message = decodeEvent(line)\n\t\tif (message === undefined) {\n\t\t\temitter.emit('error', new Error('non-JSON-RPC stdio line'))\n\t\t\tcontinue\n\t\t}\n\t\temitter.emit('message', message)\n\t}\n}\n","import type { ClientTransportEventMap, ClientTransportInterface, JSONRPCMessage } from '@src/core'\nimport type { EmitterInterface } from '@orkestrel/emitter'\nimport type { HTTPClientTransportOptions } from '../types.js'\nimport { parseJSONRPCMessage } from '@src/core'\nimport { Emitter } from '@orkestrel/emitter'\nimport { MCP_SESSION_HEADER } from '../constants.js'\nimport { readEventStream } from '../helpers.js'\n\n/**\n * The HTTP CLIENT transport for the Model Context Protocol — a\n * {@link ClientTransportInterface} that drives a REMOTE Streamable-HTTP MCP server over\n * `fetch`, the egress mirror of the server's `createMCPRoutes`.\n *\n * @remarks\n * - **Request/response over `fetch`.** `send(message)` POSTs the JSON-serialized\n * message (or batch) to `options.url` with `content-type: application/json` and an\n * `Accept` of BOTH `application/json` and `text/event-stream` (so the server may\n * answer with either framing) — plus any `options.headers` (e.g. an `Authorization`\n * bearer). It then decodes the reply and emits each decoded {@link JSONRPCMessage} on\n * the `message` event the {@link import('@src/core').MCPClientInterface} subscribes\n * to.\n * - **Both reply framings.** A `200` with an `application/json` body is parsed with\n * `parseJSONRPCMessage`; a `200` with a `text/event-stream` body is decoded via the\n * `@orkestrel/sse` {@link import('@orkestrel/sse').SSEParserInterface} ({@link\n * readEventStream}) — the inverse of the server's `openStream` seam, so the wire\n * round-trips. A `202`\n * Accepted (a notification) carries no body and emits nothing.\n * - **Session echo.** `start()` / `close()` are no-ops (a request/response transport\n * holds no long-lived connection). The `mcp-session-id` response header, when a\n * STATEFUL server sends one (on `initialize`), is captured into `session` and then\n * ECHOED as the `mcp-session-id` request header on every SUBSEQUENT request — so an\n * `MCPClient` passes a stateful server's session validation. Before initialize returns\n * an id, `session` is `undefined` and no header is sent (safe against a stateless\n * server, which neither sends nor expects one).\n * - **Total at the boundary (§14).** Every reply is narrowed (`parseJSONRPCMessage`,\n * the SSE decoder) — a non-message reply is dropped, never asserted; a `fetch` /\n * decode failure surfaces on the `error` event rather than escaping `send`.\n * - **Observable (§13).** Owns the `emitter` ({@link ClientTransportEventMap}); fires\n * `message` per decoded reply, `error` on a fault, and `close` on `close()`.\n *\n * @example\n * ```ts\n * const transport = new HTTPClientTransport({ url: 'http://localhost:3000/mcp' })\n * const client = new MCPClient({ transport })\n * await client.connect()\n * ```\n */\nexport class HTTPClientTransport implements ClientTransportInterface {\n\treadonly #emitter: Emitter<ClientTransportEventMap>\n\treadonly #url: string\n\treadonly #headers: Readonly<Record<string, string>>\n\treadonly #fetch: typeof fetch\n\treadonly #timeout: number | undefined\n\t#session: string | undefined = undefined\n\n\tconstructor(options: HTTPClientTransportOptions) {\n\t\tthis.#emitter = new Emitter<ClientTransportEventMap>()\n\t\tthis.#url = options.url\n\t\tthis.#headers = options.headers ?? {}\n\t\tthis.#fetch = options.fetch ?? globalThis.fetch\n\t\tthis.#timeout = options.timeout\n\t}\n\n\tget emitter(): EmitterInterface<ClientTransportEventMap> {\n\t\treturn this.#emitter\n\t}\n\n\tget session(): string | undefined {\n\t\treturn this.#session\n\t}\n\n\tasync start(): Promise<void> {\n\t\t// A request/response transport opens no long-lived connection — `send` issues each\n\t\t// `fetch` on demand. Nothing to arm.\n\t}\n\n\tasync send(message: JSONRPCMessage | readonly JSONRPCMessage[]): Promise<void> {\n\t\tlet response: Response\n\t\ttry {\n\t\t\tresponse = await this.#fetch(this.#url, {\n\t\t\t\tmethod: 'POST',\n\t\t\t\theaders: {\n\t\t\t\t\t'content-type': 'application/json',\n\t\t\t\t\taccept: 'application/json, text/event-stream',\n\t\t\t\t\t// Echo a captured session id so a STATEFUL server validates the request; before\n\t\t\t\t\t// `initialize` returns one `#session` is undefined → no header (safe for a\n\t\t\t\t\t// stateless server). A caller `headers` key still wins (merged last).\n\t\t\t\t\t...(this.#session === undefined ? {} : { [MCP_SESSION_HEADER]: this.#session }),\n\t\t\t\t\t...this.#headers,\n\t\t\t\t},\n\t\t\t\tbody: JSON.stringify(message),\n\t\t\t\t...(this.#timeout === undefined ? {} : { signal: AbortSignal.timeout(this.#timeout) }),\n\t\t\t})\n\t\t} catch (error) {\n\t\t\t// A network-level failure (connection refused, DNS) — surface it for observation;\n\t\t\t// the client's per-request deadline still rejects the pending request.\n\t\t\tthis.#emitter.emit('error', error)\n\t\t\treturn\n\t\t}\n\t\t// Capture a server-assigned session id (a stateless server sends none) so it is echoed\n\t\t// on subsequent requests; a missing header leaves `session` unchanged.\n\t\tconst session = response.headers.get(MCP_SESSION_HEADER)\n\t\tif (session !== null) this.#session = session\n\t\tawait this.#deliver(response)\n\t}\n\n\tasync close(): Promise<void> {\n\t\tthis.#emitter.emit('close')\n\t}\n\n\t// Decode a reply and emit each carried message. A 202 (notification accepted) has no\n\t// body — emit nothing. An `application/json` body is one envelope; a `text/event-stream`\n\t// body is decoded via the core SSEParser (one or more `data:` events). A decode failure\n\t// surfaces on `error` rather than escaping.\n\tasync #deliver(response: Response): Promise<void> {\n\t\tif (response.status === 202) return\n\t\tconst type = response.headers.get('content-type') ?? ''\n\t\ttry {\n\t\t\tif (type.includes('text/event-stream')) {\n\t\t\t\tfor (const message of await readEventStream(response))\n\t\t\t\t\tthis.#emitter.emit('message', message)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (type.includes('application/json')) {\n\t\t\t\tconst message = parseJSONRPCMessage(await response.json())\n\t\t\t\tif (message !== undefined) this.#emitter.emit('message', message)\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tthis.#emitter.emit('error', error)\n\t\t}\n\t}\n}\n","import type { JSONRPCMessage } from '@src/core'\nimport type { StreamInterface } from '@orkestrel/server'\nimport type { EventStoreEntry, MCPSessionInterface, MCPSessionOptions } from './types.js'\nimport { DEFAULT_MCP_SESSION_CAPACITY, DEFAULT_MCP_SESSION_TTL } from './constants.js'\n\n/**\n * One MCP transport session — the per-session entity a {@link\n * import('./middlewares.js').createMCPSession} middleware owns, keyed by its `id`, carrying the\n * resumable server→client push channel with its bounded replay log FOLDED IN.\n *\n * @remarks\n * The single session entity (the old `SessionState` + `EventStore` merged): it holds the\n * session `id`, its OWN bounded, replayable log of pushed server→client messages (the\n * resumable GET-SSE channel — a private `#events` `Map` + a monotone `#counter`, with\n * `capacity` / `ttl` eviction, NOT a separate store), and the set of currently OPEN\n * server→client SSE streams (a resumable `GET {path}` registers via `attach`, unregisters via\n * `detach` on disconnect). Still a small entity (not a record), built minimal + extensible.\n *\n * - **`push` is the server-initiated primitive.** It APPENDS the message to the log (assigning\n * a monotone base36 event id) and FANS it out to every attached stream as one `id:`-tagged\n * SSE event (`stream.write({ id, data })`). A push with NO attached stream is still logged,\n * so a client that connects (or reconnects with a `Last-Event-ID`) LATER replays it from the\n * log. A `write` to a closed stream is a safe no-op (the {@link\n * `@orkestrel/server`'s `openStream` contract), so a just-disconnected stream that\n * has not yet been `detach`ed never throws. A replayed event and the live one carry the\n * IDENTICAL id (the log assigns it once).\n *\n * - **`replay(afterId)` is strictly-after.** It returns every retained log entry whose id sorts\n * AFTER `afterId` in append order — the missed-events list the `GET {path}` handler writes\n * before attaching the stream for live pushes. The decision for an UNKNOWN / already-evicted\n * `afterId` (the client's cursor fell off the back of the capacity window, or never existed):\n * replay NOTHING. Replaying the whole retained log would re-deliver events the client never\n * lost (its cursor is OLDER than everything retained); returning `[]` lets the handler then\n * stream only the fresh pushes that follow `attach` — the spec-sane resume.\n *\n * - **Bounded, append-ordered, plain `Map` (§21).** The log lives in ONE insertion-ordered\n * `Map<id, entry>` — insertion order IS append order IS id order, so `replay` and capacity\n * eviction both walk the map directly. NO database mirror — the log is process-local\n * transport mechanics, not durable state. `push` first drops every entry older than `ttl`\n * (lazy TTL — no background timer, the middleware's lazy-window idiom), appends, then evicts\n * the OLDEST entries until at most `capacity` remain; `replay` also runs the lazy TTL sweep\n * first, so a stale entry is never replayed.\n *\n * - **No transport coupling beyond the SSE seam.** It holds session state + the generic {@link\n * StreamInterface} handles `attach` was handed — never a raw socket, request, or response.\n * The middleware opens the stream (the spine seam) and registers it here; this class only\n * serializes a message onto the already-open streams.\n *\n * - **Injected clock.** `push` / `replay` accept an optional `now` (epoch ms), defaulting to\n * `Date.now()` — so a test drives TTL eviction with an elapsed clock rather than a real timer\n * (AGENTS §16).\n *\n * @example\n * ```ts\n * const session = new MCPSession(crypto.randomUUID())\n * session.attach(stream) // an open resumable GET-SSE stream\n * session.push({ jsonrpc: '2.0', method: 'notifications/message', params: { text: 'hi' } })\n * // → logged AND written to `stream` as an `id:`-tagged event; a reconnect replays it\n * ```\n */\nexport class MCPSession implements MCPSessionInterface {\n\treadonly #id: string\n\treadonly #events = new Map<string, EventStoreEntry>()\n\treadonly #streams = new Set<StreamInterface>()\n\treadonly #capacity: number\n\treadonly #ttl: number\n\t#counter = 0\n\n\tconstructor(id: string, options?: MCPSessionOptions) {\n\t\tthis.#id = id\n\t\tthis.#capacity = options?.capacity ?? DEFAULT_MCP_SESSION_CAPACITY\n\t\tthis.#ttl = options?.ttl ?? DEFAULT_MCP_SESSION_TTL\n\t}\n\n\tget id(): string {\n\t\treturn this.#id\n\t}\n\n\tattach(stream: StreamInterface): void {\n\t\tthis.#streams.add(stream)\n\t}\n\n\tdetach(stream: StreamInterface): void {\n\t\tthis.#streams.delete(stream)\n\t}\n\n\tpush(message: JSONRPCMessage, now = Date.now()): string {\n\t\t// Append to the log first (assigning the monotone event id), then fan the SAME id out to\n\t\t// every open stream — so a replayed event and a live one carry the identical id.\n\t\tconst id = this.#append(message, now)\n\t\tconst data = JSON.stringify(message)\n\t\tfor (const stream of this.#streams) stream.write({ id, data })\n\t\treturn id\n\t}\n\n\treplay(afterId: string, now = Date.now()): readonly EventStoreEntry[] {\n\t\tthis.#evict(now)\n\t\tconst out: EventStoreEntry[] = []\n\t\tlet found = false\n\t\tfor (const entry of this.#events.values()) {\n\t\t\t// Collect every entry STRICTLY AFTER `afterId`, in append order.\n\t\t\tif (found) out.push(entry)\n\t\t\telse if (entry.id === afterId) found = true\n\t\t}\n\t\t// An unknown / evicted `afterId` was never matched → `found` stays false → replay nothing\n\t\t// (the documented spec-sane choice; never re-deliver un-lost events).\n\t\treturn found ? out : []\n\t}\n\n\t// Append a message to the bounded replay log under a fresh monotone id, evicting stale +\n\t// over-capacity entries — the folded EventStore.append, now private to the session.\n\t#append(message: JSONRPCMessage, now: number): string {\n\t\t// Lazy TTL sweep BEFORE appending so an idle log shrinks as it is written.\n\t\tthis.#evict(now)\n\t\tthis.#counter += 1\n\t\tconst id = this.#counter.toString(36)\n\t\tthis.#events.set(id, { id, message, timestamp: now })\n\t\t// Capacity bound: drop the OLDEST entries (front of the insertion-ordered map) until at\n\t\t// most `capacity` remain — so the log is the most-recent `capacity` pushes.\n\t\twhile (this.#events.size > this.#capacity) {\n\t\t\tconst oldest = this.#events.keys().next().value\n\t\t\tif (oldest === undefined) break\n\t\t\tthis.#events.delete(oldest)\n\t\t}\n\t\treturn id\n\t}\n\n\t// Drop every entry older than the TTL. Entries are append-ordered (oldest first) and the\n\t// timestamp is monotone with insertion, so the stale run is a PREFIX — stop at the first live\n\t// entry. A non-positive ttl is treated as no expiry (nothing ever ages out by time).\n\t#evict(now: number): void {\n\t\tif (this.#ttl <= 0) return\n\t\tconst cutoff = now - this.#ttl\n\t\tfor (const [id, entry] of this.#events) {\n\t\t\tif (entry.timestamp <= cutoff) this.#events.delete(id)\n\t\t\telse break\n\t\t}\n\t}\n}\n","import type { ClientTransportEventMap, ClientTransportInterface, JSONRPCMessage } from '@src/core'\nimport type { EmitterInterface } from '@orkestrel/emitter'\nimport type { NodeWebSocketInterface } from '@orkestrel/websocket'\nimport { parseJSONRPCMessage } from '@src/core'\nimport { Emitter } from '@orkestrel/emitter'\n\n/**\n * The per-connection JSON-RPC-over-WebSocket SERVER bridge — wraps a\n * {@link NodeWebSocketInterface} (the RFC 6455 wire wrapper) as a\n * {@link ClientTransportInterface}, the bidirectional JSON-RPC message channel\n * `createWebSocketServer` pumps `mcp.dispatch` over and the egress mirror's\n * {@link import('./WebSocketClientTransport.js').WebSocketClientTransport} reuses.\n *\n * @remarks\n * - **Reuses `ClientTransportInterface` (§21).** It IS the same generic carrier the HTTP\n * client transport implements — `emitter` (`message` / `close` / `error`), `start`,\n * `send`, `close` — so the WebSocket server and client both speak ONE transport contract,\n * no near-duplicate sibling interface. `session` is `undefined` (the stateless v1; a\n * session id is the deferred sessions tier). The name keeps the role explicit even though\n * the shape is shared.\n * - **Inbound (`message`).** `start()` subscribes to the socket's `message` event; each text\n * frame is `JSON.parse`d inside a try/catch and narrowed with `parseJSONRPCMessage` — a\n * well-formed {@link JSONRPCMessage} is re-emitted on this transport's `message` event (the\n * parsed envelope the {@link import('@src/core').MCPServerInterface} pump dispatches), while\n * a non-JSON or non-message frame is surfaced on `error` and DROPPED, never thrown (§14). It\n * also bridges the socket's `close` → this transport's `close`, and the socket's `error`.\n * - **Outbound (`send`).** `send(message | messages)` writes ONE text frame per message\n * (`nodeWs.send(JSON.stringify(...))`); the underlying wrapper no-ops a write on a\n * non-open socket, so a closed connection drops silently rather than throwing.\n * - **`close()`** closes the underlying socket (the RFC 6455 close handshake) and fires the\n * transport's `close` event (idempotent — a second `close`, or a socket-driven close, emits\n * once).\n * - **Observable (§13).** Owns the `emitter` ({@link ClientTransportEventMap}); the emitter\n * isolates a listener throw (a buggy observer never corrupts the bridge). `error` is a\n * DOMAIN event (a transport-level fault), distinct from the emitter's listener-error channel.\n */\nexport class WebSocketServerTransport implements ClientTransportInterface {\n\treadonly #emitter: Emitter<ClientTransportEventMap>\n\treadonly #socket: NodeWebSocketInterface\n\t#started = false\n\t#closed = false\n\n\tconstructor(socket: NodeWebSocketInterface) {\n\t\tthis.#emitter = new Emitter<ClientTransportEventMap>()\n\t\tthis.#socket = socket\n\t}\n\n\tget emitter(): EmitterInterface<ClientTransportEventMap> {\n\t\treturn this.#emitter\n\t}\n\n\tget session(): string | undefined {\n\t\t// The stateless v1 holds no session — a server-assigned id is the deferred tier.\n\t\treturn undefined\n\t}\n\n\tasync start(): Promise<void> {\n\t\t// Arm the socket subscriptions once: a text frame becomes a `message`, the socket's\n\t\t// close / error bridge to this transport's events. Idempotent — a second `start` is a\n\t\t// no-op (the single MCPServer pump subscribes once).\n\t\tif (this.#started || this.#closed) return\n\t\tthis.#started = true\n\t\tthis.#socket.emitter.on('message', (text) => this.#receive(text))\n\t\tthis.#socket.emitter.on('close', () => this.#onClose())\n\t\tthis.#socket.emitter.on('error', (error) => this.#emitter.emit('error', error))\n\t}\n\n\tasync send(message: JSONRPCMessage | readonly JSONRPCMessage[]): Promise<void> {\n\t\t// One text frame per message (a batch is unrolled). The wrapper drops a write on a\n\t\t// non-open socket, so a closed connection is a silent no-op rather than a throw.\n\t\tconst messages = Array.isArray(message) ? message : [message]\n\t\tfor (const one of messages) this.#socket.send(JSON.stringify(one))\n\t}\n\n\tasync close(): Promise<void> {\n\t\tif (this.#closed) return\n\t\tthis.#closed = true\n\t\tthis.#socket.close()\n\t\tthis.#emitter.emit('close')\n\t}\n\n\t// Decode one inbound text frame: `JSON.parse` → `parseJSONRPCMessage`. A well-formed\n\t// message re-emits on `message`; a malformed / non-message frame surfaces on `error` and\n\t// is dropped (§14 — the bridge never throws on adversarial wire input).\n\t#receive(text: string): void {\n\t\tlet parsed: unknown\n\t\ttry {\n\t\t\tparsed = JSON.parse(text)\n\t\t} catch (error) {\n\t\t\tthis.#emitter.emit('error', error)\n\t\t\treturn\n\t\t}\n\t\tconst message = parseJSONRPCMessage(parsed)\n\t\tif (message === undefined) {\n\t\t\tthis.#emitter.emit('error', new Error('non-JSON-RPC WebSocket frame'))\n\t\t\treturn\n\t\t}\n\t\tthis.#emitter.emit('message', message)\n\t}\n\n\t// The socket closed (peer close frame, transport teardown) — fire this transport's\n\t// `close` once. A `close()` call already flipped `#closed`, so a socket-driven close\n\t// after an explicit one does not double-emit.\n\t#onClose(): void {\n\t\tif (this.#closed) return\n\t\tthis.#closed = true\n\t\tthis.#emitter.emit('close')\n\t}\n}\n","import type { ClientTransportEventMap, ClientTransportInterface, JSONRPCMessage } from '@src/core'\nimport type { EmitterInterface } from '@orkestrel/emitter'\nimport type { NodeWebSocketInterface } from '@orkestrel/websocket'\nimport type { WebSocketClientTransportOptions } from '../types.js'\nimport type { IncomingMessage } from 'node:http'\nimport type { Duplex } from 'node:stream'\nimport { randomBytes } from 'node:crypto'\nimport { request as httpRequest } from 'node:http'\nimport { request as httpsRequest } from 'node:https'\nimport { parseJSONRPCMessage } from '@src/core'\nimport { isString } from '@orkestrel/contract'\nimport { Emitter } from '@orkestrel/emitter'\nimport {\n\tcomputeWebSocketAccept,\n\tcreateNodeWebSocket,\n\tWEBSOCKET_VERSION,\n} from '@orkestrel/websocket'\nimport { MCP_WEBSOCKET_SUBPROTOCOL } from '../constants.js'\n\n/**\n * The WebSocket CLIENT transport for the Model Context Protocol — a\n * {@link ClientTransportInterface} that drives a REMOTE MCP server over a WebSocket, the\n * egress mirror of {@link import('./factories.js').createWebSocketServer} and the WebSocket\n * sibling of {@link import('./HTTPClientTransport.js').HTTPClientTransport}.\n *\n * @remarks\n * - **Persistent bidirectional channel (unlike the HTTP transport).** `start()` performs the\n * RFC 6455 client handshake: it opens a `node:http`(`s`) `GET` carrying `Connection: Upgrade`\n * / `Upgrade: websocket` / a random `Sec-WebSocket-Key` / `Sec-WebSocket-Version: 13` /\n * `Sec-WebSocket-Protocol: mcp` (plus any `options.headers`), awaits the client `'upgrade'`\n * event, and VALIDATES `Sec-WebSocket-Accept === computeWebSocketAccept(key)` (the D2 helper)\n * — a mismatch (or a non-`101` response, or a request error) REJECTS `start()` and the socket\n * is destroyed. On success it wraps the raw upgraded socket in `createNodeWebSocket({ socket,\n * head })` (CLIENT mode — no key → frames are MASKED per §5.3) and bridges its `message`.\n * - **Inbound (`message`).** Each decoded text frame is `JSON.parse`d (guarded) and narrowed\n * with `parseJSONRPCMessage` — a {@link JSONRPCMessage} re-emits on this transport's `message`\n * event (the reply the {@link import('@src/core').MCPClientInterface} correlates by `id`); a\n * non-JSON / non-message frame surfaces on `error` and is dropped (§14). The socket's `close`\n * / `error` bridge to this transport's events.\n * - **Outbound (`send`).** `send(message | messages)` writes ONE masked text frame per message.\n * - **`close()`** closes the underlying socket and fires `close` (idempotent).\n * - **URL scheme.** `options.url` accepts a `ws://` / `wss://` URL or an `http://` / `https://`\n * one; a `ws(s)` scheme is converted to `http(s)` for the underlying upgrade request (`wss`\n * → TLS via `node:https`). Either reaches the same endpoint.\n * - **Observable (§13).** Owns the `emitter` ({@link ClientTransportEventMap}); every emit\n * the emitter isolates a listener throw (a buggy observer never corrupts the transport);\n * `error` is a DOMAIN event (a transport-level fault).\n *\n * @example\n * ```ts\n * const transport = new WebSocketClientTransport({ url: 'ws://localhost:3000/mcp' })\n * const client = new MCPClient({ transport })\n * await client.connect() // start() handshakes, then the MCP initialize runs over WS frames\n * ```\n */\nexport class WebSocketClientTransport implements ClientTransportInterface {\n\treadonly #emitter: Emitter<ClientTransportEventMap>\n\treadonly #url: string\n\treadonly #headers: Readonly<Record<string, string>>\n\t#socket: NodeWebSocketInterface | undefined = undefined\n\t#closed = false\n\n\tconstructor(options: WebSocketClientTransportOptions) {\n\t\tthis.#emitter = new Emitter<ClientTransportEventMap>()\n\t\tthis.#url = options.url\n\t\tthis.#headers = options.headers ?? {}\n\t}\n\n\tget emitter(): EmitterInterface<ClientTransportEventMap> {\n\t\treturn this.#emitter\n\t}\n\n\tget session(): string | undefined {\n\t\treturn undefined\n\t}\n\n\tasync start(): Promise<void> {\n\t\t// Already connected — a second `connect()` short-circuits in the client, but guard here\n\t\t// too (idempotent open).\n\t\tif (this.#socket !== undefined) return\n\t\tthis.#closed = false\n\t\tconst url = this.#httpURL()\n\t\tconst key = randomBytes(16).toString('base64')\n\t\tconst secure = url.protocol === 'https:'\n\t\tconst send = secure ? httpsRequest : httpRequest\n\n\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\tlet settled = false\n\t\t\tconst fail = (error: Error): void => {\n\t\t\t\tif (settled) return\n\t\t\t\tsettled = true\n\t\t\t\treject(error)\n\t\t\t}\n\t\t\tconst request = send({\n\t\t\t\thostname: url.hostname,\n\t\t\t\tport: url.port.length > 0 ? Number(url.port) : secure ? 443 : 80,\n\t\t\t\tpath: `${url.pathname}${url.search}`,\n\t\t\t\theaders: {\n\t\t\t\t\tConnection: 'Upgrade',\n\t\t\t\t\tUpgrade: 'websocket',\n\t\t\t\t\t'Sec-WebSocket-Key': key,\n\t\t\t\t\t'Sec-WebSocket-Version': WEBSOCKET_VERSION,\n\t\t\t\t\t'Sec-WebSocket-Protocol': MCP_WEBSOCKET_SUBPROTOCOL,\n\t\t\t\t\t...this.#headers,\n\t\t\t\t},\n\t\t\t})\n\n\t\t\t// The server accepted the upgrade: validate the handshake accept, then wrap the\n\t\t\t// raw socket in a CLIENT-mode NodeWebSocket (masks its frames).\n\t\t\trequest.on('upgrade', (response: IncomingMessage, socket: Duplex, head: Buffer) => {\n\t\t\t\tconst accept = response.headers['sec-websocket-accept']\n\t\t\t\tif (!isString(accept) || accept !== computeWebSocketAccept(key)) {\n\t\t\t\t\tsocket.destroy()\n\t\t\t\t\tfail(new Error('WebSocket handshake failed: Sec-WebSocket-Accept mismatch'))\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst ws = createNodeWebSocket({ socket, head })\n\t\t\t\tthis.#socket = ws\n\t\t\t\tthis.#bind(ws)\n\t\t\t\tif (!settled) {\n\t\t\t\t\tsettled = true\n\t\t\t\t\tresolve()\n\t\t\t\t}\n\t\t\t})\n\n\t\t\t// A plain (non-101) response means the server declined the upgrade.\n\t\t\trequest.on('response', (response) => {\n\t\t\t\tresponse.resume()\n\t\t\t\tfail(new Error(`WebSocket upgrade declined with status ${response.statusCode ?? 0}`))\n\t\t\t})\n\t\t\t// A connection-level failure (refused, DNS, reset).\n\t\t\trequest.on('error', (error) =>\n\t\t\t\tfail(error instanceof Error ? error : new Error(String(error))),\n\t\t\t)\n\t\t\trequest.end()\n\t\t})\n\t}\n\n\tasync send(message: JSONRPCMessage | readonly JSONRPCMessage[]): Promise<void> {\n\t\tconst socket = this.#socket\n\t\tif (socket === undefined) throw new Error('WebSocket transport is not connected')\n\t\tconst messages = Array.isArray(message) ? message : [message]\n\t\tfor (const one of messages) socket.send(JSON.stringify(one))\n\t}\n\n\tasync close(): Promise<void> {\n\t\tif (this.#closed) return\n\t\tthis.#closed = true\n\t\tconst socket = this.#socket\n\t\tthis.#socket = undefined\n\t\tif (socket !== undefined) socket.close()\n\t\tthis.#emitter.emit('close')\n\t}\n\n\t// Bridge the upgraded socket's events onto the transport: a text frame → `message`\n\t// (decoded + narrowed), the socket close → `close`, a socket fault → `error`.\n\t#bind(ws: NodeWebSocketInterface): void {\n\t\tws.emitter.on('message', (text) => this.#receive(text))\n\t\tws.emitter.on('close', () => this.#onClose())\n\t\tws.emitter.on('error', (error) => this.#emitter.emit('error', error))\n\t}\n\n\t// Decode one inbound text frame: `JSON.parse` → `parseJSONRPCMessage`. A well-formed\n\t// message re-emits on `message`; a malformed / non-message frame surfaces on `error` and\n\t// is dropped (§14 — never throws on adversarial wire input).\n\t#receive(text: string): void {\n\t\tlet parsed: unknown\n\t\ttry {\n\t\t\tparsed = JSON.parse(text)\n\t\t} catch (error) {\n\t\t\tthis.#emitter.emit('error', error)\n\t\t\treturn\n\t\t}\n\t\tconst message = parseJSONRPCMessage(parsed)\n\t\tif (message === undefined) {\n\t\t\tthis.#emitter.emit('error', new Error('non-JSON-RPC WebSocket frame'))\n\t\t\treturn\n\t\t}\n\t\tthis.#emitter.emit('message', message)\n\t}\n\n\t// The socket closed underneath us — fire `close` once (a `close()` call already flipped\n\t// `#closed`, so it does not double-emit).\n\t#onClose(): void {\n\t\tif (this.#closed) return\n\t\tthis.#closed = true\n\t\tthis.#socket = undefined\n\t\tthis.#emitter.emit('close')\n\t}\n\n\t// Normalize `options.url` to the `http(s)` URL the underlying upgrade request uses: a\n\t// `ws://` → `http://`, a `wss://` → `https://`; an `http(s)://` URL passes through. Any\n\t// other scheme throws (a clear boundary error, not a silent mis-dial).\n\t#httpURL(): URL {\n\t\tconst url = new URL(this.#url)\n\t\tif (url.protocol === 'ws:') url.protocol = 'http:'\n\t\telse if (url.protocol === 'wss:') url.protocol = 'https:'\n\t\telse if (url.protocol !== 'http:' && url.protocol !== 'https:') {\n\t\t\tthrow new Error(`unsupported WebSocket URL scheme '${url.protocol}'`)\n\t\t}\n\t\treturn url\n\t}\n}\n","import type { ClientTransportEventMap, ClientTransportInterface, JSONRPCMessage } from '@src/core'\nimport type { EmitterInterface } from '@orkestrel/emitter'\nimport type { StdioClientTransportOptions } from '../types.js'\nimport type { ChildProcessByStdio } from 'node:child_process'\nimport type { Readable, Writable } from 'node:stream'\nimport { spawn } from 'node:child_process'\nimport { Emitter } from '@orkestrel/emitter'\nimport { dispatchLines, extractLines } from '../helpers.js'\n\n/**\n * The stdio CLIENT transport for the Model Context Protocol — a\n * {@link ClientTransportInterface} that drives a CHILD PROCESS MCP server over\n * newline-delimited JSON-RPC on `stdin`/`stdout`, the stdio sibling of {@link\n * import('./HTTPClientTransport.js').HTTPClientTransport} and {@link\n * import('./WebSocketClientTransport.js').WebSocketClientTransport}.\n *\n * @remarks\n * - **Spawns the server.** `start()` runs `node:child_process`'s `spawn(options.command,\n * options.args, { env: options.env, stdio: ['pipe', 'pipe', 'inherit'] })` — the\n * child's `stdin`/`stdout` are piped for the JSON-RPC channel, its `stderr` inherits\n * the parent's (diagnostics pass through, never parsed as protocol).\n * - **Inbound (`message`).** Each `stdout` chunk is folded through the shared\n * {@link extractLines} line-framing helper (buffering a partial trailing line\n * across reads); every complete line is decoded and delivered via the shared\n * {@link dispatchLines} helper — a well-formed {@link JSONRPCMessage} emits\n * `message`, a malformed line emits `error` (§14, never throws). The child's\n * `close` bridges to this transport's `close`.\n * - **Outbound (`send`).** `send(message | messages)` writes ONE newline-terminated\n * `JSON.stringify`d line per message to the child's `stdin`.\n * - **`close()`** kills the child process and fires `close` (idempotent).\n * - **Observable (§13).** Owns the `emitter` ({@link ClientTransportEventMap}); the\n * emitter isolates a listener throw; `error` is a DOMAIN event (a transport-level\n * fault), distinct from the emitter's own listener-error channel.\n *\n * @example\n * ```ts\n * const transport = new StdioClientTransport({ command: 'node', args: ['./server.js'] })\n * const client = new MCPClient({ transport })\n * await client.connect() // start() spawns the child, then the MCP initialize runs over stdio\n * ```\n */\nexport class StdioClientTransport implements ClientTransportInterface {\n\treadonly #emitter: Emitter<ClientTransportEventMap>\n\treadonly #command: string\n\treadonly #args: readonly string[]\n\treadonly #env: Readonly<Record<string, string>> | undefined\n\t#child: ChildProcessByStdio<Writable, Readable, null> | undefined = undefined\n\t#buffer = ''\n\t#closed = false\n\n\tconstructor(options: StdioClientTransportOptions) {\n\t\tthis.#emitter = new Emitter<ClientTransportEventMap>()\n\t\tthis.#command = options.command\n\t\tthis.#args = options.args ?? []\n\t\tthis.#env = options.env\n\t}\n\n\tget emitter(): EmitterInterface<ClientTransportEventMap> {\n\t\treturn this.#emitter\n\t}\n\n\tget session(): string | undefined {\n\t\treturn undefined\n\t}\n\n\tasync start(): Promise<void> {\n\t\t// Already spawned — a second `start()` (e.g. via `connect()`) short-circuits (idempotent).\n\t\tif (this.#child !== undefined) return\n\t\tthis.#closed = false\n\t\tthis.#buffer = ''\n\t\tconst child = spawn(this.#command, [...this.#args], {\n\t\t\tenv: this.#env,\n\t\t\tstdio: ['pipe', 'pipe', 'inherit'],\n\t\t})\n\t\tthis.#child = child\n\t\tchild.stdout.on('data', (chunk: Buffer | string) => this.#receive(chunk.toString()))\n\t\tchild.on('close', () => this.#onClose())\n\t\tchild.on('error', (error) => this.#emitter.emit('error', error))\n\t}\n\n\tasync send(message: JSONRPCMessage | readonly JSONRPCMessage[]): Promise<void> {\n\t\tconst child = this.#child\n\t\tif (child === undefined) throw new Error('stdio transport is not connected')\n\t\tconst messages = Array.isArray(message) ? message : [message]\n\t\tfor (const one of messages) child.stdin.write(`${JSON.stringify(one)}\\n`)\n\t}\n\n\tasync close(): Promise<void> {\n\t\tif (this.#closed) return\n\t\tthis.#closed = true\n\t\tconst child = this.#child\n\t\tthis.#child = undefined\n\t\tif (child !== undefined) child.kill()\n\t\tthis.#emitter.emit('close')\n\t}\n\n\t// Buffer a raw stdout chunk through the shared line-framing helper, then decode + deliver\n\t// every complete line onto this transport's emitter (a partial trailing line carries\n\t// forward to the next chunk).\n\t#receive(chunk: string): void {\n\t\tconst { lines, remainder } = extractLines(this.#buffer, chunk)\n\t\tthis.#buffer = remainder\n\t\tdispatchLines(this.#emitter, lines)\n\t}\n\n\t// The child process closed — fire this transport's `close` once. A `close()` call already\n\t// flipped `#closed`, so a child-driven close after an explicit one does not double-emit.\n\t#onClose(): void {\n\t\tif (this.#closed) return\n\t\tthis.#closed = true\n\t\tthis.#child = undefined\n\t\tthis.#emitter.emit('close')\n\t}\n}\n","import type { ClientTransportEventMap, ClientTransportInterface, JSONRPCMessage } from '@src/core'\nimport type { EmitterInterface } from '@orkestrel/emitter'\nimport { Emitter } from '@orkestrel/emitter'\nimport { dispatchLines, extractLines } from '../helpers.js'\n\n/**\n * The stdio SERVER transport for the Model Context Protocol — wraps an injectable\n * readable/writable stream pair (`process.stdin`/`process.stdout` in production, a\n * test double in tests) as a {@link ClientTransportInterface}, the newline-delimited\n * JSON-RPC channel {@link import('../factories.js').createStdioServer} pumps\n * `mcp.dispatch` over, the stdio mirror of {@link\n * import('./WebSocketServerTransport.js').WebSocketServerTransport}.\n *\n * @remarks\n * - **Reuses `ClientTransportInterface` (§21).** The same generic carrier the HTTP\n * and WebSocket server transports implement — `emitter` (`message` / `close` /\n * `error`), `start`, `send`, `close`. `session` is `undefined` (the stateless v1).\n * - **Inbound (`message`).** `start()` subscribes to `input`'s `data` event; each\n * chunk is folded through the shared {@link extractLines} line-framing helper\n * (buffering a partial trailing line across reads), and every complete line is\n * decoded and delivered via the shared {@link dispatchLines} helper — a\n * well-formed {@link JSONRPCMessage} re-emits on `message`, a malformed line\n * emits `error` (§14, never throws). `input`'s `close` bridges to this\n * transport's `close`.\n * - **Outbound (`send`).** `send(message | messages)` writes ONE newline-terminated\n * `JSON.stringify`d line per message to `output`.\n * - **`close()`** fires this transport's `close` (idempotent) — the injected streams\n * are owned by the caller (typically `process.stdin`/`process.stdout`, which must\n * never be closed out from under the process) and are not torn down here.\n * - **Observable (§13).** Owns the `emitter` ({@link ClientTransportEventMap}); the\n * emitter isolates a listener throw; `error` is a DOMAIN event (a transport-level\n * fault), distinct from the emitter's own listener-error channel.\n */\nexport class StdioServerTransport implements ClientTransportInterface {\n\treadonly #emitter: Emitter<ClientTransportEventMap>\n\treadonly #input: NodeJS.ReadableStream\n\treadonly #output: NodeJS.WritableStream\n\t#buffer = ''\n\t#started = false\n\t#closed = false\n\n\tconstructor(input: NodeJS.ReadableStream, output: NodeJS.WritableStream) {\n\t\tthis.#emitter = new Emitter<ClientTransportEventMap>()\n\t\tthis.#input = input\n\t\tthis.#output = output\n\t}\n\n\tget emitter(): EmitterInterface<ClientTransportEventMap> {\n\t\treturn this.#emitter\n\t}\n\n\tget session(): string | undefined {\n\t\t// The stateless v1 holds no session — a server-assigned id is the deferred tier.\n\t\treturn undefined\n\t}\n\n\tasync start(): Promise<void> {\n\t\t// Arm the stream subscriptions once: an input chunk decodes to `message`, the input's\n\t\t// close bridges to this transport's `close`. Idempotent — a second `start` is a no-op\n\t\t// (the single MCPServer pump subscribes once).\n\t\tif (this.#started || this.#closed) return\n\t\tthis.#started = true\n\t\tthis.#input.on('data', (chunk: Buffer | string) => this.#receive(chunk.toString()))\n\t\tthis.#input.on('close', () => this.#onClose())\n\t\tthis.#input.on('error', (error) => this.#emitter.emit('error', error))\n\t}\n\n\tasync send(message: JSONRPCMessage | readonly JSONRPCMessage[]): Promise<void> {\n\t\tconst messages = Array.isArray(message) ? message : [message]\n\t\tfor (const one of messages) this.#output.write(`${JSON.stringify(one)}\\n`)\n\t}\n\n\tasync close(): Promise<void> {\n\t\tif (this.#closed) return\n\t\tthis.#closed = true\n\t\tthis.#emitter.emit('close')\n\t}\n\n\t// Buffer a raw input chunk through the shared line-framing helper, then decode + deliver\n\t// every complete line onto this transport's emitter (a partial trailing line carries\n\t// forward to the next chunk).\n\t#receive(chunk: string): void {\n\t\tconst { lines, remainder } = extractLines(this.#buffer, chunk)\n\t\tthis.#buffer = remainder\n\t\tdispatchLines(this.#emitter, lines)\n\t}\n\n\t// The input stream closed (EOF, peer teardown) — fire this transport's `close` once. A\n\t// `close()` call already flipped `#closed`, so a stream-driven close after an explicit one\n\t// does not double-emit.\n\t#onClose(): void {\n\t\tif (this.#closed) return\n\t\tthis.#closed = true\n\t\tthis.#emitter.emit('close')\n\t}\n}\n","import type { ClientTransportInterface, MCPServerInterface } from '@src/core'\nimport type { RouteInput } from '@orkestrel/router'\nimport type { UpgradeHandler } from '@orkestrel/server'\nimport type {\n\tHTTPClientTransportOptions,\n\tHTTPTransportOptions,\n\tStdioClientTransportOptions,\n\tStdioServerOptions,\n\tWebSocketClientTransportOptions,\n\tWebSocketServerOptions,\n} from './types.js'\nimport type { IncomingMessage } from 'node:http'\nimport type { Duplex } from 'node:stream'\nimport {\n\tisJSONRPCRequest,\n\tJSONRPC_INVALID_REQUEST,\n\tJSONRPC_PARSE_ERROR,\n\tjsonRPCError,\n\tparseJSONRPCMessage,\n} from '@src/core'\nimport { isString } from '@orkestrel/contract'\nimport { openStream } from '@orkestrel/server'\nimport { createNodeWebSocket, WEBSOCKET_VERSION } from '@orkestrel/websocket'\nimport { DEFAULT_MCP_PATH, MCP_WEBSOCKET_SUBPROTOCOL } from './constants.js'\nimport { acceptsEventStream, upgradeRequestPath } from './helpers.js'\nimport { HTTPClientTransport } from './transports/HTTPClientTransport.js'\nimport { StdioClientTransport } from './transports/StdioClientTransport.js'\nimport { StdioServerTransport } from './transports/StdioServerTransport.js'\nimport { WebSocketClientTransport } from './transports/WebSocketClientTransport.js'\nimport { WebSocketServerTransport } from './transports/WebSocketServerTransport.js'\n\n/**\n * Create the MCP Streamable-HTTP transport routes — mounts a transport-agnostic\n * {@link MCPServerInterface} (the `@src/core` dispatch core) on the fetch-standard router\n * spine, pumping each `POST` body through `mcp.dispatch`. Returns the {@link RouteInput}s to\n * hand to `router.add(...)`.\n *\n * @remarks\n * A SINGLE `POST {path}` route — `createMCPRoutes` is STATELESS. The handler reads its own\n * request body (its own JSON parse try/catch), so it works with or without a session\n * middleware mounted in front. It draws a sharp line between TRANSPORT-level and\n * DISPATCH-level outcomes:\n *\n * - A **transport** failure — a malformed JSON body, or a parsed value that is not a\n * JSON-RPC REQUEST — is an HTTP `400` carrying a JSON-RPC error BODY (`-32700` Parse\n * error / `-32600` Invalid Request, id `null`).\n * - A **dispatch** result — a success OR an IN-BAND JSON-RPC error from `mcp.dispatch`\n * (e.g. `-32601` method-not-found) — is an HTTP `200` carrying the JSON-RPC response\n * envelope (the error is in-band per JSON-RPC, NOT an HTTP error).\n * - A **notification** (a request with no `id`, which `dispatch` resolves to\n * `undefined`) is a `202 Accepted` with no body.\n *\n * When `streaming` is enabled (the default) and the client `Accept`s `text/event-stream`,\n * the `200` reply is framed as a Streamable-HTTP SSE response (one `data:` event carrying\n * the JSON-RPC envelope, then the stream ends) via `@orkestrel/server`'s generic\n * {@link import('@orkestrel/server').openStream} seam; otherwise it is a plain JSON body.\n *\n * **Sessions are a SEPARATE, plug-and-play middleware.** `createMCPRoutes` mints / reads no\n * session id. To make the transport STATEFUL, mount {@link\n * import('./middlewares.js').createMCPSession} IN FRONT — it owns the same `path`, mints +\n * validates the `mcp-session-id`, and serves the resumable `GET {path}` + `DELETE {path}`,\n * leaving this route to dispatch the validated `POST`.\n *\n * This is MECHANISM, not policy: compose auth / CORS / rate-limiting (and the session\n * middleware) IN FRONT as ordinary middleware — the transport route adds none.\n *\n * @typeParam TState - The consumer's opaque per-request state type\n * @param mcp - The transport-agnostic {@link MCPServerInterface} to expose over HTTP\n * @param options - Optional `path` (default {@link DEFAULT_MCP_PATH}) and `streaming`\n * (default `true`); see {@link HTTPTransportOptions}\n * @returns The {@link RouteInput}s to register with the router\n *\n * @example\n * ```ts\n * import { createMCPServer, createToolManager } from '@src/core'\n * import { createMCPRoutes } from '@src/server'\n *\n * const mcp = createMCPServer({ name: 'docs', version: '1.0.0', tools: createToolManager() })\n * const routes = createMCPRoutes(mcp) // POST /mcp dispatches JSON-RPC (JSON or SSE per Accept)\n * ```\n */\nexport function createMCPRoutes<TState = unknown>(\n\tmcp: MCPServerInterface,\n\toptions?: HTTPTransportOptions,\n): readonly RouteInput<string, TState>[] {\n\tconst path = options?.path ?? DEFAULT_MCP_PATH\n\tconst streaming = options?.streaming ?? true\n\tconst post: RouteInput<string, TState> = {\n\t\tmethod: 'POST',\n\t\tpath,\n\t\tname: 'mcp',\n\t\thandler: async (request) => {\n\t\t\tlet text: string\n\t\t\ttry {\n\t\t\t\ttext = await request.text()\n\t\t\t} catch {\n\t\t\t\t// A malformed JSON body is a TRANSPORT failure — HTTP 400 + a JSON-RPC -32700.\n\t\t\t\treturn Response.json(jsonRPCError(null, JSONRPC_PARSE_ERROR, 'Parse error'), {\n\t\t\t\t\tstatus: 400,\n\t\t\t\t})\n\t\t\t}\n\t\t\tlet parsed: unknown\n\t\t\ttry {\n\t\t\t\tparsed = JSON.parse(text)\n\t\t\t} catch {\n\t\t\t\treturn Response.json(jsonRPCError(null, JSONRPC_PARSE_ERROR, 'Parse error'), {\n\t\t\t\t\tstatus: 400,\n\t\t\t\t})\n\t\t\t}\n\t\t\tconst rpcRequest = parseJSONRPCMessage(parsed)\n\t\t\tif (rpcRequest === undefined || !('method' in rpcRequest)) {\n\t\t\t\t// Not a JSON-RPC request (a response, or any non-message) — HTTP 400 + -32600.\n\t\t\t\t// `'method' in rpcRequest` narrows the message union to `JSONRPCRequest` (no `as`).\n\t\t\t\treturn Response.json(jsonRPCError(null, JSONRPC_INVALID_REQUEST, 'Invalid Request'), {\n\t\t\t\t\tstatus: 400,\n\t\t\t\t})\n\t\t\t}\n\t\t\tconst response = await mcp.dispatch(rpcRequest)\n\t\t\tif (response === undefined) {\n\t\t\t\t// A notification (no `id`) yields no response — 202 Accepted, no body.\n\t\t\t\treturn new Response(null, { status: 202 })\n\t\t\t}\n\t\t\tif (streaming && acceptsEventStream(request)) {\n\t\t\t\t// Streamable-HTTP SSE response: one `data:` event with the JSON-RPC envelope, then end.\n\t\t\t\tconst s = openStream()\n\t\t\t\ts.write({ data: JSON.stringify(response) })\n\t\t\t\ts.end()\n\t\t\t\treturn s.response\n\t\t\t}\n\t\t\t// A dispatch result — success OR an in-band JSON-RPC error — is HTTP 200 + the envelope.\n\t\t\treturn Response.json(response)\n\t\t},\n\t}\n\treturn [post]\n}\n\n/**\n * Create the HTTP CLIENT transport for an {@link import('@src/core').MCPClientInterface}\n * — a {@link ClientTransportInterface} that drives a REMOTE Streamable-HTTP MCP server\n * over `fetch`. The egress mirror of {@link createMCPRoutes}.\n *\n * @remarks\n * Hand it to `createMCPClient({ transport })`: each JSON-RPC message the client sends is\n * `POST`ed to `options.url` with `content-type: application/json` and an `Accept` of\n * both `application/json` and `text/event-stream` (the server answers with EITHER — a\n * plain JSON envelope or a Streamable-HTTP SSE `data:` event, decoded via `@orkestrel/sse`),\n * and the reply is surfaced on the transport's `message` event for the client's id\n * correlation. Add `options.headers` (e.g. an `Authorization` bearer) to reach a guarded\n * server. `start` / `close` hold no connection; against a STATEFUL server it captures the\n * `mcp-session-id` from `initialize` and echoes it on later requests, so the same\n * `MCPClient` passes session validation (a stateless server sends none).\n *\n * @param options - `url` (the remote endpoint; REQUIRED), optional `headers` merged onto\n * every request, optional `fetch` (default `globalThis.fetch`), and optional `timeout`\n * (ms, applied via `AbortSignal.timeout`); see {@link HTTPClientTransportOptions}\n * @returns A working {@link ClientTransportInterface} over `fetch`\n *\n * @example\n * ```ts\n * import { createMCPClient } from '@src/core'\n * import { createHTTPClientTransport } from '@src/server'\n *\n * const client = createMCPClient({\n * \ttransport: createHTTPClientTransport({ url: 'http://localhost:3000/mcp' }),\n * })\n * await client.connect()\n * const tools = await client.tools()\n * ```\n */\nexport function createHTTPClientTransport(\n\toptions: HTTPClientTransportOptions,\n): ClientTransportInterface {\n\treturn new HTTPClientTransport(options)\n}\n\n/**\n * Create the MCP WebSocket transport INGRESS — an {@link UpgradeHandler} that exposes a\n * transport-agnostic {@link MCPServerInterface} over a WebSocket, the WebSocket mirror of\n * {@link createMCPRoutes}. Register it on the spine's upgrade seam.\n *\n * @remarks\n * It composes the lean RFC 6455 `@orkestrel/websocket` wrapper over `@orkestrel/server`'s\n * generic upgrade seam — the spine speaks no WebSocket, this handler does.\n *\n * - **Declines (returns `false`)** when the upgrade is not for it, so the spine fans the\n * socket to the next handler (or destroys an unclaimed one): the `Upgrade` header is not\n * `websocket`, the request path is not `options.path` (default {@link DEFAULT_MCP_PATH},\n * `'/mcp'`), the `Sec-WebSocket-Key` is absent, or the `Sec-WebSocket-Version` is not `13`.\n * A decline NEVER writes to the socket (it is not yet ours) — the spine owns the unclaimed\n * outcome.\n * - **Claims (returns `true`)** otherwise: it builds `createNodeWebSocket({ socket, key, head,\n * protocol })` (SERVER mode → writes the `101` handshake, echoing the `subprotocol`, default\n * {@link MCP_WEBSOCKET_SUBPROTOCOL} `'mcp'`, and sends UNMASKED frames), wraps it in a\n * {@link WebSocketServerTransport}, and PUMPS: each inbound {@link\n * import('@src/core').JSONRPCMessage} that is a REQUEST runs through `mcp.dispatch`, and a\n * defined response is written back as a frame — a NOTIFICATION (`dispatch` → `undefined`)\n * sends nothing. A non-request message (a stray response) is ignored. The dispatch is\n * guarded so a `dispatch` / `send` fault surfaces on the transport's `error` event rather\n * than escaping the (async) message listener.\n *\n * It is MECHANISM, not policy: compose an auth guard IN FRONT by registering an upgrade\n * handler BEFORE this one — that handler can claim (decline + destroy) an unauthenticated\n * upgrade so it never reaches this pump.\n *\n * @param mcp - The transport-agnostic {@link MCPServerInterface} to expose over WebSocket\n * @param options - Optional `path` (default {@link DEFAULT_MCP_PATH}) and `subprotocol`\n * (default {@link MCP_WEBSOCKET_SUBPROTOCOL}); see {@link WebSocketServerOptions}\n * @returns An {@link UpgradeHandler} to register with the spine's `upgrade` seam\n *\n * @example\n * ```ts\n * import { createMCPServer, createToolManager } from '@src/core'\n * import { createWebSocketServer } from '@src/server'\n *\n * const mcp = createMCPServer({ name: 'docs', version: '1.0.0', tools: createToolManager() })\n * server.upgrade(createWebSocketServer(mcp)) // an MCP client now connects over ws://…/mcp\n * ```\n */\nexport function createWebSocketServer(\n\tmcp: MCPServerInterface,\n\toptions?: WebSocketServerOptions,\n): UpgradeHandler {\n\tconst path = options?.path ?? DEFAULT_MCP_PATH\n\tconst subprotocol = options?.subprotocol ?? MCP_WEBSOCKET_SUBPROTOCOL\n\treturn (request: IncomingMessage, socket: Duplex, head: Buffer): boolean => {\n\t\t// DECLINE anything that is not our MCP WebSocket upgrade — the spine fans it onward or\n\t\t// destroys it. Never touch the socket on a decline (it is not ours yet).\n\t\tconst upgrade = request.headers['upgrade']\n\t\tif (!isString(upgrade) || upgrade.toLowerCase() !== 'websocket') return false\n\t\tif (upgradeRequestPath(request) !== path) return false\n\t\tconst key = request.headers['sec-websocket-key']\n\t\tif (!isString(key)) return false\n\t\tconst version = request.headers['sec-websocket-version']\n\t\tif (!isString(version) || version !== WEBSOCKET_VERSION) return false\n\n\t\t// CLAIM: the wrapper writes the `101` handshake (server mode) and the transport pumps\n\t\t// each request through `mcp.dispatch`, writing back a defined response (a notification\n\t\t// sends nothing). A dispatch / send fault surfaces on the transport `error` event.\n\t\tconst ws = createNodeWebSocket({ socket, key, head, protocol: subprotocol })\n\t\tconst transport = new WebSocketServerTransport(ws)\n\t\ttransport.emitter.on('message', (message) => {\n\t\t\tif (!isJSONRPCRequest(message)) return\n\t\t\tvoid (async () => {\n\t\t\t\ttry {\n\t\t\t\t\tconst response = await mcp.dispatch(message)\n\t\t\t\t\tif (response !== undefined) await transport.send(response)\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// Surface a dispatch / send fault on the transport's `error` event — but a\n\t\t\t\t\t// user `'error'` listener that itself throws would (Emitter.emit rethrows the\n\t\t\t\t\t// first listener throw) escape this `void` async listener as an UNHANDLED\n\t\t\t\t\t// rejection and, on Node ≥15, can terminate the process. Swallow that here:\n\t\t\t\t\t// a buggy observer must never crash the server (§13).\n\t\t\t\t\ttry {\n\t\t\t\t\t\ttransport.emitter.emit('error', error)\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// A throwing `error` listener is the caller's own bug — the end of the line.\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t})()\n\t\t})\n\t\tvoid transport.start()\n\t\treturn true\n\t}\n}\n\n/**\n * Create the WebSocket CLIENT transport for an {@link import('@src/core').MCPClientInterface}\n * — a {@link ClientTransportInterface} that drives a REMOTE MCP server over a WebSocket. The\n * egress mirror of {@link createWebSocketServer} and the WebSocket sibling of {@link\n * createHTTPClientTransport}.\n *\n * @remarks\n * Hand it to `createMCPClient({ transport })`: `start()` (run by `client.connect()`) performs\n * the RFC 6455 client handshake against `options.url` (accepting a `ws://` / `wss://` or an\n * `http://` / `https://` URL — a `ws(s)` scheme is converted to `http(s)` for the underlying\n * upgrade request), validates the `Sec-WebSocket-Accept` (via `@orkestrel/websocket`'s\n * `computeWebSocketAccept`), and opens a persistent bidirectional frame channel; each JSON-RPC\n * message the client `send`s is written as one masked text frame, and each decoded reply is\n * surfaced on the transport's `message` event for the client's id correlation. Add\n * `options.headers` (e.g. an `Authorization` bearer) to reach a guarded server.\n *\n * @param options - `url` (the remote WebSocket endpoint; REQUIRED) and optional `headers`\n * merged onto the upgrade request; see {@link WebSocketClientTransportOptions}\n * @returns A working {@link ClientTransportInterface} over a WebSocket\n *\n * @example\n * ```ts\n * import { createMCPClient } from '@src/core'\n * import { createWebSocketClientTransport } from '@src/server'\n *\n * const client = createMCPClient({\n * \ttransport: createWebSocketClientTransport({ url: 'ws://localhost:3000/mcp' }),\n * })\n * await client.connect()\n * const tools = await client.tools()\n * ```\n */\nexport function createWebSocketClientTransport(\n\toptions: WebSocketClientTransportOptions,\n): ClientTransportInterface {\n\treturn new WebSocketClientTransport(options)\n}\n\n/**\n * Create the stdio CLIENT transport for an {@link import('@src/core').MCPClientInterface}\n * — a {@link ClientTransportInterface} that spawns and drives a CHILD PROCESS MCP server\n * over newline-delimited JSON-RPC on `stdin`/`stdout`, the stdio sibling of {@link\n * createHTTPClientTransport} and {@link createWebSocketClientTransport}.\n *\n * @remarks\n * Hand it to `createMCPClient({ transport })`: `start()` (run by `client.connect()`)\n * spawns `options.command` with `options.args` and `options.env`, piping its\n * `stdin`/`stdout` for the JSON-RPC channel (its `stderr` inherits the parent's for\n * diagnostics). Each JSON-RPC message the client `send`s is written as one\n * newline-terminated line to the child's `stdin`; each decoded reply line from the\n * child's `stdout` is surfaced on the transport's `message` event for the client's\n * id correlation.\n *\n * @param options - `command` (the executable to spawn; REQUIRED), optional `args`,\n * and optional `env`; see {@link StdioClientTransportOptions}\n * @returns A working {@link ClientTransportInterface} over a child process's stdio\n *\n * @example\n * ```ts\n * import { createMCPClient } from '@src/core'\n * import { createStdioClientTransport } from '@src/server'\n *\n * const client = createMCPClient({\n * \ttransport: createStdioClientTransport({ command: 'node', args: ['./server.js'] }),\n * })\n * await client.connect()\n * const tools = await client.tools()\n * ```\n */\nexport function createStdioClientTransport(\n\toptions: StdioClientTransportOptions,\n): ClientTransportInterface {\n\treturn new StdioClientTransport(options)\n}\n\n/**\n * Create the MCP stdio transport INGRESS — pumps a transport-agnostic {@link\n * MCPServerInterface} over newline-delimited JSON-RPC on `stdin`/`stdout` (or an\n * injected stream pair), the stdio mirror of {@link createWebSocketServer}.\n *\n * @remarks\n * Wraps `options.input` (default `process.stdin`) / `options.output` (default\n * `process.stdout`) in a {@link import('./transports/StdioServerTransport.js').StdioServerTransport}\n * and PUMPS: each inbound {@link import('@src/core').JSONRPCMessage} that is a\n * REQUEST runs through `mcp.dispatch`, and a defined response is written back as a\n * newline-terminated line — a NOTIFICATION (`dispatch` → `undefined`) writes\n * nothing. A non-request message is ignored. The dispatch is guarded so a\n * `dispatch` / `send` fault surfaces on the transport's `error` event rather than\n * escaping the (async) message listener.\n *\n * @param mcp - The transport-agnostic {@link MCPServerInterface} to expose over stdio\n * @param options - Optional injectable `input` / `output` streams; see\n * {@link StdioServerOptions}\n * @returns A `{ start(): void; stop(): void }` handle to arm / tear down the pump\n *\n * @example\n * ```ts\n * import { createMCPServer, createToolManager } from '@src/core'\n * import { createStdioServer } from '@src/server'\n *\n * const mcp = createMCPServer({ name: 'docs', version: '1.0.0', tools: createToolManager() })\n * createStdioServer(mcp).start() // an MCP client now connects over this process's stdio\n * ```\n */\nexport function createStdioServer(\n\tmcp: MCPServerInterface,\n\toptions?: StdioServerOptions,\n): { start(): void; stop(): void } {\n\tconst input = options?.input ?? process.stdin\n\tconst output = options?.output ?? process.stdout\n\tconst transport = new StdioServerTransport(input, output)\n\ttransport.emitter.on('message', (message) => {\n\t\tif (!isJSONRPCRequest(message)) return\n\t\tvoid (async () => {\n\t\t\ttry {\n\t\t\t\tconst response = await mcp.dispatch(message)\n\t\t\t\tif (response !== undefined) await transport.send(response)\n\t\t\t} catch (error) {\n\t\t\t\t// Surface a dispatch / send fault on the transport's `error` event — but a user\n\t\t\t\t// `'error'` listener that itself throws would (Emitter.emit rethrows the first\n\t\t\t\t// listener throw) escape this `void` async listener as an unhandled rejection.\n\t\t\t\t// Swallow that here: a buggy observer must never crash the server (§13).\n\t\t\t\ttry {\n\t\t\t\t\ttransport.emitter.emit('error', error)\n\t\t\t\t} catch {\n\t\t\t\t\t// A throwing `error` listener is the caller's own bug — the end of the line.\n\t\t\t\t}\n\t\t\t}\n\t\t})()\n\t})\n\treturn {\n\t\tstart(): void {\n\t\t\tvoid transport.start()\n\t\t},\n\t\tstop(): void {\n\t\t\tvoid transport.close()\n\t\t},\n\t}\n}\n","import type { MiddlewareHandler } from '@orkestrel/server'\nimport type { MCPSessionEntry, MCPSessionOptions, MCPSessionState } from './types.js'\nimport { isInitializeRequest, parseJSONRPCMessage } from '@src/core'\nimport { openStream } from '@orkestrel/server'\nimport { DEFAULT_MCP_PATH, MCP_SESSION_HEADER } from './constants.js'\nimport { readLastEventId, readSessionHeader, rejectUnknownSession } from './helpers.js'\nimport { MCPSession } from './MCPSession.js'\n\n/**\n * Create the native MCP session {@link MiddlewareHandler} — the plug-and-play stateful layer\n * that fronts a session-agnostic {@link import('./factories.js').createMCPRoutes}. Compose it\n * via `router.use(createMCPSession())` (or the equivalent middleware seam), mirroring any\n * other closure-scoped stateful middleware. Has NO dependency on `@orkestrel/middleware` — the\n * session store, mint-on-`initialize`, and resumable stream are all native to this package.\n *\n * @remarks\n * Owns a closure `Map<string, MCPSessionEntry>` keyed by session id, and a single request\n * `path` (default {@link DEFAULT_MCP_PATH}); a request to any other path passes straight\n * through (`next()`).\n *\n * - **`POST {path}`.** Buffers `const text = await request.text()` (so the downstream route\n * can re-read it via a freshly-built forwarded `Request`). Resolves a session via {@link\n * readSessionHeader}: a VALID id touches the entry and sets `context.state.session`; an\n * ABSENT / unknown id whose (guarded) body parses to an `initialize` request ({@link\n * isInitializeRequest}) MINTS a fresh {@link MCPSession} (`crypto.randomUUID()`, `capacity`)\n * and sets `context.state.session`; neither → {@link rejectUnknownSession} (`404`). It then\n * FORWARDS a fresh `Request` carrying the buffered `text` (`next(forwarded)`) — never the\n * already-consumed original — so the route re-reads the same body, and stamps the response\n * with {@link MCP_SESSION_HEADER}.\n * - **`GET {path}`.** Resolves the session the same way (no mint — only `initialize` mints);\n * an invalid / unknown id is the same `404`. A valid session opens the resumable\n * server→client stream via `@orkestrel/server`'s {@link import('@orkestrel/server').openStream}:\n * replays every event after the client's `Last-Event-ID` ({@link readLastEventId}) BEFORE\n * attaching the stream for live pushes, then attaches; a client disconnect (`request.signal`)\n * detaches it. Long-lived — never `end()`ed here.\n * - **`DELETE {path}`.** Resolves the session; a valid id deletes it from the store and answers\n * `204`; an invalid / unknown id is the same `404`.\n *\n * It is MECHANISM, not policy, and ADDITIVE: omit it entirely for the stateless default\n * ({@link import('./factories.js').createMCPRoutes}'s only behavior). The `path` MUST match the\n * `createMCPRoutes` `path` it fronts. The WebSocket transport is inherently one session per\n * connection (the socket IS the session), so this middleware does not apply to it.\n *\n * @typeParam TState - The consumer's `TState`, which MUST extend {@link MCPSessionState} so\n * the resolved session can be threaded through `context.state.session`\n * @param options - Optional `path` (default {@link DEFAULT_MCP_PATH}), `ttl` (idle-session\n * sweep window, ms — omit for sessions that live until an explicit `DELETE`), `capacity`\n * (the folded per-session replay-log bound), and `clock` (the deterministic epoch-ms clock;\n * defaults to `Date.now`); see {@link MCPSessionOptions}\n * @returns A {@link MiddlewareHandler} that mints / validates sessions + serves the resumable\n * `GET` / `DELETE`\n *\n * @example\n * ```ts\n * import { createMCPServer, createToolManager } from '@src/core'\n * import { createMCPRoutes, createMCPSession } from '@src/server'\n *\n * const mcp = createMCPServer({ name: 'docs', version: '1.0.0', tools: createToolManager() })\n * router.use(createMCPSession({ ttl: 60_000 })) // stateful: mint + validate + resumable GET / DELETE\n * router.add(createMCPRoutes(mcp)) // the route stays session-agnostic\n * ```\n */\nexport function createMCPSession<TState extends MCPSessionState>(\n\toptions?: MCPSessionOptions,\n): MiddlewareHandler<TState> {\n\tconst path = options?.path ?? DEFAULT_MCP_PATH\n\tconst capacity = options?.capacity\n\tconst ttl = options?.ttl\n\tconst clock = options?.clock ?? Date.now\n\tconst store = new Map<string, MCPSessionEntry>()\n\n\treturn async (request, context, next) => {\n\t\tif (context.url.pathname !== path) return next()\n\t\tsweep()\n\n\t\tif (context.method === 'GET') {\n\t\t\tconst entry = resolve(request)\n\t\t\tif (entry === undefined) return rejectUnknownSession()\n\t\t\tconst stream = openStream()\n\t\t\t// A comment write flushes the response headers immediately (the underlying node:http\n\t\t\t// response only sends headers on its first `write`/`end`) — without it a client's fetch\n\t\t\t// hangs waiting for headers until the first replay/push write, which may never come.\n\t\t\tstream.comment('open')\n\t\t\tconst lastEventId = readLastEventId(request)\n\t\t\tif (lastEventId !== undefined) {\n\t\t\t\t// Replay every event STRICTLY AFTER the client's last-seen id BEFORE attaching, so the\n\t\t\t\t// missed events arrive in order ahead of any live push.\n\t\t\t\tfor (const e of entry.session.replay(lastEventId)) {\n\t\t\t\t\tstream.write({ id: e.id, data: JSON.stringify(e.message) })\n\t\t\t\t}\n\t\t\t}\n\t\t\tentry.session.attach(stream)\n\t\t\tif (request.signal.aborted) entry.session.detach(stream)\n\t\t\telse\n\t\t\t\trequest.signal.addEventListener('abort', () => entry.session.detach(stream), { once: true })\n\t\t\treturn stream.response\n\t\t}\n\n\t\tif (context.method === 'DELETE') {\n\t\t\tconst id = readSessionHeader(request)\n\t\t\tif (id === undefined || !store.has(id)) return rejectUnknownSession()\n\t\t\tstore.delete(id)\n\t\t\treturn new Response(null, { status: 204 })\n\t\t}\n\n\t\t// POST — buffer the body once so the downstream route can re-read it via a forwarded\n\t\t// Request; only `initialize` mints a fresh session when no valid id is present.\n\t\tconst text = await request.text()\n\t\tlet entry = resolve(request)\n\t\tif (entry === undefined) {\n\t\t\tlet parsed: unknown\n\t\t\ttry {\n\t\t\t\tparsed = parseJSONRPCMessage(JSON.parse(text))\n\t\t\t} catch {\n\t\t\t\tparsed = undefined\n\t\t\t}\n\t\t\tif (parsed !== undefined && isInitializeRequest(parsed)) {\n\t\t\t\tconst session = new MCPSession(crypto.randomUUID(), { capacity })\n\t\t\t\tentry = { session, touched: clock() }\n\t\t\t\tstore.set(session.id, entry)\n\t\t\t} else {\n\t\t\t\treturn rejectUnknownSession()\n\t\t\t}\n\t\t}\n\t\tcontext.state.session = entry.session\n\t\tconst forwarded = new Request(context.url, {\n\t\t\tmethod: 'POST',\n\t\t\theaders: request.headers,\n\t\t\tbody: text,\n\t\t})\n\t\tconst response = await next(forwarded)\n\t\tresponse.headers.set(MCP_SESSION_HEADER, entry.session.id)\n\t\treturn response\n\t}\n\n\t// Resolve + touch the session named by the request's `mcp-session-id` header, or\n\t// `undefined` when the header is absent or names an unknown / evicted session.\n\tfunction resolve(request: Request): MCPSessionEntry | undefined {\n\t\tconst id = readSessionHeader(request)\n\t\tif (id === undefined) return undefined\n\t\tconst entry = store.get(id)\n\t\tif (entry === undefined) return undefined\n\t\tentry.touched = clock()\n\t\treturn entry\n\t}\n\n\t// Lazy idle-TTL sweep — no background timer (the rate-limiter lazy-window idiom): drop\n\t// every session not touched within `ttl` on the next access. Omitted entirely (a no-op)\n\t// when `ttl` is unset — sessions then live until an explicit `DELETE`.\n\tfunction sweep(): void {\n\t\tif (ttl === undefined) return\n\t\tconst cutoff = clock() - ttl\n\t\tfor (const [id, entry] of store) {\n\t\t\tif (entry.touched <= cutoff) store.delete(id)\n\t\t}\n\t}\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orkestrel/mcp",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "A typed Model Context Protocol client/server with pluggable HTTP, WebSocket, and stdio transports. Part of the @orkestrel line.",
5
5
  "keywords": [
6
6
  "json-rpc",
@@ -58,7 +58,7 @@
58
58
  "copy": "node -e \"const fs=require('node:fs'),p=require('node:path'),a=process.argv[1],b=process.argv[2];fs.mkdirSync(p.dirname(b),{recursive:true});fs.cpSync(a,b,{force:true});console.log('Copied: '+a+' to '+b)\"",
59
59
  "tmp:txt": "node -e \"const fs=require('node:fs'),p=require('node:path');function walk(d){for(const e of fs.readdirSync(d,{withFileTypes:true})){const f=p.join(d,e.name);if(e.isDirectory()){walk(f)}else if(!e.name.endsWith('.md')&&!e.name.endsWith('.txt')){const t=f+'.txt';if(!fs.existsSync(t)){fs.renameSync(f,t)}else{console.warn('Skipping '+f+' — target exists: '+t)}}}}try{walk('tmp')}catch(e){if(e.code!=='ENOENT')throw e}\"",
60
60
  "lint": "oxlint --config .oxlintrc.json --fix .",
61
- "check": "tsc --noEmit --project tsconfig.json",
61
+ "check": "tsc --noEmit --project tsconfig.json && npm run check:src",
62
62
  "check:src": "npm run check:src:core && npm run check:src:server",
63
63
  "check:src:core": "tsc --noEmit -p configs/src/tsconfig.core.json",
64
64
  "check:src:server": "tsc --noEmit -p configs/src/tsconfig.server.json",
@@ -74,33 +74,33 @@
74
74
  "build:src": "npm run build:src:core && npm run build:src:server",
75
75
  "build:src:core": "vite build --config configs/src/vite.core.config.ts && npm run copy dist/src/core/index.d.ts dist/src/core/index.d.cts",
76
76
  "build:src:server": "vite build --config configs/src/vite.server.config.ts && npm run copy dist/src/server/index.d.ts dist/src/server/index.d.cts",
77
- "prepublishOnly": "npm run format:check && npm run lint:check && npm run check && npm run check:src && npm run build && npm test"
77
+ "prepublishOnly": "npm run format:check && npm run lint:check && npm run check && npm run build && npm test"
78
78
  },
79
79
  "dependencies": {
80
- "@orkestrel/agent": "^0.0.4",
81
- "@orkestrel/contract": "^0.0.1",
82
- "@orkestrel/emitter": "^0.0.1",
83
- "@orkestrel/sse": "^0.0.1",
84
- "@orkestrel/websocket": "^0.0.1"
80
+ "@orkestrel/agent": "^0.0.7",
81
+ "@orkestrel/contract": "^0.0.5",
82
+ "@orkestrel/emitter": "^0.0.3",
83
+ "@orkestrel/sse": "^0.0.3",
84
+ "@orkestrel/websocket": "^0.0.3"
85
85
  },
86
86
  "devDependencies": {
87
- "@microsoft/api-extractor": "^7.58.9",
88
- "@orkestrel/guide": "^0.0.1",
89
- "@orkestrel/router": "^0.0.1",
90
- "@orkestrel/server": "^0.0.2",
87
+ "@microsoft/api-extractor": "^7.58.11",
88
+ "@orkestrel/guide": "^0.0.5",
89
+ "@orkestrel/router": "^0.0.4",
90
+ "@orkestrel/server": "^0.0.6",
91
91
  "@types/node": "^26.1.1",
92
- "oxfmt": "^0.58.0",
93
- "oxlint": "^1.73.0",
92
+ "oxfmt": "^0.59.0",
93
+ "oxlint": "^1.74.0",
94
94
  "typescript": "^6.0.3",
95
- "vite": "^8.1.4",
95
+ "vite": "^8.1.5",
96
96
  "vite-plugin-dts": "^5.0.3",
97
97
  "vitest": "^4.1.10"
98
98
  },
99
99
  "peerDependencies": {
100
- "@orkestrel/router": "^0.0.1",
101
- "@orkestrel/server": "^0.0.2"
100
+ "@orkestrel/router": "^0.0.4",
101
+ "@orkestrel/server": "^0.0.6"
102
102
  },
103
103
  "engines": {
104
- "node": ">=24"
104
+ "node": ">=22"
105
105
  }
106
106
  }