@orkestrel/mcp 0.0.3 → 0.0.5

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","#deliver","#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","../../../src/browser/serve.ts"],"sourcesContent":["// The MCP browser-transport constants (AGENTS §5 constants file) — the wire-level\n// header name the browser-face HTTP client transport echoes, matching the Node\n// face's `MCP_SESSION_HEADER` (`src/server/constants.ts`) byte-for-byte. The browser\n// face imports nothing from `src/server` (peer environment faces, per AGENTS §2), so\n// the literal is declared once here too — the SAME string, not a shared symbol.\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// `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 `serve.ts`'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 (`serve.ts`) `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\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 { 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 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 (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\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 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`\n * returns an id, `session` is `undefined` and no header is sent (safe against a\n * stateless 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 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))\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 { 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 | readonly 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 messages = Array.isArray(message) ? message : [message]\n\t\tfor (const one of messages) {\n\t\t\tconst text = JSON.stringify(one)\n\t\t\tconst socket = this.#socket\n\t\t\tif (socket !== undefined && socket.readyState === WebSocket.OPEN) socket.send(text)\n\t\t\telse this.#queue.push(text)\n\t\t}\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\tServeMCPScopeInterface,\n\tWebSocketClientTransportOptions,\n} from './types.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, so the same `MCPClient` passes\n * session validation (a stateless server sends none).\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` (`serve.ts`) 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","import type { ServeMCPOptions, ServeMCPScopeInterface } from './types.js'\nimport { bindServer, createMCPServer } from '@src/core'\nimport { DEFAULT_MCP_SERVER_NAME, DEFAULT_MCP_SERVER_VERSION } from './constants.js'\nimport { createScopeTransport } from './factories.js'\nimport { createScopeMessageListener } from './helpers.js'\n\n// The `serveWorker` analog (PROPOSAL §3.3.4): boot an `MCPServer` inside a hostable\n// scope and wire its message events to it. `serveMCP(options)` is a one-liner over\n// `globalThis`; `serveMCPScope(scope, options)` is the testable core — it takes the\n// scope as a parameter so a test drives it with a scope double instead of the real\n// `globalThis`.\n//\n// UNIFIED dedicated-worker / Service-Worker wiring, no upfront detection flag: a\n// dedicated worker's implicit channel (no port) and a Service Worker's per-client\n// `MessagePort` are structurally distinguishable PER EVENT (`event.ports.length`),\n// so ONE `message` listener handles both shapes uniformly — an event carrying a port\n// spawns a per-port `MessagePortTransport` binding (multi-client); an event with no\n// port but a string `data` routes through the implicit scope channel. Per AGENTS §3.3.4,\n// this face imports project code freely (`@src/core` bundles into the worker) — the\n// inlined-guards exception `node:worker_threads`' type-stripping forces on the Node\n// face's `serve.ts` (`@orkestrel/worker`) does NOT apply here and is not copied.\n\n/**\n * Boot an `MCPServer` inside a hostable scope (a dedicated Web Worker's `self`, or a\n * Service Worker's `self`) and wire its message events to it.\n *\n * @remarks\n * **Trust boundary — mechanism, not policy.** `serveMCPScope` exposes the ENTIRE\n * supplied `tools` registry to EVERY client the scope accepts a port from, with NO\n * built-in origin or identity check. In a Service Worker that means every same-origin\n * context the SW controls (any window, worker, or iframe can\n * `controller.postMessage(msg, [port])` and get a fully-bound server with complete\n * tool-call access). Origin allow-listing, handshake tokens, and any other gating are\n * the embedding application's responsibility — compose a guard in front. Use the\n * `accept` option to gate port-bearing events before binding: return `false` to drop\n * the event entirely (no binding, no reply).\n *\n * **Lifetime / per-client binding accumulation.** Each accepted port-bearing event\n * creates a fresh `MessagePortTransport` + `bindServer` binding that lives for the\n * scope's lifetime — there is NO per-client reaping, because `MessagePort` provides\n * no \"peer closed\" signal. For bounded, long-lived client sets this is fine; embedders\n * with high client churn must track and invoke the dispose function themselves to\n * avoid unbounded accumulation.\n *\n * **Portless events and the implicit scope channel.** A portless `message` event\n * (e.g. `controller.postMessage('<json-rpc>')` in a Service Worker) delivers its\n * string directly to the implicit scope transport — **the tool EXECUTES** — even\n * though no reply can reach the caller. In a `ServiceWorkerGlobalScope` the reply\n * path (`scopeTransport.send` → `scope.postMessage`) throws (no `self.postMessage`),\n * and `bindServer` routes the throw to the server emitter's `error` event (see\n * `@src/core bindServer`), so the un-repliable reply is dropped. The net effect is\n * **blind side-effecting ingress**: the tool runs but the caller gets no result.\n * Crucially, **`accept` does NOT gate this channel** — it is consulted only for\n * port-bearing events. In a Service Worker, if `accept` is your sole guard, ensure\n * all clients connect through transferred `MessagePort`s (port-bearing messages), or\n * restrict the exposed tools to side-effect-free operations, or validate a token\n * inside the tools themselves.\n *\n * Binds the implicit scope channel EAGERLY (at call time, not lazily on first use) —\n * `bindServer` is called once against a {@link import('./types.js').ScopeTransportInterface}\n * wrapping `scope` for the whole lifetime of the returned dispose, so a dedicated\n * worker's very first portless message is served with no first-use setup cost or\n * ordering hazard.\n *\n * Every inbound `message` event is inspected structurally: `event.ports.length > 0`\n * spawns a fresh {@link import('./factories.js').createMessagePortTransport} +\n * `bindServer` for THAT port (tracked for teardown) — this holds even on a\n * dedicated-worker-shaped scope, the unified design's deliberate cross-case. An\n * event with NO ports and a STRING `event.data` is delivered onto the implicit scope\n * channel; any other event (no ports, non-string data) is dropped.\n *\n * @param scope - The hostable scope to wire (structurally, `self` inside a worker)\n * @param options - `tools` (the live registry to expose; REQUIRED), optional\n * `name`/`version` (default {@link import('./constants.js').DEFAULT_MCP_SERVER_NAME} /\n * {@link import('./constants.js').DEFAULT_MCP_SERVER_VERSION}), optional `accept`\n * (origin/identity gate for port-bearing events); see {@link ServeMCPOptions}\n * @returns A dispose function — unbinds every binding, closes every accepted\n * `MessagePort`, and removes the scope's `message` listener. Idempotent.\n *\n * @example\n * ```ts\n * const scope = { postMessage() {}, addEventListener() {}, removeEventListener() {} }\n * const dispose = serveMCPScope(scope, {\n * tools: createToolManager(),\n * // Prefer token-in-data — event.origin is empty for same-origin worker messages.\n * accept: (event) => event.data === 'my-secret-token',\n * })\n * // ... later:\n * dispose()\n * ```\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 scope (`globalThis` — a dedicated\n * Web Worker or a Service Worker) and wire its message events to it.\n *\n * @remarks\n * A one-liner over {@link serveMCPScope}: `serveMCP(options)` is exactly\n * `serveMCPScope(globalThis, options)`. Kept as its own export so the scope-facing\n * wiring stays independently testable (AGENTS §5) — drive {@link serveMCPScope}\n * directly with a scope double for a test, and this thin wrapper for real deploys.\n *\n * **Trust boundary and lifecycle** — see {@link serveMCPScope}'s `@remarks`. The same\n * considerations apply: ENTIRE tool registry exposed to every accepted port-bearing\n * event; use `accept` to gate; per-client bindings accumulate for the scope's lifetime.\n *\n * @param options - `tools` (the live registry to expose; REQUIRED), optional\n * `name`/`version`, optional `accept` (origin/identity gate); see {@link ServeMCPOptions}\n * @returns A dispose function — see {@link serveMCPScope}\n *\n * @example\n * ```ts\n * // Inside a dedicated Web Worker's entry module:\n * import { serveMCP } from '@orkestrel/mcp/browser'\n * import { createToolManager, createTool } from '@orkestrel/agent'\n *\n * const tools = createToolManager()\n * tools.add(createTool({ name: 'add', execute: (a) => Number(a.x) + Number(a.y) }))\n * const dispose = serveMCP({ tools, name: 'worker-mcp', version: '1.0.0' })\n * // ... later, on teardown:\n * dispose()\n * ```\n */\nexport function serveMCP(options: ServeMCPOptions): () => void {\n\treturn serveMCPScope(globalThis, options)\n}\n"],"mappings":"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"}
@@ -280,6 +280,134 @@ function initializeResult(name, version, requested) {
280
280
  }
281
281
  };
282
282
  }
283
+ /**
284
+ * Pipe an {@link MCPTransportInterface} into an {@link MCPServerInterface} — every
285
+ * inbound message runs through `server.handle`, and a defined reply is written back
286
+ * via `transport.send`.
287
+ *
288
+ * @remarks
289
+ * `server.handle` already turns a malformed message into a serialized `-32700` /
290
+ * `-32600` reply and a notification into `undefined` (no reply), so this binder adds
291
+ * no parsing of its own. A `transport.send` throw or rejection is caught and routed
292
+ * to `server.emitter`'s `error` event (never rethrown, never an unhandled rejection);
293
+ * a listener on that event that itself throws is swallowed (the end of the line —
294
+ * the caller's own bug, never this binder's). The returned unbind DETACHES this
295
+ * binder (further inbound messages and the transport's `closed` signal are ignored)
296
+ * WITHOUT closing the transport — closing is the caller's decision.
297
+ *
298
+ * `listen`/`closed` are REPLACE semantics (§ port contract): the returned unbind
299
+ * DETACHES by replacing this binder's own handlers with no-ops, so a subsequent
300
+ * `bindServer` call on the SAME transport is never double-dispatched by a stale
301
+ * subscription left behind — an unbind→rebind cycle yields exactly one reply per
302
+ * request.
303
+ *
304
+ * @param server - The transport-agnostic server to dispatch inbound messages over
305
+ * @param transport - The duplex channel to pipe the server over
306
+ * @returns Detach this binder from the transport (does not close it)
307
+ *
308
+ * @example
309
+ * ```ts
310
+ * const unbind = bindServer(server, transport)
311
+ * // ... later, detach without closing:
312
+ * unbind()
313
+ * ```
314
+ */
315
+ function bindServer(server, transport) {
316
+ let active = true;
317
+ transport.listen((message) => {
318
+ if (!active) return;
319
+ (async () => {
320
+ try {
321
+ const response = await server.handle(message);
322
+ if (response !== void 0) await transport.send(response);
323
+ } catch (error) {
324
+ try {
325
+ server.emitter.emit("error", error);
326
+ } catch {}
327
+ }
328
+ })();
329
+ });
330
+ transport.closed(() => {
331
+ active = false;
332
+ });
333
+ return () => {
334
+ active = false;
335
+ transport.listen(() => {});
336
+ transport.closed(() => {});
337
+ };
338
+ }
339
+ /**
340
+ * Pipe an {@link MCPTransportInterface} into an {@link MCPClientInterface} — every
341
+ * inbound message is decoded and delivered onto the client's OWN transport
342
+ * (`client.transport.emitter`'s `message` / `close` events), resolving/rejecting the
343
+ * client's correlated pending requests exactly as a direct reply would.
344
+ *
345
+ * @remarks
346
+ * The client's outbound writes flow through `client.transport.send` — its existing,
347
+ * unmodified request/response correlation — so `client` must have been constructed
348
+ * with a {@link import('./types.js').ClientTransportInterface} that itself carries
349
+ * the SAME `transport` (see {@link import('./factories.js').createDuplexClientTransport},
350
+ * the additive factory that adapts an {@link MCPTransportInterface} into that shape);
351
+ * this binder then completes the inbound half by decoding each message and pushing it
352
+ * onto `client.transport.emitter` (an {@link import('@orkestrel/emitter').EmitterInterface}
353
+ * exposes `emit`, so no client modification is needed). A malformed / non-JSON-RPC
354
+ * inbound message is DROPPED (§14, total — never throws); a delivery fault is routed to
355
+ * `client.transport.emitter`'s `error` event (never rethrown). The returned unbind
356
+ * DETACHES this binder (further inbound messages and the transport's `closed` signal are
357
+ * ignored) WITHOUT closing the transport.
358
+ *
359
+ * `listen`/`closed` are REPLACE semantics (§ port contract): the returned unbind
360
+ * DETACHES by replacing this binder's own handlers with no-ops, so a subsequent
361
+ * `bindClient` call on the SAME transport is never double-dispatched by a stale
362
+ * subscription left behind — an unbind→rebind cycle delivers exactly one `message`
363
+ * emit per inbound reply.
364
+ *
365
+ * @param client - The transport-agnostic client whose transport to deliver messages onto
366
+ * @param transport - The duplex channel to pipe the client over
367
+ * @returns Detach this binder from the transport (does not close it)
368
+ *
369
+ * @example
370
+ * ```ts
371
+ * const client = createMCPClient({ transport: createDuplexClientTransport(transport) })
372
+ * const unbind = bindClient(client, transport)
373
+ * await client.connect()
374
+ * // ... later, detach without closing:
375
+ * unbind()
376
+ * ```
377
+ */
378
+ function bindClient(client, transport) {
379
+ let active = true;
380
+ transport.listen((message) => {
381
+ if (!active) return;
382
+ let parsed;
383
+ try {
384
+ parsed = JSON.parse(message);
385
+ } catch {
386
+ return;
387
+ }
388
+ const decoded = parseJSONRPCMessage(parsed);
389
+ if (decoded === void 0) return;
390
+ try {
391
+ client.transport.emitter.emit("message", decoded);
392
+ } catch (error) {
393
+ try {
394
+ client.transport.emitter.emit("error", error);
395
+ } catch {}
396
+ }
397
+ });
398
+ transport.closed(() => {
399
+ if (!active) return;
400
+ active = false;
401
+ try {
402
+ client.transport.emitter.emit("close");
403
+ } catch {}
404
+ });
405
+ return () => {
406
+ active = false;
407
+ transport.listen(() => {});
408
+ transport.closed(() => {});
409
+ };
410
+ }
283
411
  //#endregion
