@orkestrel/mcp 0.0.5 → 0.0.7
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.
- package/dist/src/browser/index.d.ts +41 -108
- package/dist/src/browser/index.js +55 -120
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/core/index.cjs +118 -53
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +77 -19
- package/dist/src/core/index.d.ts +77 -19
- package/dist/src/core/index.js +117 -54
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +147 -115
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +70 -29
- package/dist/src/server/index.d.ts +70 -29
- package/dist/src/server/index.js +149 -118
- package/dist/src/server/index.js.map +1 -1
- package/package.json +35 -34
|
@@ -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
|
|
28
|
-
*
|
|
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
|
|
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`
|
|
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
|
|
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()`
|
|
220
|
-
*
|
|
221
|
-
* STATEFUL server sends one (on
|
|
222
|
-
*
|
|
223
|
-
* `
|
|
224
|
-
*
|
|
225
|
-
*
|
|
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
|
|
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
|
|
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 -
|
|
426
|
-
*
|
|
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
|
|
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
|
-
*
|
|
478
|
-
*
|
|
479
|
-
*
|
|
480
|
-
*
|
|
481
|
-
*
|
|
482
|
-
*
|
|
483
|
-
*
|
|
484
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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()`
|
|
266
|
-
*
|
|
267
|
-
* STATEFUL server sends one (on
|
|
268
|
-
*
|
|
269
|
-
* `
|
|
270
|
-
*
|
|
271
|
-
*
|
|
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.#
|
|
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.#
|
|
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
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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
|
|
528
|
-
*
|
|
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`
|
|
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
|
|
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
|
-
*
|
|
634
|
-
*
|
|
635
|
-
*
|
|
636
|
-
*
|
|
637
|
-
*
|
|
638
|
-
*
|
|
639
|
-
*
|
|
640
|
-
*
|
|
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
|
|
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 -
|
|
733
|
-
*
|
|
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
|