@orkestrel/mcp 0.0.4 → 0.0.6
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 +564 -0
- package/dist/src/browser/index.js +690 -0
- package/dist/src/browser/index.js.map +1 -0
- package/dist/src/core/index.cjs +277 -39
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +211 -17
- package/dist/src/core/index.d.ts +211 -17
- package/dist/src/core/index.js +273 -40
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +225 -139
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +138 -51
- package/dist/src/server/index.d.ts +138 -51
- package/dist/src/server/index.js +226 -142
- package/dist/src/server/index.js.map +1 -1
- package/package.json +38 -25
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, 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. */
|
|
@@ -263,6 +266,131 @@ function dispatchLines(emitter, lines) {
|
|
|
263
266
|
emitter.emit("message", message);
|
|
264
267
|
}
|
|
265
268
|
}
|
|
269
|
+
/**
|
|
270
|
+
* Bridge a message-channel {@link ClientTransportInterface} (the shape the stdio and
|
|
271
|
+
* WebSocket SERVER transports already implement) into the environment-agnostic
|
|
272
|
+
* {@link import('@src/core').MCPTransportInterface} port — the adapter
|
|
273
|
+
* {@link import('./factories.js').createStdioServer} and {@link
|
|
274
|
+
* import('./factories.js').createWebSocketServer} pipe through `bindServer`, so the
|
|
275
|
+
* request/reply/error pump those two factories used to hand-roll identically now
|
|
276
|
+
* lives ONCE in the core binder.
|
|
277
|
+
*
|
|
278
|
+
* @remarks
|
|
279
|
+
* `send` decodes the already-serialized reply string back to a {@link JSONRPCMessage}
|
|
280
|
+
* and writes it via `transport.send` (the SAME `JSON.stringify` the underlying
|
|
281
|
+
* transport already performs, so the wire bytes are unchanged). `listen` filters
|
|
282
|
+
* `transport`'s `message` event to REQUESTS ONLY — a stray response is ignored,
|
|
283
|
+
* exactly as the prior hand-rolled pumps did — and re-serializes each one back to a
|
|
284
|
+
* string for `bindServer`. `closed` bridges `transport`'s `close` event. `close`
|
|
285
|
+
* closes the underlying `transport`.
|
|
286
|
+
*
|
|
287
|
+
* @remarks Per {@link import('@src/core').MCPTransportInterface}, `listen`/`closed`
|
|
288
|
+
* each hold THE SINGLE current handler (a second call REPLACES the first, never adds).
|
|
289
|
+
* Since the underlying `transport.emitter` is ADD-based (`on` subscribes, never
|
|
290
|
+
* replaces), this bridge installs ONE stable emitter listener per event on first use
|
|
291
|
+
* and re-routes it to whichever handler is CURRENTLY registered (`undefined` while
|
|
292
|
+
* none is), so rebinding never double-dispatches.
|
|
293
|
+
*
|
|
294
|
+
* @remarks A response whose `result` serializes away (e.g. `undefined`) is dropped by
|
|
295
|
+
* the message validators on the wire's decode side — an asymmetry the stdio/WS carrier
|
|
296
|
+
* shares with the streamable-HTTP face, since both round-trip through `JSON.stringify`
|
|
297
|
+
* / `JSON.parse` before re-validation.
|
|
298
|
+
*
|
|
299
|
+
* @param transport - The message-channel transport to bridge (stdio or WebSocket)
|
|
300
|
+
* @returns An {@link import('@src/core').MCPTransportInterface} `bindServer` can drive
|
|
301
|
+
*
|
|
302
|
+
* @example
|
|
303
|
+
* ```ts
|
|
304
|
+
* import { bindServer } from '@src/core'
|
|
305
|
+
*
|
|
306
|
+
* const transport = new StdioServerTransport(process.stdin, process.stdout)
|
|
307
|
+
* bindServer(mcp, bridgeMessageTransport(transport))
|
|
308
|
+
* ```
|
|
309
|
+
*/
|
|
310
|
+
function bridgeMessageTransport(transport) {
|
|
311
|
+
let onMessage;
|
|
312
|
+
let onClosed;
|
|
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
|
+
});
|
|
320
|
+
return {
|
|
321
|
+
async send(message) {
|
|
322
|
+
const decoded = decodeEvent(message);
|
|
323
|
+
if (decoded === void 0) return;
|
|
324
|
+
await transport.send(decoded);
|
|
325
|
+
},
|
|
326
|
+
listen(handler) {
|
|
327
|
+
onMessage = handler;
|
|
328
|
+
},
|
|
329
|
+
closed(handler) {
|
|
330
|
+
onClosed = handler;
|
|
331
|
+
},
|
|
332
|
+
async close() {
|
|
333
|
+
await transport.close();
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
}
|
|
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
|
+
}
|
|
266
394
|
//#endregion
|
|
267
395
|
//#region src/server/transports/HTTPClientTransport.ts
|
|
268
396
|
/**
|
|
@@ -272,7 +400,7 @@ function dispatchLines(emitter, lines) {
|
|
|
272
400
|
*
|
|
273
401
|
* @remarks
|
|
274
402
|
* - **Request/response over `fetch`.** `send(message)` POSTs the JSON-serialized
|
|
275
|
-
* message
|
|
403
|
+
* message to `options.url` with `content-type: application/json` and an
|
|
276
404
|
* `Accept` of BOTH `application/json` and `text/event-stream` (so the server may
|
|
277
405
|
* answer with either framing) — plus any `options.headers` (e.g. an `Authorization`
|
|
278
406
|
* bearer). It then decodes the reply and emits each decoded {@link JSONRPCMessage} on
|
|
@@ -284,13 +412,18 @@ function dispatchLines(emitter, lines) {
|
|
|
284
412
|
* readEventStream}) — the inverse of the server's `openStream` seam, so the wire
|
|
285
413
|
* round-trips. A `202`
|
|
286
414
|
* Accepted (a notification) carries no body and emits nothing.
|
|
287
|
-
* - **Session echo.** `start()`
|
|
288
|
-
*
|
|
289
|
-
* STATEFUL server sends one (on
|
|
290
|
-
*
|
|
291
|
-
* `
|
|
292
|
-
*
|
|
293
|
-
*
|
|
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()`.
|
|
294
427
|
* - **Total at the boundary (§14).** Every reply is narrowed (`parseJSONRPCMessage`,
|
|
295
428
|
* the SSE decoder) — a non-message reply is dropped, never asserted; a `fetch` /
|
|
296
429
|
* decode failure surfaces on the `error` event rather than escaping `send`.
|
|
@@ -311,6 +444,7 @@ var HTTPClientTransport = class {
|
|
|
311
444
|
#fetch;
|
|
312
445
|
#timeout;
|
|
313
446
|
#session = void 0;
|
|
447
|
+
#protocol = void 0;
|
|
314
448
|
constructor(options) {
|
|
315
449
|
this.#emitter = new Emitter();
|
|
316
450
|
this.#url = options.url;
|
|
@@ -334,6 +468,7 @@ var HTTPClientTransport = class {
|
|
|
334
468
|
"content-type": "application/json",
|
|
335
469
|
accept: "application/json, text/event-stream",
|
|
336
470
|
...this.#session === void 0 ? {} : { [MCP_SESSION_HEADER]: this.#session },
|
|
471
|
+
...this.#protocol === void 0 ? {} : { [MCP_PROTOCOL_VERSION_HEADER]: this.#protocol },
|
|
337
472
|
...this.#headers
|
|
338
473
|
},
|
|
339
474
|
body: JSON.stringify(message),
|
|
@@ -348,6 +483,7 @@ var HTTPClientTransport = class {
|
|
|
348
483
|
await this.#deliver(response);
|
|
349
484
|
}
|
|
350
485
|
async close() {
|
|
486
|
+
this.#protocol = void 0;
|
|
351
487
|
this.#emitter.emit("close");
|
|
352
488
|
}
|
|
353
489
|
async #deliver(response) {
|
|
@@ -355,17 +491,21 @@ var HTTPClientTransport = class {
|
|
|
355
491
|
const type = response.headers.get("content-type") ?? "";
|
|
356
492
|
try {
|
|
357
493
|
if (type.includes("text/event-stream")) {
|
|
358
|
-
for (const message of await readEventStream(response)) this.#
|
|
494
|
+
for (const message of await readEventStream(response)) this.#capture(message);
|
|
359
495
|
return;
|
|
360
496
|
}
|
|
361
497
|
if (type.includes("application/json")) {
|
|
362
498
|
const message = parseJSONRPCMessage(await response.json());
|
|
363
|
-
if (message !== void 0) this.#
|
|
499
|
+
if (message !== void 0) this.#capture(message);
|
|
364
500
|
}
|
|
365
501
|
} catch (error) {
|
|
366
502
|
this.#emitter.emit("error", error);
|
|
367
503
|
}
|
|
368
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
|
+
}
|
|
369
509
|
};
|
|
370
510
|
//#endregion
|
|
371
511
|
//#region src/server/MCPSession.ts
|
|
@@ -507,8 +647,8 @@ var MCPSession = class {
|
|
|
507
647
|
* parsed envelope the {@link import('@src/core').MCPServerInterface} pump dispatches), while
|
|
508
648
|
* a non-JSON or non-message frame is surfaced on `error` and DROPPED, never thrown (§14). It
|
|
509
649
|
* also bridges the socket's `close` → this transport's `close`, and the socket's `error`.
|
|
510
|
-
* - **Outbound (`send`).** `send(message
|
|
511
|
-
* (`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
|
|
512
652
|
* non-open socket, so a closed connection drops silently rather than throwing.
|
|
513
653
|
* - **`close()`** closes the underlying socket (the RFC 6455 close handshake) and fires the
|
|
514
654
|
* transport's `close` event (idempotent — a second `close`, or a socket-driven close, emits
|
|
@@ -538,8 +678,7 @@ var WebSocketServerTransport = class {
|
|
|
538
678
|
this.#socket.emitter.on("error", (error) => this.#emitter.emit("error", error));
|
|
539
679
|
}
|
|
540
680
|
async send(message) {
|
|
541
|
-
|
|
542
|
-
for (const one of messages) this.#socket.send(JSON.stringify(one));
|
|
681
|
+
this.#socket.send(JSON.stringify(message));
|
|
543
682
|
}
|
|
544
683
|
async close() {
|
|
545
684
|
if (this.#closed) return;
|
|
@@ -590,7 +729,7 @@ var WebSocketServerTransport = class {
|
|
|
590
729
|
* event (the reply the {@link import('@src/core').MCPClientInterface} correlates by `id`); a
|
|
591
730
|
* non-JSON / non-message frame surfaces on `error` and is dropped (§14). The socket's `close`
|
|
592
731
|
* / `error` bridge to this transport's events.
|
|
593
|
-
* - **Outbound (`send`).** `send(message
|
|
732
|
+
* - **Outbound (`send`).** `send(message)` writes one masked text frame.
|
|
594
733
|
* - **`close()`** closes the underlying socket and fires `close` (idempotent).
|
|
595
734
|
* - **URL scheme.** `options.url` accepts a `ws://` / `wss://` URL or an `http://` / `https://`
|
|
596
735
|
* one; a `ws(s)` scheme is converted to `http(s)` for the underlying upgrade request (`wss`
|
|
@@ -629,12 +768,6 @@ var WebSocketClientTransport = class {
|
|
|
629
768
|
const secure = url.protocol === "https:";
|
|
630
769
|
const send = secure ? request$1 : request;
|
|
631
770
|
await new Promise((resolve, reject) => {
|
|
632
|
-
let settled = false;
|
|
633
|
-
const fail = (error) => {
|
|
634
|
-
if (settled) return;
|
|
635
|
-
settled = true;
|
|
636
|
-
reject(error);
|
|
637
|
-
};
|
|
638
771
|
const request = send({
|
|
639
772
|
hostname: url.hostname,
|
|
640
773
|
port: url.port.length > 0 ? Number(url.port) : secure ? 443 : 80,
|
|
@@ -652,7 +785,7 @@ var WebSocketClientTransport = class {
|
|
|
652
785
|
const accept = response.headers["sec-websocket-accept"];
|
|
653
786
|
if (!isString(accept) || accept !== computeWebSocketAccept(key)) {
|
|
654
787
|
socket.destroy();
|
|
655
|
-
|
|
788
|
+
reject(/* @__PURE__ */ new Error("WebSocket handshake failed: Sec-WebSocket-Accept mismatch"));
|
|
656
789
|
return;
|
|
657
790
|
}
|
|
658
791
|
const ws = createNodeWebSocket({
|
|
@@ -661,24 +794,20 @@ var WebSocketClientTransport = class {
|
|
|
661
794
|
});
|
|
662
795
|
this.#socket = ws;
|
|
663
796
|
this.#bind(ws);
|
|
664
|
-
|
|
665
|
-
settled = true;
|
|
666
|
-
resolve();
|
|
667
|
-
}
|
|
797
|
+
resolve();
|
|
668
798
|
});
|
|
669
799
|
request.on("response", (response) => {
|
|
670
800
|
response.resume();
|
|
671
|
-
|
|
801
|
+
reject(/* @__PURE__ */ new Error(`WebSocket upgrade declined with status ${response.statusCode ?? 0}`));
|
|
672
802
|
});
|
|
673
|
-
request.on("error", (error) =>
|
|
803
|
+
request.on("error", (error) => reject(error instanceof Error ? error : new Error(String(error))));
|
|
674
804
|
request.end();
|
|
675
805
|
});
|
|
676
806
|
}
|
|
677
807
|
async send(message) {
|
|
678
808
|
const socket = this.#socket;
|
|
679
809
|
if (socket === void 0) throw new Error("WebSocket transport is not connected");
|
|
680
|
-
|
|
681
|
-
for (const one of messages) socket.send(JSON.stringify(one));
|
|
810
|
+
socket.send(JSON.stringify(message));
|
|
682
811
|
}
|
|
683
812
|
async close() {
|
|
684
813
|
if (this.#closed) return;
|
|
@@ -742,8 +871,8 @@ var WebSocketClientTransport = class {
|
|
|
742
871
|
* {@link dispatchLines} helper — a well-formed {@link JSONRPCMessage} emits
|
|
743
872
|
* `message`, a malformed line emits `error` (§14, never throws). The child's
|
|
744
873
|
* `close` bridges to this transport's `close`.
|
|
745
|
-
* - **Outbound (`send`).** `send(message
|
|
746
|
-
* `JSON.stringify`d line
|
|
874
|
+
* - **Outbound (`send`).** `send(message)` writes one newline-terminated
|
|
875
|
+
* `JSON.stringify`d line to the child's `stdin`.
|
|
747
876
|
* - **`close()`** kills the child process and fires `close` (idempotent).
|
|
748
877
|
* - **Observable (§13).** Owns the `emitter` ({@link ClientTransportEventMap}); the
|
|
749
878
|
* emitter isolates a listener throw; `error` is a DOMAIN event (a transport-level
|
|
@@ -794,8 +923,7 @@ var StdioClientTransport = class {
|
|
|
794
923
|
async send(message) {
|
|
795
924
|
const child = this.#child;
|
|
796
925
|
if (child === void 0) throw new Error("stdio transport is not connected");
|
|
797
|
-
|
|
798
|
-
for (const one of messages) child.stdin.write(`${JSON.stringify(one)}\n`);
|
|
926
|
+
child.stdin.write(`${JSON.stringify(message)}\n`);
|
|
799
927
|
}
|
|
800
928
|
async close() {
|
|
801
929
|
if (this.#closed) return;
|
|
@@ -838,8 +966,8 @@ var StdioClientTransport = class {
|
|
|
838
966
|
* well-formed {@link JSONRPCMessage} re-emits on `message`, a malformed line
|
|
839
967
|
* emits `error` (§14, never throws). `input`'s `close` bridges to this
|
|
840
968
|
* transport's `close`.
|
|
841
|
-
* - **Outbound (`send`).** `send(message
|
|
842
|
-
* `JSON.stringify`d line
|
|
969
|
+
* - **Outbound (`send`).** `send(message)` writes one newline-terminated
|
|
970
|
+
* `JSON.stringify`d line to `output`.
|
|
843
971
|
* - **`close()`** fires this transport's `close` (idempotent) — the injected streams
|
|
844
972
|
* are owned by the caller (typically `process.stdin`/`process.stdout`, which must
|
|
845
973
|
* never be closed out from under the process) and are not torn down here.
|
|
@@ -871,8 +999,7 @@ var StdioServerTransport = class {
|
|
|
871
999
|
this.#input.on("error", (error) => this.#emitter.emit("error", error));
|
|
872
1000
|
}
|
|
873
1001
|
async send(message) {
|
|
874
|
-
|
|
875
|
-
for (const one of messages) this.#output.write(`${JSON.stringify(one)}\n`);
|
|
1002
|
+
this.#output.write(`${JSON.stringify(message)}\n`);
|
|
876
1003
|
}
|
|
877
1004
|
async close() {
|
|
878
1005
|
if (this.#closed) return;
|
|
@@ -907,6 +1034,9 @@ var StdioServerTransport = class {
|
|
|
907
1034
|
* - A **transport** failure — a malformed JSON body, or a parsed value that is not a
|
|
908
1035
|
* JSON-RPC REQUEST — is an HTTP `400` carrying a JSON-RPC error BODY (`-32700` Parse
|
|
909
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.
|
|
910
1040
|
* - A **dispatch** result — a success OR an IN-BAND JSON-RPC error from `mcp.dispatch`
|
|
911
1041
|
* (e.g. `-32601` method-not-found) — is an HTTP `200` carrying the JSON-RPC response
|
|
912
1042
|
* envelope (the error is in-band per JSON-RPC, NOT an HTTP error).
|
|
@@ -943,37 +1073,11 @@ var StdioServerTransport = class {
|
|
|
943
1073
|
* ```
|
|
944
1074
|
*/
|
|
945
1075
|
function createMCPRoutes(mcp, options) {
|
|
946
|
-
const path = options?.path ?? "/mcp";
|
|
947
|
-
const streaming = options?.streaming ?? true;
|
|
948
1076
|
return [{
|
|
949
1077
|
method: "POST",
|
|
950
|
-
path,
|
|
1078
|
+
path: options?.path ?? "/mcp",
|
|
951
1079
|
name: "mcp",
|
|
952
|
-
handler:
|
|
953
|
-
let text;
|
|
954
|
-
try {
|
|
955
|
-
text = await request.text();
|
|
956
|
-
} catch {
|
|
957
|
-
return Response.json(jsonRPCError(null, JSONRPC_PARSE_ERROR, "Parse error"), { status: 400 });
|
|
958
|
-
}
|
|
959
|
-
let parsed;
|
|
960
|
-
try {
|
|
961
|
-
parsed = JSON.parse(text);
|
|
962
|
-
} catch {
|
|
963
|
-
return Response.json(jsonRPCError(null, JSONRPC_PARSE_ERROR, "Parse error"), { status: 400 });
|
|
964
|
-
}
|
|
965
|
-
const rpcRequest = parseJSONRPCMessage(parsed);
|
|
966
|
-
if (rpcRequest === void 0 || !("method" in rpcRequest)) return Response.json(jsonRPCError(null, JSONRPC_INVALID_REQUEST, "Invalid Request"), { status: 400 });
|
|
967
|
-
const response = await mcp.dispatch(rpcRequest);
|
|
968
|
-
if (response === void 0) return new Response(null, { status: 202 });
|
|
969
|
-
if (streaming && acceptsEventStream(request)) {
|
|
970
|
-
const s = openStream();
|
|
971
|
-
s.write({ data: JSON.stringify(response) });
|
|
972
|
-
s.end();
|
|
973
|
-
return s.response;
|
|
974
|
-
}
|
|
975
|
-
return Response.json(response);
|
|
976
|
-
}
|
|
1080
|
+
handler: createMCPPostHandler(mcp, options?.streaming ?? true)
|
|
977
1081
|
}];
|
|
978
1082
|
}
|
|
979
1083
|
/**
|
|
@@ -989,8 +1093,10 @@ function createMCPRoutes(mcp, options) {
|
|
|
989
1093
|
* and the reply is surfaced on the transport's `message` event for the client's id
|
|
990
1094
|
* correlation. Add `options.headers` (e.g. an `Authorization` bearer) to reach a guarded
|
|
991
1095
|
* server. `start` / `close` hold no connection; against a STATEFUL server it captures the
|
|
992
|
-
* `mcp-session-id` from `initialize` and echoes it on later requests
|
|
993
|
-
*
|
|
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.
|
|
994
1100
|
*
|
|
995
1101
|
* @param options - `url` (the remote endpoint; REQUIRED), optional `headers` merged onto
|
|
996
1102
|
* every request, optional `fetch` (default `globalThis.fetch`), and optional `timeout`
|
|
@@ -1030,12 +1136,13 @@ function createHTTPClientTransport(options) {
|
|
|
1030
1136
|
* - **Claims (returns `true`)** otherwise: it builds `createNodeWebSocket({ socket, key, head,
|
|
1031
1137
|
* protocol })` (SERVER mode → writes the `101` handshake, echoing the `subprotocol`, default
|
|
1032
1138
|
* {@link MCP_WEBSOCKET_SUBPROTOCOL} `'mcp'`, and sends UNMASKED frames), wraps it in a
|
|
1033
|
-
* {@link WebSocketServerTransport}, and
|
|
1034
|
-
* import('@src/core').
|
|
1035
|
-
*
|
|
1036
|
-
*
|
|
1037
|
-
*
|
|
1038
|
-
*
|
|
1139
|
+
* {@link WebSocketServerTransport}, and pipes it through the core {@link
|
|
1140
|
+
* import('@src/core').MCPTransportInterface} port via {@link
|
|
1141
|
+
* import('./helpers.js').bridgeMessageTransport} + {@link import('@src/core').bindServer}:
|
|
1142
|
+
* each inbound REQUEST runs through `mcp.dispatch`, and a defined response is written back
|
|
1143
|
+
* as a frame — a NOTIFICATION sends nothing, and a non-request message (a stray response) is
|
|
1144
|
+
* ignored. A `dispatch` / `send` fault surfaces on `mcp.emitter`'s `error` event rather than
|
|
1145
|
+
* escaping the (async) message pump.
|
|
1039
1146
|
*
|
|
1040
1147
|
* It is MECHANISM, not policy: compose an auth guard IN FRONT by registering an upgrade
|
|
1041
1148
|
* handler BEFORE this one — that handler can claim (decline + destroy) an unauthenticated
|
|
@@ -1072,19 +1179,7 @@ function createWebSocketServer(mcp, options) {
|
|
|
1072
1179
|
head,
|
|
1073
1180
|
protocol: subprotocol
|
|
1074
1181
|
}));
|
|
1075
|
-
|
|
1076
|
-
if (!isJSONRPCRequest(message)) return;
|
|
1077
|
-
(async () => {
|
|
1078
|
-
try {
|
|
1079
|
-
const response = await mcp.dispatch(message);
|
|
1080
|
-
if (response !== void 0) await transport.send(response);
|
|
1081
|
-
} catch (error) {
|
|
1082
|
-
try {
|
|
1083
|
-
transport.emitter.emit("error", error);
|
|
1084
|
-
} catch {}
|
|
1085
|
-
}
|
|
1086
|
-
})();
|
|
1087
|
-
});
|
|
1182
|
+
bindServer(mcp, bridgeMessageTransport(transport));
|
|
1088
1183
|
transport.start();
|
|
1089
1184
|
return true;
|
|
1090
1185
|
};
|
|
@@ -1166,12 +1261,13 @@ function createStdioClientTransport(options) {
|
|
|
1166
1261
|
* @remarks
|
|
1167
1262
|
* Wraps `options.input` (default `process.stdin`) / `options.output` (default
|
|
1168
1263
|
* `process.stdout`) in a {@link import('./transports/StdioServerTransport.js').StdioServerTransport}
|
|
1169
|
-
* and
|
|
1170
|
-
*
|
|
1171
|
-
*
|
|
1172
|
-
*
|
|
1173
|
-
*
|
|
1174
|
-
* escaping the (async) message
|
|
1264
|
+
* and pipes it through the core {@link import('@src/core').MCPTransportInterface} port
|
|
1265
|
+
* via {@link import('./helpers.js').bridgeMessageTransport} + {@link
|
|
1266
|
+
* import('@src/core').bindServer}: each inbound REQUEST runs through `mcp.dispatch`, and
|
|
1267
|
+
* a defined response is written back as a newline-terminated line — a NOTIFICATION
|
|
1268
|
+
* writes nothing, and a non-request message is ignored. A `dispatch` / `send` fault
|
|
1269
|
+
* surfaces on `mcp.emitter`'s `error` event rather than escaping the (async) message
|
|
1270
|
+
* pump.
|
|
1175
1271
|
*
|
|
1176
1272
|
* @param mcp - The transport-agnostic {@link MCPServerInterface} to expose over stdio
|
|
1177
1273
|
* @param options - Optional injectable `input` / `output` streams; see
|
|
@@ -1189,19 +1285,7 @@ function createStdioClientTransport(options) {
|
|
|
1189
1285
|
*/
|
|
1190
1286
|
function createStdioServer(mcp, options) {
|
|
1191
1287
|
const transport = new StdioServerTransport(options?.input ?? process.stdin, options?.output ?? process.stdout);
|
|
1192
|
-
|
|
1193
|
-
if (!isJSONRPCRequest(message)) return;
|
|
1194
|
-
(async () => {
|
|
1195
|
-
try {
|
|
1196
|
-
const response = await mcp.dispatch(message);
|
|
1197
|
-
if (response !== void 0) await transport.send(response);
|
|
1198
|
-
} catch (error) {
|
|
1199
|
-
try {
|
|
1200
|
-
transport.emitter.emit("error", error);
|
|
1201
|
-
} catch {}
|
|
1202
|
-
}
|
|
1203
|
-
})();
|
|
1204
|
-
});
|
|
1288
|
+
bindServer(mcp, bridgeMessageTransport(transport));
|
|
1205
1289
|
return {
|
|
1206
1290
|
start() {
|
|
1207
1291
|
transport.start();
|
|
@@ -1275,30 +1359,43 @@ function createMCPSession(options) {
|
|
|
1275
1359
|
const store = /* @__PURE__ */ new Map();
|
|
1276
1360
|
return async (request, context, next) => {
|
|
1277
1361
|
if (context.url.pathname !== path) return next();
|
|
1278
|
-
|
|
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
|
+
}
|
|
1279
1383
|
if (context.method === "GET") {
|
|
1280
|
-
const entry = resolve(request);
|
|
1281
1384
|
if (entry === void 0) return rejectUnknownSession();
|
|
1385
|
+
const session = entry.session;
|
|
1282
1386
|
const stream = openStream();
|
|
1283
1387
|
stream.comment("open");
|
|
1284
1388
|
const lastEventId = readLastEventId(request);
|
|
1285
|
-
if (lastEventId !== void 0) for (const e of
|
|
1389
|
+
if (lastEventId !== void 0) for (const e of session.replay(lastEventId)) stream.write({
|
|
1286
1390
|
id: e.id,
|
|
1287
1391
|
data: JSON.stringify(e.message)
|
|
1288
1392
|
});
|
|
1289
|
-
|
|
1290
|
-
if (request.signal.aborted)
|
|
1291
|
-
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 });
|
|
1292
1396
|
return stream.response;
|
|
1293
1397
|
}
|
|
1294
|
-
if (context.method === "DELETE") {
|
|
1295
|
-
const id = readSessionHeader(request);
|
|
1296
|
-
if (id === void 0 || !store.has(id)) return rejectUnknownSession();
|
|
1297
|
-
store.delete(id);
|
|
1298
|
-
return new Response(null, { status: 204 });
|
|
1299
|
-
}
|
|
1300
1398
|
const text = await request.text();
|
|
1301
|
-
let entry = resolve(request);
|
|
1302
1399
|
if (entry === void 0) {
|
|
1303
1400
|
let parsed;
|
|
1304
1401
|
try {
|
|
@@ -1307,7 +1404,7 @@ function createMCPSession(options) {
|
|
|
1307
1404
|
parsed = void 0;
|
|
1308
1405
|
}
|
|
1309
1406
|
if (parsed !== void 0 && isInitializeRequest(parsed)) {
|
|
1310
|
-
const session = new MCPSession(crypto.randomUUID(), { capacity });
|
|
1407
|
+
const session = new MCPSession(crypto.randomUUID(), capacity !== void 0 ? { capacity } : {});
|
|
1311
1408
|
entry = {
|
|
1312
1409
|
session,
|
|
1313
1410
|
touched: clock()
|
|
@@ -1315,7 +1412,7 @@ function createMCPSession(options) {
|
|
|
1315
1412
|
store.set(session.id, entry);
|
|
1316
1413
|
} else return rejectUnknownSession();
|
|
1317
1414
|
}
|
|
1318
|
-
context.state
|
|
1415
|
+
if (!Reflect.set(context.state, "session", entry.session)) throw new Error("MCP session state is not writable");
|
|
1319
1416
|
const response = await next(new Request(context.url, {
|
|
1320
1417
|
method: "POST",
|
|
1321
1418
|
headers: request.headers,
|
|
@@ -1324,21 +1421,8 @@ function createMCPSession(options) {
|
|
|
1324
1421
|
response.headers.set(MCP_SESSION_HEADER, entry.session.id);
|
|
1325
1422
|
return response;
|
|
1326
1423
|
};
|
|
1327
|
-
function resolve(request) {
|
|
1328
|
-
const id = readSessionHeader(request);
|
|
1329
|
-
if (id === void 0) return void 0;
|
|
1330
|
-
const entry = store.get(id);
|
|
1331
|
-
if (entry === void 0) return void 0;
|
|
1332
|
-
entry.touched = clock();
|
|
1333
|
-
return entry;
|
|
1334
|
-
}
|
|
1335
|
-
function sweep() {
|
|
1336
|
-
if (ttl === void 0) return;
|
|
1337
|
-
const cutoff = clock() - ttl;
|
|
1338
|
-
for (const [id, entry] of store) if (entry.touched <= cutoff) store.delete(id);
|
|
1339
|
-
}
|
|
1340
1424
|
}
|
|
1341
1425
|
//#endregion
|
|
1342
|
-
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, 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 };
|
|
1343
1427
|
|
|
1344
1428
|
//# sourceMappingURL=index.js.map
|