284
412
  //#region src/core/MCPServer.ts
285
413
  /**
@@ -643,6 +771,48 @@ function createMCPServer(options) {
643
771
  function createMCPClient(options) {
644
772
  return new MCPClient(options);
645
773
  }
774
+ /**
775
+ * Adapt an {@link MCPTransportInterface} (the environment-agnostic duplex message
776
+ * channel) into a {@link ClientTransportInterface} — the additive bridge that lets
777
+ * `createMCPClient` run over the new port without any change to `MCPClient`'s
778
+ * existing shape.
779
+ *
780
+ * @remarks
781
+ * Hand the RESULT to `createMCPClient({ transport })`, then pass the SAME
782
+ * `transport` to {@link import('./helpers.js').bindClient} to complete the inbound
783
+ * wiring: `send` serializes each outbound {@link JSONRPCMessage} (or batch, one per
784
+ * message) and writes it via `transport.send`; `close` closes the underlying
785
+ * `transport`; `start` is a no-op (the duplex channel is already open by the time
786
+ * it is handed in — there is no separate connect step at this layer); `session` is
787
+ * always `undefined` (session correlation is a higher-level concern the duplex port
788
+ * does not carry). Inbound delivery (`emitter`'s `message` / `close` events) is
789
+ * `bindClient`'s job, not this factory's — the returned object exposes a `message`-
790
+ * capable emitter for `bindClient` to push onto.
791
+ *
792
+ * @param transport - The duplex channel to adapt
793
+ * @returns A {@link ClientTransportInterface} `createMCPClient` can drive
794
+ *
795
+ * @example
796
+ * ```ts
797
+ * const client = createMCPClient({ transport: createDuplexClientTransport(transport) })
798
+ * const unbind = bindClient(client, transport)
799
+ * await client.connect()
800
+ * ```
801
+ */
802
+ function createDuplexClientTransport(transport) {
803
+ return {
804
+ emitter: new _orkestrel_emitter.Emitter(),
805
+ session: void 0,
806
+ async start() {},
807
+ async send(message) {
808
+ const messages = Array.isArray(message) ? message : [message];
809
+ for (const one of messages) await transport.send(JSON.stringify(one));
810
+ },
811
+ async close() {
812
+ await transport.close();
813
+ }
814
+ };
815
+ }
646
816
  //#endregion
647
817
  exports.DEFAULT_MCP_CLIENT_NAME = DEFAULT_MCP_CLIENT_NAME;
648
818
  exports.DEFAULT_MCP_CLIENT_VERSION = DEFAULT_MCP_CLIENT_VERSION;
@@ -656,8 +826,11 @@ exports.MCPClient = MCPClient;
656
826
  exports.MCPServer = MCPServer;
657
827
  exports.MCP_PROTOCOL_VERSION = MCP_PROTOCOL_VERSION;
658
828
  exports.SUPPORTED_PROTOCOL_VERSIONS = SUPPORTED_PROTOCOL_VERSIONS;
829
+ exports.bindClient = bindClient;
830
+ exports.bindServer = bindServer;
659
831
  exports.buildToolDescriptors = buildToolDescriptors;
660
832
  exports.buildToolResult = buildToolResult;
833
+ exports.createDuplexClientTransport = createDuplexClientTransport;
661
834
  exports.createMCPClient = createMCPClient;
662
835
  exports.createMCPServer = createMCPServer;
