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