@orkestrel/mcp 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":["#port","#receive","#closed","#onMessage","#onClosed","#emitter","#url","#headers","#fetch","#timeout","#session","#protocol","#deliver","#capture","#emitter","#url","#protocols","#socket","#closed","#bind","#flush","#queue","#receive","#onClose"],"sources":["../../../src/browser/constants.ts","../../../src/browser/transports/MessagePortTransport.ts","../../../src/browser/helpers.ts","../../../src/browser/transports/HTTPClientTransport.ts","../../../src/browser/transports/WebSocketClientTransport.ts","../../../src/browser/factories.ts"],"sourcesContent":["// The MCP browser-transport constants (AGENTS §5 constants file) — the wire-level\n// header names the browser-face HTTP client transport echoes, matching the Node\n// face's session and protocol-version headers byte-for-byte. The browser face imports\n// nothing from `src/server` (peer environment faces, per AGENTS §2), so the literals\n// are declared once here too — the SAME strings, not shared symbols.\n\n/**\n * The Streamable-HTTP transport header that carries the MCP session id. The browser\n * face's {@link import('./transports/HTTPClientTransport.js').HTTPClientTransport}\n * ECHOES this header exactly like the Node face's `HTTPClientTransport`\n * (`src/server`), so the same client interoperates with an `MCPSession`-based\n * server unchanged.\n */\nexport const MCP_SESSION_HEADER = 'mcp-session-id'\n\n/**\n * The Streamable-HTTP transport header carrying the negotiated MCP protocol version\n * on every post-initialize request. The browser HTTP client captures the initialize\n * result's `protocolVersion` and sends this header on each subsequent request.\n */\nexport const MCP_PROTOCOL_VERSION_HEADER = 'mcp-protocol-version'\n\n// `serveMCP` server-identity defaults — `src/core`'s `createMCPServer` REQUIRES\n// `name`/`version`, but `ServeMCPOptions` (this face's bootstrap) makes both optional\n// (mirroring the CLIENT identity defaults, `DEFAULT_MCP_CLIENT_NAME` /\n// `DEFAULT_MCP_CLIENT_VERSION`, `src/core/constants.ts`), so `serveMCPScope` falls\n// back to these when a caller omits them.\n\n/** The default server name `serveMCPScope` reports (`initialize`'s `serverInfo.name`) when `options.name` is omitted. */\nexport const DEFAULT_MCP_SERVER_NAME = 'taverna'\n\n/** The default server version `serveMCPScope` reports (`initialize`'s `serverInfo.version`) when `options.version` is omitted. */\nexport const DEFAULT_MCP_SERVER_VERSION = '1.0.0'\n\n// The WebSocket subprotocol constant, declared here independently of the Node face's\n// `MCP_WEBSOCKET_SUBPROTOCOL` (`src/server/constants.ts`) — peer environment faces share\n// no import (AGENTS §2), so the same value is declared twice. The browser face's\n// `WebSocketClientTransport` defaults to this value when `protocols` is omitted, matching\n// `createWebSocketServer`'s unconditional echo.\n\n/**\n * The WebSocket subprotocol `createWebSocketClientTransport` requests by default —\n * `'mcp'`, matching `createWebSocketServer`'s unconditional `Sec-WebSocket-Protocol:\n * mcp` echo. Per RFC 6455 §4.1 a client MUST fail the connection if the server returns\n * a subprotocol it did not request; Node ≥ 22 (undici) enforces this strictly, so the\n * default bakes the correct value in. Override `WebSocketClientTransportOptions.protocols`\n * only when connecting to a foreign server that speaks a different subprotocol (or `[]`\n * for no subprotocol negotiation at all).\n */\nexport const MCP_WEBSOCKET_SUBPROTOCOL = 'mcp'\n","import type { MCPTransportInterface } from '@src/core'\nimport type { MessagePortTransportOptions } from '../types.js'\nimport { isString } from '@orkestrel/contract'\n\n/**\n * The browser-face `MessagePort` transport for the Model Context Protocol — a\n * {@link MCPTransportInterface} over a native `MessagePort`, the genuinely new\n * capability this face adds: MCP over `postMessage`.\n *\n * @remarks\n * - **Symmetric.** Unlike {@link import('./WebSocketClientTransport.js').WebSocketClientTransport}\n * / {@link import('./HTTPClientTransport.js').HTTPClientTransport} (CLIENT-only\n * carriers of `@src/core`'s `ClientTransportInterface`), a `MessagePort` is a\n * plain duplex channel — the SAME class implements `@src/core`'s\n * `MCPTransportInterface` and is handed to EITHER `bindServer` or\n * `bindClient`/`createDuplexClientTransport`; which role it plays comes entirely\n * from the binder it is given to, not from anything this class decides.\n * - **`start()` at construction — bind synchronously.** `MessagePort.start()` is only\n * REQUIRED when listening via `addEventListener` (as opposed to the `onmessage`\n * setter, which implies it) — this transport uses `addEventListener`, and\n * `MCPTransportInterface` has no separate open/connect step for the caller to hook\n * a start into, so the constructor calls `port.start()` immediately: the port\n * begins dispatching QUEUED messages the moment the transport exists. This is safe\n * inside `serveMCP`'s flow (the transport is synchronously handed to `bindServer`\n * before control returns to the event loop), but is a **footgun for direct use**:\n * if you construct `new MessagePortTransport({ port })` and then `await` anything\n * before calling `listen`, messages that arrived in the gap are DROPPED. **Bind\n * synchronously after construction** — do not interleave an `await` between\n * `new MessagePortTransport(…)` and `bindServer` / `listen`.\n * - **String payloads only.** `send` posts the message string as-is (`postMessage`\n * structured-clones it — a string clones to an identical string, so the wire stays\n * plain JSON-RPC text like every other transport in this package). Inbound: a\n * non-string `event.data` (a host or a misbehaving peer posting a structured\n * object) is IGNORED — dropped silently, never forwarded, never thrown (§14) —\n * because `MCPTransportInterface` carries no `error` channel for this port to\n * surface a non-string frame on (unlike `ClientTransportInterface`'s `emitter`);\n * silently ignoring is the total, contract-shaped choice.\n * - **`messageerror` is IGNORED, not routed to `closed`.** A `messageerror` event\n * (the structured-clone deserialization of an inbound message threw) reports one\n * BAD FRAME, not a dead channel — the port itself keeps working and later, well-\n * formed messages still arrive. Routing it to `closed` would tear down the\n * `bindServer`/`bindClient` wiring (and, transitively, every session it carries)\n * over a single malformed frame, which is far more destructive than dropping that\n * one frame — so this transport registers a `messageerror` listener that does\n * nothing, deliberately.\n * - **`close()`** is idempotent: it closes the underlying `port` (`MessagePort.close()`\n * disconnects it — further `postMessage` calls on EITHER end are silently\n * undelivered, per the platform contract) and fires the registered `closed`\n * handler exactly once, whether the caller closes it once or twice. There is no\n * native \"peer closed\" signal for a `MessagePort` (unlike a WebSocket's `close`\n * event) — `closed` fires ONLY from this transport's own `close()`.\n * - **Single-handler-replace (the port contract, `@src/core`'s `MCPTransportInterface`\n * doc).** `listen`/`closed` each hold the ONE currently registered handler; a\n * second call REPLACES the first rather than adding a second subscriber.\n *\n * @example\n * ```ts\n * const { port1, port2 } = new MessageChannel()\n * const serverTransport = new MessagePortTransport({ port: port1 })\n * bindServer(server, serverTransport) // port1 side dispatches inbound requests\n *\n * const clientTransport = new MessagePortTransport({ port: port2 })\n * const client = createMCPClient({ transport: createDuplexClientTransport(clientTransport) })\n * bindClient(client, clientTransport) // port2 side is the client's carrier\n * ```\n */\nexport class MessagePortTransport implements MCPTransportInterface {\n\treadonly #port: MessagePort\n\t#onMessage: ((message: string) => void) | undefined = undefined\n\t#onClosed: (() => void) | undefined = undefined\n\t#closed = false\n\n\tconstructor(options: MessagePortTransportOptions) {\n\t\tthis.#port = options.port\n\t\tthis.#port.addEventListener('message', (event: MessageEvent) => this.#receive(event.data))\n\t\tthis.#port.addEventListener('messageerror', () => {\n\t\t\t// Intentionally ignored — one bad frame, not a dead channel; see class doc.\n\t\t})\n\t\tthis.#port.start()\n\t}\n\n\tsend(message: string): void {\n\t\tif (this.#closed) return\n\t\tthis.#port.postMessage(message)\n\t}\n\n\tlisten(handler: (message: string) => void): void {\n\t\tthis.#onMessage = handler\n\t}\n\n\tclosed(handler: () => void): void {\n\t\tthis.#onClosed = handler\n\t}\n\n\tclose(): void {\n\t\tif (this.#closed) return\n\t\tthis.#closed = true\n\t\tthis.#port.close()\n\t\tthis.#onClosed?.()\n\t}\n\n\t// Decode one inbound `postMessage` payload: a non-string `data` is dropped, never\n\t// forwarded (§14 — this port carries only plain JSON-RPC text). A string reaches the\n\t// registered `listen` handler unchanged (the string IS the JSON-RPC message; parsing is\n\t// entirely the core's concern, per the port contract).\n\t#receive(data: unknown): void {\n\t\tif (!isString(data)) return\n\t\tthis.#onMessage?.(data)\n\t}\n}\n","import type { JSONRPCMessage, MCPServerInterface } from '@src/core'\nimport type { SSEParserInterface } from '@orkestrel/sse'\nimport type { ServeMCPOptions, ScopeTransportInterface } from './types.js'\nimport { bindServer, parseJSONRPCMessage } from '@src/core'\nimport { isString } from '@orkestrel/contract'\nimport { createSSEParser } from '@orkestrel/sse'\nimport { MessagePortTransport } from './transports/MessagePortTransport.js'\n\n// The MCP browser-transport helpers (AGENTS §4.3 module-scope names — no entity\n// context). `decodeEvent` and `readEventStream` are the browser face's copies of the\n// Node face's SAME-NAMED helpers (`src/server/helpers.ts`) — peer environment faces\n// (AGENTS §2) share no import, so the CLIENT-side SSE decode step (reused by\n// `transports/HTTPClientTransport.ts`) is declared once here too. Both are total and\n// narrow at the boundary, never `as` (AGENTS §14): a malformed / non-message SSE\n// `data:` event is dropped, never thrown.\n//\n// `createScopeMessageListener` is the bootstrap factory's per-event dispatcher, extracted here\n// (AGENTS §5 — no function is declared inside another function body) so\n// `serveMCPScope` merely CALLS it and stores the RETURNED closure (an ordinary\n// value assignment, not an inline function literal) for `addEventListener` /\n// `removeEventListener` to share the same reference.\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\n * event's `data`) inside a try/catch and narrows the parsed value with\n * `parseJSONRPCMessage`. Total (§14): malformed JSON or a non-message value yields\n * `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 * 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\n * `@orkestrel/sse`'s {@link SSEParserInterface} (handling a partial line / in-progress\n * event split across reads), then narrows each dispatched event's `data` to a\n * {@link JSONRPCMessage} via {@link decodeEvent} (so a non-message / non-JSON `data:`\n * event is DROPPED, never thrown — total, §14). A `null` body (no stream) yields no\n * messages; {@link import('./transports/HTTPClientTransport.js').HTTPClientTransport}\n * reads a request/response SSE reply (the server sends one `data:` event then ends),\n * so 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\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 * Build `serveMCPScope`'s `message`-event listener — the unified\n * dispatcher that routes EVERY inbound event on a hostable scope, portless or\n * port-bearing, to the right binding.\n *\n * @remarks\n * Port-bearing events (`event.ports.length > 0`) are gated by `options.accept` FIRST\n * — when the gate returns `false` the event is dropped entirely (no binding, no reply).\n * Accepted events spawn a fresh `MessagePortTransport` over `event.ports[0]`,\n * `bindServer` `server` onto it, and record a teardown (`unbind` then `transport.close()`)\n * into `teardowns`. A port that was already seen is IGNORED — repeated delivery of the\n * same `MessagePort` would create duplicate bindings over one port (→ duplicated replies),\n * so the listener tracks seen ports and silently drops repeats.\n *\n * This branch fires on EITHER a Service-Worker-shaped scope (its normal per-client\n * channel) or a dedicated-worker-shaped one that happens to receive a port-bearing event\n * (the unified design's deliberate cross-case, needing no upfront shape flag). An event\n * with NO ports and a STRING `data` is pushed onto `scopeTransport.deliver` (the\n * implicit, already-bound scope channel); any other event (no ports, non-string data)\n * is silently dropped — total (§14), never throws.\n *\n * @param server - The `MCPServerInterface` every spawned/implicit binding dispatches over\n * @param scopeTransport - The implicit scope channel (already `bindServer`-bound) portless events deliver onto\n * @param teardowns - The shared teardown set `serveMCPScope`'s dispose drains; each port-bearing event adds one entry\n * @param options - The `ServeMCPOptions` (for `options.accept`)\n * @returns The `message`-event listener to register (and later remove) on the scope\n *\n * @example\n * ```ts\n * const teardowns = new Set<() => void>()\n * const scopeTransport = createScopeTransport(scope)\n * bindServer(server, scopeTransport)\n * const onMessage = createScopeMessageListener(server, scopeTransport, teardowns, options)\n * scope.addEventListener('message', onMessage)\n * ```\n */\nexport function createScopeMessageListener(\n\tserver: MCPServerInterface,\n\tscopeTransport: ScopeTransportInterface,\n\tteardowns: Set<() => void>,\n\toptions: ServeMCPOptions,\n): (event: MessageEvent) => void {\n\tconst seen = new Set<MessagePort>()\n\treturn (event: MessageEvent): void => {\n\t\tconst ports = event.ports\n\t\tif (ports.length > 0) {\n\t\t\t// Gate: consult accept (origin/identity check) before binding.\n\t\t\tif (options.accept !== undefined && !options.accept(event)) return\n\t\t\tconst port = ports[0]\n\t\t\tif (port === undefined) return\n\t\t\t// Deduplicate: repeated delivery of the same port would create duplicate bindings.\n\t\t\tif (seen.has(port)) return\n\t\t\tseen.add(port)\n\t\t\tconst transport = new MessagePortTransport({ port })\n\t\t\tconst unbind = bindServer(server, transport)\n\t\t\tteardowns.add(() => {\n\t\t\t\tunbind()\n\t\t\t\ttransport.close()\n\t\t\t})\n\t\t\treturn\n\t\t}\n\t\tif (isString(event.data)) scopeTransport.deliver(event.data)\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 { isJSONRPCResponse, parseJSONRPCMessage, SUPPORTED_PROTOCOL_VERSIONS } from '@src/core'\nimport { isRecord, isString } from '@orkestrel/contract'\nimport { Emitter } from '@orkestrel/emitter'\nimport { MCP_PROTOCOL_VERSION_HEADER, MCP_SESSION_HEADER } from '../constants.js'\nimport { readEventStream } from '../helpers.js'\n\n/**\n * The browser-face HTTP CLIENT transport for the Model Context Protocol — a\n * {@link ClientTransportInterface} that drives a REMOTE Streamable-HTTP MCP server\n * over the native `fetch`, the browser sibling of the Node face's\n * {@link import('@src/server').HTTPClientTransport}, honoring the SAME\n * `mcp-session-id` semantics so it interoperates with an `MCPSession`-based server\n * unchanged.\n *\n * @remarks\n * - **Request/response over `fetch`.** `send(message)` POSTs the JSON-serialized\n * message 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\n * `Authorization` bearer). It then decodes the reply and emits each decoded\n * {@link JSONRPCMessage} on the `message` event the\n * {@link import('@src/core').MCPClientInterface} subscribes 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} (the browser\n * face's own `readEventStream`) — the inverse of the server's `openStream` seam, so\n * the wire round-trips. A `202` Accepted (a notification) carries no body and emits\n * nothing.\n * - **Session and protocol echo.** `start()` is a no-op (a\n * request/response transport opens no long-lived connection). The\n * `mcp-session-id` response header, when a STATEFUL server sends one (on\n * `initialize`), is captured into `session` and then ECHOED as the\n * `mcp-session-id` request header on every SUBSEQUENT request — so an\n * `MCPClient` passes a stateful server's session validation. The\n * initialize result's `protocolVersion` is likewise captured, but only\n * when it is a SUPPORTED value, and echoed as `mcp-protocol-version` on\n * every subsequent request, as required by the 2025-06-18 Streamable-HTTP\n * transport. Before initialize returns, neither captured header is sent.\n * `close()` clears the captured protocol so a reconnect's `initialize`\n * POST is headerless; the captured `session` persists across `close()`.\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\t#protocol: 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.bind(globalThis)\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): 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.#protocol === undefined\n\t\t\t\t\t\t? {}\n\t\t\t\t\t\t: { [MCP_PROTOCOL_VERSION_HEADER]: this.#protocol }),\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\t// Clear the captured protocol before emitting `close`, so a reconnect's `initialize`\n\t// POST carries no `mcp-protocol-version` header (the captured `session` is untouched).\n\tasync close(): Promise<void> {\n\t\tthis.#protocol = undefined\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 browser-face `readEventStream` (one or more `data:` events). A\n\t// decode failure 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)) this.#capture(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.#capture(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\t// Capture the negotiated SUPPORTED protocol from the initialize result before emitting\n\t// the message, so the next request carries its required protocol-version header; any\n\t// other value (missing or unsupported) is ignored and leaves `#protocol` unchanged.\n\t#capture(message: JSONRPCMessage): void {\n\t\tif (\n\t\t\tisJSONRPCResponse(message) &&\n\t\t\tisRecord(message.result) &&\n\t\t\tisString(message.result['protocolVersion']) &&\n\t\t\tSUPPORTED_PROTOCOL_VERSIONS.includes(message.result['protocolVersion'])\n\t\t) {\n\t\t\tthis.#protocol = message.result['protocolVersion']\n\t\t}\n\t\tthis.#emitter.emit('message', message)\n\t}\n}\n","import type { ClientTransportEventMap, ClientTransportInterface, JSONRPCMessage } from '@src/core'\nimport type { EmitterInterface } from '@orkestrel/emitter'\nimport type { WebSocketClientTransportOptions } from '../types.js'\nimport { parseJSONRPCMessage } from '@src/core'\nimport { isString } from '@orkestrel/contract'\nimport { Emitter } from '@orkestrel/emitter'\nimport { MCP_WEBSOCKET_SUBPROTOCOL } from '../constants.js'\n\n/**\n * The browser-face WebSocket CLIENT transport for the Model Context Protocol — a\n * {@link ClientTransportInterface} that drives a REMOTE MCP server over the native\n * `WebSocket` global, the browser sibling of the Node face's\n * {@link import('@src/server').WebSocketClientTransport}.\n *\n * @remarks\n * - **Host-performed handshake.** `start()` opens `new WebSocket(url, protocols)` and\n * waits for the native `'open'` event — the RFC 6455 handshake itself is entirely\n * the host's concern, so this transport carries none of the Node client's\n * `node:crypto` / `node:http(s)` machinery. A connection failure (the native\n * `'error'` event while not yet `OPEN`) REJECTS `start()`.\n * - **Queued sends.** `send` writes each message as one text frame immediately once\n * the socket is `OPEN`; a `send` issued before `'open'` fires (or before `start()`\n * is even called) is QUEUED and flushed, IN ORDER, the moment the socket opens —\n * so a caller need not await `start()` before calling `send`.\n * - **Inbound (`message`).** Each decoded text frame is `JSON.parse`d (guarded) and\n * narrowed with `parseJSONRPCMessage` — a well-formed {@link JSONRPCMessage}\n * re-emits on this transport's `message` event; a non-text (binary) frame or a\n * non-JSON / non-message text frame surfaces on `error` and is DROPPED (§14 — never\n * throws on adversarial wire input).\n * - **`close()`** closes the underlying socket and fires `close` (idempotent); the\n * socket's native `close` event (a server-initiated close) fires the SAME `close`\n * exactly once total — `close()` first flips the guard, so the native event never\n * double-emits. **This transport is not reusable after `close()`** — a `send` issued\n * after `close()` is silently dropped (not queued, not delivered even on a later\n * `start()`).\n * - **Observable (§13).** Owns the `emitter` ({@link ClientTransportEventMap}); every\n * emit the emitter isolates a listener throw; `error` is a DOMAIN event (a\n * 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() // the browser 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 #protocols: string | string[] | undefined\n\t#socket: WebSocket | undefined = undefined\n\t#queue: string[] = []\n\t#closed = false\n\n\tconstructor(options: WebSocketClientTransportOptions) {\n\t\tthis.#emitter = new Emitter<ClientTransportEventMap>()\n\t\tthis.#url = options.url\n\t\tconst protocols = options.protocols\n\t\t// Default to MCP_WEBSOCKET_SUBPROTOCOL when `protocols` is omitted — matching\n\t\t// createWebSocketServer's unconditional echo. An empty array means \"no subprotocol\",\n\t\t// overriding the default explicitly for foreign servers.\n\t\tthis.#protocols =\n\t\t\ttypeof protocols === 'string'\n\t\t\t\t? protocols\n\t\t\t\t: protocols === undefined\n\t\t\t\t\t? MCP_WEBSOCKET_SUBPROTOCOL\n\t\t\t\t\t: protocols.length === 0\n\t\t\t\t\t\t? undefined\n\t\t\t\t\t\t: [...protocols]\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 socket = new WebSocket(this.#url, this.#protocols)\n\t\tthis.#socket = socket\n\t\tthis.#bind(socket)\n\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\tsocket.addEventListener(\n\t\t\t\t'open',\n\t\t\t\t() => {\n\t\t\t\t\tthis.#flush(socket)\n\t\t\t\t\tresolve()\n\t\t\t\t},\n\t\t\t\t{ once: true },\n\t\t\t)\n\t\t\tsocket.addEventListener(\n\t\t\t\t'error',\n\t\t\t\t() => {\n\t\t\t\t\tif (socket.readyState !== WebSocket.OPEN) {\n\t\t\t\t\t\tthis.#socket = undefined\n\t\t\t\t\t\treject(new Error('WebSocket connection failed'))\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t{ once: true },\n\t\t\t)\n\t\t})\n\t}\n\n\tasync send(message: JSONRPCMessage): Promise<void> {\n\t\t// After close(), silently drop — never queue (a closed transport is not reusable;\n\t\t// queued messages would resurrect on a later start() which is not a supported pattern).\n\t\tif (this.#closed) return\n\t\tconst text = JSON.stringify(message)\n\t\tconst socket = this.#socket\n\t\tif (socket !== undefined && socket.readyState === WebSocket.OPEN) socket.send(text)\n\t\telse this.#queue.push(text)\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 native 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(socket: WebSocket): void {\n\t\tsocket.addEventListener('message', (event: MessageEvent) => this.#receive(event.data))\n\t\tsocket.addEventListener('close', () => this.#onClose())\n\t\tsocket.addEventListener('error', (event) => this.#emitter.emit('error', event))\n\t}\n\n\t// Write every queued (pre-open) message, in order, as the socket opens.\n\t#flush(socket: WebSocket): void {\n\t\tfor (const text of this.#queue.splice(0)) socket.send(text)\n\t}\n\n\t// Decode one inbound frame: a non-text (binary) frame is rejected without a throw; a\n\t// text frame is `JSON.parse`d → `parseJSONRPCMessage`. A well-formed message re-emits on\n\t// `message`; a malformed / non-message frame surfaces on `error` and is dropped (§14 —\n\t// never throws on adversarial wire input).\n\t#receive(data: unknown): void {\n\t\tif (!isString(data)) {\n\t\t\tthis.#emitter.emit('error', new Error('non-text WebSocket frame'))\n\t\t\treturn\n\t\t}\n\t\tlet parsed: unknown\n\t\ttry {\n\t\t\tparsed = JSON.parse(data)\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","import type { ClientTransportInterface, MCPTransportInterface } from '@src/core'\nimport type {\n\tHTTPClientTransportOptions,\n\tMessagePortTransportOptions,\n\tScopeTransportInterface,\n\tServeMCPOptions,\n\tServeMCPScopeInterface,\n\tWebSocketClientTransportOptions,\n} from './types.js'\nimport { bindServer, createMCPServer } from '@src/core'\nimport { DEFAULT_MCP_SERVER_NAME, DEFAULT_MCP_SERVER_VERSION } from './constants.js'\nimport { createScopeMessageListener } from './helpers.js'\nimport { HTTPClientTransport } from './transports/HTTPClientTransport.js'\nimport { MessagePortTransport } from './transports/MessagePortTransport.js'\nimport { WebSocketClientTransport } from './transports/WebSocketClientTransport.js'\n\n/**\n * Create the browser-face WebSocket CLIENT transport for an\n * {@link import('@src/core').MCPClientInterface} — a {@link ClientTransportInterface}\n * that drives a REMOTE MCP server over the native `WebSocket` global, the browser\n * sibling of the Node face's `createWebSocketClientTransport` (`@src/server`).\n *\n * @remarks\n * Hand it to `createMCPClient({ transport })`: `start()` (run by `client.connect()`)\n * opens `new WebSocket(options.url, options.protocols)` and awaits the native\n * `'open'` event — the RFC 6455 handshake itself is the browser's concern. Each\n * JSON-RPC message the client `send`s before the socket opens is QUEUED and flushed,\n * in order, once it does; each decoded reply is surfaced on the transport's\n * `message` event for the client's id correlation.\n *\n * @param options - `url` (the remote WebSocket endpoint; REQUIRED) and optional\n * `protocols` (the WebSocket subprotocol(s) to request); see\n * {@link WebSocketClientTransportOptions}\n * @returns A working {@link ClientTransportInterface} over the native `WebSocket`\n *\n * @example\n * ```ts\n * import { createMCPClient } from '@orkestrel/mcp'\n * import { createWebSocketClientTransport } from '@orkestrel/mcp/browser'\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 browser-face HTTP CLIENT transport for an\n * {@link import('@src/core').MCPClientInterface} — a {@link ClientTransportInterface}\n * that drives a REMOTE Streamable-HTTP MCP server over the native `fetch`, the\n * browser sibling of the Node face's `createHTTPClientTransport` (`@src/server`).\n *\n * @remarks\n * Hand it to `createMCPClient({ transport })`: each JSON-RPC message the client\n * sends is `POST`ed to `options.url` with `content-type: application/json` and an\n * `Accept` of both `application/json` and `text/event-stream` (the server answers\n * with EITHER — a plain JSON envelope or a Streamable-HTTP SSE `data:` event,\n * decoded via `@orkestrel/sse`), and the reply is surfaced on the transport's\n * `message` event for the client's id correlation. Add `options.headers` (e.g. an\n * `Authorization` bearer) to reach a guarded server. `start` / `close` hold no\n * connection; against a STATEFUL server it captures the `mcp-session-id` from\n * `initialize` and echoes it on later requests. It also captures the initialize\n * result's `protocolVersion` and sends `mcp-protocol-version` on every subsequent\n * request, so the same `MCPClient` passes the session and 2025-06-18 protocol\n * gates without caller wiring.\n *\n * @param options - `url` (the remote endpoint; REQUIRED), optional `headers` merged\n * onto every request, optional `fetch` (default `globalThis.fetch`), and optional\n * `timeout` (ms, applied via `AbortSignal.timeout`); see\n * {@link HTTPClientTransportOptions}\n * @returns A working {@link ClientTransportInterface} over the native `fetch`\n *\n * @example\n * ```ts\n * import { createMCPClient } from '@orkestrel/mcp'\n * import { createHTTPClientTransport } from '@orkestrel/mcp/browser'\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 browser-face `MessagePort` transport — a\n * {@link import('@src/core').MCPTransportInterface} over a native `MessagePort`, the\n * SYMMETRIC carrier that works as either a server or a client transport depending on\n * which binder ({@link import('@src/core').bindServer} or\n * {@link import('@src/core').bindClient}) it is handed to.\n *\n * @remarks\n * `port.start()` runs at construction (see {@link MessagePortTransport}'s doc for\n * why); inbound payloads are string-only (a non-string `postMessage` payload is\n * dropped, never thrown); `messageerror` is ignored (one bad frame does not close the\n * channel); `close()` closes the port and fires `closed` exactly once.\n *\n * @param options - `port` (the `MessagePort` half to drive; REQUIRED); see\n * {@link MessagePortTransportOptions}\n * @returns A working {@link import('@src/core').MCPTransportInterface} over the port\n *\n * @example\n * ```ts\n * import { bindServer, createMCPServer } from '@orkestrel/mcp'\n * import { createMessagePortTransport } from '@orkestrel/mcp/browser'\n *\n * const { port1, port2 } = new MessageChannel()\n * bindServer(createMCPServer({ name: 's', version: '1.0.0', tools }), createMessagePortTransport({ port: port1 }))\n * ```\n */\nexport function createMessagePortTransport(\n\toptions: MessagePortTransportOptions,\n): MCPTransportInterface {\n\treturn new MessagePortTransport(options)\n}\n\n/**\n * Adapt a hostable {@link ServeMCPScopeInterface} (`self` in a dedicated Web Worker,\n * or any structurally matching double) into a {@link ScopeTransportInterface} — the\n * implicit, portless message channel `serveMCPScope` binds for the\n * dedicated-worker shape.\n *\n * @remarks\n * `send` writes each outbound string via `scope.postMessage`. `listen`/`closed`\n * register the SINGLE handler `deliver` / the underlying close path route through —\n * `serveMCPScope`'s own `scope` `message`-event listener calls `deliver(event.data)`\n * for every portless, string-payload event (there is no native registration point on\n * the scope itself for `serveMCPScope` to hand a `listen` handler to, so `deliver` is\n * the bridge). `close()` fires the registered `closed` handler — a scope has nothing\n * physically closable, so this is the only teardown signal available.\n *\n * @param scope - The hostable scope to adapt (structurally, `self` / `globalThis`\n * inside a dedicated Web Worker)\n * @returns A {@link ScopeTransportInterface} `serveMCPScope` binds and drives via `deliver`\n *\n * @example\n * ```ts\n * const scopeTransport = createScopeTransport(self)\n * const unbind = bindServer(server, scopeTransport)\n * ```\n */\nexport function createScopeTransport(scope: ServeMCPScopeInterface): ScopeTransportInterface {\n\tlet onMessage: ((message: string) => void) | undefined\n\tlet onClosed: (() => void) | undefined\n\treturn {\n\t\tsend(message: string): void {\n\t\t\tscope.postMessage(message)\n\t\t},\n\t\tlisten(handler: (message: string) => void): void {\n\t\t\tonMessage = handler\n\t\t},\n\t\tclosed(handler: () => void): void {\n\t\t\tonClosed = handler\n\t\t},\n\t\tclose(): void {\n\t\t\tonClosed?.()\n\t\t},\n\t\tdeliver(message: string): void {\n\t\t\tonMessage?.(message)\n\t\t},\n\t}\n}\n\n/**\n * Boot an `MCPServer` inside a hostable worker scope and wire its message events to it.\n *\n * @remarks\n * Port-bearing events are gated by `options.accept`, deduplicated by port, and receive\n * their own `MessagePortTransport` binding. Portless string events use the scope's\n * implicit channel. The returned disposer removes the listener, unbinds the implicit\n * channel, and closes every accepted port binding.\n *\n * @param scope - The hostable worker scope to wire\n * @param options - The tools, optional identity, and optional port-event gate\n * @returns An idempotent disposer for every binding owned by this call\n */\nexport function serveMCPScope(scope: ServeMCPScopeInterface, options: ServeMCPOptions): () => void {\n\tconst server = createMCPServer({\n\t\ttools: options.tools,\n\t\tname: options.name ?? DEFAULT_MCP_SERVER_NAME,\n\t\tversion: options.version ?? DEFAULT_MCP_SERVER_VERSION,\n\t})\n\tconst scopeTransport = createScopeTransport(scope)\n\tconst unbindScope = bindServer(server, scopeTransport)\n\tconst teardowns = new Set<() => void>()\n\tconst onMessage = createScopeMessageListener(server, scopeTransport, teardowns, options)\n\tscope.addEventListener('message', onMessage)\n\tlet disposed = false\n\treturn () => {\n\t\tif (disposed) return\n\t\tdisposed = true\n\t\tscope.removeEventListener('message', onMessage)\n\t\tunbindScope()\n\t\tfor (const teardown of teardowns) teardown()\n\t\tteardowns.clear()\n\t}\n}\n\n/**\n * Boot an `MCPServer` inside the current hostable worker scope.\n *\n * @param options - The tools, optional identity, and optional port-event gate\n * @returns The disposer returned by {@link serveMCPScope}\n */\nexport function serveMCP(options: ServeMCPOptions): () => void {\n\treturn serveMCPScope(globalThis, options)\n}\n"],"mappings":";;;;;;;;;;;;AAaA,IAAa,qBAAqB;;;;;;AAOlC,IAAa,8BAA8B;;AAS3C,IAAa,0BAA0B;;AAGvC,IAAa,6BAA6B;;;;;;;;;;AAiB1C,IAAa,4BAA4B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACiBzC,IAAa,uBAAb,MAAmE;CAClE;CACA,aAAsD,KAAA;CACtD,YAAsC,KAAA;CACtC,UAAU;CAEV,YAAY,SAAsC;EACjD,KAAKA,QAAQ,QAAQ;EACrB,KAAKA,MAAM,iBAAiB,YAAY,UAAwB,KAAKC,SAAS,MAAM,IAAI,CAAC;EACzF,KAAKD,MAAM,iBAAiB,sBAAsB,CAElD,CAAC;EACD,KAAKA,MAAM,MAAM;CAClB;CAEA,KAAK,SAAuB;EAC3B,IAAI,KAAKE,SAAS;EAClB,KAAKF,MAAM,YAAY,OAAO;CAC/B;CAEA,OAAO,SAA0C;EAChD,KAAKG,aAAa;CACnB;CAEA,OAAO,SAA2B;EACjC,KAAKC,YAAY;CAClB;CAEA,QAAc;EACb,IAAI,KAAKF,SAAS;EAClB,KAAKA,UAAU;EACf,KAAKF,MAAM,MAAM;EACjB,KAAKI,YAAY;CAClB;CAMA,SAAS,MAAqB;EAC7B,IAAI,CAAC,SAAS,IAAI,GAAG;EACrB,KAAKD,aAAa,IAAI;CACvB;AACD;;;;;;;;;;;;;;;;AC1EA,SAAgB,YAAY,MAA0C;CACrE,IAAI;EACH,OAAO,oBAAoB,KAAK,MAAM,IAAI,CAAC;CAC5C,QAAQ;EACP;CACD;AACD;;;;;;;;;;;;;;;;;;;AAoBA,eAAsB,gBAAgB,UAAwD;CAC7F,MAAM,OAAO,SAAS;CACtB,IAAI,SAAS,MAAM,OAAO,CAAC;CAC3B,MAAM,SAAS,KAAK,UAAU;CAC9B,MAAM,UAAU,IAAI,YAAY;CAChC,MAAM,SAA6B,gBAAgB;CACnD,MAAM,WAA6B,CAAC;CACpC,IAAI;EACH,SAAS;GACR,MAAM,EAAE,MAAM,UAAU,MAAM,OAAO,KAAK;GAC1C,IAAI,MAAM;GACV,KAAK,MAAM,SAAS,OAAO,MAAM,QAAQ,OAAO,OAAO,EAAE,QAAQ,KAAK,CAAC,CAAC,GAAG;IAC1E,MAAM,UAAU,YAAY,MAAM,IAAI;IACtC,IAAI,YAAY,KAAA,GAAW,SAAS,KAAK,OAAO;GACjD;EACD;CACD,UAAU;EACT,OAAO,YAAY;CACpB;CACA,OAAO;AACR;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsCA,SAAgB,2BACf,QACA,gBACA,WACA,SACgC;CAChC,MAAM,uBAAO,IAAI,IAAiB;CAClC,QAAQ,UAA8B;EACrC,MAAM,QAAQ,MAAM;EACpB,IAAI,MAAM,SAAS,GAAG;GAErB,IAAI,QAAQ,WAAW,KAAA,KAAa,CAAC,QAAQ,OAAO,KAAK,GAAG;GAC5D,MAAM,OAAO,MAAM;GACnB,IAAI,SAAS,KAAA,GAAW;GAExB,IAAI,KAAK,IAAI,IAAI,GAAG;GACpB,KAAK,IAAI,IAAI;GACb,MAAM,YAAY,IAAI,qBAAqB,EAAE,KAAK,CAAC;GACnD,MAAM,SAAS,WAAW,QAAQ,SAAS;GAC3C,UAAU,UAAU;IACnB,OAAO;IACP,UAAU,MAAM;GACjB,CAAC;GACD;EACD;EACA,IAAI,SAAS,MAAM,IAAI,GAAG,eAAe,QAAQ,MAAM,IAAI;CAC5D;AACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC1FA,IAAa,sBAAb,MAAqE;CACpE;CACA;CACA;CACA;CACA;CACA,WAA+B,KAAA;CAC/B,YAAgC,KAAA;CAEhC,YAAY,SAAqC;EAChD,KAAKE,WAAW,IAAI,QAAiC;EACrD,KAAKC,OAAO,QAAQ;EACpB,KAAKC,WAAW,QAAQ,WAAW,CAAC;EACpC,KAAKC,SAAS,QAAQ,SAAS,WAAW,MAAM,KAAK,UAAU;EAC/D,KAAKC,WAAW,QAAQ;CACzB;CAEA,IAAI,UAAqD;EACxD,OAAO,KAAKJ;CACb;CAEA,IAAI,UAA8B;EACjC,OAAO,KAAKK;CACb;CAEA,MAAM,QAAuB,CAG7B;CAEA,MAAM,KAAK,SAAwC;EAClD,IAAI;EACJ,IAAI;GACH,WAAW,MAAM,KAAKF,OAAO,KAAKF,MAAM;IACvC,QAAQ;IACR,SAAS;KACR,gBAAgB;KAChB,QAAQ;KAIR,GAAI,KAAKI,aAAa,KAAA,IAAY,CAAC,IAAI,GAAG,qBAAqB,KAAKA,SAAS;KAC7E,GAAI,KAAKC,cAAc,KAAA,IACpB,CAAC,IACD,GAAG,8BAA8B,KAAKA,UAAU;KACnD,GAAG,KAAKJ;IACT;IACA,MAAM,KAAK,UAAU,OAAO;IAC5B,GAAI,KAAKE,aAAa,KAAA,IAAY,CAAC,IAAI,EAAE,QAAQ,YAAY,QAAQ,KAAKA,QAAQ,EAAE;GACrF,CAAC;EACF,SAAS,OAAO;GAGf,KAAKJ,SAAS,KAAK,SAAS,KAAK;GACjC;EACD;EAGA,MAAM,UAAU,SAAS,QAAQ,IAAI,kBAAkB;EACvD,IAAI,YAAY,MAAM,KAAKK,WAAW;EACtC,MAAM,KAAKE,SAAS,QAAQ;CAC7B;CAIA,MAAM,QAAuB;EAC5B,KAAKD,YAAY,KAAA;EACjB,KAAKN,SAAS,KAAK,OAAO;CAC3B;CAMA,MAAMO,SAAS,UAAmC;EACjD,IAAI,SAAS,WAAW,KAAK;EAC7B,MAAM,OAAO,SAAS,QAAQ,IAAI,cAAc,KAAK;EACrD,IAAI;GACH,IAAI,KAAK,SAAS,mBAAmB,GAAG;IACvC,KAAK,MAAM,WAAW,MAAM,gBAAgB,QAAQ,GAAG,KAAKC,SAAS,OAAO;IAC5E;GACD;GACA,IAAI,KAAK,SAAS,kBAAkB,GAAG;IACtC,MAAM,UAAU,oBAAoB,MAAM,SAAS,KAAK,CAAC;IACzD,IAAI,YAAY,KAAA,GAAW,KAAKA,SAAS,OAAO;GACjD;EACD,SAAS,OAAO;GACf,KAAKR,SAAS,KAAK,SAAS,KAAK;EAClC;CACD;CAKA,SAAS,SAA+B;EACvC,IACC,kBAAkB,OAAO,KACzB,SAAS,QAAQ,MAAM,KACvB,SAAS,QAAQ,OAAO,kBAAkB,KAC1C,4BAA4B,SAAS,QAAQ,OAAO,kBAAkB,GAEtE,KAAKM,YAAY,QAAQ,OAAO;EAEjC,KAAKN,SAAS,KAAK,WAAW,OAAO;CACtC;AACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACnHA,IAAa,2BAAb,MAA0E;CACzE;CACA;CACA;CACA,UAAiC,KAAA;CACjC,SAAmB,CAAC;CACpB,UAAU;CAEV,YAAY,SAA0C;EACrD,KAAKS,WAAW,IAAI,QAAiC;EACrD,KAAKC,OAAO,QAAQ;EACpB,MAAM,YAAY,QAAQ;EAI1B,KAAKC,aACJ,OAAO,cAAc,WAClB,YACA,cAAc,KAAA,IAAA,QAEb,UAAU,WAAW,IACpB,KAAA,IACA,CAAC,GAAG,SAAS;CACpB;CAEA,IAAI,UAAqD;EACxD,OAAO,KAAKF;CACb;CAEA,IAAI,UAA8B,CAElC;CAEA,MAAM,QAAuB;EAG5B,IAAI,KAAKG,YAAY,KAAA,GAAW;EAChC,KAAKC,UAAU;EACf,MAAM,SAAS,IAAI,UAAU,KAAKH,MAAM,KAAKC,UAAU;EACvD,KAAKC,UAAU;EACf,KAAKE,MAAM,MAAM;EACjB,MAAM,IAAI,SAAe,SAAS,WAAW;GAC5C,OAAO,iBACN,cACM;IACL,KAAKC,OAAO,MAAM;IAClB,QAAQ;GACT,GACA,EAAE,MAAM,KAAK,CACd;GACA,OAAO,iBACN,eACM;IACL,IAAI,OAAO,eAAe,UAAU,MAAM;KACzC,KAAKH,UAAU,KAAA;KACf,uBAAO,IAAI,MAAM,6BAA6B,CAAC;IAChD;GACD,GACA,EAAE,MAAM,KAAK,CACd;EACD,CAAC;CACF;CAEA,MAAM,KAAK,SAAwC;EAGlD,IAAI,KAAKC,SAAS;EAClB,MAAM,OAAO,KAAK,UAAU,OAAO;EACnC,MAAM,SAAS,KAAKD;EACpB,IAAI,WAAW,KAAA,KAAa,OAAO,eAAe,UAAU,MAAM,OAAO,KAAK,IAAI;OAC7E,KAAKI,OAAO,KAAK,IAAI;CAC3B;CAEA,MAAM,QAAuB;EAC5B,IAAI,KAAKH,SAAS;EAClB,KAAKA,UAAU;EACf,MAAM,SAAS,KAAKD;EACpB,KAAKA,UAAU,KAAA;EACf,IAAI,WAAW,KAAA,GAAW,OAAO,MAAM;EACvC,KAAKH,SAAS,KAAK,OAAO;CAC3B;CAIA,MAAM,QAAyB;EAC9B,OAAO,iBAAiB,YAAY,UAAwB,KAAKQ,SAAS,MAAM,IAAI,CAAC;EACrF,OAAO,iBAAiB,eAAe,KAAKC,SAAS,CAAC;EACtD,OAAO,iBAAiB,UAAU,UAAU,KAAKT,SAAS,KAAK,SAAS,KAAK,CAAC;CAC/E;CAGA,OAAO,QAAyB;EAC/B,KAAK,MAAM,QAAQ,KAAKO,OAAO,OAAO,CAAC,GAAG,OAAO,KAAK,IAAI;CAC3D;CAMA,SAAS,MAAqB;EAC7B,IAAI,CAAC,SAAS,IAAI,GAAG;GACpB,KAAKP,SAAS,KAAK,yBAAS,IAAI,MAAM,0BAA0B,CAAC;GACjE;EACD;EACA,IAAI;EACJ,IAAI;GACH,SAAS,KAAK,MAAM,IAAI;EACzB,SAAS,OAAO;GACf,KAAKA,SAAS,KAAK,SAAS,KAAK;GACjC;EACD;EACA,MAAM,UAAU,oBAAoB,MAAM;EAC1C,IAAI,YAAY,KAAA,GAAW;GAC1B,KAAKA,SAAS,KAAK,yBAAS,IAAI,MAAM,8BAA8B,CAAC;GACrE;EACD;EACA,KAAKA,SAAS,KAAK,WAAW,OAAO;CACtC;CAIA,WAAiB;EAChB,IAAI,KAAKI,SAAS;EAClB,KAAKA,UAAU;EACf,KAAKD,UAAU,KAAA;EACf,KAAKH,SAAS,KAAK,OAAO;CAC3B;AACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC9HA,SAAgB,+BACf,SAC2B;CAC3B,OAAO,IAAI,yBAAyB,OAAO;AAC5C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwCA,SAAgB,0BACf,SAC2B;CAC3B,OAAO,IAAI,oBAAoB,OAAO;AACvC;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,SAAgB,2BACf,SACwB;CACxB,OAAO,IAAI,qBAAqB,OAAO;AACxC;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,SAAgB,qBAAqB,OAAwD;CAC5F,IAAI;CACJ,IAAI;CACJ,OAAO;EACN,KAAK,SAAuB;GAC3B,MAAM,YAAY,OAAO;EAC1B;EACA,OAAO,SAA0C;GAChD,YAAY;EACb;EACA,OAAO,SAA2B;GACjC,WAAW;EACZ;EACA,QAAc;GACb,WAAW;EACZ;EACA,QAAQ,SAAuB;GAC9B,YAAY,OAAO;EACpB;CACD;AACD;;;;;;;;;;;;;;AAeA,SAAgB,cAAc,OAA+B,SAAsC;CAClG,MAAM,SAAS,gBAAgB;EAC9B,OAAO,QAAQ;EACf,MAAM,QAAQ,QAAA;EACd,SAAS,QAAQ,WAAA;CAClB,CAAC;CACD,MAAM,iBAAiB,qBAAqB,KAAK;CACjD,MAAM,cAAc,WAAW,QAAQ,cAAc;CACrD,MAAM,4BAAY,IAAI,IAAgB;CACtC,MAAM,YAAY,2BAA2B,QAAQ,gBAAgB,WAAW,OAAO;CACvF,MAAM,iBAAiB,WAAW,SAAS;CAC3C,IAAI,WAAW;CACf,aAAa;EACZ,IAAI,UAAU;EACd,WAAW;EACX,MAAM,oBAAoB,WAAW,SAAS;EAC9C,YAAY;EACZ,KAAK,MAAM,YAAY,WAAW,SAAS;EAC3C,UAAU,MAAM;CACjB;AACD;;;;;;;AAQA,SAAgB,SAAS,SAAsC;CAC9D,OAAO,cAAc,YAAY,OAAO;AACzC"}
@@ -6,15 +6,16 @@ let _orkestrel_agent = require("@orkestrel/agent");
6
6
  /** The MCP protocol revision this server implements (the default negotiated version). */
