@orkestrel/mcp 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,631 @@
1
+ import { ClientTransportEventMap } from '../../core/index.ts';
2
+ import { ClientTransportInterface } from '../../core/index.ts';
3
+ import { ClientTransportInterface as ClientTransportInterface_2 } from '../core/index.ts';
4
+ import { EmitterInterface } from '@orkestrel/emitter';
5
+ import { JSONRPCMessage } from '../core/index.ts';
6
+ import { JSONRPCMessage as JSONRPCMessage_2 } from '../../core/index.ts';
7
+ import { MCPServerInterface } from '../core/index.ts';
8
+ import { MCPTransportInterface } from '../core/index.ts';
9
+ import { MCPTransportInterface as MCPTransportInterface_2 } from '../../core/index.ts';
10
+ import { ToolManagerInterface } from '@orkestrel/agent';
11
+
12
+ /**
13
+ * Create the browser-face HTTP CLIENT transport for an
14
+ * {@link import('@src/core').MCPClientInterface} — a {@link ClientTransportInterface}
15
+ * that drives a REMOTE Streamable-HTTP MCP server over the native `fetch`, the
16
+ * browser sibling of the Node face's `createHTTPClientTransport` (`@src/server`).
17
+ *
18
+ * @remarks
19
+ * Hand it to `createMCPClient({ transport })`: each JSON-RPC message the client
20
+ * sends is `POST`ed to `options.url` with `content-type: application/json` and an
21
+ * `Accept` of both `application/json` and `text/event-stream` (the server answers
22
+ * with EITHER — a plain JSON envelope or a Streamable-HTTP SSE `data:` event,
23
+ * decoded via `@orkestrel/sse`), and the reply is surfaced on the transport's
24
+ * `message` event for the client's id correlation. Add `options.headers` (e.g. an
25
+ * `Authorization` bearer) to reach a guarded server. `start` / `close` hold no
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).
29
+ *
30
+ * @param options - `url` (the remote endpoint; REQUIRED), optional `headers` merged
31
+ * onto every request, optional `fetch` (default `globalThis.fetch`), and optional
32
+ * `timeout` (ms, applied via `AbortSignal.timeout`); see
33
+ * {@link HTTPClientTransportOptions}
34
+ * @returns A working {@link ClientTransportInterface} over the native `fetch`
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * import { createMCPClient } from '@orkestrel/mcp'
39
+ * import { createHTTPClientTransport } from '@orkestrel/mcp/browser'
40
+ *
41
+ * const client = createMCPClient({
42
+ * transport: createHTTPClientTransport({ url: 'http://localhost:3000/mcp' }),
43
+ * })
44
+ * await client.connect()
45
+ * const tools = await client.tools()
46
+ * ```
47
+ */
48
+ export declare function createHTTPClientTransport(options: HTTPClientTransportOptions): ClientTransportInterface_2;
49
+
50
+ /**
51
+ * Create the browser-face `MessagePort` transport — a
52
+ * {@link import('@src/core').MCPTransportInterface} over a native `MessagePort`, the
53
+ * SYMMETRIC carrier that works as either a server or a client transport depending on
54
+ * which binder ({@link import('@src/core').bindServer} or
55
+ * {@link import('@src/core').bindClient}) it is handed to.
56
+ *
57
+ * @remarks
58
+ * `port.start()` runs at construction (see {@link MessagePortTransport}'s doc for
59
+ * why); inbound payloads are string-only (a non-string `postMessage` payload is
60
+ * dropped, never thrown); `messageerror` is ignored (one bad frame does not close the
61
+ * channel); `close()` closes the port and fires `closed` exactly once.
62
+ *
63
+ * @param options - `port` (the `MessagePort` half to drive; REQUIRED); see
64
+ * {@link MessagePortTransportOptions}
65
+ * @returns A working {@link import('@src/core').MCPTransportInterface} over the port
66
+ *
67
+ * @example
68
+ * ```ts
69
+ * import { bindServer, createMCPServer } from '@orkestrel/mcp'
70
+ * import { createMessagePortTransport } from '@orkestrel/mcp/browser'
71
+ *
72
+ * const { port1, port2 } = new MessageChannel()
73
+ * bindServer(createMCPServer({ name: 's', version: '1.0.0', tools }), createMessagePortTransport({ port: port1 }))
74
+ * ```
75
+ */
76
+ export declare function createMessagePortTransport(options: MessagePortTransportOptions): MCPTransportInterface;
77
+
78
+ /**
79
+ * Build `serveMCPScope`'s (`serve.ts`) `message`-event listener — the unified
80
+ * dispatcher that routes EVERY inbound event on a hostable scope, portless or
81
+ * port-bearing, to the right binding.
82
+ *
83
+ * @remarks
84
+ * Port-bearing events (`event.ports.length > 0`) are gated by `options.accept` FIRST
85
+ * — when the gate returns `false` the event is dropped entirely (no binding, no reply).
86
+ * Accepted events spawn a fresh `MessagePortTransport` over `event.ports[0]`,
87
+ * `bindServer` `server` onto it, and record a teardown (`unbind` then `transport.close()`)
88
+ * into `teardowns`. A port that was already seen is IGNORED — repeated delivery of the
89
+ * same `MessagePort` would create duplicate bindings over one port (→ duplicated replies),
90
+ * so the listener tracks seen ports and silently drops repeats.
91
+ *
92
+ * This branch fires on EITHER a Service-Worker-shaped scope (its normal per-client
93
+ * channel) or a dedicated-worker-shaped one that happens to receive a port-bearing event
94
+ * (the unified design's deliberate cross-case, needing no upfront shape flag). An event
95
+ * with NO ports and a STRING `data` is pushed onto `scopeTransport.deliver` (the
96
+ * implicit, already-bound scope channel); any other event (no ports, non-string data)
97
+ * is silently dropped — total (§14), never throws.
98
+ *
99
+ * @param server - The `MCPServerInterface` every spawned/implicit binding dispatches over
100
+ * @param scopeTransport - The implicit scope channel (already `bindServer`-bound) portless events deliver onto
101
+ * @param teardowns - The shared teardown set `serveMCPScope`'s dispose drains; each port-bearing event adds one entry
102
+ * @param options - The `ServeMCPOptions` (for `options.accept`)
103
+ * @returns The `message`-event listener to register (and later remove) on the scope
104
+ *
105
+ * @example
106
+ * ```ts
107
+ * const teardowns = new Set<() => void>()
108
+ * const scopeTransport = createScopeTransport(scope)
109
+ * bindServer(server, scopeTransport)
110
+ * const onMessage = createScopeMessageListener(server, scopeTransport, teardowns, options)
111
+ * scope.addEventListener('message', onMessage)
112
+ * ```
113
+ */
114
+ export declare function createScopeMessageListener(server: MCPServerInterface, scopeTransport: ScopeTransportInterface, teardowns: Set<() => void>, options: ServeMCPOptions): (event: MessageEvent) => void;
115
+
116
+ /**
117
+ * Adapt a hostable {@link ServeMCPScopeInterface} (`self` in a dedicated Web Worker,
118
+ * or any structurally matching double) into a {@link ScopeTransportInterface} — the
119
+ * implicit, portless message channel `serveMCPScope` (`serve.ts`) binds for the
120
+ * dedicated-worker shape.
121
+ *
122
+ * @remarks
123
+ * `send` writes each outbound string via `scope.postMessage`. `listen`/`closed`
124
+ * register the SINGLE handler `deliver` / the underlying close path route through —
125
+ * `serveMCPScope`'s own `scope` `message`-event listener calls `deliver(event.data)`
126
+ * for every portless, string-payload event (there is no native registration point on
127
+ * the scope itself for `serveMCPScope` to hand a `listen` handler to, so `deliver` is
128
+ * the bridge). `close()` fires the registered `closed` handler — a scope has nothing
129
+ * physically closable, so this is the only teardown signal available.
130
+ *
131
+ * @param scope - The hostable scope to adapt (structurally, `self` / `globalThis`
132
+ * inside a dedicated Web Worker)
133
+ * @returns A {@link ScopeTransportInterface} `serveMCPScope` binds and drives via `deliver`
134
+ *
135
+ * @example
136
+ * ```ts
137
+ * const scopeTransport = createScopeTransport(self)
138
+ * const unbind = bindServer(server, scopeTransport)
139
+ * ```
140
+ */
141
+ export declare function createScopeTransport(scope: ServeMCPScopeInterface): ScopeTransportInterface;
142
+
143
+ /**
144
+ * Create the browser-face WebSocket CLIENT transport for an
145
+ * {@link import('@src/core').MCPClientInterface} — a {@link ClientTransportInterface}
146
+ * that drives a REMOTE MCP server over the native `WebSocket` global, the browser
147
+ * sibling of the Node face's `createWebSocketClientTransport` (`@src/server`).
148
+ *
149
+ * @remarks
150
+ * Hand it to `createMCPClient({ transport })`: `start()` (run by `client.connect()`)
151
+ * opens `new WebSocket(options.url, options.protocols)` and awaits the native
152
+ * `'open'` event — the RFC 6455 handshake itself is the browser's concern. Each
153
+ * JSON-RPC message the client `send`s before the socket opens is QUEUED and flushed,
154
+ * in order, once it does; each decoded reply is surfaced on the transport's
155
+ * `message` event for the client's id correlation.
156
+ *
157
+ * @param options - `url` (the remote WebSocket endpoint; REQUIRED) and optional
158
+ * `protocols` (the WebSocket subprotocol(s) to request); see
159
+ * {@link WebSocketClientTransportOptions}
160
+ * @returns A working {@link ClientTransportInterface} over the native `WebSocket`
161
+ *
162
+ * @example
163
+ * ```ts
164
+ * import { createMCPClient } from '@orkestrel/mcp'
165
+ * import { createWebSocketClientTransport } from '@orkestrel/mcp/browser'
166
+ *
167
+ * const client = createMCPClient({
168
+ * transport: createWebSocketClientTransport({ url: 'ws://localhost:3000/mcp' }),
169
+ * })
170
+ * await client.connect()
171
+ * const tools = await client.tools()
172
+ * ```
173
+ */
174
+ export declare function createWebSocketClientTransport(options: WebSocketClientTransportOptions): ClientTransportInterface_2;
175
+
176
+ /**
177
+ * Decode one SSE event's `data` string into a {@link JSONRPCMessage}, or `undefined`
178
+ * when it is not one — the per-event step {@link readEventStream} folds over.
179
+ *
180
+ * @remarks
181
+ * `JSON.parse`s the `data` (the server serializes the JSON-RPC envelope as the
182
+ * event's `data`) inside a try/catch and narrows the parsed value with
183
+ * `parseJSONRPCMessage`. Total (§14): malformed JSON or a non-message value yields
184
+ * `undefined`, never throws.
185
+ *
186
+ * @param data - One SSE event's `data` payload
187
+ * @returns The decoded {@link JSONRPCMessage}, or `undefined`
188
+ */
189
+ export declare function decodeEvent(data: string): JSONRPCMessage | undefined;
190
+
191
+ /** The default server name `serveMCPScope` reports (`initialize`'s `serverInfo.name`) when `options.name` is omitted. */
192
+ export declare const DEFAULT_MCP_SERVER_NAME = "taverna";
193
+
194
+ /** The default server version `serveMCPScope` reports (`initialize`'s `serverInfo.version`) when `options.version` is omitted. */
195
+ export declare const DEFAULT_MCP_SERVER_VERSION = "1.0.0";
196
+
197
+ /**
198
+ * The browser-face HTTP CLIENT transport for the Model Context Protocol — a
199
+ * {@link ClientTransportInterface} that drives a REMOTE Streamable-HTTP MCP server
200
+ * over the native `fetch`, the browser sibling of the Node face's
201
+ * {@link import('@src/server').HTTPClientTransport}, honoring the SAME
202
+ * `mcp-session-id` semantics so it interoperates with an `MCPSession`-based server
203
+ * unchanged.
204
+ *
205
+ * @remarks
206
+ * - **Request/response over `fetch`.** `send(message)` POSTs the JSON-serialized
207
+ * message (or batch) to `options.url` with `content-type: application/json` and an
208
+ * `Accept` of BOTH `application/json` and `text/event-stream` (so the server may
209
+ * answer with either framing) — plus any `options.headers` (e.g. an
210
+ * `Authorization` bearer). It then decodes the reply and emits each decoded
211
+ * {@link JSONRPCMessage} on the `message` event the
212
+ * {@link import('@src/core').MCPClientInterface} subscribes to.
213
+ * - **Both reply framings.** A `200` with an `application/json` body is parsed with
214
+ * `parseJSONRPCMessage`; a `200` with a `text/event-stream` body is decoded via the
215
+ * `@orkestrel/sse` {@link import('@orkestrel/sse').SSEParserInterface} (the browser
216
+ * face's own `readEventStream`) — the inverse of the server's `openStream` seam, so
217
+ * the wire round-trips. A `202` Accepted (a notification) carries no body and emits
218
+ * 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).
226
+ * - **Total at the boundary (§14).** Every reply is narrowed (`parseJSONRPCMessage`,
227
+ * the SSE decoder) — a non-message reply is dropped, never asserted; a `fetch` /
228
+ * decode failure surfaces on the `error` event rather than escaping `send`.
229
+ * - **Observable (§13).** Owns the `emitter` ({@link ClientTransportEventMap}); fires
230
+ * `message` per decoded reply, `error` on a fault, and `close` on `close()`.
231
+ *
232
+ * @example
233
+ * ```ts
234
+ * const transport = new HTTPClientTransport({ url: 'http://localhost:3000/mcp' })
235
+ * const client = new MCPClient({ transport })
236
+ * await client.connect()
237
+ * ```
238
+ */
239
+ export declare class HTTPClientTransport implements ClientTransportInterface {
240
+ #private;
241
+ constructor(options: HTTPClientTransportOptions);
242
+ get emitter(): EmitterInterface<ClientTransportEventMap>;
243
+ get session(): string | undefined;
244
+ start(): Promise<void>;
245
+ send(message: JSONRPCMessage_2 | readonly JSONRPCMessage_2[]): Promise<void>;
246
+ close(): Promise<void>;
247
+ }
248
+
249
+ /**
250
+ * Options for `createHTTPClientTransport` (browser face) — the remote MCP server's
251
+ * URL and any extra request headers.
252
+ *
253
+ * @remarks
254
+ * - `url` — the absolute URL of the remote server's Streamable-HTTP endpoint (the
255
+ * `POST` target every JSON-RPC message is written to). REQUIRED.
256
+ * - `headers` — extra request headers merged onto every `POST` (e.g. an
257
+ * `Authorization` bearer for a guarded server). The transport always sets
258
+ * `content-type: application/json` and an `Accept` of both `application/json` and
259
+ * `text/event-stream`; a key supplied here is merged on top.
260
+ * - `fetch` — the `fetch` implementation to issue each `POST` with; defaults to
261
+ * `globalThis.fetch`. Injectable for a test double or a non-global `fetch`.
262
+ * - `timeout` — an optional per-request timeout in milliseconds; when set, each
263
+ * `fetch` call is issued with `signal: AbortSignal.timeout(timeout)`. Omit for no
264
+ * transport-level deadline.
265
+ */
266
+ export declare interface HTTPClientTransportOptions {
267
+ readonly url: string;
268
+ readonly headers?: Readonly<Record<string, string>>;
269
+ readonly fetch?: typeof fetch;
270
+ readonly timeout?: number;
271
+ }
272
+
273
+ /**
274
+ * The Streamable-HTTP transport header that carries the MCP session id. The browser
275
+ * face's {@link import('./transports/HTTPClientTransport.js').HTTPClientTransport}
276
+ * ECHOES this header exactly like the Node face's `HTTPClientTransport`
277
+ * (`src/server`), so the same client interoperates with an `MCPSession`-based
278
+ * server unchanged.
279
+ */
280
+ export declare const MCP_SESSION_HEADER = "mcp-session-id";
281
+
282
+ /**
283
+ * The WebSocket subprotocol `createWebSocketClientTransport` requests by default —
284
+ * `'mcp'`, matching `createWebSocketServer`'s unconditional `Sec-WebSocket-Protocol:
285
+ * mcp` echo. Per RFC 6455 §4.1 a client MUST fail the connection if the server returns
286
+ * a subprotocol it did not request; Node ≥ 22 (undici) enforces this strictly, so the
287
+ * default bakes the correct value in. Override `WebSocketClientTransportOptions.protocols`
288
+ * only when connecting to a foreign server that speaks a different subprotocol (or `[]`
289
+ * for no subprotocol negotiation at all).
290
+ */
291
+ export declare const MCP_WEBSOCKET_SUBPROTOCOL = "mcp";
292
+
293
+ /**
294
+ * The browser-face `MessagePort` transport for the Model Context Protocol — a
295
+ * {@link MCPTransportInterface} over a native `MessagePort`, the genuinely new
296
+ * capability this face adds: MCP over `postMessage`.
297
+ *
298
+ * @remarks
299
+ * - **Symmetric.** Unlike {@link import('./WebSocketClientTransport.js').WebSocketClientTransport}
300
+ * / {@link import('./HTTPClientTransport.js').HTTPClientTransport} (CLIENT-only
301
+ * carriers of `@src/core`'s `ClientTransportInterface`), a `MessagePort` is a
302
+ * plain duplex channel — the SAME class implements `@src/core`'s
303
+ * `MCPTransportInterface` and is handed to EITHER `bindServer` or
304
+ * `bindClient`/`createDuplexClientTransport`; which role it plays comes entirely
305
+ * from the binder it is given to, not from anything this class decides.
306
+ * - **`start()` at construction — bind synchronously.** `MessagePort.start()` is only
307
+ * REQUIRED when listening via `addEventListener` (as opposed to the `onmessage`
308
+ * setter, which implies it) — this transport uses `addEventListener`, and
309
+ * `MCPTransportInterface` has no separate open/connect step for the caller to hook
310
+ * a start into, so the constructor calls `port.start()` immediately: the port
311
+ * begins dispatching QUEUED messages the moment the transport exists. This is safe
312
+ * inside `serveMCP`'s flow (the transport is synchronously handed to `bindServer`
313
+ * before control returns to the event loop), but is a **footgun for direct use**:
314
+ * if you construct `new MessagePortTransport({ port })` and then `await` anything
315
+ * before calling `listen`, messages that arrived in the gap are DROPPED. **Bind
316
+ * synchronously after construction** — do not interleave an `await` between
317
+ * `new MessagePortTransport(…)` and `bindServer` / `listen`.
318
+ * - **String payloads only.** `send` posts the message string as-is (`postMessage`
319
+ * structured-clones it — a string clones to an identical string, so the wire stays
320
+ * plain JSON-RPC text like every other transport in this package). Inbound: a
321
+ * non-string `event.data` (a host or a misbehaving peer posting a structured
322
+ * object) is IGNORED — dropped silently, never forwarded, never thrown (§14) —
323
+ * because `MCPTransportInterface` carries no `error` channel for this port to
324
+ * surface a non-string frame on (unlike `ClientTransportInterface`'s `emitter`);
325
+ * silently ignoring is the total, contract-shaped choice.
326
+ * - **`messageerror` is IGNORED, not routed to `closed`.** A `messageerror` event
327
+ * (the structured-clone deserialization of an inbound message threw) reports one
328
+ * BAD FRAME, not a dead channel — the port itself keeps working and later, well-
329
+ * formed messages still arrive. Routing it to `closed` would tear down the
330
+ * `bindServer`/`bindClient` wiring (and, transitively, every session it carries)
331
+ * over a single malformed frame, which is far more destructive than dropping that
332
+ * one frame — so this transport registers a `messageerror` listener that does
333
+ * nothing, deliberately.
334
+ * - **`close()`** is idempotent: it closes the underlying `port` (`MessagePort.close()`
335
+ * disconnects it — further `postMessage` calls on EITHER end are silently
336
+ * undelivered, per the platform contract) and fires the registered `closed`
337
+ * handler exactly once, whether the caller closes it once or twice. There is no
338
+ * native "peer closed" signal for a `MessagePort` (unlike a WebSocket's `close`
339
+ * event) — `closed` fires ONLY from this transport's own `close()`.
340
+ * - **Single-handler-replace (the port contract, `@src/core`'s `MCPTransportInterface`
341
+ * doc).** `listen`/`closed` each hold the ONE currently registered handler; a
342
+ * second call REPLACES the first rather than adding a second subscriber.
343
+ *
344
+ * @example
345
+ * ```ts
346
+ * const { port1, port2 } = new MessageChannel()
347
+ * const serverTransport = new MessagePortTransport({ port: port1 })
348
+ * bindServer(server, serverTransport) // port1 side dispatches inbound requests
349
+ *
350
+ * const clientTransport = new MessagePortTransport({ port: port2 })
351
+ * const client = createMCPClient({ transport: createDuplexClientTransport(clientTransport) })
352
+ * bindClient(client, clientTransport) // port2 side is the client's carrier
353
+ * ```
354
+ */
355
+ export declare class MessagePortTransport implements MCPTransportInterface_2 {
356
+ #private;
357
+ constructor(options: MessagePortTransportOptions);
358
+ send(message: string): void;
359
+ listen(handler: (message: string) => void): void;
360
+ closed(handler: () => void): void;
361
+ close(): void;
362
+ }
363
+
364
+ /**
365
+ * Options for `createMessagePortTransport` — the native `MessagePort` a
366
+ * {@link MessagePortTransport} sends and listens on.
367
+ *
368
+ * @remarks
369
+ * `port` — the channel half to drive (e.g. one side of a `new MessageChannel()`, or
370
+ * the port a `message` event's `ports[0]` carried). REQUIRED. The SAME transport
371
+ * works as either a server or a client carrier — the role comes from whether it is
372
+ * handed to `bindServer` or `bindClient`/`createDuplexClientTransport` (`@src/core`).
373
+ */
374
+ export declare interface MessagePortTransportOptions {
375
+ readonly port: MessagePort;
376
+ }
377
+
378
+ /**
379
+ * Decode a `fetch` Response's Server-Sent-Events body into the JSON-RPC messages it
380
+ * carried — the CLIENT-side inverse of the server's Streamable-HTTP SSE response.
381
+ *
382
+ * @remarks
383
+ * Reads the whole `response.body` stream chunk-by-chunk through a `TextDecoder({
384
+ * stream: true })` (handling a multi-byte char split across reads) and
385
+ * `@orkestrel/sse`'s {@link SSEParserInterface} (handling a partial line / in-progress
386
+ * event split across reads), then narrows each dispatched event's `data` to a
387
+ * {@link JSONRPCMessage} via {@link decodeEvent} (so a non-message / non-JSON `data:`
388
+ * event is DROPPED, never thrown — total, §14). A `null` body (no stream) yields no
389
+ * messages; {@link import('./transports/HTTPClientTransport.js').HTTPClientTransport}
390
+ * reads a request/response SSE reply (the server sends one `data:` event then ends),
391
+ * so this drains to completion.
392
+ *
393
+ * @param response - The SSE `fetch` Response to decode (its `body` is read to completion)
394
+ * @returns Every {@link JSONRPCMessage} the stream carried, in order
395
+ */
396
+ export declare function readEventStream(response: Response): Promise<readonly JSONRPCMessage[]>;
397
+
398
+ /**
399
+ * A duplex {@link MCPTransportInterface} adapting a message-event-bearing SCOPE
400
+ * (`self` in a dedicated Web Worker, or any object shaped the same way) — the
401
+ * internal carrier `serveMCPScope` binds to route the implicit (portless) message
402
+ * channel, plus the `deliver` entry point the scope's own `message` listener pushes
403
+ * an inbound string through (the scope itself never registers `listen`'s handler
404
+ * for the caller — `serveMCPScope`'s dispatcher does, via this `deliver`).
405
+ */
406
+ export declare interface ScopeTransportInterface extends MCPTransportInterface {
407
+ /** Push one inbound message string into the currently registered `listen` handler. */
408
+ deliver(message: string): void;
409
+ }
410
+
411
+ /**
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.
424
+ *
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
+ * ```
441
+ */
442
+ export declare function serveMCP(options: ServeMCPOptions): () => void;
443
+
444
+ /**
445
+ * Options for `serveMCP` / `serveMCPScope` — the live {@link ToolManagerInterface} to
446
+ * expose plus the optional server identity, mirroring `createMCPServer`'s
447
+ * `MCPServerOptions` (`@src/core`) but with `name`/`version` OPTIONAL (defaulting to
448
+ * {@link import('./constants.js').DEFAULT_MCP_SERVER_NAME} /
449
+ * {@link import('./constants.js').DEFAULT_MCP_SERVER_VERSION}).
450
+ *
451
+ * @remarks
452
+ * - `accept` — optional identity gate consulted **before** a port-bearing `message`
453
+ * event is accepted; return `false` to drop the event (no binding, no reply).
454
+ * **`accept` gates ONLY port-bearing events** — portless messages bypass it and
455
+ * deliver directly to the implicit scope channel (the tool executes, blind; in a
456
+ * Service Worker the reply is silently dropped — see `serveMCPScope`'s portless note).
457
+ * Prefer a handshake token in `event.data` as the primary pattern
458
+ * (e.g. `(event) => event.data === token`) — for same-origin worker/MessagePort
459
+ * messages `event.origin` is frequently the empty string, making origin
460
+ * allow-listing unreliable; origin checks are meaningful for cross-origin
461
+ * `postMessage` only. When omitted, ALL port-bearing events are accepted — every
462
+ * same-origin context that can reach the scope gets full tool-call access.
463
+ * See `serveMCPScope`'s trust-boundary and portless-events notes.
464
+ */
465
+ export declare interface ServeMCPOptions {
466
+ readonly tools: ToolManagerInterface;
467
+ readonly name?: string;
468
+ readonly version?: string;
469
+ readonly accept?: (event: MessageEvent) => boolean;
470
+ }
471
+
472
+ /**
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.
475
+ *
476
+ * @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
+ * ```
540
+ */
541
+ export declare function serveMCPScope(scope: ServeMCPScopeInterface, options: ServeMCPOptions): () => void;
542
+
543
+ /**
544
+ * The structural shape `serveMCPScope` needs from a hostable scope — `self` in a
545
+ * dedicated Web Worker or a Service Worker (or any double matching this shape).
546
+ *
547
+ * @remarks
548
+ * Only the three members `serveMCPScope` actually touches: `postMessage` (the
549
+ * dedicated-worker implicit reply channel), and `addEventListener` /
550
+ * `removeEventListener` for `'message'` (every inbound event, portless or
551
+ * 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
553
+ * structurally (it exposes far more, which this narrower shape ignores).
554
+ */
555
+ export declare interface ServeMCPScopeInterface {
556
+ postMessage(message: unknown): void;
557
+ addEventListener(type: 'message', listener: (event: MessageEvent) => void): void;
558
+ removeEventListener(type: 'message', listener: (event: MessageEvent) => void): void;
559
+ }
560
+
561
+ /**
562
+ * The browser-face WebSocket CLIENT transport for the Model Context Protocol — a
563
+ * {@link ClientTransportInterface} that drives a REMOTE MCP server over the native
564
+ * `WebSocket` global, the browser sibling of the Node face's
565
+ * {@link import('@src/server').WebSocketClientTransport}.
566
+ *
567
+ * @remarks
568
+ * - **Host-performed handshake.** `start()` opens `new WebSocket(url, protocols)` and
569
+ * waits for the native `'open'` event — the RFC 6455 handshake itself is entirely
570
+ * the host's concern, so this transport carries none of the Node client's
571
+ * `node:crypto` / `node:http(s)` machinery. A connection failure (the native
572
+ * `'error'` event while not yet `OPEN`) REJECTS `start()`.
573
+ * - **Queued sends.** `send` writes each message as one text frame immediately once
574
+ * the socket is `OPEN`; a `send` issued before `'open'` fires (or before `start()`
575
+ * is even called) is QUEUED and flushed, IN ORDER, the moment the socket opens —
576
+ * so a caller need not await `start()` before calling `send`.
577
+ * - **Inbound (`message`).** Each decoded text frame is `JSON.parse`d (guarded) and
578
+ * narrowed with `parseJSONRPCMessage` — a well-formed {@link JSONRPCMessage}
579
+ * re-emits on this transport's `message` event; a non-text (binary) frame or a
580
+ * non-JSON / non-message text frame surfaces on `error` and is DROPPED (§14 — never
581
+ * throws on adversarial wire input).
582
+ * - **`close()`** closes the underlying socket and fires `close` (idempotent); the
583
+ * socket's native `close` event (a server-initiated close) fires the SAME `close`
584
+ * exactly once total — `close()` first flips the guard, so the native event never
585
+ * double-emits. **This transport is not reusable after `close()`** — a `send` issued
586
+ * after `close()` is silently dropped (not queued, not delivered even on a later
587
+ * `start()`).
588
+ * - **Observable (§13).** Owns the `emitter` ({@link ClientTransportEventMap}); every
589
+ * emit the emitter isolates a listener throw; `error` is a DOMAIN event (a
590
+ * transport-level fault).
591
+ *
592
+ * @example
593
+ * ```ts
594
+ * const transport = new WebSocketClientTransport({ url: 'ws://localhost:3000/mcp' })
595
+ * const client = new MCPClient({ transport })
596
+ * await client.connect() // the browser handshakes, then the MCP initialize runs over WS frames
597
+ * ```
598
+ */
599
+ export declare class WebSocketClientTransport implements ClientTransportInterface {
600
+ #private;
601
+ constructor(options: WebSocketClientTransportOptions);
602
+ get emitter(): EmitterInterface<ClientTransportEventMap>;
603
+ get session(): string | undefined;
604
+ start(): Promise<void>;
605
+ send(message: JSONRPCMessage_2 | readonly JSONRPCMessage_2[]): Promise<void>;
606
+ close(): Promise<void>;
607
+ }
608
+
609
+ /**
610
+ * Options for `createWebSocketClientTransport` (browser face) — the remote MCP
611
+ * WebSocket endpoint and any negotiated subprotocols.
612
+ *
613
+ * @remarks
614
+ * - `url` — the absolute `ws://` / `wss://` (or `http://` / `https://`, accepted by
615
+ * the native `WebSocket` constructor the same way) URL of the remote server's
616
+ * WebSocket endpoint. REQUIRED.
617
+ * - `protocols` — the WebSocket subprotocol(s) to request. **Defaults to
618
+ * {@link import('./constants.js').MCP_WEBSOCKET_SUBPROTOCOL} (`'mcp'`)**, matching
619
+ * `createWebSocketServer`'s unconditional `Sec-WebSocket-Protocol: mcp` echo. Per
620
+ * RFC 6455 §4.1 a client must fail the connection if the server returns a subprotocol
621
+ * it did not request; Node ≥ 22 (undici) enforces this strictly, so the default saves
622
+ * you from that trap when connecting to this repo's own server. Override only when
623
+ * targeting a foreign server that speaks a different (or no) subprotocol — pass `[]`
624
+ * to request no subprotocol at all.
625
+ */
626
+ export declare interface WebSocketClientTransportOptions {
627
+ readonly url: string;
628
+ readonly protocols?: string | readonly string[];
629
+ }
630
+
631
+ export { }