@orkestrel/mcp 0.0.26 → 0.0.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,214 +1,22 @@
1
- import { bindServer, createMCPServer, inferRequestVersion, isJSONRPCResponse, isMCPVersion, isModernRequest, parseJSONRPCMessage } from "../core/index.js";
2
- import { isRecord, isString } from "@orkestrel/contract";
3
- import { createSSEParser } from "@orkestrel/sse";
1
+ import { isString } from "@orkestrel/contract";
2
+ import { HTTPClientTransport, MCP_WEBSOCKET_SUBPROTOCOL, bindServer, createMCPServer, deliverMessage } from "../core/index.js";
4
3
  import { Emitter } from "@orkestrel/emitter";
5
4
  //#region src/browser/constants.ts
6
- /**
7
- * The Streamable-HTTP transport header that carries the MCP session id. The browser
8
- * face's {@link import('./transports/HTTPClientTransport.js').HTTPClientTransport}
9
- * ECHOES this header exactly like the Node face's `HTTPClientTransport`
10
- * (`src/server`), so the same client interoperates with an `MCPSession`-based
11
- * server unchanged.
12
- */
13
- var MCP_SESSION_HEADER = "mcp-session-id";
14
- /**
15
- * The Streamable-HTTP transport header carrying the MCP protocol version. Modern
16
- * requests derive it from their own `_meta`; legacy requests echo the negotiated
17
- * initialize result on each subsequent request.
18
- */
19
- var MCP_PROTOCOL_VERSION_HEADER = "mcp-protocol-version";
20
- /**
21
- * The modern Streamable-HTTP request header carrying the JSON-RPC method. It is
22
- * emitted on every modern request and never on a legacy request.
23
- */
24
- var MCP_METHOD_HEADER = "mcp-method";
25
- /**
26
- * The modern Streamable-HTTP request header carrying a named target. The browser
27
- * HTTP client emits it only for `tools/call`, from that request's `params.name`.
28
- */
29
- var MCP_NAME_HEADER = "mcp-name";
30
- /** The default server name `serveMCPScope` reports (`initialize`'s `serverInfo.name`) when `options.name` is omitted. */
31
- var DEFAULT_MCP_SERVER_NAME = "taverna";
32
- /** The default server version `serveMCPScope` reports (`initialize`'s `serverInfo.version`) when `options.version` is omitted. */
5
+ /** Supplies the default server name `createScopeServer` reports (`initialize`'s `serverInfo.name`) when `options.name` is omitted. */
6
+ var DEFAULT_MCP_SERVER_NAME = "@orkestrel/mcp";
7
+ /** Supplies the default server version `createScopeServer` reports (`initialize`'s `serverInfo.version`) when `options.version` is omitted. */
33
8
  var DEFAULT_MCP_SERVER_VERSION = "1.0.0";