7
7
  var MCP_PROTOCOL_VERSION = "2025-06-18";
8
8
  /**
9
- * The MCP protocol revisions this server can negotiate — the current
10
- * {@link MCP_PROTOCOL_VERSION} plus a prior rev a client may still request.
9
+ * The MCP protocol revisions this server can negotiate.
11
10
  *
12
11
  * @remarks
13
12
  * `initialize` echoes the client's requested `protocolVersion` when it appears in
14
13
  * this list, else falls back to {@link MCP_PROTOCOL_VERSION}. Frozen so the list is
15
- * an immutable contract.
14
+ * an immutable contract. The package does not advertise `2025-03-26` because that
15
+ * revision mandates JSON-RPC batching, while this package accepts only individual
16
+ * JSON-RPC messages.
16
17
  */
17
- var SUPPORTED_PROTOCOL_VERSIONS = Object.freeze(["2025-06-18", "2025-03-26"]);
18
+ var SUPPORTED_PROTOCOL_VERSIONS = Object.freeze(["2025-06-18"]);
18
19
  /** JSON-RPC 2.0 reserved error: invalid JSON was received (the message did not parse). */
19
20
  var JSONRPC_PARSE_ERROR = -32700;
20
21
  /** JSON-RPC 2.0 reserved error: the payload was not a valid Request object. */
