@orkestrel/mcp 0.0.12 → 0.0.13
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 +47 -1
- package/dist/src/browser/index.d.ts +36 -24
- package/dist/src/browser/index.js +19 -13
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/core/index.cjs +4061 -983
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +4024 -1069
- package/dist/src/core/index.d.ts +4024 -1069
- package/dist/src/core/index.js +3982 -973
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +264 -108
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +220 -76
- package/dist/src/server/index.d.ts +220 -76
- package/dist/src/server/index.js +265 -112
- package/dist/src/server/index.js.map +1 -1
- package/package.json +7 -6
|
@@ -99,6 +99,55 @@ function createReadableStream(pull, cancel) {
|
|
|
99
99
|
});
|
|
100
100
|
}
|
|
101
101
|
/**
|
|
102
|
+
* Pump a controlled held-open exchange onto an open SSE stream — one `data:` event per
|
|
103
|
+
* notification in order, then the terminating response — and END the exchange however the
|
|
104
|
+
* pump leaves.
|
|
105
|
+
*
|
|
106
|
+
* @remarks
|
|
107
|
+
* The Streamable-HTTP twin of {@link import('@src/core').sendStream}, and it owns exactly what
|
|
108
|
+
* that owns. The `finally` releases the exchange on EVERY exit — the normal terminal, a
|
|
109
|
+
* producer that threw, a `write` that threw, and an abort alike — because nothing else will:
|
|
110
|
+
* a request whose client vanished cancels nothing by itself, so an exchange this pump walks
|
|
111
|
+
* away from keeps its producer, its request lifetime, and its live subscription slot forever.
|
|
112
|
+
* The exchange is released BEFORE the body ends, so the slot is already back when the response
|
|
113
|
+
* completes.
|
|
114
|
+
*
|
|
115
|
+
* Total (§14) — never throws and never rejects. A held-open SSE response has already sent its
|
|
116
|
+
* headers and part of its body, so there is no failure the transport could still convert into
|
|
117
|
+
* a different answer; the honest end of a broken stream is a closed one, and the fault itself
|
|
118
|
+
* is already legible on `server.emitter`'s `error` event, which is where a contained fault
|
|
119
|
+
* belongs.
|
|
120
|
+
*
|
|
121
|
+
* @param stream - The controlled held-open answer to write out and then end
|
|
122
|
+
* @param sse - The open SSE stream to write each serialized message onto
|
|
123
|
+
* @returns Resolves once the exchange has ended and the SSE body has been closed
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```ts
|
|
127
|
+
* const answer = await mcp.dispatch(invocation, { signal: disconnect.signal })
|
|
128
|
+
* if (answer !== undefined && Symbol.asyncIterator in answer) {
|
|
129
|
+
* const sse = openStream()
|
|
130
|
+
* queueMicrotask(() => void sendEventStream(answer, sse))
|
|
131
|
+
* }
|
|
132
|
+
* ```
|
|
133
|
+
*/
|
|
134
|
+
async function sendEventStream(stream, sse) {
|
|
135
|
+
try {
|
|
136
|
+
try {
|
|
137
|
+
let next = await stream.next();
|
|
138
|
+
while (next.done !== true) {
|
|
139
|
+
sse.write({ data: JSON.stringify(next.value) });
|
|
140
|
+
next = await stream.next();
|
|
141
|
+
}
|
|
142
|
+
sse.write({ data: JSON.stringify(next.value) });
|
|
143
|
+
} finally {
|
|
144
|
+
await stream[Symbol.asyncDispose]();
|
|
145
|
+
}
|
|
146
|
+
} catch {} finally {
|
|
147
|
+
sse.end();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
102
151
|
* Whether the request's `Accept` header opts into a Server-Sent-Events response.
|
|
103
152
|
*
|
|
104
153
|
* @remarks
|
|
@@ -185,9 +234,9 @@ function readLastEventId(request) {
|
|
|
185
234
|
* JSON-RPC error body.
|
|
186
235
|
*
|
|
187
236
|
* @remarks
|
|
188
|
-
* Returns `Response.json(buildJSONRPCError(
|
|
189
|
-
* { status: 404 })`, mirroring `createMCPRoutes`'s `400` transport-failure shape (a
|
|
190
|
-
* JSON-RPC error BODY with
|
|
237
|
+
* Returns `Response.json(buildJSONRPCError(undefined, JSONRPC_INVALID_REQUEST, 'Session not
|
|
238
|
+
* found'), { status: 404 })`, mirroring `createMCPRoutes`'s `400` transport-failure shape (a
|
|
239
|
+
* JSON-RPC error BODY with NO id) but at the session-not-found status. Shared by
|
|
191
240
|
* every {@link import('./middlewares.js').createMCPSession} validation site — the
|
|
192
241
|
* non-`initialize` `POST` path, the resumable `GET {path}` open, and the `DELETE {path}`
|
|
193
242
|
* session-end (each a missing / unknown / TTL-evicted id) — so the single `404` envelope
|
|
@@ -196,7 +245,7 @@ function readLastEventId(request) {
|
|
|
196
245
|
* @returns The `404` JSON-RPC error `Response`
|
|
197
246
|
*/
|
|
198
247
|
function rejectUnknownSession() {
|
|
199
|
-
return Response.json((0, _src_core.buildJSONRPCError)(
|
|
248
|
+
return Response.json((0, _src_core.buildJSONRPCError)(void 0, _src_core.JSONRPC_INVALID_REQUEST, "Session not found"), { status: 404 });
|
|
200
249
|
}
|
|
201
250
|
/**
|
|
202
251
|
* Decode a `fetch` Response's Server-Sent-Events body into the JSON-RPC messages it
|
|
@@ -303,7 +352,7 @@ function extractLines(buffer, chunk) {
|
|
|
303
352
|
}
|
|
304
353
|
/**
|
|
305
354
|
* Decode and deliver each complete newline-framed line onto a {@link
|
|
306
|
-
*
|
|
355
|
+
* MCPClientTransportEventMap} emitter — the shared per-chunk dispatch step both stdio
|
|
307
356
|
* transports (client and server) run their {@link extractLines} output through.
|
|
308
357
|
*
|
|
309
358
|
* @remarks
|
|
@@ -328,7 +377,7 @@ function dispatchLines(emitter, lines) {
|
|
|
328
377
|
}
|
|
329
378
|
}
|
|
330
379
|
/**
|
|
331
|
-
* Bridge a message-channel {@link
|
|
380
|
+
* Bridge a message-channel {@link MCPClientTransportInterface} (the shape the stdio and
|
|
332
381
|
* WebSocket SERVER transports already implement) into the environment-agnostic
|
|
333
382
|
* {@link import('@src/core').MCPTransportInterface} port — the adapter
|
|
334
383
|
* {@link import('./factories.js').createStdioServer} and {@link
|
|
@@ -340,11 +389,22 @@ function dispatchLines(emitter, lines) {
|
|
|
340
389
|
* `send` decodes the already-serialized reply string back to a {@link JSONRPCMessage}
|
|
341
390
|
* and writes it via `transport.send` (the SAME `JSON.stringify` the underlying
|
|
342
391
|
* transport already performs, so the wire bytes are unchanged). `listen` filters
|
|
343
|
-
* `transport`'s `message` event to
|
|
344
|
-
* exactly as the prior hand-rolled pumps did — and re-serializes each one
|
|
345
|
-
* string for `bindServer`. `closed` bridges `transport`'s `close` event. `close`
|
|
392
|
+
* `transport`'s `message` event to INVOCATIONS ONLY — requests and notifications, never a
|
|
393
|
+
* stray response, exactly as the prior hand-rolled pumps did — and re-serializes each one
|
|
394
|
+
* back to a string for `bindServer`. `closed` bridges `transport`'s `close` event. `close`
|
|
346
395
|
* closes the underlying `transport`.
|
|
347
396
|
*
|
|
397
|
+
* @remarks A message crossing this bridge is decoded and re-encoded TWICE, and that is
|
|
398
|
+
* ACCEPTED rather than accidental. Inbound: the carrier already parsed the frame into a
|
|
399
|
+
* {@link JSONRPCMessage}, and `listen` re-serializes it so `bindServer` can decode it again
|
|
400
|
+
* under the server's own `limit`. Outbound: `bindServer` serialized the reply, `send` parses
|
|
401
|
+
* it back, and the carrier stringifies it once more. The cost is two extra `JSON.parse` /
|
|
402
|
+
* `JSON.stringify` round trips per message, paid to keep ONE pump in the core binder instead
|
|
403
|
+
* of a hand-rolled one per carrier. It is BOUNDED rather than unbounded because the binder
|
|
404
|
+
* decodes within `server.limit.message`, so an oversized frame is refused before the second
|
|
405
|
+
* decode rather than after it. Removing the cost means giving `MCPTransportInterface` a
|
|
406
|
+
* message-shaped face beside its string one, which every transport would then carry.
|
|
407
|
+
*
|
|
348
408
|
* @remarks Per {@link import('@src/core').MCPTransportInterface}, `listen`/`closed`
|
|
349
409
|
* each hold THE SINGLE current handler (a second call REPLACES the first, never adds).
|
|
350
410
|
* Since the underlying `transport.emitter` is ADD-based (`on` subscribes, never
|
|
@@ -372,7 +432,7 @@ function bridgeMessageTransport(transport) {
|
|
|
372
432
|
let onMessage;
|
|
373
433
|
let onClosed;
|
|
374
434
|
transport.emitter.on("message", (message) => {
|
|
375
|
-
if (!(0, _src_core.
|
|
435
|
+
if (!(0, _src_core.isJSONRPCInvocation)(message)) return;
|
|
376
436
|
onMessage?.(JSON.stringify(message));
|
|
377
437
|
});
|
|
378
438
|
transport.emitter.on("close", () => {
|
|
@@ -407,7 +467,7 @@ function bridgeMessageTransport(transport) {
|
|
|
407
467
|
* the active session. Messages name the expected value but never echo the client-supplied one.
|
|
408
468
|
*
|
|
409
469
|
* @param request - The HTTP request carrying the headers
|
|
410
|
-
* @param reference - The parsed
|
|
470
|
+
* @param reference - The parsed invocation body, or the active legacy session version
|
|
411
471
|
* @returns The first header issue, or `undefined` when the applicable headers agree
|
|
412
472
|
*
|
|
413
473
|
* @example
|
|
@@ -485,7 +545,7 @@ function inferHeaderIssue(request, reference) {
|
|
|
485
545
|
* A supported legacy request is pinned exactly. A modern, malformed, absent, or unsupported
|
|
486
546
|
* request selects the newest supported legacy revision, matching the core initialize result.
|
|
487
547
|
*
|
|
488
|
-
* @param request - The legacy initialize
|
|
548
|
+
* @param request - The legacy initialize invocation
|
|
489
549
|
* @returns The negotiated legacy protocol revision
|
|
490
550
|
*/
|
|
491
551
|
function inferLegacyVersion(request) {
|
|
@@ -517,35 +577,78 @@ function inferStatus(response, era) {
|
|
|
517
577
|
//#endregion
|
|
518
578
|
//#region src/server/transports/HTTPDisconnect.ts
|
|
519
579
|
/**
|
|
520
|
-
*
|
|
580
|
+
* Compose one incoming HTTP request lifetime with one MCP-owned SSE response lifetime.
|
|
521
581
|
*
|
|
522
582
|
* @remarks
|
|
523
|
-
*
|
|
524
|
-
*
|
|
525
|
-
*
|
|
526
|
-
*
|
|
527
|
-
* the
|
|
528
|
-
*
|
|
529
|
-
*
|
|
583
|
+
* The composed {@link signal} observes request abort and EVERY way this response can end
|
|
584
|
+
* without one: consumer cancellation of the bridged body, a forwarding failure mid-pump, and a
|
|
585
|
+
* keepalive tick that finds the SSE stream already closed. That last pair is the whole point of
|
|
586
|
+
* the composition — a client that vanishes mid-stream aborts nothing by itself, so unless this
|
|
587
|
+
* object raises the signal on its own failure paths, the handler, the controlled stream, and
|
|
588
|
+
* the producer behind them all keep running for a response that can no longer be written.
|
|
589
|
+
* Graceful upstream completion is the one terminal that does NOT abort: the body simply closes,
|
|
590
|
+
* because the exchange finished rather than ended.
|
|
591
|
+
*
|
|
592
|
+
* {@link bridge} preserves the source response status and headers, forwards its body bytes, and
|
|
593
|
+
* 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.
|
|
595
|
+
* It supplies no handler or session policy.
|
|
596
|
+
*
|
|
597
|
+
* The keepalive interval is a BUDGET, sanitized like every other numeric knob in this package:
|
|
598
|
+
* anything that is not a positive integer — `0`, a negative, a fractional value, `NaN`,
|
|
599
|
+
* `Infinity` — falls back to {@link DEFAULT_MCP_KEEPALIVE_INTERVAL}, and a larger value clamps
|
|
600
|
+
* to Node's `2_147_483_647` ms timer maximum. None may reach the platform's timer floor, where
|
|
601
|
+
* an idle-liveness tick becomes the polling this package forbids everywhere else.
|
|
602
|
+
*
|
|
603
|
+
* @example
|
|
604
|
+
* ```ts
|
|
605
|
+
* import { HTTPDisconnect } from '@orkestrel/mcp/server'
|
|
606
|
+
* import { openStream } from '@orkestrel/server'
|
|
607
|
+
*
|
|
608
|
+
* const disconnect = new HTTPDisconnect(request.signal, { interval: 15_000 })
|
|
609
|
+
* const stream = openStream()
|
|
610
|
+
* const response = disconnect.bridge(stream)
|
|
611
|
+
* ```
|
|
530
612
|
*/
|
|
531
613
|
var HTTPDisconnect = class {
|
|
532
|
-
#
|
|
614
|
+
#response = new AbortController();
|
|
533
615
|
#lifecycle = new AbortController();
|
|
534
616
|
#interval;
|
|
535
617
|
#signal;
|
|
536
618
|
#timer;
|
|
619
|
+
#pulling = false;
|
|
620
|
+
/**
|
|
621
|
+
* Create the lifecycle composition for one request and its future SSE response.
|
|
622
|
+
*
|
|
623
|
+
* @param signal - The incoming request signal
|
|
624
|
+
* @param options - Optional keepalive `interval` in milliseconds; an invalid value falls back
|
|
625
|
+
* to {@link DEFAULT_MCP_KEEPALIVE_INTERVAL}, and one above Node's timer maximum clamps to it
|
|
626
|
+
*/
|
|
537
627
|
constructor(signal, options) {
|
|
538
|
-
|
|
539
|
-
this.#
|
|
628
|
+
const interval = (0, _orkestrel_contract.sanitizeBudget)(options?.interval, DEFAULT_MCP_KEEPALIVE_INTERVAL);
|
|
629
|
+
this.#interval = interval > 0 ? Math.min(interval, 2147483647) : DEFAULT_MCP_KEEPALIVE_INTERVAL;
|
|
630
|
+
this.#signal = AbortSignal.any([signal, this.#response.signal]);
|
|
540
631
|
}
|
|
632
|
+
/**
|
|
633
|
+
* The signal aborted by the incoming request, or by any end of this response that is not
|
|
634
|
+
* its graceful completion.
|
|
635
|
+
*
|
|
636
|
+
* @returns The composed lifecycle signal
|
|
637
|
+
*/
|
|
541
638
|
get signal() {
|
|
542
639
|
return this.#signal;
|
|
543
640
|
}
|
|
544
641
|
/**
|
|
545
|
-
* Bridge
|
|
642
|
+
* Bridge one open SSE response through cancellation-aware byte forwarding and keepalives.
|
|
643
|
+
*
|
|
644
|
+
* Consumer cancellation, a read failure while forwarding, and a keepalive tick that finds the
|
|
645
|
+
* SSE stream already closed each abort {@link signal}; consumer cancellation also cancels the
|
|
646
|
+
* upstream reader. Upstream completion closes the returned body without inventing an abort.
|
|
647
|
+
* Every terminal path clears the keepalive timer and detaches the bridge-owned abort listener.
|
|
546
648
|
*
|
|
547
649
|
* @param stream - The open SSE stream whose response will be consumed by the HTTP writer
|
|
548
|
-
* @returns A response
|
|
650
|
+
* @returns A one-use response preserving status, status text, headers, and SSE body bytes
|
|
651
|
+
* @throws When the supplied SSE response has no body
|
|
549
652
|
*/
|
|
550
653
|
bridge(stream) {
|
|
551
654
|
const response = stream.response;
|
|
@@ -553,28 +656,32 @@ var HTTPDisconnect = class {
|
|
|
553
656
|
if (body === null) throw new Error("MCP SSE response has no body");
|
|
554
657
|
const reader = body.getReader();
|
|
555
658
|
this.#timer = setInterval(() => {
|
|
556
|
-
if (stream.closed)
|
|
557
|
-
|
|
659
|
+
if (stream.closed) {
|
|
660
|
+
if (!this.#pulling) this.#abort();
|
|
661
|
+
} else stream.comment(SSE_KEEPALIVE_COMMENT);
|
|
558
662
|
}, this.#interval);
|
|
559
|
-
this.#signal.addEventListener("abort", () => this.#
|
|
663
|
+
this.#signal.addEventListener("abort", () => this.#release(), {
|
|
560
664
|
once: true,
|
|
561
665
|
signal: this.#lifecycle.signal
|
|
562
666
|
});
|
|
563
|
-
if (this.#signal.aborted
|
|
667
|
+
if (this.#signal.aborted) this.#release();
|
|
668
|
+
else if (stream.closed) this.#abort();
|
|
564
669
|
return new Response(createReadableStream(async (controller) => {
|
|
670
|
+
this.#pulling = true;
|
|
565
671
|
try {
|
|
566
672
|
const chunk = await reader.read();
|
|
567
673
|
if (chunk.done) {
|
|
568
|
-
this.#
|
|
674
|
+
this.#release();
|
|
569
675
|
controller.close();
|
|
570
676
|
} else controller.enqueue(chunk.value);
|
|
571
677
|
} catch (error) {
|
|
572
|
-
this.#
|
|
678
|
+
this.#abort();
|
|
573
679
|
controller.error(error);
|
|
680
|
+
} finally {
|
|
681
|
+
this.#pulling = false;
|
|
574
682
|
}
|
|
575
683
|
}, async (reason) => {
|
|
576
|
-
this.#abort
|
|
577
|
-
this.#stop();
|
|
684
|
+
this.#abort();
|
|
578
685
|
await reader.cancel(reason);
|
|
579
686
|
}), {
|
|
580
687
|
status: response.status,
|
|
@@ -582,13 +689,17 @@ var HTTPDisconnect = class {
|
|
|
582
689
|
headers: response.headers
|
|
583
690
|
});
|
|
584
691
|
}
|
|
585
|
-
#
|
|
692
|
+
#release() {
|
|
586
693
|
if (this.#timer !== void 0) {
|
|
587
694
|
clearInterval(this.#timer);
|
|
588
695
|
this.#timer = void 0;
|
|
589
696
|
}
|
|
590
697
|
this.#lifecycle.abort();
|
|
591
698
|
}
|
|
699
|
+
#abort() {
|
|
700
|
+
this.#release();
|
|
701
|
+
this.#response.abort();
|
|
702
|
+
}
|
|
592
703
|
};
|
|
593
704
|
//#endregion
|
|
594
705
|
//#region src/server/handlers.ts
|
|
@@ -608,18 +719,18 @@ var HTTPDisconnect = class {
|
|
|
608
719
|
* defined value is added to `MCPDispatchOptions`, while `undefined` is omitted.
|
|
609
720
|
*
|
|
610
721
|
* @typeParam TState - The consumer's opaque per-request route state type
|
|
611
|
-
* @param mcp - The transport-agnostic MCP
|
|
722
|
+
* @param mcp - The transport-agnostic MCP dispatcher to dispatch through
|
|
612
723
|
* @param options - Optional streaming, origin-validation, SSE keepalive, and caller-extraction options
|
|
613
724
|
* @returns A request handler for the stateless MCP POST route
|
|
614
725
|
*
|
|
615
726
|
* @example
|
|
616
727
|
* ```ts
|
|
617
|
-
* import { createMCPServer } from '@orkestrel/mcp'
|
|
728
|
+
* import { createMCPLegacy, createMCPServer } from '@orkestrel/mcp'
|
|
618
729
|
* import { createMCPPostHandler } from '@orkestrel/mcp/server'
|
|
619
730
|
* import { createToolManager } from '@orkestrel/tool'
|
|
620
731
|
*
|
|
621
732
|
* const mcp = createMCPServer({ identity: { name: 'docs', version: '1.0.0' }, tools: createToolManager() })
|
|
622
|
-
* const handler = createMCPPostHandler(mcp, { streaming: true })
|
|
733
|
+
* const handler = createMCPPostHandler(createMCPLegacy(mcp), { streaming: true })
|
|
623
734
|
* await handler(new Request('http://localhost/mcp', {
|
|
624
735
|
* method: 'POST',
|
|
625
736
|
* body: '{"jsonrpc":"2.0","method":"ping","id":1}',
|
|
@@ -635,23 +746,23 @@ function createMCPPostHandler(mcp, options) {
|
|
|
635
746
|
try {
|
|
636
747
|
text = await request.text();
|
|
637
748
|
} catch {
|
|
638
|
-
return Response.json((0, _src_core.buildJSONRPCError)(
|
|
749
|
+
return Response.json((0, _src_core.buildJSONRPCError)(void 0, _src_core.JSONRPC_PARSE_ERROR, "Parse error"), { status: 400 });
|
|
639
750
|
}
|
|
640
751
|
let parsed;
|
|
641
752
|
try {
|
|
642
753
|
parsed = JSON.parse(text);
|
|
643
754
|
} catch {
|
|
644
|
-
return Response.json((0, _src_core.buildJSONRPCError)(
|
|
755
|
+
return Response.json((0, _src_core.buildJSONRPCError)(void 0, _src_core.JSONRPC_PARSE_ERROR, "Parse error"), { status: 400 });
|
|
645
756
|
}
|
|
646
|
-
const
|
|
647
|
-
if (
|
|
648
|
-
const era = (0, _src_core.isModernRequest)(
|
|
649
|
-
const id =
|
|
757
|
+
const invocation = (0, _src_core.parseJSONRPCMessage)(parsed);
|
|
758
|
+
if (invocation === void 0 || !("method" in invocation)) return Response.json((0, _src_core.buildJSONRPCError)(void 0, _src_core.JSONRPC_INVALID_REQUEST, "Invalid Request"), { status: 400 });
|
|
759
|
+
const era = (0, _src_core.isModernRequest)(invocation) ? "modern" : "legacy";
|
|
760
|
+
const id = invocation.id;
|
|
650
761
|
const protocol = request.headers.get(MCP_PROTOCOL_VERSION_HEADER);
|
|
651
762
|
if (era === "modern") {
|
|
652
|
-
if ((0, _src_core.parseRequestContext)(
|
|
763
|
+
if ((0, _src_core.parseRequestContext)(invocation) === void 0) return Response.json((0, _src_core.buildJSONRPCError)(id, _src_core.JSONRPC_INVALID_PARAMS, "Invalid params: malformed modern request metadata"), { status: 400 });
|
|
653
764
|
}
|
|
654
|
-
const issue = inferHeaderIssue(request,
|
|
765
|
+
const issue = inferHeaderIssue(request, invocation);
|
|
655
766
|
if (issue !== void 0) return Response.json((0, _src_core.buildJSONRPCError)(id, _src_core.MCP_HEADER_MISMATCH, issue.message), { status: 400 });
|
|
656
767
|
if (era === "legacy") {
|
|
657
768
|
if (protocol !== null && !(0, _src_core.isMCPVersion)(protocol)) return Response.json((0, _src_core.buildJSONRPCError)(id, _src_core.MCP_UNSUPPORTED_VERSION, `Unsupported MCP protocol version '${protocol}'`, {
|
|
@@ -661,25 +772,14 @@ function createMCPPostHandler(mcp, options) {
|
|
|
661
772
|
}
|
|
662
773
|
const disconnect = new HTTPDisconnect(request.signal, options?.keepalive);
|
|
663
774
|
const caller = options?.caller?.(request, context);
|
|
664
|
-
const response = await mcp.dispatch(
|
|
775
|
+
const response = await mcp.dispatch(invocation, {
|
|
665
776
|
signal: disconnect.signal,
|
|
666
777
|
...caller === void 0 ? {} : { caller }
|
|
667
778
|
});
|
|
668
779
|
if (response !== void 0 && Symbol.asyncIterator in response) {
|
|
669
780
|
const stream = (0, _orkestrel_server.openStream)();
|
|
670
781
|
stream.response.headers.set(SSE_BUFFERING_HEADER, "no");
|
|
671
|
-
queueMicrotask(
|
|
672
|
-
try {
|
|
673
|
-
let next = await response.next();
|
|
674
|
-
while (!next.done) {
|
|
675
|
-
stream.write({ data: JSON.stringify(next.value) });
|
|
676
|
-
next = await response.next();
|
|
677
|
-
}
|
|
678
|
-
stream.write({ data: JSON.stringify(next.value) });
|
|
679
|
-
} catch {} finally {
|
|
680
|
-
stream.end();
|
|
681
|
-
}
|
|
682
|
-
});
|
|
782
|
+
queueMicrotask(() => void sendEventStream(response, stream));
|
|
683
783
|
return disconnect.bridge(stream);
|
|
684
784
|
}
|
|
685
785
|
const status = inferStatus(response, era);
|
|
@@ -698,7 +798,7 @@ function createMCPPostHandler(mcp, options) {
|
|
|
698
798
|
//#region src/server/transports/HTTPClientTransport.ts
|
|
699
799
|
/**
|
|
700
800
|
* The HTTP CLIENT transport for the Model Context Protocol — a
|
|
701
|
-
* {@link
|
|
801
|
+
* {@link MCPClientTransportInterface} that drives a REMOTE Streamable-HTTP MCP server over
|
|
702
802
|
* `fetch`, the egress mirror of the server's `createMCPRoutes`.
|
|
703
803
|
*
|
|
704
804
|
* @remarks
|
|
@@ -731,7 +831,7 @@ function createMCPPostHandler(mcp, options) {
|
|
|
731
831
|
* - **Total at the boundary (§14).** Every reply is narrowed (`parseJSONRPCMessage`,
|
|
732
832
|
* the SSE decoder) — a non-message reply is dropped, never asserted; a `fetch` /
|
|
733
833
|
* decode failure surfaces on the `error` event rather than escaping `send`.
|
|
734
|
-
* - **Observable (§13).** Owns the `emitter` ({@link
|
|
834
|
+
* - **Observable (§13).** Owns the `emitter` ({@link MCPClientTransportEventMap}); fires
|
|
735
835
|
* `message` per decoded reply, `error` on a fault, and `close` on `close()`.
|
|
736
836
|
*
|
|
737
837
|
* @example
|
|
@@ -762,6 +862,9 @@ var HTTPClientTransport = class {
|
|
|
762
862
|
get session() {
|
|
763
863
|
return this.#session;
|
|
764
864
|
}
|
|
865
|
+
get duplex() {
|
|
866
|
+
return false;
|
|
867
|
+
}
|
|
765
868
|
async start() {}
|
|
766
869
|
async send(message) {
|
|
767
870
|
let response;
|
|
@@ -791,11 +894,11 @@ var HTTPClientTransport = class {
|
|
|
791
894
|
this.#emitter.emit("close");
|
|
792
895
|
}
|
|
793
896
|
#buildHeaders(message) {
|
|
794
|
-
if ((0, _src_core.
|
|
795
|
-
const version = (
|
|
897
|
+
if ((0, _src_core.isModernRequest)(message)) {
|
|
898
|
+
const version = (0, _src_core.inferRequestVersion)(message);
|
|
796
899
|
const name = message.params?.["name"];
|
|
797
900
|
return {
|
|
798
|
-
...
|
|
901
|
+
...version === void 0 ? {} : { [MCP_PROTOCOL_VERSION_HEADER]: version },
|
|
799
902
|
[MCP_METHOD_HEADER]: message.method,
|
|
800
903
|
...message.method === "tools/call" && (0, _orkestrel_contract.isString)(name) ? { [MCP_NAME_HEADER]: name } : {}
|
|
801
904
|
};
|
|
@@ -946,12 +1049,12 @@ var MCPSession = class {
|
|
|
946
1049
|
/**
|
|
947
1050
|
* The per-connection JSON-RPC-over-WebSocket SERVER bridge — wraps a
|
|
948
1051
|
* {@link NodeWebSocketInterface} (the RFC 6455 wire wrapper) as a
|
|
949
|
-
* {@link
|
|
1052
|
+
* {@link MCPClientTransportInterface}, the bidirectional JSON-RPC message channel
|
|
950
1053
|
* `createWebSocketServer` pumps `mcp.dispatch` over and the egress mirror's
|
|
951
1054
|
* {@link import('./WebSocketClientTransport.js').WebSocketClientTransport} reuses.
|
|
952
1055
|
*
|
|
953
1056
|
* @remarks
|
|
954
|
-
* - **Reuses `
|
|
1057
|
+
* - **Reuses `MCPClientTransportInterface` (§21).** It IS the same generic carrier the HTTP
|
|
955
1058
|
* client transport implements — `emitter` (`message` / `close` / `error`), `start`,
|
|
956
1059
|
* `send`, `close` — so the WebSocket server and client both speak ONE transport contract,
|
|
957
1060
|
* no near-duplicate sibling interface. `session` is `undefined` (the stateless v1; a
|
|
@@ -969,7 +1072,7 @@ var MCPSession = class {
|
|
|
969
1072
|
* - **`close()`** closes the underlying socket (the RFC 6455 close handshake) and fires the
|
|
970
1073
|
* transport's `close` event (idempotent — a second `close`, or a socket-driven close, emits
|
|
971
1074
|
* once).
|
|
972
|
-
* - **Observable (§13).** Owns the `emitter` ({@link
|
|
1075
|
+
* - **Observable (§13).** Owns the `emitter` ({@link MCPClientTransportEventMap}); the emitter
|
|
973
1076
|
* isolates a listener throw (a buggy observer never corrupts the bridge). `error` is a
|
|
974
1077
|
* DOMAIN event (a transport-level fault), distinct from the emitter's listener-error channel.
|
|
975
1078
|
*/
|
|
@@ -986,6 +1089,9 @@ var WebSocketServerTransport = class {
|
|
|
986
1089
|
return this.#emitter;
|
|
987
1090
|
}
|
|
988
1091
|
get session() {}
|
|
1092
|
+
get duplex() {
|
|
1093
|
+
return true;
|
|
1094
|
+
}
|
|
989
1095
|
async start() {
|
|
990
1096
|
if (this.#started || this.#closed) return;
|
|
991
1097
|
this.#started = true;
|
|
@@ -1027,7 +1133,7 @@ var WebSocketServerTransport = class {
|
|
|
1027
1133
|
//#region src/server/transports/WebSocketClientTransport.ts
|
|
1028
1134
|
/**
|
|
1029
1135
|
* The WebSocket CLIENT transport for the Model Context Protocol — a
|
|
1030
|
-
* {@link
|
|
1136
|
+
* {@link MCPClientTransportInterface} that drives a REMOTE MCP server over a WebSocket, the
|
|
1031
1137
|
* egress mirror of {@link import('./factories.js').createWebSocketServer} and the WebSocket
|
|
1032
1138
|
* sibling of {@link import('./HTTPClientTransport.js').HTTPClientTransport}.
|
|
1033
1139
|
*
|
|
@@ -1040,6 +1146,12 @@ var WebSocketServerTransport = class {
|
|
|
1040
1146
|
* — a mismatch (or a non-`101` response, or a request error) REJECTS `start()` and the socket
|
|
1041
1147
|
* is destroyed. On success it wraps the raw upgraded socket in `createNodeWebSocket({ socket,
|
|
1042
1148
|
* head })` (CLIENT mode — no key → frames are MASKED per §5.3) and bridges its `message`.
|
|
1149
|
+
* - **The arriving socket is RE-ASKED for, never assumed.** `start()` suspends across that
|
|
1150
|
+
* connect and upgrade, so it re-checks the transport's state before installing anything: a
|
|
1151
|
+
* concurrent `start()` that already installed a socket, or a {@link close} that ended the
|
|
1152
|
+
* transport while the handshake was on the wire, both WIN — the socket that arrives late is
|
|
1153
|
+
* DESTROYED and never bound, so no orphan is left re-emitting frames at nobody. Both
|
|
1154
|
+
* `start()` calls still resolve; exactly one socket is ever bound.
|
|
1043
1155
|
* - **Inbound (`message`).** Each decoded text frame is `JSON.parse`d (guarded) and narrowed
|
|
1044
1156
|
* with `parseJSONRPCMessage` — a {@link JSONRPCMessage} re-emits on this transport's `message`
|
|
1045
1157
|
* event (the reply the {@link import('@src/core').MCPClientInterface} correlates by `id`); a
|
|
@@ -1050,7 +1162,7 @@ var WebSocketServerTransport = class {
|
|
|
1050
1162
|
* - **URL scheme.** `options.url` accepts a `ws://` / `wss://` URL or an `http://` / `https://`
|
|
1051
1163
|
* one; a `ws(s)` scheme is converted to `http(s)` for the underlying upgrade request (`wss`
|
|
1052
1164
|
* → TLS via `node:https`). Either reaches the same endpoint.
|
|
1053
|
-
* - **Observable (§13).** Owns the `emitter` ({@link
|
|
1165
|
+
* - **Observable (§13).** Owns the `emitter` ({@link MCPClientTransportEventMap}); every emit
|
|
1054
1166
|
* the emitter isolates a listener throw (a buggy observer never corrupts the transport);
|
|
1055
1167
|
* `error` is a DOMAIN event (a transport-level fault).
|
|
1056
1168
|
*
|
|
@@ -1076,6 +1188,9 @@ var WebSocketClientTransport = class {
|
|
|
1076
1188
|
return this.#emitter;
|
|
1077
1189
|
}
|
|
1078
1190
|
get session() {}
|
|
1191
|
+
get duplex() {
|
|
1192
|
+
return true;
|
|
1193
|
+
}
|
|
1079
1194
|
async start() {
|
|
1080
1195
|
if (this.#socket !== void 0) return;
|
|
1081
1196
|
this.#closed = false;
|
|
@@ -1104,6 +1219,11 @@ var WebSocketClientTransport = class {
|
|
|
1104
1219
|
reject(/* @__PURE__ */ new Error("WebSocket handshake failed: Sec-WebSocket-Accept mismatch"));
|
|
1105
1220
|
return;
|
|
1106
1221
|
}
|
|
1222
|
+
if (this.#closed || this.#socket !== void 0) {
|
|
1223
|
+
socket.destroy();
|
|
1224
|
+
resolve();
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1107
1227
|
const ws = (0, _orkestrel_websocket.createNodeWebSocket)({
|
|
1108
1228
|
socket,
|
|
1109
1229
|
head
|
|
@@ -1135,7 +1255,7 @@ var WebSocketClientTransport = class {
|
|
|
1135
1255
|
}
|
|
1136
1256
|
#bind(ws) {
|
|
1137
1257
|
ws.emitter.on("message", (text) => this.#receive(text));
|
|
1138
|
-
ws.emitter.on("close", () => this.#onClose());
|
|
1258
|
+
ws.emitter.on("close", () => this.#onClose(ws));
|
|
1139
1259
|
ws.emitter.on("error", (error) => this.#emitter.emit("error", error));
|
|
1140
1260
|
}
|
|
1141
1261
|
#receive(text) {
|
|
@@ -1153,8 +1273,8 @@ var WebSocketClientTransport = class {
|
|
|
1153
1273
|
}
|
|
1154
1274
|
this.#emitter.emit("message", message);
|
|
1155
1275
|
}
|
|
1156
|
-
#onClose() {
|
|
1157
|
-
if (this.#closed) return;
|
|
1276
|
+
#onClose(socket) {
|
|
1277
|
+
if (this.#closed || this.#socket !== socket) return;
|
|
1158
1278
|
this.#closed = true;
|
|
1159
1279
|
this.#socket = void 0;
|
|
1160
1280
|
this.#emitter.emit("close");
|
|
@@ -1171,7 +1291,7 @@ var WebSocketClientTransport = class {
|
|
|
1171
1291
|
//#region src/server/transports/StdioClientTransport.ts
|
|
1172
1292
|
/**
|
|
1173
1293
|
* The stdio CLIENT transport for the Model Context Protocol — a
|
|
1174
|
-
* {@link
|
|
1294
|
+
* {@link MCPClientTransportInterface} that drives a CHILD PROCESS MCP server over
|
|
1175
1295
|
* newline-delimited JSON-RPC on `stdin`/`stdout`, the stdio sibling of {@link
|
|
1176
1296
|
* import('./HTTPClientTransport.js').HTTPClientTransport} and {@link
|
|
1177
1297
|
* import('./WebSocketClientTransport.js').WebSocketClientTransport}.
|
|
@@ -1190,7 +1310,7 @@ var WebSocketClientTransport = class {
|
|
|
1190
1310
|
* - **Outbound (`send`).** `send(message)` writes one newline-terminated
|
|
1191
1311
|
* `JSON.stringify`d line to the child's `stdin`.
|
|
1192
1312
|
* - **`close()`** kills the child process and fires `close` (idempotent).
|
|
1193
|
-
* - **Observable (§13).** Owns the `emitter` ({@link
|
|
1313
|
+
* - **Observable (§13).** Owns the `emitter` ({@link MCPClientTransportEventMap}); the
|
|
1194
1314
|
* emitter isolates a listener throw; `error` is a DOMAIN event (a transport-level
|
|
1195
1315
|
* fault), distinct from the emitter's own listener-error channel.
|
|
1196
1316
|
*
|
|
@@ -1219,6 +1339,9 @@ var StdioClientTransport = class {
|
|
|
1219
1339
|
return this.#emitter;
|
|
1220
1340
|
}
|
|
1221
1341
|
get session() {}
|
|
1342
|
+
get duplex() {
|
|
1343
|
+
return true;
|
|
1344
|
+
}
|
|
1222
1345
|
async start() {
|
|
1223
1346
|
if (this.#child !== void 0) return;
|
|
1224
1347
|
this.#closed = false;
|
|
@@ -1233,7 +1356,7 @@ var StdioClientTransport = class {
|
|
|
1233
1356
|
});
|
|
1234
1357
|
this.#child = child;
|
|
1235
1358
|
child.stdout.on("data", (chunk) => this.#receive(chunk.toString()));
|
|
1236
|
-
child.on("close", () => this.#onClose());
|
|
1359
|
+
child.on("close", () => this.#onClose(child));
|
|
1237
1360
|
child.on("error", (error) => this.#emitter.emit("error", error));
|
|
1238
1361
|
}
|
|
1239
1362
|
async send(message) {
|
|
@@ -1254,8 +1377,8 @@ var StdioClientTransport = class {
|
|
|
1254
1377
|
this.#buffer = remainder;
|
|
1255
1378
|
dispatchLines(this.#emitter, lines);
|
|
1256
1379
|
}
|
|
1257
|
-
#onClose() {
|
|
1258
|
-
if (this.#closed) return;
|
|
1380
|
+
#onClose(child) {
|
|
1381
|
+
if (this.#closed || this.#child !== child) return;
|
|
1259
1382
|
this.#closed = true;
|
|
1260
1383
|
this.#child = void 0;
|
|
1261
1384
|
this.#emitter.emit("close");
|
|
@@ -1266,13 +1389,13 @@ var StdioClientTransport = class {
|
|
|
1266
1389
|
/**
|
|
1267
1390
|
* The stdio SERVER transport for the Model Context Protocol — wraps an injectable
|
|
1268
1391
|
* readable/writable stream pair (`process.stdin`/`process.stdout` in production, a
|
|
1269
|
-
* test double in tests) as a {@link
|
|
1392
|
+
* test double in tests) as a {@link MCPClientTransportInterface}, the newline-delimited
|
|
1270
1393
|
* JSON-RPC channel {@link import('../factories.js').createStdioServer} pumps
|
|
1271
1394
|
* `mcp.dispatch` over, the stdio mirror of {@link
|
|
1272
1395
|
* import('./WebSocketServerTransport.js').WebSocketServerTransport}.
|
|
1273
1396
|
*
|
|
1274
1397
|
* @remarks
|
|
1275
|
-
* - **Reuses `
|
|
1398
|
+
* - **Reuses `MCPClientTransportInterface` (§21).** The same generic carrier the HTTP
|
|
1276
1399
|
* and WebSocket server transports implement — `emitter` (`message` / `close` /
|
|
1277
1400
|
* `error`), `start`, `send`, `close`. `session` is `undefined` (the stateless v1).
|
|
1278
1401
|
* - **Inbound (`message`).** `start()` subscribes to `input`'s `data` event; each
|
|
@@ -1287,7 +1410,7 @@ var StdioClientTransport = class {
|
|
|
1287
1410
|
* - **`close()`** fires this transport's `close` (idempotent) — the injected streams
|
|
1288
1411
|
* are owned by the caller (typically `process.stdin`/`process.stdout`, which must
|
|
1289
1412
|
* never be closed out from under the process) and are not torn down here.
|
|
1290
|
-
* - **Observable (§13).** Owns the `emitter` ({@link
|
|
1413
|
+
* - **Observable (§13).** Owns the `emitter` ({@link MCPClientTransportEventMap}); the
|
|
1291
1414
|
* emitter isolates a listener throw; `error` is a DOMAIN event (a transport-level
|
|
1292
1415
|
* fault), distinct from the emitter's own listener-error channel.
|
|
1293
1416
|
*/
|
|
@@ -1307,6 +1430,9 @@ var StdioServerTransport = class {
|
|
|
1307
1430
|
return this.#emitter;
|
|
1308
1431
|
}
|
|
1309
1432
|
get session() {}
|
|
1433
|
+
get duplex() {
|
|
1434
|
+
return true;
|
|
1435
|
+
}
|
|
1310
1436
|
async start() {
|
|
1311
1437
|
if (this.#started || this.#closed) return;
|
|
1312
1438
|
this.#started = true;
|
|
@@ -1336,8 +1462,24 @@ var StdioServerTransport = class {
|
|
|
1336
1462
|
//#endregion
|
|
1337
1463
|
//#region src/server/factories.ts
|
|
1338
1464
|
/**
|
|
1465
|
+
* Adapt the installed server token primitives to the host-neutral MCP continuation port.
|
|
1466
|
+
*
|
|
1467
|
+
* @param secret - Current signing secret or `[current, ...older]` rotation list
|
|
1468
|
+
* @returns A continuation port that seals and opens opaque canonical state strings
|
|
1469
|
+
*/
|
|
1470
|
+
function createMCPContinuation(secret) {
|
|
1471
|
+
return {
|
|
1472
|
+
seal(value) {
|
|
1473
|
+
return (0, _orkestrel_server.signToken)(value, { secret });
|
|
1474
|
+
},
|
|
1475
|
+
open(value) {
|
|
1476
|
+
return (0, _orkestrel_server.verifyToken)(value, secret);
|
|
1477
|
+
}
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1480
|
+
/**
|
|
1339
1481
|
* Create the MCP Streamable-HTTP transport routes — mounts a transport-agnostic
|
|
1340
|
-
* {@link
|
|
1482
|
+
* {@link MCPDispatcherInterface} (the `@src/core` dispatch boundary) on the fetch-standard router
|
|
1341
1483
|
* spine, pumping each `POST` body through `mcp.dispatch`. Returns the {@link RouteInput}s to
|
|
1342
1484
|
* hand to `router.add(...)`.
|
|
1343
1485
|
*
|
|
@@ -1348,14 +1490,14 @@ var StdioServerTransport = class {
|
|
|
1348
1490
|
* DISPATCH-level outcomes:
|
|
1349
1491
|
*
|
|
1350
1492
|
* - A **transport** failure — a malformed JSON body, or a parsed value that is not a
|
|
1351
|
-
* JSON-RPC
|
|
1352
|
-
* error / `-32600` Invalid Request,
|
|
1493
|
+
* JSON-RPC INVOCATION — is an HTTP `400` carrying a JSON-RPC error BODY (`-32700` Parse
|
|
1494
|
+
* error / `-32600` Invalid Request), with the `id` it could not read OMITTED.
|
|
1353
1495
|
* - Modern protocol/method/name headers are validated against the body; a mismatch is
|
|
1354
1496
|
* HTTP `400` + `-32020`. Headerless initialize is accepted, a live legacy session supplies
|
|
1355
1497
|
* its pinned revision, and every other headerless request is rejected.
|
|
1356
1498
|
* - Legacy dispatch errors stay IN-BAND at HTTP `200`; modern errors map to `400` for
|
|
1357
1499
|
* `-32020` / `-32021` / `-32022` / `-32602`, `404` for `-32601`, and `200` otherwise.
|
|
1358
|
-
* - A **notification** (
|
|
1500
|
+
* - A **notification** (an invocation with no `id`, which `dispatch` resolves to
|
|
1359
1501
|
* `undefined`) is a `202 Accepted` with no body.
|
|
1360
1502
|
*
|
|
1361
1503
|
* When `streaming` is enabled (the default) and the client `Accept`s `text/event-stream`,
|
|
@@ -1374,7 +1516,7 @@ var StdioServerTransport = class {
|
|
|
1374
1516
|
* allowlist or explicitly delegates validation to an upstream layer.
|
|
1375
1517
|
*
|
|
1376
1518
|
* @typeParam TState - The consumer's opaque per-request state type
|
|
1377
|
-
* @param mcp - The transport-agnostic {@link
|
|
1519
|
+
* @param mcp - The transport-agnostic {@link MCPDispatcherInterface} to expose over HTTP
|
|
1378
1520
|
* @param options - Optional `path` (default {@link DEFAULT_MCP_PATH}) and `streaming`
|
|
1379
1521
|
* (default `true`), plus shared origin, keepalive, and synchronous caller-extraction options; see
|
|
1380
1522
|
* {@link HTTPTransportOptions}
|
|
@@ -1382,11 +1524,11 @@ var StdioServerTransport = class {
|
|
|
1382
1524
|
*
|
|
1383
1525
|
* @example
|
|
1384
1526
|
* ```ts
|
|
1385
|
-
* import { createMCPServer, createToolManager } from '@src/core'
|
|
1527
|
+
* import { createMCPLegacy, createMCPServer, createToolManager } from '@src/core'
|
|
1386
1528
|
* import { createMCPRoutes } from '@src/server'
|
|
1387
1529
|
*
|
|
1388
1530
|
* const mcp = createMCPServer({ identity: { name: 'docs', version: '1.0.0' }, tools: createToolManager() })
|
|
1389
|
-
* const routes = createMCPRoutes(mcp) //
|
|
1531
|
+
* const routes = createMCPRoutes(createMCPLegacy(mcp)) // both eras; pass `mcp` for modern only
|
|
1390
1532
|
* ```
|
|
1391
1533
|
*/
|
|
1392
1534
|
function createMCPRoutes(mcp, options) {
|
|
@@ -1399,7 +1541,7 @@ function createMCPRoutes(mcp, options) {
|
|
|
1399
1541
|
}
|
|
1400
1542
|
/**
|
|
1401
1543
|
* Create the HTTP CLIENT transport for an {@link import('@src/core').MCPClientInterface}
|
|
1402
|
-
* — a {@link
|
|
1544
|
+
* — a {@link MCPClientTransportInterface} that drives a REMOTE Streamable-HTTP MCP server
|
|
1403
1545
|
* over `fetch`. The egress mirror of {@link createMCPRoutes}.
|
|
1404
1546
|
*
|
|
1405
1547
|
* @remarks
|
|
@@ -1418,7 +1560,7 @@ function createMCPRoutes(mcp, options) {
|
|
|
1418
1560
|
* @param options - `url` (the remote endpoint; REQUIRED), optional `headers` merged onto
|
|
1419
1561
|
* every request, optional `fetch` (default `globalThis.fetch`), and optional `timeout`
|
|
1420
1562
|
* (ms, applied via `AbortSignal.timeout`); see {@link HTTPClientTransportOptions}
|
|
1421
|
-
* @returns A working {@link
|
|
1563
|
+
* @returns A working {@link MCPClientTransportInterface} over `fetch`
|
|
1422
1564
|
*
|
|
1423
1565
|
* @example
|
|
1424
1566
|
* ```ts
|
|
@@ -1437,7 +1579,7 @@ function createHTTPClientTransport(options) {
|
|
|
1437
1579
|
}
|
|
1438
1580
|
/**
|
|
1439
1581
|
* Create the MCP WebSocket transport INGRESS — an {@link UpgradeHandler} that exposes a
|
|
1440
|
-
* transport-agnostic {@link
|
|
1582
|
+
* transport-agnostic {@link MCPDispatcherInterface} over a WebSocket, the WebSocket mirror of
|
|
1441
1583
|
* {@link createMCPRoutes}. Register it on the spine's upgrade seam.
|
|
1442
1584
|
*
|
|
1443
1585
|
* @remarks
|
|
@@ -1465,7 +1607,7 @@ function createHTTPClientTransport(options) {
|
|
|
1465
1607
|
* handler BEFORE this one — that handler can claim (decline + destroy) an unauthenticated
|
|
1466
1608
|
* upgrade so it never reaches this pump.
|
|
1467
1609
|
*
|
|
1468
|
-
* @param mcp - The transport-agnostic {@link
|
|
1610
|
+
* @param mcp - The transport-agnostic {@link MCPDispatcherInterface} to expose over WebSocket
|
|
1469
1611
|
* @param options - Optional `path` (default {@link DEFAULT_MCP_PATH}) and `subprotocol`
|
|
1470
1612
|
* (default {@link MCP_WEBSOCKET_SUBPROTOCOL}); see {@link WebSocketServerOptions}
|
|
1471
1613
|
* @returns An {@link UpgradeHandler} to register with the spine's `upgrade` seam
|
|
@@ -1503,7 +1645,7 @@ function createWebSocketServer(mcp, options) {
|
|
|
1503
1645
|
}
|
|
1504
1646
|
/**
|
|
1505
1647
|
* Create the WebSocket CLIENT transport for an {@link import('@src/core').MCPClientInterface}
|
|
1506
|
-
* — a {@link
|
|
1648
|
+
* — a {@link MCPClientTransportInterface} that drives a REMOTE MCP server over a WebSocket. The
|
|
1507
1649
|
* egress mirror of {@link createWebSocketServer} and the WebSocket sibling of {@link
|
|
1508
1650
|
* createHTTPClientTransport}.
|
|
1509
1651
|
*
|
|
@@ -1519,7 +1661,7 @@ function createWebSocketServer(mcp, options) {
|
|
|
1519
1661
|
*
|
|
1520
1662
|
* @param options - `url` (the remote WebSocket endpoint; REQUIRED) and optional `headers`
|
|
1521
1663
|
* merged onto the upgrade request; see {@link WebSocketClientTransportOptions}
|
|
1522
|
-
* @returns A working {@link
|
|
1664
|
+
* @returns A working {@link MCPClientTransportInterface} over a WebSocket
|
|
1523
1665
|
*
|
|
1524
1666
|
* @example
|
|
1525
1667
|
* ```ts
|
|
@@ -1538,7 +1680,7 @@ function createWebSocketClientTransport(options) {
|
|
|
1538
1680
|
}
|
|
1539
1681
|
/**
|
|
1540
1682
|
* Create the stdio CLIENT transport for an {@link import('@src/core').MCPClientInterface}
|
|
1541
|
-
* — a {@link
|
|
1683
|
+
* — a {@link MCPClientTransportInterface} that spawns and drives a CHILD PROCESS MCP server
|
|
1542
1684
|
* over newline-delimited JSON-RPC on `stdin`/`stdout`, the stdio sibling of {@link
|
|
1543
1685
|
* createHTTPClientTransport} and {@link createWebSocketClientTransport}.
|
|
1544
1686
|
*
|
|
@@ -1553,7 +1695,7 @@ function createWebSocketClientTransport(options) {
|
|
|
1553
1695
|
*
|
|
1554
1696
|
* @param options - `command` (the executable to spawn; REQUIRED), optional `args`,
|
|
1555
1697
|
* and optional `env`; see {@link StdioClientTransportOptions}
|
|
1556
|
-
* @returns A working {@link
|
|
1698
|
+
* @returns A working {@link MCPClientTransportInterface} over a child process's stdio
|
|
1557
1699
|
*
|
|
1558
1700
|
* @example
|
|
1559
1701
|
* ```ts
|
|
@@ -1572,7 +1714,7 @@ function createStdioClientTransport(options) {
|
|
|
1572
1714
|
}
|
|
1573
1715
|
/**
|
|
1574
1716
|
* Create the MCP stdio transport INGRESS — pumps a transport-agnostic {@link
|
|
1575
|
-
*
|
|
1717
|
+
* MCPDispatcherInterface} over newline-delimited JSON-RPC on `stdin`/`stdout` (or an
|
|
1576
1718
|
* injected stream pair), the stdio mirror of {@link createWebSocketServer}.
|
|
1577
1719
|
*
|
|
1578
1720
|
* @remarks
|
|
@@ -1586,7 +1728,7 @@ function createStdioClientTransport(options) {
|
|
|
1586
1728
|
* surfaces on `mcp.emitter`'s `error` event rather than escaping the (async) message
|
|
1587
1729
|
* pump.
|
|
1588
1730
|
*
|
|
1589
|
-
* @param mcp - The transport-agnostic {@link
|
|
1731
|
+
* @param mcp - The transport-agnostic {@link MCPDispatcherInterface} to expose over stdio
|
|
1590
1732
|
* @param options - Optional injectable `input` / `output` streams; see
|
|
1591
1733
|
* {@link StdioServerOptions}
|
|
1592
1734
|
* @returns A `{ start(): void; stop(): void }` handle to arm / tear down the pump
|
|
@@ -1639,7 +1781,10 @@ function createStdioServer(mcp, options) {
|
|
|
1639
1781
|
* live-session request. It then
|
|
1640
1782
|
* FORWARDS a fresh `Request` carrying the buffered `text` (`next(forwarded)`) — never the
|
|
1641
1783
|
* already-consumed original — so the route re-reads the same body, and stamps the response
|
|
1642
|
-
* with {@link MCP_SESSION_HEADER}.
|
|
1784
|
+
* with {@link MCP_SESSION_HEADER}. The entry's `touched` instant is read AFTER that
|
|
1785
|
+
* downstream response, because it means the LAST ACCESS: a request slower than `ttl` would
|
|
1786
|
+
* otherwise store a session that is already expired, and the write-back RE-ASKS the store, so
|
|
1787
|
+
* a `DELETE` arriving while the request was suspended is not undone.
|
|
1643
1788
|
* - **`GET {path}`.** Resolves the session the same way (no mint — only `initialize` mints);
|
|
1644
1789
|
* an invalid / unknown id is the same `404`. A valid session opens the resumable
|
|
1645
1790
|
* server→client stream via `@orkestrel/server`'s {@link import('@orkestrel/server').openStream}:
|
|
@@ -1741,21 +1886,23 @@ function createMCPSession(options) {
|
|
|
1741
1886
|
}
|
|
1742
1887
|
if (context.method !== "POST" || text === void 0) return next();
|
|
1743
1888
|
let created;
|
|
1744
|
-
if (entry === void 0)
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1889
|
+
if (entry === void 0) {
|
|
1890
|
+
if (parsed !== void 0 && (0, _src_core.isInitializeRequest)(parsed)) {
|
|
1891
|
+
created = {
|
|
1892
|
+
session: new MCPSession(crypto.randomUUID(), capacity !== void 0 ? { capacity } : {}),
|
|
1893
|
+
touched: clock(),
|
|
1894
|
+
version: inferLegacyVersion(parsed)
|
|
1895
|
+
};
|
|
1896
|
+
entry = created;
|
|
1897
|
+
} else return rejectUnknownSession();
|
|
1898
|
+
}
|
|
1752
1899
|
if (!Reflect.set(context.state, "session", entry.session)) throw new Error("MCP session state is not writable");
|
|
1753
1900
|
const headers = new Headers(request.headers);
|
|
1754
1901
|
if (parsed === void 0 || !(0, _src_core.isInitializeRequest)(parsed)) {
|
|
1755
1902
|
const issue = inferHeaderIssue(request, entry.version);
|
|
1756
1903
|
if (issue?.reason === "missing") headers.set(MCP_PROTOCOL_VERSION_HEADER, entry.version);
|
|
1757
1904
|
else if (issue !== void 0) {
|
|
1758
|
-
const requestId = parsed !== void 0 && "method" in parsed ? parsed.id
|
|
1905
|
+
const requestId = parsed !== void 0 && "method" in parsed ? parsed.id : void 0;
|
|
1759
1906
|
return Response.json((0, _src_core.buildJSONRPCError)(requestId, _src_core.MCP_HEADER_MISMATCH, issue.message), { status: 400 });
|
|
1760
1907
|
}
|
|
1761
1908
|
}
|
|
@@ -1767,8 +1914,14 @@ function createMCPSession(options) {
|
|
|
1767
1914
|
}));
|
|
1768
1915
|
if (created !== void 0) {
|
|
1769
1916
|
if (!response.ok) return response;
|
|
1770
|
-
store.set(created.session.id,
|
|
1771
|
-
|
|
1917
|
+
store.set(created.session.id, {
|
|
1918
|
+
...created,
|
|
1919
|
+
touched: clock()
|
|
1920
|
+
});
|
|
1921
|
+
} else if (store.get(entry.session.id) === entry) store.set(entry.session.id, {
|
|
1922
|
+
...entry,
|
|
1923
|
+
touched: clock()
|
|
1924
|
+
});
|
|
1772
1925
|
response.headers.set(MCP_SESSION_HEADER, entry.session.id);
|
|
1773
1926
|
return response;
|
|
1774
1927
|
};
|
|
@@ -1779,6 +1932,7 @@ exports.DEFAULT_MCP_PATH = DEFAULT_MCP_PATH;
|
|
|
1779
1932
|
exports.DEFAULT_MCP_SESSION_CAPACITY = DEFAULT_MCP_SESSION_CAPACITY;
|
|
1780
1933
|
exports.DEFAULT_MCP_SESSION_TTL = DEFAULT_MCP_SESSION_TTL;
|
|
1781
1934
|
exports.HTTPClientTransport = HTTPClientTransport;
|
|
1935
|
+
exports.HTTPDisconnect = HTTPDisconnect;
|
|
1782
1936
|
exports.MCPSession = MCPSession;
|
|
1783
1937
|
exports.MCP_METHOD_HEADER = MCP_METHOD_HEADER;
|
|
1784
1938
|
exports.MCP_NAME_HEADER = MCP_NAME_HEADER;
|
|
@@ -1796,6 +1950,7 @@ exports.acceptsEventStream = acceptsEventStream;
|
|
|
1796
1950
|
exports.allowsOrigin = allowsOrigin;
|
|
1797
1951
|
exports.bridgeMessageTransport = bridgeMessageTransport;
|
|
1798
1952
|
exports.createHTTPClientTransport = createHTTPClientTransport;
|
|
1953
|
+
exports.createMCPContinuation = createMCPContinuation;
|
|
1799
1954
|
exports.createMCPPostHandler = createMCPPostHandler;
|
|
1800
1955
|
exports.createMCPRoutes = createMCPRoutes;
|
|
1801
1956
|
exports.createMCPSession = createMCPSession;
|
|
@@ -1814,6 +1969,7 @@ exports.readEventStream = readEventStream;
|
|
|
1814
1969
|
exports.readLastEventId = readLastEventId;
|
|
1815
1970
|
exports.readSessionHeader = readSessionHeader;
|
|
1816
1971
|
exports.rejectUnknownSession = rejectUnknownSession;
|
|
1972
|
+
exports.sendEventStream = sendEventStream;
|
|
1817
1973
|
exports.upgradeRequestPath = upgradeRequestPath;
|
|
1818
1974
|
|
|
1819
1975
|
//# sourceMappingURL=index.cjs.map
|