@orkestrel/mcp 0.0.25 → 0.0.27

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.
@@ -235,7 +235,8 @@ export declare const DEFAULT_MCP_SERVER_VERSION = "1.0.0";
235
235
  * initialize result's `protocolVersion` is likewise captured, but only
236
236
  * when it is a SUPPORTED value, and echoed as `mcp-protocol-version` alone on
237
237
  * subsequent legacy requests. Modern requests instead derive protocol and method
238
- * headers from the message, plus the name header only for `tools/call`.
238
+ * headers from the message, plus the name header only for `tools/call` — carried in the
239
+ * protocol's Base64 sentinel form whenever the tool name cannot ride as plain ASCII.
239
240
  * Before initialize returns, neither captured legacy header is sent.
240
241
  * `close()` clears the captured protocol so a reconnect's `initialize`
241
242
  * POST is headerless; the captured `session` persists across `close()`.
@@ -542,7 +543,13 @@ export declare interface ServeMCPScopeInterface {
542
543
  * - **Queued sends.** `send` writes each message as one text frame immediately once
543
544
  * the socket is `OPEN`; a `send` issued before `'open'` fires (or before `start()`
544
545
  * is even called) is QUEUED and flushed, IN ORDER, the moment the socket opens —
545
- * so a caller need not await `start()` before calling `send`.
546
+ * so a caller need not await `start()` before calling `send`. A queue rides ONE
547
+ * connection: a close DISCARDS whatever is still in it.
548
+ * - **A closed channel REJECTS.** The native socket confirms nothing about a write, so this
549
+ * transport answers from its own state: a `send` after `close()`, or on a socket already
550
+ * reporting `CLOSING` / `CLOSED`, REJECTS with `WebSocket transport is not connected` rather
551
+ * than resolving on a frame nobody wrote. Only the closed state rejects — a pre-open `send`
552
+ * still queues.
546
553
  * - **Inbound (`message`).** Each decoded text frame is `JSON.parse`d (guarded) and
547
554
  * narrowed with `parseJSONRPCMessage` — a well-formed {@link JSONRPCMessage}
548
555
  * re-emits on this transport's `message` event; a non-text (binary) frame or a
@@ -553,8 +560,10 @@ export declare interface ServeMCPScopeInterface {
553
560
  * SAME `close` exactly once total — `close()` first flips the guard, so the native event
554
561
  * never double-emits, and the released socket reports its own close to nobody. Closing before
555
562
  * the socket opens resolves the pending `start()` rather than leaving it pending, matching the
556
- * Node face. A `send` issued after `close()` is silently dropped (not queued), so a closed
557
- * transport delivers nothing until a `start()` opens a new connection.
563
+ * Node face. A `send` issued after `close()` REJECTS (it is never queued), and the
564
+ * pre-open queue is DISCARDED by `close()` and by the native `close` event alike — so a
565
+ * closed transport delivers nothing until a `start()` opens a new connection, and nothing
566
+ * the caller handed the abandoned connection rides that one.
558
567
  * - **Observable.** Owns the `emitter` ({@link MCPClientTransportEventMap}); every
559
568
  * emit the emitter isolates a listener throw; `error` is a DOMAIN event (a
560
569
  * transport-level fault).
@@ -1,5 +1,5 @@
1
- import { bindServer, createMCPServer, inferRequestVersion, isJSONRPCResponse, isMCPVersion, isModernRequest, parseJSONRPCMessage } from "../core/index.js";
2
- import { isRecord, isString } from "@orkestrel/contract";
1
+ import { bindServer, buildHeaderParameters, buildHeaderProjection, createMCPServer, encodeSentinel, inferRequestVersion, isJSONRPCResponse, isMCPVersion, isModernRequest, parseJSONRPCMessage } from "../core/index.js";
2
+ import { isArray, isRecord, isString } from "@orkestrel/contract";
3
3
  import { createSSEParser } from "@orkestrel/sse";
4
4
  import { Emitter } from "@orkestrel/emitter";
5
5
  //#region src/browser/constants.ts
@@ -74,7 +74,8 @@ var MCP_WEBSOCKET_SUBPROTOCOL = "mcp";
74
74
  * initialize result's `protocolVersion` is likewise captured, but only
75
75
  * when it is a SUPPORTED value, and echoed as `mcp-protocol-version` alone on
76
76
  * subsequent legacy requests. Modern requests instead derive protocol and method
77
- * headers from the message, plus the name header only for `tools/call`.
77
+ * headers from the message, plus the name header only for `tools/call` — carried in the
78
+ * protocol's Base64 sentinel form whenever the tool name cannot ride as plain ASCII.
78
79
  * Before initialize returns, neither captured legacy header is sent.
79
80
  * `close()` clears the captured protocol so a reconnect's `initialize`
80
81
  * POST is headerless; the captured `session` persists across `close()`.
@@ -103,8 +104,11 @@ var HTTPClientTransport = class {
103
104
  #fetch;
104
105
  #timeout;
105
106
  #pending = /* @__PURE__ */ new Set();
107
+ #parameters = /* @__PURE__ */ new Map();
108
+ #stamps = /* @__PURE__ */ new WeakMap();
106
109
  #session = void 0;
107
110
  #protocol = void 0;
111
+ #generation = 0;
108
112
  #closed = false;
109
113
  constructor(options) {
110
114
  this.#emitter = new Emitter();
@@ -126,6 +130,7 @@ var HTTPClientTransport = class {
126
130
  this.#closed = false;
127
131
  }
128
132
  async send(message) {
133
+ this.#stamp(message);
129
134
  const request = new AbortController();
130
135
  this.#pending.add(request);
131
136
  try {
@@ -134,6 +139,11 @@ var HTTPClientTransport = class {
134
139
  this.#pending.delete(request);
135
140
  }
136
141
  }
142
+ #stamp(message) {
143
+ if (!isModernRequest(message) || message.method !== "tools/list") return;
144
+ if (message.params?.["cursor"] === void 0) this.#generation += 1;
145
+ this.#stamps.set(message, this.#generation);
146
+ }
137
147
  async #exchange(message, signal) {
138
148
  let response;
139
149
  try {
@@ -155,7 +165,7 @@ var HTTPClientTransport = class {
155
165
  }
156
166
  const session = response.headers.get(MCP_SESSION_HEADER);
157
167
  if (session !== null) this.#session = session;
158
- await this.#deliver(response);
168
+ await this.#deliver(response, message);
159
169
  }
160
170
  async close() {
161
171
  if (this.#closed) return;
@@ -172,30 +182,63 @@ var HTTPClientTransport = class {
172
182
  return {
173
183
  ...version === void 0 ? {} : { [MCP_PROTOCOL_VERSION_HEADER]: version },
174
184
  [MCP_METHOD_HEADER]: message.method,
175
- ...message.method === "tools/call" && isString(name) ? { [MCP_NAME_HEADER]: name } : {}
185
+ ...message.method === "tools/call" && isString(name) ? {
186
+ [MCP_NAME_HEADER]: encodeSentinel(name),
187
+ ...buildHeaderProjection(this.#parameters.get(name) ?? [], message.params?.["arguments"])
188
+ } : {}
176
189
  };
177
190
  }
178
191
  return this.#protocol === void 0 ? {} : { [MCP_PROTOCOL_VERSION_HEADER]: this.#protocol };
179
192
  }
180
- async #deliver(response) {
193
+ async #deliver(response, sent) {
181
194
  if (response.status === 202) return;
182
195
  const type = response.headers.get("content-type") ?? "";
183
196
  try {
184
197
  if (type.includes("text/event-stream")) {
185
- for (const message of await readEventStream(response)) this.#capture(message);
198
+ for (const message of await readEventStream(response)) this.#capture(message, sent);
186
199
  return;
187
200
  }
188
201
  if (type.includes("application/json")) {
189
202
  const message = parseJSONRPCMessage(await response.json());
190
- if (message !== void 0) this.#capture(message);
203
+ if (message !== void 0) this.#capture(message, sent);
191
204
  }
192
205
  } catch (error) {
193
206
  this.#emitter.emit("error", error);
194
207
  }
195
208
  }
196
- #capture(message) {
209
+ #capture(message, sent) {
197
210
  if (isJSONRPCResponse(message) && isRecord(message.result) && isMCPVersion(message.result["protocolVersion"])) this.#protocol = message.result["protocolVersion"];
198
- this.#emitter.emit("message", message);
211
+ this.#emitter.emit("message", this.#select(message, sent));
212
+ }
213
+ #select(message, sent) {
214
+ if (!isModernRequest(sent) || sent.method !== "tools/list") return message;
215
+ if (!isJSONRPCResponse(message) || message.error !== void 0) return message;
216
+ const result = message.result;
217
+ const listed = isRecord(result) ? result["tools"] : void 0;
218
+ if (!isRecord(result) || !isArray(listed)) return message;
219
+ const current = this.#stamps.get(sent) === this.#generation;
220
+ if (current && sent.params?.["cursor"] === void 0) this.#parameters.clear();
221
+ const kept = [];
222
+ for (const tool of listed) {
223
+ if (!isRecord(tool) || !isString(tool["name"])) {
224
+ kept.push(tool);
225
+ continue;
226
+ }
227
+ const parameters = buildHeaderParameters(tool["inputSchema"]);
228
+ if (parameters === void 0) {
229
+ this.#emitter.emit("error", /* @__PURE__ */ new Error(`MCP tool '${tool["name"]}' is excluded from tools/list: its inputSchema carries an invalid x-mcp-header annotation`));
230
+ continue;
231
+ }
232
+ if (current) this.#parameters.set(tool["name"], parameters);
233
+ kept.push(tool);
234
+ }
235
+ return {
236
+ ...message,
237
+ result: {
238
+ ...result,
239
+ tools: kept
240
+ }
241
+ };
199
242
  }
200
243
  };
201
244
  //#endregion
@@ -318,7 +361,13 @@ var MessagePortTransport = class {
318
361
  * - **Queued sends.** `send` writes each message as one text frame immediately once
319
362
  * the socket is `OPEN`; a `send` issued before `'open'` fires (or before `start()`
320
363
  * is even called) is QUEUED and flushed, IN ORDER, the moment the socket opens —
321
- * so a caller need not await `start()` before calling `send`.
364
+ * so a caller need not await `start()` before calling `send`. A queue rides ONE
365
+ * connection: a close DISCARDS whatever is still in it.
366
+ * - **A closed channel REJECTS.** The native socket confirms nothing about a write, so this
367
+ * transport answers from its own state: a `send` after `close()`, or on a socket already
368
+ * reporting `CLOSING` / `CLOSED`, REJECTS with `WebSocket transport is not connected` rather
369
+ * than resolving on a frame nobody wrote. Only the closed state rejects — a pre-open `send`
370
+ * still queues.
322
371
  * - **Inbound (`message`).** Each decoded text frame is `JSON.parse`d (guarded) and
323
372
  * narrowed with `parseJSONRPCMessage` — a well-formed {@link JSONRPCMessage}
324
373
  * re-emits on this transport's `message` event; a non-text (binary) frame or a
@@ -329,8 +378,10 @@ var MessagePortTransport = class {
329
378
  * SAME `close` exactly once total — `close()` first flips the guard, so the native event
330
379
  * never double-emits, and the released socket reports its own close to nobody. Closing before
331
380
  * the socket opens resolves the pending `start()` rather than leaving it pending, matching the
332
- * Node face. A `send` issued after `close()` is silently dropped (not queued), so a closed
333
- * transport delivers nothing until a `start()` opens a new connection.
381
+ * Node face. A `send` issued after `close()` REJECTS (it is never queued), and the
382
+ * pre-open queue is DISCARDED by `close()` and by the native `close` event alike — so a
383
+ * closed transport delivers nothing until a `start()` opens a new connection, and nothing
384
+ * the caller handed the abandoned connection rides that one.
334
385
  * - **Observable.** Owns the `emitter` ({@link MCPClientTransportEventMap}); every
335
386
  * emit the emitter isolates a listener throw; `error` is a DOMAIN event (a
336
387
  * transport-level fault).
@@ -385,15 +436,16 @@ var WebSocketClientTransport = class {
385
436
  });
386
437
  }
387
438
  async send(message) {
388
- if (this.#closed) return;
389
- const text = JSON.stringify(message);
390
439
  const socket = this.#socket;
440
+ if (this.#closed || socket?.readyState === WebSocket.CLOSING || socket?.readyState === WebSocket.CLOSED) throw new Error("WebSocket transport is not connected");
441
+ const text = JSON.stringify(message);
391
442
  if (socket !== void 0 && socket.readyState === WebSocket.OPEN) socket.send(text);
392
443
  else this.#queue.push(text);
393
444
  }
394
445
  async close() {
395
446
  if (this.#closed) return;
396
447
  this.#closed = true;
448
+ this.#queue = [];
397
449
  const socket = this.#socket;
398
450
  const resolve = this.#resolve;
399
451
  this.#releaseHandshake();
@@ -466,6 +518,7 @@ var WebSocketClientTransport = class {
466
518
  #onClose() {
467
519
  if (this.#closed) return;
468
520
  this.#closed = true;
521
+ this.#queue = [];
469
522
  this.#release();
470
523
  this.#socket = void 0;
471
524
  this.#emitter.emit("close");
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../../src/browser/constants.ts","../../../src/browser/transports/HTTPClientTransport.ts","../../../src/browser/transports/MessagePortTransport.ts","../../../src/browser/transports/WebSocketClientTransport.ts","../../../src/browser/factories.ts","../../../src/browser/helpers.ts"],"sourcesContent":["// The MCP browser-transport constants — the wire-level\n// header names the browser-face HTTP client transport echoes, matching the Node\n// face's session, protocol-version, method, and name headers byte-for-byte. The\n// browser face imports nothing from `src/server` (peer environment faces share no\n// import), so the literals are declared once here too — the SAME strings, not\n// shared symbols.\n\n/**\n * The Streamable-HTTP transport header that carries the MCP session id. The browser\n * face's {@link import('./transports/HTTPClientTransport.js').HTTPClientTransport}\n * ECHOES this header exactly like the Node face's `HTTPClientTransport`\n * (`src/server`), so the same client interoperates with an `MCPSession`-based\n * server unchanged.\n */\nexport const MCP_SESSION_HEADER = 'mcp-session-id'\n\n/**\n * The Streamable-HTTP transport header carrying the MCP protocol version. Modern\n * requests derive it from their own `_meta`; legacy requests echo the negotiated\n * initialize result on each subsequent request.\n */\nexport const MCP_PROTOCOL_VERSION_HEADER = 'mcp-protocol-version'\n\n/**\n * The modern Streamable-HTTP request header carrying the JSON-RPC method. It is\n * emitted on every modern request and never on a legacy request.\n */\nexport const MCP_METHOD_HEADER = 'mcp-method'\n\n/**\n * The modern Streamable-HTTP request header carrying a named target. The browser\n * HTTP client emits it only for `tools/call`, from that request's `params.name`.\n */\nexport const MCP_NAME_HEADER = 'mcp-name'\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, so the same value is declared on each face. The browser face's\n// `WebSocketClientTransport` defaults to this value when `protocols` is omitted, and\n// `createWebSocketServer` selects it from the client's offer.\n\n/**\n * The WebSocket subprotocol `createWebSocketClientTransport` requests by default —\n * `'mcp'`, which `createWebSocketServer` selects when the client offers it. Per RFC 6455\n * §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 {\n\tMCPClientTransportEventMap,\n\tMCPClientTransportInterface,\n\tJSONRPCMessage,\n} from '@src/core'\nimport type { EmitterInterface } from '@orkestrel/emitter'\nimport type { HTTPClientTransportOptions } from '../types.js'\nimport {\n\tinferRequestVersion,\n\tisJSONRPCResponse,\n\tisMCPVersion,\n\tisModernRequest,\n\tparseJSONRPCMessage,\n} from '@src/core'\nimport { isRecord, isString } from '@orkestrel/contract'\nimport { Emitter } from '@orkestrel/emitter'\nimport {\n\tMCP_METHOD_HEADER,\n\tMCP_NAME_HEADER,\n\tMCP_PROTOCOL_VERSION_HEADER,\n\tMCP_SESSION_HEADER,\n} 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 MCPClientTransportInterface} that drives a REMOTE Streamable-HTTP MCP server\n * over the native `fetch`, the browser sibling of the Node face's\n * {@link import('@orkestrel/mcp/server').HTTPClientTransport}, honoring the SAME\n * `mcp-session-id` semantics so it interoperates with an `MCPSession`-based server\n * unchanged.\n *\n * @remarks\n * - **Request/response over `fetch`.** `send(message)` POSTs the JSON-serialized\n * message to `options.url` with `content-type: application/json` and an\n * `Accept` of BOTH `application/json` and `text/event-stream` (so the server may\n * answer with either framing) — plus any `options.headers` (for example, an\n * `Authorization` bearer). It then decodes the reply and emits each decoded\n * {@link JSONRPCMessage} on the `message` event the\n * {@link import('@orkestrel/mcp').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 with the\n * `@orkestrel/sse` {@link import('@orkestrel/sse').SSEParserInterface} (the browser\n * face's own `readEventStream`) — the inverse of the server's `openStream` seam, so\n * the wire round-trips. A `202` Accepted (a notification) carries no body and emits\n * nothing.\n * - **Session and protocol headers.** `start()` is a no-op (a\n * request/response transport opens no long-lived connection). The\n * `mcp-session-id` response header, when a STATEFUL server sends one (on\n * `initialize`), is captured into `session` and then ECHOED as the\n * `mcp-session-id` request header on every SUBSEQUENT request — so an\n * `MCPClient` passes a stateful server's session validation. The\n * initialize result's `protocolVersion` is likewise captured, but only\n * when it is a SUPPORTED value, and echoed as `mcp-protocol-version` alone on\n * subsequent legacy requests. Modern requests instead derive protocol and method\n * headers from the message, plus the name header only for `tools/call`.\n * Before initialize returns, neither captured legacy header is sent.\n * `close()` clears the captured protocol so a reconnect's `initialize`\n * POST is headerless; the captured `session` persists across `close()`.\n * - **`close()` releases what is in flight.** Every `fetch` this transport still has open is\n * ABORTED, which cancels the response body a `send` is reading — an SSE reply the server\n * never ends would otherwise outlive the transport, with nothing left able to reach it. The\n * aborted read surfaces on `error` and the `send` reporting it resolves. `close()` is\n * idempotent (one `close` event per connected lifetime), and `start()` opens the next one.\n * - **Total at the boundary.** 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.** Owns the `emitter` ({@link MCPClientTransportEventMap}); 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 MCPClientTransportInterface {\n\treadonly #emitter: Emitter<MCPClientTransportEventMap>\n\treadonly #url: string\n\treadonly #headers: Readonly<Record<string, string>>\n\treadonly #fetch: typeof fetch\n\treadonly #timeout: number | undefined\n\t// The requests on the wire, one controller each. `close` is the only thing that can\n\t// reach them: a `send` parked on a reply that never ends holds both the request and its\n\t// response reader, and no other seam this transport exposes leads back to either.\n\treadonly #pending = new Set<AbortController>()\n\t#session: string | undefined = undefined\n\t#protocol: string | undefined = undefined\n\t#closed = false\n\n\tconstructor(options: HTTPClientTransportOptions) {\n\t\tthis.#emitter = new Emitter<MCPClientTransportEventMap>()\n\t\tthis.#url = options.url\n\t\tthis.#headers = options.headers ?? {}\n\t\tthis.#fetch = options.fetch ?? globalThis.fetch.bind(globalThis)\n\t\tthis.#timeout = options.timeout\n\t}\n\n\tget emitter(): EmitterInterface<MCPClientTransportEventMap> {\n\t\treturn this.#emitter\n\t}\n\n\tget session(): string | undefined {\n\t\treturn this.#session\n\t}\n\n\tget duplex(): boolean {\n\t\t// Streamable HTTP carries no client-initiated notification: the dated revision defines\n\t\t// none over it, and closing the response stream is the cancellation signal instead.\n\t\treturn false\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. There is nothing to arm; opening the next connected lifetime is all\n\t\t// this does, so a transport an earlier `close` ended sends again from here.\n\t\tthis.#closed = false\n\t}\n\n\tasync send(message: JSONRPCMessage): Promise<void> {\n\t\tconst request = new AbortController()\n\t\tthis.#pending.add(request)\n\t\ttry {\n\t\t\tawait this.#exchange(message, request.signal)\n\t\t} finally {\n\t\t\tthis.#pending.delete(request)\n\t\t}\n\t}\n\n\t// One request/response exchange under `signal`: `close` aborts it, and a `timeout` option\n\t// composes with it so whichever fires first ends the same fetch and the same body read.\n\tasync #exchange(message: JSONRPCMessage, signal: AbortSignal): 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.#buildHeaders(message),\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\tsignal:\n\t\t\t\t\tthis.#timeout === undefined\n\t\t\t\t\t\t? signal\n\t\t\t\t\t\t: AbortSignal.any([signal, AbortSignal.timeout(this.#timeout)]),\n\t\t\t})\n\t\t} catch (error) {\n\t\t\t// A network-level failure (connection refused, DNS) — surface it for observation;\n\t\t\t// the client's per-request deadline still rejects the pending request.\n\t\t\tthis.#emitter.emit('error', error)\n\t\t\treturn\n\t\t}\n\t\t// Capture a server-assigned session id (a stateless server sends none) so it is echoed\n\t\t// on subsequent requests; a missing header leaves `session` unchanged.\n\t\tconst session = response.headers.get(MCP_SESSION_HEADER)\n\t\tif (session !== null) this.#session = session\n\t\tawait this.#deliver(response)\n\t}\n\n\t// Abort every request still on the wire, then clear the captured protocol before emitting\n\t// `close`, so a reconnect's `initialize` POST carries no `mcp-protocol-version` header (the\n\t// captured `session` is untouched). Idempotent: a second `close` on a transport this one\n\t// already ended releases nothing and emits nothing.\n\tasync close(): Promise<void> {\n\t\tif (this.#closed) return\n\t\tthis.#closed = true\n\t\tfor (const request of this.#pending) request.abort()\n\t\tthis.#pending.clear()\n\t\tthis.#protocol = undefined\n\t\tthis.#emitter.emit('close')\n\t}\n\n\t// Modern requests announce their own protocol version, so the header is projected from the\n\t// message through the SHARED `inferRequestVersion` — the same read the server's own\n\t// expectation performs, and the same read the Node face performs. Legacy requests carry\n\t// the version captured from the `initialize` handshake instead.\n\t#buildHeaders(message: JSONRPCMessage): Readonly<Record<string, string>> {\n\t\tif (isModernRequest(message)) {\n\t\t\tconst version = inferRequestVersion(message)\n\t\t\tconst name = message.params?.['name']\n\t\t\treturn {\n\t\t\t\t...(version === undefined ? {} : { [MCP_PROTOCOL_VERSION_HEADER]: version }),\n\t\t\t\t[MCP_METHOD_HEADER]: message.method,\n\t\t\t\t...(message.method === 'tools/call' && isString(name) ? { [MCP_NAME_HEADER]: name } : {}),\n\t\t\t}\n\t\t}\n\t\treturn this.#protocol === undefined ? {} : { [MCP_PROTOCOL_VERSION_HEADER]: this.#protocol }\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 with the browser-face `readEventStream` (one or more `data:` events). A\n\t// decode failure surfaces on `error` rather than escaping.\n\tasync #deliver(response: Response): Promise<void> {\n\t\tif (response.status === 202) return\n\t\tconst type = response.headers.get('content-type') ?? ''\n\t\ttry {\n\t\t\tif (type.includes('text/event-stream')) {\n\t\t\t\tfor (const message of await readEventStream(response)) this.#capture(message)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (type.includes('application/json')) {\n\t\t\t\tconst message = parseJSONRPCMessage(await response.json())\n\t\t\t\tif (message !== undefined) this.#capture(message)\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tthis.#emitter.emit('error', error)\n\t\t}\n\t}\n\n\t// Capture the negotiated SUPPORTED protocol from the initialize result before emitting\n\t// the message, so the next request carries its required protocol-version header; any\n\t// other value (missing or unsupported) is ignored and leaves `#protocol` unchanged.\n\t#capture(message: JSONRPCMessage): void {\n\t\tif (\n\t\t\tisJSONRPCResponse(message) &&\n\t\t\tisRecord(message.result) &&\n\t\t\tisMCPVersion(message.result['protocolVersion'])\n\t\t) {\n\t\t\tthis.#protocol = message.result['protocolVersion']\n\t\t}\n\t\tthis.#emitter.emit('message', message)\n\t}\n}\n","import type { 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 `@orkestrel/mcp`'s `MCPClientTransportInterface`), a `MessagePort` is a\n * plain duplex channel — the SAME class implements `@orkestrel/mcp`'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 with `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 —\n * because `MCPTransportInterface` carries no `error` channel for this port to\n * surface a non-string frame on (unlike `MCPClientTransportInterface`'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, `@orkestrel/mcp`'s `MCPTransportInterface`\n * doc).** `listen`/`closed` each hold the one active 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\treadonly #message = (event: MessageEvent): void => this.#receive(event.data)\n\treadonly #malformed = (): void => {}\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', this.#message)\n\t\tthis.#port.addEventListener('messageerror', this.#malformed)\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\tconst onClosed = this.#onClosed\n\t\tthis.#onMessage = undefined\n\t\tthis.#onClosed = undefined\n\t\tthis.#port.removeEventListener('message', this.#message)\n\t\tthis.#port.removeEventListener('messageerror', this.#malformed)\n\t\tthis.#port.close()\n\t\tonClosed?.()\n\t}\n\n\t// Decode one inbound `postMessage` payload: a non-string `data` is dropped, never\n\t// forwarded (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 {\n\tMCPClientTransportEventMap,\n\tMCPClientTransportInterface,\n\tJSONRPCMessage,\n} 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 MCPClientTransportInterface} that drives a REMOTE MCP server over the native\n * `WebSocket` global, the browser sibling of the Node face's\n * {@link import('@orkestrel/mcp/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 (never\n * throws on adversarial wire input).\n * - **`close()`** unsubscribes from the underlying socket, closes it, and fires `close`\n * (idempotent); the socket's native `close` event (a server-initiated close) fires the\n * SAME `close` exactly once total — `close()` first flips the guard, so the native event\n * never double-emits, and the released socket reports its own close to nobody. Closing before\n * the socket opens resolves the pending `start()` rather than leaving it pending, matching the\n * Node face. A `send` issued after `close()` is silently dropped (not queued), so a closed\n * transport delivers nothing until a `start()` opens a new connection.\n * - **Observable.** Owns the `emitter` ({@link MCPClientTransportEventMap}); 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 MCPClientTransportInterface {\n\treadonly #emitter: Emitter<MCPClientTransportEventMap>\n\treadonly #url: string\n\treadonly #protocols: string | string[] | undefined\n\t// Bound once, as fields, so `close` can remove exactly the listeners `#bind` installed: an\n\t// inline arrow is a new function on every call and can never be removed by reference.\n\treadonly #frame = (event: MessageEvent<unknown>): void => this.#receive(event.data)\n\treadonly #ending = (): void => this.#onClose()\n\treadonly #failure = (event: Event): void => this.#emitter.emit('error', event)\n\treadonly #opening = (): void => this.#onOpen()\n\treadonly #rejection = (): void => this.#onHandshakeError()\n\t#socket: WebSocket | undefined = undefined\n\t#handshake: WebSocket | undefined = undefined\n\t#resolve: (() => void) | undefined = undefined\n\t#reject: ((error: Error) => void) | undefined = undefined\n\t#queue: string[] = []\n\t#closed = false\n\n\tconstructor(options: WebSocketClientTransportOptions) {\n\t\tthis.#emitter = new Emitter<MCPClientTransportEventMap>()\n\t\tthis.#url = options.url\n\t\tconst protocols = options.protocols\n\t\t// Default to MCP_WEBSOCKET_SUBPROTOCOL when `protocols` is omitted; the server selects it\n\t\t// from this offer. 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<MCPClientTransportEventMap> {\n\t\treturn this.#emitter\n\t}\n\n\tget session(): string | undefined {\n\t\treturn undefined\n\t}\n\n\tget duplex(): boolean {\n\t\t// A socket is bidirectional for its whole life: either side writes a frame whenever it\n\t\t// has one, with no request to attach it to.\n\t\treturn true\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\tthis.#handshake = socket\n\t\t\tthis.#resolve = resolve\n\t\t\tthis.#reject = reject\n\t\t\tsocket.addEventListener('open', this.#opening)\n\t\t\tsocket.addEventListener('error', this.#rejection)\n\t\t})\n\t}\n\n\tasync send(message: JSONRPCMessage): Promise<void> {\n\t\t// After close(), silently drop — never queue (a closed transport is not reusable;\n\t\t// queued messages would resurrect on a later start() which is not a supported pattern).\n\t\tif (this.#closed) return\n\t\tconst text = JSON.stringify(message)\n\t\tconst socket = this.#socket\n\t\tif (socket !== undefined && socket.readyState === WebSocket.OPEN) socket.send(text)\n\t\telse this.#queue.push(text)\n\t}\n\n\tasync close(): Promise<void> {\n\t\tif (this.#closed) return\n\t\tthis.#closed = true\n\t\tconst socket = this.#socket\n\t\tconst resolve = this.#resolve\n\t\tthis.#releaseHandshake()\n\t\tthis.#release()\n\t\tthis.#socket = undefined\n\t\tif (socket !== undefined) socket.close()\n\t\tthis.#emitter.emit('close')\n\t\tresolve?.()\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', this.#frame)\n\t\tsocket.addEventListener('close', this.#ending)\n\t\tsocket.addEventListener('error', this.#failure)\n\t}\n\n\t// Unsubscribe from the socket this transport holds. A closing socket goes on\n\t// firing its own events, so a bridge left installed on one this transport has released\n\t// would report a connection it no longer owns.\n\t#release(): void {\n\t\tconst socket = this.#socket\n\t\tif (socket === undefined) return\n\t\tsocket.removeEventListener('message', this.#frame)\n\t\tsocket.removeEventListener('close', this.#ending)\n\t\tsocket.removeEventListener('error', this.#failure)\n\t}\n\n\t#releaseHandshake(): void {\n\t\tconst socket = this.#handshake\n\t\tif (socket === undefined) return\n\t\tsocket.removeEventListener('open', this.#opening)\n\t\tsocket.removeEventListener('error', this.#rejection)\n\t\tthis.#handshake = undefined\n\t\tthis.#resolve = undefined\n\t\tthis.#reject = undefined\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#onOpen(): void {\n\t\tconst socket = this.#handshake\n\t\tconst resolve = this.#resolve\n\t\tif (socket === undefined || resolve === undefined) return\n\t\tthis.#releaseHandshake()\n\t\tthis.#flush(socket)\n\t\tresolve()\n\t}\n\n\t#onHandshakeError(): void {\n\t\tconst socket = this.#handshake\n\t\tconst reject = this.#reject\n\t\tif (socket === undefined || reject === undefined || socket.readyState === WebSocket.OPEN) return\n\t\tthis.#releaseHandshake()\n\t\tthis.#release()\n\t\tthis.#socket = undefined\n\t\treject(new Error('WebSocket connection failed'))\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\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. Only the socket this transport still\n\t// holds can reach here: a superseded one was unsubscribed when it was released, so its own\n\t// later close cannot end the connection that replaced it.\n\t#onClose(): void {\n\t\tif (this.#closed) return\n\t\tthis.#closed = true\n\t\tthis.#release()\n\t\tthis.#socket = undefined\n\t\tthis.#emitter.emit('close')\n\t}\n}\n","import type { MCPClientTransportInterface, 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 * Creates the browser-face WebSocket CLIENT transport for an\n * {@link import('@orkestrel/mcp').MCPClientInterface} — a {@link MCPClientTransportInterface}\n * that drives a REMOTE MCP server over the native `WebSocket` global, the browser\n * sibling of the Node face's `createWebSocketClientTransport` (`@orkestrel/mcp/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 MCPClientTransportInterface} 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): MCPClientTransportInterface {\n\treturn new WebSocketClientTransport(options)\n}\n\n/**\n * Creates the browser-face HTTP CLIENT transport for an\n * {@link import('@orkestrel/mcp').MCPClientInterface} — a {@link MCPClientTransportInterface}\n * that drives a REMOTE Streamable-HTTP MCP server over the native `fetch`, the\n * browser sibling of the Node face's `createHTTPClientTransport` (`@orkestrel/mcp/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 with `@orkestrel/sse`), and the reply is surfaced on the transport's\n * `message` event for the client's id correlation. Add `options.headers` (for example, an\n * `Authorization` bearer) to reach a guarded server. `start` / `close` hold no\n * connection; against a STATEFUL server it captures the `mcp-session-id` from\n * `initialize` and echoes it on later requests. It also captures the initialize\n * result's `protocolVersion` and sends `mcp-protocol-version` alone on subsequent\n * legacy requests. Modern requests instead derive `mcp-protocol-version` and\n * `mcp-method` from the message, plus `mcp-name` only for `tools/call`, so the\n * same `MCPClient` passes either era's protocol gates without caller wiring.\n *\n * @param options - `url` (the remote endpoint; REQUIRED), optional `headers` merged\n * onto every request, optional `fetch` (default `globalThis.fetch`), and optional\n * `timeout` (ms, applied with `AbortSignal.timeout`); see\n * {@link HTTPClientTransportOptions}\n * @returns A working {@link MCPClientTransportInterface} 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): MCPClientTransportInterface {\n\treturn new HTTPClientTransport(options)\n}\n\n/**\n * Creates the browser-face `MessagePort` transport — a\n * {@link import('@orkestrel/mcp').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('@orkestrel/mcp').bindServer} or\n * {@link import('@orkestrel/mcp').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('@orkestrel/mcp').MCPTransportInterface} over the port\n *\n * @example\n * ```ts\n * import { bindServer, createMCPLegacy, createMCPServer } from '@orkestrel/mcp'\n * import { createMessagePortTransport } from '@orkestrel/mcp/browser'\n *\n * const { port1, port2 } = new MessageChannel()\n * const mcp = createMCPServer({ identity: { name: 's', version: '1.0.0' }, tools })\n * bindServer(createMCPLegacy(mcp), createMessagePortTransport({ port: port1 })) // answers `initialize` too; pass `mcp` alone for modern-only\n * ```\n */\nexport function createMessagePortTransport(\n\toptions: MessagePortTransportOptions,\n): MCPTransportInterface {\n\treturn new MessagePortTransport(options)\n}\n\n/**\n * Adapts a hostable {@link ServeMCPScopeInterface} (`self` in a dedicated Web Worker,\n * or any structurally matching double) into a {@link ScopeTransportInterface} — the\n * implicit, portless message channel `serveMCPScope` binds for the\n * dedicated-worker shape.\n *\n * @remarks\n * `send` writes each outbound string through `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 through `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 { JSONRPCMessage, MCPServerInterface } from '@src/core'\nimport type { SSEParserInterface } from '@orkestrel/sse'\nimport type { ServeMCPOptions, ScopeTransportInterface, ServeMCPScopeInterface } from './types.js'\nimport { bindServer, createMCPServer, parseJSONRPCMessage } from '@src/core'\nimport { isString } from '@orkestrel/contract'\nimport { createSSEParser } from '@orkestrel/sse'\nimport { DEFAULT_MCP_SERVER_NAME, DEFAULT_MCP_SERVER_VERSION } from './constants.js'\nimport { createScopeTransport } from './factories.js'\nimport { MessagePortTransport } from './transports/MessagePortTransport.js'\n\n// The MCP browser-transport helpers — module-scope names, so they carry 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 share\n// 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`: a malformed / non-message SSE\n// `data:` event is dropped, never thrown.\n//\n// `serveMCPScope` / `serveMCP` are the worker bootstrap. They are reusable exported\n// infrastructure that BOOTS and BINDS — the browser sibling of `src/core`'s\n// `bindServer` / `bindClient` — and each returns an idempotent disposer rather than an\n// entity, so they belong here rather than in `factories.ts` (`.claude/rules/architecture.md`\n// kind purity: placement follows what a function is, and every exported `factories.ts`\n// function is named `create*`). The value factory they compose, `createScopeTransport`,\n// stays in `factories.ts`.\n//\n// `createScopeMessageListener` is the bootstrap's per-event dispatcher, extracted\n// (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 * Decodes 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: 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 * Decodes 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} through {@link decodeEvent} (so a non-message / non-JSON `data:`\n * event is DROPPED, never thrown — total). 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 * Builds `serveMCPScope`'s `message`-event listener — the unified\n * dispatcher that routes EVERY inbound event on a hostable scope, portless or\n * port-bearing, to the right binding.\n *\n * @remarks\n * Port-bearing events (`event.ports.length > 0`) are gated by `options.accept` FIRST\n * — when the gate returns `false` the event is dropped entirely (no binding, no reply).\n * Accepted events spawn a fresh `MessagePortTransport` over `event.ports[0]`,\n * `bindServer` `server` onto it, and record a teardown (`unbind` then `transport.close()`)\n * into `teardowns` KEYED BY THAT PORT. A port already present is IGNORED — repeated delivery\n * of the same `MessagePort` would create duplicate bindings over one port (→ duplicated\n * replies), so a repeat is silently dropped.\n *\n * The key is what makes `teardowns` the ONLY place an accepted port is remembered. A separate\n * seen-port set would be a second collection over the same lifetime, and the caller's disposer\n * would have to remember to empty both — so a long-lived scope such as a Service Worker would\n * retain every port it ever accepted, closed and unbound ones included. Membership answers\n * \"already bound?\" and `clear()` drops the binding and the dedup together.\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, 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 map `serveMCPScope`'s dispose drains and clears, keyed by the accepted port; 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 Map<MessagePort, () => 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: Map<MessagePort, () => void>,\n\toptions: ServeMCPOptions,\n): (event: MessageEvent) => void {\n\treturn (event: MessageEvent): void => {\n\t\tconst ports = event.ports\n\t\tif (ports.length > 0) {\n\t\t\t// Gate: consult accept (origin/identity check) before binding.\n\t\t\tif (options.accept !== undefined && !options.accept(event)) return\n\t\t\tconst port = ports[0]\n\t\t\tif (port === undefined) return\n\t\t\t// Deduplicate off the teardown map itself: repeated delivery of the same port would\n\t\t\t// create duplicate bindings, and a second collection recording the same fact is one\n\t\t\t// the disposer can forget to empty.\n\t\t\tif (teardowns.has(port)) return\n\t\t\tconst transport = new MessagePortTransport({ port })\n\t\t\tconst unbind = bindServer(server, transport)\n\t\t\tteardowns.set(port, () => {\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\n/**\n * Boots an `MCPServer` inside a hostable worker scope and wires its message events to it.\n *\n * @remarks\n * Port-bearing events are gated by `options.accept`, deduplicated by port, and receive\n * their own `MessagePortTransport` binding. Portless string events use the scope's\n * implicit channel. The returned disposer removes the listener, unbinds the implicit\n * channel, closes every accepted port binding, and drops the ports themselves — the\n * bindings are held in one map keyed by port, so nothing survives the clear. The served\n * endpoint is modern-only: it answers a legacy `initialize` with `-32601`. A dual-era\n * worker composes `bindServer(createMCPLegacy(mcp), …)` instead of this function.\n *\n * @param scope - The hostable worker scope to wire\n * @param options - The tools, optional identity, and optional port-event gate\n * @returns An idempotent disposer for every binding owned by this call\n */\nexport function serveMCPScope(scope: ServeMCPScopeInterface, options: ServeMCPOptions): () => void {\n\tconst server = createMCPServer({\n\t\ttools: options.tools,\n\t\tidentity: {\n\t\t\tname: options.name ?? DEFAULT_MCP_SERVER_NAME,\n\t\t\tversion: options.version ?? DEFAULT_MCP_SERVER_VERSION,\n\t\t},\n\t})\n\tconst scopeTransport = createScopeTransport(scope)\n\tconst unbindScope = bindServer(server, scopeTransport)\n\tconst teardowns = new Map<MessagePort, () => 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.values()) teardown()\n\t\t// One clear releases the bindings AND the ports they were keyed by, so a scope that\n\t\t// outlives its disposer — a Service Worker — retains neither.\n\t\tteardowns.clear()\n\t}\n}\n\n/**\n * Boots an `MCPServer` inside the current hostable worker scope.\n *\n * @remarks\n * The served endpoint is modern-only: it answers a legacy `initialize` with `-32601`. A\n * dual-era worker composes `bindServer(createMCPLegacy(mcp), …)` instead of this function.\n *\n * @param options - The tools, optional identity, and optional port-event gate\n * @returns The disposer returned by {@link serveMCPScope}\n */\nexport function serveMCP(options: ServeMCPOptions): () => void {\n\treturn serveMCPScope(globalThis, options)\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../src/browser/constants.ts","../../../src/browser/transports/HTTPClientTransport.ts","../../../src/browser/transports/MessagePortTransport.ts","../../../src/browser/transports/WebSocketClientTransport.ts","../../../src/browser/factories.ts","../../../src/browser/helpers.ts"],"sourcesContent":["// The MCP browser-transport constants — the wire-level\n// header names the browser-face HTTP client transport echoes, matching the Node\n// face's session, protocol-version, method, and name headers byte-for-byte. The\n// browser face imports nothing from `src/server` (peer environment faces share no\n// import), so the literals are declared once here too — the SAME strings, not\n// shared symbols.\n\n/**\n * The Streamable-HTTP transport header that carries the MCP session id. The browser\n * face's {@link import('./transports/HTTPClientTransport.js').HTTPClientTransport}\n * ECHOES this header exactly like the Node face's `HTTPClientTransport`\n * (`src/server`), so the same client interoperates with an `MCPSession`-based\n * server unchanged.\n */\nexport const MCP_SESSION_HEADER = 'mcp-session-id'\n\n/**\n * The Streamable-HTTP transport header carrying the MCP protocol version. Modern\n * requests derive it from their own `_meta`; legacy requests echo the negotiated\n * initialize result on each subsequent request.\n */\nexport const MCP_PROTOCOL_VERSION_HEADER = 'mcp-protocol-version'\n\n/**\n * The modern Streamable-HTTP request header carrying the JSON-RPC method. It is\n * emitted on every modern request and never on a legacy request.\n */\nexport const MCP_METHOD_HEADER = 'mcp-method'\n\n/**\n * The modern Streamable-HTTP request header carrying a named target. The browser\n * HTTP client emits it only for `tools/call`, from that request's `params.name`.\n */\nexport const MCP_NAME_HEADER = 'mcp-name'\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, so the same value is declared on each face. The browser face's\n// `WebSocketClientTransport` defaults to this value when `protocols` is omitted, and\n// `createWebSocketServer` selects it from the client's offer.\n\n/**\n * The WebSocket subprotocol `createWebSocketClientTransport` requests by default —\n * `'mcp'`, which `createWebSocketServer` selects when the client offers it. Per RFC 6455\n * §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 {\n\tMCPClientTransportEventMap,\n\tMCPClientTransportInterface,\n\tMCPHeaderParameter,\n\tJSONRPCMessage,\n} from '@src/core'\nimport type { EmitterInterface } from '@orkestrel/emitter'\nimport type { HTTPClientTransportOptions } from '../types.js'\nimport {\n\tbuildHeaderParameters,\n\tbuildHeaderProjection,\n\tencodeSentinel,\n\tinferRequestVersion,\n\tisJSONRPCResponse,\n\tisMCPVersion,\n\tisModernRequest,\n\tparseJSONRPCMessage,\n} from '@src/core'\nimport { isArray, isRecord, isString } from '@orkestrel/contract'\nimport { Emitter } from '@orkestrel/emitter'\nimport {\n\tMCP_METHOD_HEADER,\n\tMCP_NAME_HEADER,\n\tMCP_PROTOCOL_VERSION_HEADER,\n\tMCP_SESSION_HEADER,\n} 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 MCPClientTransportInterface} that drives a REMOTE Streamable-HTTP MCP server\n * over the native `fetch`, the browser sibling of the Node face's\n * {@link import('@orkestrel/mcp/server').HTTPClientTransport}, honoring the SAME\n * `mcp-session-id` semantics so it interoperates with an `MCPSession`-based server\n * unchanged.\n *\n * @remarks\n * - **Request/response over `fetch`.** `send(message)` POSTs the JSON-serialized\n * message to `options.url` with `content-type: application/json` and an\n * `Accept` of BOTH `application/json` and `text/event-stream` (so the server may\n * answer with either framing) — plus any `options.headers` (for example, an\n * `Authorization` bearer). It then decodes the reply and emits each decoded\n * {@link JSONRPCMessage} on the `message` event the\n * {@link import('@orkestrel/mcp').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 with the\n * `@orkestrel/sse` {@link import('@orkestrel/sse').SSEParserInterface} (the browser\n * face's own `readEventStream`) — the inverse of the server's `openStream` seam, so\n * the wire round-trips. A `202` Accepted (a notification) carries no body and emits\n * nothing.\n * - **Session and protocol headers.** `start()` is a no-op (a\n * request/response transport opens no long-lived connection). The\n * `mcp-session-id` response header, when a STATEFUL server sends one (on\n * `initialize`), is captured into `session` and then ECHOED as the\n * `mcp-session-id` request header on every SUBSEQUENT request — so an\n * `MCPClient` passes a stateful server's session validation. The\n * initialize result's `protocolVersion` is likewise captured, but only\n * when it is a SUPPORTED value, and echoed as `mcp-protocol-version` alone on\n * subsequent legacy requests. Modern requests instead derive protocol and method\n * headers from the message, plus the name header only for `tools/call` — carried in the\n * protocol's Base64 sentinel form whenever the tool name cannot ride as plain ASCII.\n * Before initialize returns, neither captured legacy header is sent.\n * `close()` clears the captured protocol so a reconnect's `initialize`\n * POST is headerless; the captured `session` persists across `close()`.\n * - **`close()` releases what is in flight.** Every `fetch` this transport still has open is\n * ABORTED, which cancels the response body a `send` is reading — an SSE reply the server\n * never ends would otherwise outlive the transport, with nothing left able to reach it. The\n * aborted read surfaces on `error` and the `send` reporting it resolves. `close()` is\n * idempotent (one `close` event per connected lifetime), and `start()` opens the next one.\n * - **Total at the boundary.** 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.** Owns the `emitter` ({@link MCPClientTransportEventMap}); 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 MCPClientTransportInterface {\n\treadonly #emitter: Emitter<MCPClientTransportEventMap>\n\treadonly #url: string\n\treadonly #headers: Readonly<Record<string, string>>\n\treadonly #fetch: typeof fetch\n\treadonly #timeout: number | undefined\n\t// The requests on the wire, one controller each. `close` is the only thing that can\n\t// reach them: a `send` parked on a reply that never ends holds both the request and its\n\t// response reader, and no other seam this transport exposes leads back to either.\n\treadonly #pending = new Set<AbortController>()\n\t// Each listed tool's `x-mcp-header` projections, read from the `tools/list` results this\n\t// transport delivered. The annotations describe a call's own arguments, so the table a\n\t// `tools/call` projects from is the one the caller was told about and nothing else.\n\treadonly #parameters = new Map<string, readonly MCPHeaderParameter[]>()\n\t// The listing lineage each `tools/list` send belongs to, stamped at send time and read back\n\t// when its answer arrives. `send` opens an independent `fetch` per call, so two listings can\n\t// answer in the opposite order to their requests; the stamp is what tells an answer from a\n\t// superseded listing apart from one the current lineage is still owed.\n\treadonly #stamps = new WeakMap<JSONRPCMessage, number>()\n\t#session: string | undefined = undefined\n\t#protocol: string | undefined = undefined\n\t#generation = 0\n\t#closed = false\n\n\tconstructor(options: HTTPClientTransportOptions) {\n\t\tthis.#emitter = new Emitter<MCPClientTransportEventMap>()\n\t\tthis.#url = options.url\n\t\tthis.#headers = options.headers ?? {}\n\t\tthis.#fetch = options.fetch ?? globalThis.fetch.bind(globalThis)\n\t\tthis.#timeout = options.timeout\n\t}\n\n\tget emitter(): EmitterInterface<MCPClientTransportEventMap> {\n\t\treturn this.#emitter\n\t}\n\n\tget session(): string | undefined {\n\t\treturn this.#session\n\t}\n\n\tget duplex(): boolean {\n\t\t// Streamable HTTP carries no client-initiated notification: the dated revision defines\n\t\t// none over it, and closing the response stream is the cancellation signal instead.\n\t\treturn false\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. There is nothing to arm; opening the next connected lifetime is all\n\t\t// this does, so a transport an earlier `close` ended sends again from here.\n\t\tthis.#closed = false\n\t}\n\n\tasync send(message: JSONRPCMessage): Promise<void> {\n\t\tthis.#stamp(message)\n\t\tconst request = new AbortController()\n\t\tthis.#pending.add(request)\n\t\ttry {\n\t\t\tawait this.#exchange(message, request.signal)\n\t\t} finally {\n\t\t\tthis.#pending.delete(request)\n\t\t}\n\t}\n\n\t// Stamp a `tools/list` send with the listing lineage its answer may cache into, at SEND time:\n\t// a cursorless listing starts the next lineage, and a continuation joins whichever one was\n\t// current when it went out. Nothing else is stamped, because nothing else reaches the table.\n\t// The Node face runs the identical stamping.\n\t#stamp(message: JSONRPCMessage): void {\n\t\tif (!isModernRequest(message) || message.method !== 'tools/list') return\n\t\tif (message.params?.['cursor'] === undefined) this.#generation += 1\n\t\tthis.#stamps.set(message, this.#generation)\n\t}\n\n\t// One request/response exchange under `signal`: `close` aborts it, and a `timeout` option\n\t// composes with it so whichever fires first ends the same fetch and the same body read.\n\tasync #exchange(message: JSONRPCMessage, signal: AbortSignal): 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.#buildHeaders(message),\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\tsignal:\n\t\t\t\t\tthis.#timeout === undefined\n\t\t\t\t\t\t? signal\n\t\t\t\t\t\t: AbortSignal.any([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, message)\n\t}\n\n\t// Abort every request still on the wire, then clear the captured protocol before emitting\n\t// `close`, so a reconnect's `initialize` POST carries no `mcp-protocol-version` header (the\n\t// captured `session` is untouched). Idempotent: a second `close` on a transport this one\n\t// already ended releases nothing and emits nothing.\n\tasync close(): Promise<void> {\n\t\tif (this.#closed) return\n\t\tthis.#closed = true\n\t\tfor (const request of this.#pending) request.abort()\n\t\tthis.#pending.clear()\n\t\tthis.#protocol = undefined\n\t\tthis.#emitter.emit('close')\n\t}\n\n\t// Modern requests announce their own protocol version, so the header is projected from the\n\t// message through the SHARED `inferRequestVersion` — the same read the server's own\n\t// expectation performs, and the same read the Node face performs. Legacy requests carry\n\t// the version captured from the `initialize` handshake instead. `tools/call` is the one\n\t// named method `MCPClientInterface` publishes, so it is the one that stamps `Mcp-Name`; the\n\t// value rides through `encodeSentinel`, which leaves a plain tool name literal and carries\n\t// anything else as the protocol's Base64 sentinel.\n\t#buildHeaders(message: JSONRPCMessage): Readonly<Record<string, string>> {\n\t\tif (isModernRequest(message)) {\n\t\t\tconst version = inferRequestVersion(message)\n\t\t\tconst name = message.params?.['name']\n\t\t\treturn {\n\t\t\t\t...(version === undefined ? {} : { [MCP_PROTOCOL_VERSION_HEADER]: version }),\n\t\t\t\t[MCP_METHOD_HEADER]: message.method,\n\t\t\t\t...(message.method === 'tools/call' && isString(name)\n\t\t\t\t\t? {\n\t\t\t\t\t\t\t[MCP_NAME_HEADER]: encodeSentinel(name),\n\t\t\t\t\t\t\t...buildHeaderProjection(\n\t\t\t\t\t\t\t\tthis.#parameters.get(name) ?? [],\n\t\t\t\t\t\t\t\tmessage.params?.['arguments'],\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t}\n\t\t\t\t\t: {}),\n\t\t\t}\n\t\t}\n\t\treturn this.#protocol === undefined ? {} : { [MCP_PROTOCOL_VERSION_HEADER]: this.#protocol }\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 with 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, sent: JSONRPCMessage): Promise<void> {\n\t\tif (response.status === 202) return\n\t\tconst type = response.headers.get('content-type') ?? ''\n\t\ttry {\n\t\t\tif (type.includes('text/event-stream')) {\n\t\t\t\tfor (const message of await readEventStream(response)) this.#capture(message, sent)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (type.includes('application/json')) {\n\t\t\t\tconst message = parseJSONRPCMessage(await response.json())\n\t\t\t\tif (message !== undefined) this.#capture(message, sent)\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tthis.#emitter.emit('error', error)\n\t\t}\n\t}\n\n\t// Capture the negotiated SUPPORTED protocol from the initialize result before emitting\n\t// the message, so the next request carries its required protocol-version header; any\n\t// other value (missing or unsupported) is ignored and leaves `#protocol` unchanged. A\n\t// `tools/list` answer additionally passes through `#select`, which is where SEP-2243's\n\t// client-side exclusion happens — before the caller ever sees the result.\n\t#capture(message: JSONRPCMessage, sent: JSONRPCMessage): void {\n\t\tif (\n\t\t\tisJSONRPCResponse(message) &&\n\t\t\tisRecord(message.result) &&\n\t\t\tisMCPVersion(message.result['protocolVersion'])\n\t\t) {\n\t\t\tthis.#protocol = message.result['protocolVersion']\n\t\t}\n\t\tthis.#emitter.emit('message', this.#select(message, sent))\n\t}\n\n\t// SEP-2243's client half, derived from the traffic this transport already carries: cache\n\t// each listed tool's projections, and DROP every definition whose annotations violate the\n\t// constraints, so a tool this transport could not project headers for never reaches the\n\t// caller's `tools/list` result. Each exclusion is reported on `error` naming the tool,\n\t// which is this transport's observation channel for a contained fault a `send` swallows.\n\t// Everything else in the result — the cache stamps, the metadata, the valid siblings —\n\t// travels through unchanged. The Node face runs the identical selection.\n\t//\n\t// The SENT request decides whether this page joins the table or replaces it. A\n\t// `tools/list` carrying no `cursor` is a FRESH listing, so the table is cleared before\n\t// this page is cached: what the caller has now been told is this listing and nothing\n\t// earlier. A continuation carries the cursor the previous page handed back, so its page\n\t// accumulates onto the ones before it. Without the split, a tool the fresh listing OMITS\n\t// keeps projecting headers from a listing the caller has already been told is superseded.\n\t//\n\t// Arrival order decides nothing, because the SEND's own lineage stamp does. A listing another\n\t// cursorless `tools/list` superseded before its answer arrived is DELIVERED whole — the\n\t// exclusion and its `error` still apply — and caches nothing, so the table describes the\n\t// latest fresh listing and its own continuations however overlapping answers interleave. A\n\t// caller working from a superseded page projects nothing for its tools, which is the safe\n\t// direction: the server's own bounded lookup stays the validation authority.\n\t#select(message: JSONRPCMessage, sent: JSONRPCMessage): JSONRPCMessage {\n\t\tif (!isModernRequest(sent) || sent.method !== 'tools/list') return message\n\t\tif (!isJSONRPCResponse(message) || message.error !== undefined) return message\n\t\tconst result = message.result\n\t\tconst listed = isRecord(result) ? result['tools'] : undefined\n\t\tif (!isRecord(result) || !isArray(listed)) return message\n\t\tconst current = this.#stamps.get(sent) === this.#generation\n\t\tif (current && sent.params?.['cursor'] === undefined) this.#parameters.clear()\n\t\tconst kept: unknown[] = []\n\t\tfor (const tool of listed) {\n\t\t\tif (!isRecord(tool) || !isString(tool['name'])) {\n\t\t\t\tkept.push(tool)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tconst parameters = buildHeaderParameters(tool['inputSchema'])\n\t\t\tif (parameters === undefined) {\n\t\t\t\tthis.#emitter.emit(\n\t\t\t\t\t'error',\n\t\t\t\t\tnew Error(\n\t\t\t\t\t\t`MCP tool '${tool['name']}' is excluded from tools/list: its inputSchema carries an invalid x-mcp-header annotation`,\n\t\t\t\t\t),\n\t\t\t\t)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif (current) this.#parameters.set(tool['name'], parameters)\n\t\t\tkept.push(tool)\n\t\t}\n\t\treturn { ...message, result: { ...result, tools: kept } }\n\t}\n}\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 `@orkestrel/mcp`'s `MCPClientTransportInterface`), a `MessagePort` is a\n * plain duplex channel — the SAME class implements `@orkestrel/mcp`'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 with `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 —\n * because `MCPTransportInterface` carries no `error` channel for this port to\n * surface a non-string frame on (unlike `MCPClientTransportInterface`'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, `@orkestrel/mcp`'s `MCPTransportInterface`\n * doc).** `listen`/`closed` each hold the one active 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\treadonly #message = (event: MessageEvent): void => this.#receive(event.data)\n\treadonly #malformed = (): void => {}\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', this.#message)\n\t\tthis.#port.addEventListener('messageerror', this.#malformed)\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\tconst onClosed = this.#onClosed\n\t\tthis.#onMessage = undefined\n\t\tthis.#onClosed = undefined\n\t\tthis.#port.removeEventListener('message', this.#message)\n\t\tthis.#port.removeEventListener('messageerror', this.#malformed)\n\t\tthis.#port.close()\n\t\tonClosed?.()\n\t}\n\n\t// Decode one inbound `postMessage` payload: a non-string `data` is dropped, never\n\t// forwarded (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 {\n\tMCPClientTransportEventMap,\n\tMCPClientTransportInterface,\n\tJSONRPCMessage,\n} 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 MCPClientTransportInterface} that drives a REMOTE MCP server over the native\n * `WebSocket` global, the browser sibling of the Node face's\n * {@link import('@orkestrel/mcp/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`. A queue rides ONE\n * connection: a close DISCARDS whatever is still in it.\n * - **A closed channel REJECTS.** The native socket confirms nothing about a write, so this\n * transport answers from its own state: a `send` after `close()`, or on a socket already\n * reporting `CLOSING` / `CLOSED`, REJECTS with `WebSocket transport is not connected` rather\n * than resolving on a frame nobody wrote. Only the closed state rejects — a pre-open `send`\n * still queues.\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 (never\n * throws on adversarial wire input).\n * - **`close()`** unsubscribes from the underlying socket, closes it, and fires `close`\n * (idempotent); the socket's native `close` event (a server-initiated close) fires the\n * SAME `close` exactly once total — `close()` first flips the guard, so the native event\n * never double-emits, and the released socket reports its own close to nobody. Closing before\n * the socket opens resolves the pending `start()` rather than leaving it pending, matching the\n * Node face. A `send` issued after `close()` REJECTS (it is never queued), and the\n * pre-open queue is DISCARDED — by `close()` and by the native `close` event alike — so a\n * closed transport delivers nothing until a `start()` opens a new connection, and nothing\n * the caller handed the abandoned connection rides that one.\n * - **Observable.** Owns the `emitter` ({@link MCPClientTransportEventMap}); 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 MCPClientTransportInterface {\n\treadonly #emitter: Emitter<MCPClientTransportEventMap>\n\treadonly #url: string\n\treadonly #protocols: string | string[] | undefined\n\t// Bound once, as fields, so `close` can remove exactly the listeners `#bind` installed: an\n\t// inline arrow is a new function on every call and can never be removed by reference.\n\treadonly #frame = (event: MessageEvent<unknown>): void => this.#receive(event.data)\n\treadonly #ending = (): void => this.#onClose()\n\treadonly #failure = (event: Event): void => this.#emitter.emit('error', event)\n\treadonly #opening = (): void => this.#onOpen()\n\treadonly #rejection = (): void => this.#onHandshakeError()\n\t#socket: WebSocket | undefined = undefined\n\t#handshake: WebSocket | undefined = undefined\n\t#resolve: (() => void) | undefined = undefined\n\t#reject: ((error: Error) => void) | undefined = undefined\n\t#queue: string[] = []\n\t#closed = false\n\n\tconstructor(options: WebSocketClientTransportOptions) {\n\t\tthis.#emitter = new Emitter<MCPClientTransportEventMap>()\n\t\tthis.#url = options.url\n\t\tconst protocols = options.protocols\n\t\t// Default to MCP_WEBSOCKET_SUBPROTOCOL when `protocols` is omitted; the server selects it\n\t\t// from this offer. 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<MCPClientTransportEventMap> {\n\t\treturn this.#emitter\n\t}\n\n\tget session(): string | undefined {\n\t\treturn undefined\n\t}\n\n\tget duplex(): boolean {\n\t\t// A socket is bidirectional for its whole life: either side writes a frame whenever it\n\t\t// has one, with no request to attach it to.\n\t\treturn true\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\tthis.#handshake = socket\n\t\t\tthis.#resolve = resolve\n\t\t\tthis.#reject = reject\n\t\t\tsocket.addEventListener('open', this.#opening)\n\t\t\tsocket.addEventListener('error', this.#rejection)\n\t\t})\n\t}\n\n\tasync send(message: JSONRPCMessage): Promise<void> {\n\t\tconst socket = this.#socket\n\t\t// A closed transport, and a socket the host has already moved past OPEN, each name a\n\t\t// channel that will never carry this frame. Resolving would tell the client the message\n\t\t// was written and leave its correlated request pending to its own deadline. The socket's\n\t\t// own state is a SECOND source rather than a copy of the first: the native `close` event\n\t\t// lags the readyState transition, so a server-initiated close leaves this transport's flag\n\t\t// clear while the socket already reports `CLOSING`.\n\t\tif (\n\t\t\tthis.#closed ||\n\t\t\tsocket?.readyState === WebSocket.CLOSING ||\n\t\t\tsocket?.readyState === WebSocket.CLOSED\n\t\t) {\n\t\t\tthrow new Error('WebSocket transport is not connected')\n\t\t}\n\t\tconst text = JSON.stringify(message)\n\t\t// No socket yet (`start()` has not run) or still `CONNECTING`: queue it, and `#flush`\n\t\t// writes the whole queue in order the moment the socket opens.\n\t\tif (socket !== undefined && socket.readyState === WebSocket.OPEN) socket.send(text)\n\t\telse this.#queue.push(text)\n\t}\n\n\tasync close(): Promise<void> {\n\t\tif (this.#closed) return\n\t\tthis.#closed = true\n\t\t// The queue belongs to the connection the caller handed those frames to. Keeping it\n\t\t// would write them onto whatever socket a later `start()` opens, delivering a message\n\t\t// against a connection the caller had already abandoned.\n\t\tthis.#queue = []\n\t\tconst socket = this.#socket\n\t\tconst resolve = this.#resolve\n\t\tthis.#releaseHandshake()\n\t\tthis.#release()\n\t\tthis.#socket = undefined\n\t\tif (socket !== undefined) socket.close()\n\t\tthis.#emitter.emit('close')\n\t\tresolve?.()\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', this.#frame)\n\t\tsocket.addEventListener('close', this.#ending)\n\t\tsocket.addEventListener('error', this.#failure)\n\t}\n\n\t// Unsubscribe from the socket this transport holds. A closing socket goes on\n\t// firing its own events, so a bridge left installed on one this transport has released\n\t// would report a connection it no longer owns.\n\t#release(): void {\n\t\tconst socket = this.#socket\n\t\tif (socket === undefined) return\n\t\tsocket.removeEventListener('message', this.#frame)\n\t\tsocket.removeEventListener('close', this.#ending)\n\t\tsocket.removeEventListener('error', this.#failure)\n\t}\n\n\t#releaseHandshake(): void {\n\t\tconst socket = this.#handshake\n\t\tif (socket === undefined) return\n\t\tsocket.removeEventListener('open', this.#opening)\n\t\tsocket.removeEventListener('error', this.#rejection)\n\t\tthis.#handshake = undefined\n\t\tthis.#resolve = undefined\n\t\tthis.#reject = undefined\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#onOpen(): void {\n\t\tconst socket = this.#handshake\n\t\tconst resolve = this.#resolve\n\t\tif (socket === undefined || resolve === undefined) return\n\t\tthis.#releaseHandshake()\n\t\tthis.#flush(socket)\n\t\tresolve()\n\t}\n\n\t#onHandshakeError(): void {\n\t\tconst socket = this.#handshake\n\t\tconst reject = this.#reject\n\t\tif (socket === undefined || reject === undefined || socket.readyState === WebSocket.OPEN) return\n\t\tthis.#releaseHandshake()\n\t\tthis.#release()\n\t\tthis.#socket = undefined\n\t\treject(new Error('WebSocket connection failed'))\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\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. Only the socket this transport still\n\t// holds can reach here: a superseded one was unsubscribed when it was released, so its own\n\t// later close cannot end the connection that replaced it.\n\t#onClose(): void {\n\t\tif (this.#closed) return\n\t\tthis.#closed = true\n\t\t// Same rule as `close()`: the ended connection takes its queue with it.\n\t\tthis.#queue = []\n\t\tthis.#release()\n\t\tthis.#socket = undefined\n\t\tthis.#emitter.emit('close')\n\t}\n}\n","import type { MCPClientTransportInterface, 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 * Creates the browser-face WebSocket CLIENT transport for an\n * {@link import('@orkestrel/mcp').MCPClientInterface} — a {@link MCPClientTransportInterface}\n * that drives a REMOTE MCP server over the native `WebSocket` global, the browser\n * sibling of the Node face's `createWebSocketClientTransport` (`@orkestrel/mcp/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 MCPClientTransportInterface} 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): MCPClientTransportInterface {\n\treturn new WebSocketClientTransport(options)\n}\n\n/**\n * Creates the browser-face HTTP CLIENT transport for an\n * {@link import('@orkestrel/mcp').MCPClientInterface} — a {@link MCPClientTransportInterface}\n * that drives a REMOTE Streamable-HTTP MCP server over the native `fetch`, the\n * browser sibling of the Node face's `createHTTPClientTransport` (`@orkestrel/mcp/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 with `@orkestrel/sse`), and the reply is surfaced on the transport's\n * `message` event for the client's id correlation. Add `options.headers` (for example, an\n * `Authorization` bearer) to reach a guarded server. `start` / `close` hold no\n * connection; against a STATEFUL server it captures the `mcp-session-id` from\n * `initialize` and echoes it on later requests. It also captures the initialize\n * result's `protocolVersion` and sends `mcp-protocol-version` alone on subsequent\n * legacy requests. Modern requests instead derive `mcp-protocol-version` and\n * `mcp-method` from the message, plus `mcp-name` only for `tools/call`, so the\n * same `MCPClient` passes either era's protocol gates without caller wiring.\n *\n * @param options - `url` (the remote endpoint; REQUIRED), optional `headers` merged\n * onto every request, optional `fetch` (default `globalThis.fetch`), and optional\n * `timeout` (ms, applied with `AbortSignal.timeout`); see\n * {@link HTTPClientTransportOptions}\n * @returns A working {@link MCPClientTransportInterface} 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): MCPClientTransportInterface {\n\treturn new HTTPClientTransport(options)\n}\n\n/**\n * Creates the browser-face `MessagePort` transport — a\n * {@link import('@orkestrel/mcp').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('@orkestrel/mcp').bindServer} or\n * {@link import('@orkestrel/mcp').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('@orkestrel/mcp').MCPTransportInterface} over the port\n *\n * @example\n * ```ts\n * import { bindServer, createMCPLegacy, createMCPServer } from '@orkestrel/mcp'\n * import { createMessagePortTransport } from '@orkestrel/mcp/browser'\n *\n * const { port1, port2 } = new MessageChannel()\n * const mcp = createMCPServer({ identity: { name: 's', version: '1.0.0' }, tools })\n * bindServer(createMCPLegacy(mcp), createMessagePortTransport({ port: port1 })) // answers `initialize` too; pass `mcp` alone for modern-only\n * ```\n */\nexport function createMessagePortTransport(\n\toptions: MessagePortTransportOptions,\n): MCPTransportInterface {\n\treturn new MessagePortTransport(options)\n}\n\n/**\n * Adapts a hostable {@link ServeMCPScopeInterface} (`self` in a dedicated Web Worker,\n * or any structurally matching double) into a {@link ScopeTransportInterface} — the\n * implicit, portless message channel `serveMCPScope` binds for the\n * dedicated-worker shape.\n *\n * @remarks\n * `send` writes each outbound string through `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 through `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 { JSONRPCMessage, MCPServerInterface } from '@src/core'\nimport type { SSEParserInterface } from '@orkestrel/sse'\nimport type { ServeMCPOptions, ScopeTransportInterface, ServeMCPScopeInterface } from './types.js'\nimport { bindServer, createMCPServer, parseJSONRPCMessage } from '@src/core'\nimport { isString } from '@orkestrel/contract'\nimport { createSSEParser } from '@orkestrel/sse'\nimport { DEFAULT_MCP_SERVER_NAME, DEFAULT_MCP_SERVER_VERSION } from './constants.js'\nimport { createScopeTransport } from './factories.js'\nimport { MessagePortTransport } from './transports/MessagePortTransport.js'\n\n// The MCP browser-transport helpers — module-scope names, so they carry 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 share\n// 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`: a malformed / non-message SSE\n// `data:` event is dropped, never thrown.\n//\n// `serveMCPScope` / `serveMCP` are the worker bootstrap. They are reusable exported\n// infrastructure that BOOTS and BINDS — the browser sibling of `src/core`'s\n// `bindServer` / `bindClient` — and each returns an idempotent disposer rather than an\n// entity, so they belong here rather than in `factories.ts` (`.claude/rules/architecture.md`\n// kind purity: placement follows what a function is, and every exported `factories.ts`\n// function is named `create*`). The value factory they compose, `createScopeTransport`,\n// stays in `factories.ts`.\n//\n// `createScopeMessageListener` is the bootstrap's per-event dispatcher, extracted\n// (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 * Decodes 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: 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 * Decodes 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} through {@link decodeEvent} (so a non-message / non-JSON `data:`\n * event is DROPPED, never thrown — total). 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 * Builds `serveMCPScope`'s `message`-event listener — the unified\n * dispatcher that routes EVERY inbound event on a hostable scope, portless or\n * port-bearing, to the right binding.\n *\n * @remarks\n * Port-bearing events (`event.ports.length > 0`) are gated by `options.accept` FIRST\n * — when the gate returns `false` the event is dropped entirely (no binding, no reply).\n * Accepted events spawn a fresh `MessagePortTransport` over `event.ports[0]`,\n * `bindServer` `server` onto it, and record a teardown (`unbind` then `transport.close()`)\n * into `teardowns` KEYED BY THAT PORT. A port already present is IGNORED — repeated delivery\n * of the same `MessagePort` would create duplicate bindings over one port (→ duplicated\n * replies), so a repeat is silently dropped.\n *\n * The key is what makes `teardowns` the ONLY place an accepted port is remembered. A separate\n * seen-port set would be a second collection over the same lifetime, and the caller's disposer\n * would have to remember to empty both — so a long-lived scope such as a Service Worker would\n * retain every port it ever accepted, closed and unbound ones included. Membership answers\n * \"already bound?\" and `clear()` drops the binding and the dedup together.\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, 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 map `serveMCPScope`'s dispose drains and clears, keyed by the accepted port; 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 Map<MessagePort, () => 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: Map<MessagePort, () => void>,\n\toptions: ServeMCPOptions,\n): (event: MessageEvent) => void {\n\treturn (event: MessageEvent): void => {\n\t\tconst ports = event.ports\n\t\tif (ports.length > 0) {\n\t\t\t// Gate: consult accept (origin/identity check) before binding.\n\t\t\tif (options.accept !== undefined && !options.accept(event)) return\n\t\t\tconst port = ports[0]\n\t\t\tif (port === undefined) return\n\t\t\t// Deduplicate off the teardown map itself: repeated delivery of the same port would\n\t\t\t// create duplicate bindings, and a second collection recording the same fact is one\n\t\t\t// the disposer can forget to empty.\n\t\t\tif (teardowns.has(port)) return\n\t\t\tconst transport = new MessagePortTransport({ port })\n\t\t\tconst unbind = bindServer(server, transport)\n\t\t\tteardowns.set(port, () => {\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\n/**\n * Boots an `MCPServer` inside a hostable worker scope and wires its message events to it.\n *\n * @remarks\n * Port-bearing events are gated by `options.accept`, deduplicated by port, and receive\n * their own `MessagePortTransport` binding. Portless string events use the scope's\n * implicit channel. The returned disposer removes the listener, unbinds the implicit\n * channel, closes every accepted port binding, and drops the ports themselves — the\n * bindings are held in one map keyed by port, so nothing survives the clear. The served\n * endpoint is modern-only: it answers a legacy `initialize` with `-32601`. A dual-era\n * worker composes `bindServer(createMCPLegacy(mcp), …)` instead of this function.\n *\n * @param scope - The hostable worker scope to wire\n * @param options - The tools, optional identity, and optional port-event gate\n * @returns An idempotent disposer for every binding owned by this call\n */\nexport function serveMCPScope(scope: ServeMCPScopeInterface, options: ServeMCPOptions): () => void {\n\tconst server = createMCPServer({\n\t\ttools: options.tools,\n\t\tidentity: {\n\t\t\tname: options.name ?? DEFAULT_MCP_SERVER_NAME,\n\t\t\tversion: options.version ?? DEFAULT_MCP_SERVER_VERSION,\n\t\t},\n\t})\n\tconst scopeTransport = createScopeTransport(scope)\n\tconst unbindScope = bindServer(server, scopeTransport)\n\tconst teardowns = new Map<MessagePort, () => 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.values()) teardown()\n\t\t// One clear releases the bindings AND the ports they were keyed by, so a scope that\n\t\t// outlives its disposer — a Service Worker — retains neither.\n\t\tteardowns.clear()\n\t}\n}\n\n/**\n * Boots an `MCPServer` inside the current hostable worker scope.\n *\n * @remarks\n * The served endpoint is modern-only: it answers a legacy `initialize` with `-32601`. A\n * dual-era worker composes `bindServer(createMCPLegacy(mcp), …)` instead of this function.\n *\n * @param options - The tools, optional identity, and optional port-event gate\n * @returns The disposer returned by {@link serveMCPScope}\n */\nexport function serveMCP(options: ServeMCPOptions): () => void {\n\treturn serveMCPScope(globalThis, options)\n}\n"],"mappings":";;;;;;;;;;;;AAcA,IAAa,qBAAqB;;;;;;AAOlC,IAAa,8BAA8B;;;;;AAM3C,IAAa,oBAAoB;;;;;AAMjC,IAAa,kBAAkB;;AAS/B,IAAa,0BAA0B;;AAGvC,IAAa,6BAA6B;;;;;;;;;;AAiB1C,IAAa,4BAA4B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACoBzC,IAAa,sBAAb,MAAwE;CACvE;CACA;CACA;CACA;CACA;CAIA,2BAAoB,IAAI,IAAqB;CAI7C,8BAAuB,IAAI,IAA2C;CAKtE,0BAAmB,IAAI,QAAgC;CACvD,WAA+B,KAAA;CAC/B,YAAgC,KAAA;CAChC,cAAc;CACd,UAAU;CAEV,YAAY,SAAqC;EAChD,KAAK,WAAW,IAAI,QAAoC;EACxD,KAAK,OAAO,QAAQ;EACpB,KAAK,WAAW,QAAQ,WAAW,CAAC;EACpC,KAAK,SAAS,QAAQ,SAAS,WAAW,MAAM,KAAK,UAAU;EAC/D,KAAK,WAAW,QAAQ;CACzB;CAEA,IAAI,UAAwD;EAC3D,OAAO,KAAK;CACb;CAEA,IAAI,UAA8B;EACjC,OAAO,KAAK;CACb;CAEA,IAAI,SAAkB;EAGrB,OAAO;CACR;CAEA,MAAM,QAAuB;EAI5B,KAAK,UAAU;CAChB;CAEA,MAAM,KAAK,SAAwC;EAClD,KAAK,OAAO,OAAO;EACnB,MAAM,UAAU,IAAI,gBAAgB;EACpC,KAAK,SAAS,IAAI,OAAO;EACzB,IAAI;GACH,MAAM,KAAK,UAAU,SAAS,QAAQ,MAAM;EAC7C,UAAU;GACT,KAAK,SAAS,OAAO,OAAO;EAC7B;CACD;CAMA,OAAO,SAA+B;EACrC,IAAI,CAAC,gBAAgB,OAAO,KAAK,QAAQ,WAAW,cAAc;EAClE,IAAI,QAAQ,SAAS,cAAc,KAAA,GAAW,KAAK,eAAe;EAClE,KAAK,QAAQ,IAAI,SAAS,KAAK,WAAW;CAC3C;CAIA,MAAM,UAAU,SAAyB,QAAoC;EAC5E,IAAI;EACJ,IAAI;GACH,WAAW,MAAM,KAAK,OAAO,KAAK,MAAM;IACvC,QAAQ;IACR,SAAS;KACR,gBAAgB;KAChB,QAAQ;KAIR,GAAI,KAAK,aAAa,KAAA,IAAY,CAAC,IAAI,GAAG,qBAAqB,KAAK,SAAS;KAC7E,GAAG,KAAK,cAAc,OAAO;KAC7B,GAAG,KAAK;IACT;IACA,MAAM,KAAK,UAAU,OAAO;IAC5B,QACC,KAAK,aAAa,KAAA,IACf,SACA,YAAY,IAAI,CAAC,QAAQ,YAAY,QAAQ,KAAK,QAAQ,CAAC,CAAC;GACjE,CAAC;EACF,SAAS,OAAO;GAGf,KAAK,SAAS,KAAK,SAAS,KAAK;GACjC;EACD;EAGA,MAAM,UAAU,SAAS,QAAQ,IAAI,kBAAkB;EACvD,IAAI,YAAY,MAAM,KAAK,WAAW;EACtC,MAAM,KAAK,SAAS,UAAU,OAAO;CACtC;CAMA,MAAM,QAAuB;EAC5B,IAAI,KAAK,SAAS;EAClB,KAAK,UAAU;EACf,KAAK,MAAM,WAAW,KAAK,UAAU,QAAQ,MAAM;EACnD,KAAK,SAAS,MAAM;EACpB,KAAK,YAAY,KAAA;EACjB,KAAK,SAAS,KAAK,OAAO;CAC3B;CASA,cAAc,SAA2D;EACxE,IAAI,gBAAgB,OAAO,GAAG;GAC7B,MAAM,UAAU,oBAAoB,OAAO;GAC3C,MAAM,OAAO,QAAQ,SAAS;GAC9B,OAAO;IACN,GAAI,YAAY,KAAA,IAAY,CAAC,IAAI,GAAG,8BAA8B,QAAQ;KACzE,oBAAoB,QAAQ;IAC7B,GAAI,QAAQ,WAAW,gBAAgB,SAAS,IAAI,IACjD;MACC,kBAAkB,eAAe,IAAI;KACtC,GAAG,sBACF,KAAK,YAAY,IAAI,IAAI,KAAK,CAAC,GAC/B,QAAQ,SAAS,YAClB;IACD,IACC,CAAC;GACL;EACD;EACA,OAAO,KAAK,cAAc,KAAA,IAAY,CAAC,IAAI,GAAG,8BAA8B,KAAK,UAAU;CAC5F;CAMA,MAAM,SAAS,UAAoB,MAAqC;EACvE,IAAI,SAAS,WAAW,KAAK;EAC7B,MAAM,OAAO,SAAS,QAAQ,IAAI,cAAc,KAAK;EACrD,IAAI;GACH,IAAI,KAAK,SAAS,mBAAmB,GAAG;IACvC,KAAK,MAAM,WAAW,MAAM,gBAAgB,QAAQ,GAAG,KAAK,SAAS,SAAS,IAAI;IAClF;GACD;GACA,IAAI,KAAK,SAAS,kBAAkB,GAAG;IACtC,MAAM,UAAU,oBAAoB,MAAM,SAAS,KAAK,CAAC;IACzD,IAAI,YAAY,KAAA,GAAW,KAAK,SAAS,SAAS,IAAI;GACvD;EACD,SAAS,OAAO;GACf,KAAK,SAAS,KAAK,SAAS,KAAK;EAClC;CACD;CAOA,SAAS,SAAyB,MAA4B;EAC7D,IACC,kBAAkB,OAAO,KACzB,SAAS,QAAQ,MAAM,KACvB,aAAa,QAAQ,OAAO,kBAAkB,GAE9C,KAAK,YAAY,QAAQ,OAAO;EAEjC,KAAK,SAAS,KAAK,WAAW,KAAK,QAAQ,SAAS,IAAI,CAAC;CAC1D;CAuBA,QAAQ,SAAyB,MAAsC;EACtE,IAAI,CAAC,gBAAgB,IAAI,KAAK,KAAK,WAAW,cAAc,OAAO;EACnE,IAAI,CAAC,kBAAkB,OAAO,KAAK,QAAQ,UAAU,KAAA,GAAW,OAAO;EACvE,MAAM,SAAS,QAAQ;EACvB,MAAM,SAAS,SAAS,MAAM,IAAI,OAAO,WAAW,KAAA;EACpD,IAAI,CAAC,SAAS,MAAM,KAAK,CAAC,QAAQ,MAAM,GAAG,OAAO;EAClD,MAAM,UAAU,KAAK,QAAQ,IAAI,IAAI,MAAM,KAAK;EAChD,IAAI,WAAW,KAAK,SAAS,cAAc,KAAA,GAAW,KAAK,YAAY,MAAM;EAC7E,MAAM,OAAkB,CAAC;EACzB,KAAK,MAAM,QAAQ,QAAQ;GAC1B,IAAI,CAAC,SAAS,IAAI,KAAK,CAAC,SAAS,KAAK,OAAO,GAAG;IAC/C,KAAK,KAAK,IAAI;IACd;GACD;GACA,MAAM,aAAa,sBAAsB,KAAK,cAAc;GAC5D,IAAI,eAAe,KAAA,GAAW;IAC7B,KAAK,SAAS,KACb,yBACA,IAAI,MACH,aAAa,KAAK,QAAQ,0FAC3B,CACD;IACA;GACD;GACA,IAAI,SAAS,KAAK,YAAY,IAAI,KAAK,SAAS,UAAU;GAC1D,KAAK,KAAK,IAAI;EACf;EACA,OAAO;GAAE,GAAG;GAAS,QAAQ;IAAE,GAAG;IAAQ,OAAO;GAAK;EAAE;CACzD;AACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC9PA,IAAa,uBAAb,MAAmE;CAClE;CACA,YAAqB,UAA8B,KAAK,SAAS,MAAM,IAAI;CAC3E,mBAAkC,CAAC;CACnC,aAAsD,KAAA;CACtD,YAAsC,KAAA;CACtC,UAAU;CAEV,YAAY,SAAsC;EACjD,KAAK,QAAQ,QAAQ;EACrB,KAAK,MAAM,iBAAiB,WAAW,KAAK,QAAQ;EACpD,KAAK,MAAM,iBAAiB,gBAAgB,KAAK,UAAU;EAC3D,KAAK,MAAM,MAAM;CAClB;CAEA,KAAK,SAAuB;EAC3B,IAAI,KAAK,SAAS;EAClB,KAAK,MAAM,YAAY,OAAO;CAC/B;CAEA,OAAO,SAA0C;EAChD,KAAK,aAAa;CACnB;CAEA,OAAO,SAA2B;EACjC,KAAK,YAAY;CAClB;CAEA,QAAc;EACb,IAAI,KAAK,SAAS;EAClB,KAAK,UAAU;EACf,MAAM,WAAW,KAAK;EACtB,KAAK,aAAa,KAAA;EAClB,KAAK,YAAY,KAAA;EACjB,KAAK,MAAM,oBAAoB,WAAW,KAAK,QAAQ;EACvD,KAAK,MAAM,oBAAoB,gBAAgB,KAAK,UAAU;EAC9D,KAAK,MAAM,MAAM;EACjB,WAAW;CACZ;CAMA,SAAS,MAAqB;EAC7B,IAAI,CAAC,SAAS,IAAI,GAAG;EACrB,KAAK,aAAa,IAAI;CACvB;AACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACvDA,IAAa,2BAAb,MAA6E;CAC5E;CACA;CACA;CAGA,UAAmB,UAAuC,KAAK,SAAS,MAAM,IAAI;CAClF,gBAA+B,KAAK,SAAS;CAC7C,YAAqB,UAAuB,KAAK,SAAS,KAAK,SAAS,KAAK;CAC7E,iBAAgC,KAAK,QAAQ;CAC7C,mBAAkC,KAAK,kBAAkB;CACzD,UAAiC,KAAA;CACjC,aAAoC,KAAA;CACpC,WAAqC,KAAA;CACrC,UAAgD,KAAA;CAChD,SAAmB,CAAC;CACpB,UAAU;CAEV,YAAY,SAA0C;EACrD,KAAK,WAAW,IAAI,QAAoC;EACxD,KAAK,OAAO,QAAQ;EACpB,MAAM,YAAY,QAAQ;EAI1B,KAAK,aACJ,OAAO,cAAc,WAClB,YACA,cAAc,KAAA,IAAA,QAEb,UAAU,WAAW,IACpB,KAAA,IACA,CAAC,GAAG,SAAS;CACpB;CAEA,IAAI,UAAwD;EAC3D,OAAO,KAAK;CACb;CAEA,IAAI,UAA8B,CAElC;CAEA,IAAI,SAAkB;EAGrB,OAAO;CACR;CAEA,MAAM,QAAuB;EAG5B,IAAI,KAAK,YAAY,KAAA,GAAW;EAChC,KAAK,UAAU;EACf,MAAM,SAAS,IAAI,UAAU,KAAK,MAAM,KAAK,UAAU;EACvD,KAAK,UAAU;EACf,KAAK,MAAM,MAAM;EACjB,MAAM,IAAI,SAAe,SAAS,WAAW;GAC5C,KAAK,aAAa;GAClB,KAAK,WAAW;GAChB,KAAK,UAAU;GACf,OAAO,iBAAiB,QAAQ,KAAK,QAAQ;GAC7C,OAAO,iBAAiB,SAAS,KAAK,UAAU;EACjD,CAAC;CACF;CAEA,MAAM,KAAK,SAAwC;EAClD,MAAM,SAAS,KAAK;EAOpB,IACC,KAAK,WACL,QAAQ,eAAe,UAAU,WACjC,QAAQ,eAAe,UAAU,QAEjC,MAAM,IAAI,MAAM,sCAAsC;EAEvD,MAAM,OAAO,KAAK,UAAU,OAAO;EAGnC,IAAI,WAAW,KAAA,KAAa,OAAO,eAAe,UAAU,MAAM,OAAO,KAAK,IAAI;OAC7E,KAAK,OAAO,KAAK,IAAI;CAC3B;CAEA,MAAM,QAAuB;EAC5B,IAAI,KAAK,SAAS;EAClB,KAAK,UAAU;EAIf,KAAK,SAAS,CAAC;EACf,MAAM,SAAS,KAAK;EACpB,MAAM,UAAU,KAAK;EACrB,KAAK,kBAAkB;EACvB,KAAK,SAAS;EACd,KAAK,UAAU,KAAA;EACf,IAAI,WAAW,KAAA,GAAW,OAAO,MAAM;EACvC,KAAK,SAAS,KAAK,OAAO;EAC1B,UAAU;CACX;CAIA,MAAM,QAAyB;EAC9B,OAAO,iBAAiB,WAAW,KAAK,MAAM;EAC9C,OAAO,iBAAiB,SAAS,KAAK,OAAO;EAC7C,OAAO,iBAAiB,SAAS,KAAK,QAAQ;CAC/C;CAKA,WAAiB;EAChB,MAAM,SAAS,KAAK;EACpB,IAAI,WAAW,KAAA,GAAW;EAC1B,OAAO,oBAAoB,WAAW,KAAK,MAAM;EACjD,OAAO,oBAAoB,SAAS,KAAK,OAAO;EAChD,OAAO,oBAAoB,SAAS,KAAK,QAAQ;CAClD;CAEA,oBAA0B;EACzB,MAAM,SAAS,KAAK;EACpB,IAAI,WAAW,KAAA,GAAW;EAC1B,OAAO,oBAAoB,QAAQ,KAAK,QAAQ;EAChD,OAAO,oBAAoB,SAAS,KAAK,UAAU;EACnD,KAAK,aAAa,KAAA;EAClB,KAAK,WAAW,KAAA;EAChB,KAAK,UAAU,KAAA;CAChB;CAGA,OAAO,QAAyB;EAC/B,KAAK,MAAM,QAAQ,KAAK,OAAO,OAAO,CAAC,GAAG,OAAO,KAAK,IAAI;CAC3D;CAEA,UAAgB;EACf,MAAM,SAAS,KAAK;EACpB,MAAM,UAAU,KAAK;EACrB,IAAI,WAAW,KAAA,KAAa,YAAY,KAAA,GAAW;EACnD,KAAK,kBAAkB;EACvB,KAAK,OAAO,MAAM;EAClB,QAAQ;CACT;CAEA,oBAA0B;EACzB,MAAM,SAAS,KAAK;EACpB,MAAM,SAAS,KAAK;EACpB,IAAI,WAAW,KAAA,KAAa,WAAW,KAAA,KAAa,OAAO,eAAe,UAAU,MAAM;EAC1F,KAAK,kBAAkB;EACvB,KAAK,SAAS;EACd,KAAK,UAAU,KAAA;EACf,uBAAO,IAAI,MAAM,6BAA6B,CAAC;CAChD;CAMA,SAAS,MAAqB;EAC7B,IAAI,CAAC,SAAS,IAAI,GAAG;GACpB,KAAK,SAAS,KAAK,yBAAS,IAAI,MAAM,0BAA0B,CAAC;GACjE;EACD;EACA,IAAI;EACJ,IAAI;GACH,SAAS,KAAK,MAAM,IAAI;EACzB,SAAS,OAAO;GACf,KAAK,SAAS,KAAK,SAAS,KAAK;GACjC;EACD;EACA,MAAM,UAAU,oBAAoB,MAAM;EAC1C,IAAI,YAAY,KAAA,GAAW;GAC1B,KAAK,SAAS,KAAK,yBAAS,IAAI,MAAM,8BAA8B,CAAC;GACrE;EACD;EACA,KAAK,SAAS,KAAK,WAAW,OAAO;CACtC;CAKA,WAAiB;EAChB,IAAI,KAAK,SAAS;EAClB,KAAK,UAAU;EAEf,KAAK,SAAS,CAAC;EACf,KAAK,SAAS;EACd,KAAK,UAAU,KAAA;EACf,KAAK,SAAS,KAAK,OAAO;CAC3B;AACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AClNA,SAAgB,+BACf,SAC8B;CAC9B,OAAO,IAAI,yBAAyB,OAAO;AAC5C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyCA,SAAgB,0BACf,SAC8B;CAC9B,OAAO,IAAI,oBAAoB,OAAO;AACvC;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BA,SAAgB,2BACf,SACwB;CACxB,OAAO,IAAI,qBAAqB,OAAO;AACxC;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,SAAgB,qBAAqB,OAAwD;CAC5F,IAAI;CACJ,IAAI;CACJ,OAAO;EACN,KAAK,SAAuB;GAC3B,MAAM,YAAY,OAAO;EAC1B;EACA,OAAO,SAA0C;GAChD,YAAY;EACb;EACA,OAAO,SAA2B;GACjC,WAAW;EACZ;EACA,QAAc;GACb,WAAW;EACZ;EACA,QAAQ,SAAuB;GAC9B,YAAY,OAAO;EACpB;CACD;AACD;;;;;;;;;;;;;;;;AC/HA,SAAgB,YAAY,MAA0C;CACrE,IAAI;EACH,OAAO,oBAAoB,KAAK,MAAM,IAAI,CAAC;CAC5C,QAAQ;EACP;CACD;AACD;;;;;;;;;;;;;;;;;;;AAoBA,eAAsB,gBAAgB,UAAwD;CAC7F,MAAM,OAAO,SAAS;CACtB,IAAI,SAAS,MAAM,OAAO,CAAC;CAC3B,MAAM,SAAS,KAAK,UAAU;CAC9B,MAAM,UAAU,IAAI,YAAY;CAChC,MAAM,SAA6B,gBAAgB;CACnD,MAAM,WAA6B,CAAC;CACpC,IAAI;EACH,SAAS;GACR,MAAM,EAAE,MAAM,UAAU,MAAM,OAAO,KAAK;GAC1C,IAAI,MAAM;GACV,KAAK,MAAM,SAAS,OAAO,MAAM,QAAQ,OAAO,OAAO,EAAE,QAAQ,KAAK,CAAC,CAAC,GAAG;IAC1E,MAAM,UAAU,YAAY,MAAM,IAAI;IACtC,IAAI,YAAY,KAAA,GAAW,SAAS,KAAK,OAAO;GACjD;EACD;CACD,UAAU;EACT,OAAO,YAAY;CACpB;CACA,OAAO;AACR;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4CA,SAAgB,2BACf,QACA,gBACA,WACA,SACgC;CAChC,QAAQ,UAA8B;EACrC,MAAM,QAAQ,MAAM;EACpB,IAAI,MAAM,SAAS,GAAG;GAErB,IAAI,QAAQ,WAAW,KAAA,KAAa,CAAC,QAAQ,OAAO,KAAK,GAAG;GAC5D,MAAM,OAAO,MAAM;GACnB,IAAI,SAAS,KAAA,GAAW;GAIxB,IAAI,UAAU,IAAI,IAAI,GAAG;GACzB,MAAM,YAAY,IAAI,qBAAqB,EAAE,KAAK,CAAC;GACnD,MAAM,SAAS,WAAW,QAAQ,SAAS;GAC3C,UAAU,IAAI,YAAY;IACzB,OAAO;IACP,UAAU,MAAM;GACjB,CAAC;GACD;EACD;EACA,IAAI,SAAS,MAAM,IAAI,GAAG,eAAe,QAAQ,MAAM,IAAI;CAC5D;AACD;;;;;;;;;;;;;;;;;AAkBA,SAAgB,cAAc,OAA+B,SAAsC;CAClG,MAAM,SAAS,gBAAgB;EAC9B,OAAO,QAAQ;EACf,UAAU;GACT,MAAM,QAAQ,QAAA;GACd,SAAS,QAAQ,WAAA;EAClB;CACD,CAAC;CACD,MAAM,iBAAiB,qBAAqB,KAAK;CACjD,MAAM,cAAc,WAAW,QAAQ,cAAc;CACrD,MAAM,4BAAY,IAAI,IAA6B;CACnD,MAAM,YAAY,2BAA2B,QAAQ,gBAAgB,WAAW,OAAO;CACvF,MAAM,iBAAiB,WAAW,SAAS;CAC3C,IAAI,WAAW;CACf,aAAa;EACZ,IAAI,UAAU;EACd,WAAW;EACX,MAAM,oBAAoB,WAAW,SAAS;EAC9C,YAAY;EACZ,KAAK,MAAM,YAAY,UAAU,OAAO,GAAG,SAAS;EAGpD,UAAU,MAAM;CACjB;AACD;;;;;;;;;;;AAYA,SAAgB,SAAS,SAAsC;CAC9D,OAAO,cAAc,YAAY,OAAO;AACzC"}