@@ -35,6 +36,61 @@ var DEFAULT_MCP_CLIENT_VERSION = "1.0.0";
35
36
  */
36
37
  var DEFAULT_MCP_REQUEST_TIMEOUT = 3e4;
37
38
  //#endregion
39
+ //#region src/core/errors.ts
40
+ /**
41
+ * A remote Model Context Protocol JSON-RPC error, preserving its machine-readable
42
+ * numeric code and optional structured context.
43
+ *
44
+ * @remarks
45
+ * {@link MCPClient} throws this error only for a remote JSON-RPC `error` response.
46
+ * Local lifecycle and transport conditions such as disconnects and request timeouts
47
+ * remain plain `Error`s. `context` carries the response's optional `error.data`
48
+ * unchanged and is `undefined` when the peer omitted it.
49
+ *
50
+ * @example
51
+ * ```ts
52
+ * const error = new MCPError('Method not found', -32601, { method: 'missing' })
53
+ * error.code // -32601
54
+ * error.context // { method: 'missing' }
55
+ * ```
56
+ */
57
+ var MCPError = class extends Error {
58
+ name = "MCPError";
59
+ code;
60
+ context;
61
+ /**
62
+ * Create a remote MCP protocol error.
63
+ *
64
+ * @param message - The human-readable JSON-RPC error message
65
+ * @param code - The machine-readable numeric JSON-RPC error code
66
+ * @param context - The optional JSON-RPC `error.data` payload
67
+ */
68
+ constructor(message, code, context) {
69
+ super(message);
70
+ this.code = code;
71
+ this.context = context;
72
+ }
73
+ };
74
+ /**
75
+ * Determine whether an unknown value is an {@link MCPError}.
76
+ *
77
+ * @param value - The unknown value to inspect
78
+ * @returns `true` only when the value is an `MCPError`
79
+ *
80
+ * @example
81
+ * ```ts
82
+ * isMCPError(new MCPError('Method not found', -32601)) // true
83
+ * isMCPError(new Error('Method not found')) // false
84
+ * ```
85
+ */
86
+ function isMCPError(value) {
87
+ try {
88
+ return value instanceof MCPError;
89
+ } catch {
90
+ return false;
91
+ }
92
+ }
93
+ //#endregion
38
94
  //#region src/core/validators.ts