34
- /**
35
- * The WebSocket subprotocol `createWebSocketClientTransport` requests by default —
36
- * `'mcp'`, which `createWebSocketServer` selects when the client offers it. Per RFC 6455
37
- * §4.1 a client MUST fail the connection if the server returns
38
- * a subprotocol it did not request; Node ≥ 22 (undici) enforces this strictly, so the
39
- * default bakes the correct value in. Override `WebSocketClientTransportOptions.protocols`
40
- * only when connecting to a foreign server that speaks a different subprotocol (or `[]`
41
- * for no subprotocol negotiation at all).
42
- */
43
- var MCP_WEBSOCKET_SUBPROTOCOL = "mcp";
44
- //#endregion
45
- //#region src/browser/transports/HTTPClientTransport.ts
46
- /**
47
- * The browser-face HTTP CLIENT transport for the Model Context Protocol — a
48
- * {@link MCPClientTransportInterface} that drives a REMOTE Streamable-HTTP MCP server
49
- * over the native `fetch`, the browser sibling of the Node face's
50
- * {@link import('@orkestrel/mcp/server').HTTPClientTransport}, honoring the SAME
51
- * `mcp-session-id` semantics so it interoperates with an `MCPSession`-based server
52
- * unchanged.
53
- *
54
- * @remarks
55
- * - **Request/response over `fetch`.** `send(message)` POSTs the JSON-serialized
56
- * message to `options.url` with `content-type: application/json` and an
57
- * `Accept` of BOTH `application/json` and `text/event-stream` (so the server may
58
- * answer with either framing) — plus any `options.headers` (for example, an
59
- * `Authorization` bearer). It then decodes the reply and emits each decoded
60
- * {@link JSONRPCMessage} on the `message` event the
61
- * {@link import('@orkestrel/mcp').MCPClientInterface} subscribes to.
62
- * - **Both reply framings.** A `200` with an `application/json` body is parsed with
63
- * `parseJSONRPCMessage`; a `200` with a `text/event-stream` body is decoded with the
64
- * `@orkestrel/sse` {@link import('@orkestrel/sse').SSEParserInterface} (the browser
65
- * face's own `readEventStream`) — the inverse of the server's `openStream` seam, so
66
- * the wire round-trips. A `202` Accepted (a notification) carries no body and emits
67
- * nothing.
68
- * - **Session and protocol headers.** `start()` is a no-op (a
69
- * request/response transport opens no long-lived connection). The
70
- * `mcp-session-id` response header, when a STATEFUL server sends one (on
71
- * `initialize`), is captured into `session` and then ECHOED as the
72
- * `mcp-session-id` request header on every SUBSEQUENT request — so an
73
- * `MCPClient` passes a stateful server's session validation. The
74
- * initialize result's `protocolVersion` is likewise captured, but only
75
- * when it is a SUPPORTED value, and echoed as `mcp-protocol-version` alone on
76
- * subsequent legacy requests. Modern requests instead derive protocol and method
77
- * headers from the message, plus the name header only for `tools/call`.
78
- * Before initialize returns, neither captured legacy header is sent.
79
- * `close()` clears the captured protocol so a reconnect's `initialize`
80
- * POST is headerless; the captured `session` persists across `close()`.
81
- * - **`close()` releases what is in flight.** Every `fetch` this transport still has open is
82
- * ABORTED, which cancels the response body a `send` is reading — an SSE reply the server
83
- * never ends would otherwise outlive the transport, with nothing left able to reach it. The
84
- * aborted read surfaces on `error` and the `send` reporting it resolves. `close()` is
85
- * idempotent (one `close` event per connected lifetime), and `start()` opens the next one.
86
- * - **Total at the boundary.** Every reply is narrowed (`parseJSONRPCMessage`,
87
- * the SSE decoder) — a non-message reply is dropped, never asserted; a `fetch` /
88
- * decode failure surfaces on the `error` event rather than escaping `send`.
89
- * - **Observable.** Owns the `emitter` ({@link MCPClientTransportEventMap}); fires
90
- * `message` per decoded reply, `error` on a fault, and `close` on `close()`.
91
- *
92
- * @example
93
- * ```ts
94
- * const transport = new HTTPClientTransport({ url: 'http://localhost:3000/mcp' })
95
- * const client = new MCPClient({ transport })
96
- * await client.connect()
97
- * ```
98
- */
99
- var HTTPClientTransport = class {
100
- #emitter;
101
- #url;
102
- #headers;
103
- #fetch;
104
- #timeout;
105
- #pending = /* @__PURE__ */ new Set();
106
- #session = void 0;
107
- #protocol = void 0;
108
- #closed = false;
109
- constructor(options) {
110
- this.#emitter = new Emitter();
111
- this.#url = options.url;
112
- this.#headers = options.headers ?? {};
113
- this.#fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
114
- this.#timeout = options.timeout;
115
- }
116
- get emitter() {
117
- return this.#emitter;
118
- }
119
- get session() {
120
- return this.#session;
121
- }
122
- get duplex() {
123
- return false;
124
- }
125
- async start() {
126
- this.#closed = false;
127
- }
128
- async send(message) {
129
- const request = new AbortController();
130
- this.#pending.add(request);
131
- try {
132
- await this.#exchange(message, request.signal);
133
- } finally {
134
- this.#pending.delete(request);
135
- }
136
- }
137
- async #exchange(message, signal) {
138
- let response;
139
- try {
140
- response = await this.#fetch(this.#url, {
141
- method: "POST",
142
- headers: {
143
- "content-type": "application/json",
144
- accept: "application/json, text/event-stream",
145
- ...this.#session === void 0 ? {} : { [MCP_SESSION_HEADER]: this.#session },
146
- ...this.#buildHeaders(message),
147
- ...this.#headers
148
- },
149
- body: JSON.stringify(message),
150
- signal: this.#timeout === void 0 ? signal : AbortSignal.any([signal, AbortSignal.timeout(this.#timeout)])
151
- });
152
- } catch (error) {
153
- this.#emitter.emit("error", error);
154
- return;
155
- }
156
- const session = response.headers.get(MCP_SESSION_HEADER);
157
- if (session !== null) this.#session = session;
158
- await this.#deliver(response);
159
- }
160
- async close() {
161
- if (this.#closed) return;
162
- this.#closed = true;
163
- for (const request of this.#pending) request.abort();
164
- this.#pending.clear();
165
- this.#protocol = void 0;
166
- this.#emitter.emit("close");
167
- }
168
- #buildHeaders(message) {
169
- if (isModernRequest(message)) {
170
- const version = inferRequestVersion(message);
171
- const name = message.params?.["name"];
172
- return {
173
- ...version === void 0 ? {} : { [MCP_PROTOCOL_VERSION_HEADER]: version },
174
- [MCP_METHOD_HEADER]: message.method,
175
- ...message.method === "tools/call" && isString(name) ? { [MCP_NAME_HEADER]: name } : {}
176
- };
177
- }
178
- return this.#protocol === void 0 ? {} : { [MCP_PROTOCOL_VERSION_HEADER]: this.#protocol };
179
- }
180
- async #deliver(response) {
181
- if (response.status === 202) return;
182
- const type = response.headers.get("content-type") ?? "";
183
- try {
184
- if (type.includes("text/event-stream")) {
185
- for (const message of await readEventStream(response)) this.#capture(message);
186
- return;
187
- }
188
- if (type.includes("application/json")) {
189
- const message = parseJSONRPCMessage(await response.json());
190
- if (message !== void 0) this.#capture(message);
191
- }
192
- } catch (error) {
193
- this.#emitter.emit("error", error);
194
- }
195
- }
196
- #capture(message) {
197
- if (isJSONRPCResponse(message) && isRecord(message.result) && isMCPVersion(message.result["protocolVersion"])) this.#protocol = message.result["protocolVersion"];
198
- this.#emitter.emit("message", message);
199
- }
200
- };
201
9
  //#endregion
