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