39
95
  /**
40
96
  * Determine whether a value is a valid JSON-RPC REQUEST `id` — a string, a number,
@@ -280,6 +336,132 @@ function initializeResult(name, version, requested) {
280
336
  }
281
337
  };
282
338
  }
339
+ /**
340
+ * Pipe an {@link MCPTransportInterface} into an {@link MCPServerInterface} — every
341
+ * inbound message runs through `server.handle`, and a defined reply is written back
342
+ * via `transport.send`.
343
+ *
344
+ * @remarks
345
+ * `server.handle` already turns a malformed message into a serialized `-32700` /
346
+ * `-32600` reply and a notification into `undefined` (no reply), so this binder adds
347
+ * no parsing of its own. A `transport.send` throw or rejection is caught and routed
348
+ * to `server.emitter`'s `error` event (never rethrown, never an unhandled rejection);
349
+ * a listener on that event that itself throws is swallowed (the end of the line —
350
+ * the caller's own bug, never this binder's). The returned unbind DETACHES this
351
+ * binder (further inbound messages and the transport's `closed` signal are ignored)
352
+ * WITHOUT closing the transport — closing is the caller's decision.
353
+ *
354
+ * `listen`/`closed` are REPLACE semantics (§ port contract): the returned unbind
355
+ * DETACHES by replacing this binder's own handlers with no-ops, so a subsequent
356
+ * `bindServer` call on the SAME transport is never double-dispatched by a stale
357
+ * subscription left behind — an unbind→rebind cycle yields exactly one reply per
358
+ * request.
359
+ *
360
+ * @param server - The transport-agnostic server to dispatch inbound messages over
361
+ * @param transport - The duplex channel to pipe the server over
362
+ * @returns Detach this binder from the transport (does not close it)
363
+ *
364
+ * @example
365
+ * ```ts
366
+ * const unbind = bindServer(server, transport)
367
+ * // ... later, detach without closing:
368
+ * unbind()
369
+ * ```
370
+ */
371
+ function bindServer(server, transport) {
372
+ let active = true;
373
+ transport.listen(async (message) => {
374
+ if (!active) return;
375
+ try {
376
+ const response = await server.handle(message);
377
+ if (response !== void 0) await transport.send(response);
378
+ } catch (error) {
379
+ try {
380
+ server.emitter.emit("error", error);
381
+ } catch {}
382
+ }
383
+ });
384
+ transport.closed(() => {
385
+ active = false;
386
+ });
387
+ return () => {
388
+ active = false;
389
+ transport.listen(() => {});
390
+ transport.closed(() => {});
391
+ };
392
+ }
393
+ /**
394
+ * Pipe an {@link MCPTransportInterface} into an {@link MCPClientInterface} — every
395
+ * inbound message is decoded and delivered onto the client's OWN transport
396
+ * (`client.transport.emitter`'s `message` / `close` events), resolving/rejecting the
397
+ * client's correlated pending requests exactly as a direct reply would.
398
+ *
399
+ * @remarks
400
+ * The client's outbound writes flow through `client.transport.send` — its existing,
401
+ * unmodified request/response correlation — so `client` must have been constructed
402
+ * with a {@link import('./types.js').ClientTransportInterface} that itself carries
403
+ * the SAME `transport` (see {@link import('./factories.js').createDuplexClientTransport},
404
+ * the additive factory that adapts an {@link MCPTransportInterface} into that shape);
405
+ * this binder then completes the inbound half by decoding each message and pushing it
406
+ * onto `client.transport.emitter` (an {@link import('@orkestrel/emitter').EmitterInterface}
407
+ * exposes `emit`, so no client modification is needed). A malformed / non-JSON-RPC
408
+ * inbound message is DROPPED (§14, total — never throws); a delivery fault is routed to
409
+ * `client.transport.emitter`'s `error` event (never rethrown). The returned unbind
410
+ * DETACHES this binder (further inbound messages and the transport's `closed` signal are
411
+ * ignored) WITHOUT closing the transport.
412
+ *
413
+ * `listen`/`closed` are REPLACE semantics (§ port contract): the returned unbind
414
+ * DETACHES by replacing this binder's own handlers with no-ops, so a subsequent
415
+ * `bindClient` call on the SAME transport is never double-dispatched by a stale
416
+ * subscription left behind — an unbind→rebind cycle delivers exactly one `message`
417
+ * emit per inbound reply.
418
+ *
419
+ * @param client - The transport-agnostic client whose transport to deliver messages onto
420
+ * @param transport - The duplex channel to pipe the client over
421
+ * @returns Detach this binder from the transport (does not close it)
422
+ *
423
+ * @example
424
+ * ```ts
425
+ * const client = createMCPClient({ transport: createDuplexClientTransport(transport) })
426
+ * const unbind = bindClient(client, transport)
427
+ * await client.connect()
428
+ * // ... later, detach without closing:
429
+ * unbind()
430
+ * ```
431
+ */
432
+ function bindClient(client, transport) {
433
+ let active = true;
434
+ transport.listen((message) => {
435
+ if (!active) return;
436
+ let parsed;
437
+ try {
438
+ parsed = JSON.parse(message);
439
+ } catch {
440
+ return;
441
+ }
442
+ const decoded = parseJSONRPCMessage(parsed);
443
+ if (decoded === void 0) return;
444
+ try {
445
+ client.transport.emitter.emit("message", decoded);
446
+ } catch (error) {
447
+ try {
448
+ client.transport.emitter.emit("error", error);
449
+ } catch {}
450
+ }
451
+ });
452
+ transport.closed(() => {
453
+ if (!active) return;
454
+ active = false;
455
+ try {
456
+ client.transport.emitter.emit("close");
457
+ } catch {}
458
+ });
459
+ return () => {
460
+ active = false;
461
+ transport.listen(() => {});
462
+ transport.closed(() => {});
463
+ };
464
+ }
283
465
  //#endregion
