@orkestrel/mcp 0.0.5 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -24,8 +24,10 @@ import { ToolManagerInterface } from '@orkestrel/agent';
24
24
  * `message` event for the client's id correlation. Add `options.headers` (e.g. an
25
25
  * `Authorization` bearer) to reach a guarded server. `start` / `close` hold no
26
26
  * connection; against a STATEFUL server it captures the `mcp-session-id` from
27
- * `initialize` and echoes it on later requests, so the same `MCPClient` passes
28
- * session validation (a stateless server sends none).
27
+ * `initialize` and echoes it on later requests. It also captures the initialize
28
+ * result's `protocolVersion` and sends `mcp-protocol-version` on every subsequent
29
+ * request, so the same `MCPClient` passes the session and 2025-06-18 protocol
30
+ * gates without caller wiring.
29
31
  *
30
32
  * @param options - `url` (the remote endpoint; REQUIRED), optional `headers` merged
31
33
  * onto every request, optional `fetch` (default `globalThis.fetch`), and optional
@@ -76,7 +78,7 @@ export declare function createHTTPClientTransport(options: HTTPClientTransportOp
76
78
  export declare function createMessagePortTransport(options: MessagePortTransportOptions): MCPTransportInterface;
77
79
 
78
80
  /**
79
- * Build `serveMCPScope`'s (`serve.ts`) `message`-event listener — the unified
81
+ * Build `serveMCPScope`'s `message`-event listener — the unified
80
82
  * dispatcher that routes EVERY inbound event on a hostable scope, portless or
81
83
  * port-bearing, to the right binding.
82
84
  *
@@ -116,7 +118,7 @@ export declare function createScopeMessageListener(server: MCPServerInterface, s
116
118
  /**
117
119
  * Adapt a hostable {@link ServeMCPScopeInterface} (`self` in a dedicated Web Worker,
118
120
  * or any structurally matching double) into a {@link ScopeTransportInterface} — the
119
- * implicit, portless message channel `serveMCPScope` (`serve.ts`) binds for the
121
+ * implicit, portless message channel `serveMCPScope` binds for the
120
122
  * dedicated-worker shape.
121
123
  *
122
124
  * @remarks
@@ -204,7 +206,7 @@ export declare const DEFAULT_MCP_SERVER_VERSION = "1.0.0";
204
206
  *
205
207
  * @remarks
206
208
  * - **Request/response over `fetch`.** `send(message)` POSTs the JSON-serialized
207
- * message (or batch) to `options.url` with `content-type: application/json` and an
209
+ * message to `options.url` with `content-type: application/json` and an
208
210
  * `Accept` of BOTH `application/json` and `text/event-stream` (so the server may
209
211
  * answer with either framing) — plus any `options.headers` (e.g. an
210
212
  * `Authorization` bearer). It then decodes the reply and emits each decoded
@@ -216,13 +218,18 @@ export declare const DEFAULT_MCP_SERVER_VERSION = "1.0.0";
216
218
  * face's own `readEventStream`) — the inverse of the server's `openStream` seam, so
217
219
  * the wire round-trips. A `202` Accepted (a notification) carries no body and emits
218
220
  * nothing.
219
- * - **Session echo.** `start()` / `close()` are no-ops (a request/response transport
220
- * holds no long-lived connection). The `mcp-session-id` response header, when a
221
- * STATEFUL server sends one (on `initialize`), is captured into `session` and then
222
- * ECHOED as the `mcp-session-id` request header on every SUBSEQUENT request — so an
223
- * `MCPClient` passes a stateful server's session validation. Before `initialize`
224
- * returns an id, `session` is `undefined` and no header is sent (safe against a
225
- * stateless server, which neither sends nor expects one).
221
+ * - **Session and protocol echo.** `start()` is a no-op (a
222
+ * request/response transport opens no long-lived connection). The
223
+ * `mcp-session-id` response header, when a STATEFUL server sends one (on
224
+ * `initialize`), is captured into `session` and then ECHOED as the
225
+ * `mcp-session-id` request header on every SUBSEQUENT request so an
226
+ * `MCPClient` passes a stateful server's session validation. The
227
+ * initialize result's `protocolVersion` is likewise captured, but only
228
+ * when it is a SUPPORTED value, and echoed as `mcp-protocol-version` on
229
+ * every subsequent request, as required by the 2025-06-18 Streamable-HTTP
230
+ * transport. Before initialize returns, neither captured header is sent.
231
+ * `close()` clears the captured protocol so a reconnect's `initialize`
232
+ * POST is headerless; the captured `session` persists across `close()`.
226
233
  * - **Total at the boundary (§14).** Every reply is narrowed (`parseJSONRPCMessage`,
227
234
  * the SSE decoder) — a non-message reply is dropped, never asserted; a `fetch` /
228
235
  * decode failure surfaces on the `error` event rather than escaping `send`.
@@ -242,7 +249,7 @@ export declare class HTTPClientTransport implements ClientTransportInterface {
242
249
  get emitter(): EmitterInterface<ClientTransportEventMap>;
243
250
  get session(): string | undefined;
244
251
  start(): Promise<void>;
245
- send(message: JSONRPCMessage_2 | readonly JSONRPCMessage_2[]): Promise<void>;
252
+ send(message: JSONRPCMessage_2): Promise<void>;
246
253
  close(): Promise<void>;
247
254
  }
248
255
 
@@ -270,6 +277,13 @@ export declare interface HTTPClientTransportOptions {
270
277
  readonly timeout?: number;
271
278
  }
272
279
 
280
+ /**
281
+ * The Streamable-HTTP transport header carrying the negotiated MCP protocol version
282
+ * on every post-initialize request. The browser HTTP client captures the initialize
283
+ * result's `protocolVersion` and sends this header on each subsequent request.
284
+ */
285
+ export declare const MCP_PROTOCOL_VERSION_HEADER = "mcp-protocol-version";
286
+
273
287
  /**
274
288
  * The Streamable-HTTP transport header that carries the MCP session id. The browser
275
289
  * face's {@link import('./transports/HTTPClientTransport.js').HTTPClientTransport}
@@ -409,35 +423,10 @@ export declare interface ScopeTransportInterface extends MCPTransportInterface {
409
423
  }
410
424
 
411
425
  /**
412
- * Boot an `MCPServer` inside the CURRENT hostable scope (`globalThis` — a dedicated
413
- * Web Worker or a Service Worker) and wire its message events to it.
414
- *
415
- * @remarks
416
- * A one-liner over {@link serveMCPScope}: `serveMCP(options)` is exactly
417
- * `serveMCPScope(globalThis, options)`. Kept as its own export so the scope-facing
418
- * wiring stays independently testable (AGENTS §5) — drive {@link serveMCPScope}
419
- * directly with a scope double for a test, and this thin wrapper for real deploys.
420
- *
421
- * **Trust boundary and lifecycle** — see {@link serveMCPScope}'s `@remarks`. The same
422
- * considerations apply: ENTIRE tool registry exposed to every accepted port-bearing
423
- * event; use `accept` to gate; per-client bindings accumulate for the scope's lifetime.
426
+ * Boot an `MCPServer` inside the current hostable worker scope.
424
427
  *
425
- * @param options - `tools` (the live registry to expose; REQUIRED), optional
426
- * `name`/`version`, optional `accept` (origin/identity gate); see {@link ServeMCPOptions}
427
- * @returns A dispose function — see {@link serveMCPScope}
428
- *
429
- * @example
430
- * ```ts
431
- * // Inside a dedicated Web Worker's entry module:
432
- * import { serveMCP } from '@orkestrel/mcp/browser'
433
- * import { createToolManager, createTool } from '@orkestrel/agent'
434
- *
435
- * const tools = createToolManager()
436
- * tools.add(createTool({ name: 'add', execute: (a) => Number(a.x) + Number(a.y) }))
437
- * const dispose = serveMCP({ tools, name: 'worker-mcp', version: '1.0.0' })
438
- * // ... later, on teardown:
439
- * dispose()
440
- * ```
428
+ * @param options - The tools, optional identity, and optional port-event gate
429
+ * @returns The disposer returned by {@link serveMCPScope}
441
430
  */
442
431
  export declare function serveMCP(options: ServeMCPOptions): () => void;
443
432
 
@@ -470,73 +459,17 @@ export declare interface ServeMCPOptions {
470
459
  }
471
460
 
472
461
  /**
473
- * Boot an `MCPServer` inside a hostable scope (a dedicated Web Worker's `self`, or a
474
- * Service Worker's `self`) and wire its message events to it.
462
+ * Boot an `MCPServer` inside a hostable worker scope and wire its message events to it.
475
463
  *
476
464
  * @remarks
477
- * **Trust boundary mechanism, not policy.** `serveMCPScope` exposes the ENTIRE
478
- * supplied `tools` registry to EVERY client the scope accepts a port from, with NO
479
- * built-in origin or identity check. In a Service Worker that means every same-origin
480
- * context the SW controls (any window, worker, or iframe can
481
- * `controller.postMessage(msg, [port])` and get a fully-bound server with complete
482
- * tool-call access). Origin allow-listing, handshake tokens, and any other gating are
483
- * the embedding application's responsibility compose a guard in front. Use the
484
- * `accept` option to gate port-bearing events before binding: return `false` to drop
485
- * the event entirely (no binding, no reply).
486
- *
487
- * **Lifetime / per-client binding accumulation.** Each accepted port-bearing event
488
- * creates a fresh `MessagePortTransport` + `bindServer` binding that lives for the
489
- * scope's lifetime — there is NO per-client reaping, because `MessagePort` provides
490
- * no "peer closed" signal. For bounded, long-lived client sets this is fine; embedders
491
- * with high client churn must track and invoke the dispose function themselves to
492
- * avoid unbounded accumulation.
493
- *
494
- * **Portless events and the implicit scope channel.** A portless `message` event
495
- * (e.g. `controller.postMessage('<json-rpc>')` in a Service Worker) delivers its
496
- * string directly to the implicit scope transport — **the tool EXECUTES** — even
497
- * though no reply can reach the caller. In a `ServiceWorkerGlobalScope` the reply
498
- * path (`scopeTransport.send` → `scope.postMessage`) throws (no `self.postMessage`),
499
- * and `bindServer` routes the throw to the server emitter's `error` event (see
500
- * `@src/core bindServer`), so the un-repliable reply is dropped. The net effect is
501
- * **blind side-effecting ingress**: the tool runs but the caller gets no result.
502
- * Crucially, **`accept` does NOT gate this channel** — it is consulted only for
503
- * port-bearing events. In a Service Worker, if `accept` is your sole guard, ensure
504
- * all clients connect through transferred `MessagePort`s (port-bearing messages), or
505
- * restrict the exposed tools to side-effect-free operations, or validate a token
506
- * inside the tools themselves.
507
- *
508
- * Binds the implicit scope channel EAGERLY (at call time, not lazily on first use) —
509
- * `bindServer` is called once against a {@link import('./types.js').ScopeTransportInterface}
510
- * wrapping `scope` for the whole lifetime of the returned dispose, so a dedicated
511
- * worker's very first portless message is served with no first-use setup cost or
512
- * ordering hazard.
513
- *
514
- * Every inbound `message` event is inspected structurally: `event.ports.length > 0`
515
- * spawns a fresh {@link import('./factories.js').createMessagePortTransport} +
516
- * `bindServer` for THAT port (tracked for teardown) — this holds even on a
517
- * dedicated-worker-shaped scope, the unified design's deliberate cross-case. An
518
- * event with NO ports and a STRING `event.data` is delivered onto the implicit scope
519
- * channel; any other event (no ports, non-string data) is dropped.
520
- *
521
- * @param scope - The hostable scope to wire (structurally, `self` inside a worker)
522
- * @param options - `tools` (the live registry to expose; REQUIRED), optional
523
- * `name`/`version` (default {@link import('./constants.js').DEFAULT_MCP_SERVER_NAME} /
524
- * {@link import('./constants.js').DEFAULT_MCP_SERVER_VERSION}), optional `accept`
525
- * (origin/identity gate for port-bearing events); see {@link ServeMCPOptions}
526
- * @returns A dispose function — unbinds every binding, closes every accepted
527
- * `MessagePort`, and removes the scope's `message` listener. Idempotent.
528
- *
529
- * @example
530
- * ```ts
531
- * const scope = { postMessage() {}, addEventListener() {}, removeEventListener() {} }
532
- * const dispose = serveMCPScope(scope, {
533
- * tools: createToolManager(),
534
- * // Prefer token-in-data — event.origin is empty for same-origin worker messages.
535
- * accept: (event) => event.data === 'my-secret-token',
536
- * })
537
- * // ... later:
538
- * dispose()
539
- * ```
465
+ * Port-bearing events are gated by `options.accept`, deduplicated by port, and receive
466
+ * their own `MessagePortTransport` binding. Portless string events use the scope's
467
+ * implicit channel. The returned disposer removes the listener, unbinds the implicit
468
+ * channel, and closes every accepted port binding.
469
+ *
470
+ * @param scope - The hostable worker scope to wire
471
+ * @param options - The tools, optional identity, and optional port-event gate
472
+ * @returns An idempotent disposer for every binding owned by this call
540
473
  */
541
474
  export declare function serveMCPScope(scope: ServeMCPScopeInterface, options: ServeMCPOptions): () => void;
542
475
 
@@ -549,7 +482,7 @@ export declare function serveMCPScope(scope: ServeMCPScopeInterface, options: Se
549
482
  * dedicated-worker implicit reply channel), and `addEventListener` /
550
483
  * `removeEventListener` for `'message'` (every inbound event, portless or
551
484
  * port-bearing, arrives through the SAME listener — see {@link ServeMCPOptions}'s
552
- * doc and `serve.ts`). A real `self` / `globalThis` inside a worker satisfies this
485
+ * doc and the bootstrap factories). A real `self` / `globalThis` inside a worker satisfies this
553
486
  * structurally (it exposes far more, which this narrower shape ignores).
