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