284
466
  //#region src/core/MCPServer.ts
285
467
  /**
@@ -322,8 +504,8 @@ var MCPServer = class {
322
504
  #tools;
323
505
  constructor(options) {
324
506
  this.#emitter = new _orkestrel_emitter.Emitter({
325
- on: options.on,
326
- error: options.error
507
+ ...options.on !== void 0 ? { on: options.on } : {},
508
+ ...options.error !== void 0 ? { error: options.error } : {}
327
509
  });
328
510
  this.#name = options.name;
329
511
  this.#version = options.version;
@@ -388,8 +570,9 @@ var MCPServer = class {
388
570
  *
389
571
  * @remarks
390
572
  * - **The mirror of `MCPServer`.** The server DISPATCHES requests over a tool registry;
391
- * this client ISSUES them over a transport. `connect` runs `initialize` then sends
392
- * `notifications/initialized`; `tools()` lists the remote tools and wraps each as a
573
+ * this client ISSUES them over a transport. `connect` runs `initialize`, validates and
574
+ * exposes the negotiated `protocol`, then sends `notifications/initialized`; `tools()`
575
+ * lists the remote tools and wraps each as a
393
576
  * local {@link ToolInterface} whose `execute` calls back through `call`; `call` runs a
394
577
  * remote `tools/call` and returns the tool's value (a remote `isError: true` throws
395
578
  * locally, so an agent's {@link import('@orkestrel/agent').ToolManagerInterface}
@@ -426,10 +609,11 @@ var MCPClient = class {
426
609
  #pending = /* @__PURE__ */ new Map();