202
10
  //#region src/browser/transports/MessagePortTransport.ts
203
11
  /**
204
- * The browser-face `MessagePort` transport for the Model Context Protocol — a
205
- * {@link MCPTransportInterface} over a native `MessagePort`, the genuinely new
206
- * capability this face adds: MCP over `postMessage`.
12
+ * Carries the Model Context Protocol over a native `MessagePort` from the browser face — a
13
+ * {@link MCPTransportInterface}, the genuinely new capability this face adds: MCP over
14
+ * `postMessage`.
207
15
  *
208
16
  * @remarks
209
17
  * - **Symmetric.** Unlike {@link import('./WebSocketClientTransport.js').WebSocketClientTransport}
210
- * / {@link import('./HTTPClientTransport.js').HTTPClientTransport} (CLIENT-only
211
- * carriers of `@orkestrel/mcp`'s `MCPClientTransportInterface`), a `MessagePort` is a
18
+ * / {@link import('@orkestrel/mcp').HTTPClientTransport} (CLIENT-only
19
+ * carriers of `@orkestrel/mcp`'s `MCPMessageTransportInterface`), a `MessagePort` is a
212
20
  * plain duplex channel — the SAME class implements `@orkestrel/mcp`'s
213
21
  * `MCPTransportInterface` and is handed to EITHER `bindServer` or
214
22
  * `bindClient`/`createDuplexClientTransport`; which role it plays comes entirely
@@ -219,7 +27,7 @@ var HTTPClientTransport = class {
219
27
  * `MCPTransportInterface` has no separate open/connect step for the caller to hook
220
28
  * a start into, so the constructor calls `port.start()` immediately: the port
221
29
  * begins dispatching QUEUED messages the moment the transport exists. This is safe
222
- * inside `serveMCP`'s flow (the transport is synchronously handed to `bindServer`
30
+ * inside `createScopeServer`'s flow (the transport is synchronously handed to `bindServer`
223
31
  * before control returns to the event loop), but is a **footgun for direct use**:
224
32
  * if you construct `new MessagePortTransport({ port })` and then `await` anything
225
33
  * before calling `listen`, messages that arrived in the gap are DROPPED. **Bind
@@ -231,16 +39,17 @@ var HTTPClientTransport = class {
231
39
  * non-string `event.data` (a host or a misbehaving peer posting a structured
232
40
  * object) is IGNORED — dropped silently, never forwarded, never thrown —
233
41
  * because `MCPTransportInterface` carries no `error` channel for this port to
234
- * surface a non-string frame on (unlike `MCPClientTransportInterface`'s `emitter`);
42
+ * surface a non-string frame on (unlike `MCPMessageTransportInterface`'s `emitter`);
235
43
  * silently ignoring is the total, contract-shaped choice.
236
44
  * - **`messageerror` is IGNORED, not routed to `closed`.** A `messageerror` event
237
45
  * (the structured-clone deserialization of an inbound message threw) reports one
238
46
  * BAD FRAME, not a dead channel — the port itself keeps working and later, well-
239
- * formed messages still arrive. Routing it to `closed` would tear down the
47
+ * formed messages still arrive. This transport registers no listener for it: an
48
+ * unhandled `messageerror` on a `MessagePort` neither throws, closes the port, nor
49
+ * reaches this transport, so one bad frame costs exactly that frame and nothing
50
+ * tears the binding down. Routing it to `closed` would tear down the
240
51
  * `bindServer`/`bindClient` wiring (and, transitively, every session it carries)
241
- * over a single malformed frame, which is far more destructive than dropping that
242
- * one frame — so this transport registers a `messageerror` listener that does
243
- * nothing, deliberately.
52
+ * over a single malformed frame.
244
53
  * - **`close()`** is idempotent: it closes the underlying `port` (`MessagePort.close()`
245
54
  * disconnects it — further `postMessage` calls on EITHER end are silently
246
55
  * undelivered, per the platform contract) and fires the registered `closed`
@@ -265,14 +74,12 @@ var HTTPClientTransport = class {
265
74
  var MessagePortTransport = class {
266
75
  #port;
267
76
  #message = (event) => this.#receive(event.data);
268
- #malformed = () => {};
269
77
  #onMessage = void 0;
270
78
  #onClosed = void 0;
271
79
  #closed = false;
272
80
  constructor(options) {
273
81
  this.#port = options.port;
274
82
  this.#port.addEventListener("message", this.#message);
275
- this.#port.addEventListener("messageerror", this.#malformed);
276
83
  this.#port.start();
277
84
  }
278
85
  send(message) {
@@ -292,7 +99,6 @@ var MessagePortTransport = class {
292
99
  this.#onMessage = void 0;
293
100
  this.#onClosed = void 0;
294
101
  this.#port.removeEventListener("message", this.#message);
295
- this.#port.removeEventListener("messageerror", this.#malformed);
296
102
  this.#port.close();
297
103
  onClosed?.();
298
104
  }
@@ -304,9 +110,8 @@ var MessagePortTransport = class {
304
110
  //#endregion
305
111
  //#region src/browser/transports/WebSocketClientTransport.ts
306
112
  /**
307
- * The browser-face WebSocket CLIENT transport for the Model Context Protocol — a
308
- * {@link MCPClientTransportInterface} that drives a REMOTE MCP server over the native
309
- * `WebSocket` global, the browser sibling of the Node face's
113
+ * Drives a REMOTE MCP server over the native `WebSocket` global from the browser face — a
114
+ * CLIENT {@link MCPMessageTransportInterface}, the browser sibling of the Node face's
310
115
  * {@link import('@orkestrel/mcp/server').WebSocketClientTransport}.
311
116
  *
312
117
  * @remarks
@@ -318,9 +123,15 @@ var MessagePortTransport = class {
318
123
  * - **Queued sends.** `send` writes each message as one text frame immediately once
319
124
  * the socket is `OPEN`; a `send` issued before `'open'` fires (or before `start()`
320
125
  * 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`.
322
- * - **Inbound (`message`).** Each decoded text frame is `JSON.parse`d (guarded) and
323
- * narrowed with `parseJSONRPCMessage` a well-formed {@link JSONRPCMessage}
126
+ * so a caller need not await `start()` before calling `send`. A queue rides ONE
127
+ * connection: a close DISCARDS whatever is still in it.
128
+ * - **A closed channel REJECTS.** The native socket confirms nothing about a write, so this
129
+ * transport answers from its own state: a `send` after `close()`, or on a socket already
130
+ * reporting `CLOSING` / `CLOSED`, REJECTS with `WebSocket transport is not connected` rather
131
+ * than resolving on a frame nobody wrote. Only the closed state rejects — a pre-open `send`
132
+ * still queues.
133
+ * - **Inbound (`message`).** Each decoded text frame runs through the shared
134
+ * `deliverMessage` fold (parse, then narrow) — a well-formed {@link JSONRPCMessage}
324
135
  * re-emits on this transport's `message` event; a non-text (binary) frame or a
325
136
  * non-JSON / non-message text frame surfaces on `error` and is DROPPED (never
326
137
  * throws on adversarial wire input).
@@ -329,9 +140,11 @@ var MessagePortTransport = class {
329
140
  * SAME `close` exactly once total — `close()` first flips the guard, so the native event
330
141
  * never double-emits, and the released socket reports its own close to nobody. Closing before
331
142
  * 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.
334
- * - **Observable.** Owns the `emitter` ({@link MCPClientTransportEventMap}); every
143
+ * Node face. A `send` issued after `close()` REJECTS (it is never queued), and the
144
+ * pre-open queue is DISCARDED by `close()` and by the native `close` event alike — so a
145
+ * closed transport delivers nothing until a `start()` opens a new connection, and nothing
146
+ * the caller handed the abandoned connection rides that one.
147
+ * - **Observable.** Owns the `emitter` ({@link MCPMessageTransportEventMap}); every
335
148
  * emit the emitter isolates a listener throw; `error` is a DOMAIN event (a
336
149
  * transport-level fault).
337
150
  *
@@ -361,7 +174,7 @@ var WebSocketClientTransport = class {
361
174
  this.#emitter = new Emitter();
362
175
  this.#url = options.url;
363
176
  const protocols = options.protocols;
364
- this.#protocols = typeof protocols === "string" ? protocols : protocols === void 0 ? "mcp" : protocols.length === 0 ? void 0 : [...protocols];
177
+ this.#protocols = typeof protocols === "string" ? protocols : protocols === void 0 ? MCP_WEBSOCKET_SUBPROTOCOL : protocols.length === 0 ? void 0 : [...protocols];
365
178
  }
366
179
  get emitter() {
367
180
  return this.#emitter;
@@ -385,15 +198,16 @@ var WebSocketClientTransport = class {
385
198
  });
386
199
  }
387
200
  async send(message) {
388
- if (this.#closed) return;
389
- const text = JSON.stringify(message);
390
201
  const socket = this.#socket;
202
+ if (this.#closed || socket?.readyState === WebSocket.CLOSING || socket?.readyState === WebSocket.CLOSED) throw new Error("WebSocket transport is not connected");
203
+ const text = JSON.stringify(message);
391
204
  if (socket !== void 0 && socket.readyState === WebSocket.OPEN) socket.send(text);
392
205
  else this.#queue.push(text);
393
206
  }
394
207
  async close() {
395
208
  if (this.#closed) return;
396
209
  this.#closed = true;
210
+ this.#queue = [];
397
211
  const socket = this.#socket;
398
212
  const resolve = this.#resolve;
399
213
  this.#releaseHandshake();
@@ -449,23 +263,12 @@ var WebSocketClientTransport = class {
449
263
  this.#emitter.emit("error", /* @__PURE__ */ new Error("non-text WebSocket frame"));
