@orkestrel/mcp 0.0.5 → 0.0.7
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/dist/src/browser/index.d.ts +41 -108
- package/dist/src/browser/index.js +55 -120
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/core/index.cjs +118 -53
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +77 -19
- package/dist/src/core/index.d.ts +77 -19
- package/dist/src/core/index.js +117 -54
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +147 -115
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +70 -29
- package/dist/src/server/index.d.ts +70 -29
- package/dist/src/server/index.js +149 -118
- package/dist/src/server/index.js.map +1 -1
- package/package.json +35 -34
|
@@ -2,13 +2,13 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
2
2
|
let _orkestrel_sse = require("@orkestrel/sse");
|
|
3
3
|
let _src_core = require("../core/index.cjs");
|
|
4
4
|
let _orkestrel_contract = require("@orkestrel/contract");
|
|
5
|
+
let _orkestrel_server = require("@orkestrel/server");
|
|
5
6
|
let _orkestrel_emitter = require("@orkestrel/emitter");
|
|
6
7
|
let node_crypto = require("node:crypto");
|
|
7
8
|
let node_http = require("node:http");
|
|
8
9
|
let node_https = require("node:https");
|
|
9
10
|
let _orkestrel_websocket = require("@orkestrel/websocket");
|
|
10
11
|
let node_child_process = require("node:child_process");
|
|
11
|
-
let _orkestrel_server = require("@orkestrel/server");
|
|
12
12
|
//#region src/server/constants.ts
|
|
13
13
|
/**
|
|
14
14
|
* The Streamable-HTTP transport header that carries the MCP session id. When a {@link
|
|
@@ -18,10 +18,13 @@ let _orkestrel_server = require("@orkestrel/server");
|
|
|
18
18
|
*/
|
|
19
19
|
var MCP_SESSION_HEADER = "mcp-session-id";
|
|
20
20
|
/**
|
|
21
|
-
* The Streamable-HTTP transport header
|
|
22
|
-
* on
|
|
23
|
-
*
|
|
24
|
-
*
|
|
21
|
+
* The Streamable-HTTP transport header carrying the negotiated MCP protocol version
|
|
22
|
+
* on every post-initialize client request.
|
|
23
|
+
*
|
|
24
|
+
* @remarks
|
|
25
|
+
* Required by MCP 2025-06-18 after initialization. Both HTTP client transports
|
|
26
|
+
* capture the initialize result's `protocolVersion` and send it on subsequent
|
|
27
|
+
* requests; `createMCPRoutes` rejects a present unsupported value before dispatch.
|
|
25
28
|
*/
|
|
26
29
|
var MCP_PROTOCOL_VERSION_HEADER = "mcp-protocol-version";
|
|
27
30
|
/** The default request path `createMCPRoutes` mounts the transport's `POST` route at. */
|
|
@@ -308,18 +311,13 @@ function dispatchLines(emitter, lines) {
|
|
|
308
311
|
function bridgeMessageTransport(transport) {
|
|
309
312
|
let onMessage;
|
|
310
313
|
let onClosed;
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
});
|
|
319
|
-
transport.emitter.on("close", () => {
|
|
320
|
-
onClosed?.();
|
|
321
|
-
});
|
|
322
|
-
}
|
|
314
|
+
transport.emitter.on("message", (message) => {
|
|
315
|
+
if (!(0, _src_core.isJSONRPCRequest)(message)) return;
|
|
316
|
+
onMessage?.(JSON.stringify(message));
|
|
317
|
+
});
|
|
318
|
+
transport.emitter.on("close", () => {
|
|
319
|
+
onClosed?.();
|
|
320
|
+
});
|
|
323
321
|
return {
|
|
324
322
|
async send(message) {
|
|
325
323
|
const decoded = decodeEvent(message);
|
|
@@ -327,11 +325,9 @@ function bridgeMessageTransport(transport) {
|
|
|
327
325
|
await transport.send(decoded);
|
|
328
326
|
},
|
|
329
327
|
listen(handler) {
|
|
330
|
-
subscribe();
|
|
331
328
|
onMessage = handler;
|
|
332
329
|
},
|
|
333
330
|
closed(handler) {
|
|
334
|
-
subscribe();
|
|
335
331
|
onClosed = handler;
|
|
336
332
|
},
|
|
337
333
|
async close() {
|
|
@@ -340,6 +336,63 @@ function bridgeMessageTransport(transport) {
|
|
|
340
336
|
};
|
|
341
337
|
}
|
|
342
338
|
//#endregion
|
|
339
|
+
//#region src/server/handlers.ts
|
|
340
|
+
/**
|
|
341
|
+
* Create the Streamable-HTTP POST handler used by `createMCPRoutes`.
|
|
342
|
+
*
|
|
343
|
+
* @remarks
|
|
344
|
+
* A present `mcp-protocol-version` header must name a supported revision; an
|
|
345
|
+
* unsupported value returns an HTTP `400` JSON-RPC invalid-request error without
|
|
346
|
+
* dispatching. An absent header is accepted for the initialize/bootstrap request.
|
|
347
|
+
*
|
|
348
|
+
* @param mcp - The transport-agnostic MCP server to dispatch through
|
|
349
|
+
* @param streaming - Whether an event-stream response may be negotiated
|
|
350
|
+
* @returns A request handler for the stateless MCP POST route
|
|
351
|
+
*
|
|
352
|
+
* @example
|
|
353
|
+
* ```ts
|
|
354
|
+
* import { createMCPServer } from '@orkestrel/mcp'
|
|
355
|
+
* import { createMCPPostHandler } from '@orkestrel/mcp/server'
|
|
356
|
+
* import { createToolManager } from '@orkestrel/agent'
|
|
357
|
+
*
|
|
358
|
+
* const mcp = createMCPServer({ name: 'docs', version: '1.0.0', tools: createToolManager() })
|
|
359
|
+
* const handler = createMCPPostHandler(mcp, true)
|
|
360
|
+
* await handler(new Request('http://localhost/mcp', {
|
|
361
|
+
* method: 'POST',
|
|
362
|
+
* body: '{"jsonrpc":"2.0","method":"ping","id":1}',
|
|
363
|
+
* }))
|
|
364
|
+
* ```
|
|
365
|
+
*/
|
|
366
|
+
function createMCPPostHandler(mcp, streaming) {
|
|
367
|
+
return async (request) => {
|
|
368
|
+
const protocol = request.headers.get(MCP_PROTOCOL_VERSION_HEADER);
|
|
369
|
+
if (protocol !== null && !_src_core.SUPPORTED_PROTOCOL_VERSIONS.includes(protocol)) return Response.json((0, _src_core.jsonRPCError)(null, _src_core.JSONRPC_INVALID_REQUEST, `Unsupported MCP protocol version '${protocol}'`), { status: 400 });
|
|
370
|
+
let text;
|
|
371
|
+
try {
|
|
372
|
+
text = await request.text();
|
|
373
|
+
} catch {
|
|
374
|
+
return Response.json((0, _src_core.jsonRPCError)(null, _src_core.JSONRPC_PARSE_ERROR, "Parse error"), { status: 400 });
|
|
375
|
+
}
|
|
376
|
+
let parsed;
|
|
377
|
+
try {
|
|
378
|
+
parsed = JSON.parse(text);
|
|
379
|
+
} catch {
|
|
380
|
+
return Response.json((0, _src_core.jsonRPCError)(null, _src_core.JSONRPC_PARSE_ERROR, "Parse error"), { status: 400 });
|
|
381
|
+
}
|
|
382
|
+
const rpcRequest = (0, _src_core.parseJSONRPCMessage)(parsed);
|
|
383
|
+
if (rpcRequest === void 0 || !("method" in rpcRequest)) return Response.json((0, _src_core.jsonRPCError)(null, _src_core.JSONRPC_INVALID_REQUEST, "Invalid Request"), { status: 400 });
|
|
384
|
+
const response = await mcp.dispatch(rpcRequest);
|
|
385
|
+
if (response === void 0) return new Response(null, { status: 202 });
|
|
386
|
+
if (streaming && acceptsEventStream(request)) {
|
|
387
|
+
const stream = (0, _orkestrel_server.openStream)();
|
|
388
|
+
stream.write({ data: JSON.stringify(response) });
|
|
389
|
+
stream.end();
|
|
390
|
+
return stream.response;
|
|
391
|
+
}
|
|
392
|
+
return Response.json(response);
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
//#endregion
|
|
343
396
|
//#region src/server/transports/HTTPClientTransport.ts
|
|
344
397
|
/**
|
|
345
398
|
* The HTTP CLIENT transport for the Model Context Protocol — a
|
|
@@ -348,7 +401,7 @@ function bridgeMessageTransport(transport) {
|
|
|
348
401
|
*
|
|
349
402
|
* @remarks
|
|
350
403
|
* - **Request/response over `fetch`.** `send(message)` POSTs the JSON-serialized
|
|
351
|
-
* message
|
|
404
|
+
* message to `options.url` with `content-type: application/json` and an
|
|
352
405
|
* `Accept` of BOTH `application/json` and `text/event-stream` (so the server may
|
|
353
406
|
* answer with either framing) — plus any `options.headers` (e.g. an `Authorization`
|
|
354
407
|
* bearer). It then decodes the reply and emits each decoded {@link JSONRPCMessage} on
|
|
@@ -360,13 +413,18 @@ function bridgeMessageTransport(transport) {
|
|
|
360
413
|
* readEventStream}) — the inverse of the server's `openStream` seam, so the wire
|
|
361
414
|
* round-trips. A `202`
|
|
362
415
|
* Accepted (a notification) carries no body and emits nothing.
|
|
363
|
-
* - **Session echo.** `start()`
|
|
364
|
-
*
|
|
365
|
-
* STATEFUL server sends one (on
|
|
366
|
-
*
|
|
367
|
-
* `
|
|
368
|
-
*
|
|
369
|
-
*
|
|
416
|
+
* - **Session and protocol echo.** `start()` is a no-op (a
|
|
417
|
+
* request/response transport opens no long-lived connection). The
|
|
418
|
+
* `mcp-session-id` response header, when a STATEFUL server sends one (on
|
|
419
|
+
* `initialize`), is captured into `session` and then ECHOED as the
|
|
420
|
+
* `mcp-session-id` request header on every SUBSEQUENT request — so an
|
|
421
|
+
* `MCPClient` passes a stateful server's session validation. The
|
|
422
|
+
* initialize result's `protocolVersion` is likewise captured, but only
|
|
423
|
+
* when it is a SUPPORTED value, and echoed as `mcp-protocol-version` on
|
|
424
|
+
* every subsequent request, as required by the 2025-06-18 Streamable-HTTP
|
|
425
|
+
* transport. Before initialize returns, neither captured header is sent.
|
|
426
|
+
* `close()` clears the captured protocol so a reconnect's `initialize`
|
|
427
|
+
* POST is headerless; the captured `session` persists across `close()`.
|
|
370
428
|
* - **Total at the boundary (§14).** Every reply is narrowed (`parseJSONRPCMessage`,
|
|
371
429
|
* the SSE decoder) — a non-message reply is dropped, never asserted; a `fetch` /
|
|
372
430
|
* decode failure surfaces on the `error` event rather than escaping `send`.
|
|
@@ -387,6 +445,7 @@ var HTTPClientTransport = class {
|
|
|
387
445
|
#fetch;
|
|
388
446
|
#timeout;
|
|
389
447
|
#session = void 0;
|
|
448
|
+
#protocol = void 0;
|
|
390
449
|
constructor(options) {
|
|
391
450
|
this.#emitter = new _orkestrel_emitter.Emitter();
|
|
392
451
|
this.#url = options.url;
|
|
@@ -410,6 +469,7 @@ var HTTPClientTransport = class {
|
|
|
410
469
|
"content-type": "application/json",
|
|
411
470
|
accept: "application/json, text/event-stream",
|
|
412
471
|
...this.#session === void 0 ? {} : { [MCP_SESSION_HEADER]: this.#session },
|
|
472
|
+
...this.#protocol === void 0 ? {} : { [MCP_PROTOCOL_VERSION_HEADER]: this.#protocol },
|
|
413
473
|
...this.#headers
|
|
414
474
|
},
|
|
415
475
|
body: JSON.stringify(message),
|
|
@@ -424,6 +484,7 @@ var HTTPClientTransport = class {
|
|
|
424
484
|
await this.#deliver(response);
|
|
425
485
|
}
|
|
426
486
|
async close() {
|
|
487
|
+
this.#protocol = void 0;
|
|
427
488
|
this.#emitter.emit("close");
|
|
428
489
|
}
|
|
429
490
|
async #deliver(response) {
|
|
@@ -431,17 +492,21 @@ var HTTPClientTransport = class {
|
|
|
431
492
|
const type = response.headers.get("content-type") ?? "";
|
|
432
493
|
try {
|
|
433
494
|
if (type.includes("text/event-stream")) {
|
|
434
|
-
for (const message of await readEventStream(response)) this.#
|
|
495
|
+
for (const message of await readEventStream(response)) this.#capture(message);
|
|
435
496
|
return;
|
|
436
497
|
}
|
|
437
498
|
if (type.includes("application/json")) {
|
|
438
499
|
const message = (0, _src_core.parseJSONRPCMessage)(await response.json());
|
|
439
|
-
if (message !== void 0) this.#
|
|
500
|
+
if (message !== void 0) this.#capture(message);
|
|
440
501
|
}
|
|
441
502
|
} catch (error) {
|
|
442
503
|
this.#emitter.emit("error", error);
|
|
443
504
|
}
|
|
444
505
|
}
|
|
506
|
+
#capture(message) {
|
|
507
|
+
if ((0, _src_core.isJSONRPCResponse)(message) && (0, _orkestrel_contract.isRecord)(message.result) && (0, _orkestrel_contract.isString)(message.result["protocolVersion"]) && _src_core.SUPPORTED_PROTOCOL_VERSIONS.includes(message.result["protocolVersion"])) this.#protocol = message.result["protocolVersion"];
|
|
508
|
+
this.#emitter.emit("message", message);
|
|
509
|
+
}
|
|
445
510
|
};
|
|
446
511
|
//#endregion
|
|
447
512
|
//#region src/server/MCPSession.ts
|
|
@@ -583,8 +648,8 @@ var MCPSession = class {
|
|
|
583
648
|
* parsed envelope the {@link import('@src/core').MCPServerInterface} pump dispatches), while
|
|
584
649
|
* a non-JSON or non-message frame is surfaced on `error` and DROPPED, never thrown (§14). It
|
|
585
650
|
* also bridges the socket's `close` → this transport's `close`, and the socket's `error`.
|
|
586
|
-
* - **Outbound (`send`).** `send(message
|
|
587
|
-
* (`nodeWs.send(JSON.stringify(
|
|
651
|
+
* - **Outbound (`send`).** `send(message)` writes one text frame
|
|
652
|
+
* (`nodeWs.send(JSON.stringify(message))`); the underlying wrapper no-ops a write on a
|
|
588
653
|
* non-open socket, so a closed connection drops silently rather than throwing.
|
|
589
654
|
* - **`close()`** closes the underlying socket (the RFC 6455 close handshake) and fires the
|
|
590
655
|
* transport's `close` event (idempotent — a second `close`, or a socket-driven close, emits
|
|
@@ -614,8 +679,7 @@ var WebSocketServerTransport = class {
|
|
|
614
679
|
this.#socket.emitter.on("error", (error) => this.#emitter.emit("error", error));
|
|
615
680
|
}
|
|
616
681
|
async send(message) {
|
|
617
|
-
|
|
618
|
-
for (const one of messages) this.#socket.send(JSON.stringify(one));
|
|
682
|
+
this.#socket.send(JSON.stringify(message));
|
|
619
683
|
}
|
|
620
684
|
async close() {
|
|
621
685
|
if (this.#closed) return;
|
|
@@ -666,7 +730,7 @@ var WebSocketServerTransport = class {
|
|
|
666
730
|
* event (the reply the {@link import('@src/core').MCPClientInterface} correlates by `id`); a
|
|
667
731
|
* non-JSON / non-message frame surfaces on `error` and is dropped (§14). The socket's `close`
|
|
668
732
|
* / `error` bridge to this transport's events.
|
|
669
|
-
* - **Outbound (`send`).** `send(message
|
|
733
|
+
* - **Outbound (`send`).** `send(message)` writes one masked text frame.
|
|
670
734
|
* - **`close()`** closes the underlying socket and fires `close` (idempotent).
|
|
671
735
|
* - **URL scheme.** `options.url` accepts a `ws://` / `wss://` URL or an `http://` / `https://`
|
|
672
736
|
* one; a `ws(s)` scheme is converted to `http(s)` for the underlying upgrade request (`wss`
|
|
@@ -705,12 +769,6 @@ var WebSocketClientTransport = class {
|
|
|
705
769
|
const secure = url.protocol === "https:";
|
|
706
770
|
const send = secure ? node_https.request : node_http.request;
|
|
707
771
|
await new Promise((resolve, reject) => {
|
|
708
|
-
let settled = false;
|
|
709
|
-
const fail = (error) => {
|
|
710
|
-
if (settled) return;
|
|
711
|
-
settled = true;
|
|
712
|
-
reject(error);
|
|
713
|
-
};
|
|
714
772
|
const request = send({
|
|
715
773
|
hostname: url.hostname,
|
|
716
774
|
port: url.port.length > 0 ? Number(url.port) : secure ? 443 : 80,
|
|
@@ -728,7 +786,7 @@ var WebSocketClientTransport = class {
|
|
|
728
786
|
const accept = response.headers["sec-websocket-accept"];
|
|
729
787
|
if (!(0, _orkestrel_contract.isString)(accept) || accept !== (0, _orkestrel_websocket.computeWebSocketAccept)(key)) {
|
|
730
788
|
socket.destroy();
|
|
731
|
-
|
|
789
|
+
reject(/* @__PURE__ */ new Error("WebSocket handshake failed: Sec-WebSocket-Accept mismatch"));
|
|
732
790
|
return;
|
|
733
791
|
}
|
|
734
792
|
const ws = (0, _orkestrel_websocket.createNodeWebSocket)({
|
|
@@ -737,24 +795,20 @@ var WebSocketClientTransport = class {
|
|
|
737
795
|
});
|
|
738
796
|
this.#socket = ws;
|
|
739
797
|
this.#bind(ws);
|
|
740
|
-
|
|
741
|
-
settled = true;
|
|
742
|
-
resolve();
|
|
743
|
-
}
|
|
798
|
+
resolve();
|
|
744
799
|
});
|
|
745
800
|
request.on("response", (response) => {
|
|
746
801
|
response.resume();
|
|
747
|
-
|
|
802
|
+
reject(/* @__PURE__ */ new Error(`WebSocket upgrade declined with status ${response.statusCode ?? 0}`));
|
|
748
803
|
});
|
|
749
|
-
request.on("error", (error) =>
|
|
804
|
+
request.on("error", (error) => reject(error instanceof Error ? error : new Error(String(error))));
|
|
750
805
|
request.end();
|
|
751
806
|
});
|
|
752
807
|
}
|
|
753
808
|
async send(message) {
|
|
754
809
|
const socket = this.#socket;
|
|
755
810
|
if (socket === void 0) throw new Error("WebSocket transport is not connected");
|
|
756
|
-
|
|
757
|
-
for (const one of messages) socket.send(JSON.stringify(one));
|
|
811
|
+
socket.send(JSON.stringify(message));
|
|
758
812
|
}
|
|
759
813
|
async close() {
|
|
760
814
|
if (this.#closed) return;
|
|
@@ -818,8 +872,8 @@ var WebSocketClientTransport = class {
|
|
|
818
872
|
* {@link dispatchLines} helper — a well-formed {@link JSONRPCMessage} emits
|
|
819
873
|
* `message`, a malformed line emits `error` (§14, never throws). The child's
|
|
820
874
|
* `close` bridges to this transport's `close`.
|
|
821
|
-
* - **Outbound (`send`).** `send(message
|
|
822
|
-
* `JSON.stringify`d line
|
|
875
|
+
* - **Outbound (`send`).** `send(message)` writes one newline-terminated
|
|
876
|
+
* `JSON.stringify`d line to the child's `stdin`.
|
|
823
877
|
* - **`close()`** kills the child process and fires `close` (idempotent).
|
|
824
878
|
* - **Observable (§13).** Owns the `emitter` ({@link ClientTransportEventMap}); the
|
|
825
879
|
* emitter isolates a listener throw; `error` is a DOMAIN event (a transport-level
|
|
@@ -870,8 +924,7 @@ var StdioClientTransport = class {
|
|
|
870
924
|
async send(message) {
|
|
871
925
|
const child = this.#child;
|
|
872
926
|
if (child === void 0) throw new Error("stdio transport is not connected");
|
|
873
|
-
|
|
874
|
-
for (const one of messages) child.stdin.write(`${JSON.stringify(one)}\n`);
|
|
927
|
+
child.stdin.write(`${JSON.stringify(message)}\n`);
|
|
875
928
|
}
|
|
876
929
|
async close() {
|
|
877
930
|
if (this.#closed) return;
|
|
@@ -914,8 +967,8 @@ var StdioClientTransport = class {
|
|
|
914
967
|
* well-formed {@link JSONRPCMessage} re-emits on `message`, a malformed line
|
|
915
968
|
* emits `error` (§14, never throws). `input`'s `close` bridges to this
|
|
916
969
|
* transport's `close`.
|
|
917
|
-
* - **Outbound (`send`).** `send(message
|
|
918
|
-
* `JSON.stringify`d line
|
|
970
|
+
* - **Outbound (`send`).** `send(message)` writes one newline-terminated
|
|
971
|
+
* `JSON.stringify`d line to `output`.
|
|
919
972
|
* - **`close()`** fires this transport's `close` (idempotent) — the injected streams
|
|
920
973
|
* are owned by the caller (typically `process.stdin`/`process.stdout`, which must
|
|
921
974
|
* never be closed out from under the process) and are not torn down here.
|
|
@@ -947,8 +1000,7 @@ var StdioServerTransport = class {
|
|
|
947
1000
|
this.#input.on("error", (error) => this.#emitter.emit("error", error));
|
|
948
1001
|
}
|
|
949
1002
|
async send(message) {
|
|
950
|
-
|
|
951
|
-
for (const one of messages) this.#output.write(`${JSON.stringify(one)}\n`);
|
|
1003
|
+
this.#output.write(`${JSON.stringify(message)}\n`);
|
|
952
1004
|
}
|
|
953
1005
|
async close() {
|
|
954
1006
|
if (this.#closed) return;
|
|
@@ -983,6 +1035,9 @@ var StdioServerTransport = class {
|
|
|
983
1035
|
* - A **transport** failure — a malformed JSON body, or a parsed value that is not a
|
|
984
1036
|
* JSON-RPC REQUEST — is an HTTP `400` carrying a JSON-RPC error BODY (`-32700` Parse
|
|
985
1037
|
* error / `-32600` Invalid Request, id `null`).
|
|
1038
|
+
* - A present `mcp-protocol-version` header is validated before dispatch: a supported
|
|
1039
|
+
* value proceeds, while an unsupported value returns HTTP `400` with a JSON-RPC
|
|
1040
|
+
* `-32600` body. An absent value proceeds for initialize/bootstrap compatibility.
|
|
986
1041
|
* - A **dispatch** result — a success OR an IN-BAND JSON-RPC error from `mcp.dispatch`
|
|
987
1042
|
* (e.g. `-32601` method-not-found) — is an HTTP `200` carrying the JSON-RPC response
|
|
988
1043
|
* envelope (the error is in-band per JSON-RPC, NOT an HTTP error).
|
|
@@ -1019,37 +1074,11 @@ var StdioServerTransport = class {
|
|
|
1019
1074
|
* ```
|
|
1020
1075
|
*/
|
|
1021
1076
|
function createMCPRoutes(mcp, options) {
|
|
1022
|
-
const path = options?.path ?? "/mcp";
|
|
1023
|
-
const streaming = options?.streaming ?? true;
|
|
1024
1077
|
return [{
|
|
1025
1078
|
method: "POST",
|
|
1026
|
-
path,
|
|
1079
|
+
path: options?.path ?? "/mcp",
|
|
1027
1080
|
name: "mcp",
|
|
1028
|
-
handler:
|
|
1029
|
-
let text;
|
|
1030
|
-
try {
|
|
1031
|
-
text = await request.text();
|
|
1032
|
-
} catch {
|
|
1033
|
-
return Response.json((0, _src_core.jsonRPCError)(null, _src_core.JSONRPC_PARSE_ERROR, "Parse error"), { status: 400 });
|
|
1034
|
-
}
|
|
1035
|
-
let parsed;
|
|
1036
|
-
try {
|
|
1037
|
-
parsed = JSON.parse(text);
|
|
1038
|
-
} catch {
|
|
1039
|
-
return Response.json((0, _src_core.jsonRPCError)(null, _src_core.JSONRPC_PARSE_ERROR, "Parse error"), { status: 400 });
|
|
1040
|
-
}
|
|
1041
|
-
const rpcRequest = (0, _src_core.parseJSONRPCMessage)(parsed);
|
|
1042
|
-
if (rpcRequest === void 0 || !("method" in rpcRequest)) return Response.json((0, _src_core.jsonRPCError)(null, _src_core.JSONRPC_INVALID_REQUEST, "Invalid Request"), { status: 400 });
|
|
1043
|
-
const response = await mcp.dispatch(rpcRequest);
|
|
1044
|
-
if (response === void 0) return new Response(null, { status: 202 });
|
|
1045
|
-
if (streaming && acceptsEventStream(request)) {
|
|
1046
|
-
const s = (0, _orkestrel_server.openStream)();
|
|
1047
|
-
s.write({ data: JSON.stringify(response) });
|
|
1048
|
-
s.end();
|
|
1049
|
-
return s.response;
|
|
1050
|
-
}
|
|
1051
|
-
return Response.json(response);
|
|
1052
|
-
}
|
|
1081
|
+
handler: createMCPPostHandler(mcp, options?.streaming ?? true)
|
|
1053
1082
|
}];
|
|
1054
1083
|
}
|
|
1055
1084
|
/**
|
|
@@ -1065,8 +1094,10 @@ function createMCPRoutes(mcp, options) {
|
|
|
1065
1094
|
* and the reply is surfaced on the transport's `message` event for the client's id
|
|
1066
1095
|
* correlation. Add `options.headers` (e.g. an `Authorization` bearer) to reach a guarded
|
|
1067
1096
|
* server. `start` / `close` hold no connection; against a STATEFUL server it captures the
|
|
1068
|
-
* `mcp-session-id` from `initialize` and echoes it on later requests
|
|
1069
|
-
*
|
|
1097
|
+
* `mcp-session-id` from `initialize` and echoes it on later requests. It also captures
|
|
1098
|
+
* the initialize result's `protocolVersion` and sends `mcp-protocol-version` on every
|
|
1099
|
+
* subsequent request, so the same `MCPClient` passes the session and 2025-06-18
|
|
1100
|
+
* protocol gates without caller wiring.
|
|
1070
1101
|
*
|
|
1071
1102
|
* @param options - `url` (the remote endpoint; REQUIRED), optional `headers` merged onto
|
|
1072
1103
|
* every request, optional `fetch` (default `globalThis.fetch`), and optional `timeout`
|
|
@@ -1329,30 +1360,43 @@ function createMCPSession(options) {
|
|
|
1329
1360
|
const store = /* @__PURE__ */ new Map();
|
|
1330
1361
|
return async (request, context, next) => {
|
|
1331
1362
|
if (context.url.pathname !== path) return next();
|
|
1332
|
-
|
|
1363
|
+
if (ttl !== void 0) {
|
|
1364
|
+
const cutoff = clock() - ttl;
|
|
1365
|
+
for (const [id, entry] of store) if (entry.touched <= cutoff) store.delete(id);
|
|
1366
|
+
}
|
|
1367
|
+
if (context.method === "DELETE") {
|
|
1368
|
+
const id = readSessionHeader(request);
|
|
1369
|
+
if (id === void 0 || !store.delete(id)) return rejectUnknownSession();
|
|
1370
|
+
return new Response(null, { status: 204 });
|
|
1371
|
+
}
|
|
1372
|
+
let entry;
|
|
1373
|
+
const id = readSessionHeader(request);
|
|
1374
|
+
if (id !== void 0) {
|
|
1375
|
+
const current = store.get(id);
|
|
1376
|
+
if (current !== void 0) {
|
|
1377
|
+
entry = {
|
|
1378
|
+
session: current.session,
|
|
1379
|
+
touched: clock()
|
|
1380
|
+
};
|
|
1381
|
+
store.set(id, entry);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1333
1384
|
if (context.method === "GET") {
|
|
1334
|
-
const entry = resolve(request);
|
|
1335
1385
|
if (entry === void 0) return rejectUnknownSession();
|
|
1386
|
+
const session = entry.session;
|
|
1336
1387
|
const stream = (0, _orkestrel_server.openStream)();
|
|
1337
1388
|
stream.comment("open");
|
|
1338
1389
|
const lastEventId = readLastEventId(request);
|
|
1339
|
-
if (lastEventId !== void 0) for (const e of
|
|
1390
|
+
if (lastEventId !== void 0) for (const e of session.replay(lastEventId)) stream.write({
|
|
1340
1391
|
id: e.id,
|
|
1341
1392
|
data: JSON.stringify(e.message)
|
|
1342
1393
|
});
|
|
1343
|
-
|
|
1344
|
-
if (request.signal.aborted)
|
|
1345
|
-
else request.signal.addEventListener("abort", () =>
|
|
1394
|
+
session.attach(stream);
|
|
1395
|
+
if (request.signal.aborted) session.detach(stream);
|
|
1396
|
+
else request.signal.addEventListener("abort", () => session.detach(stream), { once: true });
|
|
1346
1397
|
return stream.response;
|
|
1347
1398
|
}
|
|
1348
|
-
if (context.method === "DELETE") {
|
|
1349
|
-
const id = readSessionHeader(request);
|
|
1350
|
-
if (id === void 0 || !store.has(id)) return rejectUnknownSession();
|
|
1351
|
-
store.delete(id);
|
|
1352
|
-
return new Response(null, { status: 204 });
|
|
1353
|
-
}
|
|
1354
1399
|
const text = await request.text();
|
|
1355
|
-
let entry = resolve(request);
|
|
1356
1400
|
if (entry === void 0) {
|
|
1357
1401
|
let parsed;
|
|
1358
1402
|
try {
|
|
@@ -1361,7 +1405,7 @@ function createMCPSession(options) {
|
|
|
1361
1405
|
parsed = void 0;
|
|
1362
1406
|
}
|
|
1363
1407
|
if (parsed !== void 0 && (0, _src_core.isInitializeRequest)(parsed)) {
|
|
1364
|
-
const session = new MCPSession(crypto.randomUUID(), { capacity });
|
|
1408
|
+
const session = new MCPSession(crypto.randomUUID(), capacity !== void 0 ? { capacity } : {});
|
|
1365
1409
|
entry = {
|
|
1366
1410
|
session,
|
|
1367
1411
|
touched: clock()
|
|
@@ -1369,7 +1413,7 @@ function createMCPSession(options) {
|
|
|
1369
1413
|
store.set(session.id, entry);
|
|
1370
1414
|
} else return rejectUnknownSession();
|
|
1371
1415
|
}
|
|
1372
|
-
context.state
|
|
1416
|
+
if (!Reflect.set(context.state, "session", entry.session)) throw new Error("MCP session state is not writable");
|
|
1373
1417
|
const response = await next(new Request(context.url, {
|
|
1374
1418
|
method: "POST",
|
|
1375
1419
|
headers: request.headers,
|
|
@@ -1378,19 +1422,6 @@ function createMCPSession(options) {
|
|
|
1378
1422
|
response.headers.set(MCP_SESSION_HEADER, entry.session.id);
|
|
1379
1423
|
return response;
|
|
1380
1424
|
};
|
|
1381
|
-
function resolve(request) {
|
|
1382
|
-
const id = readSessionHeader(request);
|
|
1383
|
-
if (id === void 0) return void 0;
|
|
1384
|
-
const entry = store.get(id);
|
|
1385
|
-
if (entry === void 0) return void 0;
|
|
1386
|
-
entry.touched = clock();
|
|
1387
|
-
return entry;
|
|
1388
|
-
}
|
|
1389
|
-
function sweep() {
|
|
1390
|
-
if (ttl === void 0) return;
|
|
1391
|
-
const cutoff = clock() - ttl;
|
|
1392
|
-
for (const [id, entry] of store) if (entry.touched <= cutoff) store.delete(id);
|
|
1393
|
-
}
|
|
1394
1425
|
}
|
|
1395
1426
|
//#endregion
|
|
1396
1427
|
exports.DEFAULT_MCP_PATH = DEFAULT_MCP_PATH;
|
|
@@ -1408,6 +1439,7 @@ exports.WebSocketServerTransport = WebSocketServerTransport;
|
|
|
1408
1439
|
exports.acceptsEventStream = acceptsEventStream;
|
|
1409
1440
|
exports.bridgeMessageTransport = bridgeMessageTransport;
|
|
1410
1441
|
exports.createHTTPClientTransport = createHTTPClientTransport;
|
|
1442
|
+
exports.createMCPPostHandler = createMCPPostHandler;
|
|
1411
1443
|
exports.createMCPRoutes = createMCPRoutes;
|
|
1412
1444
|
exports.createMCPSession = createMCPSession;
|
|
1413
1445
|
exports.createStdioClientTransport = createStdioClientTransport;
|