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