427
610
  #nextId = 0;
428
611
  #connected = false;
612
+ #protocol = void 0;
429
613
  constructor(options) {
430
614
  this.#emitter = new _orkestrel_emitter.Emitter({
431
- on: options.on,
432
- error: options.error
615
+ ...options.on !== void 0 ? { on: options.on } : {},
616
+ ...options.error !== void 0 ? { error: options.error } : {}
433
617
  });
434
618
  this.#transport = options.transport;
435
619
  this.#name = options.name ?? "taverna";
@@ -443,6 +627,9 @@ var MCPClient = class {
443
627
  get connected() {
444
628
  return this.#connected;
445
629
  }
630
+ get protocol() {
631
+ return this.#protocol;
632
+ }
446
633
  get transport() {
447
634
  return this.#transport;
448
635
  }
@@ -452,7 +639,7 @@ var MCPClient = class {
452
639
  async connect() {
453
640
  if (this.#connected) return;
454
641
  await this.#transport.start();
455
- await this.#request("initialize", {
642
+ const result = await this.#request("initialize", {
456
643
  protocolVersion: MCP_PROTOCOL_VERSION,
457
644
  capabilities: {},
458
645
  clientInfo: {
@@ -460,6 +647,13 @@ var MCPClient = class {
460
647
  version: this.#version
461
648
  }
462
649
  });
650
+ const protocol = (0, _orkestrel_contract.isRecord)(result) ? result["protocolVersion"] : void 0;
651
+ if (!(0, _orkestrel_contract.isString)(protocol) || !SUPPORTED_PROTOCOL_VERSIONS.includes(protocol)) {
652
+ await this.#transport.close();
653
+ if ((0, _orkestrel_contract.isString)(protocol)) throw new Error(`MCP server negotiated unsupported protocol version '${protocol}'`);
654
+ throw new Error("MCP server returned a non-string protocol version");
655
+ }
656
+ this.#protocol = protocol;
463
657
  this.#connected = true;
464
658
  await this.#transport.send({
465
659
  jsonrpc: "2.0",
@@ -470,8 +664,8 @@ var MCPClient = class {
470
664
  async disconnect() {
471
665
  if (!this.#connected) return;
472
666
  this.#connected = false;
473
- for (const pending of this.#pending.values()) pending.reject(/* @__PURE__ */ new Error("MCP client disconnected"));
474
- this.#pending.clear();
667
+ this.#protocol = void 0;
668
+ for (const id of this.#pending.keys()) this.#settle(id, /* @__PURE__ */ new Error("MCP client disconnected"), true);
475
669
  await this.#transport.close();
476
670
  this.#emitter.emit("disconnect");
477
671
  }
@@ -511,38 +705,24 @@ var MCPClient = class {
511
705
  };
512
706
  return new Promise((resolve, reject) => {
513
707
  const deadline = AbortSignal.timeout(this.#timeout);
514
- const settle = () => {
515
- this.#pending.delete(id);
516
- deadline.removeEventListener("abort", onDeadline);
517
- };
518
- const onDeadline = () => {
519
- settle();
520
- reject(/* @__PURE__ */ new Error(`MCP request '${method}' timed out after ${this.#timeout}ms`));
521
- };
522
- deadline.addEventListener("abort", onDeadline, { once: true });
708
+ const timeout = this.#timeoutRequest.bind(this, id, method);
709
+ deadline.addEventListener("abort", timeout, { once: true });
523
710
  this.#pending.set(id, {
524
- resolve: (value) => {
525
- settle();
526
- resolve(value);
527
- },
528
- reject: (error) => {
529
- settle();
530
- reject(error);
531
- }
711
+ resolve,
712
+ reject,
713
+ deadline,
714
+ timeout
532
715
  });
533
716
  this.#transport.send(request).catch((error) => {
534
- const pending = this.#pending.get(id);
535
- if (pending === void 0) return;
536
- pending.reject(error instanceof Error ? error : new Error(String(error)));
717
+ this.#settle(id, error instanceof Error ? error : new Error(String(error)), true);
537
718
  });
538
719
  });
539
720
  }
540
721
  #receive(message) {
541
722
  if (isJSONRPCResponse(message) && isRequestId(message.id)) {
542
- const pending = this.#pending.get(message.id);
543
- if (pending !== void 0) {
544
- if (message.error !== void 0) pending.reject(/* @__PURE__ */ new Error(`MCP error ${message.error.code}: ${message.error.message}`));
545
- else pending.resolve(message.result);
723
+ if (this.#pending.has(message.id)) {
724
+ if (message.error !== void 0) this.#settle(message.id, new MCPError(message.error.message, message.error.code, message.error.data), true);
725
+ else this.#settle(message.id, message.result, false);
546
726
  return;
547
727
  }
548
728
  }
@@ -553,7 +733,7 @@ var MCPClient = class {
553
733
  const description = descriptor["description"];
554
734
  const options = {
555
735
  name,
556
- execute: (args) => this.call(name, args)
736
+ execute: this.call.bind(this, name)
557
737
  };
558
738
  if ((0, _orkestrel_contract.isString)(description)) options.description = description;
559
739
  if ((0, _orkestrel_contract.isRecord)(inputSchema)) options.parameters = inputSchema;
@@ -565,6 +745,17 @@ var MCPClient = class {
565
745
  for (const block of result["content"]) if ((0, _orkestrel_contract.isRecord)(block) && (0, _orkestrel_contract.isString)(block["text"])) parts.push(block["text"]);
566
746
  return parts.join("\n");
567
747
  }
748
+ #timeoutRequest(id, method) {
749
+ this.#settle(id, /* @__PURE__ */ new Error(`MCP request '${method}' timed out after ${this.#timeout}ms`), true);
750
+ }
751
+ #settle(id, value, failed) {
752
+ const pending = this.#pending.get(id);
753
+ if (pending === void 0) return;
754
+ this.#pending.delete(id);
755
+ pending.deadline.removeEventListener("abort", pending.timeout);
756
+ if (failed) pending.reject(value);
757
+ else pending.resolve(value);
758
+ }
568
759
  };
569
760
  //#endregion
570
761
  //#region src/core/factories.ts
@@ -614,7 +805,8 @@ function createMCPServer(options) {
614
805
  * @remarks
615
806
  * The egress mirror of {@link createMCPServer}: where the server exposes a local tool
616
807
  * registry over MCP, the client USES a remote server's tools. `connect()` handshakes,
617
- * `tools()` lists + wraps the remote tools (each `execute` calls back over the wire),
808
+ * validates and exposes the negotiated protocol, `tools()` lists + wraps the remote
809
+ * tools (each `execute` calls back over the wire),
618
810
  * and `call(name, args)` runs a remote `tools/call` (a remote tool failure throws
619
811
  * locally, so an agent's {@link import('@orkestrel/agent').ToolManagerInterface}
620
812
  * isolates it). The transport is injected — a concrete one (the HTTP transport over
@@ -643,6 +835,47 @@ function createMCPServer(options) {
643
835
  function createMCPClient(options) {
644
836
  return new MCPClient(options);
645
837
  }
838
+ /**
839
+ * Adapt an {@link MCPTransportInterface} (the environment-agnostic duplex message
840
+ * channel) into a {@link ClientTransportInterface} — the additive bridge that lets
841
+ * `createMCPClient` run over the new port without any change to `MCPClient`'s
842
+ * existing shape.
843
+ *
844
+ * @remarks
845
+ * Hand the RESULT to `createMCPClient({ transport })`, then pass the SAME
846
+ * `transport` to {@link import('./helpers.js').bindClient} to complete the inbound
847
+ * wiring: `send` serializes each outbound {@link JSONRPCMessage} and writes it via
848
+ * `transport.send`; `close` closes the underlying
849
+ * `transport`; `start` is a no-op (the duplex channel is already open by the time
850
+ * it is handed in — there is no separate connect step at this layer); `session` is
851
+ * always `undefined` (session correlation is a higher-level concern the duplex port
852
+ * does not carry). Inbound delivery (`emitter`'s `message` / `close` events) is
853
+ * `bindClient`'s job, not this factory's — the returned object exposes a `message`-
854
+ * capable emitter for `bindClient` to push onto.
855
+ *
856
+ * @param transport - The duplex channel to adapt
857
+ * @returns A {@link ClientTransportInterface} `createMCPClient` can drive
858
+ *
859
+ * @example
860
+ * ```ts
861
+ * const client = createMCPClient({ transport: createDuplexClientTransport(transport) })
862
+ * const unbind = bindClient(client, transport)
863
+ * await client.connect()
864
+ * ```
865
+ */
866
+ function createDuplexClientTransport(transport) {
867
+ return {
868
+ emitter: new _orkestrel_emitter.Emitter(),
869
+ session: void 0,
870
+ async start() {},
871
+ async send(message) {
872
+ await transport.send(JSON.stringify(message));
873
+ },
874
+ async close() {
875
+ await transport.close();
876
+ }
877
+ };
878
+ }
646
879
  //#endregion
647
880
  exports.DEFAULT_MCP_CLIENT_NAME = DEFAULT_MCP_CLIENT_NAME;
648
881
  exports.DEFAULT_MCP_CLIENT_VERSION = DEFAULT_MCP_CLIENT_VERSION;
@@ -653,11 +886,15 @@ exports.JSONRPC_METHOD_NOT_FOUND = JSONRPC_METHOD_NOT_FOUND;
653
886
  exports.JSONRPC_PARSE_ERROR = JSONRPC_PARSE_ERROR;
654
887
  exports.JSONRPC_SERVER_ERROR = JSONRPC_SERVER_ERROR;
655
888
  exports.MCPClient = MCPClient;
889
+ exports.MCPError = MCPError;
656
890
  exports.MCPServer = MCPServer;
657
891
  exports.MCP_PROTOCOL_VERSION = MCP_PROTOCOL_VERSION;
658
892
  exports.SUPPORTED_PROTOCOL_VERSIONS = SUPPORTED_PROTOCOL_VERSIONS;
893
+ exports.bindClient = bindClient;
894
+ exports.bindServer = bindServer;
659
895
  exports.buildToolDescriptors = buildToolDescriptors;
660
896
  exports.buildToolResult = buildToolResult;
897
+ exports.createDuplexClientTransport = createDuplexClientTransport;
661
898
  exports.createMCPClient = createMCPClient;
662
899
  exports.createMCPServer = createMCPServer;
663
900
  exports.initializeResult = initializeResult;
@@ -665,6 +902,7 @@ exports.isInitializeRequest = isInitializeRequest;
665
902
  exports.isJSONRPCMessage = isJSONRPCMessage;
666
903
  exports.isJSONRPCRequest = isJSONRPCRequest;
667
904
  exports.isJSONRPCResponse = isJSONRPCResponse;
905
+ exports.isMCPError = isMCPError;
668
906
  exports.isRequestId = isRequestId;
669
907
  exports.jsonRPCError = jsonRPCError;
670
908
  exports.jsonRPCResult = jsonRPCResult;