@orkestrel/mcp 0.0.27 → 0.0.29
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 +12 -15
- package/dist/src/browser/index.d.ts +184 -324
- package/dist/src/browser/index.js +166 -469
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/core/index.cjs +826 -352
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +1265 -855
- package/dist/src/core/index.d.ts +1265 -855
- package/dist/src/core/index.js +815 -352
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +364 -680
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +395 -516
- package/dist/src/server/index.d.ts +395 -516
- package/dist/src/server/index.js +358 -665
- package/dist/src/server/index.js.map +1 -1
- package/package.json +26 -27
|
@@ -1,298 +1,64 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { createSSEParser } from "@orkestrel/sse";
|
|
1
|
+
import { isString } from "@orkestrel/contract";
|
|
2
|
+
import { HTTPClientTransport, MCP_WEBSOCKET_SUBPROTOCOL, bindServer, createMCPServer, deliverMessage } from "../core/index.js";
|
|
4
3
|
import { Emitter } from "@orkestrel/emitter";
|
|
5
4
|
//#region src/browser/constants.ts
|
|
6
|
-
/**
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
* ECHOES this header exactly like the Node face's `HTTPClientTransport`
|
|
10
|
-
* (`src/server`), so the same client interoperates with an `MCPSession`-based
|
|
11
|
-
* server unchanged.
|
|
12
|
-
*/
|
|
13
|
-
var MCP_SESSION_HEADER = "mcp-session-id";
|
|
14
|
-
/**
|
|
15
|
-
* The Streamable-HTTP transport header carrying the MCP protocol version. Modern
|
|
16
|
-
* requests derive it from their own `_meta`; legacy requests echo the negotiated
|
|
17
|
-
* initialize result on each subsequent request.
|
|
18
|
-
*/
|
|
19
|
-
var MCP_PROTOCOL_VERSION_HEADER = "mcp-protocol-version";
|
|
20
|
-
/**
|
|
21
|
-
* The modern Streamable-HTTP request header carrying the JSON-RPC method. It is
|
|
22
|
-
* emitted on every modern request and never on a legacy request.
|
|
23
|
-
*/
|
|
24
|
-
var MCP_METHOD_HEADER = "mcp-method";
|
|
25
|
-
/**
|
|
26
|
-
* The modern Streamable-HTTP request header carrying a named target. The browser
|
|
27
|
-
* HTTP client emits it only for `tools/call`, from that request's `params.name`.
|
|
28
|
-
*/
|
|
29
|
-
var MCP_NAME_HEADER = "mcp-name";
|
|
30
|
-
/** The default server name `serveMCPScope` reports (`initialize`'s `serverInfo.name`) when `options.name` is omitted. */
|
|
31
|
-
var DEFAULT_MCP_SERVER_NAME = "taverna";
|
|
32
|
-
/** The default server version `serveMCPScope` reports (`initialize`'s `serverInfo.version`) when `options.version` is omitted. */
|
|
5
|
+
/** Supplies the default server name `createScopeServer` reports (`initialize`'s `serverInfo.name`) when `options.name` is omitted. */
|
|
6
|
+
var DEFAULT_MCP_SERVER_NAME = "@orkestrel/mcp";
|
|
7
|
+
/** Supplies the default server version `createScopeServer` reports (`initialize`'s `serverInfo.version`) when `options.version` is omitted. */
|
|
33
8
|
var DEFAULT_MCP_SERVER_VERSION = "1.0.0";
|
|
34
|
-
/**
|
|
35
|
-
* The WebSocket subprotocol `createWebSocketClientTransport` requests by default —
|
|
36
|
-
* `'mcp'`, which `createWebSocketServer` selects when the client offers it. Per RFC 6455
|
|
37
|
-
* §4.1 a client MUST fail the connection if the server returns
|
|
38
|
-
* a subprotocol it did not request; Node ≥ 22 (undici) enforces this strictly, so the
|
|
39
|
-
* default bakes the correct value in. Override `WebSocketClientTransportOptions.protocols`
|
|
40
|
-
* only when connecting to a foreign server that speaks a different subprotocol (or `[]`
|
|
41
|
-
* for no subprotocol negotiation at all).
|
|
42
|
-
*/
|
|
43
|
-
var MCP_WEBSOCKET_SUBPROTOCOL = "mcp";
|
|
44
|
-
//#endregion
|
|
45
|
-
//#region src/browser/transports/HTTPClientTransport.ts
|
|
46
|
-
/**
|
|
47
|
-
* The browser-face HTTP CLIENT transport for the Model Context Protocol — a
|
|
48
|
-
* {@link MCPClientTransportInterface} that drives a REMOTE Streamable-HTTP MCP server
|
|
49
|
-
* over the native `fetch`, the browser sibling of the Node face's
|
|
50
|
-
* {@link import('@orkestrel/mcp/server').HTTPClientTransport}, honoring the SAME
|
|
51
|
-
* `mcp-session-id` semantics so it interoperates with an `MCPSession`-based server
|
|
52
|
-
* unchanged.
|
|
53
|
-
*
|
|
54
|
-
* @remarks
|
|
55
|
-
* - **Request/response over `fetch`.** `send(message)` POSTs the JSON-serialized
|
|
56
|
-
* message to `options.url` with `content-type: application/json` and an
|
|
57
|
-
* `Accept` of BOTH `application/json` and `text/event-stream` (so the server may
|
|
58
|
-
* answer with either framing) — plus any `options.headers` (for example, an
|
|
59
|
-
* `Authorization` bearer). It then decodes the reply and emits each decoded
|
|
60
|
-
* {@link JSONRPCMessage} on the `message` event the
|
|
61
|
-
* {@link import('@orkestrel/mcp').MCPClientInterface} subscribes to.
|
|
62
|
-
* - **Both reply framings.** A `200` with an `application/json` body is parsed with
|
|
63
|
-
* `parseJSONRPCMessage`; a `200` with a `text/event-stream` body is decoded with the
|
|
64
|
-
* `@orkestrel/sse` {@link import('@orkestrel/sse').SSEParserInterface} (the browser
|
|
65
|
-
* face's own `readEventStream`) — the inverse of the server's `openStream` seam, so
|
|
66
|
-
* the wire round-trips. A `202` Accepted (a notification) carries no body and emits
|
|
67
|
-
* nothing.
|
|
68
|
-
* - **Session and protocol headers.** `start()` is a no-op (a
|
|
69
|
-
* request/response transport opens no long-lived connection). The
|
|
70
|
-
* `mcp-session-id` response header, when a STATEFUL server sends one (on
|
|
71
|
-
* `initialize`), is captured into `session` and then ECHOED as the
|
|
72
|
-
* `mcp-session-id` request header on every SUBSEQUENT request — so an
|
|
73
|
-
* `MCPClient` passes a stateful server's session validation. The
|
|
74
|
-
* initialize result's `protocolVersion` is likewise captured, but only
|
|
75
|
-
* when it is a SUPPORTED value, and echoed as `mcp-protocol-version` alone on
|
|
76
|
-
* subsequent legacy requests. Modern requests instead derive protocol and method
|
|
77
|
-
* headers from the message, plus the name header only for `tools/call` — carried in the
|
|
78
|
-
* protocol's Base64 sentinel form whenever the tool name cannot ride as plain ASCII.
|
|
79
|
-
* Before initialize returns, neither captured legacy header is sent.
|
|
80
|
-
* `close()` clears the captured protocol so a reconnect's `initialize`
|
|
81
|
-
* POST is headerless; the captured `session` persists across `close()`.
|
|
82
|
-
* - **`close()` releases what is in flight.** Every `fetch` this transport still has open is
|
|
83
|
-
* ABORTED, which cancels the response body a `send` is reading — an SSE reply the server
|
|
84
|
-
* never ends would otherwise outlive the transport, with nothing left able to reach it. The
|
|
85
|
-
* aborted read surfaces on `error` and the `send` reporting it resolves. `close()` is
|
|
86
|
-
* idempotent (one `close` event per connected lifetime), and `start()` opens the next one.
|
|
87
|
-
* - **Total at the boundary.** Every reply is narrowed (`parseJSONRPCMessage`,
|
|
88
|
-
* the SSE decoder) — a non-message reply is dropped, never asserted; a `fetch` /
|
|
89
|
-
* decode failure surfaces on the `error` event rather than escaping `send`.
|
|
90
|
-
* - **Observable.** Owns the `emitter` ({@link MCPClientTransportEventMap}); fires
|
|
91
|
-
* `message` per decoded reply, `error` on a fault, and `close` on `close()`.
|
|
92
|
-
*
|
|
93
|
-
* @example
|
|
94
|
-
* ```ts
|
|
95
|
-
* const transport = new HTTPClientTransport({ url: 'http://localhost:3000/mcp' })
|
|
96
|
-
* const client = new MCPClient({ transport })
|
|
97
|
-
* await client.connect()
|
|
98
|
-
* ```
|
|
99
|
-
*/
|
|
100
|
-
var HTTPClientTransport = class {
|
|
101
|
-
#emitter;
|
|
102
|
-
#url;
|
|
103
|
-
#headers;
|
|
104
|
-
#fetch;
|
|
105
|
-
#timeout;
|
|
106
|
-
#pending = /* @__PURE__ */ new Set();
|
|
107
|
-
#parameters = /* @__PURE__ */ new Map();
|
|
108
|
-
#stamps = /* @__PURE__ */ new WeakMap();
|
|
109
|
-
#session = void 0;
|
|
110
|
-
#protocol = void 0;
|
|
111
|
-
#generation = 0;
|
|
112
|
-
#closed = false;
|
|
113
|
-
constructor(options) {
|
|
114
|
-
this.#emitter = new Emitter();
|
|
115
|
-
this.#url = options.url;
|
|
116
|
-
this.#headers = options.headers ?? {};
|
|
117
|
-
this.#fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
118
|
-
this.#timeout = options.timeout;
|
|
119
|
-
}
|
|
120
|
-
get emitter() {
|
|
121
|
-
return this.#emitter;
|
|
122
|
-
}
|
|
123
|
-
get session() {
|
|
124
|
-
return this.#session;
|
|
125
|
-
}
|
|
126
|
-
get duplex() {
|
|
127
|
-
return false;
|
|
128
|
-
}
|
|
129
|
-
async start() {
|
|
130
|
-
this.#closed = false;
|
|
131
|
-
}
|
|
132
|
-
async send(message) {
|
|
133
|
-
this.#stamp(message);
|
|
134
|
-
const request = new AbortController();
|
|
135
|
-
this.#pending.add(request);
|
|
136
|
-
try {
|
|
137
|
-
await this.#exchange(message, request.signal);
|
|
138
|
-
} finally {
|
|
139
|
-
this.#pending.delete(request);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
#stamp(message) {
|
|
143
|
-
if (!isModernRequest(message) || message.method !== "tools/list") return;
|
|
144
|
-
if (message.params?.["cursor"] === void 0) this.#generation += 1;
|
|
145
|
-
this.#stamps.set(message, this.#generation);
|
|
146
|
-
}
|
|
147
|
-
async #exchange(message, signal) {
|
|
148
|
-
let response;
|
|
149
|
-
try {
|
|
150
|
-
response = await this.#fetch(this.#url, {
|
|
151
|
-
method: "POST",
|
|
152
|
-
headers: {
|
|
153
|
-
"content-type": "application/json",
|
|
154
|
-
accept: "application/json, text/event-stream",
|
|
155
|
-
...this.#session === void 0 ? {} : { [MCP_SESSION_HEADER]: this.#session },
|
|
156
|
-
...this.#buildHeaders(message),
|
|
157
|
-
...this.#headers
|
|
158
|
-
},
|
|
159
|
-
body: JSON.stringify(message),
|
|
160
|
-
signal: this.#timeout === void 0 ? signal : AbortSignal.any([signal, AbortSignal.timeout(this.#timeout)])
|
|
161
|
-
});
|
|
162
|
-
} catch (error) {
|
|
163
|
-
this.#emitter.emit("error", error);
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
const session = response.headers.get(MCP_SESSION_HEADER);
|
|
167
|
-
if (session !== null) this.#session = session;
|
|
168
|
-
await this.#deliver(response, message);
|
|
169
|
-
}
|
|
170
|
-
async close() {
|
|
171
|
-
if (this.#closed) return;
|
|
172
|
-
this.#closed = true;
|
|
173
|
-
for (const request of this.#pending) request.abort();
|
|
174
|
-
this.#pending.clear();
|
|
175
|
-
this.#protocol = void 0;
|
|
176
|
-
this.#emitter.emit("close");
|
|
177
|
-
}
|
|
178
|
-
#buildHeaders(message) {
|
|
179
|
-
if (isModernRequest(message)) {
|
|
180
|
-
const version = inferRequestVersion(message);
|
|
181
|
-
const name = message.params?.["name"];
|
|
182
|
-
return {
|
|
183
|
-
...version === void 0 ? {} : { [MCP_PROTOCOL_VERSION_HEADER]: version },
|
|
184
|
-
[MCP_METHOD_HEADER]: message.method,
|
|
185
|
-
...message.method === "tools/call" && isString(name) ? {
|
|
186
|
-
[MCP_NAME_HEADER]: encodeSentinel(name),
|
|
187
|
-
...buildHeaderProjection(this.#parameters.get(name) ?? [], message.params?.["arguments"])
|
|
188
|
-
} : {}
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
return this.#protocol === void 0 ? {} : { [MCP_PROTOCOL_VERSION_HEADER]: this.#protocol };
|
|
192
|
-
}
|
|
193
|
-
async #deliver(response, sent) {
|
|
194
|
-
if (response.status === 202) return;
|
|
195
|
-
const type = response.headers.get("content-type") ?? "";
|
|
196
|
-
try {
|
|
197
|
-
if (type.includes("text/event-stream")) {
|
|
198
|
-
for (const message of await readEventStream(response)) this.#capture(message, sent);
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
if (type.includes("application/json")) {
|
|
202
|
-
const message = parseJSONRPCMessage(await response.json());
|
|
203
|
-
if (message !== void 0) this.#capture(message, sent);
|
|
204
|
-
}
|
|
205
|
-
} catch (error) {
|
|
206
|
-
this.#emitter.emit("error", error);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
#capture(message, sent) {
|
|
210
|
-
if (isJSONRPCResponse(message) && isRecord(message.result) && isMCPVersion(message.result["protocolVersion"])) this.#protocol = message.result["protocolVersion"];
|
|
211
|
-
this.#emitter.emit("message", this.#select(message, sent));
|
|
212
|
-
}
|
|
213
|
-
#select(message, sent) {
|
|
214
|
-
if (!isModernRequest(sent) || sent.method !== "tools/list") return message;
|
|
215
|
-
if (!isJSONRPCResponse(message) || message.error !== void 0) return message;
|
|
216
|
-
const result = message.result;
|
|
217
|
-
const listed = isRecord(result) ? result["tools"] : void 0;
|
|
218
|
-
if (!isRecord(result) || !isArray(listed)) return message;
|
|
219
|
-
const current = this.#stamps.get(sent) === this.#generation;
|
|
220
|
-
if (current && sent.params?.["cursor"] === void 0) this.#parameters.clear();
|
|
221
|
-
const kept = [];
|
|
222
|
-
for (const tool of listed) {
|
|
223
|
-
if (!isRecord(tool) || !isString(tool["name"])) {
|
|
224
|
-
kept.push(tool);
|
|
225
|
-
continue;
|
|
226
|
-
}
|
|
227
|
-
const parameters = buildHeaderParameters(tool["inputSchema"]);
|
|
228
|
-
if (parameters === void 0) {
|
|
229
|
-
this.#emitter.emit("error", /* @__PURE__ */ new Error(`MCP tool '${tool["name"]}' is excluded from tools/list: its inputSchema carries an invalid x-mcp-header annotation`));
|
|
230
|
-
continue;
|
|
231
|
-
}
|
|
232
|
-
if (current) this.#parameters.set(tool["name"], parameters);
|
|
233
|
-
kept.push(tool);
|
|
234
|
-
}
|
|
235
|
-
return {
|
|
236
|
-
...message,
|
|
237
|
-
result: {
|
|
238
|
-
...result,
|
|
239
|
-
tools: kept
|
|
240
|
-
}
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
|
-
};
|
|
244
9
|
//#endregion
|
|
245
10
|
//#region src/browser/transports/MessagePortTransport.ts
|
|
246
11
|
/**
|
|
247
|
-
*
|
|
248
|
-
* {@link MCPTransportInterface}
|
|
249
|
-
*
|
|
12
|
+
* Carries the Model Context Protocol over a native `MessagePort` from the browser face — a
|
|
13
|
+
* {@link MCPTransportInterface}, the genuinely new capability this face adds: MCP over
|
|
14
|
+
* `postMessage`.
|
|
250
15
|
*
|
|
251
16
|
* @remarks
|
|
252
17
|
* - **Symmetric.** Unlike {@link import('./WebSocketClientTransport.js').WebSocketClientTransport}
|
|
253
|
-
* / {@link import('
|
|
254
|
-
* carriers of `@orkestrel/mcp`'s `
|
|
255
|
-
* plain duplex channel — the
|
|
256
|
-
* `MCPTransportInterface` and is handed to
|
|
18
|
+
* / {@link import('@orkestrel/mcp').HTTPClientTransport} (CLIENT-only
|
|
19
|
+
* carriers of `@orkestrel/mcp`'s `MCPMessageTransportInterface`), a `MessagePort` is a
|
|
20
|
+
* plain duplex channel — the same class implements `@orkestrel/mcp`'s
|
|
21
|
+
* `MCPTransportInterface` and is handed to either `bindServer` or
|
|
257
22
|
* `bindClient`/`createDuplexClientTransport`; which role it plays comes entirely
|
|
258
23
|
* from the binder it is given to, not from anything this class decides.
|
|
259
24
|
* - **`start()` at construction — bind synchronously.** `MessagePort.start()` is only
|
|
260
|
-
*
|
|
25
|
+
* required when listening with `addEventListener` (as opposed to the `onmessage`
|
|
261
26
|
* setter, which implies it) — this transport uses `addEventListener`, and
|
|
262
27
|
* `MCPTransportInterface` has no separate open/connect step for the caller to hook
|
|
263
28
|
* a start into, so the constructor calls `port.start()` immediately: the port
|
|
264
|
-
* begins dispatching
|
|
265
|
-
* inside `
|
|
29
|
+
* begins dispatching queued messages the moment the transport exists. This is safe
|
|
30
|
+
* inside `createScopeServer`'s flow (the transport is synchronously handed to `bindServer`
|
|
266
31
|
* before control returns to the event loop), but is a **footgun for direct use**:
|
|
267
32
|
* if you construct `new MessagePortTransport({ port })` and then `await` anything
|
|
268
|
-
* before calling `listen`, messages that arrived in the gap are
|
|
33
|
+
* before calling `listen`, messages that arrived in the gap are dropped. **Bind
|
|
269
34
|
* synchronously after construction** — do not interleave an `await` between
|
|
270
35
|
* `new MessagePortTransport(…)` and `bindServer` / `listen`.
|
|
271
36
|
* - **String payloads only.** `send` posts the message string as-is (`postMessage`
|
|
272
37
|
* structured-clones it — a string clones to an identical string, so the wire stays
|
|
273
38
|
* plain JSON-RPC text like every other transport in this package). Inbound: a
|
|
274
39
|
* non-string `event.data` (a host or a misbehaving peer posting a structured
|
|
275
|
-
* object) is
|
|
40
|
+
* object) is ignored — dropped silently, never forwarded, never thrown —
|
|
276
41
|
* because `MCPTransportInterface` carries no `error` channel for this port to
|
|
277
|
-
* surface a non-string frame on (unlike `
|
|
42
|
+
* surface a non-string frame on (unlike `MCPMessageTransportInterface`'s `emitter`);
|
|
278
43
|
* silently ignoring is the total, contract-shaped choice.
|
|
279
|
-
* - **`messageerror` is
|
|
44
|
+
* - **`messageerror` is ignored, not routed to `closed`.** A `messageerror` event
|
|
280
45
|
* (the structured-clone deserialization of an inbound message threw) reports one
|
|
281
|
-
*
|
|
282
|
-
* formed messages still arrive.
|
|
46
|
+
* bad frame, not a dead channel — the port itself keeps working and later, well-
|
|
47
|
+
* formed messages still arrive. This transport registers no listener for it: an
|
|
48
|
+
* unhandled `messageerror` on a `MessagePort` neither throws, closes the port, nor
|
|
49
|
+
* reaches this transport, so one bad frame costs exactly that frame and nothing
|
|
50
|
+
* tears the binding down. Routing it to `closed` would tear down the
|
|
283
51
|
* `bindServer`/`bindClient` wiring (and, transitively, every session it carries)
|
|
284
|
-
* over a single malformed frame
|
|
285
|
-
* one frame — so this transport registers a `messageerror` listener that does
|
|
286
|
-
* nothing, deliberately.
|
|
52
|
+
* over a single malformed frame.
|
|
287
53
|
* - **`close()`** is idempotent: it closes the underlying `port` (`MessagePort.close()`
|
|
288
|
-
* disconnects it — further `postMessage` calls on
|
|
54
|
+
* disconnects it — further `postMessage` calls on either end are silently
|
|
289
55
|
* undelivered, per the platform contract) and fires the registered `closed`
|
|
290
56
|
* handler exactly once, whether the caller closes it once or twice. There is no
|
|
291
57
|
* native "peer closed" signal for a `MessagePort` (unlike a WebSocket's `close`
|
|
292
|
-
* event) — `closed` fires
|
|
58
|
+
* event) — `closed` fires only from this transport's own `close()`.
|
|
293
59
|
* - **Single-handler-replace (the port contract, `@orkestrel/mcp`'s `MCPTransportInterface`
|
|
294
60
|
* doc).** `listen`/`closed` each hold the one active handler; a
|
|
295
|
-
* second call
|
|
61
|
+
* second call replaces the first rather than adding a second subscriber.
|
|
296
62
|
*
|
|
297
63
|
* @example
|
|
298
64
|
* ```ts
|
|
@@ -308,14 +74,12 @@ var HTTPClientTransport = class {
|
|
|
308
74
|
var MessagePortTransport = class {
|
|
309
75
|
#port;
|
|
310
76
|
#message = (event) => this.#receive(event.data);
|
|
311
|
-
#malformed = () => {};
|
|
312
77
|
#onMessage = void 0;
|
|
313
78
|
#onClosed = void 0;
|
|
314
79
|
#closed = false;
|
|
315
80
|
constructor(options) {
|
|
316
81
|
this.#port = options.port;
|
|
317
82
|
this.#port.addEventListener("message", this.#message);
|
|
318
|
-
this.#port.addEventListener("messageerror", this.#malformed);
|
|
319
83
|
this.#port.start();
|
|
320
84
|
}
|
|
321
85
|
send(message) {
|
|
@@ -335,7 +99,6 @@ var MessagePortTransport = class {
|
|
|
335
99
|
this.#onMessage = void 0;
|
|
336
100
|
this.#onClosed = void 0;
|
|
337
101
|
this.#port.removeEventListener("message", this.#message);
|
|
338
|
-
this.#port.removeEventListener("messageerror", this.#malformed);
|
|
339
102
|
this.#port.close();
|
|
340
103
|
onClosed?.();
|
|
341
104
|
}
|
|
@@ -347,43 +110,42 @@ var MessagePortTransport = class {
|
|
|
347
110
|
//#endregion
|
|
348
111
|
//#region src/browser/transports/WebSocketClientTransport.ts
|
|
349
112
|
/**
|
|
350
|
-
*
|
|
351
|
-
* {@link
|
|
352
|
-
*
|
|
353
|
-
* {@link import('@orkestrel/mcp/server').WebSocketClientTransport}.
|
|
113
|
+
* Drives a remote MCP server over the native `WebSocket` global from the browser face, as a
|
|
114
|
+
* client {@link MCPMessageTransportInterface}. This class is the browser sibling of the Node
|
|
115
|
+
* face's {@link import('@orkestrel/mcp/server').WebSocketClientTransport}.
|
|
354
116
|
*
|
|
355
117
|
* @remarks
|
|
356
118
|
* - **Host-performed handshake.** `start()` opens `new WebSocket(url, protocols)` and
|
|
357
119
|
* waits for the native `'open'` event — the RFC 6455 handshake itself is entirely
|
|
358
120
|
* the host's concern, so this transport carries none of the Node client's
|
|
359
121
|
* `node:crypto` / `node:http(s)` machinery. A connection failure (the native
|
|
360
|
-
* `'error'` event while not yet `OPEN`)
|
|
122
|
+
* `'error'` event while not yet `OPEN`) rejects `start()`.
|
|
361
123
|
* - **Queued sends.** `send` writes each message as one text frame immediately once
|
|
362
124
|
* the socket is `OPEN`; a `send` issued before `'open'` fires (or before `start()`
|
|
363
|
-
* is even called) is
|
|
364
|
-
* so a caller need not await `start()` before calling `send`. A queue rides
|
|
365
|
-
* connection: a close
|
|
366
|
-
* - **A closed channel
|
|
125
|
+
* is even called) is queued and flushed, in order, the moment the socket opens —
|
|
126
|
+
* so a caller need not await `start()` before calling `send`. A queue rides one
|
|
127
|
+
* connection: a close discards whatever is still in it.
|
|
128
|
+
* - **A closed channel rejects.** The native socket confirms nothing about a write, so this
|
|
367
129
|
* transport answers from its own state: a `send` after `close()`, or on a socket already
|
|
368
|
-
* reporting `CLOSING` / `CLOSED`,
|
|
130
|
+
* reporting `CLOSING` / `CLOSED`, rejects with `WebSocket transport is not connected` rather
|
|
369
131
|
* than resolving on a frame nobody wrote. Only the closed state rejects — a pre-open `send`
|
|
370
132
|
* still queues.
|
|
371
|
-
* - **Inbound (`message`).** Each decoded text frame
|
|
372
|
-
*
|
|
133
|
+
* - **Inbound (`message`).** Each decoded text frame runs through the shared
|
|
134
|
+
* `deliverMessage` fold (parse, then narrow) — a well-formed {@link JSONRPCMessage}
|
|
373
135
|
* re-emits on this transport's `message` event; a non-text (binary) frame or a
|
|
374
|
-
* non-JSON / non-message text frame surfaces on `error` and is
|
|
136
|
+
* non-JSON / non-message text frame surfaces on `error` and is dropped (never
|
|
375
137
|
* throws on adversarial wire input).
|
|
376
138
|
* - **`close()`** unsubscribes from the underlying socket, closes it, and fires `close`
|
|
377
139
|
* (idempotent); the socket's native `close` event (a server-initiated close) fires the
|
|
378
|
-
*
|
|
140
|
+
* same `close` exactly once total — `close()` first flips the guard, so the native event
|
|
379
141
|
* never double-emits, and the released socket reports its own close to nobody. Closing before
|
|
380
142
|
* the socket opens resolves the pending `start()` rather than leaving it pending, matching the
|
|
381
|
-
* Node face. A `send` issued after `close()`
|
|
382
|
-
* pre-open queue is
|
|
143
|
+
* Node face. A `send` issued after `close()` rejects (it is never queued), and the
|
|
144
|
+
* pre-open queue is discarded — by `close()` and by the native `close` event alike — so a
|
|
383
145
|
* closed transport delivers nothing until a `start()` opens a new connection, and nothing
|
|
384
146
|
* the caller handed the abandoned connection rides that one.
|
|
385
|
-
* - **Observable.** Owns the `emitter` ({@link
|
|
386
|
-
* emit the emitter isolates a listener throw; `error` is a
|
|
147
|
+
* - **Observable.** Owns the `emitter` ({@link MCPMessageTransportEventMap}); every
|
|
148
|
+
* emit the emitter isolates a listener throw; `error` is a domain event (a
|
|
387
149
|
* transport-level fault).
|
|
388
150
|
*
|
|
389
151
|
* @example
|
|
@@ -412,7 +174,7 @@ var WebSocketClientTransport = class {
|
|
|
412
174
|
this.#emitter = new Emitter();
|
|
413
175
|
this.#url = options.url;
|
|
414
176
|
const protocols = options.protocols;
|
|
415
|
-
this.#protocols = typeof protocols === "string" ? protocols : protocols === void 0 ?
|
|
177
|
+
this.#protocols = typeof protocols === "string" ? protocols : protocols === void 0 ? MCP_WEBSOCKET_SUBPROTOCOL : protocols.length === 0 ? void 0 : [...protocols];
|
|
416
178
|
}
|
|
417
179
|
get emitter() {
|
|
418
180
|
return this.#emitter;
|
|
@@ -501,19 +263,7 @@ var WebSocketClientTransport = class {
|
|
|
501
263
|
this.#emitter.emit("error", /* @__PURE__ */ new Error("non-text WebSocket frame"));
|
|
502
264
|
return;
|
|
503
265
|
}
|
|
504
|
-
|
|
505
|
-
try {
|
|
506
|
-
parsed = JSON.parse(data);
|
|
507
|
-
} catch (error) {
|
|
508
|
-
this.#emitter.emit("error", error);
|
|
509
|
-
return;
|
|
510
|
-
}
|
|
511
|
-
const message = parseJSONRPCMessage(parsed);
|
|
512
|
-
if (message === void 0) {
|
|
513
|
-
this.#emitter.emit("error", /* @__PURE__ */ new Error("non-JSON-RPC WebSocket frame"));
|
|
514
|
-
return;
|
|
515
|
-
}
|
|
516
|
-
this.#emitter.emit("message", message);
|
|
266
|
+
deliverMessage(this.#emitter, data, "non-JSON-RPC WebSocket frame");
|
|
517
267
|
}
|
|
518
268
|
#onClose() {
|
|
519
269
|
if (this.#closed) return;
|
|
@@ -527,23 +277,23 @@ var WebSocketClientTransport = class {
|
|
|
527
277
|
//#endregion
|
|
528
278
|
//#region src/browser/factories.ts
|
|
529
279
|
/**
|
|
530
|
-
* Creates the browser-face WebSocket
|
|
531
|
-
* {@link import('@orkestrel/mcp').MCPClientInterface} — a {@link
|
|
532
|
-
* that drives a
|
|
533
|
-
* sibling of the Node face's `createWebSocketClientTransport` (`@orkestrel/mcp/server`).
|
|
280
|
+
* Creates the browser-face WebSocket client transport for an
|
|
281
|
+
* {@link import('@orkestrel/mcp').MCPClientInterface} — a {@link MCPMessageTransportInterface}
|
|
282
|
+
* that drives a remote MCP server over the native `WebSocket` global. This factory is the
|
|
283
|
+
* browser sibling of the Node face's `createWebSocketClientTransport` (`@orkestrel/mcp/server`).
|
|
534
284
|
*
|
|
535
285
|
* @remarks
|
|
536
286
|
* Hand it to `createMCPClient({ transport })`: `start()` (run by `client.connect()`)
|
|
537
287
|
* opens `new WebSocket(options.url, options.protocols)` and awaits the native
|
|
538
288
|
* `'open'` event — the RFC 6455 handshake itself is the browser's concern. Each
|
|
539
|
-
* JSON-RPC message the client `send`s before the socket opens is
|
|
289
|
+
* JSON-RPC message the client `send`s before the socket opens is queued and flushed,
|
|
540
290
|
* in order, once it does; each decoded reply is surfaced on the transport's
|
|
541
291
|
* `message` event for the client's id correlation.
|
|
542
292
|
*
|
|
543
|
-
* @param options - `url` (the remote WebSocket endpoint;
|
|
293
|
+
* @param options - `url` (the remote WebSocket endpoint; required) and optional
|
|
544
294
|
* `protocols` (the WebSocket subprotocol(s) to request); see
|
|
545
295
|
* {@link WebSocketClientTransportOptions}
|
|
546
|
-
* @returns A working {@link
|
|
296
|
+
* @returns A working {@link MCPMessageTransportInterface} over the native `WebSocket`
|
|
547
297
|
*
|
|
548
298
|
* @example
|
|
549
299
|
* ```ts
|
|
@@ -561,31 +311,36 @@ function createWebSocketClientTransport(options) {
|
|
|
561
311
|
return new WebSocketClientTransport(options);
|
|
562
312
|
}
|
|
563
313
|
/**
|
|
564
|
-
* Creates the
|
|
565
|
-
* {@link import('@orkestrel/mcp').MCPClientInterface} — a {@link
|
|
566
|
-
* that drives a
|
|
567
|
-
*
|
|
314
|
+
* Creates the HTTP client transport for an
|
|
315
|
+
* {@link import('@orkestrel/mcp').MCPClientInterface} — a {@link MCPMessageTransportInterface}
|
|
316
|
+
* that drives a remote Streamable-HTTP MCP server over the native `fetch`.
|
|
317
|
+
*
|
|
318
|
+
* @remarks
|
|
319
|
+
* It returns the core {@link import('@orkestrel/mcp').HTTPClientTransport}, the same class the
|
|
320
|
+
* Node face's `createHTTPClientTransport` returns, because the class touches `fetch`,
|
|
321
|
+
* `Response`, `AbortController`, `AbortSignal`, and `WeakMap` alone. This factory exists so a
|
|
322
|
+
* page imports its transport from the face it already imports everything else from.
|
|
568
323
|
*
|
|
569
324
|
* @remarks
|
|
570
325
|
* Hand it to `createMCPClient({ transport })`: each JSON-RPC message the client
|
|
571
326
|
* sends is `POST`ed to `options.url` with `content-type: application/json` and an
|
|
572
327
|
* `Accept` of both `application/json` and `text/event-stream` (the server answers
|
|
573
|
-
* with
|
|
328
|
+
* with either — a plain JSON envelope or a Streamable-HTTP SSE `data:` event,
|
|
574
329
|
* decoded with `@orkestrel/sse`), and the reply is surfaced on the transport's
|
|
575
330
|
* `message` event for the client's id correlation. Add `options.headers` (for example, an
|
|
576
331
|
* `Authorization` bearer) to reach a guarded server. `start` / `close` hold no
|
|
577
|
-
* connection; against a
|
|
332
|
+
* connection; against a stateful server it captures the `mcp-session-id` from
|
|
578
333
|
* `initialize` and echoes it on later requests. It also captures the initialize
|
|
579
334
|
* result's `protocolVersion` and sends `mcp-protocol-version` alone on subsequent
|
|
580
335
|
* legacy requests. Modern requests instead derive `mcp-protocol-version` and
|
|
581
336
|
* `mcp-method` from the message, plus `mcp-name` only for `tools/call`, so the
|
|
582
337
|
* same `MCPClient` passes either era's protocol gates without caller wiring.
|
|
583
338
|
*
|
|
584
|
-
* @param options - `url` (the remote endpoint;
|
|
339
|
+
* @param options - `url` (the remote endpoint; required), optional `headers` merged
|
|
585
340
|
* onto every request, optional `fetch` (default `globalThis.fetch`), and optional
|
|
586
341
|
* `timeout` (ms, applied with `AbortSignal.timeout`); see
|
|
587
342
|
* {@link HTTPClientTransportOptions}
|
|
588
|
-
* @returns A working {@link
|
|
343
|
+
* @returns A working {@link MCPMessageTransportInterface} over the native `fetch`
|
|
589
344
|
*
|
|
590
345
|
* @example
|
|
591
346
|
* ```ts
|
|
@@ -605,7 +360,7 @@ function createHTTPClientTransport(options) {
|
|
|
605
360
|
/**
|
|
606
361
|
* Creates the browser-face `MessagePort` transport — a
|
|
607
362
|
* {@link import('@orkestrel/mcp').MCPTransportInterface} over a native `MessagePort`, the
|
|
608
|
-
*
|
|
363
|
+
* symmetric carrier that works as either a server or a client transport depending on
|
|
609
364
|
* which binder ({@link import('@orkestrel/mcp').bindServer} or
|
|
610
365
|
* {@link import('@orkestrel/mcp').bindClient}) it is handed to.
|
|
611
366
|
*
|
|
@@ -615,7 +370,7 @@ function createHTTPClientTransport(options) {
|
|
|
615
370
|
* dropped, never thrown); `messageerror` is ignored (one bad frame does not close the
|
|
616
371
|
* channel); `close()` closes the port and fires `closed` exactly once.
|
|
617
372
|
*
|
|
618
|
-
* @param options - `port` (the `MessagePort` half to drive;
|
|
373
|
+
* @param options - `port` (the `MessagePort` half to drive; required); see
|
|
619
374
|
* {@link MessagePortTransportOptions}
|
|
620
375
|
* @returns A working {@link import('@orkestrel/mcp').MCPTransportInterface} over the port
|
|
621
376
|
*
|
|
@@ -633,143 +388,92 @@ function createMessagePortTransport(options) {
|
|
|
633
388
|
return new MessagePortTransport(options);
|
|
634
389
|
}
|
|
635
390
|
/**
|
|
636
|
-
*
|
|
637
|
-
*
|
|
638
|
-
* implicit, portless message channel `serveMCPScope` binds for the
|
|
639
|
-
* dedicated-worker shape.
|
|
391
|
+
* Creates an `MCPServer` hosted inside a worker scope and wires that scope's message events
|
|
392
|
+
* to it — the browser face's bootstrap, and the twin of the Node face's `createStdioServer`.
|
|
640
393
|
*
|
|
641
394
|
* @remarks
|
|
642
|
-
* `
|
|
643
|
-
*
|
|
644
|
-
*
|
|
645
|
-
* for every portless, string-payload event (there is no native registration point on
|
|
646
|
-
* the scope itself for `serveMCPScope` to hand a `listen` handler to, so `deliver` is
|
|
647
|
-
* the bridge). `close()` fires the registered `closed` handler — a scope has nothing
|
|
648
|
-
* physically closable, so this is the only teardown signal available.
|
|
395
|
+
* `scope` defaults to `globalThis`, which is `self` inside a dedicated Web Worker or a
|
|
396
|
+
* Service Worker, so a worker boots with `createScopeServer({ tools })` alone; pass a scope
|
|
397
|
+
* explicitly to host a server on a double or on another message-event-bearing object.
|
|
649
398
|
*
|
|
650
|
-
*
|
|
651
|
-
*
|
|
652
|
-
*
|
|
399
|
+
* Port-bearing events are gated by `options.accept`, deduplicated by port, and receive
|
|
400
|
+
* their own `MessagePortTransport` binding. Portless string events use the scope's
|
|
401
|
+
* implicit channel. The returned handle's `stop` removes the listener, unbinds the implicit
|
|
402
|
+
* channel, closes every accepted port binding, and drops the ports themselves — the
|
|
403
|
+
* bindings are held in one map keyed by port, so nothing survives the clear. The served
|
|
404
|
+
* endpoint is modern-only: it answers a legacy `initialize` with `-32601`. A dual-era
|
|
405
|
+
* worker composes `bindServer(createMCPLegacy(mcp), …)` instead of this factory.
|
|
406
|
+
*
|
|
407
|
+
* @param options - The tools, optional identity, and optional port-event gate; see
|
|
408
|
+
* {@link ScopeServerOptions}
|
|
409
|
+
* @param scope - The hostable scope to wire; defaults to `globalThis`
|
|
410
|
+
* @returns A {@link ScopeServerInterface} whose `stop` ends every binding this call owns
|
|
653
411
|
*
|
|
654
412
|
* @example
|
|
655
413
|
* ```ts
|
|
656
|
-
*
|
|
657
|
-
*
|
|
658
|
-
* ```
|
|
659
|
-
*/
|
|
660
|
-
function createScopeTransport(scope) {
|
|
661
|
-
let onMessage;
|
|
662
|
-
let onClosed;
|
|
663
|
-
return {
|
|
664
|
-
send(message) {
|
|
665
|
-
scope.postMessage(message);
|
|
666
|
-
},
|
|
667
|
-
listen(handler) {
|
|
668
|
-
onMessage = handler;
|
|
669
|
-
},
|
|
670
|
-
closed(handler) {
|
|
671
|
-
onClosed = handler;
|
|
672
|
-
},
|
|
673
|
-
close() {
|
|
674
|
-
onClosed?.();
|
|
675
|
-
},
|
|
676
|
-
deliver(message) {
|
|
677
|
-
onMessage?.(message);
|
|
678
|
-
}
|
|
679
|
-
};
|
|
680
|
-
}
|
|
681
|
-
//#endregion
|
|
682
|
-
//#region src/browser/helpers.ts
|
|
683
|
-
/**
|
|
684
|
-
* Decodes one SSE event's `data` string into a {@link JSONRPCMessage}, or `undefined`
|
|
685
|
-
* when it is not one — the per-event step {@link readEventStream} folds over.
|
|
414
|
+
* import { createScopeServer } from '@orkestrel/mcp/browser'
|
|
415
|
+
* import { createToolManager } from '@orkestrel/tool'
|
|
686
416
|
*
|
|
687
|
-
*
|
|
688
|
-
*
|
|
689
|
-
*
|
|
690
|
-
*
|
|
691
|
-
*
|
|
692
|
-
*
|
|
693
|
-
* @param data - One SSE event's `data` payload
|
|
694
|
-
* @returns The decoded {@link JSONRPCMessage}, or `undefined`
|
|
695
|
-
*/
|
|
696
|
-
function decodeEvent(data) {
|
|
697
|
-
try {
|
|
698
|
-
return parseJSONRPCMessage(JSON.parse(data));
|
|
699
|
-
} catch {
|
|
700
|
-
return;
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
/**
|
|
704
|
-
* Decodes a `fetch` Response's Server-Sent-Events body into the JSON-RPC messages it
|
|
705
|
-
* carried — the CLIENT-side inverse of the server's Streamable-HTTP SSE response.
|
|
706
|
-
*
|
|
707
|
-
* @remarks
|
|
708
|
-
* Reads the whole `response.body` stream chunk-by-chunk through a `TextDecoder({
|
|
709
|
-
* stream: true })` (handling a multi-byte char split across reads) and
|
|
710
|
-
* `@orkestrel/sse`'s {@link SSEParserInterface} (handling a partial line / in-progress
|
|
711
|
-
* event split across reads), then narrows each dispatched event's `data` to a
|
|
712
|
-
* {@link JSONRPCMessage} through {@link decodeEvent} (so a non-message / non-JSON `data:`
|
|
713
|
-
* event is DROPPED, never thrown — total). A `null` body (no stream) yields no
|
|
714
|
-
* messages; {@link import('./transports/HTTPClientTransport.js').HTTPClientTransport}
|
|
715
|
-
* reads a request/response SSE reply (the server sends one `data:` event then ends),
|
|
716
|
-
* so this drains to completion.
|
|
717
|
-
*
|
|
718
|
-
* @param response - The SSE `fetch` Response to decode (its `body` is read to completion)
|
|
719
|
-
* @returns Every {@link JSONRPCMessage} the stream carried, in order
|
|
417
|
+
* // Inside a Web Worker: the scope defaults to `globalThis`.
|
|
418
|
+
* const worker = createScopeServer({ tools: createToolManager() })
|
|
419
|
+
* // ... later, release every binding this call owns:
|
|
420
|
+
* worker.stop()
|
|
421
|
+
* ```
|
|
720
422
|
*/
|
|
721
|
-
|
|
722
|
-
const
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
const messages = [];
|
|
728
|
-
try {
|
|
729
|
-
for (;;) {
|
|
730
|
-
const { done, value } = await reader.read();
|
|
731
|
-
if (done) break;
|
|
732
|
-
for (const event of parser.parse(decoder.decode(value, { stream: true }))) {
|
|
733
|
-
const message = decodeEvent(event.data);
|
|
734
|
-
if (message !== void 0) messages.push(message);
|
|
735
|
-
}
|
|
423
|
+
function createScopeServer(options, scope = globalThis) {
|
|
424
|
+
const server = createMCPServer({
|
|
425
|
+
tools: options.tools,
|
|
426
|
+
identity: {
|
|
427
|
+
name: options.name ?? "@orkestrel/mcp",
|
|
428
|
+
version: options.version ?? "1.0.0"
|
|
736
429
|
}
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
430
|
+
});
|
|
431
|
+
const scopeTransport = createScopeTransport(scope);
|
|
432
|
+
const unbindScope = bindServer(server, scopeTransport);
|
|
433
|
+
const teardowns = /* @__PURE__ */ new Map();
|
|
434
|
+
const onMessage = createScopeMessageListener(server, scopeTransport, teardowns, options);
|
|
435
|
+
scope.addEventListener("message", onMessage);
|
|
436
|
+
let stopped = false;
|
|
437
|
+
return { stop() {
|
|
438
|
+
if (stopped) return;
|
|
439
|
+
stopped = true;
|
|
440
|
+
scope.removeEventListener("message", onMessage);
|
|
441
|
+
unbindScope();
|
|
442
|
+
for (const teardown of teardowns.values()) teardown();
|
|
443
|
+
teardowns.clear();
|
|
444
|
+
} };
|
|
741
445
|
}
|
|
742
446
|
/**
|
|
743
|
-
* Builds
|
|
744
|
-
*
|
|
745
|
-
*
|
|
447
|
+
* Builds {@link createScopeServer}'s `message`-event listener — the unified dispatcher that
|
|
448
|
+
* routes every inbound event on a hostable scope, portless or port-bearing, to the right
|
|
449
|
+
* binding.
|
|
746
450
|
*
|
|
747
451
|
* @remarks
|
|
748
|
-
* Port-bearing events (`event.ports.length > 0`) are gated by `options.accept`
|
|
452
|
+
* Port-bearing events (`event.ports.length > 0`) are gated by `options.accept` first
|
|
749
453
|
* — when the gate returns `false` the event is dropped entirely (no binding, no reply).
|
|
750
454
|
* Accepted events spawn a fresh `MessagePortTransport` over `event.ports[0]`,
|
|
751
455
|
* `bindServer` `server` onto it, and record a teardown (`unbind` then `transport.close()`)
|
|
752
|
-
* into `teardowns`
|
|
456
|
+
* into `teardowns` keyed by that port. A port already present is ignored — repeated delivery
|
|
753
457
|
* of the same `MessagePort` would create duplicate bindings over one port (→ duplicated
|
|
754
458
|
* replies), so a repeat is silently dropped.
|
|
755
459
|
*
|
|
756
|
-
* The key is what makes `teardowns` the
|
|
757
|
-
* seen-port set would be a second collection over the same lifetime, and the
|
|
758
|
-
* would have to remember to empty both — so a long-lived scope such as a Service Worker
|
|
759
|
-
* retain every port it ever accepted, closed and unbound ones included. Membership
|
|
760
|
-
* "already bound?" and `clear()` drops the binding and the dedup together.
|
|
460
|
+
* The key is what makes `teardowns` the only place an accepted port is remembered. A separate
|
|
461
|
+
* seen-port set would be a second collection over the same lifetime, and the scope server's
|
|
462
|
+
* `stop` would have to remember to empty both — so a long-lived scope such as a Service Worker
|
|
463
|
+
* would retain every port it ever accepted, closed and unbound ones included. Membership
|
|
464
|
+
* answers "already bound?" and `clear()` drops the binding and the dedup together.
|
|
761
465
|
*
|
|
762
|
-
* This branch fires on
|
|
466
|
+
* This branch fires on either a Service-Worker-shaped scope (its normal per-client
|
|
763
467
|
* channel) or a dedicated-worker-shaped one that happens to receive a port-bearing event
|
|
764
468
|
* (the unified design's deliberate cross-case, needing no upfront shape flag). An event
|
|
765
|
-
* with
|
|
469
|
+
* with no ports and a string `data` is pushed onto `scopeTransport.deliver` (the
|
|
766
470
|
* implicit, already-bound scope channel); any other event (no ports, non-string data)
|
|
767
471
|
* is silently dropped — total, never throws.
|
|
768
472
|
*
|
|
769
473
|
* @param server - The `MCPServerInterface` every spawned/implicit binding dispatches over
|
|
770
474
|
* @param scopeTransport - The implicit scope channel (already `bindServer`-bound) portless events deliver onto
|
|
771
|
-
* @param teardowns - The shared teardown map
|
|
772
|
-
* @param options - The `
|
|
475
|
+
* @param teardowns - The shared teardown map the scope server's `stop` drains and clears, keyed by the accepted port; each port-bearing event adds one entry
|
|
476
|
+
* @param options - The `ScopeServerOptions` (for `options.accept`)
|
|
773
477
|
* @returns The `message`-event listener to register (and later remove) on the scope
|
|
774
478
|
*
|
|
775
479
|
* @example
|
|
@@ -801,58 +505,51 @@ function createScopeMessageListener(server, scopeTransport, teardowns, options)
|
|
|
801
505
|
};
|
|
802
506
|
}
|
|
803
507
|
/**
|
|
804
|
-
*
|
|
508
|
+
* Adapts a hostable {@link ScopeInterface} (`self` in a dedicated Web Worker, or any
|
|
509
|
+
* structurally matching double) into a {@link ScopeTransportInterface} — the implicit,
|
|
510
|
+
* portless message channel {@link createScopeServer} binds for the dedicated-worker shape.
|
|
805
511
|
*
|
|
806
512
|
* @remarks
|
|
807
|
-
*
|
|
808
|
-
*
|
|
809
|
-
*
|
|
810
|
-
*
|
|
811
|
-
*
|
|
812
|
-
*
|
|
813
|
-
*
|
|
513
|
+
* `send` writes each outbound string through `scope.postMessage`. `listen`/`closed`
|
|
514
|
+
* register the single handler `deliver` / the underlying close path route through —
|
|
515
|
+
* the scope server's own `scope` `message`-event listener calls `deliver(event.data)`
|
|
516
|
+
* for every portless, string-payload event (there is no native registration point on
|
|
517
|
+
* the scope itself for the scope server to hand a `listen` handler to, so `deliver` is
|
|
518
|
+
* the bridge). `close()` fires the registered `closed` handler — a scope has nothing
|
|
519
|
+
* physically closable, so this is the only teardown signal available.
|
|
814
520
|
*
|
|
815
|
-
* @param scope - The hostable
|
|
816
|
-
*
|
|
817
|
-
* @returns
|
|
521
|
+
* @param scope - The hostable scope to adapt (structurally, `self` / `globalThis`
|
|
522
|
+
* inside a dedicated Web Worker)
|
|
523
|
+
* @returns A {@link ScopeTransportInterface} the scope server binds and drives through `deliver`
|
|
524
|
+
*
|
|
525
|
+
* @example
|
|
526
|
+
* ```ts
|
|
527
|
+
* const scopeTransport = createScopeTransport(self)
|
|
528
|
+
* const unbind = bindServer(server, scopeTransport)
|
|
529
|
+
* ```
|
|
818
530
|
*/
|
|
819
|
-
function
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
531
|
+
function createScopeTransport(scope) {
|
|
532
|
+
let onMessage;
|
|
533
|
+
let onClosed;
|
|
534
|
+
return {
|
|
535
|
+
send(message) {
|
|
536
|
+
scope.postMessage(message);
|
|
537
|
+
},
|
|
538
|
+
listen(handler) {
|
|
539
|
+
onMessage = handler;
|
|
540
|
+
},
|
|
541
|
+
closed(handler) {
|
|
542
|
+
onClosed = handler;
|
|
543
|
+
},
|
|
544
|
+
close() {
|
|
545
|
+
onClosed?.();
|
|
546
|
+
},
|
|
547
|
+
deliver(message) {
|
|
548
|
+
onMessage?.(message);
|
|
825
549
|
}
|
|
826
|
-
});
|
|
827
|
-
const scopeTransport = createScopeTransport(scope);
|
|
828
|
-
const unbindScope = bindServer(server, scopeTransport);
|
|
829
|
-
const teardowns = /* @__PURE__ */ new Map();
|
|
830
|
-
const onMessage = createScopeMessageListener(server, scopeTransport, teardowns, options);
|
|
831
|
-
scope.addEventListener("message", onMessage);
|
|
832
|
-
let disposed = false;
|
|
833
|
-
return () => {
|
|
834
|
-
if (disposed) return;
|
|
835
|
-
disposed = true;
|
|
836
|
-
scope.removeEventListener("message", onMessage);
|
|
837
|
-
unbindScope();
|
|
838
|
-
for (const teardown of teardowns.values()) teardown();
|
|
839
|
-
teardowns.clear();
|
|
840
550
|
};
|
|
841
551
|
}
|
|
842
|
-
/**
|
|
843
|
-
* Boots an `MCPServer` inside the current hostable worker scope.
|
|
844
|
-
*
|
|
845
|
-
* @remarks
|
|
846
|
-
* The served endpoint is modern-only: it answers a legacy `initialize` with `-32601`. A
|
|
847
|
-
* dual-era worker composes `bindServer(createMCPLegacy(mcp), …)` instead of this function.
|
|
848
|
-
*
|
|
849
|
-
* @param options - The tools, optional identity, and optional port-event gate
|
|
850
|
-
* @returns The disposer returned by {@link serveMCPScope}
|
|
851
|
-
*/
|
|
852
|
-
function serveMCP(options) {
|
|
853
|
-
return serveMCPScope(globalThis, options);
|
|
854
|
-
}
|
|
855
552
|
//#endregion
|
|
856
|
-
export { DEFAULT_MCP_SERVER_NAME, DEFAULT_MCP_SERVER_VERSION,
|
|
553
|
+
export { DEFAULT_MCP_SERVER_NAME, DEFAULT_MCP_SERVER_VERSION, MessagePortTransport, WebSocketClientTransport, createHTTPClientTransport, createMessagePortTransport, createScopeMessageListener, createScopeServer, createScopeTransport, createWebSocketClientTransport };
|
|
857
554
|
|
|
858
555
|
//# sourceMappingURL=index.js.map
|