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