@orkestrel/mcp 0.0.19 → 0.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -22
- package/dist/src/browser/index.d.ts +92 -70
- package/dist/src/browser/index.js +164 -81
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/core/index.cjs +269 -257
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +351 -358
- package/dist/src/core/index.d.ts +351 -358
- package/dist/src/core/index.js +270 -257
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +331 -173
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +266 -146
- package/dist/src/server/index.d.ts +266 -146
- package/dist/src/server/index.js +331 -173
- package/dist/src/server/index.js.map +1 -1
- package/package.json +13 -10
|
@@ -10,6 +10,7 @@ let node_https = require("node:https");
|
|
|
10
10
|
let _orkestrel_websocket = require("@orkestrel/websocket");
|
|
11
11
|
let _orkestrel_process_server = require("@orkestrel/process/server");
|
|
12
12
|
let _orkestrel_process = require("@orkestrel/process");
|
|
13
|
+
let node_stream = require("node:stream");
|
|
13
14
|
//#region src/server/constants.ts
|
|
14
15
|
/**
|
|
15
16
|
* The Streamable-HTTP transport header that carries the MCP session id. When a {@link
|
|
@@ -87,7 +88,7 @@ var DEFAULT_MCP_SESSION_TTL = 3e5;
|
|
|
87
88
|
//#endregion
|
|
88
89
|
//#region src/server/helpers.ts
|
|
89
90
|
/**
|
|
90
|
-
*
|
|
91
|
+
* Creates a readable stream from its pull and cancellation behaviours.
|
|
91
92
|
*
|
|
92
93
|
* @param pull - The behaviour that supplies the stream's next chunk
|
|
93
94
|
* @param cancel - The behaviour that releases the stream after consumer cancellation
|
|
@@ -100,12 +101,12 @@ function createReadableStream(pull, cancel) {
|
|
|
100
101
|
});
|
|
101
102
|
}
|
|
102
103
|
/**
|
|
103
|
-
*
|
|
104
|
+
* Pumps a controlled held-open exchange onto an open SSE stream — one `data:` event per
|
|
104
105
|
* notification in order, then the terminating response — and END the exchange however the
|
|
105
106
|
* pump leaves.
|
|
106
107
|
*
|
|
107
108
|
* @remarks
|
|
108
|
-
* The Streamable-HTTP twin of {@link import('@
|
|
109
|
+
* The Streamable-HTTP twin of {@link import('@orkestrel/mcp').sendStream}, and it owns exactly what
|
|
109
110
|
* that owns. The `finally` releases the exchange on EVERY exit — the normal terminal, a
|
|
110
111
|
* producer that threw, a `write` that threw, and an abort alike — because nothing else will:
|
|
111
112
|
* a request whose client vanished cancels nothing by itself, so an exchange this pump walks
|
|
@@ -113,7 +114,7 @@ function createReadableStream(pull, cancel) {
|
|
|
113
114
|
* The exchange is released BEFORE the body ends, so the slot is already back when the response
|
|
114
115
|
* completes.
|
|
115
116
|
*
|
|
116
|
-
* Total
|
|
117
|
+
* Total — never throws and never rejects. A held-open SSE response has already sent its
|
|
117
118
|
* headers and part of its body, so there is no failure the transport could still convert into
|
|
118
119
|
* a different answer; the honest end of a broken stream is a closed one, and the fault itself
|
|
119
120
|
* is already legible on `server.emitter`'s `error` event, which is where a contained fault
|
|
@@ -195,7 +196,7 @@ function allowsOrigin(request, options) {
|
|
|
195
196
|
return options?.origins?.includes(parsed.origin) ?? false;
|
|
196
197
|
}
|
|
197
198
|
/**
|
|
198
|
-
*
|
|
199
|
+
* Reads the request's `mcp-session-id` header — the session id a stateful transport
|
|
199
200
|
* validates, or `undefined` when absent.
|
|
200
201
|
*
|
|
201
202
|
* @remarks
|
|
@@ -213,7 +214,7 @@ function readSessionHeader(request) {
|
|
|
213
214
|
return id === null ? void 0 : id;
|
|
214
215
|
}
|
|
215
216
|
/**
|
|
216
|
-
*
|
|
217
|
+
* Reads the request's `Last-Event-ID` header — the SSE resume cursor a client sends when it
|
|
217
218
|
* reconnects to the resumable `GET {path}` stream, or `undefined` when absent.
|
|
218
219
|
*
|
|
219
220
|
* @remarks
|
|
@@ -231,7 +232,7 @@ function readLastEventId(request) {
|
|
|
231
232
|
return id === null ? void 0 : id;
|
|
232
233
|
}
|
|
233
234
|
/**
|
|
234
|
-
*
|
|
235
|
+
* Builds the stateful transport's "unknown session" rejection — an HTTP `404` carrying a
|
|
235
236
|
* JSON-RPC error body.
|
|
236
237
|
*
|
|
237
238
|
* @remarks
|
|
@@ -249,16 +250,16 @@ function rejectUnknownSession() {
|
|
|
249
250
|
return Response.json((0, _src_core.buildJSONRPCError)(void 0, _src_core.JSONRPC_INVALID_REQUEST, "Session not found"), { status: 404 });
|
|
250
251
|
}
|
|
251
252
|
/**
|
|
252
|
-
*
|
|
253
|
+
* Decodes a `fetch` Response's Server-Sent-Events body into the JSON-RPC messages it
|
|
253
254
|
* carried — the CLIENT-side inverse of the server's Streamable-HTTP SSE response.
|
|
254
255
|
*
|
|
255
256
|
* @remarks
|
|
256
257
|
* Reads the whole `response.body` stream chunk-by-chunk through a `TextDecoder({
|
|
257
258
|
* stream: true })` (handling a multi-byte char split across reads) and `@orkestrel/sse`'s
|
|
258
259
|
* {@link SSEParserInterface} (handling a partial line / in-progress event split across
|
|
259
|
-
* reads), then narrows each dispatched event's `data` to a {@link JSONRPCMessage}
|
|
260
|
+
* reads), then narrows each dispatched event's `data` to a {@link JSONRPCMessage} with
|
|
260
261
|
* `parseJSONRPCMessage` (so a non-message / non-JSON `data:` event is DROPPED, never
|
|
261
|
-
* thrown — total
|
|
262
|
+
* thrown — total). It reuses the SAME `SSEParser` the server's `openStream` seam
|
|
262
263
|
* serializes against, so the wire round-trips. A `null` body (no stream) yields no
|
|
263
264
|
* messages; the {@link import('./transports/HTTPClientTransport.js').HTTPClientTransport}
|
|
264
265
|
* reads a request/response SSE reply (the server sends one `data:` event then ends), so
|
|
@@ -289,13 +290,13 @@ async function readEventStream(response) {
|
|
|
289
290
|
return messages;
|
|
290
291
|
}
|
|
291
292
|
/**
|
|
292
|
-
*
|
|
293
|
+
* Decodes one SSE event's `data` string into a {@link JSONRPCMessage}, or `undefined`
|
|
293
294
|
* when it is not one — the per-event step {@link readEventStream} folds over.
|
|
294
295
|
*
|
|
295
296
|
* @remarks
|
|
296
297
|
* `JSON.parse`s the `data` (the server serializes the JSON-RPC envelope as the event's
|
|
297
298
|
* `data`) inside a try/catch and narrows the parsed value with `parseJSONRPCMessage`.
|
|
298
|
-
* Total
|
|
299
|
+
* Total: malformed JSON or a non-message value yields `undefined`, never throws.
|
|
299
300
|
*
|
|
300
301
|
* @param data - One SSE event's `data` payload
|
|
301
302
|
* @returns The decoded {@link JSONRPCMessage}, or `undefined`
|
|
@@ -308,13 +309,13 @@ function decodeEvent(data) {
|
|
|
308
309
|
}
|
|
309
310
|
}
|
|
310
311
|
/**
|
|
311
|
-
*
|
|
312
|
+
* Reads the path (without the query string) of a raw `node:http` protocol-upgrade request —
|
|
312
313
|
* the `createWebSocketServer` upgrade-path match.
|
|
313
314
|
*
|
|
314
315
|
* @remarks
|
|
315
316
|
* A `node:http` {@link import('node:http').IncomingMessage}'s `url` is the request TARGET
|
|
316
|
-
* (`'/mcp?x=1'`), narrowed with `isString` (
|
|
317
|
-
* absent target; it is parsed against a
|
|
317
|
+
* (`'/mcp?x=1'`), narrowed with `isString` (never `as`) and defaulting to `'/'` for an
|
|
318
|
+
* absent target; it is parsed against a placeholder base (only the pathname matters for the upgrade
|
|
318
319
|
* decision) and the `pathname` returned. The upgrade handler compares this against its
|
|
319
320
|
* configured `path` to decide whether to claim the socket. Total — never throws on an
|
|
320
321
|
* adversarial / absent target.
|
|
@@ -327,7 +328,7 @@ function upgradeRequestPath(request) {
|
|
|
327
328
|
return new URL(target, "http://localhost").pathname;
|
|
328
329
|
}
|
|
329
330
|
/**
|
|
330
|
-
*
|
|
331
|
+
* Folds one more chunk of raw stdio bytes into a newline-framed buffer — the shared
|
|
331
332
|
* line-framing step both stdio transports (client and server) read their inbound
|
|
332
333
|
* newline-delimited JSON-RPC messages through.
|
|
333
334
|
*
|
|
@@ -352,19 +353,20 @@ function extractLines(buffer, chunk) {
|
|
|
352
353
|
};
|
|
353
354
|
}
|
|
354
355
|
/**
|
|
355
|
-
*
|
|
356
|
+
* Decodes and delivers each complete newline-framed line onto a {@link
|
|
356
357
|
* MCPClientTransportEventMap} emitter — the shared per-chunk dispatch step both stdio
|
|
357
|
-
* transports
|
|
358
|
+
* transports run their framed lines through: the server transport frames with {@link
|
|
359
|
+
* extractLines}, the client transport takes its lines from the process supervisor.
|
|
358
360
|
*
|
|
359
361
|
* @remarks
|
|
360
362
|
* A blank line is skipped (a stray trailing newline). Every other line is decoded
|
|
361
363
|
* with {@link decodeEvent} (`JSON.parse` + `parseJSONRPCMessage`, guarded); a
|
|
362
364
|
* well-formed {@link JSONRPCMessage} emits `message`, a malformed / non-message line
|
|
363
|
-
* emits `error` (
|
|
365
|
+
* emits `error` (total, never throws). Pure w.r.t. its own state — the emit is
|
|
364
366
|
* the caller-owned side effect.
|
|
365
367
|
*
|
|
366
368
|
* @param emitter - The transport's {@link EmitterInterface} to emit `message` / `error` onto
|
|
367
|
-
* @param lines - The complete lines
|
|
369
|
+
* @param lines - The complete lines to decode and deliver
|
|
368
370
|
*/
|
|
369
371
|
function dispatchLines(emitter, lines) {
|
|
370
372
|
for (const line of lines) {
|
|
@@ -378,17 +380,17 @@ function dispatchLines(emitter, lines) {
|
|
|
378
380
|
}
|
|
379
381
|
}
|
|
380
382
|
/**
|
|
381
|
-
*
|
|
383
|
+
* Bridges a message-channel {@link MCPClientTransportInterface} (the shape the stdio and
|
|
382
384
|
* WebSocket SERVER transports already implement) into the environment-agnostic
|
|
383
|
-
* {@link import('@
|
|
385
|
+
* {@link import('@orkestrel/mcp').MCPTransportInterface} port — the adapter
|
|
384
386
|
* {@link import('./factories.js').createStdioServer} and {@link
|
|
385
387
|
* import('./factories.js').createWebSocketServer} pipe through `bindServer`, so the
|
|
386
|
-
* request/reply/error pump those
|
|
388
|
+
* request/reply/error pump those factories used to hand-roll identically now
|
|
387
389
|
* lives ONCE in the core binder.
|
|
388
390
|
*
|
|
389
391
|
* @remarks
|
|
390
392
|
* `send` decodes the already-serialized reply string back to a {@link JSONRPCMessage}
|
|
391
|
-
* and writes it
|
|
393
|
+
* and writes it through `transport.send` (the same `JSON.stringify` the underlying
|
|
392
394
|
* transport already performs, so the wire bytes are unchanged). `listen` filters
|
|
393
395
|
* `transport`'s `message` event to INVOCATIONS ONLY — requests and notifications, never a
|
|
394
396
|
* stray response, exactly as the prior hand-rolled pumps did — and re-serializes each one
|
|
@@ -406,24 +408,24 @@ function dispatchLines(emitter, lines) {
|
|
|
406
408
|
* decode rather than after it. Removing the cost means giving `MCPTransportInterface` a
|
|
407
409
|
* message-shaped face beside its string one, which every transport would then carry.
|
|
408
410
|
*
|
|
409
|
-
* @remarks Per {@link import('@
|
|
411
|
+
* @remarks Per {@link import('@orkestrel/mcp').MCPTransportInterface}, `listen`/`closed`
|
|
410
412
|
* each hold THE SINGLE current handler (a second call REPLACES the first, never adds).
|
|
411
|
-
*
|
|
413
|
+
* Because the underlying `transport.emitter` is ADD-based (`on` subscribes, never
|
|
412
414
|
* replaces), this bridge installs ONE stable emitter listener per event on first use
|
|
413
|
-
* and re-routes it to whichever handler is
|
|
415
|
+
* and re-routes it to whichever handler is active (`undefined` while
|
|
414
416
|
* none is), so rebinding never double-dispatches.
|
|
415
417
|
*
|
|
416
|
-
* @remarks A response whose `result` serializes away (
|
|
418
|
+
* @remarks A response whose `result` serializes away (for example, `undefined`) is dropped by
|
|
417
419
|
* the message validators on the wire's decode side — an asymmetry the stdio/WS carrier
|
|
418
|
-
* shares with the streamable-HTTP face,
|
|
420
|
+
* shares with the streamable-HTTP face, because both round-trip through `JSON.stringify`
|
|
419
421
|
* / `JSON.parse` before re-validation.
|
|
420
422
|
*
|
|
421
423
|
* @param transport - The message-channel transport to bridge (stdio or WebSocket)
|
|
422
|
-
* @returns An {@link import('@
|
|
424
|
+
* @returns An {@link import('@orkestrel/mcp').MCPTransportInterface} `bindServer` can drive
|
|
423
425
|
*
|
|
424
426
|
* @example
|
|
425
427
|
* ```ts
|
|
426
|
-
* import { bindServer } from '@
|
|
428
|
+
* import { bindServer } from '@orkestrel/mcp'
|
|
427
429
|
*
|
|
428
430
|
* const transport = new StdioServerTransport(process.stdin, process.stdout)
|
|
429
431
|
* bindServer(mcp, bridgeMessageTransport(transport))
|
|
@@ -459,7 +461,7 @@ function bridgeMessageTransport(transport) {
|
|
|
459
461
|
//#endregion
|
|
460
462
|
//#region src/server/inferers.ts
|
|
461
463
|
/**
|
|
462
|
-
*
|
|
464
|
+
* Infers the first required MCP HTTP header that is missing or mismatched.
|
|
463
465
|
*
|
|
464
466
|
* @remarks
|
|
465
467
|
* A modern request derives its protocol, method, and tools/call-only name expectations from
|
|
@@ -540,7 +542,7 @@ function inferHeaderIssue(request, reference) {
|
|
|
540
542
|
};
|
|
541
543
|
}
|
|
542
544
|
/**
|
|
543
|
-
*
|
|
545
|
+
* Infers the legacy revision an `initialize` request negotiates.
|
|
544
546
|
*
|
|
545
547
|
* @remarks
|
|
546
548
|
* A supported legacy request is pinned exactly. A modern, malformed, absent, or unsupported
|
|
@@ -556,7 +558,7 @@ function inferLegacyVersion(request) {
|
|
|
556
558
|
return _src_core.MCP_PROTOCOL_VERSION;
|
|
557
559
|
}
|
|
558
560
|
/**
|
|
559
|
-
*
|
|
561
|
+
* Infers the HTTP status for one MCP dispatch outcome without changing its JSON-RPC body.
|
|
560
562
|
*
|
|
561
563
|
* @remarks
|
|
562
564
|
* Notifications are accepted with `202`. Legacy response envelopes retain uniform `200`
|
|
@@ -578,7 +580,7 @@ function inferStatus(response, era) {
|
|
|
578
580
|
//#endregion
|
|
579
581
|
//#region src/server/transports/HTTPDisconnect.ts
|
|
580
582
|
/**
|
|
581
|
-
*
|
|
583
|
+
* Composes one incoming HTTP request lifetime with one MCP-owned SSE response lifetime.
|
|
582
584
|
*
|
|
583
585
|
* @remarks
|
|
584
586
|
* The composed {@link signal} observes request abort and EVERY way this response can end
|
|
@@ -592,7 +594,8 @@ function inferStatus(response, era) {
|
|
|
592
594
|
*
|
|
593
595
|
* {@link bridge} preserves the source response status and headers, forwards its body bytes, and
|
|
594
596
|
* owns keepalive comments plus listener/timer cleanup until upstream completion, request abort,
|
|
595
|
-
* or consumer cancellation. This is a single-response lifecycle object, not a reusable bridge
|
|
597
|
+
* or consumer cancellation. This is a single-response lifecycle object, not a reusable bridge:
|
|
598
|
+
* a second {@link bridge} call THROWS rather than arming a second keepalive over one lifecycle.
|
|
596
599
|
* It supplies no handler or session policy.
|
|
597
600
|
*
|
|
598
601
|
* The keepalive interval is a BUDGET, sanitized like every other numeric knob in this package:
|
|
@@ -617,9 +620,10 @@ var HTTPDisconnect = class {
|
|
|
617
620
|
#interval;
|
|
618
621
|
#signal;
|
|
619
622
|
#timer;
|
|
623
|
+
#bridged = false;
|
|
620
624
|
#pulling = false;
|
|
621
625
|
/**
|
|
622
|
-
*
|
|
626
|
+
* Creates the lifecycle composition for one request and its future SSE response.
|
|
623
627
|
*
|
|
624
628
|
* @param signal - The incoming request signal
|
|
625
629
|
* @param options - Optional keepalive `interval` in milliseconds; an invalid value falls back
|
|
@@ -640,7 +644,7 @@ var HTTPDisconnect = class {
|
|
|
640
644
|
return this.#signal;
|
|
641
645
|
}
|
|
642
646
|
/**
|
|
643
|
-
*
|
|
647
|
+
* Bridges one open SSE response through cancellation-aware byte forwarding and keepalives.
|
|
644
648
|
*
|
|
645
649
|
* Consumer cancellation, a read failure while forwarding, and a keepalive tick that finds the
|
|
646
650
|
* SSE stream already closed each abort {@link signal}; consumer cancellation also cancels the
|
|
@@ -649,9 +653,12 @@ var HTTPDisconnect = class {
|
|
|
649
653
|
*
|
|
650
654
|
* @param stream - The open SSE stream whose response will be consumed by the HTTP writer
|
|
651
655
|
* @returns A one-use response preserving status, status text, headers, and SSE body bytes
|
|
652
|
-
* @throws When the supplied SSE response
|
|
656
|
+
* @throws When this disconnect has already bridged a stream, or the supplied SSE response
|
|
657
|
+
* has no body
|
|
653
658
|
*/
|
|
654
659
|
bridge(stream) {
|
|
660
|
+
if (this.#bridged) throw new Error("MCP SSE response is already bridged");
|
|
661
|
+
this.#bridged = true;
|
|
655
662
|
const response = stream.response;
|
|
656
663
|
const body = response.body;
|
|
657
664
|
if (body === null) throw new Error("MCP SSE response has no body");
|
|
@@ -705,7 +712,7 @@ var HTTPDisconnect = class {
|
|
|
705
712
|
//#endregion
|
|
706
713
|
//#region src/server/handlers.ts
|
|
707
714
|
/**
|
|
708
|
-
*
|
|
715
|
+
* Creates the Streamable-HTTP POST handler used by `createMCPRoutes`.
|
|
709
716
|
*
|
|
710
717
|
* @remarks
|
|
711
718
|
* Modern requests require matching protocol/method headers and a matching name header only
|
|
@@ -731,7 +738,7 @@ var HTTPDisconnect = class {
|
|
|
731
738
|
* import { createToolManager } from '@orkestrel/tool'
|
|
732
739
|
*
|
|
733
740
|
* const mcp = createMCPServer({ identity: { name: 'docs', version: '1.0.0' }, tools: createToolManager() })
|
|
734
|
-
* const handler = createMCPPostHandler(createMCPLegacy(mcp), { streaming: true })
|
|
741
|
+
* const handler = createMCPPostHandler(createMCPLegacy(mcp), { streaming: true }) // answers `initialize` too; pass `mcp` alone for modern-only
|
|
735
742
|
* await handler(new Request('http://localhost/mcp', {
|
|
736
743
|
* method: 'POST',
|
|
737
744
|
* body: '{"jsonrpc":"2.0","method":"ping","id":1}',
|
|
@@ -806,12 +813,12 @@ function createMCPPostHandler(mcp, options) {
|
|
|
806
813
|
* - **Request/response over `fetch`.** `send(message)` POSTs the JSON-serialized
|
|
807
814
|
* message to `options.url` with `content-type: application/json` and an
|
|
808
815
|
* `Accept` of BOTH `application/json` and `text/event-stream` (so the server may
|
|
809
|
-
* answer with either framing) — plus any `options.headers` (
|
|
816
|
+
* answer with either framing) — plus any `options.headers` (for example, an `Authorization`
|
|
810
817
|
* bearer). It then decodes the reply and emits each decoded {@link JSONRPCMessage} on
|
|
811
|
-
* the `message` event the {@link import('@
|
|
818
|
+
* the `message` event the {@link import('@orkestrel/mcp').MCPClientInterface} subscribes
|
|
812
819
|
* to.
|
|
813
820
|
* - **Both reply framings.** A `200` with an `application/json` body is parsed with
|
|
814
|
-
* `parseJSONRPCMessage`; a `200` with a `text/event-stream` body is decoded
|
|
821
|
+
* `parseJSONRPCMessage`; a `200` with a `text/event-stream` body is decoded with the
|
|
815
822
|
* `@orkestrel/sse` {@link import('@orkestrel/sse').SSEParserInterface} ({@link
|
|
816
823
|
* readEventStream}) — the inverse of the server's `openStream` seam, so the wire
|
|
817
824
|
* round-trips. A `202`
|
|
@@ -829,10 +836,15 @@ function createMCPPostHandler(mcp, options) {
|
|
|
829
836
|
* Before initialize returns, neither captured legacy header is sent.
|
|
830
837
|
* `close()` clears the captured protocol so a reconnect's `initialize`
|
|
831
838
|
* POST is headerless; the captured `session` persists across `close()`.
|
|
832
|
-
* -
|
|
839
|
+
* - **`close()` releases what is in flight.** Every `fetch` this transport still has open is
|
|
840
|
+
* ABORTED, which cancels the response body a `send` is reading — an SSE reply the server
|
|
841
|
+
* never ends would otherwise outlive the transport, with nothing left able to reach it. The
|
|
842
|
+
* aborted read surfaces on `error` and the `send` reporting it resolves. `close()` is
|
|
843
|
+
* idempotent (one `close` event per connected lifetime), and `start()` opens the next one.
|
|
844
|
+
* - **Total at the boundary.** Every reply is narrowed (`parseJSONRPCMessage`,
|
|
833
845
|
* the SSE decoder) — a non-message reply is dropped, never asserted; a `fetch` /
|
|
834
846
|
* decode failure surfaces on the `error` event rather than escaping `send`.
|
|
835
|
-
* - **Observable
|
|
847
|
+
* - **Observable.** Owns the `emitter` ({@link MCPClientTransportEventMap}); fires
|
|
836
848
|
* `message` per decoded reply, `error` on a fault, and `close` on `close()`.
|
|
837
849
|
*
|
|
838
850
|
* @example
|
|
@@ -848,8 +860,10 @@ var HTTPClientTransport = class {
|
|
|
848
860
|
#headers;
|
|
849
861
|
#fetch;
|
|
850
862
|
#timeout;
|
|
863
|
+
#pending = /* @__PURE__ */ new Set();
|
|
851
864
|
#session = void 0;
|
|
852
865
|
#protocol = void 0;
|
|
866
|
+
#closed = false;
|
|
853
867
|
constructor(options) {
|
|
854
868
|
this.#emitter = new _orkestrel_emitter.Emitter();
|
|
855
869
|
this.#url = options.url;
|
|
@@ -866,8 +880,19 @@ var HTTPClientTransport = class {
|
|
|
866
880
|
get duplex() {
|
|
867
881
|
return false;
|
|
868
882
|
}
|
|
869
|
-
async start() {
|
|
883
|
+
async start() {
|
|
884
|
+
this.#closed = false;
|
|
885
|
+
}
|
|
870
886
|
async send(message) {
|
|
887
|
+
const request = new AbortController();
|
|
888
|
+
this.#pending.add(request);
|
|
889
|
+
try {
|
|
890
|
+
await this.#exchange(message, request.signal);
|
|
891
|
+
} finally {
|
|
892
|
+
this.#pending.delete(request);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
async #exchange(message, signal) {
|
|
871
896
|
let response;
|
|
872
897
|
try {
|
|
873
898
|
response = await this.#fetch(this.#url, {
|
|
@@ -880,7 +905,7 @@ var HTTPClientTransport = class {
|
|
|
880
905
|
...this.#headers
|
|
881
906
|
},
|
|
882
907
|
body: JSON.stringify(message),
|
|
883
|
-
|
|
908
|
+
signal: this.#timeout === void 0 ? signal : AbortSignal.any([signal, AbortSignal.timeout(this.#timeout)])
|
|
884
909
|
});
|
|
885
910
|
} catch (error) {
|
|
886
911
|
this.#emitter.emit("error", error);
|
|
@@ -891,6 +916,10 @@ var HTTPClientTransport = class {
|
|
|
891
916
|
await this.#deliver(response);
|
|
892
917
|
}
|
|
893
918
|
async close() {
|
|
919
|
+
if (this.#closed) return;
|
|
920
|
+
this.#closed = true;
|
|
921
|
+
for (const request of this.#pending) request.abort();
|
|
922
|
+
this.#pending.clear();
|
|
894
923
|
this.#protocol = void 0;
|
|
895
924
|
this.#emitter.emit("close");
|
|
896
925
|
}
|
|
@@ -938,8 +967,8 @@ var HTTPClientTransport = class {
|
|
|
938
967
|
* The single session entity (the old `SessionState` + `EventStore` merged): it holds the
|
|
939
968
|
* session `id`, its OWN bounded, replayable log of pushed server→client messages (the
|
|
940
969
|
* resumable GET-SSE channel — a private `#events` `Map` + a monotone `#counter`, with
|
|
941
|
-
* `capacity` / `ttl` eviction,
|
|
942
|
-
* server→client SSE streams (a resumable `GET {path}` registers
|
|
970
|
+
* `capacity` / `ttl` eviction, not a separate store), and the set of open
|
|
971
|
+
* server→client SSE streams (a resumable `GET {path}` registers through `attach`, unregisters through
|
|
943
972
|
* `detach` on disconnect). Still a small entity (not a record), built minimal + extensible.
|
|
944
973
|
*
|
|
945
974
|
* - **`push` is the server-initiated primitive.** It APPENDS the message to the log (assigning
|
|
@@ -959,7 +988,7 @@ var HTTPClientTransport = class {
|
|
|
959
988
|
* lost (its cursor is OLDER than everything retained); returning `[]` lets the handler then
|
|
960
989
|
* stream only the fresh pushes that follow `attach` — the spec-sane resume.
|
|
961
990
|
*
|
|
962
|
-
* - **Bounded, append-ordered, plain `Map
|
|
991
|
+
* - **Bounded, append-ordered, plain `Map`.** The log lives in ONE insertion-ordered
|
|
963
992
|
* `Map<id, entry>` — insertion order IS append order IS id order, so `replay` and capacity
|
|
964
993
|
* eviction both walk the map directly. NO database mirror — the log is process-local
|
|
965
994
|
* transport mechanics, not durable state. `push` first drops every entry older than `ttl`
|
|
@@ -973,8 +1002,8 @@ var HTTPClientTransport = class {
|
|
|
973
1002
|
* serializes a message onto the already-open streams.
|
|
974
1003
|
*
|
|
975
1004
|
* - **Injected clock.** `push` / `replay` accept an optional `now` (epoch ms), defaulting to
|
|
976
|
-
* `Date.now()` — so a test drives TTL eviction with an elapsed clock rather than a real
|
|
977
|
-
*
|
|
1005
|
+
* `Date.now()` — so a test drives TTL eviction with an elapsed clock rather than a real
|
|
1006
|
+
* timer.
|
|
978
1007
|
*
|
|
979
1008
|
* @example
|
|
980
1009
|
* ```ts
|
|
@@ -1055,7 +1084,7 @@ var MCPSession = class {
|
|
|
1055
1084
|
* {@link import('./WebSocketClientTransport.js').WebSocketClientTransport} reuses.
|
|
1056
1085
|
*
|
|
1057
1086
|
* @remarks
|
|
1058
|
-
* - **Reuses `MCPClientTransportInterface
|
|
1087
|
+
* - **Reuses `MCPClientTransportInterface`.** It IS the same generic carrier the HTTP
|
|
1059
1088
|
* client transport implements — `emitter` (`message` / `close` / `error`), `start`,
|
|
1060
1089
|
* `send`, `close` — so the WebSocket server and client both speak ONE transport contract,
|
|
1061
1090
|
* no near-duplicate sibling interface. `session` is `undefined` (the stateless v1; a
|
|
@@ -1064,22 +1093,27 @@ var MCPSession = class {
|
|
|
1064
1093
|
* - **Inbound (`message`).** `start()` subscribes to the socket's `message` event; each text
|
|
1065
1094
|
* frame is `JSON.parse`d inside a try/catch and narrowed with `parseJSONRPCMessage` — a
|
|
1066
1095
|
* well-formed {@link JSONRPCMessage} is re-emitted on this transport's `message` event (the
|
|
1067
|
-
* parsed envelope the {@link import('@
|
|
1068
|
-
* a non-JSON or non-message frame is surfaced on `error` and DROPPED, never thrown
|
|
1096
|
+
* parsed envelope the {@link import('@orkestrel/mcp').MCPServerInterface} pump dispatches), while
|
|
1097
|
+
* a non-JSON or non-message frame is surfaced on `error` and DROPPED, never thrown. It
|
|
1069
1098
|
* also bridges the socket's `close` → this transport's `close`, and the socket's `error`.
|
|
1070
1099
|
* - **Outbound (`send`).** `send(message)` writes one text frame
|
|
1071
1100
|
* (`nodeWs.send(JSON.stringify(message))`); the underlying wrapper no-ops a write on a
|
|
1072
1101
|
* non-open socket, so a closed connection drops silently rather than throwing.
|
|
1073
|
-
* - **`close()`**
|
|
1074
|
-
*
|
|
1075
|
-
* once).
|
|
1076
|
-
*
|
|
1102
|
+
* - **`close()`** removes the subscriptions `start()` installed on the socket, closes the
|
|
1103
|
+
* underlying socket (the RFC 6455 close handshake), and fires the transport's `close` event
|
|
1104
|
+
* (idempotent — a second `close`, or a socket-driven close, emits once). A frame that arrives
|
|
1105
|
+
* between that release and the peer's close echo reaches nothing: the socket-driven close path
|
|
1106
|
+
* releases the same way, so a closed transport is never subscribed to a live socket.
|
|
1107
|
+
* - **Observable.** Owns the `emitter` ({@link MCPClientTransportEventMap}); the emitter
|
|
1077
1108
|
* isolates a listener throw (a buggy observer never corrupts the bridge). `error` is a
|
|
1078
1109
|
* DOMAIN event (a transport-level fault), distinct from the emitter's listener-error channel.
|
|
1079
1110
|
*/
|
|
1080
1111
|
var WebSocketServerTransport = class {
|
|
1081
1112
|
#emitter;
|
|
1082
1113
|
#socket;
|
|
1114
|
+
#frame = (text) => this.#receive(text);
|
|
1115
|
+
#ending = () => this.#onClose();
|
|
1116
|
+
#failure = (error) => this.#emitter.emit("error", error);
|
|
1083
1117
|
#started = false;
|
|
1084
1118
|
#closed = false;
|
|
1085
1119
|
constructor(socket) {
|
|
@@ -1096,9 +1130,9 @@ var WebSocketServerTransport = class {
|
|
|
1096
1130
|
async start() {
|
|
1097
1131
|
if (this.#started || this.#closed) return;
|
|
1098
1132
|
this.#started = true;
|
|
1099
|
-
this.#socket.emitter.on("message",
|
|
1100
|
-
this.#socket.emitter.on("close",
|
|
1101
|
-
this.#socket.emitter.on("error",
|
|
1133
|
+
this.#socket.emitter.on("message", this.#frame);
|
|
1134
|
+
this.#socket.emitter.on("close", this.#ending);
|
|
1135
|
+
this.#socket.emitter.on("error", this.#failure);
|
|
1102
1136
|
}
|
|
1103
1137
|
async send(message) {
|
|
1104
1138
|
this.#socket.send(JSON.stringify(message));
|
|
@@ -1106,6 +1140,7 @@ var WebSocketServerTransport = class {
|
|
|
1106
1140
|
async close() {
|
|
1107
1141
|
if (this.#closed) return;
|
|
1108
1142
|
this.#closed = true;
|
|
1143
|
+
this.#release();
|
|
1109
1144
|
this.#socket.close();
|
|
1110
1145
|
this.#emitter.emit("close");
|
|
1111
1146
|
}
|
|
@@ -1127,8 +1162,14 @@ var WebSocketServerTransport = class {
|
|
|
1127
1162
|
#onClose() {
|
|
1128
1163
|
if (this.#closed) return;
|
|
1129
1164
|
this.#closed = true;
|
|
1165
|
+
this.#release();
|
|
1130
1166
|
this.#emitter.emit("close");
|
|
1131
1167
|
}
|
|
1168
|
+
#release() {
|
|
1169
|
+
this.#socket.emitter.off("message", this.#frame);
|
|
1170
|
+
this.#socket.emitter.off("close", this.#ending);
|
|
1171
|
+
this.#socket.emitter.off("error", this.#failure);
|
|
1172
|
+
}
|
|
1132
1173
|
};
|
|
1133
1174
|
//#endregion
|
|
1134
1175
|
//#region src/server/transports/WebSocketClientTransport.ts
|
|
@@ -1146,7 +1187,8 @@ var WebSocketServerTransport = class {
|
|
|
1146
1187
|
* event, and VALIDATES `Sec-WebSocket-Accept === computeWebSocketAccept(key)` (the D2 helper)
|
|
1147
1188
|
* — a mismatch (or a non-`101` response, or a request error) REJECTS `start()` and the socket
|
|
1148
1189
|
* is destroyed. On success it wraps the raw upgraded socket in `createNodeWebSocket({ socket,
|
|
1149
|
-
* head })` (CLIENT mode — no key → frames are MASKED per §5.3) and bridges its
|
|
1190
|
+
* head })` (CLIENT mode — no key → frames are MASKED per RFC 6455 §5.3) and bridges its
|
|
1191
|
+
* `message`.
|
|
1150
1192
|
* - **The arriving socket is RE-ASKED for, never assumed.** `start()` suspends across that
|
|
1151
1193
|
* connect and upgrade, so it re-checks the transport's state before installing anything: a
|
|
1152
1194
|
* concurrent `start()` that already installed a socket, or a {@link close} that ended the
|
|
@@ -1155,15 +1197,18 @@ var WebSocketServerTransport = class {
|
|
|
1155
1197
|
* `start()` calls still resolve; exactly one socket is ever bound.
|
|
1156
1198
|
* - **Inbound (`message`).** Each decoded text frame is `JSON.parse`d (guarded) and narrowed
|
|
1157
1199
|
* with `parseJSONRPCMessage` — a {@link JSONRPCMessage} re-emits on this transport's `message`
|
|
1158
|
-
* event (the reply the {@link import('@
|
|
1159
|
-
* non-JSON / non-message frame surfaces on `error` and is dropped
|
|
1200
|
+
* event (the reply the {@link import('@orkestrel/mcp').MCPClientInterface} correlates by `id`); a
|
|
1201
|
+
* non-JSON / non-message frame surfaces on `error` and is dropped. The socket's `close`
|
|
1160
1202
|
* / `error` bridge to this transport's events.
|
|
1161
1203
|
* - **Outbound (`send`).** `send(message)` writes one masked text frame.
|
|
1162
|
-
* - **`close()`**
|
|
1204
|
+
* - **`close()`** unsubscribes from the socket, closes it, and fires `close` (idempotent). An
|
|
1205
|
+
* upgrade still on the wire is DESTROYED, so a `close()` during the handshake ends the
|
|
1206
|
+
* transport at once instead of waiting for a peer that may never answer — the suspended
|
|
1207
|
+
* `start()` resolves, because the close is the outcome its caller asked for.
|
|
1163
1208
|
* - **URL scheme.** `options.url` accepts a `ws://` / `wss://` URL or an `http://` / `https://`
|
|
1164
1209
|
* one; a `ws(s)` scheme is converted to `http(s)` for the underlying upgrade request (`wss`
|
|
1165
|
-
* → TLS
|
|
1166
|
-
* - **Observable
|
|
1210
|
+
* → TLS through `node:https`). Either reaches the same endpoint.
|
|
1211
|
+
* - **Observable.** Owns the `emitter` ({@link MCPClientTransportEventMap}); every emit
|
|
1167
1212
|
* the emitter isolates a listener throw (a buggy observer never corrupts the transport);
|
|
1168
1213
|
* `error` is a DOMAIN event (a transport-level fault).
|
|
1169
1214
|
*
|
|
@@ -1178,7 +1223,11 @@ var WebSocketClientTransport = class {
|
|
|
1178
1223
|
#emitter;
|
|
1179
1224
|
#url;
|
|
1180
1225
|
#headers;
|
|
1226
|
+
#frame = (text) => this.#receive(text);
|
|
1227
|
+
#ending = () => this.#onClose();
|
|
1228
|
+
#failure = (error) => this.#emitter.emit("error", error);
|
|
1181
1229
|
#socket = void 0;
|
|
1230
|
+
#request = void 0;
|
|
1182
1231
|
#closed = false;
|
|
1183
1232
|
constructor(options) {
|
|
1184
1233
|
this.#emitter = new _orkestrel_emitter.Emitter();
|
|
@@ -1195,8 +1244,28 @@ var WebSocketClientTransport = class {
|
|
|
1195
1244
|
async start() {
|
|
1196
1245
|
if (this.#socket !== void 0) return;
|
|
1197
1246
|
this.#closed = false;
|
|
1198
|
-
|
|
1199
|
-
|
|
1247
|
+
try {
|
|
1248
|
+
await this.#connect(this.#httpURL(), (0, node_crypto.randomBytes)(16).toString("base64"));
|
|
1249
|
+
} finally {
|
|
1250
|
+
this.#request = void 0;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
async send(message) {
|
|
1254
|
+
const socket = this.#socket;
|
|
1255
|
+
if (socket === void 0) throw new Error("WebSocket transport is not connected");
|
|
1256
|
+
socket.send(JSON.stringify(message));
|
|
1257
|
+
}
|
|
1258
|
+
async close() {
|
|
1259
|
+
if (this.#closed) return;
|
|
1260
|
+
this.#closed = true;
|
|
1261
|
+
this.#request?.destroy();
|
|
1262
|
+
const socket = this.#socket;
|
|
1263
|
+
this.#release();
|
|
1264
|
+
this.#socket = void 0;
|
|
1265
|
+
if (socket !== void 0) socket.close();
|
|
1266
|
+
this.#emitter.emit("close");
|
|
1267
|
+
}
|
|
1268
|
+
async #connect(url, key) {
|
|
1200
1269
|
const secure = url.protocol === "https:";
|
|
1201
1270
|
const send = secure ? node_https.request : node_http.request;
|
|
1202
1271
|
await new Promise((resolve, reject) => {
|
|
@@ -1213,6 +1282,7 @@ var WebSocketClientTransport = class {
|
|
|
1213
1282
|
...this.#headers
|
|
1214
1283
|
}
|
|
1215
1284
|
});
|
|
1285
|
+
this.#request = request;
|
|
1216
1286
|
request.on("upgrade", (response, socket, head) => {
|
|
1217
1287
|
const accept = response.headers["sec-websocket-accept"];
|
|
1218
1288
|
if (!(0, _orkestrel_contract.isString)(accept) || accept !== (0, _orkestrel_websocket.computeWebSocketAccept)(key)) {
|
|
@@ -1237,27 +1307,27 @@ var WebSocketClientTransport = class {
|
|
|
1237
1307
|
response.resume();
|
|
1238
1308
|
reject(/* @__PURE__ */ new Error(`WebSocket upgrade declined with status ${response.statusCode ?? 0}`));
|
|
1239
1309
|
});
|
|
1240
|
-
request.on("error", (error) =>
|
|
1310
|
+
request.on("error", (error) => {
|
|
1311
|
+
if (this.#closed) {
|
|
1312
|
+
resolve();
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
1316
|
+
});
|
|
1241
1317
|
request.end();
|
|
1242
1318
|
});
|
|
1243
1319
|
}
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1320
|
+
#bind(ws) {
|
|
1321
|
+
ws.emitter.on("message", this.#frame);
|
|
1322
|
+
ws.emitter.on("close", this.#ending);
|
|
1323
|
+
ws.emitter.on("error", this.#failure);
|
|
1248
1324
|
}
|
|
1249
|
-
|
|
1250
|
-
if (this.#closed) return;
|
|
1251
|
-
this.#closed = true;
|
|
1325
|
+
#release() {
|
|
1252
1326
|
const socket = this.#socket;
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
#bind(ws) {
|
|
1258
|
-
ws.emitter.on("message", (text) => this.#receive(text));
|
|
1259
|
-
ws.emitter.on("close", () => this.#onClose(ws));
|
|
1260
|
-
ws.emitter.on("error", (error) => this.#emitter.emit("error", error));
|
|
1327
|
+
if (socket === void 0) return;
|
|
1328
|
+
socket.emitter.off("message", this.#frame);
|
|
1329
|
+
socket.emitter.off("close", this.#ending);
|
|
1330
|
+
socket.emitter.off("error", this.#failure);
|
|
1261
1331
|
}
|
|
1262
1332
|
#receive(text) {
|
|
1263
1333
|
let parsed;
|
|
@@ -1274,9 +1344,10 @@ var WebSocketClientTransport = class {
|
|
|
1274
1344
|
}
|
|
1275
1345
|
this.#emitter.emit("message", message);
|
|
1276
1346
|
}
|
|
1277
|
-
#onClose(
|
|
1278
|
-
if (this.#closed
|
|
1347
|
+
#onClose() {
|
|
1348
|
+
if (this.#closed) return;
|
|
1279
1349
|
this.#closed = true;
|
|
1350
|
+
this.#release();
|
|
1280
1351
|
this.#socket = void 0;
|
|
1281
1352
|
this.#emitter.emit("close");
|
|
1282
1353
|
}
|
|
@@ -1292,7 +1363,7 @@ var WebSocketClientTransport = class {
|
|
|
1292
1363
|
//#region src/server/transports/StdioClientTransport.ts
|
|
1293
1364
|
/**
|
|
1294
1365
|
* The stdio CLIENT transport for the Model Context Protocol — a
|
|
1295
|
-
* {@link
|
|
1366
|
+
* {@link StdioClientTransportInterface} that drives a CHILD PROCESS MCP server over
|
|
1296
1367
|
* newline-delimited JSON-RPC on `stdin`/`stdout`, the stdio sibling of {@link
|
|
1297
1368
|
* import('./HTTPClientTransport.js').HTTPClientTransport} and {@link
|
|
1298
1369
|
* import('./WebSocketClientTransport.js').WebSocketClientTransport}.
|
|
@@ -1305,22 +1376,40 @@ var WebSocketClientTransport = class {
|
|
|
1305
1376
|
* - **Inbound (`message`).** Standard output is drained eagerly through the supervisor's
|
|
1306
1377
|
* `readline`-framed `lines` iterable, so a multi-byte UTF-8 sequence split across two reads is
|
|
1307
1378
|
* decoded whole and a final line written without a trailing newline still arrives. Each framed
|
|
1308
|
-
* line is decoded and delivered
|
|
1309
|
-
* {@link JSONRPCMessage} emits `message`, a malformed line emits `error` (
|
|
1379
|
+
* line is decoded and delivered through the shared {@link dispatchLines} helper — a well-formed
|
|
1380
|
+
* {@link JSONRPCMessage} emits `message`, a malformed line emits `error` (never throws).
|
|
1310
1381
|
* - **Outbound (`send`).** `send(message)` writes one newline-terminated `JSON.stringify`d line
|
|
1311
1382
|
* through the supervisor's `send` and AWAITS its answer, so this promise settles only after the
|
|
1312
1383
|
* host reports the line handled rather than the moment the write is queued. The supervisor never
|
|
1313
1384
|
* rejects — it answers `false` for a channel that was closed, destroyed, or ended, and for a write
|
|
1314
1385
|
* that failed — so a `false` answer REJECTS here with the same not-connected error a transport
|
|
1315
1386
|
* that was never started raises. A dead peer surfaces at the caller instead of vanishing.
|
|
1316
|
-
* - **`close()`**
|
|
1317
|
-
*
|
|
1318
|
-
*
|
|
1319
|
-
*
|
|
1320
|
-
*
|
|
1387
|
+
* - **`close()`** runs the supervisor's bounded termination and teardown, then fires `close` once
|
|
1388
|
+
* (idempotent). That teardown reaches the child's TERMINAL MOMENT, where the supervisor freezes
|
|
1389
|
+
* `evidence`, ends `lines`, and settles `exit` together, so this transport needs no release of
|
|
1390
|
+
* its own to get its line pump back: the stream ends under the pump rather than throwing at it.
|
|
1391
|
+
* A line the supervisor had already framed behind the one being delivered is dropped rather than
|
|
1392
|
+
* emitted onto a transport whose teardown has begun. A `close()` issued while that teardown runs
|
|
1393
|
+
* joins it rather than opening a second one, so it resolves only after `close` has fired, and a
|
|
1394
|
+
* `start()` issued while it runs waits behind the same barrier, so lifetimes never overlap. A
|
|
1395
|
+
* descendant can retain an inherited stdout pipe after the child exits; the supervisor's `drain`
|
|
1396
|
+
* bound cuts that wait off, so this transport's `close()` settles within that bound rather than
|
|
1397
|
+
* on the descendant. The termination itself belongs to the host: a POSIX host signals the
|
|
1398
|
+
* child's own process group `SIGTERM`, waits the grace window, then `SIGKILL`s through the same
|
|
1399
|
+
* route, so the kill reaches grandchildren rather than orphaning them, while Windows ends the
|
|
1400
|
+
* tree with `taskkill /F /T`, which nothing in the child can intercept.
|
|
1401
|
+
* - **Evidence.** `evidence` reports that retained stderr tail off the HELD child — its live tail
|
|
1402
|
+
* while the child runs, and the value the supervisor froze at that child's terminal moment
|
|
1403
|
+
* afterwards. The reference is held past that moment and replaced only by the next `start()`,
|
|
1404
|
+
* which is what keeps a post-`close()` read stable without a private copy: the frozen value
|
|
1405
|
+
* never moves again, so a detached descendant writing to the inherited stderr after the cutoff
|
|
1406
|
+
* cannot grow it. See {@link StdioClientTransportInterface.evidence} for the readings and the
|
|
1407
|
+
* byte bound.
|
|
1408
|
+
* - **Observable.** Owns the `emitter` ({@link MCPClientTransportEventMap}); the
|
|
1321
1409
|
* emitter isolates a listener throw; `error` is a DOMAIN event (a transport-level
|
|
1322
|
-
* fault, including the child spawn cause the supervisor surfaces
|
|
1323
|
-
*
|
|
1410
|
+
* fault, including the child spawn cause the supervisor surfaces and the notice that this
|
|
1411
|
+
* lifetime's `evidence` was cut off at the `drain` bound), distinct from the emitter's own
|
|
1412
|
+
* listener-error channel.
|
|
1324
1413
|
*
|
|
1325
1414
|
* @example
|
|
1326
1415
|
* ```ts
|
|
@@ -1335,6 +1424,7 @@ var StdioClientTransport = class {
|
|
|
1335
1424
|
#args;
|
|
1336
1425
|
#env;
|
|
1337
1426
|
#process = void 0;
|
|
1427
|
+
#closing = void 0;
|
|
1338
1428
|
#closed = false;
|
|
1339
1429
|
constructor(options) {
|
|
1340
1430
|
this.#emitter = new _orkestrel_emitter.Emitter();
|
|
@@ -1349,8 +1439,20 @@ var StdioClientTransport = class {
|
|
|
1349
1439
|
get duplex() {
|
|
1350
1440
|
return true;
|
|
1351
1441
|
}
|
|
1442
|
+
get evidence() {
|
|
1443
|
+
return this.#process?.evidence;
|
|
1444
|
+
}
|
|
1352
1445
|
async start() {
|
|
1353
|
-
|
|
1446
|
+
let closing = this.#closing;
|
|
1447
|
+
while (closing !== void 0) {
|
|
1448
|
+
await closing;
|
|
1449
|
+
if (this.#closing === closing) {
|
|
1450
|
+
this.#closing = void 0;
|
|
1451
|
+
break;
|
|
1452
|
+
}
|
|
1453
|
+
closing = this.#closing;
|
|
1454
|
+
}
|
|
1455
|
+
if (this.#process !== void 0 && !this.#closed) return;
|
|
1354
1456
|
this.#closed = false;
|
|
1355
1457
|
const child = new _orkestrel_process_server.Process({
|
|
1356
1458
|
command: {
|
|
@@ -1364,33 +1466,49 @@ var StdioClientTransport = class {
|
|
|
1364
1466
|
});
|
|
1365
1467
|
this.#process = child;
|
|
1366
1468
|
child.emitter.on("error", (cause) => this.#emitter.emit("error", cause));
|
|
1367
|
-
child.exit.then(() => this.#onExit(child));
|
|
1469
|
+
child.exit.then((exit) => this.#onExit(child, exit));
|
|
1368
1470
|
this.#pump(child);
|
|
1369
1471
|
}
|
|
1370
1472
|
async send(message) {
|
|
1371
|
-
const child = this.#process;
|
|
1473
|
+
const child = this.#closed ? void 0 : this.#process;
|
|
1372
1474
|
if (!(child === void 0 ? false : await child.send(JSON.stringify(message)))) throw new Error("stdio transport is not connected");
|
|
1373
1475
|
}
|
|
1374
1476
|
async close() {
|
|
1477
|
+
if (this.#closed && this.#closing === void 0) return;
|
|
1478
|
+
this.#closing ??= this.#teardown();
|
|
1479
|
+
await this.#closing;
|
|
1480
|
+
}
|
|
1481
|
+
async #teardown() {
|
|
1375
1482
|
if (this.#closed) return;
|
|
1376
1483
|
this.#closed = true;
|
|
1377
1484
|
const child = this.#process;
|
|
1378
|
-
|
|
1379
|
-
|
|
1485
|
+
if (child !== void 0) {
|
|
1486
|
+
await child.destroy();
|
|
1487
|
+
this.#report(await child.exit);
|
|
1488
|
+
}
|
|
1380
1489
|
this.#emitter.emit("close");
|
|
1381
1490
|
}
|
|
1382
1491
|
async #pump(child) {
|
|
1383
1492
|
for await (const line of child.lines) {
|
|
1384
|
-
if (this.#process !== child) return;
|
|
1493
|
+
if (this.#closed || this.#process !== child) return;
|
|
1385
1494
|
dispatchLines(this.#emitter, [line]);
|
|
1386
1495
|
}
|
|
1387
1496
|
}
|
|
1388
|
-
#onExit(child) {
|
|
1389
|
-
if (this.#
|
|
1497
|
+
#onExit(child, exit) {
|
|
1498
|
+
if (this.#process !== child) return;
|
|
1499
|
+
if (this.#closed) return;
|
|
1390
1500
|
this.#closed = true;
|
|
1391
|
-
|
|
1501
|
+
const barrier = Promise.withResolvers();
|
|
1502
|
+
this.#closing ??= barrier.promise;
|
|
1503
|
+
this.#report(exit);
|
|
1504
|
+
barrier.resolve();
|
|
1505
|
+
if (this.#closing === barrier.promise) this.#closing = void 0;
|
|
1392
1506
|
this.#emitter.emit("close");
|
|
1393
1507
|
}
|
|
1508
|
+
#report(exit) {
|
|
1509
|
+
if (exit.drained) return;
|
|
1510
|
+
this.#emitter.emit("error", /* @__PURE__ */ new Error("stdio transport evidence may be incomplete: the child streams stayed open past the supervisor drain bound"));
|
|
1511
|
+
}
|
|
1394
1512
|
};
|
|
1395
1513
|
//#endregion
|
|
1396
1514
|
//#region src/server/transports/StdioServerTransport.ts
|
|
@@ -1403,22 +1521,31 @@ var StdioClientTransport = class {
|
|
|
1403
1521
|
* import('./WebSocketServerTransport.js').WebSocketServerTransport}.
|
|
1404
1522
|
*
|
|
1405
1523
|
* @remarks
|
|
1406
|
-
* - **Reuses `MCPClientTransportInterface
|
|
1524
|
+
* - **Reuses `MCPClientTransportInterface`.** The same generic carrier the HTTP
|
|
1407
1525
|
* and WebSocket server transports implement — `emitter` (`message` / `close` /
|
|
1408
1526
|
* `error`), `start`, `send`, `close`. `session` is `undefined` (the stateless v1).
|
|
1409
1527
|
* - **Inbound (`message`).** `start()` subscribes to `input`'s `data` event; each
|
|
1410
1528
|
* chunk is folded through the shared {@link extractLines} line-framing helper
|
|
1411
1529
|
* (buffering a partial trailing line across reads), and every complete line is
|
|
1412
|
-
* decoded and delivered
|
|
1530
|
+
* decoded and delivered through the shared {@link dispatchLines} helper — a
|
|
1413
1531
|
* well-formed {@link JSONRPCMessage} re-emits on `message`, a malformed line
|
|
1414
|
-
* emits `error` (
|
|
1532
|
+
* emits `error` (never throws). `input`'s `close` bridges to this
|
|
1415
1533
|
* transport's `close`.
|
|
1416
1534
|
* - **Outbound (`send`).** `send(message)` writes one newline-terminated
|
|
1417
1535
|
* `JSON.stringify`d line to `output`.
|
|
1418
|
-
* - **`close()`**
|
|
1419
|
-
*
|
|
1420
|
-
*
|
|
1421
|
-
*
|
|
1536
|
+
* - **`close()`** removes this transport's input subscriptions and fires its `close`
|
|
1537
|
+
* event (idempotent). It pauses the input only when the caller was not already reading
|
|
1538
|
+
* it at `start` (`readableFlowing !== true`) AND no `data` listener remains once this
|
|
1539
|
+
* transport's own is removed — so a process holding `process.stdin` can exit, and a
|
|
1540
|
+
* caller's own flow is never stopped underneath it. The transport preserves flowing versus
|
|
1541
|
+
* non-flowing state and restores every caller-owned listener. A Node stream that had never been
|
|
1542
|
+
* read starts with `readableFlowing === null` and is left non-flowing (`false`), because Node
|
|
1543
|
+
* exposes no public operation that restores `null` after data consumption starts. Attaching a
|
|
1544
|
+
* later `data` listener does not resume that stream; the caller must call `resume()` before the
|
|
1545
|
+
* listener receives data. The injected streams are owned by the caller (typically
|
|
1546
|
+
* `process.stdin`/`process.stdout`), so the transport never destroys, ends, or blanket-clears
|
|
1547
|
+
* them.
|
|
1548
|
+
* - **Observable.** Owns the `emitter` ({@link MCPClientTransportEventMap}); the
|
|
1422
1549
|
* emitter isolates a listener throw; `error` is a DOMAIN event (a transport-level
|
|
1423
1550
|
* fault), distinct from the emitter's own listener-error channel.
|
|
1424
1551
|
*/
|
|
@@ -1426,9 +1553,13 @@ var StdioServerTransport = class {
|
|
|
1426
1553
|
#emitter;
|
|
1427
1554
|
#input;
|
|
1428
1555
|
#output;
|
|
1556
|
+
#data = (chunk) => this.#receive(chunk.toString());
|
|
1557
|
+
#ending = () => this.#onClose();
|
|
1558
|
+
#failure = (error) => this.#emitter.emit("error", error);
|
|
1429
1559
|
#buffer = "";
|
|
1430
1560
|
#started = false;
|
|
1431
1561
|
#closed = false;
|
|
1562
|
+
#flowing = false;
|
|
1432
1563
|
constructor(input, output) {
|
|
1433
1564
|
this.#emitter = new _orkestrel_emitter.Emitter();
|
|
1434
1565
|
this.#input = input;
|
|
@@ -1444,9 +1575,10 @@ var StdioServerTransport = class {
|
|
|
1444
1575
|
async start() {
|
|
1445
1576
|
if (this.#started || this.#closed) return;
|
|
1446
1577
|
this.#started = true;
|
|
1447
|
-
this.#input.
|
|
1448
|
-
this.#input.on("
|
|
1449
|
-
this.#input.on("
|
|
1578
|
+
this.#flowing = this.#input instanceof node_stream.Readable && this.#input.readableFlowing === true;
|
|
1579
|
+
this.#input.on("data", this.#data);
|
|
1580
|
+
this.#input.on("close", this.#ending);
|
|
1581
|
+
this.#input.on("error", this.#failure);
|
|
1450
1582
|
}
|
|
1451
1583
|
async send(message) {
|
|
1452
1584
|
this.#output.write(`${JSON.stringify(message)}\n`);
|
|
@@ -1454,6 +1586,7 @@ var StdioServerTransport = class {
|
|
|
1454
1586
|
async close() {
|
|
1455
1587
|
if (this.#closed) return;
|
|
1456
1588
|
this.#closed = true;
|
|
1589
|
+
this.#release();
|
|
1457
1590
|
this.#emitter.emit("close");
|
|
1458
1591
|
}
|
|
1459
1592
|
#receive(chunk) {
|
|
@@ -1464,13 +1597,20 @@ var StdioServerTransport = class {
|
|
|
1464
1597
|
#onClose() {
|
|
1465
1598
|
if (this.#closed) return;
|
|
1466
1599
|
this.#closed = true;
|
|
1600
|
+
this.#release();
|
|
1467
1601
|
this.#emitter.emit("close");
|
|
1468
1602
|
}
|
|
1603
|
+
#release() {
|
|
1604
|
+
this.#input.removeListener("data", this.#data);
|
|
1605
|
+
this.#input.removeListener("close", this.#ending);
|
|
1606
|
+
this.#input.removeListener("error", this.#failure);
|
|
1607
|
+
if (!this.#flowing && this.#input.listenerCount("data") === 0) this.#input.pause();
|
|
1608
|
+
}
|
|
1469
1609
|
};
|
|
1470
1610
|
//#endregion
|
|
1471
1611
|
//#region src/server/factories.ts
|
|
1472
1612
|
/**
|
|
1473
|
-
*
|
|
1613
|
+
* Adapts the installed server token primitives to the host-neutral MCP continuation port.
|
|
1474
1614
|
*
|
|
1475
1615
|
* @param secret - Current signing secret or `[current, ...older]` rotation list
|
|
1476
1616
|
* @returns A continuation port that seals and opens opaque canonical state strings
|
|
@@ -1486,8 +1626,8 @@ function createMCPContinuation(secret) {
|
|
|
1486
1626
|
};
|
|
1487
1627
|
}
|
|
1488
1628
|
/**
|
|
1489
|
-
*
|
|
1490
|
-
* {@link MCPDispatcherInterface} (the `@
|
|
1629
|
+
* Creates the MCP Streamable-HTTP transport routes — mounts a transport-agnostic
|
|
1630
|
+
* {@link MCPDispatcherInterface} (the `@orkestrel/mcp` dispatch boundary) on the fetch-standard router
|
|
1491
1631
|
* spine, pumping each `POST` body through `mcp.dispatch`. Returns the {@link RouteInput}s to
|
|
1492
1632
|
* hand to `router.add(...)`.
|
|
1493
1633
|
*
|
|
@@ -1510,7 +1650,7 @@ function createMCPContinuation(secret) {
|
|
|
1510
1650
|
*
|
|
1511
1651
|
* When `streaming` is enabled (the default) and the client `Accept`s `text/event-stream`,
|
|
1512
1652
|
* the `200` reply is framed as a Streamable-HTTP SSE response (one `data:` event carrying
|
|
1513
|
-
* the JSON-RPC envelope, then the stream ends)
|
|
1653
|
+
* the JSON-RPC envelope, then the stream ends) through `@orkestrel/server`'s generic
|
|
1514
1654
|
* {@link import('@orkestrel/server').openStream} seam; otherwise it is a plain JSON body.
|
|
1515
1655
|
*
|
|
1516
1656
|
* **Sessions are a SEPARATE, plug-and-play middleware.** `createMCPRoutes` mints / reads no
|
|
@@ -1532,11 +1672,12 @@ function createMCPContinuation(secret) {
|
|
|
1532
1672
|
*
|
|
1533
1673
|
* @example
|
|
1534
1674
|
* ```ts
|
|
1535
|
-
* import { createMCPLegacy, createMCPServer
|
|
1536
|
-
* import { createMCPRoutes } from '@
|
|
1675
|
+
* import { createMCPLegacy, createMCPServer } from '@orkestrel/mcp'
|
|
1676
|
+
* import { createMCPRoutes } from '@orkestrel/mcp/server'
|
|
1677
|
+
* import { createToolManager } from '@orkestrel/tool'
|
|
1537
1678
|
*
|
|
1538
1679
|
* const mcp = createMCPServer({ identity: { name: 'docs', version: '1.0.0' }, tools: createToolManager() })
|
|
1539
|
-
* const routes = createMCPRoutes(createMCPLegacy(mcp)) //
|
|
1680
|
+
* const routes = createMCPRoutes(createMCPLegacy(mcp)) // answers `initialize` too; pass `mcp` alone for modern-only
|
|
1540
1681
|
* ```
|
|
1541
1682
|
*/
|
|
1542
1683
|
function createMCPRoutes(mcp, options) {
|
|
@@ -1548,7 +1689,7 @@ function createMCPRoutes(mcp, options) {
|
|
|
1548
1689
|
}];
|
|
1549
1690
|
}
|
|
1550
1691
|
/**
|
|
1551
|
-
*
|
|
1692
|
+
* Creates the HTTP CLIENT transport for an {@link import('@orkestrel/mcp').MCPClientInterface}
|
|
1552
1693
|
* — a {@link MCPClientTransportInterface} that drives a REMOTE Streamable-HTTP MCP server
|
|
1553
1694
|
* over `fetch`. The egress mirror of {@link createMCPRoutes}.
|
|
1554
1695
|
*
|
|
@@ -1556,9 +1697,9 @@ function createMCPRoutes(mcp, options) {
|
|
|
1556
1697
|
* Hand it to `createMCPClient({ transport })`: each JSON-RPC message the client sends is
|
|
1557
1698
|
* `POST`ed to `options.url` with `content-type: application/json` and an `Accept` of
|
|
1558
1699
|
* both `application/json` and `text/event-stream` (the server answers with EITHER — a
|
|
1559
|
-
* plain JSON envelope or a Streamable-HTTP SSE `data:` event, decoded
|
|
1700
|
+
* plain JSON envelope or a Streamable-HTTP SSE `data:` event, decoded with `@orkestrel/sse`),
|
|
1560
1701
|
* and the reply is surfaced on the transport's `message` event for the client's id
|
|
1561
|
-
* correlation. Add `options.headers` (
|
|
1702
|
+
* correlation. Add `options.headers` (for example, an `Authorization` bearer) to reach a guarded
|
|
1562
1703
|
* server. `start` / `close` hold no connection; against a STATEFUL server it captures the
|
|
1563
1704
|
* `mcp-session-id` from `initialize` and echoes it on later requests. It also captures
|
|
1564
1705
|
* the initialize result's `protocolVersion` and sends `mcp-protocol-version` alone on each
|
|
@@ -1567,13 +1708,13 @@ function createMCPRoutes(mcp, options) {
|
|
|
1567
1708
|
*
|
|
1568
1709
|
* @param options - `url` (the remote endpoint; REQUIRED), optional `headers` merged onto
|
|
1569
1710
|
* every request, optional `fetch` (default `globalThis.fetch`), and optional `timeout`
|
|
1570
|
-
* (ms, applied
|
|
1711
|
+
* (ms, applied with `AbortSignal.timeout`); see {@link HTTPClientTransportOptions}
|
|
1571
1712
|
* @returns A working {@link MCPClientTransportInterface} over `fetch`
|
|
1572
1713
|
*
|
|
1573
1714
|
* @example
|
|
1574
1715
|
* ```ts
|
|
1575
|
-
* import { createMCPClient } from '@
|
|
1576
|
-
* import { createHTTPClientTransport } from '@
|
|
1716
|
+
* import { createMCPClient } from '@orkestrel/mcp'
|
|
1717
|
+
* import { createHTTPClientTransport } from '@orkestrel/mcp/server'
|
|
1577
1718
|
*
|
|
1578
1719
|
* const client = createMCPClient({
|
|
1579
1720
|
* transport: createHTTPClientTransport({ url: 'http://localhost:3000/mcp' }),
|
|
@@ -1586,7 +1727,7 @@ function createHTTPClientTransport(options) {
|
|
|
1586
1727
|
return new HTTPClientTransport(options);
|
|
1587
1728
|
}
|
|
1588
1729
|
/**
|
|
1589
|
-
*
|
|
1730
|
+
* Creates the MCP WebSocket transport INGRESS — an {@link UpgradeHandler} that exposes a
|
|
1590
1731
|
* transport-agnostic {@link MCPDispatcherInterface} over a WebSocket, the WebSocket mirror of
|
|
1591
1732
|
* {@link createMCPRoutes}. Register it on the spine's upgrade seam.
|
|
1592
1733
|
*
|
|
@@ -1601,11 +1742,11 @@ function createHTTPClientTransport(options) {
|
|
|
1601
1742
|
* A decline NEVER writes to the socket (it is not yet ours) — the spine owns the unclaimed
|
|
1602
1743
|
* outcome.
|
|
1603
1744
|
* - **Claims (returns `true`)** otherwise: it builds `createNodeWebSocket({ socket, key, head,
|
|
1604
|
-
* protocol })` (SERVER mode → writes the `101` handshake,
|
|
1605
|
-
*
|
|
1745
|
+
* protocol })` (SERVER mode → writes the `101` handshake, selects the configured subprotocol
|
|
1746
|
+
* only when the client's offer contains it, and sends UNMASKED frames), wraps it in a
|
|
1606
1747
|
* {@link WebSocketServerTransport}, and pipes it through the core {@link
|
|
1607
|
-
* import('@
|
|
1608
|
-
* import('./helpers.js').bridgeMessageTransport} + {@link import('@
|
|
1748
|
+
* import('@orkestrel/mcp').MCPTransportInterface} port through {@link
|
|
1749
|
+
* import('./helpers.js').bridgeMessageTransport} + {@link import('@orkestrel/mcp').bindServer}:
|
|
1609
1750
|
* each inbound REQUEST runs through `mcp.dispatch`, and a defined response is written back
|
|
1610
1751
|
* as a frame — a NOTIFICATION sends nothing, and a non-request message (a stray response) is
|
|
1611
1752
|
* ignored. A `dispatch` / `send` fault surfaces on `mcp.emitter`'s `error` event rather than
|
|
@@ -1630,19 +1771,24 @@ function createHTTPClientTransport(options) {
|
|
|
1630
1771
|
*
|
|
1631
1772
|
* @example
|
|
1632
1773
|
* ```ts
|
|
1633
|
-
* import {
|
|
1634
|
-
* import { createWebSocketServer } from '@
|
|
1774
|
+
* import { createMCPLegacy, createMCPServer } from '@orkestrel/mcp'
|
|
1775
|
+
* import { createWebSocketServer } from '@orkestrel/mcp/server'
|
|
1776
|
+
* import { createToolManager } from '@orkestrel/tool'
|
|
1635
1777
|
*
|
|
1636
1778
|
* const mcp = createMCPServer({ identity: { name: 'docs', version: '1.0.0' }, tools: createToolManager() })
|
|
1637
|
-
*
|
|
1779
|
+
* // Claims the MCP upgrade at ws://…/mcp:
|
|
1780
|
+
* server.upgrade(createWebSocketServer(createMCPLegacy(mcp), { emitter: server.emitter })) // answers `initialize` too; pass `mcp` alone for modern-only
|
|
1638
1781
|
* ```
|
|
1639
1782
|
*/
|
|
1640
1783
|
function createWebSocketServer(mcp, options) {
|
|
1641
1784
|
const path = options.path ?? "/mcp";
|
|
1642
1785
|
const subprotocol = options.subprotocol ?? "mcp";
|
|
1643
|
-
const live = /* @__PURE__ */ new
|
|
1786
|
+
const live = /* @__PURE__ */ new Map();
|
|
1644
1787
|
options.emitter.on("stop", () => {
|
|
1645
|
-
for (const transport of live)
|
|
1788
|
+
for (const [transport, unbind] of live) {
|
|
1789
|
+
unbind();
|
|
1790
|
+
transport.close();
|
|
1791
|
+
}
|
|
1646
1792
|
});
|
|
1647
1793
|
return (request, socket, head) => {
|
|
1648
1794
|
const upgrade = request.headers["upgrade"];
|
|
@@ -1652,21 +1798,26 @@ function createWebSocketServer(mcp, options) {
|
|
|
1652
1798
|
if (!(0, _orkestrel_contract.isString)(key)) return false;
|
|
1653
1799
|
const version = request.headers["sec-websocket-version"];
|
|
1654
1800
|
if (!(0, _orkestrel_contract.isString)(version) || version !== _orkestrel_websocket.WEBSOCKET_VERSION) return false;
|
|
1801
|
+
const offer = request.headers["sec-websocket-protocol"];
|
|
1802
|
+
const protocol = (0, _orkestrel_contract.isString)(offer) && offer.split(",").some((candidate) => candidate.trim() === subprotocol) ? subprotocol : void 0;
|
|
1655
1803
|
const transport = new WebSocketServerTransport((0, _orkestrel_websocket.createNodeWebSocket)({
|
|
1656
1804
|
socket,
|
|
1657
1805
|
key,
|
|
1658
1806
|
head,
|
|
1659
|
-
protocol:
|
|
1807
|
+
...protocol === void 0 ? {} : { protocol }
|
|
1660
1808
|
}));
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1809
|
+
const unbind = (0, _src_core.bindServer)(mcp, bridgeMessageTransport(transport));
|
|
1810
|
+
live.set(transport, unbind);
|
|
1811
|
+
transport.emitter.on("close", () => {
|
|
1812
|
+
live.delete(transport);
|
|
1813
|
+
unbind();
|
|
1814
|
+
});
|
|
1664
1815
|
transport.start();
|
|
1665
1816
|
return true;
|
|
1666
1817
|
};
|
|
1667
1818
|
}
|
|
1668
1819
|
/**
|
|
1669
|
-
*
|
|
1820
|
+
* Creates the WebSocket CLIENT transport for an {@link import('@orkestrel/mcp').MCPClientInterface}
|
|
1670
1821
|
* — a {@link MCPClientTransportInterface} that drives a REMOTE MCP server over a WebSocket. The
|
|
1671
1822
|
* egress mirror of {@link createWebSocketServer} and the WebSocket sibling of {@link
|
|
1672
1823
|
* createHTTPClientTransport}.
|
|
@@ -1675,11 +1826,11 @@ function createWebSocketServer(mcp, options) {
|
|
|
1675
1826
|
* Hand it to `createMCPClient({ transport })`: `start()` (run by `client.connect()`) performs
|
|
1676
1827
|
* the RFC 6455 client handshake against `options.url` (accepting a `ws://` / `wss://` or an
|
|
1677
1828
|
* `http://` / `https://` URL — a `ws(s)` scheme is converted to `http(s)` for the underlying
|
|
1678
|
-
* upgrade request), validates the `Sec-WebSocket-Accept` (
|
|
1829
|
+
* upgrade request), validates the `Sec-WebSocket-Accept` (with `@orkestrel/websocket`'s
|
|
1679
1830
|
* `computeWebSocketAccept`), and opens a persistent bidirectional frame channel; each JSON-RPC
|
|
1680
1831
|
* message the client `send`s is written as one masked text frame, and each decoded reply is
|
|
1681
1832
|
* surfaced on the transport's `message` event for the client's id correlation. Add
|
|
1682
|
-
* `options.headers` (
|
|
1833
|
+
* `options.headers` (for example, an `Authorization` bearer) to reach a guarded server.
|
|
1683
1834
|
*
|
|
1684
1835
|
* @param options - `url` (the remote WebSocket endpoint; REQUIRED) and optional `headers`
|
|
1685
1836
|
* merged onto the upgrade request; see {@link WebSocketClientTransportOptions}
|
|
@@ -1687,8 +1838,8 @@ function createWebSocketServer(mcp, options) {
|
|
|
1687
1838
|
*
|
|
1688
1839
|
* @example
|
|
1689
1840
|
* ```ts
|
|
1690
|
-
* import { createMCPClient } from '@
|
|
1691
|
-
* import { createWebSocketClientTransport } from '@
|
|
1841
|
+
* import { createMCPClient } from '@orkestrel/mcp'
|
|
1842
|
+
* import { createWebSocketClientTransport } from '@orkestrel/mcp/server'
|
|
1692
1843
|
*
|
|
1693
1844
|
* const client = createMCPClient({
|
|
1694
1845
|
* transport: createWebSocketClientTransport({ url: 'ws://localhost:3000/mcp' }),
|
|
@@ -1701,28 +1852,30 @@ function createWebSocketClientTransport(options) {
|
|
|
1701
1852
|
return new WebSocketClientTransport(options);
|
|
1702
1853
|
}
|
|
1703
1854
|
/**
|
|
1704
|
-
*
|
|
1705
|
-
* — a {@link
|
|
1855
|
+
* Creates the stdio CLIENT transport for an {@link import('@orkestrel/mcp').MCPClientInterface}
|
|
1856
|
+
* — a {@link StdioClientTransportInterface} that spawns and drives a CHILD PROCESS MCP server
|
|
1706
1857
|
* over newline-delimited JSON-RPC on `stdin`/`stdout`, the stdio sibling of {@link
|
|
1707
1858
|
* createHTTPClientTransport} and {@link createWebSocketClientTransport}.
|
|
1708
1859
|
*
|
|
1709
1860
|
* @remarks
|
|
1710
1861
|
* Hand it to `createMCPClient({ transport })`: `start()` (run by `client.connect()`)
|
|
1711
1862
|
* spawns `options.command` with `options.args` and `options.env`, piping its
|
|
1712
|
-
* `stdin`/`stdout` for the JSON-RPC channel
|
|
1713
|
-
*
|
|
1863
|
+
* `stdin`/`stdout` for the JSON-RPC channel. The child's `stderr` is piped too, and
|
|
1864
|
+
* retained as a bounded tail this transport reports as `evidence` — the parent never
|
|
1865
|
+
* inherits it. Each JSON-RPC message the client `send`s is written as one
|
|
1714
1866
|
* newline-terminated line to the child's `stdin`; each decoded reply line from the
|
|
1715
1867
|
* child's `stdout` is surfaced on the transport's `message` event for the client's
|
|
1716
1868
|
* id correlation.
|
|
1717
1869
|
*
|
|
1718
1870
|
* @param options - `command` (the executable to spawn; REQUIRED), optional `args`,
|
|
1719
1871
|
* and optional `env`; see {@link StdioClientTransportOptions}
|
|
1720
|
-
* @returns A working {@link
|
|
1872
|
+
* @returns A working {@link StdioClientTransportInterface} over a child process's stdio,
|
|
1873
|
+
* whose `evidence` carries the supervised child's bounded stderr tail
|
|
1721
1874
|
*
|
|
1722
1875
|
* @example
|
|
1723
1876
|
* ```ts
|
|
1724
|
-
* import { createMCPClient } from '@
|
|
1725
|
-
* import { createStdioClientTransport } from '@
|
|
1877
|
+
* import { createMCPClient } from '@orkestrel/mcp'
|
|
1878
|
+
* import { createStdioClientTransport } from '@orkestrel/mcp/server'
|
|
1726
1879
|
*
|
|
1727
1880
|
* const client = createMCPClient({
|
|
1728
1881
|
* transport: createStdioClientTransport({ command: 'node', args: ['./server.js'] }),
|
|
@@ -1735,16 +1888,16 @@ function createStdioClientTransport(options) {
|
|
|
1735
1888
|
return new StdioClientTransport(options);
|
|
1736
1889
|
}
|
|
1737
1890
|
/**
|
|
1738
|
-
*
|
|
1891
|
+
* Creates the MCP stdio transport INGRESS — pumps a transport-agnostic {@link
|
|
1739
1892
|
* MCPDispatcherInterface} over newline-delimited JSON-RPC on `stdin`/`stdout` (or an
|
|
1740
1893
|
* injected stream pair), the stdio mirror of {@link createWebSocketServer}.
|
|
1741
1894
|
*
|
|
1742
1895
|
* @remarks
|
|
1743
1896
|
* Wraps `options.input` (default `process.stdin`) / `options.output` (default
|
|
1744
1897
|
* `process.stdout`) in a {@link import('./transports/StdioServerTransport.js').StdioServerTransport}
|
|
1745
|
-
* and pipes it through the core {@link import('@
|
|
1746
|
-
*
|
|
1747
|
-
* import('@
|
|
1898
|
+
* and pipes it through the core {@link import('@orkestrel/mcp').MCPTransportInterface} port
|
|
1899
|
+
* through {@link import('./helpers.js').bridgeMessageTransport} + {@link
|
|
1900
|
+
* import('@orkestrel/mcp').bindServer}: each inbound REQUEST runs through `mcp.dispatch`, and
|
|
1748
1901
|
* a defined response is written back as a newline-terminated line — a NOTIFICATION
|
|
1749
1902
|
* writes nothing, and a non-request message is ignored. A `dispatch` / `send` fault
|
|
1750
1903
|
* surfaces on `mcp.emitter`'s `error` event rather than escaping the (async) message
|
|
@@ -1757,21 +1910,24 @@ function createStdioClientTransport(options) {
|
|
|
1757
1910
|
*
|
|
1758
1911
|
* @example
|
|
1759
1912
|
* ```ts
|
|
1760
|
-
* import {
|
|
1761
|
-
* import { createStdioServer } from '@
|
|
1913
|
+
* import { createMCPLegacy, createMCPServer } from '@orkestrel/mcp'
|
|
1914
|
+
* import { createStdioServer } from '@orkestrel/mcp/server'
|
|
1915
|
+
* import { createToolManager } from '@orkestrel/tool'
|
|
1762
1916
|
*
|
|
1763
1917
|
* const mcp = createMCPServer({ identity: { name: 'docs', version: '1.0.0' }, tools: createToolManager() })
|
|
1764
|
-
*
|
|
1918
|
+
* // An MCP client now connects over this process's stdio:
|
|
1919
|
+
* createStdioServer(createMCPLegacy(mcp)).start() // answers `initialize` too; pass `mcp` alone for modern-only
|
|
1765
1920
|
* ```
|
|
1766
1921
|
*/
|
|
1767
1922
|
function createStdioServer(mcp, options) {
|
|
1768
1923
|
const transport = new StdioServerTransport(options?.input ?? process.stdin, options?.output ?? process.stdout);
|
|
1769
|
-
(0, _src_core.bindServer)(mcp, bridgeMessageTransport(transport));
|
|
1924
|
+
const unbind = (0, _src_core.bindServer)(mcp, bridgeMessageTransport(transport));
|
|
1770
1925
|
return {
|
|
1771
1926
|
start() {
|
|
1772
1927
|
transport.start();
|
|
1773
1928
|
},
|
|
1774
1929
|
stop() {
|
|
1930
|
+
unbind();
|
|
1775
1931
|
transport.close();
|
|
1776
1932
|
}
|
|
1777
1933
|
};
|
|
@@ -1779,9 +1935,9 @@ function createStdioServer(mcp, options) {
|
|
|
1779
1935
|
//#endregion
|
|
1780
1936
|
//#region src/server/middlewares.ts
|
|
1781
1937
|
/**
|
|
1782
|
-
*
|
|
1938
|
+
* Creates the native MCP session {@link MiddlewareHandler} — the plug-and-play stateful layer
|
|
1783
1939
|
* that fronts a session-agnostic {@link import('./factories.js').createMCPRoutes}. Compose it
|
|
1784
|
-
*
|
|
1940
|
+
* with `router.use(createMCPSession())` (or the equivalent middleware seam), mirroring any
|
|
1785
1941
|
* other closure-scoped stateful middleware. Has NO dependency on `@orkestrel/middleware` — the
|
|
1786
1942
|
* session store, mint-on-`initialize`, and resumable stream are all native to this package.
|
|
1787
1943
|
*
|
|
@@ -1790,11 +1946,11 @@ function createStdioServer(mcp, options) {
|
|
|
1790
1946
|
* `path` (default {@link DEFAULT_MCP_PATH}); a request to any other path passes straight
|
|
1791
1947
|
* through (`next()`).
|
|
1792
1948
|
*
|
|
1793
|
-
* A modern-shaped POST also passes straight through
|
|
1949
|
+
* A modern-shaped POST also passes straight through with `next()`, ignoring any session id.
|
|
1794
1950
|
* The remaining behavior is the legacy session layer:
|
|
1795
1951
|
*
|
|
1796
1952
|
* - **`POST {path}`.** Buffers `const text = await request.text()` (so the downstream route
|
|
1797
|
-
* can re-read it
|
|
1953
|
+
* can re-read it from a freshly-built forwarded `Request`). Resolves a session through {@link
|
|
1798
1954
|
* readSessionHeader}: a VALID id touches the entry and sets `context.state.session`; an
|
|
1799
1955
|
* ABSENT / unknown id whose (guarded) body parses to an `initialize` request ({@link
|
|
1800
1956
|
* isInitializeRequest}) MINTS a fresh {@link MCPSession} (`crypto.randomUUID()`, `capacity`)
|
|
@@ -1809,7 +1965,7 @@ function createStdioServer(mcp, options) {
|
|
|
1809
1965
|
* a `DELETE` arriving while the request was suspended is not undone.
|
|
1810
1966
|
* - **`GET {path}`.** Resolves the session the same way (no mint — only `initialize` mints);
|
|
1811
1967
|
* an invalid / unknown id is the same `404`. A valid session opens the resumable
|
|
1812
|
-
* server→client stream
|
|
1968
|
+
* server→client stream through `@orkestrel/server`'s {@link import('@orkestrel/server').openStream}:
|
|
1813
1969
|
* replays every event after the client's `Last-Event-ID` ({@link readLastEventId}) BEFORE
|
|
1814
1970
|
* attaching the stream for live pushes, then attaches; cancellation of the streamed response
|
|
1815
1971
|
* body composes with `request.signal` and detaches it. Long-lived — never `end()`ed here.
|
|
@@ -1833,12 +1989,14 @@ function createStdioServer(mcp, options) {
|
|
|
1833
1989
|
*
|
|
1834
1990
|
* @example
|
|
1835
1991
|
* ```ts
|
|
1836
|
-
* import {
|
|
1837
|
-
* import { createMCPRoutes, createMCPSession } from '@
|
|
1992
|
+
* import { createMCPLegacy, createMCPServer } from '@orkestrel/mcp'
|
|
1993
|
+
* import { createMCPRoutes, createMCPSession } from '@orkestrel/mcp/server'
|
|
1994
|
+
* import { createToolManager } from '@orkestrel/tool'
|
|
1838
1995
|
*
|
|
1839
1996
|
* const mcp = createMCPServer({ identity: { name: 'docs', version: '1.0.0' }, tools: createToolManager() })
|
|
1840
1997
|
* router.use(createMCPSession({ ttl: 60_000 })) // stateful: mint + validate + resumable GET / DELETE
|
|
1841
|
-
*
|
|
1998
|
+
* // The route stays session-agnostic:
|
|
1999
|
+
* router.add(createMCPRoutes(createMCPLegacy(mcp))) // answers `initialize` too; pass `mcp` alone for modern-only
|
|
1842
2000
|
* ```
|
|
1843
2001
|
*/
|
|
1844
2002
|
function createMCPSession(options) {
|