@orkestrel/mcp 0.0.28 → 0.0.29
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 +11 -14
- package/dist/src/browser/index.d.ts +63 -65
- package/dist/src/browser/index.js +44 -44
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/core/index.cjs +233 -176
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +442 -366
- package/dist/src/core/index.d.ts +442 -366
- package/dist/src/core/index.js +233 -176
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +109 -109
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +209 -203
- package/dist/src/server/index.d.ts +209 -203
- package/dist/src/server/index.js +109 -109
- package/dist/src/server/index.js.map +1 -1
- package/package.json +24 -25
package/dist/src/server/index.js
CHANGED
|
@@ -28,9 +28,9 @@ var DEFAULT_MCP_KEEPALIVE_INTERVAL = 15e3;
|
|
|
28
28
|
/** Names the comment text written by the held-open MCP response keepalive. */
|
|
29
29
|
var SSE_KEEPALIVE_COMMENT = "keepalive";
|
|
30
30
|
/**
|
|
31
|
-
* Sets the default capacity of a session's
|
|
31
|
+
* Sets the default capacity of a session's folded resumable event log (the per-{@link
|
|
32
32
|
* import('./MCPSession.js').MCPSession} replay log) — the maximum number of pushed
|
|
33
|
-
* server→client messages retained for replay before the
|
|
33
|
+
* server→client messages retained for replay before the oldest is evicted.
|
|
34
34
|
*
|
|
35
35
|
* @remarks
|
|
36
36
|
* Bounds the replay log's memory: only the most-recent {@link DEFAULT_MCP_SESSION_CAPACITY}
|
|
@@ -56,7 +56,7 @@ var DEFAULT_MCP_SESSION_TTL = 3e5;
|
|
|
56
56
|
*
|
|
57
57
|
* @remarks
|
|
58
58
|
* Ten seconds. The load-bearing property is the ordering, not the magnitude: this bound stays
|
|
59
|
-
*
|
|
59
|
+
* below {@link import('@orkestrel/mcp').DEFAULT_MCP_REQUEST_TIMEOUT}, so a write the child never
|
|
60
60
|
* reads fails as an undeliverable message while the request that carried it is still open,
|
|
61
61
|
* rather than being masked by that request's own deadline expiring first. Override per
|
|
62
62
|
* transport with `delivery`; an explicit `0` there removes the bound.
|
|
@@ -66,16 +66,16 @@ var DEFAULT_MCP_DELIVERY = 1e4;
|
|
|
66
66
|
//#region src/server/helpers.ts
|
|
67
67
|
/**
|
|
68
68
|
* Pumps a controlled held-open exchange onto an open SSE stream — one `data:` event per
|
|
69
|
-
* notification in order, then the terminating response — and
|
|
69
|
+
* notification in order, then the terminating response — and end the exchange however the
|
|
70
70
|
* pump leaves.
|
|
71
71
|
*
|
|
72
72
|
* @remarks
|
|
73
73
|
* The Streamable-HTTP twin of {@link import('@orkestrel/mcp').sendStream}, and it owns exactly what
|
|
74
|
-
* that owns. The `finally` releases the exchange on
|
|
74
|
+
* that owns. The `finally` releases the exchange on every exit — the normal terminal, a
|
|
75
75
|
* producer that threw, a `write` that threw, and an abort alike — because nothing else will:
|
|
76
76
|
* a request whose client vanished cancels nothing by itself, so an exchange this pump walks
|
|
77
77
|
* away from keeps its producer, its request lifetime, and its live subscription slot forever.
|
|
78
|
-
* The exchange is released
|
|
78
|
+
* The exchange is released before the body ends, so the slot is already back when the response
|
|
79
79
|
* completes.
|
|
80
80
|
*
|
|
81
81
|
* Total — never throws and never rejects. A held-open SSE response has already sent its
|
|
@@ -202,7 +202,7 @@ function readLastEventId(request) {
|
|
|
202
202
|
* @remarks
|
|
203
203
|
* Returns `Response.json(buildJSONRPCError(undefined, JSONRPC_INVALID_REQUEST, 'Session not
|
|
204
204
|
* found'), { status: 404 })`, mirroring `createMCPRoutes`'s `400` transport-failure shape (a
|
|
205
|
-
* JSON-RPC error
|
|
205
|
+
* JSON-RPC error body with no id) but at the session-not-found status. Shared by
|
|
206
206
|
* every {@link import('./middlewares.js').createMCPSession} validation site — the
|
|
207
207
|
* non-`initialize` `POST` path, the resumable `GET {path}` open, and the `DELETE {path}`
|
|
208
208
|
* session-end (each a missing / unknown / TTL-evicted id) — so the single `404` envelope
|
|
@@ -218,7 +218,7 @@ function rejectUnknownSession() {
|
|
|
218
218
|
* the `createWebSocketServer` upgrade-path match.
|
|
219
219
|
*
|
|
220
220
|
* @remarks
|
|
221
|
-
* A `node:http` {@link import('node:http').IncomingMessage}'s `url` is the request
|
|
221
|
+
* A `node:http` {@link import('node:http').IncomingMessage}'s `url` is the request target
|
|
222
222
|
* (`'/mcp?x=1'`), narrowed with `isString` (never `as`) and defaulting to `'/'` for an
|
|
223
223
|
* absent target; it is parsed against a placeholder base (only the pathname matters for the upgrade
|
|
224
224
|
* decision) and the `pathname` returned. The upgrade handler compares this against its
|
|
@@ -239,7 +239,7 @@ function upgradeRequestPath(request) {
|
|
|
239
239
|
*
|
|
240
240
|
* @remarks
|
|
241
241
|
* Concatenates `buffer` (the carried-forward partial line from the previous call)
|
|
242
|
-
* with `chunk`, splits on `'\n'`, and returns every
|
|
242
|
+
* with `chunk`, splits on `'\n'`, and returns every complete line (a `'\r'` trailing
|
|
243
243
|
* a line, from a CRLF-framed peer, is trimmed) plus the final, possibly-empty
|
|
244
244
|
* fragment as the new `remainder` — the caller threads it back in as the next call's
|
|
245
245
|
* `buffer`. A chunk containing no `'\n'` yields no lines and the whole (buffer +
|
|
@@ -264,12 +264,12 @@ function extractLines(buffer, chunk) {
|
|
|
264
264
|
* The completion callback is the writable channel's backpressure boundary. A callback error and
|
|
265
265
|
* a synchronous `write` throw reject the returned promise with the original value.
|
|
266
266
|
*
|
|
267
|
-
* That callback is the
|
|
267
|
+
* That callback is the only thing that settles the promise: this helper holds no timer and no
|
|
268
268
|
* abort, so an output that neither confirms nor fails the write parks the promise for as long as
|
|
269
269
|
* the caller-owned stream holds the callback. A caller wanting a bound races this promise against
|
|
270
270
|
* one it owns — {@link import('./transports/StdioServerTransport.js').StdioServerTransport}
|
|
271
271
|
* registers such a bound per send and rejects it on `close()`, so closing the transport settles
|
|
272
|
-
* the
|
|
272
|
+
* the caller's `send` while the abandoned write stays with the stream that still holds its
|
|
273
273
|
* callback, reachable from nothing the transport retains.
|
|
274
274
|
*
|
|
275
275
|
* @param output - The writable stream that receives the line
|
|
@@ -364,7 +364,7 @@ function inferHeaderTarget(request) {
|
|
|
364
364
|
* body requires a protocol header after initialization. Messages name the expected value but
|
|
365
365
|
* never echo the client-supplied one.
|
|
366
366
|
*
|
|
367
|
-
* The expectation a
|
|
367
|
+
* The expectation a live session supplies is a different rule over a different input, so it
|
|
368
368
|
* is {@link inferSessionHeaderIssue} rather than a second arm of this one.
|
|
369
369
|
*
|
|
370
370
|
* @param request - The HTTP request carrying the headers
|
|
@@ -432,7 +432,7 @@ function inferHeaderIssue(request, invocation) {
|
|
|
432
432
|
* The session layer's rule, distinct from the body-derived one {@link inferHeaderIssue} owns:
|
|
433
433
|
* a live legacy session pinned its revision at `initialize`, so every later request on that
|
|
434
434
|
* session must name the same one. An absent header reads as `missing`, which the session
|
|
435
|
-
* middleware answers by
|
|
435
|
+
* middleware answers by supplying the pinned revision rather than refusing; a present header
|
|
436
436
|
* naming another revision reads as `mismatched` and is refused. The message names the session's
|
|
437
437
|
* revision and never echoes the client-supplied value.
|
|
438
438
|
*
|
|
@@ -464,8 +464,8 @@ function inferSessionHeaderIssue(request, version) {
|
|
|
464
464
|
*
|
|
465
465
|
* @remarks
|
|
466
466
|
* The custom-header half of the standard-header seam {@link inferHeaderIssue} owns, and it
|
|
467
|
-
* takes the
|
|
468
|
-
* rule to the `Mcp-Param-*` names the server's
|
|
467
|
+
* takes the served definition's projections rather than a header issue: SEP-2243 scopes the
|
|
468
|
+
* rule to the `Mcp-Param-*` names the server's own tool definitions annotate, so a name no
|
|
469
469
|
* parameter claims is another party's header and travels through untouched.
|
|
470
470
|
*
|
|
471
471
|
* For each recognized parameter the body's value at the parameter's own property path fixes
|
|
@@ -516,7 +516,7 @@ function inferParameterRefusal(request, parameters, values) {
|
|
|
516
516
|
*
|
|
517
517
|
* @remarks
|
|
518
518
|
* A supported legacy request is pinned exactly. A modern, malformed, absent, or unsupported
|
|
519
|
-
* request selects the newest supported legacy revision. The read is deliberately the
|
|
519
|
+
* request selects the newest supported legacy revision. The read is deliberately the same one
|
|
520
520
|
* {@link import('@orkestrel/mcp').buildInitializeResult} performs — `isMCPLegacyVersion` over
|
|
521
521
|
* the requested revision — because the session version this pins and the version that result
|
|
522
522
|
* echoes must be the one value. Routing through `inferVersion` cannot do it: that inferer is
|
|
@@ -557,22 +557,22 @@ function inferStatus(response, era) {
|
|
|
557
557
|
* Composes one incoming HTTP request lifetime with one MCP-owned SSE response lifetime.
|
|
558
558
|
*
|
|
559
559
|
* @remarks
|
|
560
|
-
* The composed {@link signal} observes request abort and
|
|
560
|
+
* The composed {@link signal} observes request abort and every way this response can end
|
|
561
561
|
* without one: consumer cancellation of the bridged body, a forwarding failure mid-pump, and a
|
|
562
562
|
* keepalive tick that finds the SSE stream already closed. That last pair is the whole point of
|
|
563
563
|
* the composition — a client that vanishes mid-stream aborts nothing by itself, so unless this
|
|
564
564
|
* object raises the signal on its own failure paths, the handler, the controlled stream, and
|
|
565
565
|
* the producer behind them all keep running for a response that can no longer be written.
|
|
566
|
-
* Graceful upstream completion is the one terminal that does
|
|
566
|
+
* Graceful upstream completion is the one terminal that does not abort: the body closes,
|
|
567
567
|
* because the exchange finished rather than ended.
|
|
568
568
|
*
|
|
569
569
|
* {@link bridge} preserves the source response status and headers, forwards its body bytes, and
|
|
570
570
|
* owns keepalive comments plus listener/timer cleanup until upstream completion, request abort,
|
|
571
571
|
* or consumer cancellation. This is a single-response lifecycle object, not a reusable bridge:
|
|
572
|
-
* a second {@link bridge} call
|
|
572
|
+
* a second {@link bridge} call throws rather than arming a second keepalive over one lifecycle.
|
|
573
573
|
* It supplies no handler or session policy.
|
|
574
574
|
*
|
|
575
|
-
* The keepalive interval is a
|
|
575
|
+
* The keepalive interval is a budget, sanitized like every other numeric knob in this package:
|
|
576
576
|
* anything that is not a positive integer — `0`, a negative, a fractional value, `NaN`,
|
|
577
577
|
* `Infinity` — falls back to {@link DEFAULT_MCP_KEEPALIVE_INTERVAL}, and a larger value clamps
|
|
578
578
|
* to Node's `2_147_483_647` ms timer maximum. None may reach the platform's timer floor, where
|
|
@@ -706,7 +706,7 @@ var HTTPDisconnect = class {
|
|
|
706
706
|
* method carrying a named target — `tools/call` and `prompts/get` against `params.name`,
|
|
707
707
|
* `resources/read` against `params.uri` — with a Base64-sentinel value decoded before the
|
|
708
708
|
* comparison; a missing, mismatched, or invalidly encoded value returns HTTP `400` + `-32020`.
|
|
709
|
-
* A protocol header naming a
|
|
709
|
+
* A protocol header naming a modern revision holds the request to that revision whatever shape
|
|
710
710
|
* its body arrived in, so a body with no parsable modern `_meta` returns HTTP `400` + `-32602`.
|
|
711
711
|
* Headerless `initialize` is accepted, while every other headerless request needs a live legacy
|
|
712
712
|
* session to supply its pinned version. A legacy-shaped request carrying a protocol header is
|
|
@@ -828,39 +828,39 @@ function createMCPPostHandler(mcp, options) {
|
|
|
828
828
|
/**
|
|
829
829
|
* Represents one MCP transport session — the per-session entity a {@link
|
|
830
830
|
* import('./middlewares.js').createMCPSession} middleware owns, keyed by its `id`, carrying the
|
|
831
|
-
* resumable server→client push channel with its bounded replay log
|
|
831
|
+
* resumable server→client push channel with its bounded replay log folded in.
|
|
832
832
|
*
|
|
833
833
|
* @remarks
|
|
834
834
|
* One entity carries the whole session: it holds the
|
|
835
|
-
* session `id`, its
|
|
835
|
+
* session `id`, its own bounded, replayable log of pushed server→client messages (the
|
|
836
836
|
* resumable GET-SSE channel — a private `#events` `Map` + a monotone `#counter`, with
|
|
837
837
|
* `capacity` / `ttl` eviction, not a separate store), and the set of open
|
|
838
838
|
* server→client SSE streams (a resumable `GET {path}` registers through `attach`, unregisters through
|
|
839
839
|
* `detach` on disconnect). Still a small entity (not a record), built minimal + extensible.
|
|
840
840
|
*
|
|
841
|
-
* - **`push` is the server-initiated primitive.** It
|
|
842
|
-
* a monotone base36 event id) and
|
|
843
|
-
* SSE event (`stream.write({ id, data })`). A push with
|
|
844
|
-
* so a client that connects (or reconnects with a `Last-Event-ID`)
|
|
841
|
+
* - **`push` is the server-initiated primitive.** It appends the message to the log (assigning
|
|
842
|
+
* a monotone base36 event id) and fans it out to every attached stream as one `id:`-tagged
|
|
843
|
+
* SSE event (`stream.write({ id, data })`). A push with no attached stream is still logged,
|
|
844
|
+
* so a client that connects (or reconnects with a `Last-Event-ID`) later replays it from the
|
|
845
845
|
* log. A `write` to a closed stream is a safe no-op (the {@link
|
|
846
846
|
* `@orkestrel/server`'s `createStream` contract), so a just-disconnected stream that
|
|
847
847
|
* has not yet been `detach`ed never throws. A replayed event and the live one carry the
|
|
848
|
-
*
|
|
848
|
+
* identical id (the log assigns it once).
|
|
849
849
|
*
|
|
850
850
|
* - **`replay(afterId)` is strictly-after.** It returns every retained log entry whose id sorts
|
|
851
|
-
*
|
|
852
|
-
* before attaching the stream for live pushes. The decision for an
|
|
851
|
+
* after `afterId` in append order — the missed-events list the `GET {path}` handler writes
|
|
852
|
+
* before attaching the stream for live pushes. The decision for an unknown / already-evicted
|
|
853
853
|
* `afterId` (the client's cursor fell off the back of the capacity window, or never existed):
|
|
854
|
-
* replay
|
|
855
|
-
* lost (its cursor is
|
|
854
|
+
* replay nothing. Replaying the whole retained log would re-deliver events the client never
|
|
855
|
+
* lost (its cursor is older than everything retained); returning `[]` lets the handler then
|
|
856
856
|
* stream only the fresh pushes that follow `attach` — the spec-sane resume.
|
|
857
857
|
*
|
|
858
|
-
* - **Bounded, append-ordered, plain `Map`.** The log lives in
|
|
859
|
-
* `Map<id, entry>` — insertion order
|
|
860
|
-
* eviction both walk the map directly.
|
|
858
|
+
* - **Bounded, append-ordered, plain `Map`.** The log lives in one insertion-ordered
|
|
859
|
+
* `Map<id, entry>` — insertion order is append order is id order, so `replay` and capacity
|
|
860
|
+
* eviction both walk the map directly. No database mirror — the log is process-local
|
|
861
861
|
* transport mechanics, not durable state. `push` first drops every entry older than `ttl`
|
|
862
862
|
* (lazy TTL — no background timer, the middleware's lazy-window idiom), appends, then evicts
|
|
863
|
-
* the
|
|
863
|
+
* the oldest entries until at most `capacity` remain; `replay` also runs the lazy TTL sweep
|
|
864
864
|
* first, so a stale entry is never replayed.
|
|
865
865
|
*
|
|
866
866
|
* - **No transport coupling beyond the SSE seam.** It holds session state + the generic {@link
|
|
@@ -949,15 +949,15 @@ var MCPSession = class {
|
|
|
949
949
|
//#region src/server/transports/WebSocketServerTransport.ts
|
|
950
950
|
/**
|
|
951
951
|
* Wraps a {@link NodeWebSocketInterface} (the RFC 6455 wire wrapper) as a
|
|
952
|
-
* {@link MCPMessageTransportInterface} — the per-connection JSON-RPC-over-WebSocket
|
|
952
|
+
* {@link MCPMessageTransportInterface} — the per-connection JSON-RPC-over-WebSocket server
|
|
953
953
|
* bridge, the bidirectional JSON-RPC message channel
|
|
954
954
|
* `createWebSocketServer` pumps `mcp.dispatch` over and the egress mirror's
|
|
955
955
|
* {@link import('./WebSocketClientTransport.js').WebSocketClientTransport} reuses.
|
|
956
956
|
*
|
|
957
957
|
* @remarks
|
|
958
|
-
* - **Reuses `MCPMessageTransportInterface`.** It
|
|
958
|
+
* - **Reuses `MCPMessageTransportInterface`.** It is the same generic carrier the HTTP
|
|
959
959
|
* client transport implements — `emitter` (`message` / `close` / `error`), `start`,
|
|
960
|
-
* `send`, `close` — so the WebSocket server and client both speak
|
|
960
|
+
* `send`, `close` — so the WebSocket server and client both speak one transport contract,
|
|
961
961
|
* no near-duplicate sibling interface. `session` is `undefined` (the stateless v1; a
|
|
962
962
|
* session id is the deferred sessions tier). The name keeps the role explicit even though
|
|
963
963
|
* the shape is shared.
|
|
@@ -965,13 +965,13 @@ var MCPSession = class {
|
|
|
965
965
|
* frame runs through the shared `deliverMessage` fold (parse, then narrow) — a
|
|
966
966
|
* well-formed {@link JSONRPCMessage} is re-emitted on this transport's `message` event (the
|
|
967
967
|
* parsed envelope the {@link import('@orkestrel/mcp').MCPServerInterface} pump dispatches), while
|
|
968
|
-
* a non-JSON or non-message frame is surfaced on `error` and
|
|
968
|
+
* a non-JSON or non-message frame is surfaced on `error` and dropped, never thrown. It
|
|
969
969
|
* also bridges the socket's `close` → this transport's `close`, and the socket's `error`.
|
|
970
970
|
* - **Outbound (`send`).** `send(message)` writes one text frame
|
|
971
971
|
* (`nodeWs.send(JSON.stringify(message))`). The underlying wrapper no-ops a write on a
|
|
972
972
|
* non-open socket and confirms nothing, so this bridge answers a closed channel from its own
|
|
973
973
|
* state and the socket's `readyState`: a `send` after `close()`, after the peer's close, or on
|
|
974
|
-
* a socket that is not `OPEN`
|
|
974
|
+
* a socket that is not `OPEN` rejects with `WebSocket transport is not connected` rather than
|
|
975
975
|
* resolving on a frame nobody wrote. `bindServer` catches that rejection and routes it to the
|
|
976
976
|
* dispatcher's `error` event, and it aborts every in-flight request the moment this transport's
|
|
977
977
|
* `close` fires — so a peer that disconnects mid-request is answered by no write at all.
|
|
@@ -982,7 +982,7 @@ var MCPSession = class {
|
|
|
982
982
|
* releases the same way, so a closed transport is never subscribed to a live socket.
|
|
983
983
|
* - **Observable.** Owns the `emitter` ({@link MCPMessageTransportEventMap}); the emitter
|
|
984
984
|
* isolates a listener throw (a buggy observer never corrupts the bridge). `error` is a
|
|
985
|
-
*
|
|
985
|
+
* domain event (a transport-level fault), distinct from the emitter's listener-error channel.
|
|
986
986
|
*/
|
|
987
987
|
var WebSocketServerTransport = class {
|
|
988
988
|
#emitter;
|
|
@@ -1039,7 +1039,7 @@ var WebSocketServerTransport = class {
|
|
|
1039
1039
|
//#endregion
|
|
1040
1040
|
//#region src/server/transports/WebSocketClientTransport.ts
|
|
1041
1041
|
/**
|
|
1042
|
-
* Drives a
|
|
1042
|
+
* Drives a remote MCP server over a WebSocket — a client
|
|
1043
1043
|
* {@link MCPMessageTransportInterface} for the Model Context Protocol, the
|
|
1044
1044
|
* egress mirror of {@link import('./factories.js').createWebSocketServer} and the WebSocket
|
|
1045
1045
|
* sibling of {@link import('@orkestrel/mcp').HTTPClientTransport}.
|
|
@@ -1049,16 +1049,16 @@ var WebSocketServerTransport = class {
|
|
|
1049
1049
|
* RFC 6455 client handshake: it opens a `node:http`(`s`) `GET` carrying `Connection: Upgrade`
|
|
1050
1050
|
* / `Upgrade: websocket` / a random `Sec-WebSocket-Key` / `Sec-WebSocket-Version: 13` /
|
|
1051
1051
|
* `Sec-WebSocket-Protocol: mcp` (plus any `options.headers`), awaits the client `'upgrade'`
|
|
1052
|
-
* event, and
|
|
1053
|
-
* — a mismatch (or a non-`101` response, or a request error)
|
|
1052
|
+
* event, and validates `Sec-WebSocket-Accept === computeWebSocketAccept(key)` (the D2 helper)
|
|
1053
|
+
* — a mismatch (or a non-`101` response, or a request error) rejects `start()` and the socket
|
|
1054
1054
|
* is destroyed. On success it wraps the raw upgraded socket in `createNodeWebSocket({ socket,
|
|
1055
|
-
* head })` (
|
|
1055
|
+
* head })` (client mode — no key → frames are masked per RFC 6455 §5.3) and bridges its
|
|
1056
1056
|
* `message`.
|
|
1057
1057
|
* - **The arriving socket is RE-ASKED for, never assumed.** `start()` suspends across that
|
|
1058
1058
|
* connect and upgrade, so it re-checks the transport's state before installing anything: a
|
|
1059
1059
|
* concurrent `start()` that already installed a socket, or a {@link close} that ended the
|
|
1060
|
-
* transport while the handshake was on the wire, both
|
|
1061
|
-
*
|
|
1060
|
+
* transport while the handshake was on the wire, both win — the socket that arrives late is
|
|
1061
|
+
* destroyed and never bound, so no orphan is left re-emitting frames at nobody. Both
|
|
1062
1062
|
* `start()` calls still resolve; exactly one socket is ever bound.
|
|
1063
1063
|
* - **Inbound (`message`).** Each decoded text frame runs through the shared `deliverMessage`
|
|
1064
1064
|
* fold (parse, then narrow) — a {@link JSONRPCMessage} re-emits on this transport's `message`
|
|
@@ -1066,14 +1066,14 @@ var WebSocketServerTransport = class {
|
|
|
1066
1066
|
* non-JSON / non-message frame surfaces on `error` and is dropped. The socket's `close`
|
|
1067
1067
|
* / `error` bridge to this transport's events.
|
|
1068
1068
|
* - **Outbound (`send`).** `send(message)` writes one masked text frame. A socket write is not
|
|
1069
|
-
* confirmed, so this transport answers a closed channel from its own state
|
|
1069
|
+
* confirmed, so this transport answers a closed channel from its own state and the socket's
|
|
1070
1070
|
* `readyState`: a `send` with no bound socket — before `start()`, after `close()`, or after the
|
|
1071
|
-
* peer ended the socket — and a `send` on a bound socket that is not `OPEN` both
|
|
1071
|
+
* peer ended the socket — and a `send` on a bound socket that is not `OPEN` both reject with
|
|
1072
1072
|
* `WebSocket transport is not connected`. It neither drops the message nor queues it for a
|
|
1073
1073
|
* connection this transport is not holding — the browser face queues a pre-open send, and this
|
|
1074
1074
|
* one, holding no connection to flush it onto, rejects that too.
|
|
1075
1075
|
* - **`close()`** unsubscribes from the socket, closes it, and fires `close` (idempotent). An
|
|
1076
|
-
* upgrade still on the wire is
|
|
1076
|
+
* upgrade still on the wire is destroyed, so a `close()` during the handshake ends the
|
|
1077
1077
|
* transport at once instead of waiting for a peer that may never answer — the suspended
|
|
1078
1078
|
* `start()` resolves, because the close is the outcome its caller asked for.
|
|
1079
1079
|
* - **URL scheme.** `options.url` accepts a `ws://` / `wss://` URL or an `http://` / `https://`
|
|
@@ -1081,7 +1081,7 @@ var WebSocketServerTransport = class {
|
|
|
1081
1081
|
* → TLS through `node:https`). Either reaches the same endpoint.
|
|
1082
1082
|
* - **Observable.** Owns the `emitter` ({@link MCPMessageTransportEventMap}); every emit
|
|
1083
1083
|
* the emitter isolates a listener throw (a buggy observer never corrupts the transport);
|
|
1084
|
-
* `error` is a
|
|
1084
|
+
* `error` is a domain event (a transport-level fault).
|
|
1085
1085
|
*
|
|
1086
1086
|
* @example
|
|
1087
1087
|
* ```ts
|
|
@@ -1221,7 +1221,7 @@ var WebSocketClientTransport = class {
|
|
|
1221
1221
|
//#endregion
|
|
1222
1222
|
//#region src/server/transports/StdioClientTransport.ts
|
|
1223
1223
|
/**
|
|
1224
|
-
* Drives a
|
|
1224
|
+
* Drives a child process MCP server over newline-delimited JSON-RPC on `stdin`/`stdout` —
|
|
1225
1225
|
* a {@link StdioClientTransportInterface}, the stdio sibling of {@link
|
|
1226
1226
|
* import('@orkestrel/mcp').HTTPClientTransport} and {@link
|
|
1227
1227
|
* import('./WebSocketClientTransport.js').WebSocketClientTransport}.
|
|
@@ -1237,14 +1237,14 @@ var WebSocketClientTransport = class {
|
|
|
1237
1237
|
* line is decoded and delivered through the shared {@link dispatchLines} helper — a well-formed
|
|
1238
1238
|
* {@link JSONRPCMessage} emits `message`, a malformed line emits `error` (never throws).
|
|
1239
1239
|
* - **Outbound (`send`).** `send(message)` writes one newline-terminated `JSON.stringify`d line
|
|
1240
|
-
* through the supervisor's `send` and
|
|
1240
|
+
* through the supervisor's `send` and awaits its answer, so this promise settles only after the
|
|
1241
1241
|
* host reports the line handled rather than the moment the write is queued. The supervisor never
|
|
1242
1242
|
* rejects — it answers `false` for a channel that was closed, destroyed, or ended, for a write
|
|
1243
1243
|
* that failed, or for one that remained unconfirmed through `delivery`. A call made without a
|
|
1244
1244
|
* live child rejects as not connected; a `false` answer from a live child rejects as unable to
|
|
1245
1245
|
* deliver. The supervisor does not disclose which cause produced that answer.
|
|
1246
1246
|
* - **`close()`** runs the supervisor's bounded termination and teardown, then fires `close` once
|
|
1247
|
-
* (idempotent). That teardown reaches the child's
|
|
1247
|
+
* (idempotent). That teardown reaches the child's terminal moment, where the supervisor freezes
|
|
1248
1248
|
* `evidence`, ends `lines`, and settles `exit` together, so this transport needs no release of
|
|
1249
1249
|
* its own to get its line pump back: the stream ends under the pump rather than throwing at it.
|
|
1250
1250
|
* A line the supervisor had already framed behind the one being delivered is dropped rather than
|
|
@@ -1257,7 +1257,7 @@ var WebSocketClientTransport = class {
|
|
|
1257
1257
|
* child's own process group `SIGTERM`, waits the grace window, then `SIGKILL`s through the same
|
|
1258
1258
|
* route, so the kill reaches grandchildren rather than orphaning them, while Windows ends the
|
|
1259
1259
|
* tree with `taskkill /F /T`, which nothing in the child can intercept.
|
|
1260
|
-
* - **Evidence.** `evidence` reports that retained stderr tail off the
|
|
1260
|
+
* - **Evidence.** `evidence` reports that retained stderr tail off the held child — its live tail
|
|
1261
1261
|
* while the child runs, and the value the supervisor froze at that child's terminal moment
|
|
1262
1262
|
* afterwards. The reference is held past that moment and replaced only by the next `start()`,
|
|
1263
1263
|
* which is what keeps a post-`close()` read stable without a private copy: the frozen value
|
|
@@ -1265,7 +1265,7 @@ var WebSocketClientTransport = class {
|
|
|
1265
1265
|
* cannot grow it. See {@link StdioClientTransportInterface.evidence} for the readings and the
|
|
1266
1266
|
* byte bound.
|
|
1267
1267
|
* - **Observable.** Owns the `emitter` ({@link MCPMessageTransportEventMap}); the
|
|
1268
|
-
* emitter isolates a listener throw; `error` is a
|
|
1268
|
+
* emitter isolates a listener throw; `error` is a domain event (a transport-level
|
|
1269
1269
|
* fault, including the child spawn cause the supervisor surfaces and the notice that this
|
|
1270
1270
|
* lifetime's `evidence` was cut off at the `drain` bound), distinct from the emitter's own
|
|
1271
1271
|
* listener-error channel.
|
|
@@ -1409,7 +1409,7 @@ var StdioClientTransport = class {
|
|
|
1409
1409
|
* - **`close()`** removes this transport's input and output subscriptions, rejects every
|
|
1410
1410
|
* pending send, and fires its `close`
|
|
1411
1411
|
* event (idempotent). It pauses the input only when the caller was not already reading
|
|
1412
|
-
* it at `start` (`readableFlowing !== true`)
|
|
1412
|
+
* it at `start` (`readableFlowing !== true`) and no `data` listener remains once this
|
|
1413
1413
|
* transport's own is removed — so a process holding `process.stdin` can exit, and a
|
|
1414
1414
|
* caller's own flow is never stopped underneath it. The transport preserves flowing versus
|
|
1415
1415
|
* non-flowing state and restores every caller-owned listener. A Node stream that had never been
|
|
@@ -1420,7 +1420,7 @@ var StdioClientTransport = class {
|
|
|
1420
1420
|
* `process.stdin`/`process.stdout`), so the transport never destroys, ends, or blanket-clears
|
|
1421
1421
|
* them.
|
|
1422
1422
|
* - **Observable.** Owns the `emitter` ({@link MCPMessageTransportEventMap}); the
|
|
1423
|
-
* emitter isolates a listener throw; `error` is a
|
|
1423
|
+
* emitter isolates a listener throw; `error` is a domain event (a transport-level
|
|
1424
1424
|
* fault), distinct from the emitter's own listener-error channel.
|
|
1425
1425
|
*/
|
|
1426
1426
|
var StdioServerTransport = class {
|
|
@@ -1528,37 +1528,37 @@ function createMCPContinuation(secret) {
|
|
|
1528
1528
|
* Creates the server-side mirror of
|
|
1529
1529
|
* {@link import('@orkestrel/mcp').createDuplexClientTransport}: the adapter that bridges a
|
|
1530
1530
|
* message-channel {@link MCPMessageTransportInterface}
|
|
1531
|
-
* (the shape the stdio and WebSocket
|
|
1531
|
+
* (the shape the stdio and WebSocket server transports already implement) onto the
|
|
1532
1532
|
* environment-agnostic {@link import('@orkestrel/mcp').MCPTransportInterface} port — what
|
|
1533
1533
|
* {@link createStdioServer} and {@link createWebSocketServer} pipe through `bindServer`, so
|
|
1534
|
-
* the request/reply/error pump those factories used to hand-roll identically now lives
|
|
1534
|
+
* the request/reply/error pump those factories used to hand-roll identically now lives once
|
|
1535
1535
|
* in the core binder. {@link import('@orkestrel/mcp').createDuplexClientTransport} adapts the
|
|
1536
|
-
* same
|
|
1536
|
+
* same contracts the other way.
|
|
1537
1537
|
*
|
|
1538
1538
|
* @remarks
|
|
1539
1539
|
* `send` decodes the already-serialized reply string back to a {@link JSONRPCMessage}
|
|
1540
1540
|
* and writes it through `transport.send` (the same `JSON.stringify` the underlying
|
|
1541
1541
|
* transport already performs, so the wire bytes are unchanged). `listen` filters
|
|
1542
|
-
* `transport`'s `message` event to
|
|
1542
|
+
* `transport`'s `message` event to invocations only — requests and notifications, never a
|
|
1543
1543
|
* stray response, exactly as the prior hand-rolled pumps did — and re-serializes each one
|
|
1544
1544
|
* back to a string for `bindServer`. `closed` bridges `transport`'s `close` event. `close`
|
|
1545
1545
|
* closes the underlying `transport`.
|
|
1546
1546
|
*
|
|
1547
|
-
* @remarks A message crossing this bridge is decoded and re-encoded
|
|
1548
|
-
*
|
|
1547
|
+
* @remarks A message crossing this bridge is decoded and re-encoded twice, and that is
|
|
1548
|
+
* accepted rather than accidental. Inbound: the carrier already parsed the frame into a
|
|
1549
1549
|
* {@link JSONRPCMessage}, and `listen` re-serializes it so `bindServer` can decode it again
|
|
1550
1550
|
* under the server's own `limit`. Outbound: `bindServer` serialized the reply, `send` parses
|
|
1551
1551
|
* it back, and the carrier stringifies it once more. The cost is two extra `JSON.parse` /
|
|
1552
|
-
* `JSON.stringify` round trips per message, paid to keep
|
|
1553
|
-
* of a hand-rolled one per carrier. It is
|
|
1552
|
+
* `JSON.stringify` round trips per message, paid to keep one pump in the core binder instead
|
|
1553
|
+
* of a hand-rolled one per carrier. It is bounded rather than unbounded because the binder
|
|
1554
1554
|
* decodes within `server.limit.message`, so an oversized frame is refused before the second
|
|
1555
1555
|
* decode rather than after it. Removing the cost means giving `MCPTransportInterface` a
|
|
1556
1556
|
* message-shaped face beside its string one, which every transport would then carry.
|
|
1557
1557
|
*
|
|
1558
1558
|
* @remarks Per {@link import('@orkestrel/mcp').MCPTransportInterface}, `listen`/`closed`
|
|
1559
|
-
* each hold
|
|
1559
|
+
* each hold the single current handler (a second call replaces the first, never adds).
|
|
1560
1560
|
* Because the underlying `transport.emitter` is ADD-based (`on` subscribes, never
|
|
1561
|
-
* replaces), this bridge installs
|
|
1561
|
+
* replaces), this bridge installs one stable emitter listener per event on first use
|
|
1562
1562
|
* and re-routes it to whichever handler is active (`undefined` while
|
|
1563
1563
|
* none is), so rebinding never double-dispatches.
|
|
1564
1564
|
*
|
|
@@ -1612,14 +1612,14 @@ function createDuplexServerTransport(transport) {
|
|
|
1612
1612
|
* hand to `router.add(...)`.
|
|
1613
1613
|
*
|
|
1614
1614
|
* @remarks
|
|
1615
|
-
* A
|
|
1615
|
+
* A single `POST {path}` route — `createMCPRoutes` is stateless. The handler reads its own
|
|
1616
1616
|
* request body (its own JSON parse try/catch), so it works with or without a session
|
|
1617
1617
|
* middleware mounted in front. It draws a sharp line between TRANSPORT-level and
|
|
1618
1618
|
* DISPATCH-level outcomes:
|
|
1619
1619
|
*
|
|
1620
1620
|
* - A **transport** failure — a malformed JSON body, or a parsed value that is not a
|
|
1621
|
-
* JSON-RPC
|
|
1622
|
-
* error / `-32600` Invalid Request), with the `id` it could not read
|
|
1621
|
+
* JSON-RPC invocation — is an HTTP `400` carrying a JSON-RPC error body (`-32700` Parse
|
|
1622
|
+
* error / `-32600` Invalid Request), with the `id` it could not read omitted.
|
|
1623
1623
|
* - Modern protocol/method/name headers are validated against the body; a mismatch is
|
|
1624
1624
|
* HTTP `400` + `-32020`. Headerless initialize is accepted, a live legacy session supplies
|
|
1625
1625
|
* its pinned revision, and every other headerless request is rejected.
|
|
@@ -1633,14 +1633,14 @@ function createDuplexServerTransport(transport) {
|
|
|
1633
1633
|
* the JSON-RPC envelope, then the stream ends) through `@orkestrel/server`'s generic
|
|
1634
1634
|
* {@link import('@orkestrel/server').createStream} seam; otherwise it is a plain JSON body.
|
|
1635
1635
|
*
|
|
1636
|
-
* **Sessions are a
|
|
1637
|
-
* session id. To make the transport
|
|
1638
|
-
* import('./middlewares.js').createMCPSession}
|
|
1636
|
+
* **Sessions are a separate, plug-and-play middleware.** `createMCPRoutes` mints / reads no
|
|
1637
|
+
* session id. To make the transport stateful, mount {@link
|
|
1638
|
+
* import('./middlewares.js').createMCPSession} in front — it owns the same `path`, mints +
|
|
1639
1639
|
* validates the `mcp-session-id`, and serves the resumable `GET {path}` + `DELETE {path}`,
|
|
1640
1640
|
* leaving this route to dispatch the validated `POST`.
|
|
1641
1641
|
*
|
|
1642
|
-
* This is
|
|
1643
|
-
*
|
|
1642
|
+
* This is mechanism, not policy: compose auth / rate-limiting (and the session middleware)
|
|
1643
|
+
* in front as ordinary middleware; the optional `origin` group carries the deployment's shared
|
|
1644
1644
|
* allowlist or explicitly delegates validation to an upstream layer.
|
|
1645
1645
|
*
|
|
1646
1646
|
* @typeParam TState - The consumer's opaque per-request state type
|
|
@@ -1669,8 +1669,8 @@ function createMCPRoutes(mcp, options) {
|
|
|
1669
1669
|
}];
|
|
1670
1670
|
}
|
|
1671
1671
|
/**
|
|
1672
|
-
* Creates the HTTP
|
|
1673
|
-
* — a {@link MCPMessageTransportInterface} that drives a
|
|
1672
|
+
* Creates the HTTP client transport for an {@link import('@orkestrel/mcp').MCPClientInterface}
|
|
1673
|
+
* — a {@link MCPMessageTransportInterface} that drives a remote Streamable-HTTP MCP server
|
|
1674
1674
|
* over `fetch`. The egress mirror of {@link createMCPRoutes}.
|
|
1675
1675
|
*
|
|
1676
1676
|
* @remarks
|
|
@@ -1681,17 +1681,17 @@ function createMCPRoutes(mcp, options) {
|
|
|
1681
1681
|
* @remarks
|
|
1682
1682
|
* Hand it to `createMCPClient({ transport })`: each JSON-RPC message the client sends is
|
|
1683
1683
|
* `POST`ed to `options.url` with `content-type: application/json` and an `Accept` of
|
|
1684
|
-
* both `application/json` and `text/event-stream` (the server answers with
|
|
1684
|
+
* both `application/json` and `text/event-stream` (the server answers with either — a
|
|
1685
1685
|
* plain JSON envelope or a Streamable-HTTP SSE `data:` event, decoded with `@orkestrel/sse`),
|
|
1686
1686
|
* and the reply is surfaced on the transport's `message` event for the client's id
|
|
1687
1687
|
* correlation. Add `options.headers` (for example, an `Authorization` bearer) to reach a guarded
|
|
1688
|
-
* server. `start` / `close` hold no connection; against a
|
|
1688
|
+
* server. `start` / `close` hold no connection; against a stateful server it captures the
|
|
1689
1689
|
* `mcp-session-id` from `initialize` and echoes it on later requests. It also captures
|
|
1690
1690
|
* the initialize result's `protocolVersion` and sends `mcp-protocol-version` alone on each
|
|
1691
1691
|
* subsequent legacy request. Modern requests derive protocol and method headers directly
|
|
1692
1692
|
* from the message, plus a name header only for `tools/call`.
|
|
1693
1693
|
*
|
|
1694
|
-
* @param options - `url` (the remote endpoint;
|
|
1694
|
+
* @param options - `url` (the remote endpoint; required), optional `headers` merged onto
|
|
1695
1695
|
* every request, optional `fetch` (default `globalThis.fetch`), and optional `timeout`
|
|
1696
1696
|
* (ms, applied with `AbortSignal.timeout`); see {@link HTTPClientTransportOptions}
|
|
1697
1697
|
* @returns A working {@link MCPMessageTransportInterface} over `fetch`
|
|
@@ -1712,7 +1712,7 @@ function createHTTPClientTransport(options) {
|
|
|
1712
1712
|
return new HTTPClientTransport(options);
|
|
1713
1713
|
}
|
|
1714
1714
|
/**
|
|
1715
|
-
* Creates the MCP WebSocket transport
|
|
1715
|
+
* Creates the MCP WebSocket transport ingress — an {@link UpgradeHandler} that exposes a
|
|
1716
1716
|
* transport-agnostic {@link MCPDispatcherInterface} over a WebSocket, the WebSocket mirror of
|
|
1717
1717
|
* {@link createMCPRoutes}. Register it on the spine's upgrade seam.
|
|
1718
1718
|
*
|
|
@@ -1724,16 +1724,16 @@ function createHTTPClientTransport(options) {
|
|
|
1724
1724
|
* socket to the next handler (or destroys an unclaimed one): the `Upgrade` header is not
|
|
1725
1725
|
* `websocket`, the request path is not `options.path` (default {@link DEFAULT_MCP_PATH},
|
|
1726
1726
|
* `'/mcp'`), the `Sec-WebSocket-Key` is absent, or the `Sec-WebSocket-Version` is not `13`.
|
|
1727
|
-
* A decline
|
|
1727
|
+
* A decline never writes to the socket (it is not yet ours) — the spine owns the unclaimed
|
|
1728
1728
|
* outcome.
|
|
1729
1729
|
* - **Claims (returns `true`)** otherwise: it builds `createNodeWebSocket({ socket, key, head,
|
|
1730
1730
|
* protocol })` (SERVER mode → writes the `101` handshake, selects the configured subprotocol
|
|
1731
|
-
* only when the client's offer contains it, and sends
|
|
1731
|
+
* only when the client's offer contains it, and sends unmasked frames), wraps it in a
|
|
1732
1732
|
* {@link WebSocketServerTransport}, and pipes it through the core {@link
|
|
1733
1733
|
* import('@orkestrel/mcp').MCPTransportInterface} port through {@link
|
|
1734
1734
|
* createDuplexServerTransport} + {@link import('@orkestrel/mcp').bindServer}:
|
|
1735
|
-
* each inbound
|
|
1736
|
-
* as a frame — a
|
|
1735
|
+
* each inbound request runs through `mcp.dispatch`, and a defined response is written back
|
|
1736
|
+
* as a frame — a notification sends nothing, and a non-request message (a stray response) is
|
|
1737
1737
|
* ignored. A `dispatch` / `send` fault surfaces on `mcp.emitter`'s `error` event rather than
|
|
1738
1738
|
* escaping the (async) message pump.
|
|
1739
1739
|
* - **Closes on the spine's `stop`.** It holds every socket it claimed and, on `options.emitter`'s
|
|
@@ -1744,12 +1744,12 @@ function createHTTPClientTransport(options) {
|
|
|
1744
1744
|
* then have the connection cut mid-protocol. A socket the peer already dropped is gone from
|
|
1745
1745
|
* the set (its transport's `close` removes it), and closing a dead one is a no-op either way.
|
|
1746
1746
|
*
|
|
1747
|
-
* It is
|
|
1748
|
-
* handler
|
|
1747
|
+
* It is mechanism, not policy: compose an auth guard in front by registering an upgrade
|
|
1748
|
+
* handler before this one — that handler can claim (decline + destroy) an unauthenticated
|
|
1749
1749
|
* upgrade so it never reaches this pump.
|
|
1750
1750
|
*
|
|
1751
1751
|
* @param mcp - The transport-agnostic {@link MCPDispatcherInterface} to expose over WebSocket
|
|
1752
|
-
* @param options - The spine's `emitter` (
|
|
1752
|
+
* @param options - The spine's `emitter` (required — the `stop` event this ingress closes its
|
|
1753
1753
|
* sockets on), plus optional `path` (default {@link DEFAULT_MCP_PATH}) and `subprotocol`
|
|
1754
1754
|
* (default {@link MCP_WEBSOCKET_SUBPROTOCOL}); see {@link WebSocketServerOptions}
|
|
1755
1755
|
* @returns An {@link UpgradeHandler} to register with the spine's `upgrade` seam
|
|
@@ -1802,8 +1802,8 @@ function createWebSocketServer(mcp, options) {
|
|
|
1802
1802
|
};
|
|
1803
1803
|
}
|
|
1804
1804
|
/**
|
|
1805
|
-
* Creates the WebSocket
|
|
1806
|
-
* — a {@link MCPMessageTransportInterface} that drives a
|
|
1805
|
+
* Creates the WebSocket client transport for an {@link import('@orkestrel/mcp').MCPClientInterface}
|
|
1806
|
+
* — a {@link MCPMessageTransportInterface} that drives a remote MCP server over a WebSocket. The
|
|
1807
1807
|
* egress mirror of {@link createWebSocketServer} and the WebSocket sibling of {@link
|
|
1808
1808
|
* createHTTPClientTransport}.
|
|
1809
1809
|
*
|
|
@@ -1817,7 +1817,7 @@ function createWebSocketServer(mcp, options) {
|
|
|
1817
1817
|
* surfaced on the transport's `message` event for the client's id correlation. Add
|
|
1818
1818
|
* `options.headers` (for example, an `Authorization` bearer) to reach a guarded server.
|
|
1819
1819
|
*
|
|
1820
|
-
* @param options - `url` (the remote WebSocket endpoint;
|
|
1820
|
+
* @param options - `url` (the remote WebSocket endpoint; required) and optional `headers`
|
|
1821
1821
|
* merged onto the upgrade request; see {@link WebSocketClientTransportOptions}
|
|
1822
1822
|
* @returns A working {@link MCPMessageTransportInterface} over a WebSocket
|
|
1823
1823
|
*
|
|
@@ -1837,8 +1837,8 @@ function createWebSocketClientTransport(options) {
|
|
|
1837
1837
|
return new WebSocketClientTransport(options);
|
|
1838
1838
|
}
|
|
1839
1839
|
/**
|
|
1840
|
-
* Creates the stdio
|
|
1841
|
-
* — a {@link StdioClientTransportInterface} that spawns and drives a
|
|
1840
|
+
* Creates the stdio client transport for an {@link import('@orkestrel/mcp').MCPClientInterface}
|
|
1841
|
+
* — a {@link StdioClientTransportInterface} that spawns and drives a child process MCP server
|
|
1842
1842
|
* over newline-delimited JSON-RPC on `stdin`/`stdout`, the stdio sibling of {@link
|
|
1843
1843
|
* createHTTPClientTransport} and {@link createWebSocketClientTransport}.
|
|
1844
1844
|
*
|
|
@@ -1855,7 +1855,7 @@ function createWebSocketClientTransport(options) {
|
|
|
1855
1855
|
* waits before the `send` rejects. An omitted `delivery` selects {@link
|
|
1856
1856
|
* import('./constants.js').DEFAULT_MCP_DELIVERY}; an explicit `0` removes the bound.
|
|
1857
1857
|
*
|
|
1858
|
-
* @param options - `command` (the executable to spawn;
|
|
1858
|
+
* @param options - `command` (the executable to spawn; required), optional `args`,
|
|
1859
1859
|
* optional `env`, and an optional `delivery` bound in milliseconds on an unconfirmed
|
|
1860
1860
|
* `stdin` write; see {@link StdioClientTransportOptions}
|
|
1861
1861
|
* @returns A working {@link StdioClientTransportInterface} over a child process's stdio,
|
|
@@ -1877,7 +1877,7 @@ function createStdioClientTransport(options) {
|
|
|
1877
1877
|
return new StdioClientTransport(options);
|
|
1878
1878
|
}
|
|
1879
1879
|
/**
|
|
1880
|
-
* Creates the MCP stdio transport
|
|
1880
|
+
* Creates the MCP stdio transport ingress — pumps a transport-agnostic {@link
|
|
1881
1881
|
* MCPDispatcherInterface} over newline-delimited JSON-RPC on `stdin`/`stdout` (or an
|
|
1882
1882
|
* injected stream pair), the stdio mirror of {@link createWebSocketServer}.
|
|
1883
1883
|
*
|
|
@@ -1886,8 +1886,8 @@ function createStdioClientTransport(options) {
|
|
|
1886
1886
|
* `process.stdout`) in a {@link import('./transports/StdioServerTransport.js').StdioServerTransport}
|
|
1887
1887
|
* and pipes it through the core {@link import('@orkestrel/mcp').MCPTransportInterface} port
|
|
1888
1888
|
* through {@link createDuplexServerTransport} + {@link
|
|
1889
|
-
* import('@orkestrel/mcp').bindServer}: each inbound
|
|
1890
|
-
* a defined response is written back as a newline-terminated line — a
|
|
1889
|
+
* import('@orkestrel/mcp').bindServer}: each inbound request runs through `mcp.dispatch`, and
|
|
1890
|
+
* a defined response is written back as a newline-terminated line — a notification
|
|
1891
1891
|
* writes nothing, and a non-request message is ignored. A `dispatch` / `send` fault
|
|
1892
1892
|
* surfaces on `mcp.emitter`'s `error` event rather than escaping the (async) message
|
|
1893
1893
|
* pump.
|
|
@@ -1928,7 +1928,7 @@ function createStdioServer(mcp, options) {
|
|
|
1928
1928
|
* Creates the native MCP session {@link MiddlewareHandler} — the plug-and-play stateful layer
|
|
1929
1929
|
* that fronts a session-agnostic {@link import('./factories.js').createMCPRoutes}. Compose it
|
|
1930
1930
|
* with `router.use(createMCPSession())` (or the equivalent middleware seam), mirroring any
|
|
1931
|
-
* other closure-scoped stateful middleware. Has
|
|
1931
|
+
* other closure-scoped stateful middleware. Has no dependency on `@orkestrel/middleware` — the
|
|
1932
1932
|
* session store, mint-on-`initialize`, and resumable stream are all native to this package.
|
|
1933
1933
|
*
|
|
1934
1934
|
* @remarks
|
|
@@ -1941,32 +1941,32 @@ function createStdioServer(mcp, options) {
|
|
|
1941
1941
|
*
|
|
1942
1942
|
* - **`POST {path}`.** Buffers `const text = await request.text()` (so the downstream route
|
|
1943
1943
|
* can re-read it from a freshly-built forwarded `Request`). Resolves a session through {@link
|
|
1944
|
-
* readSessionHeader}: a
|
|
1945
|
-
*
|
|
1946
|
-
* isInitializeRequest})
|
|
1944
|
+
* readSessionHeader}: a valid id touches the entry and sets `context.state.session`; an
|
|
1945
|
+
* absent / unknown id whose (guarded) body parses to an `initialize` request ({@link
|
|
1946
|
+
* isInitializeRequest}) mints a fresh {@link MCPSession} (`crypto.randomUUID()`, the `session`
|
|
1947
1947
|
* options group) and sets `context.state.session`; neither → {@link rejectUnknownSession}
|
|
1948
1948
|
* (`404`). The
|
|
1949
1949
|
* minted entry pins the negotiated legacy revision, which is supplied to a later headerless
|
|
1950
1950
|
* live-session request. It then
|
|
1951
|
-
*
|
|
1951
|
+
* forwards a fresh `Request` carrying the buffered `text` (`next(forwarded)`) — never the
|
|
1952
1952
|
* already-consumed original — so the route re-reads the same body, and stamps the response
|
|
1953
|
-
* with {@link MCP_SESSION_HEADER}. The entry's `touched` instant is read
|
|
1954
|
-
* downstream response, because it means the
|
|
1953
|
+
* with {@link MCP_SESSION_HEADER}. The entry's `touched` instant is read after that
|
|
1954
|
+
* downstream response, because it means the last access: a request slower than `ttl` would
|
|
1955
1955
|
* otherwise store a session that is already expired, and the write-back RE-ASKS the store, so
|
|
1956
1956
|
* a `DELETE` arriving while the request was suspended is not undone.
|
|
1957
1957
|
* - **`GET {path}`.** Resolves the session the same way (no mint — only `initialize` mints);
|
|
1958
1958
|
* an invalid / unknown id is the same `404`. A valid session opens the resumable
|
|
1959
1959
|
* server→client stream through `@orkestrel/server`'s {@link import('@orkestrel/server').createStream}:
|
|
1960
|
-
* replays every event after the client's `Last-Event-ID` ({@link readLastEventId})
|
|
1960
|
+
* replays every event after the client's `Last-Event-ID` ({@link readLastEventId}) before
|
|
1961
1961
|
* attaching the stream for live pushes, then attaches; cancellation of the streamed response
|
|
1962
1962
|
* body composes with `request.signal` and detaches it. Long-lived — never `end()`ed here.
|
|
1963
1963
|
* - **`DELETE {path}`.** Resolves the session; a valid id deletes it from the store and answers
|
|
1964
1964
|
* `204`; an invalid / unknown id is the same `404`.
|
|
1965
1965
|
*
|
|
1966
|
-
* It is
|
|
1966
|
+
* It is mechanism, not policy, and additive: omit it entirely for the stateless default
|
|
1967
1967
|
* ({@link import('./factories.js').createMCPRoutes}'s only behavior). The `path` MUST match the
|
|
1968
1968
|
* `createMCPRoutes` `path` it fronts. The WebSocket transport is inherently one session per
|
|
1969
|
-
* connection (the socket
|
|
1969
|
+
* connection (the socket is the session), so this middleware does not apply to it.
|
|
1970
1970
|
*
|
|
1971
1971
|
* @typeParam TState - The consumer's `TState`, which MUST extend {@link MCPSessionState} so
|
|
1972
1972
|
* the resolved session can be threaded through `context.state.session`
|