@orkestrel/mcp 0.0.12 → 0.0.13
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/README.md +47 -1
- package/dist/src/browser/index.d.ts +36 -24
- package/dist/src/browser/index.js +19 -13
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/core/index.cjs +4061 -983
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +4024 -1069
- package/dist/src/core/index.d.ts +4024 -1069
- package/dist/src/core/index.js +3982 -973
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +264 -108
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +220 -76
- package/dist/src/server/index.d.ts +220 -76
- package/dist/src/server/index.js +265 -112
- package/dist/src/server/index.js.map +1 -1
- package/package.json +7 -6
package/README.md
CHANGED
|
@@ -68,7 +68,53 @@ usage patterns — see [`guides/src/mcp.md`](guides/src/mcp.md).
|
|
|
68
68
|
|
|
69
69
|
Published with two entry points per the `exports` field in `package.json`:
|
|
70
70
|
the environment-agnostic core (`.`) and the Node-only server surface
|
|
71
|
-
(`./server`).
|
|
71
|
+
(`./server`). The browser face (`./browser`) is ESM only. The tarball is
|
|
72
|
+
625.0 kB packed across 18 files, 2.5 MB unpacked.
|
|
73
|
+
|
|
74
|
+
[`CHANGELOG.md`](CHANGELOG.md) lives in the repository and is not in the
|
|
75
|
+
tarball, because `files` is `["dist/src", "README.md"]`.
|
|
76
|
+
|
|
77
|
+
## Proven
|
|
78
|
+
|
|
79
|
+
`npm run test:conformance` starts the real Streamable HTTP server from this
|
|
80
|
+
package's source and runs
|
|
81
|
+
`@modelcontextprotocol/conformance@0.2.0-alpha.10` against MCP revision
|
|
82
|
+
`2026-07-28`. The recorded result is **23 passed / 0 failed**. That is a
|
|
83
|
+
genuine foreign MCP client driving this server end to end, and it is
|
|
84
|
+
evidence about the wire. It fetches the runner from the npm registry, so it
|
|
85
|
+
is a live-service project of its own and stays outside the hermetic
|
|
86
|
+
`npm test`.
|
|
87
|
+
|
|
88
|
+
**IDE integration is not claimed.** No IDE, editor, or agent host has driven
|
|
89
|
+
this server. A claim about an external client stays unproven here until one
|
|
90
|
+
representative real client of that class drives it end to end, and no client
|
|
91
|
+
of the IDE class has.
|
|
92
|
+
|
|
93
|
+
## Declared limits
|
|
94
|
+
|
|
95
|
+
Four publication facts, each with its number. Full detail, plus every
|
|
96
|
+
protocol-level gap and non-goal, is in
|
|
97
|
+
[`guides/src/mcp.md`](guides/src/mcp.md#declared-packaging-limits).
|
|
98
|
+
|
|
99
|
+
- **No IDE evidence.** See above. The conformance number is about the wire
|
|
100
|
+
and does not transfer to a host application.
|
|
101
|
+
- **No top-level `types` field.** Every `exports` subpath carries a `types`
|
|
102
|
+
condition, so `node16`, `nodenext`, and `bundler` resolution find
|
|
103
|
+
declarations. A consumer on legacy `moduleResolution: node` does not read
|
|
104
|
+
`exports` and sees an untyped package.
|
|
105
|
+
- **A build-time version notice, three times.** API Extractor bundles
|
|
106
|
+
TypeScript 5.9.3 through a transitive pin and this project compiles with
|
|
107
|
+
6.0.3, so `build` prints one notice per built face. It is informational:
|
|
108
|
+
`build` exits 0 and every declaration is emitted.
|
|
109
|
+
- **Source maps ship.** Five `.map` files are 1,130 kB of the 2.5 MB
|
|
110
|
+
unpacked, about 45 percent. They are kept so a consumer debugging a
|
|
111
|
+
protocol library steps into real source.
|
|
112
|
+
|
|
113
|
+
The notice, verbatim:
|
|
114
|
+
|
|
115
|
+
```text
|
|
116
|
+
*** The target project appears to use TypeScript 6.0.3 which is newer than the bundled compiler engine; consider upgrading API Extractor.
|
|
117
|
+
```
|
|
72
118
|
|
|
73
119
|
## License
|
|
74
120
|
|
|
@@ -1,17 +1,17 @@
|
|
|
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
1
|
import { EmitterInterface } from '@orkestrel/emitter';
|
|
5
|
-
import { JSONRPCMessage } from '
|
|
6
|
-
import { JSONRPCMessage as JSONRPCMessage_2 } from '
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
2
|
+
import { JSONRPCMessage } from '@orkestrel/mcp';
|
|
3
|
+
import { JSONRPCMessage as JSONRPCMessage_2 } from '@orkestrel/mcp';
|
|
4
|
+
import { MCPClientTransportEventMap } from '@orkestrel/mcp';
|
|
5
|
+
import { MCPClientTransportInterface } from '@orkestrel/mcp';
|
|
6
|
+
import { MCPClientTransportInterface as MCPClientTransportInterface_2 } from '@orkestrel/mcp';
|
|
7
|
+
import { MCPServerInterface } from '@orkestrel/mcp';
|
|
8
|
+
import { MCPTransportInterface } from '@orkestrel/mcp';
|
|
9
|
+
import { MCPTransportInterface as MCPTransportInterface_2 } from '@orkestrel/mcp';
|
|
10
10
|
import { ToolManagerInterface } from '@orkestrel/tool';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Create the browser-face HTTP CLIENT transport for an
|
|
14
|
-
* {@link import('@src/core').MCPClientInterface} — a {@link
|
|
14
|
+
* {@link import('@src/core').MCPClientInterface} — a {@link MCPClientTransportInterface}
|
|
15
15
|
* that drives a REMOTE Streamable-HTTP MCP server over the native `fetch`, the
|
|
16
16
|
* browser sibling of the Node face's `createHTTPClientTransport` (`@src/server`).
|
|
17
17
|
*
|
|
@@ -34,7 +34,7 @@ import { ToolManagerInterface } from '@orkestrel/tool';
|
|
|
34
34
|
* onto every request, optional `fetch` (default `globalThis.fetch`), and optional
|
|
35
35
|
* `timeout` (ms, applied via `AbortSignal.timeout`); see
|
|
36
36
|
* {@link HTTPClientTransportOptions}
|
|
37
|
-
* @returns A working {@link
|
|
37
|
+
* @returns A working {@link MCPClientTransportInterface} over the native `fetch`
|
|
38
38
|
*
|
|
39
39
|
* @example
|
|
40
40
|
* ```ts
|
|
@@ -48,7 +48,7 @@ import { ToolManagerInterface } from '@orkestrel/tool';
|
|
|
48
48
|
* const tools = await client.tools()
|
|
49
49
|
* ```
|
|
50
50
|
*/
|
|
51
|
-
export declare function createHTTPClientTransport(options: HTTPClientTransportOptions):
|
|
51
|
+
export declare function createHTTPClientTransport(options: HTTPClientTransportOptions): MCPClientTransportInterface_2;
|
|
52
52
|
|
|
53
53
|
/**
|
|
54
54
|
* Create the browser-face `MessagePort` transport — a
|
|
@@ -145,7 +145,7 @@ export declare function createScopeTransport(scope: ServeMCPScopeInterface): Sco
|
|
|
145
145
|
|
|
146
146
|
/**
|
|
147
147
|
* Create the browser-face WebSocket CLIENT transport for an
|
|
148
|
-
* {@link import('@src/core').MCPClientInterface} — a {@link
|
|
148
|
+
* {@link import('@src/core').MCPClientInterface} — a {@link MCPClientTransportInterface}
|
|
149
149
|
* that drives a REMOTE MCP server over the native `WebSocket` global, the browser
|
|
150
150
|
* sibling of the Node face's `createWebSocketClientTransport` (`@src/server`).
|
|
151
151
|
*
|
|
@@ -160,7 +160,7 @@ export declare function createScopeTransport(scope: ServeMCPScopeInterface): Sco
|
|
|
160
160
|
* @param options - `url` (the remote WebSocket endpoint; REQUIRED) and optional
|
|
161
161
|
* `protocols` (the WebSocket subprotocol(s) to request); see
|
|
162
162
|
* {@link WebSocketClientTransportOptions}
|
|
163
|
-
* @returns A working {@link
|
|
163
|
+
* @returns A working {@link MCPClientTransportInterface} over the native `WebSocket`
|
|
164
164
|
*
|
|
165
165
|
* @example
|
|
166
166
|
* ```ts
|
|
@@ -174,7 +174,7 @@ export declare function createScopeTransport(scope: ServeMCPScopeInterface): Sco
|
|
|
174
174
|
* const tools = await client.tools()
|
|
175
175
|
* ```
|
|
176
176
|
*/
|
|
177
|
-
export declare function createWebSocketClientTransport(options: WebSocketClientTransportOptions):
|
|
177
|
+
export declare function createWebSocketClientTransport(options: WebSocketClientTransportOptions): MCPClientTransportInterface_2;
|
|
178
178
|
|
|
179
179
|
/**
|
|
180
180
|
* Decode one SSE event's `data` string into a {@link JSONRPCMessage}, or `undefined`
|
|
@@ -199,7 +199,7 @@ export declare const DEFAULT_MCP_SERVER_VERSION = "1.0.0";
|
|
|
199
199
|
|
|
200
200
|
/**
|
|
201
201
|
* The browser-face HTTP CLIENT transport for the Model Context Protocol — a
|
|
202
|
-
* {@link
|
|
202
|
+
* {@link MCPClientTransportInterface} that drives a REMOTE Streamable-HTTP MCP server
|
|
203
203
|
* over the native `fetch`, the browser sibling of the Node face's
|
|
204
204
|
* {@link import('@src/server').HTTPClientTransport}, honoring the SAME
|
|
205
205
|
* `mcp-session-id` semantics so it interoperates with an `MCPSession`-based server
|
|
@@ -235,7 +235,7 @@ export declare const DEFAULT_MCP_SERVER_VERSION = "1.0.0";
|
|
|
235
235
|
* - **Total at the boundary (§14).** Every reply is narrowed (`parseJSONRPCMessage`,
|
|
236
236
|
* the SSE decoder) — a non-message reply is dropped, never asserted; a `fetch` /
|
|
237
237
|
* decode failure surfaces on the `error` event rather than escaping `send`.
|
|
238
|
-
* - **Observable (§13).** Owns the `emitter` ({@link
|
|
238
|
+
* - **Observable (§13).** Owns the `emitter` ({@link MCPClientTransportEventMap}); fires
|
|
239
239
|
* `message` per decoded reply, `error` on a fault, and `close` on `close()`.
|
|
240
240
|
*
|
|
241
241
|
* @example
|
|
@@ -245,11 +245,12 @@ export declare const DEFAULT_MCP_SERVER_VERSION = "1.0.0";
|
|
|
245
245
|
* await client.connect()
|
|
246
246
|
* ```
|
|
247
247
|
*/
|
|
248
|
-
export declare class HTTPClientTransport implements
|
|
248
|
+
export declare class HTTPClientTransport implements MCPClientTransportInterface {
|
|
249
249
|
#private;
|
|
250
250
|
constructor(options: HTTPClientTransportOptions);
|
|
251
|
-
get emitter(): EmitterInterface<
|
|
251
|
+
get emitter(): EmitterInterface<MCPClientTransportEventMap>;
|
|
252
252
|
get session(): string | undefined;
|
|
253
|
+
get duplex(): boolean;
|
|
253
254
|
start(): Promise<void>;
|
|
254
255
|
send(message: JSONRPCMessage_2): Promise<void>;
|
|
255
256
|
close(): Promise<void>;
|
|
@@ -326,7 +327,7 @@ export declare const MCP_WEBSOCKET_SUBPROTOCOL = "mcp";
|
|
|
326
327
|
* @remarks
|
|
327
328
|
* - **Symmetric.** Unlike {@link import('./WebSocketClientTransport.js').WebSocketClientTransport}
|
|
328
329
|
* / {@link import('./HTTPClientTransport.js').HTTPClientTransport} (CLIENT-only
|
|
329
|
-
* carriers of `@src/core`'s `
|
|
330
|
+
* carriers of `@src/core`'s `MCPClientTransportInterface`), a `MessagePort` is a
|
|
330
331
|
* plain duplex channel — the SAME class implements `@src/core`'s
|
|
331
332
|
* `MCPTransportInterface` and is handed to EITHER `bindServer` or
|
|
332
333
|
* `bindClient`/`createDuplexClientTransport`; which role it plays comes entirely
|
|
@@ -349,7 +350,7 @@ export declare const MCP_WEBSOCKET_SUBPROTOCOL = "mcp";
|
|
|
349
350
|
* non-string `event.data` (a host or a misbehaving peer posting a structured
|
|
350
351
|
* object) is IGNORED — dropped silently, never forwarded, never thrown (§14) —
|
|
351
352
|
* because `MCPTransportInterface` carries no `error` channel for this port to
|
|
352
|
-
* surface a non-string frame on (unlike `
|
|
353
|
+
* surface a non-string frame on (unlike `MCPClientTransportInterface`'s `emitter`);
|
|
353
354
|
* silently ignoring is the total, contract-shaped choice.
|
|
354
355
|
* - **`messageerror` is IGNORED, not routed to `closed`.** A `messageerror` event
|
|
355
356
|
* (the structured-clone deserialization of an inbound message threw) reports one
|
|
@@ -507,7 +508,7 @@ export declare interface ServeMCPScopeInterface {
|
|
|
507
508
|
|
|
508
509
|
/**
|
|
509
510
|
* The browser-face WebSocket CLIENT transport for the Model Context Protocol — a
|
|
510
|
-
* {@link
|
|
511
|
+
* {@link MCPClientTransportInterface} that drives a REMOTE MCP server over the native
|
|
511
512
|
* `WebSocket` global, the browser sibling of the Node face's
|
|
512
513
|
* {@link import('@src/server').WebSocketClientTransport}.
|
|
513
514
|
*
|
|
@@ -532,7 +533,7 @@ export declare interface ServeMCPScopeInterface {
|
|
|
532
533
|
* double-emits. **This transport is not reusable after `close()`** — a `send` issued
|
|
533
534
|
* after `close()` is silently dropped (not queued, not delivered even on a later
|
|
534
535
|
* `start()`).
|
|
535
|
-
* - **Observable (§13).** Owns the `emitter` ({@link
|
|
536
|
+
* - **Observable (§13).** Owns the `emitter` ({@link MCPClientTransportEventMap}); every
|
|
536
537
|
* emit the emitter isolates a listener throw; `error` is a DOMAIN event (a
|
|
537
538
|
* transport-level fault).
|
|
538
539
|
*
|
|
@@ -543,11 +544,12 @@ export declare interface ServeMCPScopeInterface {
|
|
|
543
544
|
* await client.connect() // the browser handshakes, then the MCP initialize runs over WS frames
|
|
544
545
|
* ```
|
|
545
546
|
*/
|
|
546
|
-
export declare class WebSocketClientTransport implements
|
|
547
|
+
export declare class WebSocketClientTransport implements MCPClientTransportInterface {
|
|
547
548
|
#private;
|
|
548
549
|
constructor(options: WebSocketClientTransportOptions);
|
|
549
|
-
get emitter(): EmitterInterface<
|
|
550
|
+
get emitter(): EmitterInterface<MCPClientTransportEventMap>;
|
|
550
551
|
get session(): string | undefined;
|
|
552
|
+
get duplex(): boolean;
|
|
551
553
|
start(): Promise<void>;
|
|
552
554
|
send(message: JSONRPCMessage_2): Promise<void>;
|
|
553
555
|
close(): Promise<void>;
|
|
@@ -569,6 +571,16 @@ export declare class WebSocketClientTransport implements ClientTransportInterfac
|
|
|
569
571
|
* you from that trap when connecting to this repo's own server. Override only when
|
|
570
572
|
* targeting a foreign server that speaks a different (or no) subprotocol — pass `[]`
|
|
571
573
|
* to request no subprotocol at all.
|
|
574
|
+
*
|
|
575
|
+
* **No `headers` here, and that divergence from the Node face's `{ url, headers }` is
|
|
576
|
+
* deliberate rather than unfinished: the host performs the WebSocket handshake.** The
|
|
577
|
+
* native constructor takes a URL and subprotocols and nothing else, so a page cannot set an
|
|
578
|
+
* upgrade request header at all — there is no seam for an `Authorization` bearer to reach.
|
|
579
|
+
* The Node face owns its own `node:http(s)` upgrade request and therefore can, which is why
|
|
580
|
+
* only that side offers `headers`. Reach a guarded server from a page with a credential the
|
|
581
|
+
* platform DOES carry: a cookie the browser attaches to the upgrade, a subprotocol token, or
|
|
582
|
+
* a signed value in the URL. Adding a `headers` key here would be an option that silently
|
|
583
|
+
* did nothing.
|
|
572
584
|
*/
|
|
573
585
|
export declare interface WebSocketClientTransportOptions {
|
|
574
586
|
readonly url: string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { bindServer, createMCPServer, isJSONRPCResponse, isMCPVersion, isModernRequest, parseJSONRPCMessage
|
|
1
|
+
import { bindServer, createMCPServer, inferRequestVersion, isJSONRPCResponse, isMCPVersion, isModernRequest, parseJSONRPCMessage } from "../core/index.js";
|
|
2
2
|
import { isRecord, isString } from "@orkestrel/contract";
|
|
3
3
|
import { createSSEParser } from "@orkestrel/sse";
|
|
4
4
|
import { Emitter } from "@orkestrel/emitter";
|
|
@@ -51,7 +51,7 @@ var MCP_WEBSOCKET_SUBPROTOCOL = "mcp";
|
|
|
51
51
|
* @remarks
|
|
52
52
|
* - **Symmetric.** Unlike {@link import('./WebSocketClientTransport.js').WebSocketClientTransport}
|
|
53
53
|
* / {@link import('./HTTPClientTransport.js').HTTPClientTransport} (CLIENT-only
|
|
54
|
-
* carriers of `@src/core`'s `
|
|
54
|
+
* carriers of `@src/core`'s `MCPClientTransportInterface`), a `MessagePort` is a
|
|
55
55
|
* plain duplex channel — the SAME class implements `@src/core`'s
|
|
56
56
|
* `MCPTransportInterface` and is handed to EITHER `bindServer` or
|
|
57
57
|
* `bindClient`/`createDuplexClientTransport`; which role it plays comes entirely
|
|
@@ -74,7 +74,7 @@ var MCP_WEBSOCKET_SUBPROTOCOL = "mcp";
|
|
|
74
74
|
* non-string `event.data` (a host or a misbehaving peer posting a structured
|
|
75
75
|
* object) is IGNORED — dropped silently, never forwarded, never thrown (§14) —
|
|
76
76
|
* because `MCPTransportInterface` carries no `error` channel for this port to
|
|
77
|
-
* surface a non-string frame on (unlike `
|
|
77
|
+
* surface a non-string frame on (unlike `MCPClientTransportInterface`'s `emitter`);
|
|
78
78
|
* silently ignoring is the total, contract-shaped choice.
|
|
79
79
|
* - **`messageerror` is IGNORED, not routed to `closed`.** A `messageerror` event
|
|
80
80
|
* (the structured-clone deserialization of an inbound message threw) reports one
|
|
@@ -259,7 +259,7 @@ function createScopeMessageListener(server, scopeTransport, teardowns, options)
|
|
|
259
259
|
//#region src/browser/transports/HTTPClientTransport.ts
|
|
260
260
|
/**
|
|
261
261
|
* The browser-face HTTP CLIENT transport for the Model Context Protocol — a
|
|
262
|
-
* {@link
|
|
262
|
+
* {@link MCPClientTransportInterface} that drives a REMOTE Streamable-HTTP MCP server
|
|
263
263
|
* over the native `fetch`, the browser sibling of the Node face's
|
|
264
264
|
* {@link import('@src/server').HTTPClientTransport}, honoring the SAME
|
|
265
265
|
* `mcp-session-id` semantics so it interoperates with an `MCPSession`-based server
|
|
@@ -295,7 +295,7 @@ function createScopeMessageListener(server, scopeTransport, teardowns, options)
|
|
|
295
295
|
* - **Total at the boundary (§14).** Every reply is narrowed (`parseJSONRPCMessage`,
|
|
296
296
|
* the SSE decoder) — a non-message reply is dropped, never asserted; a `fetch` /
|
|
297
297
|
* decode failure surfaces on the `error` event rather than escaping `send`.
|
|
298
|
-
* - **Observable (§13).** Owns the `emitter` ({@link
|
|
298
|
+
* - **Observable (§13).** Owns the `emitter` ({@link MCPClientTransportEventMap}); fires
|
|
299
299
|
* `message` per decoded reply, `error` on a fault, and `close` on `close()`.
|
|
300
300
|
*
|
|
301
301
|
* @example
|
|
@@ -326,6 +326,9 @@ var HTTPClientTransport = class {
|
|
|
326
326
|
get session() {
|
|
327
327
|
return this.#session;
|
|
328
328
|
}
|
|
329
|
+
get duplex() {
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
329
332
|
async start() {}
|
|
330
333
|
async send(message) {
|
|
331
334
|
let response;
|
|
@@ -356,10 +359,10 @@ var HTTPClientTransport = class {
|
|
|
356
359
|
}
|
|
357
360
|
#buildHeaders(message) {
|
|
358
361
|
if (isModernRequest(message)) {
|
|
359
|
-
const
|
|
362
|
+
const version = inferRequestVersion(message);
|
|
360
363
|
const name = message.params?.["name"];
|
|
361
364
|
return {
|
|
362
|
-
...
|
|
365
|
+
...version === void 0 ? {} : { [MCP_PROTOCOL_VERSION_HEADER]: version },
|
|
363
366
|
[MCP_METHOD_HEADER]: message.method,
|
|
364
367
|
...message.method === "tools/call" && isString(name) ? { [MCP_NAME_HEADER]: name } : {}
|
|
365
368
|
};
|
|
@@ -391,7 +394,7 @@ var HTTPClientTransport = class {
|
|
|
391
394
|
//#region src/browser/transports/WebSocketClientTransport.ts
|
|
392
395
|
/**
|
|
393
396
|
* The browser-face WebSocket CLIENT transport for the Model Context Protocol — a
|
|
394
|
-
* {@link
|
|
397
|
+
* {@link MCPClientTransportInterface} that drives a REMOTE MCP server over the native
|
|
395
398
|
* `WebSocket` global, the browser sibling of the Node face's
|
|
396
399
|
* {@link import('@src/server').WebSocketClientTransport}.
|
|
397
400
|
*
|
|
@@ -416,7 +419,7 @@ var HTTPClientTransport = class {
|
|
|
416
419
|
* double-emits. **This transport is not reusable after `close()`** — a `send` issued
|
|
417
420
|
* after `close()` is silently dropped (not queued, not delivered even on a later
|
|
418
421
|
* `start()`).
|
|
419
|
-
* - **Observable (§13).** Owns the `emitter` ({@link
|
|
422
|
+
* - **Observable (§13).** Owns the `emitter` ({@link MCPClientTransportEventMap}); every
|
|
420
423
|
* emit the emitter isolates a listener throw; `error` is a DOMAIN event (a
|
|
421
424
|
* transport-level fault).
|
|
422
425
|
*
|
|
@@ -444,6 +447,9 @@ var WebSocketClientTransport = class {
|
|
|
444
447
|
return this.#emitter;
|
|
445
448
|
}
|
|
446
449
|
get session() {}
|
|
450
|
+
get duplex() {
|
|
451
|
+
return true;
|
|
452
|
+
}
|
|
447
453
|
async start() {
|
|
448
454
|
if (this.#socket !== void 0) return;
|
|
449
455
|
this.#closed = false;
|
|
@@ -516,7 +522,7 @@ var WebSocketClientTransport = class {
|
|
|
516
522
|
//#region src/browser/factories.ts
|
|
517
523
|
/**
|
|
518
524
|
* Create the browser-face WebSocket CLIENT transport for an
|
|
519
|
-
* {@link import('@src/core').MCPClientInterface} — a {@link
|
|
525
|
+
* {@link import('@src/core').MCPClientInterface} — a {@link MCPClientTransportInterface}
|
|
520
526
|
* that drives a REMOTE MCP server over the native `WebSocket` global, the browser
|
|
521
527
|
* sibling of the Node face's `createWebSocketClientTransport` (`@src/server`).
|
|
522
528
|
*
|
|
@@ -531,7 +537,7 @@ var WebSocketClientTransport = class {
|
|
|
531
537
|
* @param options - `url` (the remote WebSocket endpoint; REQUIRED) and optional
|
|
532
538
|
* `protocols` (the WebSocket subprotocol(s) to request); see
|
|
533
539
|
* {@link WebSocketClientTransportOptions}
|
|
534
|
-
* @returns A working {@link
|
|
540
|
+
* @returns A working {@link MCPClientTransportInterface} over the native `WebSocket`
|
|
535
541
|
*
|
|
536
542
|
* @example
|
|
537
543
|
* ```ts
|
|
@@ -550,7 +556,7 @@ function createWebSocketClientTransport(options) {
|
|
|
550
556
|
}
|
|
551
557
|
/**
|
|
552
558
|
* Create the browser-face HTTP CLIENT transport for an
|
|
553
|
-
* {@link import('@src/core').MCPClientInterface} — a {@link
|
|
559
|
+
* {@link import('@src/core').MCPClientInterface} — a {@link MCPClientTransportInterface}
|
|
554
560
|
* that drives a REMOTE Streamable-HTTP MCP server over the native `fetch`, the
|
|
555
561
|
* browser sibling of the Node face's `createHTTPClientTransport` (`@src/server`).
|
|
556
562
|
*
|
|
@@ -573,7 +579,7 @@ function createWebSocketClientTransport(options) {
|
|
|
573
579
|
* onto every request, optional `fetch` (default `globalThis.fetch`), and optional
|
|
574
580
|
* `timeout` (ms, applied via `AbortSignal.timeout`); see
|
|
575
581
|
* {@link HTTPClientTransportOptions}
|
|
576
|
-
* @returns A working {@link
|
|
582
|
+
* @returns A working {@link MCPClientTransportInterface} over the native `fetch`
|
|
577
583
|
*
|
|
578
584
|
* @example
|
|
579
585
|
* ```ts
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["#port","#receive","#closed","#onMessage","#onClosed","#emitter","#url","#headers","#fetch","#timeout","#session","#buildHeaders","#deliver","#protocol","#capture","#emitter","#url","#protocols","#socket","#closed","#bind","#flush","#queue","#receive","#onClose"],"sources":["../../../src/browser/constants.ts","../../../src/browser/transports/MessagePortTransport.ts","../../../src/browser/helpers.ts","../../../src/browser/transports/HTTPClientTransport.ts","../../../src/browser/transports/WebSocketClientTransport.ts","../../../src/browser/factories.ts"],"sourcesContent":["// The MCP browser-transport constants (AGENTS §5 constants file) — the wire-level\n// header names the browser-face HTTP client transport echoes, matching the Node\n// face's session, protocol-version, method, and name headers byte-for-byte. The\n// browser face imports nothing from `src/server` (peer environment faces, per\n// AGENTS §2), so the literals are declared once here too — the SAME strings, not\n// shared symbols.\n\n/**\n * The Streamable-HTTP transport header that carries the MCP session id. The browser\n * face's {@link import('./transports/HTTPClientTransport.js').HTTPClientTransport}\n * ECHOES this header exactly like the Node face's `HTTPClientTransport`\n * (`src/server`), so the same client interoperates with an `MCPSession`-based\n * server unchanged.\n */\nexport const MCP_SESSION_HEADER = 'mcp-session-id'\n\n/**\n * The Streamable-HTTP transport header carrying the MCP protocol version. Modern\n * requests derive it from their own `_meta`; legacy requests echo the negotiated\n * initialize result on each subsequent request.\n */\nexport const MCP_PROTOCOL_VERSION_HEADER = 'mcp-protocol-version'\n\n/**\n * The modern Streamable-HTTP request header carrying the JSON-RPC method. It is\n * emitted on every modern request and never on a legacy request.\n */\nexport const MCP_METHOD_HEADER = 'mcp-method'\n\n/**\n * The modern Streamable-HTTP request header carrying a named target. The browser\n * HTTP client emits it only for `tools/call`, from that request's `params.name`.\n */\nexport const MCP_NAME_HEADER = 'mcp-name'\n\n// `serveMCP` server-identity defaults — `src/core`'s `createMCPServer` REQUIRES\n// `name`/`version`, but `ServeMCPOptions` (this face's bootstrap) makes both optional\n// (mirroring the CLIENT identity defaults, `DEFAULT_MCP_CLIENT_NAME` /\n// `DEFAULT_MCP_CLIENT_VERSION`, `src/core/constants.ts`), so `serveMCPScope` falls\n// back to these when a caller omits them.\n\n/** The default server name `serveMCPScope` reports (`initialize`'s `serverInfo.name`) when `options.name` is omitted. */\nexport const DEFAULT_MCP_SERVER_NAME = 'taverna'\n\n/** The default server version `serveMCPScope` reports (`initialize`'s `serverInfo.version`) when `options.version` is omitted. */\nexport const DEFAULT_MCP_SERVER_VERSION = '1.0.0'\n\n// The WebSocket subprotocol constant, declared here independently of the Node face's\n// `MCP_WEBSOCKET_SUBPROTOCOL` (`src/server/constants.ts`) — peer environment faces share\n// no import (AGENTS §2), so the same value is declared twice. The browser face's\n// `WebSocketClientTransport` defaults to this value when `protocols` is omitted, matching\n// `createWebSocketServer`'s unconditional echo.\n\n/**\n * The WebSocket subprotocol `createWebSocketClientTransport` requests by default —\n * `'mcp'`, matching `createWebSocketServer`'s unconditional `Sec-WebSocket-Protocol:\n * mcp` echo. Per RFC 6455 §4.1 a client MUST fail the connection if the server returns\n * a subprotocol it did not request; Node ≥ 22 (undici) enforces this strictly, so the\n * default bakes the correct value in. Override `WebSocketClientTransportOptions.protocols`\n * only when connecting to a foreign server that speaks a different subprotocol (or `[]`\n * for no subprotocol negotiation at all).\n */\nexport const MCP_WEBSOCKET_SUBPROTOCOL = 'mcp'\n","import type { MCPTransportInterface } from '@src/core'\nimport type { MessagePortTransportOptions } from '../types.js'\nimport { isString } from '@orkestrel/contract'\n\n/**\n * The browser-face `MessagePort` transport for the Model Context Protocol — a\n * {@link MCPTransportInterface} over a native `MessagePort`, the genuinely new\n * capability this face adds: MCP over `postMessage`.\n *\n * @remarks\n * - **Symmetric.** Unlike {@link import('./WebSocketClientTransport.js').WebSocketClientTransport}\n * / {@link import('./HTTPClientTransport.js').HTTPClientTransport} (CLIENT-only\n * carriers of `@src/core`'s `ClientTransportInterface`), a `MessagePort` is a\n * plain duplex channel — the SAME class implements `@src/core`'s\n * `MCPTransportInterface` and is handed to EITHER `bindServer` or\n * `bindClient`/`createDuplexClientTransport`; which role it plays comes entirely\n * from the binder it is given to, not from anything this class decides.\n * - **`start()` at construction — bind synchronously.** `MessagePort.start()` is only\n * REQUIRED when listening via `addEventListener` (as opposed to the `onmessage`\n * setter, which implies it) — this transport uses `addEventListener`, and\n * `MCPTransportInterface` has no separate open/connect step for the caller to hook\n * a start into, so the constructor calls `port.start()` immediately: the port\n * begins dispatching QUEUED messages the moment the transport exists. This is safe\n * inside `serveMCP`'s flow (the transport is synchronously handed to `bindServer`\n * before control returns to the event loop), but is a **footgun for direct use**:\n * if you construct `new MessagePortTransport({ port })` and then `await` anything\n * before calling `listen`, messages that arrived in the gap are DROPPED. **Bind\n * synchronously after construction** — do not interleave an `await` between\n * `new MessagePortTransport(…)` and `bindServer` / `listen`.\n * - **String payloads only.** `send` posts the message string as-is (`postMessage`\n * structured-clones it — a string clones to an identical string, so the wire stays\n * plain JSON-RPC text like every other transport in this package). Inbound: a\n * non-string `event.data` (a host or a misbehaving peer posting a structured\n * object) is IGNORED — dropped silently, never forwarded, never thrown (§14) —\n * because `MCPTransportInterface` carries no `error` channel for this port to\n * surface a non-string frame on (unlike `ClientTransportInterface`'s `emitter`);\n * silently ignoring is the total, contract-shaped choice.\n * - **`messageerror` is IGNORED, not routed to `closed`.** A `messageerror` event\n * (the structured-clone deserialization of an inbound message threw) reports one\n * BAD FRAME, not a dead channel — the port itself keeps working and later, well-\n * formed messages still arrive. Routing it to `closed` would tear down the\n * `bindServer`/`bindClient` wiring (and, transitively, every session it carries)\n * over a single malformed frame, which is far more destructive than dropping that\n * one frame — so this transport registers a `messageerror` listener that does\n * nothing, deliberately.\n * - **`close()`** is idempotent: it closes the underlying `port` (`MessagePort.close()`\n * disconnects it — further `postMessage` calls on EITHER end are silently\n * undelivered, per the platform contract) and fires the registered `closed`\n * handler exactly once, whether the caller closes it once or twice. There is no\n * native \"peer closed\" signal for a `MessagePort` (unlike a WebSocket's `close`\n * event) — `closed` fires ONLY from this transport's own `close()`.\n * - **Single-handler-replace (the port contract, `@src/core`'s `MCPTransportInterface`\n * doc).** `listen`/`closed` each hold the ONE currently registered handler; a\n * second call REPLACES the first rather than adding a second subscriber.\n *\n * @example\n * ```ts\n * const { port1, port2 } = new MessageChannel()\n * const serverTransport = new MessagePortTransport({ port: port1 })\n * bindServer(server, serverTransport) // port1 side dispatches inbound requests\n *\n * const clientTransport = new MessagePortTransport({ port: port2 })\n * const client = createMCPClient({ transport: createDuplexClientTransport(clientTransport) })\n * bindClient(client, clientTransport) // port2 side is the client's carrier\n * ```\n */\nexport class MessagePortTransport implements MCPTransportInterface {\n\treadonly #port: MessagePort\n\t#onMessage: ((message: string) => void) | undefined = undefined\n\t#onClosed: (() => void) | undefined = undefined\n\t#closed = false\n\n\tconstructor(options: MessagePortTransportOptions) {\n\t\tthis.#port = options.port\n\t\tthis.#port.addEventListener('message', (event: MessageEvent) => this.#receive(event.data))\n\t\tthis.#port.addEventListener('messageerror', () => {\n\t\t\t// Intentionally ignored — one bad frame, not a dead channel; see class doc.\n\t\t})\n\t\tthis.#port.start()\n\t}\n\n\tsend(message: string): void {\n\t\tif (this.#closed) return\n\t\tthis.#port.postMessage(message)\n\t}\n\n\tlisten(handler: (message: string) => void): void {\n\t\tthis.#onMessage = handler\n\t}\n\n\tclosed(handler: () => void): void {\n\t\tthis.#onClosed = handler\n\t}\n\n\tclose(): void {\n\t\tif (this.#closed) return\n\t\tthis.#closed = true\n\t\tthis.#port.close()\n\t\tthis.#onClosed?.()\n\t}\n\n\t// Decode one inbound `postMessage` payload: a non-string `data` is dropped, never\n\t// forwarded (§14 — this port carries only plain JSON-RPC text). A string reaches the\n\t// registered `listen` handler unchanged (the string IS the JSON-RPC message; parsing is\n\t// entirely the core's concern, per the port contract).\n\t#receive(data: unknown): void {\n\t\tif (!isString(data)) return\n\t\tthis.#onMessage?.(data)\n\t}\n}\n","import type { JSONRPCMessage, MCPServerInterface } from '@src/core'\nimport type { SSEParserInterface } from '@orkestrel/sse'\nimport type { ServeMCPOptions, ScopeTransportInterface } from './types.js'\nimport { bindServer, parseJSONRPCMessage } from '@src/core'\nimport { isString } from '@orkestrel/contract'\nimport { createSSEParser } from '@orkestrel/sse'\nimport { MessagePortTransport } from './transports/MessagePortTransport.js'\n\n// The MCP browser-transport helpers (AGENTS §4.3 module-scope names — no entity\n// context). `decodeEvent` and `readEventStream` are the browser face's copies of the\n// Node face's SAME-NAMED helpers (`src/server/helpers.ts`) — peer environment faces\n// (AGENTS §2) share no import, so the CLIENT-side SSE decode step (reused by\n// `transports/HTTPClientTransport.ts`) is declared once here too. Both are total and\n// narrow at the boundary, never `as` (AGENTS §14): a malformed / non-message SSE\n// `data:` event is dropped, never thrown.\n//\n// `createScopeMessageListener` is the bootstrap factory's per-event dispatcher, extracted here\n// (AGENTS §5 — no function is declared inside another function body) so\n// `serveMCPScope` merely CALLS it and stores the RETURNED closure (an ordinary\n// value assignment, not an inline function literal) for `addEventListener` /\n// `removeEventListener` to share the same reference.\n\n/**\n * Decode one SSE event's `data` string into a {@link JSONRPCMessage}, or `undefined`\n * when it is not one — the per-event step {@link readEventStream} folds over.\n *\n * @remarks\n * `JSON.parse`s the `data` (the server serializes the JSON-RPC envelope as the\n * event's `data`) inside a try/catch and narrows the parsed value with\n * `parseJSONRPCMessage`. Total (§14): malformed JSON or a non-message value yields\n * `undefined`, never throws.\n *\n * @param data - One SSE event's `data` payload\n * @returns The decoded {@link JSONRPCMessage}, or `undefined`\n */\nexport function decodeEvent(data: string): JSONRPCMessage | undefined {\n\ttry {\n\t\treturn parseJSONRPCMessage(JSON.parse(data))\n\t} catch {\n\t\treturn undefined\n\t}\n}\n\n/**\n * Decode a `fetch` Response's Server-Sent-Events body into the JSON-RPC messages it\n * carried — the CLIENT-side inverse of the server's Streamable-HTTP SSE response.\n *\n * @remarks\n * Reads the whole `response.body` stream chunk-by-chunk through a `TextDecoder({\n * stream: true })` (handling a multi-byte char split across reads) and\n * `@orkestrel/sse`'s {@link SSEParserInterface} (handling a partial line / in-progress\n * event split across reads), then narrows each dispatched event's `data` to a\n * {@link JSONRPCMessage} via {@link decodeEvent} (so a non-message / non-JSON `data:`\n * event is DROPPED, never thrown — total, §14). A `null` body (no stream) yields no\n * messages; {@link import('./transports/HTTPClientTransport.js').HTTPClientTransport}\n * reads a request/response SSE reply (the server sends one `data:` event then ends),\n * so this drains to completion.\n *\n * @param response - The SSE `fetch` Response to decode (its `body` is read to completion)\n * @returns Every {@link JSONRPCMessage} the stream carried, in order\n */\nexport async function readEventStream(response: Response): Promise<readonly JSONRPCMessage[]> {\n\tconst body = response.body\n\tif (body === null) return []\n\tconst reader = body.getReader()\n\tconst decoder = new TextDecoder()\n\tconst parser: SSEParserInterface = createSSEParser()\n\tconst messages: JSONRPCMessage[] = []\n\ttry {\n\t\tfor (;;) {\n\t\t\tconst { done, value } = await reader.read()\n\t\t\tif (done) break\n\t\t\tfor (const event of parser.parse(decoder.decode(value, { stream: true }))) {\n\t\t\t\tconst message = decodeEvent(event.data)\n\t\t\t\tif (message !== undefined) messages.push(message)\n\t\t\t}\n\t\t}\n\t} finally {\n\t\treader.releaseLock()\n\t}\n\treturn messages\n}\n\n/**\n * Build `serveMCPScope`'s `message`-event listener — the unified\n * dispatcher that routes EVERY inbound event on a hostable scope, portless or\n * port-bearing, to the right binding.\n *\n * @remarks\n * Port-bearing events (`event.ports.length > 0`) are gated by `options.accept` FIRST\n * — when the gate returns `false` the event is dropped entirely (no binding, no reply).\n * Accepted events spawn a fresh `MessagePortTransport` over `event.ports[0]`,\n * `bindServer` `server` onto it, and record a teardown (`unbind` then `transport.close()`)\n * into `teardowns`. A port that was already seen is IGNORED — repeated delivery of the\n * same `MessagePort` would create duplicate bindings over one port (→ duplicated replies),\n * so the listener tracks seen ports and silently drops repeats.\n *\n * This branch fires on EITHER a Service-Worker-shaped scope (its normal per-client\n * channel) or a dedicated-worker-shaped one that happens to receive a port-bearing event\n * (the unified design's deliberate cross-case, needing no upfront shape flag). An event\n * with NO ports and a STRING `data` is pushed onto `scopeTransport.deliver` (the\n * implicit, already-bound scope channel); any other event (no ports, non-string data)\n * is silently dropped — total (§14), never throws.\n *\n * @param server - The `MCPServerInterface` every spawned/implicit binding dispatches over\n * @param scopeTransport - The implicit scope channel (already `bindServer`-bound) portless events deliver onto\n * @param teardowns - The shared teardown set `serveMCPScope`'s dispose drains; each port-bearing event adds one entry\n * @param options - The `ServeMCPOptions` (for `options.accept`)\n * @returns The `message`-event listener to register (and later remove) on the scope\n *\n * @example\n * ```ts\n * const teardowns = new Set<() => void>()\n * const scopeTransport = createScopeTransport(scope)\n * bindServer(server, scopeTransport)\n * const onMessage = createScopeMessageListener(server, scopeTransport, teardowns, options)\n * scope.addEventListener('message', onMessage)\n * ```\n */\nexport function createScopeMessageListener(\n\tserver: MCPServerInterface,\n\tscopeTransport: ScopeTransportInterface,\n\tteardowns: Set<() => void>,\n\toptions: ServeMCPOptions,\n): (event: MessageEvent) => void {\n\tconst seen = new Set<MessagePort>()\n\treturn (event: MessageEvent): void => {\n\t\tconst ports = event.ports\n\t\tif (ports.length > 0) {\n\t\t\t// Gate: consult accept (origin/identity check) before binding.\n\t\t\tif (options.accept !== undefined && !options.accept(event)) return\n\t\t\tconst port = ports[0]\n\t\t\tif (port === undefined) return\n\t\t\t// Deduplicate: repeated delivery of the same port would create duplicate bindings.\n\t\t\tif (seen.has(port)) return\n\t\t\tseen.add(port)\n\t\t\tconst transport = new MessagePortTransport({ port })\n\t\t\tconst unbind = bindServer(server, transport)\n\t\t\tteardowns.add(() => {\n\t\t\t\tunbind()\n\t\t\t\ttransport.close()\n\t\t\t})\n\t\t\treturn\n\t\t}\n\t\tif (isString(event.data)) scopeTransport.deliver(event.data)\n\t}\n}\n","import type { ClientTransportEventMap, ClientTransportInterface, JSONRPCMessage } from '@src/core'\nimport type { EmitterInterface } from '@orkestrel/emitter'\nimport type { HTTPClientTransportOptions } from '../types.js'\nimport {\n\tisJSONRPCResponse,\n\tisMCPVersion,\n\tisModernRequest,\n\tparseJSONRPCMessage,\n\tparseRequestContext,\n} from '@src/core'\nimport { isRecord, isString } from '@orkestrel/contract'\nimport { Emitter } from '@orkestrel/emitter'\nimport {\n\tMCP_METHOD_HEADER,\n\tMCP_NAME_HEADER,\n\tMCP_PROTOCOL_VERSION_HEADER,\n\tMCP_SESSION_HEADER,\n} from '../constants.js'\nimport { readEventStream } from '../helpers.js'\n\n/**\n * The browser-face HTTP CLIENT transport for the Model Context Protocol — a\n * {@link ClientTransportInterface} that drives a REMOTE Streamable-HTTP MCP server\n * over the native `fetch`, the browser sibling of the Node face's\n * {@link import('@src/server').HTTPClientTransport}, honoring the SAME\n * `mcp-session-id` semantics so it interoperates with an `MCPSession`-based server\n * unchanged.\n *\n * @remarks\n * - **Request/response over `fetch`.** `send(message)` POSTs the JSON-serialized\n * message to `options.url` with `content-type: application/json` and an\n * `Accept` of BOTH `application/json` and `text/event-stream` (so the server may\n * answer with either framing) — plus any `options.headers` (e.g. an\n * `Authorization` bearer). It then decodes the reply and emits each decoded\n * {@link JSONRPCMessage} on the `message` event the\n * {@link import('@src/core').MCPClientInterface} subscribes to.\n * - **Both reply framings.** A `200` with an `application/json` body is parsed with\n * `parseJSONRPCMessage`; a `200` with a `text/event-stream` body is decoded via the\n * `@orkestrel/sse` {@link import('@orkestrel/sse').SSEParserInterface} (the browser\n * face's own `readEventStream`) — the inverse of the server's `openStream` seam, so\n * the wire round-trips. A `202` Accepted (a notification) carries no body and emits\n * nothing.\n * - **Session and protocol headers.** `start()` is a no-op (a\n * request/response transport opens no long-lived connection). The\n * `mcp-session-id` response header, when a STATEFUL server sends one (on\n * `initialize`), is captured into `session` and then ECHOED as the\n * `mcp-session-id` request header on every SUBSEQUENT request — so an\n * `MCPClient` passes a stateful server's session validation. The\n * initialize result's `protocolVersion` is likewise captured, but only\n * when it is a SUPPORTED value, and echoed as `mcp-protocol-version` alone on\n * subsequent legacy requests. Modern requests instead derive protocol and method\n * headers from the message, plus the name header only for `tools/call`.\n * Before initialize returns, neither captured legacy header is sent.\n * `close()` clears the captured protocol so a reconnect's `initialize`\n * POST is headerless; the captured `session` persists across `close()`.\n * - **Total at the boundary (§14).** Every reply is narrowed (`parseJSONRPCMessage`,\n * the SSE decoder) — a non-message reply is dropped, never asserted; a `fetch` /\n * decode failure surfaces on the `error` event rather than escaping `send`.\n * - **Observable (§13).** Owns the `emitter` ({@link ClientTransportEventMap}); fires\n * `message` per decoded reply, `error` on a fault, and `close` on `close()`.\n *\n * @example\n * ```ts\n * const transport = new HTTPClientTransport({ url: 'http://localhost:3000/mcp' })\n * const client = new MCPClient({ transport })\n * await client.connect()\n * ```\n */\nexport class HTTPClientTransport implements ClientTransportInterface {\n\treadonly #emitter: Emitter<ClientTransportEventMap>\n\treadonly #url: string\n\treadonly #headers: Readonly<Record<string, string>>\n\treadonly #fetch: typeof fetch\n\treadonly #timeout: number | undefined\n\t#session: string | undefined = undefined\n\t#protocol: string | undefined = undefined\n\n\tconstructor(options: HTTPClientTransportOptions) {\n\t\tthis.#emitter = new Emitter<ClientTransportEventMap>()\n\t\tthis.#url = options.url\n\t\tthis.#headers = options.headers ?? {}\n\t\tthis.#fetch = options.fetch ?? globalThis.fetch.bind(globalThis)\n\t\tthis.#timeout = options.timeout\n\t}\n\n\tget emitter(): EmitterInterface<ClientTransportEventMap> {\n\t\treturn this.#emitter\n\t}\n\n\tget session(): string | undefined {\n\t\treturn this.#session\n\t}\n\n\tasync start(): Promise<void> {\n\t\t// A request/response transport opens no long-lived connection — `send` issues each\n\t\t// `fetch` on demand. Nothing to arm.\n\t}\n\n\tasync send(message: JSONRPCMessage): Promise<void> {\n\t\tlet response: Response\n\t\ttry {\n\t\t\tresponse = await this.#fetch(this.#url, {\n\t\t\t\tmethod: 'POST',\n\t\t\t\theaders: {\n\t\t\t\t\t'content-type': 'application/json',\n\t\t\t\t\taccept: 'application/json, text/event-stream',\n\t\t\t\t\t// Echo a captured session id so a STATEFUL server validates the request; before\n\t\t\t\t\t// `initialize` returns one `#session` is undefined → no header (safe for a\n\t\t\t\t\t// stateless server). A caller `headers` key still wins (merged last).\n\t\t\t\t\t...(this.#session === undefined ? {} : { [MCP_SESSION_HEADER]: this.#session }),\n\t\t\t\t\t...this.#buildHeaders(message),\n\t\t\t\t\t...this.#headers,\n\t\t\t\t},\n\t\t\t\tbody: JSON.stringify(message),\n\t\t\t\t...(this.#timeout === undefined ? {} : { signal: AbortSignal.timeout(this.#timeout) }),\n\t\t\t})\n\t\t} catch (error) {\n\t\t\t// A network-level failure (connection refused, DNS) — surface it for observation;\n\t\t\t// the client's per-request deadline still rejects the pending request.\n\t\t\tthis.#emitter.emit('error', error)\n\t\t\treturn\n\t\t}\n\t\t// Capture a server-assigned session id (a stateless server sends none) so it is echoed\n\t\t// on subsequent requests; a missing header leaves `session` unchanged.\n\t\tconst session = response.headers.get(MCP_SESSION_HEADER)\n\t\tif (session !== null) this.#session = session\n\t\tawait this.#deliver(response)\n\t}\n\n\t// Clear the captured protocol before emitting `close`, so a reconnect's `initialize`\n\t// POST carries no `mcp-protocol-version` header (the captured `session` is untouched).\n\tasync close(): Promise<void> {\n\t\tthis.#protocol = undefined\n\t\tthis.#emitter.emit('close')\n\t}\n\n\t#buildHeaders(message: JSONRPCMessage): Readonly<Record<string, string>> {\n\t\tif (isModernRequest(message)) {\n\t\t\tconst context = parseRequestContext(message)\n\t\t\tconst name = message.params?.['name']\n\t\t\treturn {\n\t\t\t\t...(context === undefined ? {} : { [MCP_PROTOCOL_VERSION_HEADER]: context.version }),\n\t\t\t\t[MCP_METHOD_HEADER]: message.method,\n\t\t\t\t...(message.method === 'tools/call' && isString(name) ? { [MCP_NAME_HEADER]: name } : {}),\n\t\t\t}\n\t\t}\n\t\treturn this.#protocol === undefined ? {} : { [MCP_PROTOCOL_VERSION_HEADER]: this.#protocol }\n\t}\n\n\t// Decode a reply and emit each carried message. A 202 (notification accepted) has no\n\t// body — emit nothing. An `application/json` body is one envelope; a `text/event-stream`\n\t// body is decoded via the browser-face `readEventStream` (one or more `data:` events). A\n\t// decode failure surfaces on `error` rather than escaping.\n\tasync #deliver(response: Response): Promise<void> {\n\t\tif (response.status === 202) return\n\t\tconst type = response.headers.get('content-type') ?? ''\n\t\ttry {\n\t\t\tif (type.includes('text/event-stream')) {\n\t\t\t\tfor (const message of await readEventStream(response)) this.#capture(message)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (type.includes('application/json')) {\n\t\t\t\tconst message = parseJSONRPCMessage(await response.json())\n\t\t\t\tif (message !== undefined) this.#capture(message)\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tthis.#emitter.emit('error', error)\n\t\t}\n\t}\n\n\t// Capture the negotiated SUPPORTED protocol from the initialize result before emitting\n\t// the message, so the next request carries its required protocol-version header; any\n\t// other value (missing or unsupported) is ignored and leaves `#protocol` unchanged.\n\t#capture(message: JSONRPCMessage): void {\n\t\tif (\n\t\t\tisJSONRPCResponse(message) &&\n\t\t\tisRecord(message.result) &&\n\t\t\tisMCPVersion(message.result['protocolVersion'])\n\t\t) {\n\t\t\tthis.#protocol = message.result['protocolVersion']\n\t\t}\n\t\tthis.#emitter.emit('message', message)\n\t}\n}\n","import type { ClientTransportEventMap, ClientTransportInterface, JSONRPCMessage } from '@src/core'\nimport type { EmitterInterface } from '@orkestrel/emitter'\nimport type { WebSocketClientTransportOptions } from '../types.js'\nimport { parseJSONRPCMessage } from '@src/core'\nimport { isString } from '@orkestrel/contract'\nimport { Emitter } from '@orkestrel/emitter'\nimport { MCP_WEBSOCKET_SUBPROTOCOL } from '../constants.js'\n\n/**\n * The browser-face WebSocket CLIENT transport for the Model Context Protocol — a\n * {@link ClientTransportInterface} that drives a REMOTE MCP server over the native\n * `WebSocket` global, the browser sibling of the Node face's\n * {@link import('@src/server').WebSocketClientTransport}.\n *\n * @remarks\n * - **Host-performed handshake.** `start()` opens `new WebSocket(url, protocols)` and\n * waits for the native `'open'` event — the RFC 6455 handshake itself is entirely\n * the host's concern, so this transport carries none of the Node client's\n * `node:crypto` / `node:http(s)` machinery. A connection failure (the native\n * `'error'` event while not yet `OPEN`) REJECTS `start()`.\n * - **Queued sends.** `send` writes each message as one text frame immediately once\n * the socket is `OPEN`; a `send` issued before `'open'` fires (or before `start()`\n * is even called) is QUEUED and flushed, IN ORDER, the moment the socket opens —\n * so a caller need not await `start()` before calling `send`.\n * - **Inbound (`message`).** Each decoded text frame is `JSON.parse`d (guarded) and\n * narrowed with `parseJSONRPCMessage` — a well-formed {@link JSONRPCMessage}\n * re-emits on this transport's `message` event; a non-text (binary) frame or a\n * non-JSON / non-message text frame surfaces on `error` and is DROPPED (§14 — never\n * throws on adversarial wire input).\n * - **`close()`** closes the underlying socket and fires `close` (idempotent); the\n * socket's native `close` event (a server-initiated close) fires the SAME `close`\n * exactly once total — `close()` first flips the guard, so the native event never\n * double-emits. **This transport is not reusable after `close()`** — a `send` issued\n * after `close()` is silently dropped (not queued, not delivered even on a later\n * `start()`).\n * - **Observable (§13).** Owns the `emitter` ({@link ClientTransportEventMap}); every\n * emit the emitter isolates a listener throw; `error` is a DOMAIN event (a\n * transport-level fault).\n *\n * @example\n * ```ts\n * const transport = new WebSocketClientTransport({ url: 'ws://localhost:3000/mcp' })\n * const client = new MCPClient({ transport })\n * await client.connect() // the browser handshakes, then the MCP initialize runs over WS frames\n * ```\n */\nexport class WebSocketClientTransport implements ClientTransportInterface {\n\treadonly #emitter: Emitter<ClientTransportEventMap>\n\treadonly #url: string\n\treadonly #protocols: string | string[] | undefined\n\t#socket: WebSocket | undefined = undefined\n\t#queue: string[] = []\n\t#closed = false\n\n\tconstructor(options: WebSocketClientTransportOptions) {\n\t\tthis.#emitter = new Emitter<ClientTransportEventMap>()\n\t\tthis.#url = options.url\n\t\tconst protocols = options.protocols\n\t\t// Default to MCP_WEBSOCKET_SUBPROTOCOL when `protocols` is omitted — matching\n\t\t// createWebSocketServer's unconditional echo. An empty array means \"no subprotocol\",\n\t\t// overriding the default explicitly for foreign servers.\n\t\tthis.#protocols =\n\t\t\ttypeof protocols === 'string'\n\t\t\t\t? protocols\n\t\t\t\t: protocols === undefined\n\t\t\t\t\t? MCP_WEBSOCKET_SUBPROTOCOL\n\t\t\t\t\t: protocols.length === 0\n\t\t\t\t\t\t? undefined\n\t\t\t\t\t\t: [...protocols]\n\t}\n\n\tget emitter(): EmitterInterface<ClientTransportEventMap> {\n\t\treturn this.#emitter\n\t}\n\n\tget session(): string | undefined {\n\t\treturn undefined\n\t}\n\n\tasync start(): Promise<void> {\n\t\t// Already connected — a second `connect()` short-circuits in the client, but guard here\n\t\t// too (idempotent open).\n\t\tif (this.#socket !== undefined) return\n\t\tthis.#closed = false\n\t\tconst socket = new WebSocket(this.#url, this.#protocols)\n\t\tthis.#socket = socket\n\t\tthis.#bind(socket)\n\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\tsocket.addEventListener(\n\t\t\t\t'open',\n\t\t\t\t() => {\n\t\t\t\t\tthis.#flush(socket)\n\t\t\t\t\tresolve()\n\t\t\t\t},\n\t\t\t\t{ once: true },\n\t\t\t)\n\t\t\tsocket.addEventListener(\n\t\t\t\t'error',\n\t\t\t\t() => {\n\t\t\t\t\tif (socket.readyState !== WebSocket.OPEN) {\n\t\t\t\t\t\tthis.#socket = undefined\n\t\t\t\t\t\treject(new Error('WebSocket connection failed'))\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t{ once: true },\n\t\t\t)\n\t\t})\n\t}\n\n\tasync send(message: JSONRPCMessage): Promise<void> {\n\t\t// After close(), silently drop — never queue (a closed transport is not reusable;\n\t\t// queued messages would resurrect on a later start() which is not a supported pattern).\n\t\tif (this.#closed) return\n\t\tconst text = JSON.stringify(message)\n\t\tconst socket = this.#socket\n\t\tif (socket !== undefined && socket.readyState === WebSocket.OPEN) socket.send(text)\n\t\telse this.#queue.push(text)\n\t}\n\n\tasync close(): Promise<void> {\n\t\tif (this.#closed) return\n\t\tthis.#closed = true\n\t\tconst socket = this.#socket\n\t\tthis.#socket = undefined\n\t\tif (socket !== undefined) socket.close()\n\t\tthis.#emitter.emit('close')\n\t}\n\n\t// Bridge the native socket's events onto the transport: a text frame → `message`\n\t// (decoded + narrowed), the socket close → `close`, a socket fault → `error`.\n\t#bind(socket: WebSocket): void {\n\t\tsocket.addEventListener('message', (event: MessageEvent) => this.#receive(event.data))\n\t\tsocket.addEventListener('close', () => this.#onClose())\n\t\tsocket.addEventListener('error', (event) => this.#emitter.emit('error', event))\n\t}\n\n\t// Write every queued (pre-open) message, in order, as the socket opens.\n\t#flush(socket: WebSocket): void {\n\t\tfor (const text of this.#queue.splice(0)) socket.send(text)\n\t}\n\n\t// Decode one inbound frame: a non-text (binary) frame is rejected without a throw; a\n\t// text frame is `JSON.parse`d → `parseJSONRPCMessage`. A well-formed message re-emits on\n\t// `message`; a malformed / non-message frame surfaces on `error` and is dropped (§14 —\n\t// never throws on adversarial wire input).\n\t#receive(data: unknown): void {\n\t\tif (!isString(data)) {\n\t\t\tthis.#emitter.emit('error', new Error('non-text WebSocket frame'))\n\t\t\treturn\n\t\t}\n\t\tlet parsed: unknown\n\t\ttry {\n\t\t\tparsed = JSON.parse(data)\n\t\t} catch (error) {\n\t\t\tthis.#emitter.emit('error', error)\n\t\t\treturn\n\t\t}\n\t\tconst message = parseJSONRPCMessage(parsed)\n\t\tif (message === undefined) {\n\t\t\tthis.#emitter.emit('error', new Error('non-JSON-RPC WebSocket frame'))\n\t\t\treturn\n\t\t}\n\t\tthis.#emitter.emit('message', message)\n\t}\n\n\t// The socket closed underneath us — fire `close` once (a `close()` call already flipped\n\t// `#closed`, so it does not double-emit).\n\t#onClose(): void {\n\t\tif (this.#closed) return\n\t\tthis.#closed = true\n\t\tthis.#socket = undefined\n\t\tthis.#emitter.emit('close')\n\t}\n}\n","import type { ClientTransportInterface, MCPTransportInterface } from '@src/core'\nimport type {\n\tHTTPClientTransportOptions,\n\tMessagePortTransportOptions,\n\tScopeTransportInterface,\n\tServeMCPOptions,\n\tServeMCPScopeInterface,\n\tWebSocketClientTransportOptions,\n} from './types.js'\nimport { bindServer, createMCPServer } from '@src/core'\nimport { DEFAULT_MCP_SERVER_NAME, DEFAULT_MCP_SERVER_VERSION } from './constants.js'\nimport { createScopeMessageListener } from './helpers.js'\nimport { HTTPClientTransport } from './transports/HTTPClientTransport.js'\nimport { MessagePortTransport } from './transports/MessagePortTransport.js'\nimport { WebSocketClientTransport } from './transports/WebSocketClientTransport.js'\n\n/**\n * Create the browser-face WebSocket CLIENT transport for an\n * {@link import('@src/core').MCPClientInterface} — a {@link ClientTransportInterface}\n * that drives a REMOTE MCP server over the native `WebSocket` global, the browser\n * sibling of the Node face's `createWebSocketClientTransport` (`@src/server`).\n *\n * @remarks\n * Hand it to `createMCPClient({ transport })`: `start()` (run by `client.connect()`)\n * opens `new WebSocket(options.url, options.protocols)` and awaits the native\n * `'open'` event — the RFC 6455 handshake itself is the browser's concern. Each\n * JSON-RPC message the client `send`s before the socket opens is QUEUED and flushed,\n * in order, once it does; each decoded reply is surfaced on the transport's\n * `message` event for the client's id correlation.\n *\n * @param options - `url` (the remote WebSocket endpoint; REQUIRED) and optional\n * `protocols` (the WebSocket subprotocol(s) to request); see\n * {@link WebSocketClientTransportOptions}\n * @returns A working {@link ClientTransportInterface} over the native `WebSocket`\n *\n * @example\n * ```ts\n * import { createMCPClient } from '@orkestrel/mcp'\n * import { createWebSocketClientTransport } from '@orkestrel/mcp/browser'\n *\n * const client = createMCPClient({\n * \ttransport: createWebSocketClientTransport({ url: 'ws://localhost:3000/mcp' }),\n * })\n * await client.connect()\n * const tools = await client.tools()\n * ```\n */\nexport function createWebSocketClientTransport(\n\toptions: WebSocketClientTransportOptions,\n): ClientTransportInterface {\n\treturn new WebSocketClientTransport(options)\n}\n\n/**\n * Create the browser-face HTTP CLIENT transport for an\n * {@link import('@src/core').MCPClientInterface} — a {@link ClientTransportInterface}\n * that drives a REMOTE Streamable-HTTP MCP server over the native `fetch`, the\n * browser sibling of the Node face's `createHTTPClientTransport` (`@src/server`).\n *\n * @remarks\n * Hand it to `createMCPClient({ transport })`: each JSON-RPC message the client\n * sends is `POST`ed to `options.url` with `content-type: application/json` and an\n * `Accept` of both `application/json` and `text/event-stream` (the server answers\n * with EITHER — a plain JSON envelope or a Streamable-HTTP SSE `data:` event,\n * decoded via `@orkestrel/sse`), and the reply is surfaced on the transport's\n * `message` event for the client's id correlation. Add `options.headers` (e.g. an\n * `Authorization` bearer) to reach a guarded server. `start` / `close` hold no\n * connection; against a STATEFUL server it captures the `mcp-session-id` from\n * `initialize` and echoes it on later requests. It also captures the initialize\n * result's `protocolVersion` and sends `mcp-protocol-version` alone on subsequent\n * legacy requests. Modern requests instead derive `mcp-protocol-version` and\n * `mcp-method` from the message, plus `mcp-name` only for `tools/call`, so the\n * same `MCPClient` passes either era's protocol gates without caller wiring.\n *\n * @param options - `url` (the remote endpoint; REQUIRED), optional `headers` merged\n * onto every request, optional `fetch` (default `globalThis.fetch`), and optional\n * `timeout` (ms, applied via `AbortSignal.timeout`); see\n * {@link HTTPClientTransportOptions}\n * @returns A working {@link ClientTransportInterface} over the native `fetch`\n *\n * @example\n * ```ts\n * import { createMCPClient } from '@orkestrel/mcp'\n * import { createHTTPClientTransport } from '@orkestrel/mcp/browser'\n *\n * const client = createMCPClient({\n * \ttransport: createHTTPClientTransport({ url: 'http://localhost:3000/mcp' }),\n * })\n * await client.connect()\n * const tools = await client.tools()\n * ```\n */\nexport function createHTTPClientTransport(\n\toptions: HTTPClientTransportOptions,\n): ClientTransportInterface {\n\treturn new HTTPClientTransport(options)\n}\n\n/**\n * Create the browser-face `MessagePort` transport — a\n * {@link import('@src/core').MCPTransportInterface} over a native `MessagePort`, the\n * SYMMETRIC carrier that works as either a server or a client transport depending on\n * which binder ({@link import('@src/core').bindServer} or\n * {@link import('@src/core').bindClient}) it is handed to.\n *\n * @remarks\n * `port.start()` runs at construction (see {@link MessagePortTransport}'s doc for\n * why); inbound payloads are string-only (a non-string `postMessage` payload is\n * dropped, never thrown); `messageerror` is ignored (one bad frame does not close the\n * channel); `close()` closes the port and fires `closed` exactly once.\n *\n * @param options - `port` (the `MessagePort` half to drive; REQUIRED); see\n * {@link MessagePortTransportOptions}\n * @returns A working {@link import('@src/core').MCPTransportInterface} over the port\n *\n * @example\n * ```ts\n * import { bindServer, createMCPServer } from '@orkestrel/mcp'\n * import { createMessagePortTransport } from '@orkestrel/mcp/browser'\n *\n * const { port1, port2 } = new MessageChannel()\n * bindServer(createMCPServer({ identity: { name: 's', version: '1.0.0' }, tools }), createMessagePortTransport({ port: port1 }))\n * ```\n */\nexport function createMessagePortTransport(\n\toptions: MessagePortTransportOptions,\n): MCPTransportInterface {\n\treturn new MessagePortTransport(options)\n}\n\n/**\n * Adapt a hostable {@link ServeMCPScopeInterface} (`self` in a dedicated Web Worker,\n * or any structurally matching double) into a {@link ScopeTransportInterface} — the\n * implicit, portless message channel `serveMCPScope` binds for the\n * dedicated-worker shape.\n *\n * @remarks\n * `send` writes each outbound string via `scope.postMessage`. `listen`/`closed`\n * register the SINGLE handler `deliver` / the underlying close path route through —\n * `serveMCPScope`'s own `scope` `message`-event listener calls `deliver(event.data)`\n * for every portless, string-payload event (there is no native registration point on\n * the scope itself for `serveMCPScope` to hand a `listen` handler to, so `deliver` is\n * the bridge). `close()` fires the registered `closed` handler — a scope has nothing\n * physically closable, so this is the only teardown signal available.\n *\n * @param scope - The hostable scope to adapt (structurally, `self` / `globalThis`\n * inside a dedicated Web Worker)\n * @returns A {@link ScopeTransportInterface} `serveMCPScope` binds and drives via `deliver`\n *\n * @example\n * ```ts\n * const scopeTransport = createScopeTransport(self)\n * const unbind = bindServer(server, scopeTransport)\n * ```\n */\nexport function createScopeTransport(scope: ServeMCPScopeInterface): ScopeTransportInterface {\n\tlet onMessage: ((message: string) => void) | undefined\n\tlet onClosed: (() => void) | undefined\n\treturn {\n\t\tsend(message: string): void {\n\t\t\tscope.postMessage(message)\n\t\t},\n\t\tlisten(handler: (message: string) => void): void {\n\t\t\tonMessage = handler\n\t\t},\n\t\tclosed(handler: () => void): void {\n\t\t\tonClosed = handler\n\t\t},\n\t\tclose(): void {\n\t\t\tonClosed?.()\n\t\t},\n\t\tdeliver(message: string): void {\n\t\t\tonMessage?.(message)\n\t\t},\n\t}\n}\n\n/**\n * Boot an `MCPServer` inside a hostable worker scope and wire its message events to it.\n *\n * @remarks\n * Port-bearing events are gated by `options.accept`, deduplicated by port, and receive\n * their own `MessagePortTransport` binding. Portless string events use the scope's\n * implicit channel. The returned disposer removes the listener, unbinds the implicit\n * channel, and closes every accepted port binding.\n *\n * @param scope - The hostable worker scope to wire\n * @param options - The tools, optional identity, and optional port-event gate\n * @returns An idempotent disposer for every binding owned by this call\n */\nexport function serveMCPScope(scope: ServeMCPScopeInterface, options: ServeMCPOptions): () => void {\n\tconst server = createMCPServer({\n\t\ttools: options.tools,\n\t\tidentity: {\n\t\t\tname: options.name ?? DEFAULT_MCP_SERVER_NAME,\n\t\t\tversion: options.version ?? DEFAULT_MCP_SERVER_VERSION,\n\t\t},\n\t})\n\tconst scopeTransport = createScopeTransport(scope)\n\tconst unbindScope = bindServer(server, scopeTransport)\n\tconst teardowns = new Set<() => void>()\n\tconst onMessage = createScopeMessageListener(server, scopeTransport, teardowns, options)\n\tscope.addEventListener('message', onMessage)\n\tlet disposed = false\n\treturn () => {\n\t\tif (disposed) return\n\t\tdisposed = true\n\t\tscope.removeEventListener('message', onMessage)\n\t\tunbindScope()\n\t\tfor (const teardown of teardowns) teardown()\n\t\tteardowns.clear()\n\t}\n}\n\n/**\n * Boot an `MCPServer` inside the current hostable worker scope.\n *\n * @param options - The tools, optional identity, and optional port-event gate\n * @returns The disposer returned by {@link serveMCPScope}\n */\nexport function serveMCP(options: ServeMCPOptions): () => void {\n\treturn serveMCPScope(globalThis, options)\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["#port","#receive","#closed","#onMessage","#onClosed","#emitter","#url","#headers","#fetch","#timeout","#session","#buildHeaders","#deliver","#protocol","#capture","#emitter","#url","#protocols","#socket","#closed","#bind","#flush","#queue","#receive","#onClose"],"sources":["../../../src/browser/constants.ts","../../../src/browser/transports/MessagePortTransport.ts","../../../src/browser/helpers.ts","../../../src/browser/transports/HTTPClientTransport.ts","../../../src/browser/transports/WebSocketClientTransport.ts","../../../src/browser/factories.ts"],"sourcesContent":["// The MCP browser-transport constants (AGENTS §5 constants file) — the wire-level\n// header names the browser-face HTTP client transport echoes, matching the Node\n// face's session, protocol-version, method, and name headers byte-for-byte. The\n// browser face imports nothing from `src/server` (peer environment faces, per\n// AGENTS §2), so the literals are declared once here too — the SAME strings, not\n// shared symbols.\n\n/**\n * The Streamable-HTTP transport header that carries the MCP session id. The browser\n * face's {@link import('./transports/HTTPClientTransport.js').HTTPClientTransport}\n * ECHOES this header exactly like the Node face's `HTTPClientTransport`\n * (`src/server`), so the same client interoperates with an `MCPSession`-based\n * server unchanged.\n */\nexport const MCP_SESSION_HEADER = 'mcp-session-id'\n\n/**\n * The Streamable-HTTP transport header carrying the MCP protocol version. Modern\n * requests derive it from their own `_meta`; legacy requests echo the negotiated\n * initialize result on each subsequent request.\n */\nexport const MCP_PROTOCOL_VERSION_HEADER = 'mcp-protocol-version'\n\n/**\n * The modern Streamable-HTTP request header carrying the JSON-RPC method. It is\n * emitted on every modern request and never on a legacy request.\n */\nexport const MCP_METHOD_HEADER = 'mcp-method'\n\n/**\n * The modern Streamable-HTTP request header carrying a named target. The browser\n * HTTP client emits it only for `tools/call`, from that request's `params.name`.\n */\nexport const MCP_NAME_HEADER = 'mcp-name'\n\n// `serveMCP` server-identity defaults — `src/core`'s `createMCPServer` REQUIRES\n// `name`/`version`, but `ServeMCPOptions` (this face's bootstrap) makes both optional\n// (mirroring the CLIENT identity defaults, `DEFAULT_MCP_CLIENT_NAME` /\n// `DEFAULT_MCP_CLIENT_VERSION`, `src/core/constants.ts`), so `serveMCPScope` falls\n// back to these when a caller omits them.\n\n/** The default server name `serveMCPScope` reports (`initialize`'s `serverInfo.name`) when `options.name` is omitted. */\nexport const DEFAULT_MCP_SERVER_NAME = 'taverna'\n\n/** The default server version `serveMCPScope` reports (`initialize`'s `serverInfo.version`) when `options.version` is omitted. */\nexport const DEFAULT_MCP_SERVER_VERSION = '1.0.0'\n\n// The WebSocket subprotocol constant, declared here independently of the Node face's\n// `MCP_WEBSOCKET_SUBPROTOCOL` (`src/server/constants.ts`) — peer environment faces share\n// no import (AGENTS §2), so the same value is declared twice. The browser face's\n// `WebSocketClientTransport` defaults to this value when `protocols` is omitted, matching\n// `createWebSocketServer`'s unconditional echo.\n\n/**\n * The WebSocket subprotocol `createWebSocketClientTransport` requests by default —\n * `'mcp'`, matching `createWebSocketServer`'s unconditional `Sec-WebSocket-Protocol:\n * mcp` echo. Per RFC 6455 §4.1 a client MUST fail the connection if the server returns\n * a subprotocol it did not request; Node ≥ 22 (undici) enforces this strictly, so the\n * default bakes the correct value in. Override `WebSocketClientTransportOptions.protocols`\n * only when connecting to a foreign server that speaks a different subprotocol (or `[]`\n * for no subprotocol negotiation at all).\n */\nexport const MCP_WEBSOCKET_SUBPROTOCOL = 'mcp'\n","import type { MCPTransportInterface } from '@src/core'\nimport type { MessagePortTransportOptions } from '../types.js'\nimport { isString } from '@orkestrel/contract'\n\n/**\n * The browser-face `MessagePort` transport for the Model Context Protocol — a\n * {@link MCPTransportInterface} over a native `MessagePort`, the genuinely new\n * capability this face adds: MCP over `postMessage`.\n *\n * @remarks\n * - **Symmetric.** Unlike {@link import('./WebSocketClientTransport.js').WebSocketClientTransport}\n * / {@link import('./HTTPClientTransport.js').HTTPClientTransport} (CLIENT-only\n * carriers of `@src/core`'s `MCPClientTransportInterface`), a `MessagePort` is a\n * plain duplex channel — the SAME class implements `@src/core`'s\n * `MCPTransportInterface` and is handed to EITHER `bindServer` or\n * `bindClient`/`createDuplexClientTransport`; which role it plays comes entirely\n * from the binder it is given to, not from anything this class decides.\n * - **`start()` at construction — bind synchronously.** `MessagePort.start()` is only\n * REQUIRED when listening via `addEventListener` (as opposed to the `onmessage`\n * setter, which implies it) — this transport uses `addEventListener`, and\n * `MCPTransportInterface` has no separate open/connect step for the caller to hook\n * a start into, so the constructor calls `port.start()` immediately: the port\n * begins dispatching QUEUED messages the moment the transport exists. This is safe\n * inside `serveMCP`'s flow (the transport is synchronously handed to `bindServer`\n * before control returns to the event loop), but is a **footgun for direct use**:\n * if you construct `new MessagePortTransport({ port })` and then `await` anything\n * before calling `listen`, messages that arrived in the gap are DROPPED. **Bind\n * synchronously after construction** — do not interleave an `await` between\n * `new MessagePortTransport(…)` and `bindServer` / `listen`.\n * - **String payloads only.** `send` posts the message string as-is (`postMessage`\n * structured-clones it — a string clones to an identical string, so the wire stays\n * plain JSON-RPC text like every other transport in this package). Inbound: a\n * non-string `event.data` (a host or a misbehaving peer posting a structured\n * object) is IGNORED — dropped silently, never forwarded, never thrown (§14) —\n * because `MCPTransportInterface` carries no `error` channel for this port to\n * surface a non-string frame on (unlike `MCPClientTransportInterface`'s `emitter`);\n * silently ignoring is the total, contract-shaped choice.\n * - **`messageerror` is IGNORED, not routed to `closed`.** A `messageerror` event\n * (the structured-clone deserialization of an inbound message threw) reports one\n * BAD FRAME, not a dead channel — the port itself keeps working and later, well-\n * formed messages still arrive. Routing it to `closed` would tear down the\n * `bindServer`/`bindClient` wiring (and, transitively, every session it carries)\n * over a single malformed frame, which is far more destructive than dropping that\n * one frame — so this transport registers a `messageerror` listener that does\n * nothing, deliberately.\n * - **`close()`** is idempotent: it closes the underlying `port` (`MessagePort.close()`\n * disconnects it — further `postMessage` calls on EITHER end are silently\n * undelivered, per the platform contract) and fires the registered `closed`\n * handler exactly once, whether the caller closes it once or twice. There is no\n * native \"peer closed\" signal for a `MessagePort` (unlike a WebSocket's `close`\n * event) — `closed` fires ONLY from this transport's own `close()`.\n * - **Single-handler-replace (the port contract, `@src/core`'s `MCPTransportInterface`\n * doc).** `listen`/`closed` each hold the ONE currently registered handler; a\n * second call REPLACES the first rather than adding a second subscriber.\n *\n * @example\n * ```ts\n * const { port1, port2 } = new MessageChannel()\n * const serverTransport = new MessagePortTransport({ port: port1 })\n * bindServer(server, serverTransport) // port1 side dispatches inbound requests\n *\n * const clientTransport = new MessagePortTransport({ port: port2 })\n * const client = createMCPClient({ transport: createDuplexClientTransport(clientTransport) })\n * bindClient(client, clientTransport) // port2 side is the client's carrier\n * ```\n */\nexport class MessagePortTransport implements MCPTransportInterface {\n\treadonly #port: MessagePort\n\t#onMessage: ((message: string) => void) | undefined = undefined\n\t#onClosed: (() => void) | undefined = undefined\n\t#closed = false\n\n\tconstructor(options: MessagePortTransportOptions) {\n\t\tthis.#port = options.port\n\t\tthis.#port.addEventListener('message', (event: MessageEvent) => this.#receive(event.data))\n\t\tthis.#port.addEventListener('messageerror', () => {\n\t\t\t// Intentionally ignored — one bad frame, not a dead channel; see class doc.\n\t\t})\n\t\tthis.#port.start()\n\t}\n\n\tsend(message: string): void {\n\t\tif (this.#closed) return\n\t\tthis.#port.postMessage(message)\n\t}\n\n\tlisten(handler: (message: string) => void): void {\n\t\tthis.#onMessage = handler\n\t}\n\n\tclosed(handler: () => void): void {\n\t\tthis.#onClosed = handler\n\t}\n\n\tclose(): void {\n\t\tif (this.#closed) return\n\t\tthis.#closed = true\n\t\tthis.#port.close()\n\t\tthis.#onClosed?.()\n\t}\n\n\t// Decode one inbound `postMessage` payload: a non-string `data` is dropped, never\n\t// forwarded (§14 — this port carries only plain JSON-RPC text). A string reaches the\n\t// registered `listen` handler unchanged (the string IS the JSON-RPC message; parsing is\n\t// entirely the core's concern, per the port contract).\n\t#receive(data: unknown): void {\n\t\tif (!isString(data)) return\n\t\tthis.#onMessage?.(data)\n\t}\n}\n","import type { JSONRPCMessage, MCPServerInterface } from '@src/core'\nimport type { SSEParserInterface } from '@orkestrel/sse'\nimport type { ServeMCPOptions, ScopeTransportInterface } from './types.js'\nimport { bindServer, parseJSONRPCMessage } from '@src/core'\nimport { isString } from '@orkestrel/contract'\nimport { createSSEParser } from '@orkestrel/sse'\nimport { MessagePortTransport } from './transports/MessagePortTransport.js'\n\n// The MCP browser-transport helpers (AGENTS §4.3 module-scope names — no entity\n// context). `decodeEvent` and `readEventStream` are the browser face's copies of the\n// Node face's SAME-NAMED helpers (`src/server/helpers.ts`) — peer environment faces\n// (AGENTS §2) share no import, so the CLIENT-side SSE decode step (reused by\n// `transports/HTTPClientTransport.ts`) is declared once here too. Both are total and\n// narrow at the boundary, never `as` (AGENTS §14): a malformed / non-message SSE\n// `data:` event is dropped, never thrown.\n//\n// `createScopeMessageListener` is the bootstrap factory's per-event dispatcher, extracted here\n// (AGENTS §5 — no function is declared inside another function body) so\n// `serveMCPScope` merely CALLS it and stores the RETURNED closure (an ordinary\n// value assignment, not an inline function literal) for `addEventListener` /\n// `removeEventListener` to share the same reference.\n\n/**\n * Decode one SSE event's `data` string into a {@link JSONRPCMessage}, or `undefined`\n * when it is not one — the per-event step {@link readEventStream} folds over.\n *\n * @remarks\n * `JSON.parse`s the `data` (the server serializes the JSON-RPC envelope as the\n * event's `data`) inside a try/catch and narrows the parsed value with\n * `parseJSONRPCMessage`. Total (§14): malformed JSON or a non-message value yields\n * `undefined`, never throws.\n *\n * @param data - One SSE event's `data` payload\n * @returns The decoded {@link JSONRPCMessage}, or `undefined`\n */\nexport function decodeEvent(data: string): JSONRPCMessage | undefined {\n\ttry {\n\t\treturn parseJSONRPCMessage(JSON.parse(data))\n\t} catch {\n\t\treturn undefined\n\t}\n}\n\n/**\n * Decode a `fetch` Response's Server-Sent-Events body into the JSON-RPC messages it\n * carried — the CLIENT-side inverse of the server's Streamable-HTTP SSE response.\n *\n * @remarks\n * Reads the whole `response.body` stream chunk-by-chunk through a `TextDecoder({\n * stream: true })` (handling a multi-byte char split across reads) and\n * `@orkestrel/sse`'s {@link SSEParserInterface} (handling a partial line / in-progress\n * event split across reads), then narrows each dispatched event's `data` to a\n * {@link JSONRPCMessage} via {@link decodeEvent} (so a non-message / non-JSON `data:`\n * event is DROPPED, never thrown — total, §14). A `null` body (no stream) yields no\n * messages; {@link import('./transports/HTTPClientTransport.js').HTTPClientTransport}\n * reads a request/response SSE reply (the server sends one `data:` event then ends),\n * so this drains to completion.\n *\n * @param response - The SSE `fetch` Response to decode (its `body` is read to completion)\n * @returns Every {@link JSONRPCMessage} the stream carried, in order\n */\nexport async function readEventStream(response: Response): Promise<readonly JSONRPCMessage[]> {\n\tconst body = response.body\n\tif (body === null) return []\n\tconst reader = body.getReader()\n\tconst decoder = new TextDecoder()\n\tconst parser: SSEParserInterface = createSSEParser()\n\tconst messages: JSONRPCMessage[] = []\n\ttry {\n\t\tfor (;;) {\n\t\t\tconst { done, value } = await reader.read()\n\t\t\tif (done) break\n\t\t\tfor (const event of parser.parse(decoder.decode(value, { stream: true }))) {\n\t\t\t\tconst message = decodeEvent(event.data)\n\t\t\t\tif (message !== undefined) messages.push(message)\n\t\t\t}\n\t\t}\n\t} finally {\n\t\treader.releaseLock()\n\t}\n\treturn messages\n}\n\n/**\n * Build `serveMCPScope`'s `message`-event listener — the unified\n * dispatcher that routes EVERY inbound event on a hostable scope, portless or\n * port-bearing, to the right binding.\n *\n * @remarks\n * Port-bearing events (`event.ports.length > 0`) are gated by `options.accept` FIRST\n * — when the gate returns `false` the event is dropped entirely (no binding, no reply).\n * Accepted events spawn a fresh `MessagePortTransport` over `event.ports[0]`,\n * `bindServer` `server` onto it, and record a teardown (`unbind` then `transport.close()`)\n * into `teardowns`. A port that was already seen is IGNORED — repeated delivery of the\n * same `MessagePort` would create duplicate bindings over one port (→ duplicated replies),\n * so the listener tracks seen ports and silently drops repeats.\n *\n * This branch fires on EITHER a Service-Worker-shaped scope (its normal per-client\n * channel) or a dedicated-worker-shaped one that happens to receive a port-bearing event\n * (the unified design's deliberate cross-case, needing no upfront shape flag). An event\n * with NO ports and a STRING `data` is pushed onto `scopeTransport.deliver` (the\n * implicit, already-bound scope channel); any other event (no ports, non-string data)\n * is silently dropped — total (§14), never throws.\n *\n * @param server - The `MCPServerInterface` every spawned/implicit binding dispatches over\n * @param scopeTransport - The implicit scope channel (already `bindServer`-bound) portless events deliver onto\n * @param teardowns - The shared teardown set `serveMCPScope`'s dispose drains; each port-bearing event adds one entry\n * @param options - The `ServeMCPOptions` (for `options.accept`)\n * @returns The `message`-event listener to register (and later remove) on the scope\n *\n * @example\n * ```ts\n * const teardowns = new Set<() => void>()\n * const scopeTransport = createScopeTransport(scope)\n * bindServer(server, scopeTransport)\n * const onMessage = createScopeMessageListener(server, scopeTransport, teardowns, options)\n * scope.addEventListener('message', onMessage)\n * ```\n */\nexport function createScopeMessageListener(\n\tserver: MCPServerInterface,\n\tscopeTransport: ScopeTransportInterface,\n\tteardowns: Set<() => void>,\n\toptions: ServeMCPOptions,\n): (event: MessageEvent) => void {\n\tconst seen = new Set<MessagePort>()\n\treturn (event: MessageEvent): void => {\n\t\tconst ports = event.ports\n\t\tif (ports.length > 0) {\n\t\t\t// Gate: consult accept (origin/identity check) before binding.\n\t\t\tif (options.accept !== undefined && !options.accept(event)) return\n\t\t\tconst port = ports[0]\n\t\t\tif (port === undefined) return\n\t\t\t// Deduplicate: repeated delivery of the same port would create duplicate bindings.\n\t\t\tif (seen.has(port)) return\n\t\t\tseen.add(port)\n\t\t\tconst transport = new MessagePortTransport({ port })\n\t\t\tconst unbind = bindServer(server, transport)\n\t\t\tteardowns.add(() => {\n\t\t\t\tunbind()\n\t\t\t\ttransport.close()\n\t\t\t})\n\t\t\treturn\n\t\t}\n\t\tif (isString(event.data)) scopeTransport.deliver(event.data)\n\t}\n}\n","import type {\n\tMCPClientTransportEventMap,\n\tMCPClientTransportInterface,\n\tJSONRPCMessage,\n} from '@src/core'\nimport type { EmitterInterface } from '@orkestrel/emitter'\nimport type { HTTPClientTransportOptions } from '../types.js'\nimport {\n\tinferRequestVersion,\n\tisJSONRPCResponse,\n\tisMCPVersion,\n\tisModernRequest,\n\tparseJSONRPCMessage,\n} from '@src/core'\nimport { isRecord, isString } from '@orkestrel/contract'\nimport { Emitter } from '@orkestrel/emitter'\nimport {\n\tMCP_METHOD_HEADER,\n\tMCP_NAME_HEADER,\n\tMCP_PROTOCOL_VERSION_HEADER,\n\tMCP_SESSION_HEADER,\n} from '../constants.js'\nimport { readEventStream } from '../helpers.js'\n\n/**\n * The browser-face HTTP CLIENT transport for the Model Context Protocol — a\n * {@link MCPClientTransportInterface} that drives a REMOTE Streamable-HTTP MCP server\n * over the native `fetch`, the browser sibling of the Node face's\n * {@link import('@src/server').HTTPClientTransport}, honoring the SAME\n * `mcp-session-id` semantics so it interoperates with an `MCPSession`-based server\n * unchanged.\n *\n * @remarks\n * - **Request/response over `fetch`.** `send(message)` POSTs the JSON-serialized\n * message to `options.url` with `content-type: application/json` and an\n * `Accept` of BOTH `application/json` and `text/event-stream` (so the server may\n * answer with either framing) — plus any `options.headers` (e.g. an\n * `Authorization` bearer). It then decodes the reply and emits each decoded\n * {@link JSONRPCMessage} on the `message` event the\n * {@link import('@src/core').MCPClientInterface} subscribes to.\n * - **Both reply framings.** A `200` with an `application/json` body is parsed with\n * `parseJSONRPCMessage`; a `200` with a `text/event-stream` body is decoded via the\n * `@orkestrel/sse` {@link import('@orkestrel/sse').SSEParserInterface} (the browser\n * face's own `readEventStream`) — the inverse of the server's `openStream` seam, so\n * the wire round-trips. A `202` Accepted (a notification) carries no body and emits\n * nothing.\n * - **Session and protocol headers.** `start()` is a no-op (a\n * request/response transport opens no long-lived connection). The\n * `mcp-session-id` response header, when a STATEFUL server sends one (on\n * `initialize`), is captured into `session` and then ECHOED as the\n * `mcp-session-id` request header on every SUBSEQUENT request — so an\n * `MCPClient` passes a stateful server's session validation. The\n * initialize result's `protocolVersion` is likewise captured, but only\n * when it is a SUPPORTED value, and echoed as `mcp-protocol-version` alone on\n * subsequent legacy requests. Modern requests instead derive protocol and method\n * headers from the message, plus the name header only for `tools/call`.\n * Before initialize returns, neither captured legacy header is sent.\n * `close()` clears the captured protocol so a reconnect's `initialize`\n * POST is headerless; the captured `session` persists across `close()`.\n * - **Total at the boundary (§14).** Every reply is narrowed (`parseJSONRPCMessage`,\n * the SSE decoder) — a non-message reply is dropped, never asserted; a `fetch` /\n * decode failure surfaces on the `error` event rather than escaping `send`.\n * - **Observable (§13).** Owns the `emitter` ({@link MCPClientTransportEventMap}); fires\n * `message` per decoded reply, `error` on a fault, and `close` on `close()`.\n *\n * @example\n * ```ts\n * const transport = new HTTPClientTransport({ url: 'http://localhost:3000/mcp' })\n * const client = new MCPClient({ transport })\n * await client.connect()\n * ```\n */\nexport class HTTPClientTransport implements MCPClientTransportInterface {\n\treadonly #emitter: Emitter<MCPClientTransportEventMap>\n\treadonly #url: string\n\treadonly #headers: Readonly<Record<string, string>>\n\treadonly #fetch: typeof fetch\n\treadonly #timeout: number | undefined\n\t#session: string | undefined = undefined\n\t#protocol: string | undefined = undefined\n\n\tconstructor(options: HTTPClientTransportOptions) {\n\t\tthis.#emitter = new Emitter<MCPClientTransportEventMap>()\n\t\tthis.#url = options.url\n\t\tthis.#headers = options.headers ?? {}\n\t\tthis.#fetch = options.fetch ?? globalThis.fetch.bind(globalThis)\n\t\tthis.#timeout = options.timeout\n\t}\n\n\tget emitter(): EmitterInterface<MCPClientTransportEventMap> {\n\t\treturn this.#emitter\n\t}\n\n\tget session(): string | undefined {\n\t\treturn this.#session\n\t}\n\n\tget duplex(): boolean {\n\t\t// Streamable HTTP carries no client-initiated notification: the dated revision defines\n\t\t// none over it, and closing the response stream is the cancellation signal instead.\n\t\treturn false\n\t}\n\n\tasync start(): Promise<void> {\n\t\t// A request/response transport opens no long-lived connection — `send` issues each\n\t\t// `fetch` on demand. Nothing to arm.\n\t}\n\n\tasync send(message: JSONRPCMessage): Promise<void> {\n\t\tlet response: Response\n\t\ttry {\n\t\t\tresponse = await this.#fetch(this.#url, {\n\t\t\t\tmethod: 'POST',\n\t\t\t\theaders: {\n\t\t\t\t\t'content-type': 'application/json',\n\t\t\t\t\taccept: 'application/json, text/event-stream',\n\t\t\t\t\t// Echo a captured session id so a STATEFUL server validates the request; before\n\t\t\t\t\t// `initialize` returns one `#session` is undefined → no header (safe for a\n\t\t\t\t\t// stateless server). A caller `headers` key still wins (merged last).\n\t\t\t\t\t...(this.#session === undefined ? {} : { [MCP_SESSION_HEADER]: this.#session }),\n\t\t\t\t\t...this.#buildHeaders(message),\n\t\t\t\t\t...this.#headers,\n\t\t\t\t},\n\t\t\t\tbody: JSON.stringify(message),\n\t\t\t\t...(this.#timeout === undefined ? {} : { signal: AbortSignal.timeout(this.#timeout) }),\n\t\t\t})\n\t\t} catch (error) {\n\t\t\t// A network-level failure (connection refused, DNS) — surface it for observation;\n\t\t\t// the client's per-request deadline still rejects the pending request.\n\t\t\tthis.#emitter.emit('error', error)\n\t\t\treturn\n\t\t}\n\t\t// Capture a server-assigned session id (a stateless server sends none) so it is echoed\n\t\t// on subsequent requests; a missing header leaves `session` unchanged.\n\t\tconst session = response.headers.get(MCP_SESSION_HEADER)\n\t\tif (session !== null) this.#session = session\n\t\tawait this.#deliver(response)\n\t}\n\n\t// Clear the captured protocol before emitting `close`, so a reconnect's `initialize`\n\t// POST carries no `mcp-protocol-version` header (the captured `session` is untouched).\n\tasync close(): Promise<void> {\n\t\tthis.#protocol = undefined\n\t\tthis.#emitter.emit('close')\n\t}\n\n\t// Modern requests announce their own protocol version, so the header is projected from the\n\t// message through the SHARED `inferRequestVersion` — the same read the server's own\n\t// expectation performs, and the same read the Node face performs. Legacy requests carry\n\t// the version captured from the `initialize` handshake instead.\n\t#buildHeaders(message: JSONRPCMessage): Readonly<Record<string, string>> {\n\t\tif (isModernRequest(message)) {\n\t\t\tconst version = inferRequestVersion(message)\n\t\t\tconst name = message.params?.['name']\n\t\t\treturn {\n\t\t\t\t...(version === undefined ? {} : { [MCP_PROTOCOL_VERSION_HEADER]: version }),\n\t\t\t\t[MCP_METHOD_HEADER]: message.method,\n\t\t\t\t...(message.method === 'tools/call' && isString(name) ? { [MCP_NAME_HEADER]: name } : {}),\n\t\t\t}\n\t\t}\n\t\treturn this.#protocol === undefined ? {} : { [MCP_PROTOCOL_VERSION_HEADER]: this.#protocol }\n\t}\n\n\t// Decode a reply and emit each carried message. A 202 (notification accepted) has no\n\t// body — emit nothing. An `application/json` body is one envelope; a `text/event-stream`\n\t// body is decoded via the browser-face `readEventStream` (one or more `data:` events). A\n\t// decode failure surfaces on `error` rather than escaping.\n\tasync #deliver(response: Response): Promise<void> {\n\t\tif (response.status === 202) return\n\t\tconst type = response.headers.get('content-type') ?? ''\n\t\ttry {\n\t\t\tif (type.includes('text/event-stream')) {\n\t\t\t\tfor (const message of await readEventStream(response)) this.#capture(message)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (type.includes('application/json')) {\n\t\t\t\tconst message = parseJSONRPCMessage(await response.json())\n\t\t\t\tif (message !== undefined) this.#capture(message)\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tthis.#emitter.emit('error', error)\n\t\t}\n\t}\n\n\t// Capture the negotiated SUPPORTED protocol from the initialize result before emitting\n\t// the message, so the next request carries its required protocol-version header; any\n\t// other value (missing or unsupported) is ignored and leaves `#protocol` unchanged.\n\t#capture(message: JSONRPCMessage): void {\n\t\tif (\n\t\t\tisJSONRPCResponse(message) &&\n\t\t\tisRecord(message.result) &&\n\t\t\tisMCPVersion(message.result['protocolVersion'])\n\t\t) {\n\t\t\tthis.#protocol = message.result['protocolVersion']\n\t\t}\n\t\tthis.#emitter.emit('message', message)\n\t}\n}\n","import type {\n\tMCPClientTransportEventMap,\n\tMCPClientTransportInterface,\n\tJSONRPCMessage,\n} from '@src/core'\nimport type { EmitterInterface } from '@orkestrel/emitter'\nimport type { WebSocketClientTransportOptions } from '../types.js'\nimport { parseJSONRPCMessage } from '@src/core'\nimport { isString } from '@orkestrel/contract'\nimport { Emitter } from '@orkestrel/emitter'\nimport { MCP_WEBSOCKET_SUBPROTOCOL } from '../constants.js'\n\n/**\n * The browser-face WebSocket CLIENT transport for the Model Context Protocol — a\n * {@link MCPClientTransportInterface} that drives a REMOTE MCP server over the native\n * `WebSocket` global, the browser sibling of the Node face's\n * {@link import('@src/server').WebSocketClientTransport}.\n *\n * @remarks\n * - **Host-performed handshake.** `start()` opens `new WebSocket(url, protocols)` and\n * waits for the native `'open'` event — the RFC 6455 handshake itself is entirely\n * the host's concern, so this transport carries none of the Node client's\n * `node:crypto` / `node:http(s)` machinery. A connection failure (the native\n * `'error'` event while not yet `OPEN`) REJECTS `start()`.\n * - **Queued sends.** `send` writes each message as one text frame immediately once\n * the socket is `OPEN`; a `send` issued before `'open'` fires (or before `start()`\n * is even called) is QUEUED and flushed, IN ORDER, the moment the socket opens —\n * so a caller need not await `start()` before calling `send`.\n * - **Inbound (`message`).** Each decoded text frame is `JSON.parse`d (guarded) and\n * narrowed with `parseJSONRPCMessage` — a well-formed {@link JSONRPCMessage}\n * re-emits on this transport's `message` event; a non-text (binary) frame or a\n * non-JSON / non-message text frame surfaces on `error` and is DROPPED (§14 — never\n * throws on adversarial wire input).\n * - **`close()`** closes the underlying socket and fires `close` (idempotent); the\n * socket's native `close` event (a server-initiated close) fires the SAME `close`\n * exactly once total — `close()` first flips the guard, so the native event never\n * double-emits. **This transport is not reusable after `close()`** — a `send` issued\n * after `close()` is silently dropped (not queued, not delivered even on a later\n * `start()`).\n * - **Observable (§13).** Owns the `emitter` ({@link MCPClientTransportEventMap}); every\n * emit the emitter isolates a listener throw; `error` is a DOMAIN event (a\n * transport-level fault).\n *\n * @example\n * ```ts\n * const transport = new WebSocketClientTransport({ url: 'ws://localhost:3000/mcp' })\n * const client = new MCPClient({ transport })\n * await client.connect() // the browser handshakes, then the MCP initialize runs over WS frames\n * ```\n */\nexport class WebSocketClientTransport implements MCPClientTransportInterface {\n\treadonly #emitter: Emitter<MCPClientTransportEventMap>\n\treadonly #url: string\n\treadonly #protocols: string | string[] | undefined\n\t#socket: WebSocket | undefined = undefined\n\t#queue: string[] = []\n\t#closed = false\n\n\tconstructor(options: WebSocketClientTransportOptions) {\n\t\tthis.#emitter = new Emitter<MCPClientTransportEventMap>()\n\t\tthis.#url = options.url\n\t\tconst protocols = options.protocols\n\t\t// Default to MCP_WEBSOCKET_SUBPROTOCOL when `protocols` is omitted — matching\n\t\t// createWebSocketServer's unconditional echo. An empty array means \"no subprotocol\",\n\t\t// overriding the default explicitly for foreign servers.\n\t\tthis.#protocols =\n\t\t\ttypeof protocols === 'string'\n\t\t\t\t? protocols\n\t\t\t\t: protocols === undefined\n\t\t\t\t\t? MCP_WEBSOCKET_SUBPROTOCOL\n\t\t\t\t\t: protocols.length === 0\n\t\t\t\t\t\t? undefined\n\t\t\t\t\t\t: [...protocols]\n\t}\n\n\tget emitter(): EmitterInterface<MCPClientTransportEventMap> {\n\t\treturn this.#emitter\n\t}\n\n\tget session(): string | undefined {\n\t\treturn undefined\n\t}\n\n\tget duplex(): boolean {\n\t\t// A socket is bidirectional for its whole life: either side writes a frame whenever it\n\t\t// has one, with no request to attach it to.\n\t\treturn true\n\t}\n\n\tasync start(): Promise<void> {\n\t\t// Already connected — a second `connect()` short-circuits in the client, but guard here\n\t\t// too (idempotent open).\n\t\tif (this.#socket !== undefined) return\n\t\tthis.#closed = false\n\t\tconst socket = new WebSocket(this.#url, this.#protocols)\n\t\tthis.#socket = socket\n\t\tthis.#bind(socket)\n\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\tsocket.addEventListener(\n\t\t\t\t'open',\n\t\t\t\t() => {\n\t\t\t\t\tthis.#flush(socket)\n\t\t\t\t\tresolve()\n\t\t\t\t},\n\t\t\t\t{ once: true },\n\t\t\t)\n\t\t\tsocket.addEventListener(\n\t\t\t\t'error',\n\t\t\t\t() => {\n\t\t\t\t\tif (socket.readyState !== WebSocket.OPEN) {\n\t\t\t\t\t\tthis.#socket = undefined\n\t\t\t\t\t\treject(new Error('WebSocket connection failed'))\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t{ once: true },\n\t\t\t)\n\t\t})\n\t}\n\n\tasync send(message: JSONRPCMessage): Promise<void> {\n\t\t// After close(), silently drop — never queue (a closed transport is not reusable;\n\t\t// queued messages would resurrect on a later start() which is not a supported pattern).\n\t\tif (this.#closed) return\n\t\tconst text = JSON.stringify(message)\n\t\tconst socket = this.#socket\n\t\tif (socket !== undefined && socket.readyState === WebSocket.OPEN) socket.send(text)\n\t\telse this.#queue.push(text)\n\t}\n\n\tasync close(): Promise<void> {\n\t\tif (this.#closed) return\n\t\tthis.#closed = true\n\t\tconst socket = this.#socket\n\t\tthis.#socket = undefined\n\t\tif (socket !== undefined) socket.close()\n\t\tthis.#emitter.emit('close')\n\t}\n\n\t// Bridge the native socket's events onto the transport: a text frame → `message`\n\t// (decoded + narrowed), the socket close → `close`, a socket fault → `error`.\n\t#bind(socket: WebSocket): void {\n\t\tsocket.addEventListener('message', (event: MessageEvent) => this.#receive(event.data))\n\t\tsocket.addEventListener('close', () => this.#onClose())\n\t\tsocket.addEventListener('error', (event) => this.#emitter.emit('error', event))\n\t}\n\n\t// Write every queued (pre-open) message, in order, as the socket opens.\n\t#flush(socket: WebSocket): void {\n\t\tfor (const text of this.#queue.splice(0)) socket.send(text)\n\t}\n\n\t// Decode one inbound frame: a non-text (binary) frame is rejected without a throw; a\n\t// text frame is `JSON.parse`d → `parseJSONRPCMessage`. A well-formed message re-emits on\n\t// `message`; a malformed / non-message frame surfaces on `error` and is dropped (§14 —\n\t// never throws on adversarial wire input).\n\t#receive(data: unknown): void {\n\t\tif (!isString(data)) {\n\t\t\tthis.#emitter.emit('error', new Error('non-text WebSocket frame'))\n\t\t\treturn\n\t\t}\n\t\tlet parsed: unknown\n\t\ttry {\n\t\t\tparsed = JSON.parse(data)\n\t\t} catch (error) {\n\t\t\tthis.#emitter.emit('error', error)\n\t\t\treturn\n\t\t}\n\t\tconst message = parseJSONRPCMessage(parsed)\n\t\tif (message === undefined) {\n\t\t\tthis.#emitter.emit('error', new Error('non-JSON-RPC WebSocket frame'))\n\t\t\treturn\n\t\t}\n\t\tthis.#emitter.emit('message', message)\n\t}\n\n\t// The socket closed underneath us — fire `close` once (a `close()` call already flipped\n\t// `#closed`, so it does not double-emit).\n\t#onClose(): void {\n\t\tif (this.#closed) return\n\t\tthis.#closed = true\n\t\tthis.#socket = undefined\n\t\tthis.#emitter.emit('close')\n\t}\n}\n","import type { MCPClientTransportInterface, MCPTransportInterface } from '@src/core'\nimport type {\n\tHTTPClientTransportOptions,\n\tMessagePortTransportOptions,\n\tScopeTransportInterface,\n\tServeMCPOptions,\n\tServeMCPScopeInterface,\n\tWebSocketClientTransportOptions,\n} from './types.js'\nimport { bindServer, createMCPServer } from '@src/core'\nimport { DEFAULT_MCP_SERVER_NAME, DEFAULT_MCP_SERVER_VERSION } from './constants.js'\nimport { createScopeMessageListener } from './helpers.js'\nimport { HTTPClientTransport } from './transports/HTTPClientTransport.js'\nimport { MessagePortTransport } from './transports/MessagePortTransport.js'\nimport { WebSocketClientTransport } from './transports/WebSocketClientTransport.js'\n\n/**\n * Create the browser-face WebSocket CLIENT transport for an\n * {@link import('@src/core').MCPClientInterface} — a {@link MCPClientTransportInterface}\n * that drives a REMOTE MCP server over the native `WebSocket` global, the browser\n * sibling of the Node face's `createWebSocketClientTransport` (`@src/server`).\n *\n * @remarks\n * Hand it to `createMCPClient({ transport })`: `start()` (run by `client.connect()`)\n * opens `new WebSocket(options.url, options.protocols)` and awaits the native\n * `'open'` event — the RFC 6455 handshake itself is the browser's concern. Each\n * JSON-RPC message the client `send`s before the socket opens is QUEUED and flushed,\n * in order, once it does; each decoded reply is surfaced on the transport's\n * `message` event for the client's id correlation.\n *\n * @param options - `url` (the remote WebSocket endpoint; REQUIRED) and optional\n * `protocols` (the WebSocket subprotocol(s) to request); see\n * {@link WebSocketClientTransportOptions}\n * @returns A working {@link MCPClientTransportInterface} over the native `WebSocket`\n *\n * @example\n * ```ts\n * import { createMCPClient } from '@orkestrel/mcp'\n * import { createWebSocketClientTransport } from '@orkestrel/mcp/browser'\n *\n * const client = createMCPClient({\n * \ttransport: createWebSocketClientTransport({ url: 'ws://localhost:3000/mcp' }),\n * })\n * await client.connect()\n * const tools = await client.tools()\n * ```\n */\nexport function createWebSocketClientTransport(\n\toptions: WebSocketClientTransportOptions,\n): MCPClientTransportInterface {\n\treturn new WebSocketClientTransport(options)\n}\n\n/**\n * Create the browser-face HTTP CLIENT transport for an\n * {@link import('@src/core').MCPClientInterface} — a {@link MCPClientTransportInterface}\n * that drives a REMOTE Streamable-HTTP MCP server over the native `fetch`, the\n * browser sibling of the Node face's `createHTTPClientTransport` (`@src/server`).\n *\n * @remarks\n * Hand it to `createMCPClient({ transport })`: each JSON-RPC message the client\n * sends is `POST`ed to `options.url` with `content-type: application/json` and an\n * `Accept` of both `application/json` and `text/event-stream` (the server answers\n * with EITHER — a plain JSON envelope or a Streamable-HTTP SSE `data:` event,\n * decoded via `@orkestrel/sse`), and the reply is surfaced on the transport's\n * `message` event for the client's id correlation. Add `options.headers` (e.g. an\n * `Authorization` bearer) to reach a guarded server. `start` / `close` hold no\n * connection; against a STATEFUL server it captures the `mcp-session-id` from\n * `initialize` and echoes it on later requests. It also captures the initialize\n * result's `protocolVersion` and sends `mcp-protocol-version` alone on subsequent\n * legacy requests. Modern requests instead derive `mcp-protocol-version` and\n * `mcp-method` from the message, plus `mcp-name` only for `tools/call`, so the\n * same `MCPClient` passes either era's protocol gates without caller wiring.\n *\n * @param options - `url` (the remote endpoint; REQUIRED), optional `headers` merged\n * onto every request, optional `fetch` (default `globalThis.fetch`), and optional\n * `timeout` (ms, applied via `AbortSignal.timeout`); see\n * {@link HTTPClientTransportOptions}\n * @returns A working {@link MCPClientTransportInterface} over the native `fetch`\n *\n * @example\n * ```ts\n * import { createMCPClient } from '@orkestrel/mcp'\n * import { createHTTPClientTransport } from '@orkestrel/mcp/browser'\n *\n * const client = createMCPClient({\n * \ttransport: createHTTPClientTransport({ url: 'http://localhost:3000/mcp' }),\n * })\n * await client.connect()\n * const tools = await client.tools()\n * ```\n */\nexport function createHTTPClientTransport(\n\toptions: HTTPClientTransportOptions,\n): MCPClientTransportInterface {\n\treturn new HTTPClientTransport(options)\n}\n\n/**\n * Create the browser-face `MessagePort` transport — a\n * {@link import('@src/core').MCPTransportInterface} over a native `MessagePort`, the\n * SYMMETRIC carrier that works as either a server or a client transport depending on\n * which binder ({@link import('@src/core').bindServer} or\n * {@link import('@src/core').bindClient}) it is handed to.\n *\n * @remarks\n * `port.start()` runs at construction (see {@link MessagePortTransport}'s doc for\n * why); inbound payloads are string-only (a non-string `postMessage` payload is\n * dropped, never thrown); `messageerror` is ignored (one bad frame does not close the\n * channel); `close()` closes the port and fires `closed` exactly once.\n *\n * @param options - `port` (the `MessagePort` half to drive; REQUIRED); see\n * {@link MessagePortTransportOptions}\n * @returns A working {@link import('@src/core').MCPTransportInterface} over the port\n *\n * @example\n * ```ts\n * import { bindServer, createMCPServer } from '@orkestrel/mcp'\n * import { createMessagePortTransport } from '@orkestrel/mcp/browser'\n *\n * const { port1, port2 } = new MessageChannel()\n * bindServer(createMCPServer({ identity: { name: 's', version: '1.0.0' }, tools }), createMessagePortTransport({ port: port1 }))\n * ```\n */\nexport function createMessagePortTransport(\n\toptions: MessagePortTransportOptions,\n): MCPTransportInterface {\n\treturn new MessagePortTransport(options)\n}\n\n/**\n * Adapt a hostable {@link ServeMCPScopeInterface} (`self` in a dedicated Web Worker,\n * or any structurally matching double) into a {@link ScopeTransportInterface} — the\n * implicit, portless message channel `serveMCPScope` binds for the\n * dedicated-worker shape.\n *\n * @remarks\n * `send` writes each outbound string via `scope.postMessage`. `listen`/`closed`\n * register the SINGLE handler `deliver` / the underlying close path route through —\n * `serveMCPScope`'s own `scope` `message`-event listener calls `deliver(event.data)`\n * for every portless, string-payload event (there is no native registration point on\n * the scope itself for `serveMCPScope` to hand a `listen` handler to, so `deliver` is\n * the bridge). `close()` fires the registered `closed` handler — a scope has nothing\n * physically closable, so this is the only teardown signal available.\n *\n * @param scope - The hostable scope to adapt (structurally, `self` / `globalThis`\n * inside a dedicated Web Worker)\n * @returns A {@link ScopeTransportInterface} `serveMCPScope` binds and drives via `deliver`\n *\n * @example\n * ```ts\n * const scopeTransport = createScopeTransport(self)\n * const unbind = bindServer(server, scopeTransport)\n * ```\n */\nexport function createScopeTransport(scope: ServeMCPScopeInterface): ScopeTransportInterface {\n\tlet onMessage: ((message: string) => void) | undefined\n\tlet onClosed: (() => void) | undefined\n\treturn {\n\t\tsend(message: string): void {\n\t\t\tscope.postMessage(message)\n\t\t},\n\t\tlisten(handler: (message: string) => void): void {\n\t\t\tonMessage = handler\n\t\t},\n\t\tclosed(handler: () => void): void {\n\t\t\tonClosed = handler\n\t\t},\n\t\tclose(): void {\n\t\t\tonClosed?.()\n\t\t},\n\t\tdeliver(message: string): void {\n\t\t\tonMessage?.(message)\n\t\t},\n\t}\n}\n\n/**\n * Boot an `MCPServer` inside a hostable worker scope and wire its message events to it.\n *\n * @remarks\n * Port-bearing events are gated by `options.accept`, deduplicated by port, and receive\n * their own `MessagePortTransport` binding. Portless string events use the scope's\n * implicit channel. The returned disposer removes the listener, unbinds the implicit\n * channel, and closes every accepted port binding.\n *\n * @param scope - The hostable worker scope to wire\n * @param options - The tools, optional identity, and optional port-event gate\n * @returns An idempotent disposer for every binding owned by this call\n */\nexport function serveMCPScope(scope: ServeMCPScopeInterface, options: ServeMCPOptions): () => void {\n\tconst server = createMCPServer({\n\t\ttools: options.tools,\n\t\tidentity: {\n\t\t\tname: options.name ?? DEFAULT_MCP_SERVER_NAME,\n\t\t\tversion: options.version ?? DEFAULT_MCP_SERVER_VERSION,\n\t\t},\n\t})\n\tconst scopeTransport = createScopeTransport(scope)\n\tconst unbindScope = bindServer(server, scopeTransport)\n\tconst teardowns = new Set<() => void>()\n\tconst onMessage = createScopeMessageListener(server, scopeTransport, teardowns, options)\n\tscope.addEventListener('message', onMessage)\n\tlet disposed = false\n\treturn () => {\n\t\tif (disposed) return\n\t\tdisposed = true\n\t\tscope.removeEventListener('message', onMessage)\n\t\tunbindScope()\n\t\tfor (const teardown of teardowns) teardown()\n\t\tteardowns.clear()\n\t}\n}\n\n/**\n * Boot an `MCPServer` inside the current hostable worker scope.\n *\n * @param options - The tools, optional identity, and optional port-event gate\n * @returns The disposer returned by {@link serveMCPScope}\n */\nexport function serveMCP(options: ServeMCPOptions): () => void {\n\treturn serveMCPScope(globalThis, options)\n}\n"],"mappings":"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"}
|