@orkestrel/mcp 0.0.19 → 0.0.21
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 +23 -22
- package/dist/src/browser/index.d.ts +92 -70
- package/dist/src/browser/index.js +164 -81
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/core/index.cjs +269 -257
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +351 -358
- package/dist/src/core/index.d.ts +351 -358
- package/dist/src/core/index.js +270 -257
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +331 -173
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +266 -146
- package/dist/src/server/index.d.ts +266 -146
- package/dist/src/server/index.js +331 -173
- package/dist/src/server/index.js.map +1 -1
- package/package.json +13 -10
|
@@ -33,8 +33,8 @@ var DEFAULT_MCP_SERVER_NAME = "taverna";
|
|
|
33
33
|
var DEFAULT_MCP_SERVER_VERSION = "1.0.0";
|
|
34
34
|
/**
|
|
35
35
|
* The WebSocket subprotocol `createWebSocketClientTransport` requests by default —
|
|
36
|
-
* `'mcp'`,
|
|
37
|
-
*
|
|
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
38
|
* a subprotocol it did not request; Node ≥ 22 (undici) enforces this strictly, so the
|
|
39
39
|
* default bakes the correct value in. Override `WebSocketClientTransportOptions.protocols`
|
|
40
40
|
* only when connecting to a foreign server that speaks a different subprotocol (or `[]`
|
|
@@ -47,7 +47,7 @@ var MCP_WEBSOCKET_SUBPROTOCOL = "mcp";
|
|
|
47
47
|
* The browser-face HTTP CLIENT transport for the Model Context Protocol — a
|
|
48
48
|
* {@link MCPClientTransportInterface} that drives a REMOTE Streamable-HTTP MCP server
|
|
49
49
|
* over the native `fetch`, the browser sibling of the Node face's
|
|
50
|
-
* {@link import('@
|
|
50
|
+
* {@link import('@orkestrel/mcp/server').HTTPClientTransport}, honoring the SAME
|
|
51
51
|
* `mcp-session-id` semantics so it interoperates with an `MCPSession`-based server
|
|
52
52
|
* unchanged.
|
|
53
53
|
*
|
|
@@ -55,12 +55,12 @@ var MCP_WEBSOCKET_SUBPROTOCOL = "mcp";
|
|
|
55
55
|
* - **Request/response over `fetch`.** `send(message)` POSTs the JSON-serialized
|
|
56
56
|
* message to `options.url` with `content-type: application/json` and an
|
|
57
57
|
* `Accept` of BOTH `application/json` and `text/event-stream` (so the server may
|
|
58
|
-
* answer with either framing) — plus any `options.headers` (
|
|
58
|
+
* answer with either framing) — plus any `options.headers` (for example, an
|
|
59
59
|
* `Authorization` bearer). It then decodes the reply and emits each decoded
|
|
60
60
|
* {@link JSONRPCMessage} on the `message` event the
|
|
61
|
-
* {@link import('@
|
|
61
|
+
* {@link import('@orkestrel/mcp').MCPClientInterface} subscribes to.
|
|
62
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
|
|
63
|
+
* `parseJSONRPCMessage`; a `200` with a `text/event-stream` body is decoded with the
|
|
64
64
|
* `@orkestrel/sse` {@link import('@orkestrel/sse').SSEParserInterface} (the browser
|
|
65
65
|
* face's own `readEventStream`) — the inverse of the server's `openStream` seam, so
|
|
66
66
|
* the wire round-trips. A `202` Accepted (a notification) carries no body and emits
|
|
@@ -78,10 +78,15 @@ var MCP_WEBSOCKET_SUBPROTOCOL = "mcp";
|
|
|
78
78
|
* Before initialize returns, neither captured legacy header is sent.
|
|
79
79
|
* `close()` clears the captured protocol so a reconnect's `initialize`
|
|
80
80
|
* POST is headerless; the captured `session` persists across `close()`.
|
|
81
|
-
* -
|
|
81
|
+
* - **`close()` releases what is in flight.** Every `fetch` this transport still has open is
|
|
82
|
+
* ABORTED, which cancels the response body a `send` is reading — an SSE reply the server
|
|
83
|
+
* never ends would otherwise outlive the transport, with nothing left able to reach it. The
|
|
84
|
+
* aborted read surfaces on `error` and the `send` reporting it resolves. `close()` is
|
|
85
|
+
* idempotent (one `close` event per connected lifetime), and `start()` opens the next one.
|
|
86
|
+
* - **Total at the boundary.** Every reply is narrowed (`parseJSONRPCMessage`,
|
|
82
87
|
* the SSE decoder) — a non-message reply is dropped, never asserted; a `fetch` /
|
|
83
88
|
* decode failure surfaces on the `error` event rather than escaping `send`.
|
|
84
|
-
* - **Observable
|
|
89
|
+
* - **Observable.** Owns the `emitter` ({@link MCPClientTransportEventMap}); fires
|
|
85
90
|
* `message` per decoded reply, `error` on a fault, and `close` on `close()`.
|
|
86
91
|
*
|
|
87
92
|
* @example
|
|
@@ -97,8 +102,10 @@ var HTTPClientTransport = class {
|
|
|
97
102
|
#headers;
|
|
98
103
|
#fetch;
|
|
99
104
|
#timeout;
|
|
105
|
+
#pending = /* @__PURE__ */ new Set();
|
|
100
106
|
#session = void 0;
|
|
101
107
|
#protocol = void 0;
|
|
108
|
+
#closed = false;
|
|
102
109
|
constructor(options) {
|
|
103
110
|
this.#emitter = new Emitter();
|
|
104
111
|
this.#url = options.url;
|
|
@@ -115,8 +122,19 @@ var HTTPClientTransport = class {
|
|
|
115
122
|
get duplex() {
|
|
116
123
|
return false;
|
|
117
124
|
}
|
|
118
|
-
async start() {
|
|
125
|
+
async start() {
|
|
126
|
+
this.#closed = false;
|
|
127
|
+
}
|
|
119
128
|
async send(message) {
|
|
129
|
+
const request = new AbortController();
|
|
130
|
+
this.#pending.add(request);
|
|
131
|
+
try {
|
|
132
|
+
await this.#exchange(message, request.signal);
|
|
133
|
+
} finally {
|
|
134
|
+
this.#pending.delete(request);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async #exchange(message, signal) {
|
|
120
138
|
let response;
|
|
121
139
|
try {
|
|
122
140
|
response = await this.#fetch(this.#url, {
|
|
@@ -129,7 +147,7 @@ var HTTPClientTransport = class {
|
|
|
129
147
|
...this.#headers
|
|
130
148
|
},
|
|
131
149
|
body: JSON.stringify(message),
|
|
132
|
-
|
|
150
|
+
signal: this.#timeout === void 0 ? signal : AbortSignal.any([signal, AbortSignal.timeout(this.#timeout)])
|
|
133
151
|
});
|
|
134
152
|
} catch (error) {
|
|
135
153
|
this.#emitter.emit("error", error);
|
|
@@ -140,6 +158,10 @@ var HTTPClientTransport = class {
|
|
|
140
158
|
await this.#deliver(response);
|
|
141
159
|
}
|
|
142
160
|
async close() {
|
|
161
|
+
if (this.#closed) return;
|
|
162
|
+
this.#closed = true;
|
|
163
|
+
for (const request of this.#pending) request.abort();
|
|
164
|
+
this.#pending.clear();
|
|
143
165
|
this.#protocol = void 0;
|
|
144
166
|
this.#emitter.emit("close");
|
|
145
167
|
}
|
|
@@ -186,13 +208,13 @@ var HTTPClientTransport = class {
|
|
|
186
208
|
* @remarks
|
|
187
209
|
* - **Symmetric.** Unlike {@link import('./WebSocketClientTransport.js').WebSocketClientTransport}
|
|
188
210
|
* / {@link import('./HTTPClientTransport.js').HTTPClientTransport} (CLIENT-only
|
|
189
|
-
* carriers of `@
|
|
190
|
-
* plain duplex channel — the SAME class implements `@
|
|
211
|
+
* carriers of `@orkestrel/mcp`'s `MCPClientTransportInterface`), a `MessagePort` is a
|
|
212
|
+
* plain duplex channel — the SAME class implements `@orkestrel/mcp`'s
|
|
191
213
|
* `MCPTransportInterface` and is handed to EITHER `bindServer` or
|
|
192
214
|
* `bindClient`/`createDuplexClientTransport`; which role it plays comes entirely
|
|
193
215
|
* from the binder it is given to, not from anything this class decides.
|
|
194
216
|
* - **`start()` at construction — bind synchronously.** `MessagePort.start()` is only
|
|
195
|
-
* REQUIRED when listening
|
|
217
|
+
* REQUIRED when listening with `addEventListener` (as opposed to the `onmessage`
|
|
196
218
|
* setter, which implies it) — this transport uses `addEventListener`, and
|
|
197
219
|
* `MCPTransportInterface` has no separate open/connect step for the caller to hook
|
|
198
220
|
* a start into, so the constructor calls `port.start()` immediately: the port
|
|
@@ -207,7 +229,7 @@ var HTTPClientTransport = class {
|
|
|
207
229
|
* structured-clones it — a string clones to an identical string, so the wire stays
|
|
208
230
|
* plain JSON-RPC text like every other transport in this package). Inbound: a
|
|
209
231
|
* non-string `event.data` (a host or a misbehaving peer posting a structured
|
|
210
|
-
* object) is IGNORED — dropped silently, never forwarded, never thrown
|
|
232
|
+
* object) is IGNORED — dropped silently, never forwarded, never thrown —
|
|
211
233
|
* because `MCPTransportInterface` carries no `error` channel for this port to
|
|
212
234
|
* surface a non-string frame on (unlike `MCPClientTransportInterface`'s `emitter`);
|
|
213
235
|
* silently ignoring is the total, contract-shaped choice.
|
|
@@ -225,8 +247,8 @@ var HTTPClientTransport = class {
|
|
|
225
247
|
* handler exactly once, whether the caller closes it once or twice. There is no
|
|
226
248
|
* native "peer closed" signal for a `MessagePort` (unlike a WebSocket's `close`
|
|
227
249
|
* event) — `closed` fires ONLY from this transport's own `close()`.
|
|
228
|
-
* - **Single-handler-replace (the port contract, `@
|
|
229
|
-
* doc).** `listen`/`closed` each hold the
|
|
250
|
+
* - **Single-handler-replace (the port contract, `@orkestrel/mcp`'s `MCPTransportInterface`
|
|
251
|
+
* doc).** `listen`/`closed` each hold the one active handler; a
|
|
230
252
|
* second call REPLACES the first rather than adding a second subscriber.
|
|
231
253
|
*
|
|
232
254
|
* @example
|
|
@@ -242,13 +264,15 @@ var HTTPClientTransport = class {
|
|
|
242
264
|
*/
|
|
243
265
|
var MessagePortTransport = class {
|
|
244
266
|
#port;
|
|
267
|
+
#message = (event) => this.#receive(event.data);
|
|
268
|
+
#malformed = () => {};
|
|
245
269
|
#onMessage = void 0;
|
|
246
270
|
#onClosed = void 0;
|
|
247
271
|
#closed = false;
|
|
248
272
|
constructor(options) {
|
|
249
273
|
this.#port = options.port;
|
|
250
|
-
this.#port.addEventListener("message",
|
|
251
|
-
this.#port.addEventListener("messageerror",
|
|
274
|
+
this.#port.addEventListener("message", this.#message);
|
|
275
|
+
this.#port.addEventListener("messageerror", this.#malformed);
|
|
252
276
|
this.#port.start();
|
|
253
277
|
}
|
|
254
278
|
send(message) {
|
|
@@ -264,8 +288,13 @@ var MessagePortTransport = class {
|
|
|
264
288
|
close() {
|
|
265
289
|
if (this.#closed) return;
|
|
266
290
|
this.#closed = true;
|
|
291
|
+
const onClosed = this.#onClosed;
|
|
292
|
+
this.#onMessage = void 0;
|
|
293
|
+
this.#onClosed = void 0;
|
|
294
|
+
this.#port.removeEventListener("message", this.#message);
|
|
295
|
+
this.#port.removeEventListener("messageerror", this.#malformed);
|
|
267
296
|
this.#port.close();
|
|
268
|
-
|
|
297
|
+
onClosed?.();
|
|
269
298
|
}
|
|
270
299
|
#receive(data) {
|
|
271
300
|
if (!isString(data)) return;
|
|
@@ -278,7 +307,7 @@ var MessagePortTransport = class {
|
|
|
278
307
|
* The browser-face WebSocket CLIENT transport for the Model Context Protocol — a
|
|
279
308
|
* {@link MCPClientTransportInterface} that drives a REMOTE MCP server over the native
|
|
280
309
|
* `WebSocket` global, the browser sibling of the Node face's
|
|
281
|
-
* {@link import('@
|
|
310
|
+
* {@link import('@orkestrel/mcp/server').WebSocketClientTransport}.
|
|
282
311
|
*
|
|
283
312
|
* @remarks
|
|
284
313
|
* - **Host-performed handshake.** `start()` opens `new WebSocket(url, protocols)` and
|
|
@@ -293,15 +322,16 @@ var MessagePortTransport = class {
|
|
|
293
322
|
* - **Inbound (`message`).** Each decoded text frame is `JSON.parse`d (guarded) and
|
|
294
323
|
* narrowed with `parseJSONRPCMessage` — a well-formed {@link JSONRPCMessage}
|
|
295
324
|
* re-emits on this transport's `message` event; a non-text (binary) frame or a
|
|
296
|
-
* non-JSON / non-message text frame surfaces on `error` and is DROPPED (
|
|
325
|
+
* non-JSON / non-message text frame surfaces on `error` and is DROPPED (never
|
|
297
326
|
* throws on adversarial wire input).
|
|
298
|
-
* - **`close()`**
|
|
299
|
-
* socket's native `close` event (a server-initiated close) fires the
|
|
300
|
-
* exactly once total — `close()` first flips the guard, so the native event
|
|
301
|
-
* double-emits
|
|
302
|
-
*
|
|
303
|
-
* `
|
|
304
|
-
*
|
|
327
|
+
* - **`close()`** unsubscribes from the underlying socket, closes it, and fires `close`
|
|
328
|
+
* (idempotent); the socket's native `close` event (a server-initiated close) fires the
|
|
329
|
+
* SAME `close` exactly once total — `close()` first flips the guard, so the native event
|
|
330
|
+
* never double-emits, and the released socket reports its own close to nobody. Closing before
|
|
331
|
+
* the socket opens resolves the pending `start()` rather than leaving it pending, matching the
|
|
332
|
+
* Node face. A `send` issued after `close()` is silently dropped (not queued), so a closed
|
|
333
|
+
* transport delivers nothing until a `start()` opens a new connection.
|
|
334
|
+
* - **Observable.** Owns the `emitter` ({@link MCPClientTransportEventMap}); every
|
|
305
335
|
* emit the emitter isolates a listener throw; `error` is a DOMAIN event (a
|
|
306
336
|
* transport-level fault).
|
|
307
337
|
*
|
|
@@ -316,7 +346,15 @@ var WebSocketClientTransport = class {
|
|
|
316
346
|
#emitter;
|
|
317
347
|
#url;
|
|
318
348
|
#protocols;
|
|
349
|
+
#frame = (event) => this.#receive(event.data);
|
|
350
|
+
#ending = () => this.#onClose();
|
|
351
|
+
#failure = (event) => this.#emitter.emit("error", event);
|
|
352
|
+
#opening = () => this.#onOpen();
|
|
353
|
+
#rejection = () => this.#onHandshakeError();
|
|
319
354
|
#socket = void 0;
|
|
355
|
+
#handshake = void 0;
|
|
356
|
+
#resolve = void 0;
|
|
357
|
+
#reject = void 0;
|
|
320
358
|
#queue = [];
|
|
321
359
|
#closed = false;
|
|
322
360
|
constructor(options) {
|
|
@@ -339,16 +377,11 @@ var WebSocketClientTransport = class {
|
|
|
339
377
|
this.#socket = socket;
|
|
340
378
|
this.#bind(socket);
|
|
341
379
|
await new Promise((resolve, reject) => {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
socket.addEventListener("error",
|
|
347
|
-
if (socket.readyState !== WebSocket.OPEN) {
|
|
348
|
-
this.#socket = void 0;
|
|
349
|
-
reject(/* @__PURE__ */ new Error("WebSocket connection failed"));
|
|
350
|
-
}
|
|
351
|
-
}, { once: true });
|
|
380
|
+
this.#handshake = socket;
|
|
381
|
+
this.#resolve = resolve;
|
|
382
|
+
this.#reject = reject;
|
|
383
|
+
socket.addEventListener("open", this.#opening);
|
|
384
|
+
socket.addEventListener("error", this.#rejection);
|
|
352
385
|
});
|
|
353
386
|
}
|
|
354
387
|
async send(message) {
|
|
@@ -362,18 +395,55 @@ var WebSocketClientTransport = class {
|
|
|
362
395
|
if (this.#closed) return;
|
|
363
396
|
this.#closed = true;
|
|
364
397
|
const socket = this.#socket;
|
|
398
|
+
const resolve = this.#resolve;
|
|
399
|
+
this.#releaseHandshake();
|
|
400
|
+
this.#release();
|
|
365
401
|
this.#socket = void 0;
|
|
366
402
|
if (socket !== void 0) socket.close();
|
|
367
403
|
this.#emitter.emit("close");
|
|
404
|
+
resolve?.();
|
|
368
405
|
}
|
|
369
406
|
#bind(socket) {
|
|
370
|
-
socket.addEventListener("message",
|
|
371
|
-
socket.addEventListener("close",
|
|
372
|
-
socket.addEventListener("error",
|
|
407
|
+
socket.addEventListener("message", this.#frame);
|
|
408
|
+
socket.addEventListener("close", this.#ending);
|
|
409
|
+
socket.addEventListener("error", this.#failure);
|
|
410
|
+
}
|
|
411
|
+
#release() {
|
|
412
|
+
const socket = this.#socket;
|
|
413
|
+
if (socket === void 0) return;
|
|
414
|
+
socket.removeEventListener("message", this.#frame);
|
|
415
|
+
socket.removeEventListener("close", this.#ending);
|
|
416
|
+
socket.removeEventListener("error", this.#failure);
|
|
417
|
+
}
|
|
418
|
+
#releaseHandshake() {
|
|
419
|
+
const socket = this.#handshake;
|
|
420
|
+
if (socket === void 0) return;
|
|
421
|
+
socket.removeEventListener("open", this.#opening);
|
|
422
|
+
socket.removeEventListener("error", this.#rejection);
|
|
423
|
+
this.#handshake = void 0;
|
|
424
|
+
this.#resolve = void 0;
|
|
425
|
+
this.#reject = void 0;
|
|
373
426
|
}
|
|
374
427
|
#flush(socket) {
|
|
375
428
|
for (const text of this.#queue.splice(0)) socket.send(text);
|
|
376
429
|
}
|
|
430
|
+
#onOpen() {
|
|
431
|
+
const socket = this.#handshake;
|
|
432
|
+
const resolve = this.#resolve;
|
|
433
|
+
if (socket === void 0 || resolve === void 0) return;
|
|
434
|
+
this.#releaseHandshake();
|
|
435
|
+
this.#flush(socket);
|
|
436
|
+
resolve();
|
|
437
|
+
}
|
|
438
|
+
#onHandshakeError() {
|
|
439
|
+
const socket = this.#handshake;
|
|
440
|
+
const reject = this.#reject;
|
|
441
|
+
if (socket === void 0 || reject === void 0 || socket.readyState === WebSocket.OPEN) return;
|
|
442
|
+
this.#releaseHandshake();
|
|
443
|
+
this.#release();
|
|
444
|
+
this.#socket = void 0;
|
|
445
|
+
reject(/* @__PURE__ */ new Error("WebSocket connection failed"));
|
|
446
|
+
}
|
|
377
447
|
#receive(data) {
|
|
378
448
|
if (!isString(data)) {
|
|
379
449
|
this.#emitter.emit("error", /* @__PURE__ */ new Error("non-text WebSocket frame"));
|
|
@@ -396,6 +466,7 @@ var WebSocketClientTransport = class {
|
|
|
396
466
|
#onClose() {
|
|
397
467
|
if (this.#closed) return;
|
|
398
468
|
this.#closed = true;
|
|
469
|
+
this.#release();
|
|
399
470
|
this.#socket = void 0;
|
|
400
471
|
this.#emitter.emit("close");
|
|
401
472
|
}
|
|
@@ -403,10 +474,10 @@ var WebSocketClientTransport = class {
|
|
|
403
474
|
//#endregion
|
|
404
475
|
//#region src/browser/factories.ts
|
|
405
476
|
/**
|
|
406
|
-
*
|
|
407
|
-
* {@link import('@
|
|
477
|
+
* Creates the browser-face WebSocket CLIENT transport for an
|
|
478
|
+
* {@link import('@orkestrel/mcp').MCPClientInterface} — a {@link MCPClientTransportInterface}
|
|
408
479
|
* that drives a REMOTE MCP server over the native `WebSocket` global, the browser
|
|
409
|
-
* sibling of the Node face's `createWebSocketClientTransport` (`@
|
|
480
|
+
* sibling of the Node face's `createWebSocketClientTransport` (`@orkestrel/mcp/server`).
|
|
410
481
|
*
|
|
411
482
|
* @remarks
|
|
412
483
|
* Hand it to `createMCPClient({ transport })`: `start()` (run by `client.connect()`)
|
|
@@ -437,18 +508,18 @@ function createWebSocketClientTransport(options) {
|
|
|
437
508
|
return new WebSocketClientTransport(options);
|
|
438
509
|
}
|
|
439
510
|
/**
|
|
440
|
-
*
|
|
441
|
-
* {@link import('@
|
|
511
|
+
* Creates the browser-face HTTP CLIENT transport for an
|
|
512
|
+
* {@link import('@orkestrel/mcp').MCPClientInterface} — a {@link MCPClientTransportInterface}
|
|
442
513
|
* that drives a REMOTE Streamable-HTTP MCP server over the native `fetch`, the
|
|
443
|
-
* browser sibling of the Node face's `createHTTPClientTransport` (`@
|
|
514
|
+
* browser sibling of the Node face's `createHTTPClientTransport` (`@orkestrel/mcp/server`).
|
|
444
515
|
*
|
|
445
516
|
* @remarks
|
|
446
517
|
* Hand it to `createMCPClient({ transport })`: each JSON-RPC message the client
|
|
447
518
|
* sends is `POST`ed to `options.url` with `content-type: application/json` and an
|
|
448
519
|
* `Accept` of both `application/json` and `text/event-stream` (the server answers
|
|
449
520
|
* with EITHER — a plain JSON envelope or a Streamable-HTTP SSE `data:` event,
|
|
450
|
-
* decoded
|
|
451
|
-
* `message` event for the client's id correlation. Add `options.headers` (
|
|
521
|
+
* decoded with `@orkestrel/sse`), and the reply is surfaced on the transport's
|
|
522
|
+
* `message` event for the client's id correlation. Add `options.headers` (for example, an
|
|
452
523
|
* `Authorization` bearer) to reach a guarded server. `start` / `close` hold no
|
|
453
524
|
* connection; against a STATEFUL server it captures the `mcp-session-id` from
|
|
454
525
|
* `initialize` and echoes it on later requests. It also captures the initialize
|
|
@@ -459,7 +530,7 @@ function createWebSocketClientTransport(options) {
|
|
|
459
530
|
*
|
|
460
531
|
* @param options - `url` (the remote endpoint; REQUIRED), optional `headers` merged
|
|
461
532
|
* onto every request, optional `fetch` (default `globalThis.fetch`), and optional
|
|
462
|
-
* `timeout` (ms, applied
|
|
533
|
+
* `timeout` (ms, applied with `AbortSignal.timeout`); see
|
|
463
534
|
* {@link HTTPClientTransportOptions}
|
|
464
535
|
* @returns A working {@link MCPClientTransportInterface} over the native `fetch`
|
|
465
536
|
*
|
|
@@ -479,11 +550,11 @@ function createHTTPClientTransport(options) {
|
|
|
479
550
|
return new HTTPClientTransport(options);
|
|
480
551
|
}
|
|
481
552
|
/**
|
|
482
|
-
*
|
|
483
|
-
* {@link import('@
|
|
553
|
+
* Creates the browser-face `MessagePort` transport — a
|
|
554
|
+
* {@link import('@orkestrel/mcp').MCPTransportInterface} over a native `MessagePort`, the
|
|
484
555
|
* SYMMETRIC carrier that works as either a server or a client transport depending on
|
|
485
|
-
* which binder ({@link import('@
|
|
486
|
-
* {@link import('@
|
|
556
|
+
* which binder ({@link import('@orkestrel/mcp').bindServer} or
|
|
557
|
+
* {@link import('@orkestrel/mcp').bindClient}) it is handed to.
|
|
487
558
|
*
|
|
488
559
|
* @remarks
|
|
489
560
|
* `port.start()` runs at construction (see {@link MessagePortTransport}'s doc for
|
|
@@ -493,28 +564,29 @@ function createHTTPClientTransport(options) {
|
|
|
493
564
|
*
|
|
494
565
|
* @param options - `port` (the `MessagePort` half to drive; REQUIRED); see
|
|
495
566
|
* {@link MessagePortTransportOptions}
|
|
496
|
-
* @returns A working {@link import('@
|
|
567
|
+
* @returns A working {@link import('@orkestrel/mcp').MCPTransportInterface} over the port
|
|
497
568
|
*
|
|
498
569
|
* @example
|
|
499
570
|
* ```ts
|
|
500
|
-
* import { bindServer, createMCPServer } from '@orkestrel/mcp'
|
|
571
|
+
* import { bindServer, createMCPLegacy, createMCPServer } from '@orkestrel/mcp'
|
|
501
572
|
* import { createMessagePortTransport } from '@orkestrel/mcp/browser'
|
|
502
573
|
*
|
|
503
574
|
* const { port1, port2 } = new MessageChannel()
|
|
504
|
-
*
|
|
575
|
+
* const mcp = createMCPServer({ identity: { name: 's', version: '1.0.0' }, tools })
|
|
576
|
+
* bindServer(createMCPLegacy(mcp), createMessagePortTransport({ port: port1 })) // answers `initialize` too; pass `mcp` alone for modern-only
|
|
505
577
|
* ```
|
|
506
578
|
*/
|
|
507
579
|
function createMessagePortTransport(options) {
|
|
508
580
|
return new MessagePortTransport(options);
|
|
509
581
|
}
|
|
510
582
|
/**
|
|
511
|
-
*
|
|
583
|
+
* Adapts a hostable {@link ServeMCPScopeInterface} (`self` in a dedicated Web Worker,
|
|
512
584
|
* or any structurally matching double) into a {@link ScopeTransportInterface} — the
|
|
513
585
|
* implicit, portless message channel `serveMCPScope` binds for the
|
|
514
586
|
* dedicated-worker shape.
|
|
515
587
|
*
|
|
516
588
|
* @remarks
|
|
517
|
-
* `send` writes each outbound string
|
|
589
|
+
* `send` writes each outbound string through `scope.postMessage`. `listen`/`closed`
|
|
518
590
|
* register the SINGLE handler `deliver` / the underlying close path route through —
|
|
519
591
|
* `serveMCPScope`'s own `scope` `message`-event listener calls `deliver(event.data)`
|
|
520
592
|
* for every portless, string-payload event (there is no native registration point on
|
|
@@ -524,7 +596,7 @@ function createMessagePortTransport(options) {
|
|
|
524
596
|
*
|
|
525
597
|
* @param scope - The hostable scope to adapt (structurally, `self` / `globalThis`
|
|
526
598
|
* inside a dedicated Web Worker)
|
|
527
|
-
* @returns A {@link ScopeTransportInterface} `serveMCPScope` binds and drives
|
|
599
|
+
* @returns A {@link ScopeTransportInterface} `serveMCPScope` binds and drives through `deliver`
|
|
528
600
|
*
|
|
529
601
|
* @example
|
|
530
602
|
* ```ts
|
|
@@ -556,13 +628,13 @@ function createScopeTransport(scope) {
|
|
|
556
628
|
//#endregion
|
|
557
629
|
//#region src/browser/helpers.ts
|
|
558
630
|
/**
|
|
559
|
-
*
|
|
631
|
+
* Decodes one SSE event's `data` string into a {@link JSONRPCMessage}, or `undefined`
|
|
560
632
|
* when it is not one — the per-event step {@link readEventStream} folds over.
|
|
561
633
|
*
|
|
562
634
|
* @remarks
|
|
563
635
|
* `JSON.parse`s the `data` (the server serializes the JSON-RPC envelope as the
|
|
564
636
|
* event's `data`) inside a try/catch and narrows the parsed value with
|
|
565
|
-
* `parseJSONRPCMessage`. Total
|
|
637
|
+
* `parseJSONRPCMessage`. Total: malformed JSON or a non-message value yields
|
|
566
638
|
* `undefined`, never throws.
|
|
567
639
|
*
|
|
568
640
|
* @param data - One SSE event's `data` payload
|
|
@@ -576,7 +648,7 @@ function decodeEvent(data) {
|
|
|
576
648
|
}
|
|
577
649
|
}
|
|
578
650
|
/**
|
|
579
|
-
*
|
|
651
|
+
* Decodes a `fetch` Response's Server-Sent-Events body into the JSON-RPC messages it
|
|
580
652
|
* carried — the CLIENT-side inverse of the server's Streamable-HTTP SSE response.
|
|
581
653
|
*
|
|
582
654
|
* @remarks
|
|
@@ -584,8 +656,8 @@ function decodeEvent(data) {
|
|
|
584
656
|
* stream: true })` (handling a multi-byte char split across reads) and
|
|
585
657
|
* `@orkestrel/sse`'s {@link SSEParserInterface} (handling a partial line / in-progress
|
|
586
658
|
* event split across reads), then narrows each dispatched event's `data` to a
|
|
587
|
-
* {@link JSONRPCMessage}
|
|
588
|
-
* event is DROPPED, never thrown — total
|
|
659
|
+
* {@link JSONRPCMessage} through {@link decodeEvent} (so a non-message / non-JSON `data:`
|
|
660
|
+
* event is DROPPED, never thrown — total). A `null` body (no stream) yields no
|
|
589
661
|
* messages; {@link import('./transports/HTTPClientTransport.js').HTTPClientTransport}
|
|
590
662
|
* reads a request/response SSE reply (the server sends one `data:` event then ends),
|
|
591
663
|
* so this drains to completion.
|
|
@@ -615,7 +687,7 @@ async function readEventStream(response) {
|
|
|
615
687
|
return messages;
|
|
616
688
|
}
|
|
617
689
|
/**
|
|
618
|
-
*
|
|
690
|
+
* Builds `serveMCPScope`'s `message`-event listener — the unified
|
|
619
691
|
* dispatcher that routes EVERY inbound event on a hostable scope, portless or
|
|
620
692
|
* port-bearing, to the right binding.
|
|
621
693
|
*
|
|
@@ -624,26 +696,32 @@ async function readEventStream(response) {
|
|
|
624
696
|
* — when the gate returns `false` the event is dropped entirely (no binding, no reply).
|
|
625
697
|
* Accepted events spawn a fresh `MessagePortTransport` over `event.ports[0]`,
|
|
626
698
|
* `bindServer` `server` onto it, and record a teardown (`unbind` then `transport.close()`)
|
|
627
|
-
* into `teardowns
|
|
628
|
-
* same `MessagePort` would create duplicate bindings over one port (→ duplicated
|
|
629
|
-
* so
|
|
699
|
+
* into `teardowns` KEYED BY THAT PORT. A port already present is IGNORED — repeated delivery
|
|
700
|
+
* of the same `MessagePort` would create duplicate bindings over one port (→ duplicated
|
|
701
|
+
* replies), so a repeat is silently dropped.
|
|
702
|
+
*
|
|
703
|
+
* The key is what makes `teardowns` the ONLY place an accepted port is remembered. A separate
|
|
704
|
+
* seen-port set would be a second collection over the same lifetime, and the caller's disposer
|
|
705
|
+
* would have to remember to empty both — so a long-lived scope such as a Service Worker would
|
|
706
|
+
* retain every port it ever accepted, closed and unbound ones included. Membership answers
|
|
707
|
+
* "already bound?" and `clear()` drops the binding and the dedup together.
|
|
630
708
|
*
|
|
631
709
|
* This branch fires on EITHER a Service-Worker-shaped scope (its normal per-client
|
|
632
710
|
* channel) or a dedicated-worker-shaped one that happens to receive a port-bearing event
|
|
633
711
|
* (the unified design's deliberate cross-case, needing no upfront shape flag). An event
|
|
634
712
|
* with NO ports and a STRING `data` is pushed onto `scopeTransport.deliver` (the
|
|
635
713
|
* implicit, already-bound scope channel); any other event (no ports, non-string data)
|
|
636
|
-
* is silently dropped — total
|
|
714
|
+
* is silently dropped — total, never throws.
|
|
637
715
|
*
|
|
638
716
|
* @param server - The `MCPServerInterface` every spawned/implicit binding dispatches over
|
|
639
717
|
* @param scopeTransport - The implicit scope channel (already `bindServer`-bound) portless events deliver onto
|
|
640
|
-
* @param teardowns - The shared teardown
|
|
718
|
+
* @param teardowns - The shared teardown map `serveMCPScope`'s dispose drains and clears, keyed by the accepted port; each port-bearing event adds one entry
|
|
641
719
|
* @param options - The `ServeMCPOptions` (for `options.accept`)
|
|
642
720
|
* @returns The `message`-event listener to register (and later remove) on the scope
|
|
643
721
|
*
|
|
644
722
|
* @example
|
|
645
723
|
* ```ts
|
|
646
|
-
* const teardowns = new
|
|
724
|
+
* const teardowns = new Map<MessagePort, () => void>()
|
|
647
725
|
* const scopeTransport = createScopeTransport(scope)
|
|
648
726
|
* bindServer(server, scopeTransport)
|
|
649
727
|
* const onMessage = createScopeMessageListener(server, scopeTransport, teardowns, options)
|
|
@@ -651,18 +729,16 @@ async function readEventStream(response) {
|
|
|
651
729
|
* ```
|
|
652
730
|
*/
|
|
653
731
|
function createScopeMessageListener(server, scopeTransport, teardowns, options) {
|
|
654
|
-
const seen = /* @__PURE__ */ new Set();
|
|
655
732
|
return (event) => {
|
|
656
733
|
const ports = event.ports;
|
|
657
734
|
if (ports.length > 0) {
|
|
658
735
|
if (options.accept !== void 0 && !options.accept(event)) return;
|
|
659
736
|
const port = ports[0];
|
|
660
737
|
if (port === void 0) return;
|
|
661
|
-
if (
|
|
662
|
-
seen.add(port);
|
|
738
|
+
if (teardowns.has(port)) return;
|
|
663
739
|
const transport = new MessagePortTransport({ port });
|
|
664
740
|
const unbind = bindServer(server, transport);
|
|
665
|
-
teardowns.
|
|
741
|
+
teardowns.set(port, () => {
|
|
666
742
|
unbind();
|
|
667
743
|
transport.close();
|
|
668
744
|
});
|
|
@@ -672,13 +748,16 @@ function createScopeMessageListener(server, scopeTransport, teardowns, options)
|
|
|
672
748
|
};
|
|
673
749
|
}
|
|
674
750
|
/**
|
|
675
|
-
*
|
|
751
|
+
* Boots an `MCPServer` inside a hostable worker scope and wires its message events to it.
|
|
676
752
|
*
|
|
677
753
|
* @remarks
|
|
678
754
|
* Port-bearing events are gated by `options.accept`, deduplicated by port, and receive
|
|
679
755
|
* their own `MessagePortTransport` binding. Portless string events use the scope's
|
|
680
756
|
* implicit channel. The returned disposer removes the listener, unbinds the implicit
|
|
681
|
-
* channel,
|
|
757
|
+
* channel, closes every accepted port binding, and drops the ports themselves — the
|
|
758
|
+
* bindings are held in one map keyed by port, so nothing survives the clear. The served
|
|
759
|
+
* endpoint is modern-only: it answers a legacy `initialize` with `-32601`. A dual-era
|
|
760
|
+
* worker composes `bindServer(createMCPLegacy(mcp), …)` instead of this function.
|
|
682
761
|
*
|
|
683
762
|
* @param scope - The hostable worker scope to wire
|
|
684
763
|
* @param options - The tools, optional identity, and optional port-event gate
|
|
@@ -694,7 +773,7 @@ function serveMCPScope(scope, options) {
|
|
|
694
773
|
});
|
|
695
774
|
const scopeTransport = createScopeTransport(scope);
|
|
696
775
|
const unbindScope = bindServer(server, scopeTransport);
|
|
697
|
-
const teardowns = /* @__PURE__ */ new
|
|
776
|
+
const teardowns = /* @__PURE__ */ new Map();
|
|
698
777
|
const onMessage = createScopeMessageListener(server, scopeTransport, teardowns, options);
|
|
699
778
|
scope.addEventListener("message", onMessage);
|
|
700
779
|
let disposed = false;
|
|
@@ -703,12 +782,16 @@ function serveMCPScope(scope, options) {
|
|
|
703
782
|
disposed = true;
|
|
704
783
|
scope.removeEventListener("message", onMessage);
|
|
705
784
|
unbindScope();
|
|
706
|
-
for (const teardown of teardowns) teardown();
|
|
785
|
+
for (const teardown of teardowns.values()) teardown();
|
|
707
786
|
teardowns.clear();
|
|
708
787
|
};
|
|
709
788
|
}
|
|
710
789
|
/**
|
|
711
|
-
*
|
|
790
|
+
* Boots an `MCPServer` inside the current hostable worker scope.
|
|
791
|
+
*
|
|
792
|
+
* @remarks
|
|
793
|
+
* The served endpoint is modern-only: it answers a legacy `initialize` with `-32601`. A
|
|
794
|
+
* dual-era worker composes `bindServer(createMCPLegacy(mcp), …)` instead of this function.
|
|
712
795
|
*
|
|
713
796
|
* @param options - The tools, optional identity, and optional port-event gate
|
|
714
797
|
* @returns The disposer returned by {@link serveMCPScope}
|