@orkestrel/mcp 0.0.13 → 0.0.15
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 +2 -3
- package/dist/src/browser/index.js +214 -214
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +18 -4
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +22 -6
- package/dist/src/server/index.d.ts +22 -6
- package/dist/src/server/index.js +18 -4
- package/dist/src/server/index.js.map +1 -1
- package/package.json +20 -16
package/README.md
CHANGED
|
@@ -81,9 +81,8 @@ package's source and runs
|
|
|
81
81
|
`@modelcontextprotocol/conformance@0.2.0-alpha.10` against MCP revision
|
|
82
82
|
`2026-07-28`. The recorded result is **23 passed / 0 failed**. That is a
|
|
83
83
|
genuine foreign MCP client driving this server end to end, and it is
|
|
84
|
-
evidence about the wire. It
|
|
85
|
-
|
|
86
|
-
`npm test`.
|
|
84
|
+
evidence about the wire. It resolves the runner from `node_modules` and
|
|
85
|
+
drives a loopback socket, so the run is offline and `npm test` gates it.
|
|
87
86
|
|
|
88
87
|
**IDE integration is not claimed.** No IDE, editor, or agent host has driven
|
|
89
88
|
this server. A claim about an external client stays unproven here until one
|
|
@@ -42,220 +42,6 @@ var DEFAULT_MCP_SERVER_VERSION = "1.0.0";
|
|
|
42
42
|
*/
|
|
43
43
|
var MCP_WEBSOCKET_SUBPROTOCOL = "mcp";
|
|
44
44
|
//#endregion
|
|
45
|
-
//#region src/browser/transports/MessagePortTransport.ts
|
|
46
|
-
/**
|
|
47
|
-
* The browser-face `MessagePort` transport for the Model Context Protocol — a
|
|
48
|
-
* {@link MCPTransportInterface} over a native `MessagePort`, the genuinely new
|
|
49
|
-
* capability this face adds: MCP over `postMessage`.
|
|
50
|
-
*
|
|
51
|
-
* @remarks
|
|
52
|
-
* - **Symmetric.** Unlike {@link import('./WebSocketClientTransport.js').WebSocketClientTransport}
|
|
53
|
-
* / {@link import('./HTTPClientTransport.js').HTTPClientTransport} (CLIENT-only
|
|
54
|
-
* carriers of `@src/core`'s `MCPClientTransportInterface`), a `MessagePort` is a
|
|
55
|
-
* plain duplex channel — the SAME class implements `@src/core`'s
|
|
56
|
-
* `MCPTransportInterface` and is handed to EITHER `bindServer` or
|
|
57
|
-
* `bindClient`/`createDuplexClientTransport`; which role it plays comes entirely
|
|
58
|
-
* from the binder it is given to, not from anything this class decides.
|
|
59
|
-
* - **`start()` at construction — bind synchronously.** `MessagePort.start()` is only
|
|
60
|
-
* REQUIRED when listening via `addEventListener` (as opposed to the `onmessage`
|
|
61
|
-
* setter, which implies it) — this transport uses `addEventListener`, and
|
|
62
|
-
* `MCPTransportInterface` has no separate open/connect step for the caller to hook
|
|
63
|
-
* a start into, so the constructor calls `port.start()` immediately: the port
|
|
64
|
-
* begins dispatching QUEUED messages the moment the transport exists. This is safe
|
|
65
|
-
* inside `serveMCP`'s flow (the transport is synchronously handed to `bindServer`
|
|
66
|
-
* before control returns to the event loop), but is a **footgun for direct use**:
|
|
67
|
-
* if you construct `new MessagePortTransport({ port })` and then `await` anything
|
|
68
|
-
* before calling `listen`, messages that arrived in the gap are DROPPED. **Bind
|
|
69
|
-
* synchronously after construction** — do not interleave an `await` between
|
|
70
|
-
* `new MessagePortTransport(…)` and `bindServer` / `listen`.
|
|
71
|
-
* - **String payloads only.** `send` posts the message string as-is (`postMessage`
|
|
72
|
-
* structured-clones it — a string clones to an identical string, so the wire stays
|
|
73
|
-
* plain JSON-RPC text like every other transport in this package). Inbound: a
|
|
74
|
-
* non-string `event.data` (a host or a misbehaving peer posting a structured
|
|
75
|
-
* object) is IGNORED — dropped silently, never forwarded, never thrown (§14) —
|
|
76
|
-
* because `MCPTransportInterface` carries no `error` channel for this port to
|
|
77
|
-
* surface a non-string frame on (unlike `MCPClientTransportInterface`'s `emitter`);
|
|
78
|
-
* silently ignoring is the total, contract-shaped choice.
|
|
79
|
-
* - **`messageerror` is IGNORED, not routed to `closed`.** A `messageerror` event
|
|
80
|
-
* (the structured-clone deserialization of an inbound message threw) reports one
|
|
81
|
-
* BAD FRAME, not a dead channel — the port itself keeps working and later, well-
|
|
82
|
-
* formed messages still arrive. Routing it to `closed` would tear down the
|
|
83
|
-
* `bindServer`/`bindClient` wiring (and, transitively, every session it carries)
|
|
84
|
-
* over a single malformed frame, which is far more destructive than dropping that
|
|
85
|
-
* one frame — so this transport registers a `messageerror` listener that does
|
|
86
|
-
* nothing, deliberately.
|
|
87
|
-
* - **`close()`** is idempotent: it closes the underlying `port` (`MessagePort.close()`
|
|
88
|
-
* disconnects it — further `postMessage` calls on EITHER end are silently
|
|
89
|
-
* undelivered, per the platform contract) and fires the registered `closed`
|
|
90
|
-
* handler exactly once, whether the caller closes it once or twice. There is no
|
|
91
|
-
* native "peer closed" signal for a `MessagePort` (unlike a WebSocket's `close`
|
|
92
|
-
* event) — `closed` fires ONLY from this transport's own `close()`.
|
|
93
|
-
* - **Single-handler-replace (the port contract, `@src/core`'s `MCPTransportInterface`
|
|
94
|
-
* doc).** `listen`/`closed` each hold the ONE currently registered handler; a
|
|
95
|
-
* second call REPLACES the first rather than adding a second subscriber.
|
|
96
|
-
*
|
|
97
|
-
* @example
|
|
98
|
-
* ```ts
|
|
99
|
-
* const { port1, port2 } = new MessageChannel()
|
|
100
|
-
* const serverTransport = new MessagePortTransport({ port: port1 })
|
|
101
|
-
* bindServer(server, serverTransport) // port1 side dispatches inbound requests
|
|
102
|
-
*
|
|
103
|
-
* const clientTransport = new MessagePortTransport({ port: port2 })
|
|
104
|
-
* const client = createMCPClient({ transport: createDuplexClientTransport(clientTransport) })
|
|
105
|
-
* bindClient(client, clientTransport) // port2 side is the client's carrier
|
|
106
|
-
* ```
|
|
107
|
-
*/
|
|
108
|
-
var MessagePortTransport = class {
|
|
109
|
-
#port;
|
|
110
|
-
#onMessage = void 0;
|
|
111
|
-
#onClosed = void 0;
|
|
112
|
-
#closed = false;
|
|
113
|
-
constructor(options) {
|
|
114
|
-
this.#port = options.port;
|
|
115
|
-
this.#port.addEventListener("message", (event) => this.#receive(event.data));
|
|
116
|
-
this.#port.addEventListener("messageerror", () => {});
|
|
117
|
-
this.#port.start();
|
|
118
|
-
}
|
|
119
|
-
send(message) {
|
|
120
|
-
if (this.#closed) return;
|
|
121
|
-
this.#port.postMessage(message);
|
|
122
|
-
}
|
|
123
|
-
listen(handler) {
|
|
124
|
-
this.#onMessage = handler;
|
|
125
|
-
}
|
|
126
|
-
closed(handler) {
|
|
127
|
-
this.#onClosed = handler;
|
|
128
|
-
}
|
|
129
|
-
close() {
|
|
130
|
-
if (this.#closed) return;
|
|
131
|
-
this.#closed = true;
|
|
132
|
-
this.#port.close();
|
|
133
|
-
this.#onClosed?.();
|
|
134
|
-
}
|
|
135
|
-
#receive(data) {
|
|
136
|
-
if (!isString(data)) return;
|
|
137
|
-
this.#onMessage?.(data);
|
|
138
|
-
}
|
|
139
|
-
};
|
|
140
|
-
//#endregion
|
|
141
|
-
//#region src/browser/helpers.ts
|
|
142
|
-
/**
|
|
143
|
-
* Decode one SSE event's `data` string into a {@link JSONRPCMessage}, or `undefined`
|
|
144
|
-
* when it is not one — the per-event step {@link readEventStream} folds over.
|
|
145
|
-
*
|
|
146
|
-
* @remarks
|
|
147
|
-
* `JSON.parse`s the `data` (the server serializes the JSON-RPC envelope as the
|
|
148
|
-
* event's `data`) inside a try/catch and narrows the parsed value with
|
|
149
|
-
* `parseJSONRPCMessage`. Total (§14): malformed JSON or a non-message value yields
|
|
150
|
-
* `undefined`, never throws.
|
|
151
|
-
*
|
|
152
|
-
* @param data - One SSE event's `data` payload
|
|
153
|
-
* @returns The decoded {@link JSONRPCMessage}, or `undefined`
|
|
154
|
-
*/
|
|
155
|
-
function decodeEvent(data) {
|
|
156
|
-
try {
|
|
157
|
-
return parseJSONRPCMessage(JSON.parse(data));
|
|
158
|
-
} catch {
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
/**
|
|
163
|
-
* Decode a `fetch` Response's Server-Sent-Events body into the JSON-RPC messages it
|
|
164
|
-
* carried — the CLIENT-side inverse of the server's Streamable-HTTP SSE response.
|
|
165
|
-
*
|
|
166
|
-
* @remarks
|
|
167
|
-
* Reads the whole `response.body` stream chunk-by-chunk through a `TextDecoder({
|
|
168
|
-
* stream: true })` (handling a multi-byte char split across reads) and
|
|
169
|
-
* `@orkestrel/sse`'s {@link SSEParserInterface} (handling a partial line / in-progress
|
|
170
|
-
* event split across reads), then narrows each dispatched event's `data` to a
|
|
171
|
-
* {@link JSONRPCMessage} via {@link decodeEvent} (so a non-message / non-JSON `data:`
|
|
172
|
-
* event is DROPPED, never thrown — total, §14). A `null` body (no stream) yields no
|
|
173
|
-
* messages; {@link import('./transports/HTTPClientTransport.js').HTTPClientTransport}
|
|
174
|
-
* reads a request/response SSE reply (the server sends one `data:` event then ends),
|
|
175
|
-
* so this drains to completion.
|
|
176
|
-
*
|
|
177
|
-
* @param response - The SSE `fetch` Response to decode (its `body` is read to completion)
|
|
178
|
-
* @returns Every {@link JSONRPCMessage} the stream carried, in order
|
|
179
|
-
*/
|
|
180
|
-
async function readEventStream(response) {
|
|
181
|
-
const body = response.body;
|
|
182
|
-
if (body === null) return [];
|
|
183
|
-
const reader = body.getReader();
|
|
184
|
-
const decoder = new TextDecoder();
|
|
185
|
-
const parser = createSSEParser();
|
|
186
|
-
const messages = [];
|
|
187
|
-
try {
|
|
188
|
-
for (;;) {
|
|
189
|
-
const { done, value } = await reader.read();
|
|
190
|
-
if (done) break;
|
|
191
|
-
for (const event of parser.parse(decoder.decode(value, { stream: true }))) {
|
|
192
|
-
const message = decodeEvent(event.data);
|
|
193
|
-
if (message !== void 0) messages.push(message);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
} finally {
|
|
197
|
-
reader.releaseLock();
|
|
198
|
-
}
|
|
199
|
-
return messages;
|
|
200
|
-
}
|
|
201
|
-
/**
|
|
202
|
-
* Build `serveMCPScope`'s `message`-event listener — the unified
|
|
203
|
-
* dispatcher that routes EVERY inbound event on a hostable scope, portless or
|
|
204
|
-
* port-bearing, to the right binding.
|
|
205
|
-
*
|
|
206
|
-
* @remarks
|
|
207
|
-
* Port-bearing events (`event.ports.length > 0`) are gated by `options.accept` FIRST
|
|
208
|
-
* — when the gate returns `false` the event is dropped entirely (no binding, no reply).
|
|
209
|
-
* Accepted events spawn a fresh `MessagePortTransport` over `event.ports[0]`,
|
|
210
|
-
* `bindServer` `server` onto it, and record a teardown (`unbind` then `transport.close()`)
|
|
211
|
-
* into `teardowns`. A port that was already seen is IGNORED — repeated delivery of the
|
|
212
|
-
* same `MessagePort` would create duplicate bindings over one port (→ duplicated replies),
|
|
213
|
-
* so the listener tracks seen ports and silently drops repeats.
|
|
214
|
-
*
|
|
215
|
-
* This branch fires on EITHER a Service-Worker-shaped scope (its normal per-client
|
|
216
|
-
* channel) or a dedicated-worker-shaped one that happens to receive a port-bearing event
|
|
217
|
-
* (the unified design's deliberate cross-case, needing no upfront shape flag). An event
|
|
218
|
-
* with NO ports and a STRING `data` is pushed onto `scopeTransport.deliver` (the
|
|
219
|
-
* implicit, already-bound scope channel); any other event (no ports, non-string data)
|
|
220
|
-
* is silently dropped — total (§14), never throws.
|
|
221
|
-
*
|
|
222
|
-
* @param server - The `MCPServerInterface` every spawned/implicit binding dispatches over
|
|
223
|
-
* @param scopeTransport - The implicit scope channel (already `bindServer`-bound) portless events deliver onto
|
|
224
|
-
* @param teardowns - The shared teardown set `serveMCPScope`'s dispose drains; each port-bearing event adds one entry
|
|
225
|
-
* @param options - The `ServeMCPOptions` (for `options.accept`)
|
|
226
|
-
* @returns The `message`-event listener to register (and later remove) on the scope
|
|
227
|
-
*
|
|
228
|
-
* @example
|
|
229
|
-
* ```ts
|
|
230
|
-
* const teardowns = new Set<() => void>()
|
|
231
|
-
* const scopeTransport = createScopeTransport(scope)
|
|
232
|
-
* bindServer(server, scopeTransport)
|
|
233
|
-
* const onMessage = createScopeMessageListener(server, scopeTransport, teardowns, options)
|
|
234
|
-
* scope.addEventListener('message', onMessage)
|
|
235
|
-
* ```
|
|
236
|
-
*/
|
|
237
|
-
function createScopeMessageListener(server, scopeTransport, teardowns, options) {
|
|
238
|
-
const seen = /* @__PURE__ */ new Set();
|
|
239
|
-
return (event) => {
|
|
240
|
-
const ports = event.ports;
|
|
241
|
-
if (ports.length > 0) {
|
|
242
|
-
if (options.accept !== void 0 && !options.accept(event)) return;
|
|
243
|
-
const port = ports[0];
|
|
244
|
-
if (port === void 0) return;
|
|
245
|
-
if (seen.has(port)) return;
|
|
246
|
-
seen.add(port);
|
|
247
|
-
const transport = new MessagePortTransport({ port });
|
|
248
|
-
const unbind = bindServer(server, transport);
|
|
249
|
-
teardowns.add(() => {
|
|
250
|
-
unbind();
|
|
251
|
-
transport.close();
|
|
252
|
-
});
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
if (isString(event.data)) scopeTransport.deliver(event.data);
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
//#endregion
|
|
259
45
|
//#region src/browser/transports/HTTPClientTransport.ts
|
|
260
46
|
/**
|
|
261
47
|
* The browser-face HTTP CLIENT transport for the Model Context Protocol — a
|
|
@@ -391,6 +177,102 @@ var HTTPClientTransport = class {
|
|
|
391
177
|
}
|
|
392
178
|
};
|
|
393
179
|
//#endregion
|
|
180
|
+
//#region src/browser/transports/MessagePortTransport.ts
|
|
181
|
+
/**
|
|
182
|
+
* The browser-face `MessagePort` transport for the Model Context Protocol — a
|
|
183
|
+
* {@link MCPTransportInterface} over a native `MessagePort`, the genuinely new
|
|
184
|
+
* capability this face adds: MCP over `postMessage`.
|
|
185
|
+
*
|
|
186
|
+
* @remarks
|
|
187
|
+
* - **Symmetric.** Unlike {@link import('./WebSocketClientTransport.js').WebSocketClientTransport}
|
|
188
|
+
* / {@link import('./HTTPClientTransport.js').HTTPClientTransport} (CLIENT-only
|
|
189
|
+
* carriers of `@src/core`'s `MCPClientTransportInterface`), a `MessagePort` is a
|
|
190
|
+
* plain duplex channel — the SAME class implements `@src/core`'s
|
|
191
|
+
* `MCPTransportInterface` and is handed to EITHER `bindServer` or
|
|
192
|
+
* `bindClient`/`createDuplexClientTransport`; which role it plays comes entirely
|
|
193
|
+
* from the binder it is given to, not from anything this class decides.
|
|
194
|
+
* - **`start()` at construction — bind synchronously.** `MessagePort.start()` is only
|
|
195
|
+
* REQUIRED when listening via `addEventListener` (as opposed to the `onmessage`
|
|
196
|
+
* setter, which implies it) — this transport uses `addEventListener`, and
|
|
197
|
+
* `MCPTransportInterface` has no separate open/connect step for the caller to hook
|
|
198
|
+
* a start into, so the constructor calls `port.start()` immediately: the port
|
|
199
|
+
* begins dispatching QUEUED messages the moment the transport exists. This is safe
|
|
200
|
+
* inside `serveMCP`'s flow (the transport is synchronously handed to `bindServer`
|
|
201
|
+
* before control returns to the event loop), but is a **footgun for direct use**:
|
|
202
|
+
* if you construct `new MessagePortTransport({ port })` and then `await` anything
|
|
203
|
+
* before calling `listen`, messages that arrived in the gap are DROPPED. **Bind
|
|
204
|
+
* synchronously after construction** — do not interleave an `await` between
|
|
205
|
+
* `new MessagePortTransport(…)` and `bindServer` / `listen`.
|
|
206
|
+
* - **String payloads only.** `send` posts the message string as-is (`postMessage`
|
|
207
|
+
* structured-clones it — a string clones to an identical string, so the wire stays
|
|
208
|
+
* plain JSON-RPC text like every other transport in this package). Inbound: a
|
|
209
|
+
* non-string `event.data` (a host or a misbehaving peer posting a structured
|
|
210
|
+
* object) is IGNORED — dropped silently, never forwarded, never thrown (§14) —
|
|
211
|
+
* because `MCPTransportInterface` carries no `error` channel for this port to
|
|
212
|
+
* surface a non-string frame on (unlike `MCPClientTransportInterface`'s `emitter`);
|
|
213
|
+
* silently ignoring is the total, contract-shaped choice.
|
|
214
|
+
* - **`messageerror` is IGNORED, not routed to `closed`.** A `messageerror` event
|
|
215
|
+
* (the structured-clone deserialization of an inbound message threw) reports one
|
|
216
|
+
* BAD FRAME, not a dead channel — the port itself keeps working and later, well-
|
|
217
|
+
* formed messages still arrive. Routing it to `closed` would tear down the
|
|
218
|
+
* `bindServer`/`bindClient` wiring (and, transitively, every session it carries)
|
|
219
|
+
* over a single malformed frame, which is far more destructive than dropping that
|
|
220
|
+
* one frame — so this transport registers a `messageerror` listener that does
|
|
221
|
+
* nothing, deliberately.
|
|
222
|
+
* - **`close()`** is idempotent: it closes the underlying `port` (`MessagePort.close()`
|
|
223
|
+
* disconnects it — further `postMessage` calls on EITHER end are silently
|
|
224
|
+
* undelivered, per the platform contract) and fires the registered `closed`
|
|
225
|
+
* handler exactly once, whether the caller closes it once or twice. There is no
|
|
226
|
+
* native "peer closed" signal for a `MessagePort` (unlike a WebSocket's `close`
|
|
227
|
+
* event) — `closed` fires ONLY from this transport's own `close()`.
|
|
228
|
+
* - **Single-handler-replace (the port contract, `@src/core`'s `MCPTransportInterface`
|
|
229
|
+
* doc).** `listen`/`closed` each hold the ONE currently registered handler; a
|
|
230
|
+
* second call REPLACES the first rather than adding a second subscriber.
|
|
231
|
+
*
|
|
232
|
+
* @example
|
|
233
|
+
* ```ts
|
|
234
|
+
* const { port1, port2 } = new MessageChannel()
|
|
235
|
+
* const serverTransport = new MessagePortTransport({ port: port1 })
|
|
236
|
+
* bindServer(server, serverTransport) // port1 side dispatches inbound requests
|
|
237
|
+
*
|
|
238
|
+
* const clientTransport = new MessagePortTransport({ port: port2 })
|
|
239
|
+
* const client = createMCPClient({ transport: createDuplexClientTransport(clientTransport) })
|
|
240
|
+
* bindClient(client, clientTransport) // port2 side is the client's carrier
|
|
241
|
+
* ```
|
|
242
|
+
*/
|
|
243
|
+
var MessagePortTransport = class {
|
|
244
|
+
#port;
|
|
245
|
+
#onMessage = void 0;
|
|
246
|
+
#onClosed = void 0;
|
|
247
|
+
#closed = false;
|
|
248
|
+
constructor(options) {
|
|
249
|
+
this.#port = options.port;
|
|
250
|
+
this.#port.addEventListener("message", (event) => this.#receive(event.data));
|
|
251
|
+
this.#port.addEventListener("messageerror", () => {});
|
|
252
|
+
this.#port.start();
|
|
253
|
+
}
|
|
254
|
+
send(message) {
|
|
255
|
+
if (this.#closed) return;
|
|
256
|
+
this.#port.postMessage(message);
|
|
257
|
+
}
|
|
258
|
+
listen(handler) {
|
|
259
|
+
this.#onMessage = handler;
|
|
260
|
+
}
|
|
261
|
+
closed(handler) {
|
|
262
|
+
this.#onClosed = handler;
|
|
263
|
+
}
|
|
264
|
+
close() {
|
|
265
|
+
if (this.#closed) return;
|
|
266
|
+
this.#closed = true;
|
|
267
|
+
this.#port.close();
|
|
268
|
+
this.#onClosed?.();
|
|
269
|
+
}
|
|
270
|
+
#receive(data) {
|
|
271
|
+
if (!isString(data)) return;
|
|
272
|
+
this.#onMessage?.(data);
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
//#endregion
|
|
394
276
|
//#region src/browser/transports/WebSocketClientTransport.ts
|
|
395
277
|
/**
|
|
396
278
|
* The browser-face WebSocket CLIENT transport for the Model Context Protocol — a
|
|
@@ -671,6 +553,124 @@ function createScopeTransport(scope) {
|
|
|
671
553
|
}
|
|
672
554
|
};
|
|
673
555
|
}
|
|
556
|
+
//#endregion
|
|
557
|
+
//#region src/browser/helpers.ts
|
|
558
|
+
/**
|
|
559
|
+
* Decode one SSE event's `data` string into a {@link JSONRPCMessage}, or `undefined`
|
|
560
|
+
* when it is not one — the per-event step {@link readEventStream} folds over.
|
|
561
|
+
*
|
|
562
|
+
* @remarks
|
|
563
|
+
* `JSON.parse`s the `data` (the server serializes the JSON-RPC envelope as the
|
|
564
|
+
* event's `data`) inside a try/catch and narrows the parsed value with
|
|
565
|
+
* `parseJSONRPCMessage`. Total (§14): malformed JSON or a non-message value yields
|
|
566
|
+
* `undefined`, never throws.
|
|
567
|
+
*
|
|
568
|
+
* @param data - One SSE event's `data` payload
|
|
569
|
+
* @returns The decoded {@link JSONRPCMessage}, or `undefined`
|
|
570
|
+
*/
|
|
571
|
+
function decodeEvent(data) {
|
|
572
|
+
try {
|
|
573
|
+
return parseJSONRPCMessage(JSON.parse(data));
|
|
574
|
+
} catch {
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Decode a `fetch` Response's Server-Sent-Events body into the JSON-RPC messages it
|
|
580
|
+
* carried — the CLIENT-side inverse of the server's Streamable-HTTP SSE response.
|
|
581
|
+
*
|
|
582
|
+
* @remarks
|
|
583
|
+
* Reads the whole `response.body` stream chunk-by-chunk through a `TextDecoder({
|
|
584
|
+
* stream: true })` (handling a multi-byte char split across reads) and
|
|
585
|
+
* `@orkestrel/sse`'s {@link SSEParserInterface} (handling a partial line / in-progress
|
|
586
|
+
* event split across reads), then narrows each dispatched event's `data` to a
|
|
587
|
+
* {@link JSONRPCMessage} via {@link decodeEvent} (so a non-message / non-JSON `data:`
|
|
588
|
+
* event is DROPPED, never thrown — total, §14). A `null` body (no stream) yields no
|
|
589
|
+
* messages; {@link import('./transports/HTTPClientTransport.js').HTTPClientTransport}
|
|
590
|
+
* reads a request/response SSE reply (the server sends one `data:` event then ends),
|
|
591
|
+
* so this drains to completion.
|
|
592
|
+
*
|
|
593
|
+
* @param response - The SSE `fetch` Response to decode (its `body` is read to completion)
|
|
594
|
+
* @returns Every {@link JSONRPCMessage} the stream carried, in order
|
|
595
|
+
*/
|
|
596
|
+
async function readEventStream(response) {
|
|
597
|
+
const body = response.body;
|
|
598
|
+
if (body === null) return [];
|
|
599
|
+
const reader = body.getReader();
|
|
600
|
+
const decoder = new TextDecoder();
|
|
601
|
+
const parser = createSSEParser();
|
|
602
|
+
const messages = [];
|
|
603
|
+
try {
|
|
604
|
+
for (;;) {
|
|
605
|
+
const { done, value } = await reader.read();
|
|
606
|
+
if (done) break;
|
|
607
|
+
for (const event of parser.parse(decoder.decode(value, { stream: true }))) {
|
|
608
|
+
const message = decodeEvent(event.data);
|
|
609
|
+
if (message !== void 0) messages.push(message);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
} finally {
|
|
613
|
+
reader.releaseLock();
|
|
614
|
+
}
|
|
615
|
+
return messages;
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Build `serveMCPScope`'s `message`-event listener — the unified
|
|
619
|
+
* dispatcher that routes EVERY inbound event on a hostable scope, portless or
|
|
620
|
+
* port-bearing, to the right binding.
|
|
621
|
+
*
|
|
622
|
+
* @remarks
|
|
623
|
+
* Port-bearing events (`event.ports.length > 0`) are gated by `options.accept` FIRST
|
|
624
|
+
* — when the gate returns `false` the event is dropped entirely (no binding, no reply).
|
|
625
|
+
* Accepted events spawn a fresh `MessagePortTransport` over `event.ports[0]`,
|
|
626
|
+
* `bindServer` `server` onto it, and record a teardown (`unbind` then `transport.close()`)
|
|
627
|
+
* into `teardowns`. A port that was already seen is IGNORED — repeated delivery of the
|
|
628
|
+
* same `MessagePort` would create duplicate bindings over one port (→ duplicated replies),
|
|
629
|
+
* so the listener tracks seen ports and silently drops repeats.
|
|
630
|
+
*
|
|
631
|
+
* This branch fires on EITHER a Service-Worker-shaped scope (its normal per-client
|
|
632
|
+
* channel) or a dedicated-worker-shaped one that happens to receive a port-bearing event
|
|
633
|
+
* (the unified design's deliberate cross-case, needing no upfront shape flag). An event
|
|
634
|
+
* with NO ports and a STRING `data` is pushed onto `scopeTransport.deliver` (the
|
|
635
|
+
* implicit, already-bound scope channel); any other event (no ports, non-string data)
|
|
636
|
+
* is silently dropped — total (§14), never throws.
|
|
637
|
+
*
|
|
638
|
+
* @param server - The `MCPServerInterface` every spawned/implicit binding dispatches over
|
|
639
|
+
* @param scopeTransport - The implicit scope channel (already `bindServer`-bound) portless events deliver onto
|
|
640
|
+
* @param teardowns - The shared teardown set `serveMCPScope`'s dispose drains; each port-bearing event adds one entry
|
|
641
|
+
* @param options - The `ServeMCPOptions` (for `options.accept`)
|
|
642
|
+
* @returns The `message`-event listener to register (and later remove) on the scope
|
|
643
|
+
*
|
|
644
|
+
* @example
|
|
645
|
+
* ```ts
|
|
646
|
+
* const teardowns = new Set<() => void>()
|
|
647
|
+
* const scopeTransport = createScopeTransport(scope)
|
|
648
|
+
* bindServer(server, scopeTransport)
|
|
649
|
+
* const onMessage = createScopeMessageListener(server, scopeTransport, teardowns, options)
|
|
650
|
+
* scope.addEventListener('message', onMessage)
|
|
651
|
+
* ```
|
|
652
|
+
*/
|
|
653
|
+
function createScopeMessageListener(server, scopeTransport, teardowns, options) {
|
|
654
|
+
const seen = /* @__PURE__ */ new Set();
|
|
655
|
+
return (event) => {
|
|
656
|
+
const ports = event.ports;
|
|
657
|
+
if (ports.length > 0) {
|
|
658
|
+
if (options.accept !== void 0 && !options.accept(event)) return;
|
|
659
|
+
const port = ports[0];
|
|
660
|
+
if (port === void 0) return;
|
|
661
|
+
if (seen.has(port)) return;
|
|
662
|
+
seen.add(port);
|
|
663
|
+
const transport = new MessagePortTransport({ port });
|
|
664
|
+
const unbind = bindServer(server, transport);
|
|
665
|
+
teardowns.add(() => {
|
|
666
|
+
unbind();
|
|
667
|
+
transport.close();
|
|
668
|
+
});
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
if (isString(event.data)) scopeTransport.deliver(event.data);
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
674
|
/**
|
|
675
675
|
* Boot an `MCPServer` inside a hostable worker scope and wire its message events to it.
|
|
676
676
|
*
|