450
264
  return;
451
265
  }
452
- let parsed;
453
- try {
454
- parsed = JSON.parse(data);
455
- } catch (error) {
456
- this.#emitter.emit("error", error);
457
- return;
458
- }
459
- const message = parseJSONRPCMessage(parsed);
460
- if (message === void 0) {
461
- this.#emitter.emit("error", /* @__PURE__ */ new Error("non-JSON-RPC WebSocket frame"));
462
- return;
463
- }
464
- this.#emitter.emit("message", message);
266
+ deliverMessage(this.#emitter, data, "non-JSON-RPC WebSocket frame");
465
267
  }
466
268
  #onClose() {
467
269
  if (this.#closed) return;
468
270
  this.#closed = true;
271
+ this.#queue = [];
469
272
  this.#release();
470
273
  this.#socket = void 0;
471
274
  this.#emitter.emit("close");
@@ -475,7 +278,7 @@ var WebSocketClientTransport = class {
475
278
  //#region src/browser/factories.ts
476
279
  /**
477
280
  * Creates the browser-face WebSocket CLIENT transport for an
478
- * {@link import('@orkestrel/mcp').MCPClientInterface} — a {@link MCPClientTransportInterface}
281
+ * {@link import('@orkestrel/mcp').MCPClientInterface} — a {@link MCPMessageTransportInterface}
479
282
  * that drives a REMOTE MCP server over the native `WebSocket` global, the browser
480
283
  * sibling of the Node face's `createWebSocketClientTransport` (`@orkestrel/mcp/server`).
481
284
  *
@@ -490,7 +293,7 @@ var WebSocketClientTransport = class {
490
293
  * @param options - `url` (the remote WebSocket endpoint; REQUIRED) and optional
491
294
  * `protocols` (the WebSocket subprotocol(s) to request); see
492
295
  * {@link WebSocketClientTransportOptions}
493
- * @returns A working {@link MCPClientTransportInterface} over the native `WebSocket`
296
+ * @returns A working {@link MCPMessageTransportInterface} over the native `WebSocket`
494
297
  *
495
298
  * @example
496
299
  * ```ts
@@ -508,10 +311,15 @@ function createWebSocketClientTransport(options) {
508
311
  return new WebSocketClientTransport(options);
509
312
  }
510
313
  /**
511
- * Creates the browser-face HTTP CLIENT transport for an
512
- * {@link import('@orkestrel/mcp').MCPClientInterface} — a {@link MCPClientTransportInterface}
513
- * that drives a REMOTE Streamable-HTTP MCP server over the native `fetch`, the
514
- * browser sibling of the Node face's `createHTTPClientTransport` (`@orkestrel/mcp/server`).
314
+ * Creates the HTTP CLIENT transport for an
315
+ * {@link import('@orkestrel/mcp').MCPClientInterface} — a {@link MCPMessageTransportInterface}
316
+ * that drives a REMOTE Streamable-HTTP MCP server over the native `fetch`.
317
+ *
318
+ * @remarks
319
+ * It returns the core {@link import('@orkestrel/mcp').HTTPClientTransport}, the same class the
320
+ * Node face's `createHTTPClientTransport` returns, because the class touches `fetch`,
321
+ * `Response`, `AbortController`, `AbortSignal`, and `WeakMap` alone. This factory exists so a
322
+ * page imports its transport from the face it already imports everything else from.
515
323
  *
516
324
  * @remarks
517
325
  * Hand it to `createMCPClient({ transport })`: each JSON-RPC message the client
@@ -532,7 +340,7 @@ function createWebSocketClientTransport(options) {
532
340
  * onto every request, optional `fetch` (default `globalThis.fetch`), and optional
533
341
  * `timeout` (ms, applied with `AbortSignal.timeout`); see
534
342
  * {@link HTTPClientTransportOptions}
535
- * @returns A working {@link MCPClientTransportInterface} over the native `fetch`
343
+ * @returns A working {@link MCPMessageTransportInterface} over the native `fetch`
536
344
  *
537
345
  * @example
538
346
  * ```ts
@@ -580,116 +388,65 @@ function createMessagePortTransport(options) {
580
388
  return new MessagePortTransport(options);
581
389
  }
582
390
  /**
583
- * Adapts a hostable {@link ServeMCPScopeInterface} (`self` in a dedicated Web Worker,
584
- * or any structurally matching double) into a {@link ScopeTransportInterface} the
585
- * implicit, portless message channel `serveMCPScope` binds for the
586
- * dedicated-worker shape.
391
+ * Creates an `MCPServer` hosted inside a worker scope and wires that scope's message events
392
+ * to it the browser face's bootstrap, and the twin of the Node face's `createStdioServer`.
587
393
  *
588
394
  * @remarks
589
- * `send` writes each outbound string through `scope.postMessage`. `listen`/`closed`
590
- * register the SINGLE handler `deliver` / the underlying close path route through
591
- * `serveMCPScope`'s own `scope` `message`-event listener calls `deliver(event.data)`
592
- * for every portless, string-payload event (there is no native registration point on
593
- * the scope itself for `serveMCPScope` to hand a `listen` handler to, so `deliver` is
594
- * the bridge). `close()` fires the registered `closed` handler — a scope has nothing
595
- * physically closable, so this is the only teardown signal available.
395
+ * `scope` defaults to `globalThis`, which is `self` inside a dedicated Web Worker or a
396
+ * Service Worker, so a worker boots with `createScopeServer({ tools })` alone; pass a scope
397
+ * explicitly to host a server on a double or on another message-event-bearing object.
596
398
  *
597
- * @param scope - The hostable scope to adapt (structurally, `self` / `globalThis`
598
- * inside a dedicated Web Worker)
599
- * @returns A {@link ScopeTransportInterface} `serveMCPScope` binds and drives through `deliver`
399
+ * Port-bearing events are gated by `options.accept`, deduplicated by port, and receive
400
+ * their own `MessagePortTransport` binding. Portless string events use the scope's
401
+ * implicit channel. The returned handle's `stop` removes the listener, unbinds the implicit
402
+ * channel, closes every accepted port binding, and drops the ports themselves — the
403
+ * bindings are held in one map keyed by port, so nothing survives the clear. The served
404
+ * endpoint is modern-only: it answers a legacy `initialize` with `-32601`. A dual-era
405
+ * worker composes `bindServer(createMCPLegacy(mcp), …)` instead of this factory.
406
+ *
407
+ * @param options - The tools, optional identity, and optional port-event gate; see
408
+ * {@link ScopeServerOptions}
409
+ * @param scope - The hostable scope to wire; defaults to `globalThis`
410
+ * @returns A {@link ScopeServerInterface} whose `stop` ends every binding this call owns
600
411
  *
601
412
  * @example
602
413
  * ```ts
603
- * const scopeTransport = createScopeTransport(self)
604
- * const unbind = bindServer(server, scopeTransport)
605
- * ```
606
- */
607
- function createScopeTransport(scope) {
608
- let onMessage;
609
- let onClosed;
610
- return {
611
- send(message) {
612
- scope.postMessage(message);
613
- },
614
- listen(handler) {
615
- onMessage = handler;
616
- },
617
- closed(handler) {
618
- onClosed = handler;
619
- },
620
- close() {
621
- onClosed?.();
622
- },
623
- deliver(message) {
624
- onMessage?.(message);
625
- }
626
- };
627
- }
628
- //#endregion
629
- //#region src/browser/helpers.ts
630
- /**
631
- * Decodes one SSE event's `data` string into a {@link JSONRPCMessage}, or `undefined`
632
- * when it is not one — the per-event step {@link readEventStream} folds over.
414
+ * import { createScopeServer } from '@orkestrel/mcp/browser'
415
+ * import { createToolManager } from '@orkestrel/tool'
633
416
  *
634
- * @remarks
635
- * `JSON.parse`s the `data` (the server serializes the JSON-RPC envelope as the
636
- * event's `data`) inside a try/catch and narrows the parsed value with
637
- * `parseJSONRPCMessage`. Total: malformed JSON or a non-message value yields
638
- * `undefined`, never throws.
639
- *
640
- * @param data - One SSE event's `data` payload
641
- * @returns The decoded {@link JSONRPCMessage}, or `undefined`
642
- */
643
- function decodeEvent(data) {
644
- try {
645
- return parseJSONRPCMessage(JSON.parse(data));
646
- } catch {
647
- return;
648
- }
649
- }
650
- /**
651
- * Decodes a `fetch` Response's Server-Sent-Events body into the JSON-RPC messages it
652
- * carried — the CLIENT-side inverse of the server's Streamable-HTTP SSE response.
653
- *
654
- * @remarks
655
- * Reads the whole `response.body` stream chunk-by-chunk through a `TextDecoder({
656
- * stream: true })` (handling a multi-byte char split across reads) and
657
- * `@orkestrel/sse`'s {@link SSEParserInterface} (handling a partial line / in-progress
658
- * event split across reads), then narrows each dispatched event's `data` to a
659
- * {@link JSONRPCMessage} through {@link decodeEvent} (so a non-message / non-JSON `data:`
660
- * event is DROPPED, never thrown — total). A `null` body (no stream) yields no
661
- * messages; {@link import('./transports/HTTPClientTransport.js').HTTPClientTransport}
662
- * reads a request/response SSE reply (the server sends one `data:` event then ends),
663
- * so this drains to completion.
664
- *
665
- * @param response - The SSE `fetch` Response to decode (its `body` is read to completion)
666
- * @returns Every {@link JSONRPCMessage} the stream carried, in order
417
+ * // Inside a Web Worker: the scope defaults to `globalThis`.
418
+ * const worker = createScopeServer({ tools: createToolManager() })
419
+ * // ... later, release every binding this call owns:
420
+ * worker.stop()
421
+ * ```
667
422
  */
668
- async function readEventStream(response) {
669
- const body = response.body;
670
- if (body === null) return [];
671
- const reader = body.getReader();
672
- const decoder = new TextDecoder();
673
- const parser = createSSEParser();
674
- const messages = [];
675
- try {
676
- for (;;) {
677
- const { done, value } = await reader.read();
678
- if (done) break;
679
- for (const event of parser.parse(decoder.decode(value, { stream: true }))) {
680
- const message = decodeEvent(event.data);
681
- if (message !== void 0) messages.push(message);
682
- }
423
+ function createScopeServer(options, scope = globalThis) {
424
+ const server = createMCPServer({
425
+ tools: options.tools,
426
+ identity: {
427
+ name: options.name ?? "@orkestrel/mcp",
428
+ version: options.version ?? "1.0.0"
683
429
  }
684
- } finally {
685
- reader.releaseLock();
686
- }
687
- return messages;
430
+ });
431
+ const scopeTransport = createScopeTransport(scope);
432
+ const unbindScope = bindServer(server, scopeTransport);
433
+ const teardowns = /* @__PURE__ */ new Map();
434
+ const onMessage = createScopeMessageListener(server, scopeTransport, teardowns, options);
435
+ scope.addEventListener("message", onMessage);
436
+ let stopped = false;
437
+ return { stop() {
438
+ if (stopped) return;
439
+ stopped = true;
440
+ scope.removeEventListener("message", onMessage);
441
+ unbindScope();
442
+ for (const teardown of teardowns.values()) teardown();
443
+ teardowns.clear();
444
+ } };
688
445
  }
689
446
  /**
690
- * Builds `serveMCPScope`'s `message`-event listener — the unified
691
- * dispatcher that routes EVERY inbound event on a hostable scope, portless or
692
- * port-bearing, to the right binding.
447
+ * Builds {@link createScopeServer}'s `message`-event listener — the unified dispatcher that
448
+ * routes EVERY inbound event on a hostable scope, portless or port-bearing, to the right
449
+ * binding.
693
450
  *
694
451
  * @remarks
695
452
  * Port-bearing events (`event.ports.length > 0`) are gated by `options.accept` FIRST
@@ -701,10 +458,10 @@ async function readEventStream(response) {
701
458
  * replies), so a repeat is silently dropped.
702
459
  *
703
460
  * The key is what makes `teardowns` the ONLY place an accepted port is remembered. A separate
704
- * seen-port set would be a second collection over the same lifetime, and the caller's disposer
705
- * would have to remember to empty both — so a long-lived scope such as a Service Worker would
706
- * retain every port it ever accepted, closed and unbound ones included. Membership answers
707
- * "already bound?" and `clear()` drops the binding and the dedup together.
461
+ * seen-port set would be a second collection over the same lifetime, and the scope server's
462
+ * `stop` would have to remember to empty both — so a long-lived scope such as a Service Worker
463
+ * would retain every port it ever accepted, closed and unbound ones included. Membership
464
+ * answers "already bound?" and `clear()` drops the binding and the dedup together.
708
465
  *
709
466
  * This branch fires on EITHER a Service-Worker-shaped scope (its normal per-client
710
467
  * channel) or a dedicated-worker-shaped one that happens to receive a port-bearing event
@@ -715,8 +472,8 @@ async function readEventStream(response) {
715
472
  *
716
473
  * @param server - The `MCPServerInterface` every spawned/implicit binding dispatches over
717
474
  * @param scopeTransport - The implicit scope channel (already `bindServer`-bound) portless events deliver onto
718
- * @param teardowns - The shared teardown map `serveMCPScope`'s dispose drains and clears, keyed by the accepted port; each port-bearing event adds one entry
719
- * @param options - The `ServeMCPOptions` (for `options.accept`)
475
+ * @param teardowns - The shared teardown map the scope server's `stop` drains and clears, keyed by the accepted port; each port-bearing event adds one entry
476
+ * @param options - The `ScopeServerOptions` (for `options.accept`)
720
477
  * @returns The `message`-event listener to register (and later remove) on the scope
721
478
  *
722
479
  * @example
@@ -748,58 +505,51 @@ function createScopeMessageListener(server, scopeTransport, teardowns, options)
748
505
  };
749
506
  }
750
507
  /**
751
- * Boots an `MCPServer` inside a hostable worker scope and wires its message events to it.
508
+ * Adapts a hostable {@link ScopeInterface} (`self` in a dedicated Web Worker, or any
509
+ * structurally matching double) into a {@link ScopeTransportInterface} — the implicit,
510
+ * portless message channel {@link createScopeServer} binds for the dedicated-worker shape.
752
511
  *
753
512
  * @remarks
754
- * Port-bearing events are gated by `options.accept`, deduplicated by port, and receive
755
- * their own `MessagePortTransport` binding. Portless string events use the scope's
756
- * implicit channel. The returned disposer removes the listener, unbinds the implicit
757
- * channel, closes every accepted port binding, and drops the ports themselves the
758
- * bindings are held in one map keyed by port, so nothing survives the clear. The served
759
- * endpoint is modern-only: it answers a legacy `initialize` with `-32601`. A dual-era
760
- * worker composes `bindServer(createMCPLegacy(mcp), …)` instead of this function.
513
+ * `send` writes each outbound string through `scope.postMessage`. `listen`/`closed`
514
+ * register the SINGLE handler `deliver` / the underlying close path route through —
515
+ * the scope server's own `scope` `message`-event listener calls `deliver(event.data)`
516
+ * for every portless, string-payload event (there is no native registration point on
517
+ * the scope itself for the scope server to hand a `listen` handler to, so `deliver` is
518
+ * the bridge). `close()` fires the registered `closed` handler a scope has nothing
519
+ * physically closable, so this is the only teardown signal available.
761
520
  *
762
- * @param scope - The hostable worker scope to wire
763
- * @param options - The tools, optional identity, and optional port-event gate
764
- * @returns An idempotent disposer for every binding owned by this call
521
+ * @param scope - The hostable scope to adapt (structurally, `self` / `globalThis`
522
+ * inside a dedicated Web Worker)
523
+ * @returns A {@link ScopeTransportInterface} the scope server binds and drives through `deliver`
524
+ *
525
+ * @example
526
+ * ```ts
527
+ * const scopeTransport = createScopeTransport(self)
528
+ * const unbind = bindServer(server, scopeTransport)
529
+ * ```
765
530
  */
766
- function serveMCPScope(scope, options) {
767
- const server = createMCPServer({
768
- tools: options.tools,
769
- identity: {
770
- name: options.name ?? "taverna",
771
- version: options.version ?? "1.0.0"
531
+ function createScopeTransport(scope) {
532
+ let onMessage;
533
+ let onClosed;
534
+ return {
535
+ send(message) {
536
+ scope.postMessage(message);
537
+ },
538
+ listen(handler) {
539
+ onMessage = handler;
540
+ },
541
+ closed(handler) {
542
+ onClosed = handler;
543
+ },
544
+ close() {
545
+ onClosed?.();
546
+ },
547
+ deliver(message) {
548
+ onMessage?.(message);
772
549
  }
773
- });
774
- const scopeTransport = createScopeTransport(scope);
775
- const unbindScope = bindServer(server, scopeTransport);
776
- const teardowns = /* @__PURE__ */ new Map();
777
- const onMessage = createScopeMessageListener(server, scopeTransport, teardowns, options);
778
- scope.addEventListener("message", onMessage);
779
- let disposed = false;
780
- return () => {
781
- if (disposed) return;
782
- disposed = true;
783
- scope.removeEventListener("message", onMessage);
784
- unbindScope();
785
- for (const teardown of teardowns.values()) teardown();
786
- teardowns.clear();
787
550
  };
788
551
  }
789
- /**
790
- * Boots an `MCPServer` inside the current hostable worker scope.
791
- *
792
- * @remarks
793
- * The served endpoint is modern-only: it answers a legacy `initialize` with `-32601`. A
794
- * dual-era worker composes `bindServer(createMCPLegacy(mcp), …)` instead of this function.
795
- *
796
- * @param options - The tools, optional identity, and optional port-event gate
797
- * @returns The disposer returned by {@link serveMCPScope}
798
- */
799
- function serveMCP(options) {
800
- return serveMCPScope(globalThis, options);
801
- }
802
552
  //#endregion
803
- export { DEFAULT_MCP_SERVER_NAME, DEFAULT_MCP_SERVER_VERSION, HTTPClientTransport, MCP_METHOD_HEADER, MCP_NAME_HEADER, MCP_PROTOCOL_VERSION_HEADER, MCP_SESSION_HEADER, MCP_WEBSOCKET_SUBPROTOCOL, MessagePortTransport, WebSocketClientTransport, createHTTPClientTransport, createMessagePortTransport, createScopeMessageListener, createScopeTransport, createWebSocketClientTransport, decodeEvent, readEventStream, serveMCP, serveMCPScope };
553
+ export { DEFAULT_MCP_SERVER_NAME, DEFAULT_MCP_SERVER_VERSION, MessagePortTransport, WebSocketClientTransport, createHTTPClientTransport, createMessagePortTransport, createScopeMessageListener, createScopeServer, createScopeTransport, createWebSocketClientTransport };
804
554
 
805
555
  //# sourceMappingURL=index.js.map