663
836
  exports.initializeResult = initializeResult;
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["#emitter","#name","#version","#tools","#call","#emitter","#transport","#name","#version","#timeout","#pending","#receive","#connected","#request","#tool","#text","#nextId"],"sources":["../../../src/core/constants.ts","../../../src/core/validators.ts","../../../src/core/parsers.ts","../../../src/core/helpers.ts","../../../src/core/MCPServer.ts","../../../src/core/MCPClient.ts","../../../src/core/factories.ts"],"sourcesContent":["// MCP protocol revisions + the reserved JSON-RPC 2.0 error codes. The negotiated\n// protocol version is the current rev unless the client requests a SUPPORTED prior\n// one (see `initializeResult` in ./helpers.js). Transport-level header names\n// (session / version headers) belong to the HTTP transport sub-chunk, NOT here.\n\n/** The MCP protocol revision this server implements (the default negotiated version). */\nexport const MCP_PROTOCOL_VERSION = '2025-06-18'\n\n/**\n * The MCP protocol revisions this server can negotiate — the current\n * {@link MCP_PROTOCOL_VERSION} plus a prior rev a client may still request.\n *\n * @remarks\n * `initialize` echoes the client's requested `protocolVersion` when it appears in\n * this list, else falls back to {@link MCP_PROTOCOL_VERSION}. Frozen so the list is\n * an immutable contract.\n */\nexport const SUPPORTED_PROTOCOL_VERSIONS: readonly string[] = Object.freeze([\n\t'2025-06-18',\n\t'2025-03-26',\n])\n\n/** JSON-RPC 2.0 reserved error: invalid JSON was received (the message did not parse). */\nexport const JSONRPC_PARSE_ERROR = -32700\n\n/** JSON-RPC 2.0 reserved error: the payload was not a valid Request object. */\nexport const JSONRPC_INVALID_REQUEST = -32600\n\n/** JSON-RPC 2.0 reserved error: the requested method does not exist. */\nexport const JSONRPC_METHOD_NOT_FOUND = -32601\n\n/** JSON-RPC 2.0 reserved error: the method's parameters were invalid. */\nexport const JSONRPC_INVALID_PARAMS = -32602\n\n/** JSON-RPC 2.0 implementation-defined server error (the `-32000` to `-32099` range). */\nexport const JSONRPC_SERVER_ERROR = -32000\n\n// MCP CLIENT defaults — the identity an `MCPClient` reports in the `initialize`\n// handshake (`clientInfo`) and the per-request deadline, when the caller supplies\n// none. The egress mirror of the server's protocol-version constants above.\n\n/** The default client name reported in the MCP `initialize` handshake (`clientInfo.name`). */\nexport const DEFAULT_MCP_CLIENT_NAME = 'taverna'\n\n/** The default client version reported in the MCP `initialize` handshake (`clientInfo.version`). */\nexport const DEFAULT_MCP_CLIENT_VERSION = '1.0.0'\n\n/**\n * The default per-request deadline (ms) an `MCPClient` applies when `options.timeout`\n * is unset — a request the remote server does not answer within it rejects.\n */\nexport const DEFAULT_MCP_REQUEST_TIMEOUT = 30_000\n","import type { JSONRPCMessage, JSONRPCRequest, JSONRPCResponse } from './types.js'\nimport { isNumber, isRecord, isString, isUndefined } from '@orkestrel/contract'\n\n// AGENTS §14: every guard here is a TOTAL function over the already-`JSON.parse`d\n// value — adversarial input returns `false`, never throws. The raw-string\n// `JSON.parse` (which CAN throw) happens in `MCPServer.handle` inside a try/catch;\n// these guards only ever see a parsed `unknown`. Each is a flat structural test on\n// `isRecord` + field checks (no user callbacks), so totality is immediate.\n\n/**\n * Determine whether a value is a valid JSON-RPC REQUEST `id` — a string, a number,\n * or absent.\n *\n * @remarks\n * A request id is a string, a number, or `undefined` (its ABSENCE marks a\n * NOTIFICATION). `null` is NOT a valid request id — it is valid only on a RESPONSE.\n * Total (§14): any other input returns `false`.\n *\n * @param value - The already-parsed value to test\n * @returns `true` when `value` is a string, a number, or `undefined`\n *\n * @example\n * ```ts\n * isRequestId(1) // true\n * isRequestId('abc') // true\n * isRequestId(undefined) // true — a notification\n * isRequestId(null) // false — valid only on a response\n * ```\n */\nexport function isRequestId(value: unknown): value is string | number | undefined {\n\treturn isUndefined(value) || isString(value) || isNumber(value)\n}\n\n/**\n * Determine whether a parsed value is a {@link JSONRPCRequest}.\n *\n * @remarks\n * A request is a record with `jsonrpc === '2.0'` and a string `method`. `id`, when\n * present, must be a string or number; its ABSENCE is valid — that marks a\n * NOTIFICATION (a fire-and-forget request that yields no response). `params`, when\n * present, must be a record. Total (§14): any other input returns `false`.\n *\n * @param value - The already-parsed value to test\n * @returns `true` when `value` is a valid JSON-RPC request\n *\n * @example\n * ```ts\n * isJSONRPCRequest({ jsonrpc: '2.0', method: 'ping', id: 1 }) // true\n * isJSONRPCRequest({ jsonrpc: '2.0', method: 'notifications/initialized' }) // true — a notification\n * isJSONRPCRequest({ jsonrpc: '1.0', method: 'ping' }) // false\n * ```\n */\nexport function isJSONRPCRequest(value: unknown): value is JSONRPCRequest {\n\tif (!isRecord(value)) {\n\t\treturn false\n\t}\n\tif (value['jsonrpc'] !== '2.0' || !isString(value['method'])) {\n\t\treturn false\n\t}\n\tif (!isRequestId(value['id'])) {\n\t\treturn false\n\t}\n\tconst params = value['params']\n\treturn isUndefined(params) || isRecord(params)\n}\n\n/**\n * Determine whether a parsed value is a {@link JSONRPCResponse}.\n *\n * @remarks\n * A response is a record with `jsonrpc === '2.0'`, an `id` that is a string,\n * number, or `null`, and EXACTLY ONE of a `result` (any value, including\n * `undefined`'s absence) or an `error` (a record with a numeric `code` and string\n * `message`). Total (§14).\n *\n * @param value - The already-parsed value to test\n * @returns `true` when `value` is a valid JSON-RPC response\n */\nexport function isJSONRPCResponse(value: unknown): value is JSONRPCResponse {\n\tif (!isRecord(value)) {\n\t\treturn false\n\t}\n\tif (value['jsonrpc'] !== '2.0') {\n\t\treturn false\n\t}\n\tconst id = value['id']\n\tif (id !== null && !isString(id) && !isNumber(id)) {\n\t\treturn false\n\t}\n\tconst hasResult = Object.hasOwn(value, 'result')\n\tconst error = value['error']\n\tconst hasError = !isUndefined(error)\n\t// Exactly one of result / error — never both, never neither.\n\tif (hasResult === hasError) {\n\t\treturn false\n\t}\n\tif (hasError) {\n\t\treturn isRecord(error) && isNumber(error['code']) && isString(error['message'])\n\t}\n\treturn true\n}\n\n/**\n * Determine whether a parsed value is a {@link JSONRPCMessage} — a request or a\n * response.\n *\n * @remarks\n * The union of {@link isJSONRPCRequest} and {@link isJSONRPCResponse}. Total (§14).\n *\n * @param value - The already-parsed value to test\n * @returns `true` when `value` is a valid JSON-RPC request or response\n */\nexport function isJSONRPCMessage(value: unknown): value is JSONRPCMessage {\n\treturn isJSONRPCRequest(value) || isJSONRPCResponse(value)\n}\n\n/**\n * Determine whether a parsed value is an MCP `initialize` request — a\n * {@link JSONRPCRequest} whose `method` is `'initialize'`.\n *\n * @param value - The already-parsed value to test\n * @returns `true` when `value` is a valid `initialize` request\n *\n * @example\n * ```ts\n * isInitializeRequest({ jsonrpc: '2.0', method: 'initialize', id: 1 }) // true\n * isInitializeRequest({ jsonrpc: '2.0', method: 'ping', id: 1 }) // false\n * ```\n */\nexport function isInitializeRequest(value: unknown): value is JSONRPCRequest {\n\treturn isJSONRPCRequest(value) && value.method === 'initialize'\n}\n","import type { JSONRPCMessage } from './types.js'\nimport { isJSONRPCMessage } from './validators.js'\n\n/**\n * Narrow an already-parsed value to a {@link JSONRPCMessage}, or `undefined` when\n * it is not one.\n *\n * @remarks\n * Total (§14) — a non-message returns `undefined`, never throws. The input must\n * ALREADY be `JSON.parse`d: the raw-string parse (which can throw on malformed\n * JSON) happens in `MCPServer.handle` inside a try/catch that maps a parse failure\n * to a `-32700` response. Sound with {@link isJSONRPCMessage}: a guard-valid input\n * is returned unchanged, and every non-`undefined` output satisfies the guard.\n *\n * @param value - The already-parsed value to narrow\n * @returns The value as a {@link JSONRPCMessage}, or `undefined`\n *\n * @example\n * ```ts\n * parseJSONRPCMessage({ jsonrpc: '2.0', method: 'ping', id: 1 }) // the request\n * parseJSONRPCMessage({ method: 'ping' }) // undefined — missing jsonrpc\n * ```\n */\nexport function parseJSONRPCMessage(value: unknown): JSONRPCMessage | undefined {\n\treturn isJSONRPCMessage(value) ? value : undefined\n}\n","import type { ToolManagerInterface, ToolResult } from '@orkestrel/agent'\nimport type { JSONRPCResponse, MCPToolDescriptor, MCPToolResult } from './types.js'\nimport { MCP_PROTOCOL_VERSION, SUPPORTED_PROTOCOL_VERSIONS } from './constants.js'\n\n// Pure dispatch builders (AGENTS §5: the dispatch branches stay exported helpers,\n// not hidden privates). Each turns a piece of MCP state into the JSON-RPC `result`\n// payload (or a response envelope) the server returns — independently testable.\n\n/**\n * Build a JSON-RPC success {@link JSONRPCResponse} — the `id` echoed, the method's\n * value as `result`.\n *\n * @param id - The request's id (`null` only for a parse / invalid-request error)\n * @param result - The method's return value\n * @returns The success response envelope\n */\nexport function jsonRPCResult(id: string | number | null, result: unknown): JSONRPCResponse {\n\treturn { jsonrpc: '2.0', id, result }\n}\n\n/**\n * Build a JSON-RPC error {@link JSONRPCResponse} — the `id` echoed, the failure as\n * an `error` object.\n *\n * @param id - The request's id (`null` for a parse / invalid-request error)\n * @param code - One of the reserved JSON-RPC codes (see `./constants.js`)\n * @param message - A short human description of the failure\n * @param data - An OPTIONAL machine-readable payload (omitted from the envelope when absent)\n * @returns The error response envelope\n */\nexport function jsonRPCError(\n\tid: string | number | null,\n\tcode: number,\n\tmessage: string,\n\tdata?: unknown,\n): JSONRPCResponse {\n\treturn {\n\t\tjsonrpc: '2.0',\n\t\tid,\n\t\terror: data === undefined ? { code, message } : { code, message, data },\n\t}\n}\n\n/**\n * Map a {@link ToolManagerInterface}'s definitions to MCP `tools/list` descriptors\n * — renaming `parameters` to the wire's `inputSchema`.\n *\n * @remarks\n * Each {@link import('@orkestrel/agent').ToolDefinition} carries through its\n * `name` and (when present) `description`; its open JSON-Schema `parameters`\n * becomes `inputSchema`, defaulting to an empty object schema (`{ type: 'object' }`)\n * when a tool declares none (MCP requires an `inputSchema`).\n *\n * @param manager - The tool registry to describe\n * @returns One {@link MCPToolDescriptor} per registered tool, in registry order\n */\nexport function buildToolDescriptors(manager: ToolManagerInterface): readonly MCPToolDescriptor[] {\n\treturn manager.definitions().map((definition) => {\n\t\tconst descriptor: {\n\t\t\tname: string\n\t\t\tdescription?: string\n\t\t\tinputSchema: Readonly<Record<string, unknown>>\n\t\t} = {\n\t\t\tname: definition.name,\n\t\t\tinputSchema: definition.parameters ?? { type: 'object' },\n\t\t}\n\t\tif (definition.description !== undefined) descriptor.description = definition.description\n\t\treturn descriptor\n\t})\n}\n\n/**\n * Map an executed tool's {@link ToolResult} to an MCP {@link MCPToolResult} — the\n * value (or error) as a `text` content block.\n *\n * @remarks\n * The {@link ToolManagerInterface} already isolates a thrown tool into\n * `result.error` (so the server adds NO try/catch around `execute`): when `error`\n * is present, this builds an `isError: true` result carrying the error text, so the\n * model sees the failure as a tool result it can react to rather than a protocol\n * error; otherwise it serializes `result.value` (via `JSON.stringify`) into one\n * `text` block.\n *\n * @param result - The tool's execution outcome\n * @returns The MCP tool-call result\n */\nexport function buildToolResult(result: ToolResult): MCPToolResult {\n\tif (result.error !== undefined) {\n\t\treturn { content: [{ type: 'text', text: result.error }], isError: true }\n\t}\n\t// A content block must carry a string `text`; `JSON.stringify(undefined)` is the value\n\t// `undefined` (which serializes away), so a value-less result becomes an empty text block.\n\tconst text = result.value === undefined ? '' : JSON.stringify(result.value)\n\treturn { content: [{ type: 'text', text }] }\n}\n\n/**\n * Build the MCP `initialize` result — the negotiated protocol version, the\n * advertised capabilities, and the server identity.\n *\n * @remarks\n * Version negotiation echoes the client's `requested` version when it is one of the\n * {@link SUPPORTED_PROTOCOL_VERSIONS}, else falls back to {@link MCP_PROTOCOL_VERSION}.\n * `capabilities.tools` is an empty object — this server advertises the tools\n * capability with no sub-options (no list-changed notification yet).\n *\n * @param name - The server name (echoed in `serverInfo`)\n * @param version - The server version (echoed in `serverInfo`)\n * @param requested - The client's requested protocol version (negotiated when supported)\n * @returns The `initialize` result payload\n */\nexport function initializeResult(\n\tname: string,\n\tversion: string,\n\trequested?: string,\n): Readonly<Record<string, unknown>> {\n\tconst protocolVersion =\n\t\trequested !== undefined && SUPPORTED_PROTOCOL_VERSIONS.includes(requested)\n\t\t\t? requested\n\t\t\t: MCP_PROTOCOL_VERSION\n\treturn {\n\t\tprotocolVersion,\n\t\tcapabilities: { tools: {} },\n\t\tserverInfo: { name, version },\n\t}\n}\n","import type { EmitterInterface } from '@orkestrel/emitter'\nimport type { ToolManagerInterface } from '@orkestrel/agent'\nimport type {\n\tJSONRPCRequest,\n\tJSONRPCResponse,\n\tMCPServerEventMap,\n\tMCPServerInterface,\n\tMCPServerOptions,\n} from './types.js'\nimport { Emitter } from '@orkestrel/emitter'\nimport { isRecord, isString } from '@orkestrel/contract'\nimport {\n\tJSONRPC_INVALID_PARAMS,\n\tJSONRPC_INVALID_REQUEST,\n\tJSONRPC_METHOD_NOT_FOUND,\n\tJSONRPC_PARSE_ERROR,\n} from './constants.js'\nimport {\n\tbuildToolDescriptors,\n\tbuildToolResult,\n\tinitializeResult,\n\tjsonRPCError,\n\tjsonRPCResult,\n} from './helpers.js'\nimport { parseJSONRPCMessage } from './parsers.js'\n\n/**\n * A transport-agnostic Model Context Protocol server — dispatches JSON-RPC 2.0\n * requests over a live {@link ToolManagerInterface}, with NO transport coupling.\n *\n * @remarks\n * - **Two entry points.** `dispatch(request)` runs an already-parsed request and\n * resolves a {@link JSONRPCResponse} — or `undefined` for a NOTIFICATION (a\n * request with no `id`). `handle(message)` is the string boundary: it\n * `JSON.parse`s the raw message (a failure → a `-32700` response), narrows it to\n * a request (a non-request → a `-32600` response), dispatches, and serializes the\n * response back to a string (`undefined` for a notification).\n * - **The method switch.** `initialize` negotiates the protocol version + advertises\n * the tools capability; `notifications/initialized` is a notification (no\n * response); `ping` returns `{}`; `tools/list` lists the registry's tools (its\n * `parameters` renamed to `inputSchema`); `tools/call` runs a tool by name (the\n * {@link ToolManagerInterface} isolates a tool throw into the result `error`, which\n * maps to an `isError: true` tool result — so the server adds NO try/catch). An\n * unknown method → `-32601`; a `tools/call` with a missing / non-string `name` →\n * `-32602`.\n * - **Provider-agnostic.** Imports only core siblings — JSON-RPC + the tool registry,\n * no HTTP, no model. Wire fields are narrowed via the contracts guards (no `as`).\n * - **Observable (§13).** The owned `emitter` fires `request` at the top of every\n * dispatch; the emitter isolates a listener throw and routes it to its `error` handler\n * (the `error` option), so a listener throw can never escape the dispatch.\n *\n * @example\n * ```ts\n * const tools = createToolManager()\n * tools.add(createTool({ name: 'add', execute: (a) => Number(a.x) + Number(a.y) }))\n * const server = new MCPServer({ name: 'demo', version: '1.0.0', tools })\n * await server.handle('{\"jsonrpc\":\"2.0\",\"method\":\"ping\",\"id\":1}') // '{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{}}'\n * ```\n */\nexport class MCPServer implements MCPServerInterface {\n\treadonly #emitter: Emitter<MCPServerEventMap>\n\treadonly #name: string\n\treadonly #version: string\n\treadonly #tools: ToolManagerInterface\n\n\tconstructor(options: MCPServerOptions) {\n\t\tthis.#emitter = new Emitter<MCPServerEventMap>({ on: options.on, error: options.error })\n\t\tthis.#name = options.name\n\t\tthis.#version = options.version\n\t\tthis.#tools = options.tools\n\t}\n\n\tget emitter(): EmitterInterface<MCPServerEventMap> {\n\t\treturn this.#emitter\n\t}\n\n\tget name(): string {\n\t\treturn this.#name\n\t}\n\n\tget version(): string {\n\t\treturn this.#version\n\t}\n\n\tasync dispatch(request: JSONRPCRequest): Promise<JSONRPCResponse | undefined> {\n\t\tconst id = request.id ?? null\n\t\tthis.#emitter.emit('request', request.method, id)\n\t\t// JSON-RPC: a request with NO `id` is a NOTIFICATION — it is handled (the\n\t\t// `request` event already fired) but NEVER produces a response, whatever its\n\t\t// method (`notifications/initialized`, a fire-and-forget `ping`, an unknown\n\t\t// method — all silent). So short-circuit here, and the switch below only ever\n\t\t// runs for an id-bearing request that expects a reply.\n\t\tif (request.id === undefined) {\n\t\t\treturn undefined\n\t\t}\n\t\tswitch (request.method) {\n\t\t\tcase 'initialize': {\n\t\t\t\tconst requested = request.params?.['protocolVersion']\n\t\t\t\treturn jsonRPCResult(\n\t\t\t\t\tid,\n\t\t\t\t\tinitializeResult(this.#name, this.#version, isString(requested) ? requested : undefined),\n\t\t\t\t)\n\t\t\t}\n\t\t\tcase 'ping':\n\t\t\t\treturn jsonRPCResult(id, {})\n\t\t\tcase 'tools/list':\n\t\t\t\treturn jsonRPCResult(id, { tools: buildToolDescriptors(this.#tools) })\n\t\t\tcase 'tools/call':\n\t\t\t\treturn this.#call(request, id)\n\t\t\tdefault:\n\t\t\t\treturn jsonRPCError(id, JSONRPC_METHOD_NOT_FOUND, `Method not found: ${request.method}`)\n\t\t}\n\t}\n\n\tasync handle(message: string): Promise<string | undefined> {\n\t\tlet parsed: unknown\n\t\ttry {\n\t\t\tparsed = JSON.parse(message)\n\t\t} catch {\n\t\t\treturn JSON.stringify(jsonRPCError(null, JSONRPC_PARSE_ERROR, 'Parse error'))\n\t\t}\n\t\tconst decoded = parseJSONRPCMessage(parsed)\n\t\t// Only a REQUEST is dispatchable — a response (or any non-message) is invalid input.\n\t\tif (decoded === undefined || !('method' in decoded)) {\n\t\t\treturn JSON.stringify(jsonRPCError(null, JSONRPC_INVALID_REQUEST, 'Invalid Request'))\n\t\t}\n\t\tconst response = await this.dispatch(decoded)\n\t\treturn response === undefined ? undefined : JSON.stringify(response)\n\t}\n\n\t// Run a `tools/call`: narrow `params.name` (string) + `params.arguments` (record,\n\t// default `{}`) with no `as`, execute the tool (the manager isolates a throw into\n\t// `result.error`), and map the result to an MCP tool-call result.\n\tasync #call(request: JSONRPCRequest, id: string | number | null): Promise<JSONRPCResponse> {\n\t\tconst params = request.params\n\t\tconst name = params?.['name']\n\t\tif (!isString(name)) {\n\t\t\treturn jsonRPCError(id, JSONRPC_INVALID_PARAMS, 'Invalid params: a string `name` is required')\n\t\t}\n\t\tconst rawArguments = params?.['arguments']\n\t\tconst args = isRecord(rawArguments) ? rawArguments : {}\n\t\tconst callId = request.id === undefined ? crypto.randomUUID() : String(request.id)\n\t\tconst result = await this.#tools.execute({ id: callId, name, arguments: args })\n\t\treturn jsonRPCResult(id, buildToolResult(result))\n\t}\n}\n","import type { EmitterInterface } from '@orkestrel/emitter'\nimport type { ToolInterface } from '@orkestrel/agent'\nimport type {\n\tClientTransportInterface,\n\tJSONRPCMessage,\n\tJSONRPCRequest,\n\tMCPClientEventMap,\n\tMCPClientInterface,\n\tMCPClientOptions,\n} from './types.js'\nimport { Emitter } from '@orkestrel/emitter'\nimport { Tool } from '@orkestrel/agent'\nimport { isArray, isRecord, isString } from '@orkestrel/contract'\nimport {\n\tDEFAULT_MCP_CLIENT_NAME,\n\tDEFAULT_MCP_CLIENT_VERSION,\n\tDEFAULT_MCP_REQUEST_TIMEOUT,\n\tMCP_PROTOCOL_VERSION,\n} from './constants.js'\nimport { isJSONRPCResponse, isRequestId } from './validators.js'\n\n/**\n * A transport-agnostic Model Context Protocol CLIENT — connects to a REMOTE MCP server\n * over an injected {@link ClientTransportInterface}, runs the `initialize` handshake,\n * and exposes the server's tools as local {@link ToolInterface}s an agent can run.\n *\n * @remarks\n * - **The mirror of `MCPServer`.** The server DISPATCHES requests over a tool registry;\n * this client ISSUES them over a transport. `connect` runs `initialize` then sends\n * `notifications/initialized`; `tools()` lists the remote tools and wraps each as a\n * local {@link ToolInterface} whose `execute` calls back through `call`; `call` runs a\n * remote `tools/call` and returns the tool's value (a remote `isError: true` throws\n * locally, so an agent's {@link import('@orkestrel/agent').ToolManagerInterface}\n * isolates it into a result `error` just like a local throw).\n * - **Request↔response correlation.** Each request is tagged with a monotonic numeric\n * `id` ({@link #nextId}); a single transport `message` subscription resolves / rejects\n * the matching {@link #pending} entry by `id`. A message that is NOT a response to a\n * pending request is a server NOTIFICATION — re-surfaced on the `notification` event.\n * - **Per-request deadline.** `#request` races `AbortSignal.timeout(this.#timeout)` (the\n * taverna idiom — never a raw `setTimeout`): a server that never replies REJECTS the\n * pending request once the deadline fires, never hanging.\n * - **Transport-agnostic.** Imports only core siblings (JSON-RPC + the tool vocabulary);\n * the concrete transport is injected. Wire fields are narrowed via the contracts\n * guards (no `as`).\n * - **Observable (§13).** The owned `emitter` fires `connect` / `disconnect` /\n * `notification` / `error`; the emitter isolates a listener throw and routes it to its\n * `error` handler (the `error` option), so a listener throw can never escape.\n *\n * @example\n * ```ts\n * const client = new MCPClient({ transport, name: 'agent', version: '1.0.0' })\n * await client.connect()\n * const tools = await client.tools()\n * agent.context.tools.add(tools) // the remote tools are now the agent's\n * const value = await client.call('search', { query: 'mcp' })\n * ```\n */\nexport class MCPClient implements MCPClientInterface {\n\treadonly #emitter: Emitter<MCPClientEventMap>\n\treadonly #transport: ClientTransportInterface\n\treadonly #name: string\n\treadonly #version: string\n\treadonly #timeout: number\n\t// The in-flight requests, keyed by JSON-RPC id, each holding its promise settlers —\n\t// resolved on the matching response, rejected on an error response, the deadline, or\n\t// `disconnect`. Genuinely private glue (§5): the settler shape lives inline here.\n\treadonly #pending = new Map<\n\t\tstring | number,\n\t\t{ resolve: (value: unknown) => void; reject: (error: Error) => void }\n\t>()\n\t#nextId = 0\n\t#connected = false\n\n\tconstructor(options: MCPClientOptions) {\n\t\tthis.#emitter = new Emitter<MCPClientEventMap>({ on: options.on, error: options.error })\n\t\tthis.#transport = options.transport\n\t\tthis.#name = options.name ?? DEFAULT_MCP_CLIENT_NAME\n\t\tthis.#version = options.version ?? DEFAULT_MCP_CLIENT_VERSION\n\t\tthis.#timeout = options.timeout ?? DEFAULT_MCP_REQUEST_TIMEOUT\n\t\t// One message subscription for the client's whole life: a response settles its\n\t\t// pending request by id; anything else is a server notification.\n\t\tthis.#transport.emitter.on('message', (message) => this.#receive(message))\n\t}\n\n\tget emitter(): EmitterInterface<MCPClientEventMap> {\n\t\treturn this.#emitter\n\t}\n\n\tget connected(): boolean {\n\t\treturn this.#connected\n\t}\n\n\tget transport(): ClientTransportInterface {\n\t\treturn this.#transport\n\t}\n\n\ton<K extends keyof MCPClientEventMap>(\n\t\tevent: K,\n\t\thandler: (...args: MCPClientEventMap[K]) => void,\n\t): void {\n\t\tthis.#emitter.on(event, handler)\n\t}\n\n\tasync connect(): Promise<void> {\n\t\tif (this.#connected) return\n\t\tawait this.#transport.start()\n\t\t// The MCP handshake: negotiate the protocol version + advertise (empty) client\n\t\t// capabilities + identify ourselves, then mark connected and fire the no-args\n\t\t// `notifications/initialized` (a notification — no id, no response).\n\t\tawait this.#request('initialize', {\n\t\t\tprotocolVersion: MCP_PROTOCOL_VERSION,\n\t\t\tcapabilities: {},\n\t\t\tclientInfo: { name: this.#name, version: this.#version },\n\t\t})\n\t\tthis.#connected = true\n\t\tawait this.#transport.send({ jsonrpc: '2.0', method: 'notifications/initialized' })\n\t\tthis.#emitter.emit('connect')\n\t}\n\n\tasync disconnect(): Promise<void> {\n\t\tif (!this.#connected) return\n\t\tthis.#connected = false\n\t\t// Reject every still-pending request so no caller hangs past a disconnect, then\n\t\t// clear the map and tear the transport down.\n\t\tfor (const pending of this.#pending.values()) {\n\t\t\tpending.reject(new Error('MCP client disconnected'))\n\t\t}\n\t\tthis.#pending.clear()\n\t\tawait this.#transport.close()\n\t\tthis.#emitter.emit('disconnect')\n\t}\n\n\tasync tools(): Promise<readonly ToolInterface[]> {\n\t\tconst result = await this.#request('tools/list')\n\t\t// The wire shape is `{ tools: MCPToolDescriptor[] }` — narrow it (§14): a\n\t\t// non-record / non-array `tools` yields no tools rather than throwing.\n\t\tif (!isRecord(result) || !isArray(result['tools'])) return []\n\t\tconst tools: ToolInterface[] = []\n\t\tfor (const descriptor of result['tools']) {\n\t\t\tif (!isRecord(descriptor) || !isString(descriptor['name'])) continue\n\t\t\tconst name = descriptor['name']\n\t\t\ttools.push(this.#tool(name, descriptor))\n\t\t}\n\t\treturn tools\n\t}\n\n\tasync call(name: string, args: Readonly<Record<string, unknown>>): Promise<unknown> {\n\t\tconst result = await this.#request('tools/call', { name, arguments: args })\n\t\t// The inverse of the server's `buildToolResult`: concat the result's text blocks,\n\t\t// then either throw (a remote `isError`) or parse the JSON value.\n\t\tconst text = this.#text(result)\n\t\tif (isRecord(result) && result['isError'] === true) {\n\t\t\tthrow new Error(text.length > 0 ? text : `MCP tool '${name}' failed`)\n\t\t}\n\t\t// A success carries the value JSON-serialized into the text block(s); parse it,\n\t\t// falling back to the raw string when it is not JSON (the inverse of the server's\n\t\t// `JSON.stringify`, whose value-less result is an empty text block).\n\t\tif (text.length === 0) return undefined\n\t\ttry {\n\t\t\treturn JSON.parse(text)\n\t\t} catch {\n\t\t\treturn text\n\t\t}\n\t}\n\n\t// Issue a request and await its correlated response, bounded by the per-request\n\t// deadline. A monotonic numeric id keys the pending settlers; `AbortSignal.timeout`\n\t// (the taverna idiom — never a raw setTimeout) rejects the pending request if the\n\t// server never answers. The transport `send` is awaited so a write failure rejects\n\t// here rather than leaving a pending request to time out.\n\t#request(method: string, params?: Readonly<Record<string, unknown>>): Promise<unknown> {\n\t\tthis.#nextId += 1\n\t\tconst id = this.#nextId\n\t\tconst request: JSONRPCRequest = {\n\t\t\tjsonrpc: '2.0',\n\t\t\tid,\n\t\t\tmethod,\n\t\t\t...(params === undefined ? {} : { params }),\n\t\t}\n\t\treturn new Promise<unknown>((resolve, reject) => {\n\t\t\tconst deadline = AbortSignal.timeout(this.#timeout)\n\t\t\t// Settle once: clear the pending entry + detach the deadline listener, whichever\n\t\t\t// of (response | deadline | send-failure) fires first.\n\t\t\tconst settle = (): void => {\n\t\t\t\tthis.#pending.delete(id)\n\t\t\t\tdeadline.removeEventListener('abort', onDeadline)\n\t\t\t}\n\t\t\tconst onDeadline = (): void => {\n\t\t\t\tsettle()\n\t\t\t\treject(new Error(`MCP request '${method}' timed out after ${this.#timeout}ms`))\n\t\t\t}\n\t\t\tdeadline.addEventListener('abort', onDeadline, { once: true })\n\t\t\tthis.#pending.set(id, {\n\t\t\t\tresolve: (value) => {\n\t\t\t\t\tsettle()\n\t\t\t\t\tresolve(value)\n\t\t\t\t},\n\t\t\t\treject: (error) => {\n\t\t\t\t\tsettle()\n\t\t\t\t\treject(error)\n\t\t\t\t},\n\t\t\t})\n\t\t\tthis.#transport.send(request).catch((error: unknown) => {\n\t\t\t\tconst pending = this.#pending.get(id)\n\t\t\t\tif (pending === undefined) return\n\t\t\t\tpending.reject(error instanceof Error ? error : new Error(String(error)))\n\t\t\t})\n\t\t})\n\t}\n\n\t// Handle one inbound transport message: a response settles its pending request by\n\t// id (an error response rejects, a result resolves); anything else (a message with\n\t// no matching pending id) is a server-initiated notification, re-surfaced on the\n\t// `notification` event.\n\t#receive(message: JSONRPCMessage): void {\n\t\tif (isJSONRPCResponse(message) && isRequestId(message.id)) {\n\t\t\tconst pending = this.#pending.get(message.id)\n\t\t\tif (pending !== undefined) {\n\t\t\t\tif (message.error !== undefined) {\n\t\t\t\t\tpending.reject(new Error(`MCP error ${message.error.code}: ${message.error.message}`))\n\t\t\t\t} else {\n\t\t\t\t\tpending.resolve(message.result)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\t// Not a correlated response — a server notification (or an unsolicited response).\n\t\tthis.#emitter.emit('notification', message)\n\t}\n\n\t// Wrap one remote tool descriptor as a local tool: map `inputSchema` → `parameters`\n\t// (the inverse of the server's rename, no `as`), carry `description` when present,\n\t// and bind `execute` to a remote `tools/call` via `call`.\n\t#tool(name: string, descriptor: Readonly<Record<string, unknown>>): ToolInterface {\n\t\tconst inputSchema = descriptor['inputSchema']\n\t\tconst description = descriptor['description']\n\t\tconst options: {\n\t\t\tname: string\n\t\t\tdescription?: string\n\t\t\tparameters?: Readonly<Record<string, unknown>>\n\t\t\texecute: (args: Readonly<Record<string, unknown>>) => Promise<unknown>\n\t\t} = {\n\t\t\tname,\n\t\t\texecute: (args) => this.call(name, args),\n\t\t}\n\t\tif (isString(description)) options.description = description\n\t\tif (isRecord(inputSchema)) options.parameters = inputSchema\n\t\treturn new Tool(options)\n\t}\n\n\t// Concatenate an MCP tool-call result's text content blocks into one string — the\n\t// inverse of the server splitting a value into text block(s). Total (§14): a\n\t// non-record result, a non-array `content`, or a non-string `text` contributes\n\t// nothing rather than throwing.\n\t#text(result: unknown): string {\n\t\tif (!isRecord(result) || !isArray(result['content'])) return ''\n\t\tconst parts: string[] = []\n\t\tfor (const block of result['content']) {\n\t\t\tif (isRecord(block) && isString(block['text'])) parts.push(block['text'])\n\t\t}\n\t\treturn parts.join('\\n')\n\t}\n}\n","import type {\n\tMCPClientInterface,\n\tMCPClientOptions,\n\tMCPServerInterface,\n\tMCPServerOptions,\n} from './types.js'\nimport { MCPClient } from './MCPClient.js'\nimport { MCPServer } from './MCPServer.js'\n\n/**\n * Create a transport-agnostic Model Context Protocol server — exposes a live\n * {@link import('@orkestrel/agent').ToolManagerInterface} over JSON-RPC 2.0\n * (`initialize` / `ping` / `tools/list` / `tools/call`).\n *\n * @remarks\n * Pump raw message strings through `handle` (parse → dispatch → serialize) from a\n * transport, or call the typed `dispatch` directly with an already-parsed request.\n * The server is provider-agnostic — JSON-RPC plus the tool registry, with no HTTP\n * and no model. The {@link import('@orkestrel/agent').ToolManagerInterface} already\n * isolates a thrown tool into a result error (surfaced as an MCP `isError: true`\n * tool result), so a misbehaving tool never crashes a dispatch. Subscribe to the\n * `request` event via `server.emitter.on('request', …)` for tracing.\n *\n * @param options - `name` / `version` (the server identity), `tools` (the live\n * registry to expose), an optional `description`, and the reserved `on`\n * {@link import('@orkestrel/emitter').EmitterHooks} (see {@link MCPServerOptions})\n * @returns A working {@link MCPServerInterface}\n *\n * @example\n * ```ts\n * import { createMCPServer, createTool, createToolManager } from '@src/core'\n *\n * const tools = createToolManager()\n * tools.add(createTool({ name: 'add', execute: (a) => Number(a.x) + Number(a.y) }))\n *\n * const server = createMCPServer({ name: 'calculator', version: '1.0.0', tools })\n * server.emitter.on('request', (method, id) => log(method, id))\n *\n * // A transport pumps message strings through `handle`:\n * const reply = await server.handle('{\"jsonrpc\":\"2.0\",\"method\":\"tools/list\",\"id\":1}')\n * // reply → '{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"tools\":[{\"name\":\"add\",\"inputSchema\":{\"type\":\"object\"}}]}}'\n * ```\n */\nexport function createMCPServer(options: MCPServerOptions): MCPServerInterface {\n\treturn new MCPServer(options)\n}\n\n/**\n * Create a transport-agnostic Model Context Protocol CLIENT — connects to a REMOTE\n * MCP server over an injected {@link import('./types.js').ClientTransportInterface},\n * runs the `initialize` handshake, and exposes the server's tools as local\n * {@link import('@orkestrel/agent').ToolInterface}s an agent can run.\n *\n * @remarks\n * The egress mirror of {@link createMCPServer}: where the server exposes a local tool\n * registry over MCP, the client USES a remote server's tools. `connect()` handshakes,\n * `tools()` lists + wraps the remote tools (each `execute` calls back over the wire),\n * and `call(name, args)` runs a remote `tools/call` (a remote tool failure throws\n * locally, so an agent's {@link import('@orkestrel/agent').ToolManagerInterface}\n * isolates it). The transport is injected — a concrete one (the HTTP transport over\n * `fetch`) lives in `@src/server`; the client itself is provider-agnostic. Subscribe\n * to `connect` / `disconnect` / `notification` via `client.on(...)` (or\n * `client.emitter.on(...)`).\n *\n * @param options - `transport` (the carrier; REQUIRED), `name` / `version` (the client\n * identity), `timeout` (the per-request deadline), and the reserved `on`\n * {@link import('@orkestrel/emitter').EmitterHooks} (see {@link MCPClientOptions})\n * @returns A working {@link MCPClientInterface}\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 * agent.context.tools.add(await client.tools()) // give the agent the remote tools\n * const value = await client.call('search', { query: 'mcp' })\n * ```\n */\nexport function createMCPClient(options: MCPClientOptions): MCPClientInterface {\n\treturn new MCPClient(options)\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":["#emitter","#name","#version","#tools","#call","#emitter","#transport","#name","#version","#timeout","#pending","#receive","#connected","#request","#tool","#text","#nextId"],"sources":["../../../src/core/constants.ts","../../../src/core/validators.ts","../../../src/core/parsers.ts","../../../src/core/helpers.ts","../../../src/core/MCPServer.ts","../../../src/core/MCPClient.ts","../../../src/core/factories.ts"],"sourcesContent":["// MCP protocol revisions + the reserved JSON-RPC 2.0 error codes. The negotiated\n// protocol version is the current rev unless the client requests a SUPPORTED prior\n// one (see `initializeResult` in ./helpers.js). Transport-level header names\n// (session / version headers) belong to the HTTP transport sub-chunk, NOT here.\n\n/** The MCP protocol revision this server implements (the default negotiated version). */\nexport const MCP_PROTOCOL_VERSION = '2025-06-18'\n\n/**\n * The MCP protocol revisions this server can negotiate — the current\n * {@link MCP_PROTOCOL_VERSION} plus a prior rev a client may still request.\n *\n * @remarks\n * `initialize` echoes the client's requested `protocolVersion` when it appears in\n * this list, else falls back to {@link MCP_PROTOCOL_VERSION}. Frozen so the list is\n * an immutable contract.\n */\nexport const SUPPORTED_PROTOCOL_VERSIONS: readonly string[] = Object.freeze([\n\t'2025-06-18',\n\t'2025-03-26',\n])\n\n/** JSON-RPC 2.0 reserved error: invalid JSON was received (the message did not parse). */\nexport const JSONRPC_PARSE_ERROR = -32700\n\n/** JSON-RPC 2.0 reserved error: the payload was not a valid Request object. */\nexport const JSONRPC_INVALID_REQUEST = -32600\n\n/** JSON-RPC 2.0 reserved error: the requested method does not exist. */\nexport const JSONRPC_METHOD_NOT_FOUND = -32601\n\n/** JSON-RPC 2.0 reserved error: the method's parameters were invalid. */\nexport const JSONRPC_INVALID_PARAMS = -32602\n\n/** JSON-RPC 2.0 implementation-defined server error (the `-32000` to `-32099` range). */\nexport const JSONRPC_SERVER_ERROR = -32000\n\n// MCP CLIENT defaults — the identity an `MCPClient` reports in the `initialize`\n// handshake (`clientInfo`) and the per-request deadline, when the caller supplies\n// none. The egress mirror of the server's protocol-version constants above.\n\n/** The default client name reported in the MCP `initialize` handshake (`clientInfo.name`). */\nexport const DEFAULT_MCP_CLIENT_NAME = 'taverna'\n\n/** The default client version reported in the MCP `initialize` handshake (`clientInfo.version`). */\nexport const DEFAULT_MCP_CLIENT_VERSION = '1.0.0'\n\n/**\n * The default per-request deadline (ms) an `MCPClient` applies when `options.timeout`\n * is unset — a request the remote server does not answer within it rejects.\n */\nexport const DEFAULT_MCP_REQUEST_TIMEOUT = 30_000\n","import type { JSONRPCMessage, JSONRPCRequest, JSONRPCResponse } from './types.js'\nimport { isNumber, isRecord, isString, isUndefined } from '@orkestrel/contract'\n\n// AGENTS §14: every guard here is a TOTAL function over the already-`JSON.parse`d\n// value — adversarial input returns `false`, never throws. The raw-string\n// `JSON.parse` (which CAN throw) happens in `MCPServer.handle` inside a try/catch;\n// these guards only ever see a parsed `unknown`. Each is a flat structural test on\n// `isRecord` + field checks (no user callbacks), so totality is immediate.\n\n/**\n * Determine whether a value is a valid JSON-RPC REQUEST `id` — a string, a number,\n * or absent.\n *\n * @remarks\n * A request id is a string, a number, or `undefined` (its ABSENCE marks a\n * NOTIFICATION). `null` is NOT a valid request id — it is valid only on a RESPONSE.\n * Total (§14): any other input returns `false`.\n *\n * @param value - The already-parsed value to test\n * @returns `true` when `value` is a string, a number, or `undefined`\n *\n * @example\n * ```ts\n * isRequestId(1) // true\n * isRequestId('abc') // true\n * isRequestId(undefined) // true — a notification\n * isRequestId(null) // false — valid only on a response\n * ```\n */\nexport function isRequestId(value: unknown): value is string | number | undefined {\n\treturn isUndefined(value) || isString(value) || isNumber(value)\n}\n\n/**\n * Determine whether a parsed value is a {@link JSONRPCRequest}.\n *\n * @remarks\n * A request is a record with `jsonrpc === '2.0'` and a string `method`. `id`, when\n * present, must be a string or number; its ABSENCE is valid — that marks a\n * NOTIFICATION (a fire-and-forget request that yields no response). `params`, when\n * present, must be a record. Total (§14): any other input returns `false`.\n *\n * @param value - The already-parsed value to test\n * @returns `true` when `value` is a valid JSON-RPC request\n *\n * @example\n * ```ts\n * isJSONRPCRequest({ jsonrpc: '2.0', method: 'ping', id: 1 }) // true\n * isJSONRPCRequest({ jsonrpc: '2.0', method: 'notifications/initialized' }) // true — a notification\n * isJSONRPCRequest({ jsonrpc: '1.0', method: 'ping' }) // false\n * ```\n */\nexport function isJSONRPCRequest(value: unknown): value is JSONRPCRequest {\n\tif (!isRecord(value)) {\n\t\treturn false\n\t}\n\tif (value['jsonrpc'] !== '2.0' || !isString(value['method'])) {\n\t\treturn false\n\t}\n\tif (!isRequestId(value['id'])) {\n\t\treturn false\n\t}\n\tconst params = value['params']\n\treturn isUndefined(params) || isRecord(params)\n}\n\n/**\n * Determine whether a parsed value is a {@link JSONRPCResponse}.\n *\n * @remarks\n * A response is a record with `jsonrpc === '2.0'`, an `id` that is a string,\n * number, or `null`, and EXACTLY ONE of a `result` (any value, including\n * `undefined`'s absence) or an `error` (a record with a numeric `code` and string\n * `message`). Total (§14).\n *\n * @param value - The already-parsed value to test\n * @returns `true` when `value` is a valid JSON-RPC response\n */\nexport function isJSONRPCResponse(value: unknown): value is JSONRPCResponse {\n\tif (!isRecord(value)) {\n\t\treturn false\n\t}\n\tif (value['jsonrpc'] !== '2.0') {\n\t\treturn false\n\t}\n\tconst id = value['id']\n\tif (id !== null && !isString(id) && !isNumber(id)) {\n\t\treturn false\n\t}\n\tconst hasResult = Object.hasOwn(value, 'result')\n\tconst error = value['error']\n\tconst hasError = !isUndefined(error)\n\t// Exactly one of result / error — never both, never neither.\n\tif (hasResult === hasError) {\n\t\treturn false\n\t}\n\tif (hasError) {\n\t\treturn isRecord(error) && isNumber(error['code']) && isString(error['message'])\n\t}\n\treturn true\n}\n\n/**\n * Determine whether a parsed value is a {@link JSONRPCMessage} — a request or a\n * response.\n *\n * @remarks\n * The union of {@link isJSONRPCRequest} and {@link isJSONRPCResponse}. Total (§14).\n *\n * @param value - The already-parsed value to test\n * @returns `true` when `value` is a valid JSON-RPC request or response\n */\nexport function isJSONRPCMessage(value: unknown): value is JSONRPCMessage {\n\treturn isJSONRPCRequest(value) || isJSONRPCResponse(value)\n}\n\n/**\n * Determine whether a parsed value is an MCP `initialize` request — a\n * {@link JSONRPCRequest} whose `method` is `'initialize'`.\n *\n * @param value - The already-parsed value to test\n * @returns `true` when `value` is a valid `initialize` request\n *\n * @example\n * ```ts\n * isInitializeRequest({ jsonrpc: '2.0', method: 'initialize', id: 1 }) // true\n * isInitializeRequest({ jsonrpc: '2.0', method: 'ping', id: 1 }) // false\n * ```\n */\nexport function isInitializeRequest(value: unknown): value is JSONRPCRequest {\n\treturn isJSONRPCRequest(value) && value.method === 'initialize'\n}\n","import type { JSONRPCMessage } from './types.js'\nimport { isJSONRPCMessage } from './validators.js'\n\n/**\n * Narrow an already-parsed value to a {@link JSONRPCMessage}, or `undefined` when\n * it is not one.\n *\n * @remarks\n * Total (§14) — a non-message returns `undefined`, never throws. The input must\n * ALREADY be `JSON.parse`d: the raw-string parse (which can throw on malformed\n * JSON) happens in `MCPServer.handle` inside a try/catch that maps a parse failure\n * to a `-32700` response. Sound with {@link isJSONRPCMessage}: a guard-valid input\n * is returned unchanged, and every non-`undefined` output satisfies the guard.\n *\n * @param value - The already-parsed value to narrow\n * @returns The value as a {@link JSONRPCMessage}, or `undefined`\n *\n * @example\n * ```ts\n * parseJSONRPCMessage({ jsonrpc: '2.0', method: 'ping', id: 1 }) // the request\n * parseJSONRPCMessage({ method: 'ping' }) // undefined — missing jsonrpc\n * ```\n */\nexport function parseJSONRPCMessage(value: unknown): JSONRPCMessage | undefined {\n\treturn isJSONRPCMessage(value) ? value : undefined\n}\n","import type { ToolManagerInterface, ToolResult } from '@orkestrel/agent'\nimport type {\n\tJSONRPCResponse,\n\tMCPClientInterface,\n\tMCPServerInterface,\n\tMCPToolDescriptor,\n\tMCPToolResult,\n\tMCPTransportInterface,\n} from './types.js'\nimport { MCP_PROTOCOL_VERSION, SUPPORTED_PROTOCOL_VERSIONS } from './constants.js'\nimport { parseJSONRPCMessage } from './parsers.js'\n\n// Pure dispatch builders (AGENTS §5: the dispatch branches stay exported helpers,\n// not hidden privates). Each turns a piece of MCP state into the JSON-RPC `result`\n// payload (or a response envelope) the server returns — independently testable.\n\n/**\n * Build a JSON-RPC success {@link JSONRPCResponse} — the `id` echoed, the method's\n * value as `result`.\n *\n * @param id - The request's id (`null` only for a parse / invalid-request error)\n * @param result - The method's return value\n * @returns The success response envelope\n */\nexport function jsonRPCResult(id: string | number | null, result: unknown): JSONRPCResponse {\n\treturn { jsonrpc: '2.0', id, result }\n}\n\n/**\n * Build a JSON-RPC error {@link JSONRPCResponse} — the `id` echoed, the failure as\n * an `error` object.\n *\n * @param id - The request's id (`null` for a parse / invalid-request error)\n * @param code - One of the reserved JSON-RPC codes (see `./constants.js`)\n * @param message - A short human description of the failure\n * @param data - An OPTIONAL machine-readable payload (omitted from the envelope when absent)\n * @returns The error response envelope\n */\nexport function jsonRPCError(\n\tid: string | number | null,\n\tcode: number,\n\tmessage: string,\n\tdata?: unknown,\n): JSONRPCResponse {\n\treturn {\n\t\tjsonrpc: '2.0',\n\t\tid,\n\t\terror: data === undefined ? { code, message } : { code, message, data },\n\t}\n}\n\n/**\n * Map a {@link ToolManagerInterface}'s definitions to MCP `tools/list` descriptors\n * — renaming `parameters` to the wire's `inputSchema`.\n *\n * @remarks\n * Each {@link import('@orkestrel/agent').ToolDefinition} carries through its\n * `name` and (when present) `description`; its open JSON-Schema `parameters`\n * becomes `inputSchema`, defaulting to an empty object schema (`{ type: 'object' }`)\n * when a tool declares none (MCP requires an `inputSchema`).\n *\n * @param manager - The tool registry to describe\n * @returns One {@link MCPToolDescriptor} per registered tool, in registry order\n */\nexport function buildToolDescriptors(manager: ToolManagerInterface): readonly MCPToolDescriptor[] {\n\treturn manager.definitions().map((definition) => {\n\t\tconst descriptor: {\n\t\t\tname: string\n\t\t\tdescription?: string\n\t\t\tinputSchema: Readonly<Record<string, unknown>>\n\t\t} = {\n\t\t\tname: definition.name,\n\t\t\tinputSchema: definition.parameters ?? { type: 'object' },\n\t\t}\n\t\tif (definition.description !== undefined) descriptor.description = definition.description\n\t\treturn descriptor\n\t})\n}\n\n/**\n * Map an executed tool's {@link ToolResult} to an MCP {@link MCPToolResult} — the\n * value (or error) as a `text` content block.\n *\n * @remarks\n * The {@link ToolManagerInterface} already isolates a thrown tool into\n * `result.error` (so the server adds NO try/catch around `execute`): when `error`\n * is present, this builds an `isError: true` result carrying the error text, so the\n * model sees the failure as a tool result it can react to rather than a protocol\n * error; otherwise it serializes `result.value` (via `JSON.stringify`) into one\n * `text` block.\n *\n * @param result - The tool's execution outcome\n * @returns The MCP tool-call result\n */\nexport function buildToolResult(result: ToolResult): MCPToolResult {\n\tif (result.error !== undefined) {\n\t\treturn { content: [{ type: 'text', text: result.error }], isError: true }\n\t}\n\t// A content block must carry a string `text`; `JSON.stringify(undefined)` is the value\n\t// `undefined` (which serializes away), so a value-less result becomes an empty text block.\n\tconst text = result.value === undefined ? '' : JSON.stringify(result.value)\n\treturn { content: [{ type: 'text', text }] }\n}\n\n/**\n * Build the MCP `initialize` result — the negotiated protocol version, the\n * advertised capabilities, and the server identity.\n *\n * @remarks\n * Version negotiation echoes the client's `requested` version when it is one of the\n * {@link SUPPORTED_PROTOCOL_VERSIONS}, else falls back to {@link MCP_PROTOCOL_VERSION}.\n * `capabilities.tools` is an empty object — this server advertises the tools\n * capability with no sub-options (no list-changed notification yet).\n *\n * @param name - The server name (echoed in `serverInfo`)\n * @param version - The server version (echoed in `serverInfo`)\n * @param requested - The client's requested protocol version (negotiated when supported)\n * @returns The `initialize` result payload\n */\nexport function initializeResult(\n\tname: string,\n\tversion: string,\n\trequested?: string,\n): Readonly<Record<string, unknown>> {\n\tconst protocolVersion =\n\t\trequested !== undefined && SUPPORTED_PROTOCOL_VERSIONS.includes(requested)\n\t\t\t? requested\n\t\t\t: MCP_PROTOCOL_VERSION\n\treturn {\n\t\tprotocolVersion,\n\t\tcapabilities: { tools: {} },\n\t\tserverInfo: { name, version },\n\t}\n}\n\n// The environment-agnostic PORT binders — the keystone that lets an\n// {@link MCPServerInterface} / {@link MCPClientInterface} run over ANY\n// {@link MCPTransportInterface} (a Node stdio pair, a browser MessagePort, a Web\n// Worker `self`) with no per-environment dispatch/correlation wiring duplicated at\n// each face. Both are TOTAL: a `send` throw or rejection is caught and never\n// escapes as an unhandled rejection.\n\n/**\n * Pipe an {@link MCPTransportInterface} into an {@link MCPServerInterface} — every\n * inbound message runs through `server.handle`, and a defined reply is written back\n * via `transport.send`.\n *\n * @remarks\n * `server.handle` already turns a malformed message into a serialized `-32700` /\n * `-32600` reply and a notification into `undefined` (no reply), so this binder adds\n * no parsing of its own. A `transport.send` throw or rejection is caught and routed\n * to `server.emitter`'s `error` event (never rethrown, never an unhandled rejection);\n * a listener on that event that itself throws is swallowed (the end of the line —\n * the caller's own bug, never this binder's). The returned unbind DETACHES this\n * binder (further inbound messages and the transport's `closed` signal are ignored)\n * WITHOUT closing the transport — closing is the caller's decision.\n *\n * `listen`/`closed` are REPLACE semantics (§ port contract): the returned unbind\n * DETACHES by replacing this binder's own handlers with no-ops, so a subsequent\n * `bindServer` call on the SAME transport is never double-dispatched by a stale\n * subscription left behind — an unbind→rebind cycle yields exactly one reply per\n * request.\n *\n * @param server - The transport-agnostic server to dispatch inbound messages over\n * @param transport - The duplex channel to pipe the server over\n * @returns Detach this binder from the transport (does not close it)\n *\n * @example\n * ```ts\n * const unbind = bindServer(server, transport)\n * // ... later, detach without closing:\n * unbind()\n * ```\n */\nexport function bindServer(\n\tserver: MCPServerInterface,\n\ttransport: MCPTransportInterface,\n): () => void {\n\tlet active = true\n\ttransport.listen((message) => {\n\t\tif (!active) return\n\t\tvoid (async () => {\n\t\t\ttry {\n\t\t\t\tconst response = await server.handle(message)\n\t\t\t\tif (response !== undefined) await transport.send(response)\n\t\t\t} catch (error) {\n\t\t\t\ttry {\n\t\t\t\t\tserver.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\ttransport.closed(() => {\n\t\tactive = false\n\t})\n\treturn () => {\n\t\tactive = false\n\t\ttransport.listen(() => {})\n\t\ttransport.closed(() => {})\n\t}\n}\n\n/**\n * Pipe an {@link MCPTransportInterface} into an {@link MCPClientInterface} — every\n * inbound message is decoded and delivered onto the client's OWN transport\n * (`client.transport.emitter`'s `message` / `close` events), resolving/rejecting the\n * client's correlated pending requests exactly as a direct reply would.\n *\n * @remarks\n * The client's outbound writes flow through `client.transport.send` — its existing,\n * unmodified request/response correlation — so `client` must have been constructed\n * with a {@link import('./types.js').ClientTransportInterface} that itself carries\n * the SAME `transport` (see {@link import('./factories.js').createDuplexClientTransport},\n * the additive factory that adapts an {@link MCPTransportInterface} into that shape);\n * this binder then completes the inbound half by decoding each message and pushing it\n * onto `client.transport.emitter` (an {@link import('@orkestrel/emitter').EmitterInterface}\n * exposes `emit`, so no client modification is needed). A malformed / non-JSON-RPC\n * inbound message is DROPPED (§14, total — never throws); a delivery fault is routed to\n * `client.transport.emitter`'s `error` event (never rethrown). The returned unbind\n * DETACHES this binder (further inbound messages and the transport's `closed` signal are\n * ignored) WITHOUT closing the transport.\n *\n * `listen`/`closed` are REPLACE semantics (§ port contract): the returned unbind\n * DETACHES by replacing this binder's own handlers with no-ops, so a subsequent\n * `bindClient` call on the SAME transport is never double-dispatched by a stale\n * subscription left behind — an unbind→rebind cycle delivers exactly one `message`\n * emit per inbound reply.\n *\n * @param client - The transport-agnostic client whose transport to deliver messages onto\n * @param transport - The duplex channel to pipe the client over\n * @returns Detach this binder from the transport (does not close it)\n *\n * @example\n * ```ts\n * const client = createMCPClient({ transport: createDuplexClientTransport(transport) })\n * const unbind = bindClient(client, transport)\n * await client.connect()\n * // ... later, detach without closing:\n * unbind()\n * ```\n */\nexport function bindClient(\n\tclient: MCPClientInterface,\n\ttransport: MCPTransportInterface,\n): () => void {\n\tlet active = true\n\ttransport.listen((message) => {\n\t\tif (!active) return\n\t\tlet parsed: unknown\n\t\ttry {\n\t\t\tparsed = JSON.parse(message)\n\t\t} catch {\n\t\t\treturn\n\t\t}\n\t\tconst decoded = parseJSONRPCMessage(parsed)\n\t\tif (decoded === undefined) return\n\t\ttry {\n\t\t\tclient.transport.emitter.emit('message', decoded)\n\t\t} catch (error) {\n\t\t\ttry {\n\t\t\t\tclient.transport.emitter.emit('error', error)\n\t\t\t} catch {\n\t\t\t\t// A throwing `error` listener is the caller's own bug — the end of the line.\n\t\t\t}\n\t\t}\n\t})\n\ttransport.closed(() => {\n\t\tif (!active) return\n\t\tactive = false\n\t\ttry {\n\t\t\tclient.transport.emitter.emit('close')\n\t\t} catch {\n\t\t\t// A throwing `close` listener is the caller's own bug — the end of the line.\n\t\t}\n\t})\n\treturn () => {\n\t\tactive = false\n\t\ttransport.listen(() => {})\n\t\ttransport.closed(() => {})\n\t}\n}\n","import type { EmitterInterface } from '@orkestrel/emitter'\nimport type { ToolManagerInterface } from '@orkestrel/agent'\nimport type {\n\tJSONRPCRequest,\n\tJSONRPCResponse,\n\tMCPServerEventMap,\n\tMCPServerInterface,\n\tMCPServerOptions,\n} from './types.js'\nimport { Emitter } from '@orkestrel/emitter'\nimport { isRecord, isString } from '@orkestrel/contract'\nimport {\n\tJSONRPC_INVALID_PARAMS,\n\tJSONRPC_INVALID_REQUEST,\n\tJSONRPC_METHOD_NOT_FOUND,\n\tJSONRPC_PARSE_ERROR,\n} from './constants.js'\nimport {\n\tbuildToolDescriptors,\n\tbuildToolResult,\n\tinitializeResult,\n\tjsonRPCError,\n\tjsonRPCResult,\n} from './helpers.js'\nimport { parseJSONRPCMessage } from './parsers.js'\n\n/**\n * A transport-agnostic Model Context Protocol server — dispatches JSON-RPC 2.0\n * requests over a live {@link ToolManagerInterface}, with NO transport coupling.\n *\n * @remarks\n * - **Two entry points.** `dispatch(request)` runs an already-parsed request and\n * resolves a {@link JSONRPCResponse} — or `undefined` for a NOTIFICATION (a\n * request with no `id`). `handle(message)` is the string boundary: it\n * `JSON.parse`s the raw message (a failure → a `-32700` response), narrows it to\n * a request (a non-request → a `-32600` response), dispatches, and serializes the\n * response back to a string (`undefined` for a notification).\n * - **The method switch.** `initialize` negotiates the protocol version + advertises\n * the tools capability; `notifications/initialized` is a notification (no\n * response); `ping` returns `{}`; `tools/list` lists the registry's tools (its\n * `parameters` renamed to `inputSchema`); `tools/call` runs a tool by name (the\n * {@link ToolManagerInterface} isolates a tool throw into the result `error`, which\n * maps to an `isError: true` tool result — so the server adds NO try/catch). An\n * unknown method → `-32601`; a `tools/call` with a missing / non-string `name` →\n * `-32602`.\n * - **Provider-agnostic.** Imports only core siblings — JSON-RPC + the tool registry,\n * no HTTP, no model. Wire fields are narrowed via the contracts guards (no `as`).\n * - **Observable (§13).** The owned `emitter` fires `request` at the top of every\n * dispatch; the emitter isolates a listener throw and routes it to its `error` handler\n * (the `error` option), so a listener throw can never escape the dispatch.\n *\n * @example\n * ```ts\n * const tools = createToolManager()\n * tools.add(createTool({ name: 'add', execute: (a) => Number(a.x) + Number(a.y) }))\n * const server = new MCPServer({ name: 'demo', version: '1.0.0', tools })\n * await server.handle('{\"jsonrpc\":\"2.0\",\"method\":\"ping\",\"id\":1}') // '{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{}}'\n * ```\n */\nexport class MCPServer implements MCPServerInterface {\n\treadonly #emitter: Emitter<MCPServerEventMap>\n\treadonly #name: string\n\treadonly #version: string\n\treadonly #tools: ToolManagerInterface\n\n\tconstructor(options: MCPServerOptions) {\n\t\tthis.#emitter = new Emitter<MCPServerEventMap>({ on: options.on, error: options.error })\n\t\tthis.#name = options.name\n\t\tthis.#version = options.version\n\t\tthis.#tools = options.tools\n\t}\n\n\tget emitter(): EmitterInterface<MCPServerEventMap> {\n\t\treturn this.#emitter\n\t}\n\n\tget name(): string {\n\t\treturn this.#name\n\t}\n\n\tget version(): string {\n\t\treturn this.#version\n\t}\n\n\tasync dispatch(request: JSONRPCRequest): Promise<JSONRPCResponse | undefined> {\n\t\tconst id = request.id ?? null\n\t\tthis.#emitter.emit('request', request.method, id)\n\t\t// JSON-RPC: a request with NO `id` is a NOTIFICATION — it is handled (the\n\t\t// `request` event already fired) but NEVER produces a response, whatever its\n\t\t// method (`notifications/initialized`, a fire-and-forget `ping`, an unknown\n\t\t// method — all silent). So short-circuit here, and the switch below only ever\n\t\t// runs for an id-bearing request that expects a reply.\n\t\tif (request.id === undefined) {\n\t\t\treturn undefined\n\t\t}\n\t\tswitch (request.method) {\n\t\t\tcase 'initialize': {\n\t\t\t\tconst requested = request.params?.['protocolVersion']\n\t\t\t\treturn jsonRPCResult(\n\t\t\t\t\tid,\n\t\t\t\t\tinitializeResult(this.#name, this.#version, isString(requested) ? requested : undefined),\n\t\t\t\t)\n\t\t\t}\n\t\t\tcase 'ping':\n\t\t\t\treturn jsonRPCResult(id, {})\n\t\t\tcase 'tools/list':\n\t\t\t\treturn jsonRPCResult(id, { tools: buildToolDescriptors(this.#tools) })\n\t\t\tcase 'tools/call':\n\t\t\t\treturn this.#call(request, id)\n\t\t\tdefault:\n\t\t\t\treturn jsonRPCError(id, JSONRPC_METHOD_NOT_FOUND, `Method not found: ${request.method}`)\n\t\t}\n\t}\n\n\tasync handle(message: string): Promise<string | undefined> {\n\t\tlet parsed: unknown\n\t\ttry {\n\t\t\tparsed = JSON.parse(message)\n\t\t} catch {\n\t\t\treturn JSON.stringify(jsonRPCError(null, JSONRPC_PARSE_ERROR, 'Parse error'))\n\t\t}\n\t\tconst decoded = parseJSONRPCMessage(parsed)\n\t\t// Only a REQUEST is dispatchable — a response (or any non-message) is invalid input.\n\t\tif (decoded === undefined || !('method' in decoded)) {\n\t\t\treturn JSON.stringify(jsonRPCError(null, JSONRPC_INVALID_REQUEST, 'Invalid Request'))\n\t\t}\n\t\tconst response = await this.dispatch(decoded)\n\t\treturn response === undefined ? undefined : JSON.stringify(response)\n\t}\n\n\t// Run a `tools/call`: narrow `params.name` (string) + `params.arguments` (record,\n\t// default `{}`) with no `as`, execute the tool (the manager isolates a throw into\n\t// `result.error`), and map the result to an MCP tool-call result.\n\tasync #call(request: JSONRPCRequest, id: string | number | null): Promise<JSONRPCResponse> {\n\t\tconst params = request.params\n\t\tconst name = params?.['name']\n\t\tif (!isString(name)) {\n\t\t\treturn jsonRPCError(id, JSONRPC_INVALID_PARAMS, 'Invalid params: a string `name` is required')\n\t\t}\n\t\tconst rawArguments = params?.['arguments']\n\t\tconst args = isRecord(rawArguments) ? rawArguments : {}\n\t\tconst callId = request.id === undefined ? crypto.randomUUID() : String(request.id)\n\t\tconst result = await this.#tools.execute({ id: callId, name, arguments: args })\n\t\treturn jsonRPCResult(id, buildToolResult(result))\n\t}\n}\n","import type { EmitterInterface } from '@orkestrel/emitter'\nimport type { ToolInterface } from '@orkestrel/agent'\nimport type {\n\tClientTransportInterface,\n\tJSONRPCMessage,\n\tJSONRPCRequest,\n\tMCPClientEventMap,\n\tMCPClientInterface,\n\tMCPClientOptions,\n} from './types.js'\nimport { Emitter } from '@orkestrel/emitter'\nimport { Tool } from '@orkestrel/agent'\nimport { isArray, isRecord, isString } from '@orkestrel/contract'\nimport {\n\tDEFAULT_MCP_CLIENT_NAME,\n\tDEFAULT_MCP_CLIENT_VERSION,\n\tDEFAULT_MCP_REQUEST_TIMEOUT,\n\tMCP_PROTOCOL_VERSION,\n} from './constants.js'\nimport { isJSONRPCResponse, isRequestId } from './validators.js'\n\n/**\n * A transport-agnostic Model Context Protocol CLIENT — connects to a REMOTE MCP server\n * over an injected {@link ClientTransportInterface}, runs the `initialize` handshake,\n * and exposes the server's tools as local {@link ToolInterface}s an agent can run.\n *\n * @remarks\n * - **The mirror of `MCPServer`.** The server DISPATCHES requests over a tool registry;\n * this client ISSUES them over a transport. `connect` runs `initialize` then sends\n * `notifications/initialized`; `tools()` lists the remote tools and wraps each as a\n * local {@link ToolInterface} whose `execute` calls back through `call`; `call` runs a\n * remote `tools/call` and returns the tool's value (a remote `isError: true` throws\n * locally, so an agent's {@link import('@orkestrel/agent').ToolManagerInterface}\n * isolates it into a result `error` just like a local throw).\n * - **Request↔response correlation.** Each request is tagged with a monotonic numeric\n * `id` ({@link #nextId}); a single transport `message` subscription resolves / rejects\n * the matching {@link #pending} entry by `id`. A message that is NOT a response to a\n * pending request is a server NOTIFICATION — re-surfaced on the `notification` event.\n * - **Per-request deadline.** `#request` races `AbortSignal.timeout(this.#timeout)` (the\n * taverna idiom — never a raw `setTimeout`): a server that never replies REJECTS the\n * pending request once the deadline fires, never hanging.\n * - **Transport-agnostic.** Imports only core siblings (JSON-RPC + the tool vocabulary);\n * the concrete transport is injected. Wire fields are narrowed via the contracts\n * guards (no `as`).\n * - **Observable (§13).** The owned `emitter` fires `connect` / `disconnect` /\n * `notification` / `error`; the emitter isolates a listener throw and routes it to its\n * `error` handler (the `error` option), so a listener throw can never escape.\n *\n * @example\n * ```ts\n * const client = new MCPClient({ transport, name: 'agent', version: '1.0.0' })\n * await client.connect()\n * const tools = await client.tools()\n * agent.context.tools.add(tools) // the remote tools are now the agent's\n * const value = await client.call('search', { query: 'mcp' })\n * ```\n */\nexport class MCPClient implements MCPClientInterface {\n\treadonly #emitter: Emitter<MCPClientEventMap>\n\treadonly #transport: ClientTransportInterface\n\treadonly #name: string\n\treadonly #version: string\n\treadonly #timeout: number\n\t// The in-flight requests, keyed by JSON-RPC id, each holding its promise settlers —\n\t// resolved on the matching response, rejected on an error response, the deadline, or\n\t// `disconnect`. Genuinely private glue (§5): the settler shape lives inline here.\n\treadonly #pending = new Map<\n\t\tstring | number,\n\t\t{ resolve: (value: unknown) => void; reject: (error: Error) => void }\n\t>()\n\t#nextId = 0\n\t#connected = false\n\n\tconstructor(options: MCPClientOptions) {\n\t\tthis.#emitter = new Emitter<MCPClientEventMap>({ on: options.on, error: options.error })\n\t\tthis.#transport = options.transport\n\t\tthis.#name = options.name ?? DEFAULT_MCP_CLIENT_NAME\n\t\tthis.#version = options.version ?? DEFAULT_MCP_CLIENT_VERSION\n\t\tthis.#timeout = options.timeout ?? DEFAULT_MCP_REQUEST_TIMEOUT\n\t\t// One message subscription for the client's whole life: a response settles its\n\t\t// pending request by id; anything else is a server notification.\n\t\tthis.#transport.emitter.on('message', (message) => this.#receive(message))\n\t}\n\n\tget emitter(): EmitterInterface<MCPClientEventMap> {\n\t\treturn this.#emitter\n\t}\n\n\tget connected(): boolean {\n\t\treturn this.#connected\n\t}\n\n\tget transport(): ClientTransportInterface {\n\t\treturn this.#transport\n\t}\n\n\ton<K extends keyof MCPClientEventMap>(\n\t\tevent: K,\n\t\thandler: (...args: MCPClientEventMap[K]) => void,\n\t): void {\n\t\tthis.#emitter.on(event, handler)\n\t}\n\n\tasync connect(): Promise<void> {\n\t\tif (this.#connected) return\n\t\tawait this.#transport.start()\n\t\t// The MCP handshake: negotiate the protocol version + advertise (empty) client\n\t\t// capabilities + identify ourselves, then mark connected and fire the no-args\n\t\t// `notifications/initialized` (a notification — no id, no response).\n\t\tawait this.#request('initialize', {\n\t\t\tprotocolVersion: MCP_PROTOCOL_VERSION,\n\t\t\tcapabilities: {},\n\t\t\tclientInfo: { name: this.#name, version: this.#version },\n\t\t})\n\t\tthis.#connected = true\n\t\tawait this.#transport.send({ jsonrpc: '2.0', method: 'notifications/initialized' })\n\t\tthis.#emitter.emit('connect')\n\t}\n\n\tasync disconnect(): Promise<void> {\n\t\tif (!this.#connected) return\n\t\tthis.#connected = false\n\t\t// Reject every still-pending request so no caller hangs past a disconnect, then\n\t\t// clear the map and tear the transport down.\n\t\tfor (const pending of this.#pending.values()) {\n\t\t\tpending.reject(new Error('MCP client disconnected'))\n\t\t}\n\t\tthis.#pending.clear()\n\t\tawait this.#transport.close()\n\t\tthis.#emitter.emit('disconnect')\n\t}\n\n\tasync tools(): Promise<readonly ToolInterface[]> {\n\t\tconst result = await this.#request('tools/list')\n\t\t// The wire shape is `{ tools: MCPToolDescriptor[] }` — narrow it (§14): a\n\t\t// non-record / non-array `tools` yields no tools rather than throwing.\n\t\tif (!isRecord(result) || !isArray(result['tools'])) return []\n\t\tconst tools: ToolInterface[] = []\n\t\tfor (const descriptor of result['tools']) {\n\t\t\tif (!isRecord(descriptor) || !isString(descriptor['name'])) continue\n\t\t\tconst name = descriptor['name']\n\t\t\ttools.push(this.#tool(name, descriptor))\n\t\t}\n\t\treturn tools\n\t}\n\n\tasync call(name: string, args: Readonly<Record<string, unknown>>): Promise<unknown> {\n\t\tconst result = await this.#request('tools/call', { name, arguments: args })\n\t\t// The inverse of the server's `buildToolResult`: concat the result's text blocks,\n\t\t// then either throw (a remote `isError`) or parse the JSON value.\n\t\tconst text = this.#text(result)\n\t\tif (isRecord(result) && result['isError'] === true) {\n\t\t\tthrow new Error(text.length > 0 ? text : `MCP tool '${name}' failed`)\n\t\t}\n\t\t// A success carries the value JSON-serialized into the text block(s); parse it,\n\t\t// falling back to the raw string when it is not JSON (the inverse of the server's\n\t\t// `JSON.stringify`, whose value-less result is an empty text block).\n\t\tif (text.length === 0) return undefined\n\t\ttry {\n\t\t\treturn JSON.parse(text)\n\t\t} catch {\n\t\t\treturn text\n\t\t}\n\t}\n\n\t// Issue a request and await its correlated response, bounded by the per-request\n\t// deadline. A monotonic numeric id keys the pending settlers; `AbortSignal.timeout`\n\t// (the taverna idiom — never a raw setTimeout) rejects the pending request if the\n\t// server never answers. The transport `send` is awaited so a write failure rejects\n\t// here rather than leaving a pending request to time out.\n\t#request(method: string, params?: Readonly<Record<string, unknown>>): Promise<unknown> {\n\t\tthis.#nextId += 1\n\t\tconst id = this.#nextId\n\t\tconst request: JSONRPCRequest = {\n\t\t\tjsonrpc: '2.0',\n\t\t\tid,\n\t\t\tmethod,\n\t\t\t...(params === undefined ? {} : { params }),\n\t\t}\n\t\treturn new Promise<unknown>((resolve, reject) => {\n\t\t\tconst deadline = AbortSignal.timeout(this.#timeout)\n\t\t\t// Settle once: clear the pending entry + detach the deadline listener, whichever\n\t\t\t// of (response | deadline | send-failure) fires first.\n\t\t\tconst settle = (): void => {\n\t\t\t\tthis.#pending.delete(id)\n\t\t\t\tdeadline.removeEventListener('abort', onDeadline)\n\t\t\t}\n\t\t\tconst onDeadline = (): void => {\n\t\t\t\tsettle()\n\t\t\t\treject(new Error(`MCP request '${method}' timed out after ${this.#timeout}ms`))\n\t\t\t}\n\t\t\tdeadline.addEventListener('abort', onDeadline, { once: true })\n\t\t\tthis.#pending.set(id, {\n\t\t\t\tresolve: (value) => {\n\t\t\t\t\tsettle()\n\t\t\t\t\tresolve(value)\n\t\t\t\t},\n\t\t\t\treject: (error) => {\n\t\t\t\t\tsettle()\n\t\t\t\t\treject(error)\n\t\t\t\t},\n\t\t\t})\n\t\t\tthis.#transport.send(request).catch((error: unknown) => {\n\t\t\t\tconst pending = this.#pending.get(id)\n\t\t\t\tif (pending === undefined) return\n\t\t\t\tpending.reject(error instanceof Error ? error : new Error(String(error)))\n\t\t\t})\n\t\t})\n\t}\n\n\t// Handle one inbound transport message: a response settles its pending request by\n\t// id (an error response rejects, a result resolves); anything else (a message with\n\t// no matching pending id) is a server-initiated notification, re-surfaced on the\n\t// `notification` event.\n\t#receive(message: JSONRPCMessage): void {\n\t\tif (isJSONRPCResponse(message) && isRequestId(message.id)) {\n\t\t\tconst pending = this.#pending.get(message.id)\n\t\t\tif (pending !== undefined) {\n\t\t\t\tif (message.error !== undefined) {\n\t\t\t\t\tpending.reject(new Error(`MCP error ${message.error.code}: ${message.error.message}`))\n\t\t\t\t} else {\n\t\t\t\t\tpending.resolve(message.result)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\t// Not a correlated response — a server notification (or an unsolicited response).\n\t\tthis.#emitter.emit('notification', message)\n\t}\n\n\t// Wrap one remote tool descriptor as a local tool: map `inputSchema` → `parameters`\n\t// (the inverse of the server's rename, no `as`), carry `description` when present,\n\t// and bind `execute` to a remote `tools/call` via `call`.\n\t#tool(name: string, descriptor: Readonly<Record<string, unknown>>): ToolInterface {\n\t\tconst inputSchema = descriptor['inputSchema']\n\t\tconst description = descriptor['description']\n\t\tconst options: {\n\t\t\tname: string\n\t\t\tdescription?: string\n\t\t\tparameters?: Readonly<Record<string, unknown>>\n\t\t\texecute: (args: Readonly<Record<string, unknown>>) => Promise<unknown>\n\t\t} = {\n\t\t\tname,\n\t\t\texecute: (args) => this.call(name, args),\n\t\t}\n\t\tif (isString(description)) options.description = description\n\t\tif (isRecord(inputSchema)) options.parameters = inputSchema\n\t\treturn new Tool(options)\n\t}\n\n\t// Concatenate an MCP tool-call result's text content blocks into one string — the\n\t// inverse of the server splitting a value into text block(s). Total (§14): a\n\t// non-record result, a non-array `content`, or a non-string `text` contributes\n\t// nothing rather than throwing.\n\t#text(result: unknown): string {\n\t\tif (!isRecord(result) || !isArray(result['content'])) return ''\n\t\tconst parts: string[] = []\n\t\tfor (const block of result['content']) {\n\t\t\tif (isRecord(block) && isString(block['text'])) parts.push(block['text'])\n\t\t}\n\t\treturn parts.join('\\n')\n\t}\n}\n","import type {\n\tClientTransportEventMap,\n\tClientTransportInterface,\n\tJSONRPCMessage,\n\tMCPClientInterface,\n\tMCPClientOptions,\n\tMCPServerInterface,\n\tMCPServerOptions,\n\tMCPTransportInterface,\n} from './types.js'\nimport { Emitter } from '@orkestrel/emitter'\nimport { MCPClient } from './MCPClient.js'\nimport { MCPServer } from './MCPServer.js'\n\n/**\n * Create a transport-agnostic Model Context Protocol server — exposes a live\n * {@link import('@orkestrel/agent').ToolManagerInterface} over JSON-RPC 2.0\n * (`initialize` / `ping` / `tools/list` / `tools/call`).\n *\n * @remarks\n * Pump raw message strings through `handle` (parse → dispatch → serialize) from a\n * transport, or call the typed `dispatch` directly with an already-parsed request.\n * The server is provider-agnostic — JSON-RPC plus the tool registry, with no HTTP\n * and no model. The {@link import('@orkestrel/agent').ToolManagerInterface} already\n * isolates a thrown tool into a result error (surfaced as an MCP `isError: true`\n * tool result), so a misbehaving tool never crashes a dispatch. Subscribe to the\n * `request` event via `server.emitter.on('request', …)` for tracing.\n *\n * @param options - `name` / `version` (the server identity), `tools` (the live\n * registry to expose), an optional `description`, and the reserved `on`\n * {@link import('@orkestrel/emitter').EmitterHooks} (see {@link MCPServerOptions})\n * @returns A working {@link MCPServerInterface}\n *\n * @example\n * ```ts\n * import { createMCPServer, createTool, createToolManager } from '@src/core'\n *\n * const tools = createToolManager()\n * tools.add(createTool({ name: 'add', execute: (a) => Number(a.x) + Number(a.y) }))\n *\n * const server = createMCPServer({ name: 'calculator', version: '1.0.0', tools })\n * server.emitter.on('request', (method, id) => log(method, id))\n *\n * // A transport pumps message strings through `handle`:\n * const reply = await server.handle('{\"jsonrpc\":\"2.0\",\"method\":\"tools/list\",\"id\":1}')\n * // reply → '{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"tools\":[{\"name\":\"add\",\"inputSchema\":{\"type\":\"object\"}}]}}'\n * ```\n */\nexport function createMCPServer(options: MCPServerOptions): MCPServerInterface {\n\treturn new MCPServer(options)\n}\n\n/**\n * Create a transport-agnostic Model Context Protocol CLIENT — connects to a REMOTE\n * MCP server over an injected {@link import('./types.js').ClientTransportInterface},\n * runs the `initialize` handshake, and exposes the server's tools as local\n * {@link import('@orkestrel/agent').ToolInterface}s an agent can run.\n *\n * @remarks\n * The egress mirror of {@link createMCPServer}: where the server exposes a local tool\n * registry over MCP, the client USES a remote server's tools. `connect()` handshakes,\n * `tools()` lists + wraps the remote tools (each `execute` calls back over the wire),\n * and `call(name, args)` runs a remote `tools/call` (a remote tool failure throws\n * locally, so an agent's {@link import('@orkestrel/agent').ToolManagerInterface}\n * isolates it). The transport is injected — a concrete one (the HTTP transport over\n * `fetch`) lives in `@src/server`; the client itself is provider-agnostic. Subscribe\n * to `connect` / `disconnect` / `notification` via `client.on(...)` (or\n * `client.emitter.on(...)`).\n *\n * @param options - `transport` (the carrier; REQUIRED), `name` / `version` (the client\n * identity), `timeout` (the per-request deadline), and the reserved `on`\n * {@link import('@orkestrel/emitter').EmitterHooks} (see {@link MCPClientOptions})\n * @returns A working {@link MCPClientInterface}\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 * agent.context.tools.add(await client.tools()) // give the agent the remote tools\n * const value = await client.call('search', { query: 'mcp' })\n * ```\n */\nexport function createMCPClient(options: MCPClientOptions): MCPClientInterface {\n\treturn new MCPClient(options)\n}\n\n/**\n * Adapt an {@link MCPTransportInterface} (the environment-agnostic duplex message\n * channel) into a {@link ClientTransportInterface} — the additive bridge that lets\n * `createMCPClient` run over the new port without any change to `MCPClient`'s\n * existing shape.\n *\n * @remarks\n * Hand the RESULT to `createMCPClient({ transport })`, then pass the SAME\n * `transport` to {@link import('./helpers.js').bindClient} to complete the inbound\n * wiring: `send` serializes each outbound {@link JSONRPCMessage} (or batch, one per\n * message) and writes it via `transport.send`; `close` closes the underlying\n * `transport`; `start` is a no-op (the duplex channel is already open by the time\n * it is handed in — there is no separate connect step at this layer); `session` is\n * always `undefined` (session correlation is a higher-level concern the duplex port\n * does not carry). Inbound delivery (`emitter`'s `message` / `close` events) is\n * `bindClient`'s job, not this factory's — the returned object exposes a `message`-\n * capable emitter for `bindClient` to push onto.\n *\n * @param transport - The duplex channel to adapt\n * @returns A {@link ClientTransportInterface} `createMCPClient` can drive\n *\n * @example\n * ```ts\n * const client = createMCPClient({ transport: createDuplexClientTransport(transport) })\n * const unbind = bindClient(client, transport)\n * await client.connect()\n * ```\n */\nexport function createDuplexClientTransport(\n\ttransport: MCPTransportInterface,\n): ClientTransportInterface {\n\tconst emitter = new Emitter<ClientTransportEventMap>()\n\treturn {\n\t\temitter,\n\t\tsession: undefined,\n\t\tasync start(): Promise<void> {\n\t\t\t// The duplex channel is already open by the time it is handed in — no separate\n\t\t\t// connect step at this layer.\n\t\t},\n\t\tasync send(message: JSONRPCMessage | readonly JSONRPCMessage[]): Promise<void> {\n\t\t\tconst messages = Array.isArray(message) ? message : [message]\n\t\t\tfor (const one of messages) await transport.send(JSON.stringify(one))\n\t\t},\n\t\tasync close(): Promise<void> {\n\t\t\tawait transport.close()\n\t\t},\n\t}\n}\n"],"mappings":"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"}