554
487
  */
555
488
  export declare interface ServeMCPScopeInterface {
@@ -602,7 +535,7 @@ export declare class WebSocketClientTransport implements ClientTransportInterfac
602
535
  get emitter(): EmitterInterface<ClientTransportEventMap>;
603
536
  get session(): string | undefined;
604
537
  start(): Promise<void>;
605
- send(message: JSONRPCMessage_2 | readonly JSONRPCMessage_2[]): Promise<void>;
538
+ send(message: JSONRPCMessage_2): Promise<void>;
606
539
  close(): Promise<void>;
607
540
  }
608
541
 
@@ -1,5 +1,5 @@
1
- import { bindServer, createMCPServer, parseJSONRPCMessage } from "../core/index.js";
2
- import { isString } from "@orkestrel/contract";
1
+ import { SUPPORTED_PROTOCOL_VERSIONS, bindServer, createMCPServer, isJSONRPCResponse, parseJSONRPCMessage } from "../core/index.js";
2
+ import { 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
@@ -11,6 +11,12 @@ import { Emitter } from "@orkestrel/emitter";
11
11
  * server unchanged.
12
12
  */
13
13
  var MCP_SESSION_HEADER = "mcp-session-id";
14
+ /**
15
+ * The Streamable-HTTP transport header carrying the negotiated MCP protocol version
16
+ * on every post-initialize request. The browser HTTP client captures the initialize
17
+ * result's `protocolVersion` and sends this header on each subsequent request.
18
+ */
19
+ var MCP_PROTOCOL_VERSION_HEADER = "mcp-protocol-version";
14
20
  /** The default server name `serveMCPScope` reports (`initialize`'s `serverInfo.name`) when `options.name` is omitted. */
15
21
  var DEFAULT_MCP_SERVER_NAME = "taverna";
16
22
  /** The default server version `serveMCPScope` reports (`initialize`'s `serverInfo.version`) when `options.version` is omitted. */
@@ -183,7 +189,7 @@ async function readEventStream(response) {
183
189
  return messages;
184
190
  }
185
191
  /**
186
- * Build `serveMCPScope`'s (`serve.ts`) `message`-event listener — the unified
192
+ * Build `serveMCPScope`'s `message`-event listener — the unified
187
193
  * dispatcher that routes EVERY inbound event on a hostable scope, portless or
188
194
  * port-bearing, to the right binding.
189
195
  *
@@ -225,6 +231,7 @@ function createScopeMessageListener(server, scopeTransport, teardowns, options)
225
231
  if (ports.length > 0) {
226
232
  if (options.accept !== void 0 && !options.accept(event)) return;
227
233
  const port = ports[0];
234
+ if (port === void 0) return;
228
235
  if (seen.has(port)) return;
229
236
  seen.add(port);
230
237
  const transport = new MessagePortTransport({ port });
@@ -250,7 +257,7 @@ function createScopeMessageListener(server, scopeTransport, teardowns, options)
250
257
  *
251
258
  * @remarks
252
259
  * - **Request/response over `fetch`.** `send(message)` POSTs the JSON-serialized
253
- * message (or batch) to `options.url` with `content-type: application/json` and an
260
+ * message to `options.url` with `content-type: application/json` and an
254
261
  * `Accept` of BOTH `application/json` and `text/event-stream` (so the server may
255
262
  * answer with either framing) — plus any `options.headers` (e.g. an
256
263
  * `Authorization` bearer). It then decodes the reply and emits each decoded
@@ -262,13 +269,18 @@ function createScopeMessageListener(server, scopeTransport, teardowns, options)
262
269
  * face's own `readEventStream`) — the inverse of the server's `openStream` seam, so
263
270
  * the wire round-trips. A `202` Accepted (a notification) carries no body and emits
264
271
  * nothing.
265
- * - **Session echo.** `start()` / `close()` are no-ops (a request/response transport
266
- * holds no long-lived connection). The `mcp-session-id` response header, when a
267
- * STATEFUL server sends one (on `initialize`), is captured into `session` and then
268
- * ECHOED as the `mcp-session-id` request header on every SUBSEQUENT request — so an
269
- * `MCPClient` passes a stateful server's session validation. Before `initialize`
270
- * returns an id, `session` is `undefined` and no header is sent (safe against a
271
- * stateless server, which neither sends nor expects one).
272
+ * - **Session and protocol echo.** `start()` is a no-op (a
273
+ * request/response transport opens no long-lived connection). The
274
+ * `mcp-session-id` response header, when a STATEFUL server sends one (on
275
+ * `initialize`), is captured into `session` and then ECHOED as the
276
+ * `mcp-session-id` request header on every SUBSEQUENT request so an
277
+ * `MCPClient` passes a stateful server's session validation. The
278
+ * initialize result's `protocolVersion` is likewise captured, but only
279
+ * when it is a SUPPORTED value, and echoed as `mcp-protocol-version` on
280
+ * every subsequent request, as required by the 2025-06-18 Streamable-HTTP
281
+ * transport. Before initialize returns, neither captured header is sent.
282
+ * `close()` clears the captured protocol so a reconnect's `initialize`
283
+ * POST is headerless; the captured `session` persists across `close()`.
272
284
  * - **Total at the boundary (§14).** Every reply is narrowed (`parseJSONRPCMessage`,
273
285
  * the SSE decoder) — a non-message reply is dropped, never asserted; a `fetch` /
274
286
  * decode failure surfaces on the `error` event rather than escaping `send`.
@@ -289,11 +301,12 @@ var HTTPClientTransport = class {
289
301
  #fetch;
290
302
  #timeout;
291
303
  #session = void 0;
304
+ #protocol = void 0;
292
305
  constructor(options) {
293
306
  this.#emitter = new Emitter();
294
307
  this.#url = options.url;
295
308
  this.#headers = options.headers ?? {};
296
- this.#fetch = options.fetch ?? globalThis.fetch;
309
+ this.#fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
297
310
  this.#timeout = options.timeout;
298
311
  }
299
312
  get emitter() {
@@ -312,6 +325,7 @@ var HTTPClientTransport = class {
312
325
  "content-type": "application/json",
313
326
  accept: "application/json, text/event-stream",
314
327
  ...this.#session === void 0 ? {} : { [MCP_SESSION_HEADER]: this.#session },
328
+ ...this.#protocol === void 0 ? {} : { [MCP_PROTOCOL_VERSION_HEADER]: this.#protocol },
315
329
  ...this.#headers
316
330
  },
317
331
  body: JSON.stringify(message),
@@ -326,6 +340,7 @@ var HTTPClientTransport = class {
326
340
  await this.#deliver(response);
327
341
  }
328
342
  async close() {
343
+ this.#protocol = void 0;
329
344
  this.#emitter.emit("close");
330
345
  }
331
346
  async #deliver(response) {
@@ -333,17 +348,21 @@ var HTTPClientTransport = class {
333
348
  const type = response.headers.get("content-type") ?? "";
334
349
  try {
335
350
  if (type.includes("text/event-stream")) {
336
- for (const message of await readEventStream(response)) this.#emitter.emit("message", message);
351
+ for (const message of await readEventStream(response)) this.#capture(message);
337
352
  return;
338
353
  }
339
354
  if (type.includes("application/json")) {
340
355
  const message = parseJSONRPCMessage(await response.json());
341
- if (message !== void 0) this.#emitter.emit("message", message);
356
+ if (message !== void 0) this.#capture(message);
342
357
  }
343
358
  } catch (error) {
344
359
  this.#emitter.emit("error", error);
345
360
  }
346
361
  }
362
+ #capture(message) {
363
+ if (isJSONRPCResponse(message) && isRecord(message.result) && isString(message.result["protocolVersion"]) && SUPPORTED_PROTOCOL_VERSIONS.includes(message.result["protocolVersion"])) this.#protocol = message.result["protocolVersion"];
364
+ this.#emitter.emit("message", message);
365
+ }
347
366
  };
348
367
  //#endregion
349
368
  //#region src/browser/transports/WebSocketClientTransport.ts
@@ -423,13 +442,10 @@ var WebSocketClientTransport = class {
423
442
  }
424
443
  async send(message) {
425
444
  if (this.#closed) return;
426
- const messages = Array.isArray(message) ? message : [message];
427
- for (const one of messages) {
428
- const text = JSON.stringify(one);
429
- const socket = this.#socket;
430
- if (socket !== void 0 && socket.readyState === WebSocket.OPEN) socket.send(text);
431
- else this.#queue.push(text);
432
- }
445
+ const text = JSON.stringify(message);
446
+ const socket = this.#socket;
447
+ if (socket !== void 0 && socket.readyState === WebSocket.OPEN) socket.send(text);
448
+ else this.#queue.push(text);
433
449
  }
434
450
  async close() {
435
451
  if (this.#closed) return;
@@ -524,8 +540,10 @@ function createWebSocketClientTransport(options) {
524
540
  * `message` event for the client's id correlation. Add `options.headers` (e.g. an
525
541
  * `Authorization` bearer) to reach a guarded server. `start` / `close` hold no
526
542
  * connection; against a STATEFUL server it captures the `mcp-session-id` from
527
- * `initialize` and echoes it on later requests, so the same `MCPClient` passes
528
- * session validation (a stateless server sends none).
543
+ * `initialize` and echoes it on later requests. It also captures the initialize
544
+ * result's `protocolVersion` and sends `mcp-protocol-version` on every subsequent
545
+ * request, so the same `MCPClient` passes the session and 2025-06-18 protocol
546
+ * gates without caller wiring.
529
547
  *
530
548
  * @param options - `url` (the remote endpoint; REQUIRED), optional `headers` merged
531
549
  * onto every request, optional `fetch` (default `globalThis.fetch`), and optional
@@ -580,7 +598,7 @@ function createMessagePortTransport(options) {
580
598
  /**
581
599
  * Adapt a hostable {@link ServeMCPScopeInterface} (`self` in a dedicated Web Worker,
582
600
  * or any structurally matching double) into a {@link ScopeTransportInterface} — the
583
- * implicit, portless message channel `serveMCPScope` (`serve.ts`) binds for the
601
+ * implicit, portless message channel `serveMCPScope` binds for the
584
602
  * dedicated-worker shape.
585
603
  *
586
604
  * @remarks
@@ -623,76 +641,18 @@ function createScopeTransport(scope) {
623
641
  }
624
642
  };
625
643
  }
626
- //#endregion
627
- //#region src/browser/serve.ts
628
644
  /**
629
- * Boot an `MCPServer` inside a hostable scope (a dedicated Web Worker's `self`, or a
630
- * Service Worker's `self`) and wire its message events to it.
645
+ * Boot an `MCPServer` inside a hostable worker scope and wire its message events to it.
631
646
  *
632
647
  * @remarks
633
- * **Trust boundary mechanism, not policy.** `serveMCPScope` exposes the ENTIRE
634
- * supplied `tools` registry to EVERY client the scope accepts a port from, with NO
635
- * built-in origin or identity check. In a Service Worker that means every same-origin
636
- * context the SW controls (any window, worker, or iframe can
637
- * `controller.postMessage(msg, [port])` and get a fully-bound server with complete
638
- * tool-call access). Origin allow-listing, handshake tokens, and any other gating are
639
- * the embedding application's responsibility compose a guard in front. Use the
640
- * `accept` option to gate port-bearing events before binding: return `false` to drop
641
- * the event entirely (no binding, no reply).
642
- *
643
- * **Lifetime / per-client binding accumulation.** Each accepted port-bearing event
644
- * creates a fresh `MessagePortTransport` + `bindServer` binding that lives for the
645
- * scope's lifetime — there is NO per-client reaping, because `MessagePort` provides
646
- * no "peer closed" signal. For bounded, long-lived client sets this is fine; embedders
647
- * with high client churn must track and invoke the dispose function themselves to
648
- * avoid unbounded accumulation.
649
- *
650
- * **Portless events and the implicit scope channel.** A portless `message` event
651
- * (e.g. `controller.postMessage('<json-rpc>')` in a Service Worker) delivers its
652
- * string directly to the implicit scope transport — **the tool EXECUTES** — even
653
- * though no reply can reach the caller. In a `ServiceWorkerGlobalScope` the reply
654
- * path (`scopeTransport.send` → `scope.postMessage`) throws (no `self.postMessage`),
655
- * and `bindServer` routes the throw to the server emitter's `error` event (see
656
- * `@src/core bindServer`), so the un-repliable reply is dropped. The net effect is
657
- * **blind side-effecting ingress**: the tool runs but the caller gets no result.
658
- * Crucially, **`accept` does NOT gate this channel** — it is consulted only for
659
- * port-bearing events. In a Service Worker, if `accept` is your sole guard, ensure
660
- * all clients connect through transferred `MessagePort`s (port-bearing messages), or
661
- * restrict the exposed tools to side-effect-free operations, or validate a token
662
- * inside the tools themselves.
663
- *
664
- * Binds the implicit scope channel EAGERLY (at call time, not lazily on first use) —
665
- * `bindServer` is called once against a {@link import('./types.js').ScopeTransportInterface}
666
- * wrapping `scope` for the whole lifetime of the returned dispose, so a dedicated
667
- * worker's very first portless message is served with no first-use setup cost or
668
- * ordering hazard.
669
- *
670
- * Every inbound `message` event is inspected structurally: `event.ports.length > 0`
671
- * spawns a fresh {@link import('./factories.js').createMessagePortTransport} +
672
- * `bindServer` for THAT port (tracked for teardown) — this holds even on a
673
- * dedicated-worker-shaped scope, the unified design's deliberate cross-case. An
674
- * event with NO ports and a STRING `event.data` is delivered onto the implicit scope
675
- * channel; any other event (no ports, non-string data) is dropped.
676
- *
677
- * @param scope - The hostable scope to wire (structurally, `self` inside a worker)
678
- * @param options - `tools` (the live registry to expose; REQUIRED), optional
679
- * `name`/`version` (default {@link import('./constants.js').DEFAULT_MCP_SERVER_NAME} /
680
- * {@link import('./constants.js').DEFAULT_MCP_SERVER_VERSION}), optional `accept`
681
- * (origin/identity gate for port-bearing events); see {@link ServeMCPOptions}
682
- * @returns A dispose function — unbinds every binding, closes every accepted
683
- * `MessagePort`, and removes the scope's `message` listener. Idempotent.
684
- *
685
- * @example
686
- * ```ts
687
- * const scope = { postMessage() {}, addEventListener() {}, removeEventListener() {} }
688
- * const dispose = serveMCPScope(scope, {
689
- * tools: createToolManager(),
690
- * // Prefer token-in-data — event.origin is empty for same-origin worker messages.
691
- * accept: (event) => event.data === 'my-secret-token',
692
- * })
693
- * // ... later:
694
- * dispose()
695
- * ```
648
+ * Port-bearing events are gated by `options.accept`, deduplicated by port, and receive
649
+ * their own `MessagePortTransport` binding. Portless string events use the scope's
650
+ * implicit channel. The returned disposer removes the listener, unbinds the implicit
651
+ * channel, and closes every accepted port binding.
652
+ *
653
+ * @param scope - The hostable worker scope to wire
654
+ * @param options - The tools, optional identity, and optional port-event gate
655
+ * @returns An idempotent disposer for every binding owned by this call
696
656
  */
697
657
  function serveMCPScope(scope, options) {
698
658
  const server = createMCPServer({
@@ -716,40 +676,15 @@ function serveMCPScope(scope, options) {
716
676
  };
717
677
  }
718
678
  /**
719
- * Boot an `MCPServer` inside the CURRENT hostable scope (`globalThis` — a dedicated
720
- * Web Worker or a Service Worker) and wire its message events to it.
721
- *
722
- * @remarks
723
- * A one-liner over {@link serveMCPScope}: `serveMCP(options)` is exactly
724
- * `serveMCPScope(globalThis, options)`. Kept as its own export so the scope-facing
725
- * wiring stays independently testable (AGENTS §5) — drive {@link serveMCPScope}
726
- * directly with a scope double for a test, and this thin wrapper for real deploys.
727
- *
728
- * **Trust boundary and lifecycle** — see {@link serveMCPScope}'s `@remarks`. The same
729
- * considerations apply: ENTIRE tool registry exposed to every accepted port-bearing
730
- * event; use `accept` to gate; per-client bindings accumulate for the scope's lifetime.
679
+ * Boot an `MCPServer` inside the current hostable worker scope.
731
680
  *
732
- * @param options - `tools` (the live registry to expose; REQUIRED), optional
733
- * `name`/`version`, optional `accept` (origin/identity gate); see {@link ServeMCPOptions}
734
- * @returns A dispose function — see {@link serveMCPScope}
735
- *
736
- * @example
737
- * ```ts
738
- * // Inside a dedicated Web Worker's entry module:
739
- * import { serveMCP } from '@orkestrel/mcp/browser'
740
- * import { createToolManager, createTool } from '@orkestrel/agent'
741
- *
742
- * const tools = createToolManager()
743
- * tools.add(createTool({ name: 'add', execute: (a) => Number(a.x) + Number(a.y) }))
744
- * const dispose = serveMCP({ tools, name: 'worker-mcp', version: '1.0.0' })
745
- * // ... later, on teardown:
746
- * dispose()
747
- * ```
681
+ * @param options - The tools, optional identity, and optional port-event gate
682
+ * @returns The disposer returned by {@link serveMCPScope}
748
683
  */
749
684
  function serveMCP(options) {
750
685
  return serveMCPScope(globalThis, options);
751
686
  }
752
687
  //#endregion
753
- export { DEFAULT_MCP_SERVER_NAME, DEFAULT_MCP_SERVER_VERSION, HTTPClientTransport, MCP_SESSION_HEADER, MCP_WEBSOCKET_SUBPROTOCOL, MessagePortTransport, WebSocketClientTransport, createHTTPClientTransport, createMessagePortTransport, createScopeMessageListener, createScopeTransport, createWebSocketClientTransport, decodeEvent, readEventStream, serveMCP, serveMCPScope };
688
+ export { DEFAULT_MCP_SERVER_NAME, DEFAULT_MCP_SERVER_VERSION, HTTPClientTransport, MCP_PROTOCOL_VERSION_HEADER, MCP_SESSION_HEADER, MCP_WEBSOCKET_SUBPROTOCOL, MessagePortTransport, WebSocketClientTransport, createHTTPClientTransport, createMessagePortTransport, createScopeMessageListener, createScopeTransport, createWebSocketClientTransport, decodeEvent, readEventStream, serveMCP, serveMCPScope };
754
689
 
755
690
  //# sourceMappingURL=index.js.map