@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
|
@@ -1,30 +1,27 @@
|
|
|
1
|
-
import { EmitterInterface } from '@orkestrel/emitter';
|
|
2
|
-
import { HTTPClientTransportOptions } from '@orkestrel/mcp';
|
|
3
|
-
import { IncomingMessage } from 'node:http';
|
|
4
|
-
import { JSONRPCInvocation } from '@orkestrel/mcp';
|
|
5
|
-
import { JSONRPCMessage } from '@orkestrel/mcp';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import { StreamInterface } from '@orkestrel/server';
|
|
26
|
-
import { TokenSecret } from '@orkestrel/server';
|
|
27
|
-
import { UpgradeHandler } from '@orkestrel/server';
|
|
1
|
+
import type { EmitterInterface } from '@orkestrel/emitter';
|
|
2
|
+
import type { HTTPClientTransportOptions } from '@orkestrel/mcp';
|
|
3
|
+
import type { IncomingMessage } from 'node:http';
|
|
4
|
+
import type { JSONRPCInvocation } from '@orkestrel/mcp';
|
|
5
|
+
import type { JSONRPCMessage } from '@orkestrel/mcp';
|
|
6
|
+
import type { JSONRPCResponse } from '@orkestrel/mcp';
|
|
7
|
+
import type { MCPContinuationInterface } from '@orkestrel/mcp';
|
|
8
|
+
import type { MCPDispatcherInterface } from '@orkestrel/mcp';
|
|
9
|
+
import type { MCPEra } from '@orkestrel/mcp';
|
|
10
|
+
import type { MCPHeaderParameter } from '@orkestrel/mcp';
|
|
11
|
+
import type { MCPLegacyVersion } from '@orkestrel/mcp';
|
|
12
|
+
import type { MCPMessageTransportEventMap } from '@orkestrel/mcp';
|
|
13
|
+
import type { MCPMessageTransportInterface } from '@orkestrel/mcp';
|
|
14
|
+
import type { MCPStreamControllerInterface } from '@orkestrel/mcp';
|
|
15
|
+
import type { MCPTransportInterface } from '@orkestrel/mcp';
|
|
16
|
+
import type { MCPVersion } from '@orkestrel/mcp';
|
|
17
|
+
import type { MiddlewareHandler } from '@orkestrel/server';
|
|
18
|
+
import type { NodeWebSocketInterface } from '@orkestrel/websocket';
|
|
19
|
+
import type { RouteContext } from '@orkestrel/router';
|
|
20
|
+
import type { RouteInput } from '@orkestrel/router';
|
|
21
|
+
import type { ServerEventMap } from '@orkestrel/server';
|
|
22
|
+
import type { StreamInterface } from '@orkestrel/server';
|
|
23
|
+
import type { TokenSecret } from '@orkestrel/server';
|
|
24
|
+
import type { UpgradeHandler } from '@orkestrel/server';
|
|
28
25
|
|
|
29
26
|
/**
|
|
30
27
|
* Checks whether the request's `Accept` header opts into a Server-Sent-Events response.
|
|
@@ -61,37 +58,37 @@ export declare function allowsOrigin(request: Request, options?: MCPOriginOption
|
|
|
61
58
|
* Creates the server-side mirror of
|
|
62
59
|
* {@link import('@orkestrel/mcp').createDuplexClientTransport}: the adapter that bridges a
|
|
63
60
|
* message-channel {@link MCPMessageTransportInterface}
|
|
64
|
-
* (the shape the stdio and WebSocket
|
|
61
|
+
* (the shape the stdio and WebSocket server transports already implement) onto the
|
|
65
62
|
* environment-agnostic {@link import('@orkestrel/mcp').MCPTransportInterface} port — what
|
|
66
63
|
* {@link createStdioServer} and {@link createWebSocketServer} pipe through `bindServer`, so
|
|
67
|
-
* the request/reply/error pump those factories used to hand-roll identically now lives
|
|
64
|
+
* the request/reply/error pump those factories used to hand-roll identically now lives once
|
|
68
65
|
* in the core binder. {@link import('@orkestrel/mcp').createDuplexClientTransport} adapts the
|
|
69
|
-
* same
|
|
66
|
+
* same contracts the other way.
|
|
70
67
|
*
|
|
71
68
|
* @remarks
|
|
72
69
|
* `send` decodes the already-serialized reply string back to a {@link JSONRPCMessage}
|
|
73
70
|
* and writes it through `transport.send` (the same `JSON.stringify` the underlying
|
|
74
71
|
* transport already performs, so the wire bytes are unchanged). `listen` filters
|
|
75
|
-
* `transport`'s `message` event to
|
|
72
|
+
* `transport`'s `message` event to invocations only — requests and notifications, never a
|
|
76
73
|
* stray response, exactly as the prior hand-rolled pumps did — and re-serializes each one
|
|
77
74
|
* back to a string for `bindServer`. `closed` bridges `transport`'s `close` event. `close`
|
|
78
75
|
* closes the underlying `transport`.
|
|
79
76
|
*
|
|
80
|
-
* @remarks A message crossing this bridge is decoded and re-encoded
|
|
81
|
-
*
|
|
77
|
+
* @remarks A message crossing this bridge is decoded and re-encoded twice, and that is
|
|
78
|
+
* accepted rather than accidental. Inbound: the carrier already parsed the frame into a
|
|
82
79
|
* {@link JSONRPCMessage}, and `listen` re-serializes it so `bindServer` can decode it again
|
|
83
80
|
* under the server's own `limit`. Outbound: `bindServer` serialized the reply, `send` parses
|
|
84
81
|
* it back, and the carrier stringifies it once more. The cost is two extra `JSON.parse` /
|
|
85
|
-
* `JSON.stringify` round trips per message, paid to keep
|
|
86
|
-
* of a hand-rolled one per carrier. It is
|
|
82
|
+
* `JSON.stringify` round trips per message, paid to keep one pump in the core binder instead
|
|
83
|
+
* of a hand-rolled one per carrier. It is bounded rather than unbounded because the binder
|
|
87
84
|
* decodes within `server.limit.message`, so an oversized frame is refused before the second
|
|
88
85
|
* decode rather than after it. Removing the cost means giving `MCPTransportInterface` a
|
|
89
86
|
* message-shaped face beside its string one, which every transport would then carry.
|
|
90
87
|
*
|
|
91
88
|
* @remarks Per {@link import('@orkestrel/mcp').MCPTransportInterface}, `listen`/`closed`
|
|
92
|
-
* each hold
|
|
89
|
+
* each hold the single current handler (a second call replaces the first, never adds).
|
|
93
90
|
* Because the underlying `transport.emitter` is ADD-based (`on` subscribes, never
|
|
94
|
-
* replaces), this bridge installs
|
|
91
|
+
* replaces), this bridge installs one stable emitter listener per event on first use
|
|
95
92
|
* and re-routes it to whichever handler is active (`undefined` while
|
|
96
93
|
* none is), so rebinding never double-dispatches.
|
|
97
94
|
*
|
|
@@ -114,8 +111,8 @@ export declare function allowsOrigin(request: Request, options?: MCPOriginOption
|
|
|
114
111
|
export declare function createDuplexServerTransport(transport: MCPMessageTransportInterface): MCPTransportInterface;
|
|
115
112
|
|
|
116
113
|
/**
|
|
117
|
-
* Creates the HTTP
|
|
118
|
-
* — a {@link MCPMessageTransportInterface} that drives a
|
|
114
|
+
* Creates the HTTP client transport for an {@link import('@orkestrel/mcp').MCPClientInterface}
|
|
115
|
+
* — a {@link MCPMessageTransportInterface} that drives a remote Streamable-HTTP MCP server
|
|
119
116
|
* over `fetch`. The egress mirror of {@link createMCPRoutes}.
|
|
120
117
|
*
|
|
121
118
|
* @remarks
|
|
@@ -126,17 +123,17 @@ export declare function createDuplexServerTransport(transport: MCPMessageTranspo
|
|
|
126
123
|
* @remarks
|
|
127
124
|
* Hand it to `createMCPClient({ transport })`: each JSON-RPC message the client sends is
|
|
128
125
|
* `POST`ed to `options.url` with `content-type: application/json` and an `Accept` of
|
|
129
|
-
* both `application/json` and `text/event-stream` (the server answers with
|
|
126
|
+
* both `application/json` and `text/event-stream` (the server answers with either — a
|
|
130
127
|
* plain JSON envelope or a Streamable-HTTP SSE `data:` event, decoded with `@orkestrel/sse`),
|
|
131
128
|
* and the reply is surfaced on the transport's `message` event for the client's id
|
|
132
129
|
* correlation. Add `options.headers` (for example, an `Authorization` bearer) to reach a guarded
|
|
133
|
-
* server. `start` / `close` hold no connection; against a
|
|
130
|
+
* server. `start` / `close` hold no connection; against a stateful server it captures the
|
|
134
131
|
* `mcp-session-id` from `initialize` and echoes it on later requests. It also captures
|
|
135
132
|
* the initialize result's `protocolVersion` and sends `mcp-protocol-version` alone on each
|
|
136
133
|
* subsequent legacy request. Modern requests derive protocol and method headers directly
|
|
137
134
|
* from the message, plus a name header only for `tools/call`.
|
|
138
135
|
*
|
|
139
|
-
* @param options - `url` (the remote endpoint;
|
|
136
|
+
* @param options - `url` (the remote endpoint; required), optional `headers` merged onto
|
|
140
137
|
* every request, optional `fetch` (default `globalThis.fetch`), and optional `timeout`
|
|
141
138
|
* (ms, applied with `AbortSignal.timeout`); see {@link HTTPClientTransportOptions}
|
|
142
139
|
* @returns A working {@link MCPMessageTransportInterface} over `fetch`
|
|
@@ -171,7 +168,7 @@ export declare function createMCPContinuation(secret: TokenSecret): MCPContinuat
|
|
|
171
168
|
* method carrying a named target — `tools/call` and `prompts/get` against `params.name`,
|
|
172
169
|
* `resources/read` against `params.uri` — with a Base64-sentinel value decoded before the
|
|
173
170
|
* comparison; a missing, mismatched, or invalidly encoded value returns HTTP `400` + `-32020`.
|
|
174
|
-
* A protocol header naming a
|
|
171
|
+
* A protocol header naming a modern revision holds the request to that revision whatever shape
|
|
175
172
|
* its body arrived in, so a body with no parsable modern `_meta` returns HTTP `400` + `-32602`.
|
|
176
173
|
* Headerless `initialize` is accepted, while every other headerless request needs a live legacy
|
|
177
174
|
* session to supply its pinned version. A legacy-shaped request carrying a protocol header is
|
|
@@ -213,14 +210,14 @@ export declare function createMCPPostHandler<TState = unknown>(mcp: MCPDispatche
|
|
|
213
210
|
* hand to `router.add(...)`.
|
|
214
211
|
*
|
|
215
212
|
* @remarks
|
|
216
|
-
* A
|
|
213
|
+
* A single `POST {path}` route — `createMCPRoutes` is stateless. The handler reads its own
|
|
217
214
|
* request body (its own JSON parse try/catch), so it works with or without a session
|
|
218
215
|
* middleware mounted in front. It draws a sharp line between TRANSPORT-level and
|
|
219
216
|
* DISPATCH-level outcomes:
|
|
220
217
|
*
|
|
221
218
|
* - A **transport** failure — a malformed JSON body, or a parsed value that is not a
|
|
222
|
-
* JSON-RPC
|
|
223
|
-
* error / `-32600` Invalid Request), with the `id` it could not read
|
|
219
|
+
* JSON-RPC invocation — is an HTTP `400` carrying a JSON-RPC error body (`-32700` Parse
|
|
220
|
+
* error / `-32600` Invalid Request), with the `id` it could not read omitted.
|
|
224
221
|
* - Modern protocol/method/name headers are validated against the body; a mismatch is
|
|
225
222
|
* HTTP `400` + `-32020`. Headerless initialize is accepted, a live legacy session supplies
|
|
226
223
|
* its pinned revision, and every other headerless request is rejected.
|
|
@@ -234,14 +231,14 @@ export declare function createMCPPostHandler<TState = unknown>(mcp: MCPDispatche
|
|
|
234
231
|
* the JSON-RPC envelope, then the stream ends) through `@orkestrel/server`'s generic
|
|
235
232
|
* {@link import('@orkestrel/server').createStream} seam; otherwise it is a plain JSON body.
|
|
236
233
|
*
|
|
237
|
-
* **Sessions are a
|
|
238
|
-
* session id. To make the transport
|
|
239
|
-
* import('./middlewares.js').createMCPSession}
|
|
234
|
+
* **Sessions are a separate, plug-and-play middleware.** `createMCPRoutes` mints / reads no
|
|
235
|
+
* session id. To make the transport stateful, mount {@link
|
|
236
|
+
* import('./middlewares.js').createMCPSession} in front — it owns the same `path`, mints +
|
|
240
237
|
* validates the `mcp-session-id`, and serves the resumable `GET {path}` + `DELETE {path}`,
|
|
241
238
|
* leaving this route to dispatch the validated `POST`.
|
|
242
239
|
*
|
|
243
|
-
* This is
|
|
244
|
-
*
|
|
240
|
+
* This is mechanism, not policy: compose auth / rate-limiting (and the session middleware)
|
|
241
|
+
* in front as ordinary middleware; the optional `origin` group carries the deployment's shared
|
|
245
242
|
* allowlist or explicitly delegates validation to an upstream layer.
|
|
246
243
|
*
|
|
247
244
|
* @typeParam TState - The consumer's opaque per-request state type
|
|
@@ -267,7 +264,7 @@ export declare function createMCPRoutes<TState = unknown>(mcp: MCPDispatcherInte
|
|
|
267
264
|
* Creates the native MCP session {@link MiddlewareHandler} — the plug-and-play stateful layer
|
|
268
265
|
* that fronts a session-agnostic {@link import('./factories.js').createMCPRoutes}. Compose it
|
|
269
266
|
* with `router.use(createMCPSession())` (or the equivalent middleware seam), mirroring any
|
|
270
|
-
* other closure-scoped stateful middleware. Has
|
|
267
|
+
* other closure-scoped stateful middleware. Has no dependency on `@orkestrel/middleware` — the
|
|
271
268
|
* session store, mint-on-`initialize`, and resumable stream are all native to this package.
|
|
272
269
|
*
|
|
273
270
|
* @remarks
|
|
@@ -280,32 +277,32 @@ export declare function createMCPRoutes<TState = unknown>(mcp: MCPDispatcherInte
|
|
|
280
277
|
*
|
|
281
278
|
* - **`POST {path}`.** Buffers `const text = await request.text()` (so the downstream route
|
|
282
279
|
* can re-read it from a freshly-built forwarded `Request`). Resolves a session through {@link
|
|
283
|
-
* readSessionHeader}: a
|
|
284
|
-
*
|
|
285
|
-
* isInitializeRequest})
|
|
280
|
+
* readSessionHeader}: a valid id touches the entry and sets `context.state.session`; an
|
|
281
|
+
* absent / unknown id whose (guarded) body parses to an `initialize` request ({@link
|
|
282
|
+
* isInitializeRequest}) mints a fresh {@link MCPSession} (`crypto.randomUUID()`, the `session`
|
|
286
283
|
* options group) and sets `context.state.session`; neither → {@link rejectUnknownSession}
|
|
287
284
|
* (`404`). The
|
|
288
285
|
* minted entry pins the negotiated legacy revision, which is supplied to a later headerless
|
|
289
286
|
* live-session request. It then
|
|
290
|
-
*
|
|
287
|
+
* forwards a fresh `Request` carrying the buffered `text` (`next(forwarded)`) — never the
|
|
291
288
|
* already-consumed original — so the route re-reads the same body, and stamps the response
|
|
292
|
-
* with {@link MCP_SESSION_HEADER}. The entry's `touched` instant is read
|
|
293
|
-
* downstream response, because it means the
|
|
289
|
+
* with {@link MCP_SESSION_HEADER}. The entry's `touched` instant is read after that
|
|
290
|
+
* downstream response, because it means the last access: a request slower than `ttl` would
|
|
294
291
|
* otherwise store a session that is already expired, and the write-back RE-ASKS the store, so
|
|
295
292
|
* a `DELETE` arriving while the request was suspended is not undone.
|
|
296
293
|
* - **`GET {path}`.** Resolves the session the same way (no mint — only `initialize` mints);
|
|
297
294
|
* an invalid / unknown id is the same `404`. A valid session opens the resumable
|
|
298
295
|
* server→client stream through `@orkestrel/server`'s {@link import('@orkestrel/server').createStream}:
|
|
299
|
-
* replays every event after the client's `Last-Event-ID` ({@link readLastEventId})
|
|
296
|
+
* replays every event after the client's `Last-Event-ID` ({@link readLastEventId}) before
|
|
300
297
|
* attaching the stream for live pushes, then attaches; cancellation of the streamed response
|
|
301
298
|
* body composes with `request.signal` and detaches it. Long-lived — never `end()`ed here.
|
|
302
299
|
* - **`DELETE {path}`.** Resolves the session; a valid id deletes it from the store and answers
|
|
303
300
|
* `204`; an invalid / unknown id is the same `404`.
|
|
304
301
|
*
|
|
305
|
-
* It is
|
|
302
|
+
* It is mechanism, not policy, and additive: omit it entirely for the stateless default
|
|
306
303
|
* ({@link import('./factories.js').createMCPRoutes}'s only behavior). The `path` MUST match the
|
|
307
304
|
* `createMCPRoutes` `path` it fronts. The WebSocket transport is inherently one session per
|
|
308
|
-
* connection (the socket
|
|
305
|
+
* connection (the socket is the session), so this middleware does not apply to it.
|
|
309
306
|
*
|
|
310
307
|
* @typeParam TState - The consumer's `TState`, which MUST extend {@link MCPSessionState} so
|
|
311
308
|
* the resolved session can be threaded through `context.state.session`
|
|
@@ -333,8 +330,8 @@ export declare function createMCPRoutes<TState = unknown>(mcp: MCPDispatcherInte
|
|
|
333
330
|
export declare function createMCPSession<TState extends MCPSessionState>(options?: MCPSessionMiddlewareOptions): MiddlewareHandler<TState>;
|
|
334
331
|
|
|
335
332
|
/**
|
|
336
|
-
* Creates the stdio
|
|
337
|
-
* — a {@link StdioClientTransportInterface} that spawns and drives a
|
|
333
|
+
* Creates the stdio client transport for an {@link import('@orkestrel/mcp').MCPClientInterface}
|
|
334
|
+
* — a {@link StdioClientTransportInterface} that spawns and drives a child process MCP server
|
|
338
335
|
* over newline-delimited JSON-RPC on `stdin`/`stdout`, the stdio sibling of {@link
|
|
339
336
|
* createHTTPClientTransport} and {@link createWebSocketClientTransport}.
|
|
340
337
|
*
|
|
@@ -351,7 +348,7 @@ export declare function createMCPSession<TState extends MCPSessionState>(options
|
|
|
351
348
|
* waits before the `send` rejects. An omitted `delivery` selects {@link
|
|
352
349
|
* import('./constants.js').DEFAULT_MCP_DELIVERY}; an explicit `0` removes the bound.
|
|
353
350
|
*
|
|
354
|
-
* @param options - `command` (the executable to spawn;
|
|
351
|
+
* @param options - `command` (the executable to spawn; required), optional `args`,
|
|
355
352
|
* optional `env`, and an optional `delivery` bound in milliseconds on an unconfirmed
|
|
356
353
|
* `stdin` write; see {@link StdioClientTransportOptions}
|
|
357
354
|
* @returns A working {@link StdioClientTransportInterface} over a child process's stdio,
|
|
@@ -372,7 +369,7 @@ export declare function createMCPSession<TState extends MCPSessionState>(options
|
|
|
372
369
|
export declare function createStdioClientTransport(options: StdioClientTransportOptions): StdioClientTransportInterface;
|
|
373
370
|
|
|
374
371
|
/**
|
|
375
|
-
* Creates the MCP stdio transport
|
|
372
|
+
* Creates the MCP stdio transport ingress — pumps a transport-agnostic {@link
|
|
376
373
|
* MCPDispatcherInterface} over newline-delimited JSON-RPC on `stdin`/`stdout` (or an
|
|
377
374
|
* injected stream pair), the stdio mirror of {@link createWebSocketServer}.
|
|
378
375
|
*
|
|
@@ -381,8 +378,8 @@ export declare function createStdioClientTransport(options: StdioClientTransport
|
|
|
381
378
|
* `process.stdout`) in a {@link import('./transports/StdioServerTransport.js').StdioServerTransport}
|
|
382
379
|
* and pipes it through the core {@link import('@orkestrel/mcp').MCPTransportInterface} port
|
|
383
380
|
* through {@link createDuplexServerTransport} + {@link
|
|
384
|
-
* import('@orkestrel/mcp').bindServer}: each inbound
|
|
385
|
-
* a defined response is written back as a newline-terminated line — a
|
|
381
|
+
* import('@orkestrel/mcp').bindServer}: each inbound request runs through `mcp.dispatch`, and
|
|
382
|
+
* a defined response is written back as a newline-terminated line — a notification
|
|
386
383
|
* writes nothing, and a non-request message is ignored. A `dispatch` / `send` fault
|
|
387
384
|
* surfaces on `mcp.emitter`'s `error` event rather than escaping the (async) message
|
|
388
385
|
* pump.
|
|
@@ -407,8 +404,8 @@ export declare function createStdioClientTransport(options: StdioClientTransport
|
|
|
407
404
|
export declare function createStdioServer(mcp: MCPDispatcherInterface, options?: StdioServerOptions): StdioServerInterface;
|
|
408
405
|
|
|
409
406
|
/**
|
|
410
|
-
* Creates the WebSocket
|
|
411
|
-
* — a {@link MCPMessageTransportInterface} that drives a
|
|
407
|
+
* Creates the WebSocket client transport for an {@link import('@orkestrel/mcp').MCPClientInterface}
|
|
408
|
+
* — a {@link MCPMessageTransportInterface} that drives a remote MCP server over a WebSocket. The
|
|
412
409
|
* egress mirror of {@link createWebSocketServer} and the WebSocket sibling of {@link
|
|
413
410
|
* createHTTPClientTransport}.
|
|
414
411
|
*
|
|
@@ -422,7 +419,7 @@ export declare function createStdioServer(mcp: MCPDispatcherInterface, options?:
|
|
|
422
419
|
* surfaced on the transport's `message` event for the client's id correlation. Add
|
|
423
420
|
* `options.headers` (for example, an `Authorization` bearer) to reach a guarded server.
|
|
424
421
|
*
|
|
425
|
-
* @param options - `url` (the remote WebSocket endpoint;
|
|
422
|
+
* @param options - `url` (the remote WebSocket endpoint; required) and optional `headers`
|
|
426
423
|
* merged onto the upgrade request; see {@link WebSocketClientTransportOptions}
|
|
427
424
|
* @returns A working {@link MCPMessageTransportInterface} over a WebSocket
|
|
428
425
|
*
|
|
@@ -441,7 +438,7 @@ export declare function createStdioServer(mcp: MCPDispatcherInterface, options?:
|
|
|
441
438
|
export declare function createWebSocketClientTransport(options: WebSocketClientTransportOptions): MCPMessageTransportInterface;
|
|
442
439
|
|
|
443
440
|
/**
|
|
444
|
-
* Creates the MCP WebSocket transport
|
|
441
|
+
* Creates the MCP WebSocket transport ingress — an {@link UpgradeHandler} that exposes a
|
|
445
442
|
* transport-agnostic {@link MCPDispatcherInterface} over a WebSocket, the WebSocket mirror of
|
|
446
443
|
* {@link createMCPRoutes}. Register it on the spine's upgrade seam.
|
|
447
444
|
*
|
|
@@ -453,16 +450,16 @@ export declare function createWebSocketClientTransport(options: WebSocketClientT
|
|
|
453
450
|
* socket to the next handler (or destroys an unclaimed one): the `Upgrade` header is not
|
|
454
451
|
* `websocket`, the request path is not `options.path` (default {@link DEFAULT_MCP_PATH},
|
|
455
452
|
* `'/mcp'`), the `Sec-WebSocket-Key` is absent, or the `Sec-WebSocket-Version` is not `13`.
|
|
456
|
-
* A decline
|
|
453
|
+
* A decline never writes to the socket (it is not yet ours) — the spine owns the unclaimed
|
|
457
454
|
* outcome.
|
|
458
455
|
* - **Claims (returns `true`)** otherwise: it builds `createNodeWebSocket({ socket, key, head,
|
|
459
456
|
* protocol })` (SERVER mode → writes the `101` handshake, selects the configured subprotocol
|
|
460
|
-
* only when the client's offer contains it, and sends
|
|
457
|
+
* only when the client's offer contains it, and sends unmasked frames), wraps it in a
|
|
461
458
|
* {@link WebSocketServerTransport}, and pipes it through the core {@link
|
|
462
459
|
* import('@orkestrel/mcp').MCPTransportInterface} port through {@link
|
|
463
460
|
* createDuplexServerTransport} + {@link import('@orkestrel/mcp').bindServer}:
|
|
464
|
-
* each inbound
|
|
465
|
-
* as a frame — a
|
|
461
|
+
* each inbound request runs through `mcp.dispatch`, and a defined response is written back
|
|
462
|
+
* as a frame — a notification sends nothing, and a non-request message (a stray response) is
|
|
466
463
|
* ignored. A `dispatch` / `send` fault surfaces on `mcp.emitter`'s `error` event rather than
|
|
467
464
|
* escaping the (async) message pump.
|
|
468
465
|
* - **Closes on the spine's `stop`.** It holds every socket it claimed and, on `options.emitter`'s
|
|
@@ -473,12 +470,12 @@ export declare function createWebSocketClientTransport(options: WebSocketClientT
|
|
|
473
470
|
* then have the connection cut mid-protocol. A socket the peer already dropped is gone from
|
|
474
471
|
* the set (its transport's `close` removes it), and closing a dead one is a no-op either way.
|
|
475
472
|
*
|
|
476
|
-
* It is
|
|
477
|
-
* handler
|
|
473
|
+
* It is mechanism, not policy: compose an auth guard in front by registering an upgrade
|
|
474
|
+
* handler before this one — that handler can claim (decline + destroy) an unauthenticated
|
|
478
475
|
* upgrade so it never reaches this pump.
|
|
479
476
|
*
|
|
480
477
|
* @param mcp - The transport-agnostic {@link MCPDispatcherInterface} to expose over WebSocket
|
|
481
|
-
* @param options - The spine's `emitter` (
|
|
478
|
+
* @param options - The spine's `emitter` (required — the `stop` event this ingress closes its
|
|
482
479
|
* sockets on), plus optional `path` (default {@link DEFAULT_MCP_PATH}) and `subprotocol`
|
|
483
480
|
* (default {@link MCP_WEBSOCKET_SUBPROTOCOL}); see {@link WebSocketServerOptions}
|
|
484
481
|
* @returns An {@link UpgradeHandler} to register with the spine's `upgrade` seam
|
|
@@ -502,7 +499,7 @@ export declare function createWebSocketServer(mcp: MCPDispatcherInterface, optio
|
|
|
502
499
|
*
|
|
503
500
|
* @remarks
|
|
504
501
|
* Ten seconds. The load-bearing property is the ordering, not the magnitude: this bound stays
|
|
505
|
-
*
|
|
502
|
+
* below {@link import('@orkestrel/mcp').DEFAULT_MCP_REQUEST_TIMEOUT}, so a write the child never
|
|
506
503
|
* reads fails as an undeliverable message while the request that carried it is still open,
|
|
507
504
|
* rather than being masked by that request's own deadline expiring first. Override per
|
|
508
505
|
* transport with `delivery`; an explicit `0` there removes the bound.
|
|
@@ -523,9 +520,9 @@ export declare const DEFAULT_MCP_KEEPALIVE_INTERVAL = 15000;
|
|
|
523
520
|
export declare const DEFAULT_MCP_PATH = "/mcp";
|
|
524
521
|
|
|
525
522
|
/**
|
|
526
|
-
* Sets the default capacity of a session's
|
|
523
|
+
* Sets the default capacity of a session's folded resumable event log (the per-{@link
|
|
527
524
|
* import('./MCPSession.js').MCPSession} replay log) — the maximum number of pushed
|
|
528
|
-
* server→client messages retained for replay before the
|
|
525
|
+
* server→client messages retained for replay before the oldest is evicted.
|
|
529
526
|
*
|
|
530
527
|
* @remarks
|
|
531
528
|
* Bounds the replay log's memory: only the most-recent {@link DEFAULT_MCP_SESSION_CAPACITY}
|
|
@@ -573,7 +570,7 @@ export declare function dispatchLines(emitter: EmitterInterface<MCPMessageTransp
|
|
|
573
570
|
*
|
|
574
571
|
* @remarks
|
|
575
572
|
* Concatenates `buffer` (the carried-forward partial line from the previous call)
|
|
576
|
-
* with `chunk`, splits on `'\n'`, and returns every
|
|
573
|
+
* with `chunk`, splits on `'\n'`, and returns every complete line (a `'\r'` trailing
|
|
577
574
|
* a line, from a CRLF-framed peer, is trimmed) plus the final, possibly-empty
|
|
578
575
|
* fragment as the new `remainder` — the caller threads it back in as the next call's
|
|
579
576
|
* `buffer`. A chunk containing no `'\n'` yields no lines and the whole (buffer +
|
|
@@ -589,22 +586,22 @@ export declare function extractLines(buffer: string, chunk: string): LineExtract
|
|
|
589
586
|
* Composes one incoming HTTP request lifetime with one MCP-owned SSE response lifetime.
|
|
590
587
|
*
|
|
591
588
|
* @remarks
|
|
592
|
-
* The composed {@link signal} observes request abort and
|
|
589
|
+
* The composed {@link signal} observes request abort and every way this response can end
|
|
593
590
|
* without one: consumer cancellation of the bridged body, a forwarding failure mid-pump, and a
|
|
594
591
|
* keepalive tick that finds the SSE stream already closed. That last pair is the whole point of
|
|
595
592
|
* the composition — a client that vanishes mid-stream aborts nothing by itself, so unless this
|
|
596
593
|
* object raises the signal on its own failure paths, the handler, the controlled stream, and
|
|
597
594
|
* the producer behind them all keep running for a response that can no longer be written.
|
|
598
|
-
* Graceful upstream completion is the one terminal that does
|
|
595
|
+
* Graceful upstream completion is the one terminal that does not abort: the body closes,
|
|
599
596
|
* because the exchange finished rather than ended.
|
|
600
597
|
*
|
|
601
598
|
* {@link bridge} preserves the source response status and headers, forwards its body bytes, and
|
|
602
599
|
* owns keepalive comments plus listener/timer cleanup until upstream completion, request abort,
|
|
603
600
|
* or consumer cancellation. This is a single-response lifecycle object, not a reusable bridge:
|
|
604
|
-
* a second {@link bridge} call
|
|
601
|
+
* a second {@link bridge} call throws rather than arming a second keepalive over one lifecycle.
|
|
605
602
|
* It supplies no handler or session policy.
|
|
606
603
|
*
|
|
607
|
-
* The keepalive interval is a
|
|
604
|
+
* The keepalive interval is a budget, sanitized like every other numeric knob in this package:
|
|
608
605
|
* anything that is not a positive integer — `0`, a negative, a fractional value, `NaN`,
|
|
609
606
|
* `Infinity` — falls back to {@link DEFAULT_MCP_KEEPALIVE_INTERVAL}, and a larger value clamps
|
|
610
607
|
* to Node's `2_147_483_647` ms timer maximum. None may reach the platform's timer floor, where
|
|
@@ -657,11 +654,11 @@ export declare class HTTPDisconnect {
|
|
|
657
654
|
* Options shared by the MCP Streamable-HTTP POST handler and route factory.
|
|
658
655
|
*
|
|
659
656
|
* @remarks
|
|
660
|
-
* - `streaming` — when `true` (the
|
|
657
|
+
* - `streaming` — when `true` (the default) the transport MAY answer with a
|
|
661
658
|
* Server-Sent-Events response (one `data:` event carrying the JSON-RPC reply, then
|
|
662
659
|
* the stream ends) whenever the client's `Accept` header includes
|
|
663
660
|
* `text/event-stream`; when `false` it always answers with a plain JSON body. Either
|
|
664
|
-
* mode carries the
|
|
661
|
+
* mode carries the same JSON-RPC response envelope — the choice is purely the wire
|
|
665
662
|
* framing the Streamable-HTTP spec lets the client negotiate.
|
|
666
663
|
* - `origin` — the shared origin-validation options passed to both the route and session
|
|
667
664
|
* enforcement sites. Validation is enabled by default: requests without `Origin` pass,
|
|
@@ -686,7 +683,7 @@ export declare interface HTTPHandlerOptions<TState = unknown> {
|
|
|
686
683
|
|
|
687
684
|
/**
|
|
688
685
|
* Options for `createMCPRoutes` — the mount path plus the shared POST-handler options.
|
|
689
|
-
* `createMCPRoutes` is
|
|
686
|
+
* `createMCPRoutes` is stateless; sessions are a separate middleware ({@link
|
|
690
687
|
* import('./middlewares.js').createMCPSession}), composed with `server.use`.
|
|
691
688
|
*
|
|
692
689
|
* @remarks
|
|
@@ -713,7 +710,7 @@ export declare interface HTTPTransportOptions<TState = unknown> extends HTTPHand
|
|
|
713
710
|
* body requires a protocol header after initialization. Messages name the expected value but
|
|
714
711
|
* never echo the client-supplied one.
|
|
715
712
|
*
|
|
716
|
-
* The expectation a
|
|
713
|
+
* The expectation a live session supplies is a different rule over a different input, so it
|
|
717
714
|
* is {@link inferSessionHeaderIssue} rather than a second arm of this one.
|
|
718
715
|
*
|
|
719
716
|
* @param request - The HTTP request carrying the headers
|
|
@@ -758,7 +755,7 @@ export declare function inferHeaderTarget(request: JSONRPCInvocation): string |
|
|
|
758
755
|
*
|
|
759
756
|
* @remarks
|
|
760
757
|
* A supported legacy request is pinned exactly. A modern, malformed, absent, or unsupported
|
|
761
|
-
* request selects the newest supported legacy revision. The read is deliberately the
|
|
758
|
+
* request selects the newest supported legacy revision. The read is deliberately the same one
|
|
762
759
|
* {@link import('@orkestrel/mcp').buildInitializeResult} performs — `isMCPLegacyVersion` over
|
|
763
760
|
* the requested revision — because the session version this pins and the version that result
|
|
764
761
|
* echoes must be the one value. Routing through `inferVersion` cannot do it: that inferer is
|
|
@@ -776,8 +773,8 @@ export declare function inferLegacyVersion(request: JSONRPCInvocation): MCPLegac
|
|
|
776
773
|
*
|
|
777
774
|
* @remarks
|
|
778
775
|
* The custom-header half of the standard-header seam {@link inferHeaderIssue} owns, and it
|
|
779
|
-
* takes the
|
|
780
|
-
* rule to the `Mcp-Param-*` names the server's
|
|
776
|
+
* takes the served definition's projections rather than a header issue: SEP-2243 scopes the
|
|
777
|
+
* rule to the `Mcp-Param-*` names the server's own tool definitions annotate, so a name no
|
|
781
778
|
* parameter claims is another party's header and travels through untouched.
|
|
782
779
|
*
|
|
783
780
|
* For each recognized parameter the body's value at the parameter's own property path fixes
|
|
@@ -813,7 +810,7 @@ export declare function inferParameterRefusal(request: Request, parameters: read
|
|
|
813
810
|
* The session layer's rule, distinct from the body-derived one {@link inferHeaderIssue} owns:
|
|
814
811
|
* a live legacy session pinned its revision at `initialize`, so every later request on that
|
|
815
812
|
* session must name the same one. An absent header reads as `missing`, which the session
|
|
816
|
-
* middleware answers by
|
|
813
|
+
* middleware answers by supplying the pinned revision rather than refusing; a present header
|
|
817
814
|
* naming another revision reads as `mismatched` and is refused. The message names the session's
|
|
818
815
|
* revision and never echoes the client-supplied value.
|
|
819
816
|
*
|
|
@@ -846,7 +843,7 @@ export declare function inferStatus(response: JSONRPCResponse | undefined, era:
|
|
|
846
843
|
|
|
847
844
|
/**
|
|
848
845
|
* Represents the result of folding one more chunk of raw stdio bytes into a newline-framed
|
|
849
|
-
* buffer — every
|
|
846
|
+
* buffer — every complete line extracted (newline-terminated in the wire bytes) plus
|
|
850
847
|
* the trailing partial line carried forward as the new `remainder`.
|
|
851
848
|
*
|
|
852
849
|
* @remarks
|
|
@@ -923,39 +920,39 @@ export declare interface MCPOriginOptions {
|
|
|
923
920
|
/**
|
|
924
921
|
* Represents one MCP transport session — the per-session entity a {@link
|
|
925
922
|
* import('./middlewares.js').createMCPSession} middleware owns, keyed by its `id`, carrying the
|
|
926
|
-
* resumable server→client push channel with its bounded replay log
|
|
923
|
+
* resumable server→client push channel with its bounded replay log folded in.
|
|
927
924
|
*
|
|
928
925
|
* @remarks
|
|
929
926
|
* One entity carries the whole session: it holds the
|
|
930
|
-
* session `id`, its
|
|
927
|
+
* session `id`, its own bounded, replayable log of pushed server→client messages (the
|
|
931
928
|
* resumable GET-SSE channel — a private `#events` `Map` + a monotone `#counter`, with
|
|
932
929
|
* `capacity` / `ttl` eviction, not a separate store), and the set of open
|
|
933
930
|
* server→client SSE streams (a resumable `GET {path}` registers through `attach`, unregisters through
|
|
934
931
|
* `detach` on disconnect). Still a small entity (not a record), built minimal + extensible.
|
|
935
932
|
*
|
|
936
|
-
* - **`push` is the server-initiated primitive.** It
|
|
937
|
-
* a monotone base36 event id) and
|
|
938
|
-
* SSE event (`stream.write({ id, data })`). A push with
|
|
939
|
-
* so a client that connects (or reconnects with a `Last-Event-ID`)
|
|
933
|
+
* - **`push` is the server-initiated primitive.** It appends the message to the log (assigning
|
|
934
|
+
* a monotone base36 event id) and fans it out to every attached stream as one `id:`-tagged
|
|
935
|
+
* SSE event (`stream.write({ id, data })`). A push with no attached stream is still logged,
|
|
936
|
+
* so a client that connects (or reconnects with a `Last-Event-ID`) later replays it from the
|
|
940
937
|
* log. A `write` to a closed stream is a safe no-op (the {@link
|
|
941
938
|
* `@orkestrel/server`'s `createStream` contract), so a just-disconnected stream that
|
|
942
939
|
* has not yet been `detach`ed never throws. A replayed event and the live one carry the
|
|
943
|
-
*
|
|
940
|
+
* identical id (the log assigns it once).
|
|
944
941
|
*
|
|
945
942
|
* - **`replay(afterId)` is strictly-after.** It returns every retained log entry whose id sorts
|
|
946
|
-
*
|
|
947
|
-
* before attaching the stream for live pushes. The decision for an
|
|
943
|
+
* after `afterId` in append order — the missed-events list the `GET {path}` handler writes
|
|
944
|
+
* before attaching the stream for live pushes. The decision for an unknown / already-evicted
|
|
948
945
|
* `afterId` (the client's cursor fell off the back of the capacity window, or never existed):
|
|
949
|
-
* replay
|
|
950
|
-
* lost (its cursor is
|
|
946
|
+
* replay nothing. Replaying the whole retained log would re-deliver events the client never
|
|
947
|
+
* lost (its cursor is older than everything retained); returning `[]` lets the handler then
|
|
951
948
|
* stream only the fresh pushes that follow `attach` — the spec-sane resume.
|
|
952
949
|
*
|
|
953
|
-
* - **Bounded, append-ordered, plain `Map`.** The log lives in
|
|
954
|
-
* `Map<id, entry>` — insertion order
|
|
955
|
-
* eviction both walk the map directly.
|
|
950
|
+
* - **Bounded, append-ordered, plain `Map`.** The log lives in one insertion-ordered
|
|
951
|
+
* `Map<id, entry>` — insertion order is append order is id order, so `replay` and capacity
|
|
952
|
+
* eviction both walk the map directly. No database mirror — the log is process-local
|
|
956
953
|
* transport mechanics, not durable state. `push` first drops every entry older than `ttl`
|
|
957
954
|
* (lazy TTL — no background timer, the middleware's lazy-window idiom), appends, then evicts
|
|
958
|
-
* the
|
|
955
|
+
* the oldest entries until at most `capacity` remain; `replay` also runs the lazy TTL sweep
|
|
959
956
|
* first, so a stale entry is never replayed.
|
|
960
957
|
*
|
|
961
958
|
* - **No transport coupling beyond the SSE seam.** It holds session state + the generic {@link
|
|
@@ -1030,28 +1027,34 @@ export declare interface MCPSessionEvent {
|
|
|
1030
1027
|
* Represents one MCP transport session — the per-session entity a {@link
|
|
1031
1028
|
* import('./middlewares.js').createMCPSession} middleware owns (the {@link
|
|
1032
1029
|
* import('./MCPSession.js').MCPSession} entity), carrying the resumable server→client push
|
|
1033
|
-
* channel with its bounded replay log
|
|
1030
|
+
* channel with its bounded replay log folded in.
|
|
1034
1031
|
*
|
|
1035
1032
|
* @remarks
|
|
1036
|
-
*
|
|
1037
|
-
*
|
|
1038
|
-
*
|
|
1039
|
-
*
|
|
1040
|
-
*
|
|
1041
|
-
* it when the client disconnects).
|
|
1042
|
-
* - `push(message)` — APPEND `message` to the session's folded replay log (assigning a
|
|
1043
|
-
* monotone event id, RETURNED) and FAN it out to every attached stream as one `id:`-tagged
|
|
1044
|
-
* SSE event — the server-initiated push primitive an in-request handler calls. A push with
|
|
1045
|
-
* no attached stream is still logged, so a later-connecting / reconnecting client replays it.
|
|
1046
|
-
* - `replay(afterId)` — the missed-events list (every retained log entry STRICTLY AFTER
|
|
1047
|
-
* `afterId`, in append order) the resumable `GET {path}` handler writes ahead of live pushes;
|
|
1048
|
-
* an unknown / evicted cursor replays NOTHING (the spec-sane resume).
|
|
1033
|
+
* The application addresses a session through `context.state.session`, the {@link
|
|
1034
|
+
* MCPSessionState} slice {@link import('./middlewares.js').createMCPSession} sets. A pushed
|
|
1035
|
+
* message with no attached stream is still logged, so a client that connects or reconnects
|
|
1036
|
+
* later replays it, and the resumable `GET {path}` handler writes a replay ahead of live
|
|
1037
|
+
* pushes.
|
|
1049
1038
|
*/
|
|
1050
1039
|
export declare interface MCPSessionInterface {
|
|
1040
|
+
/** Holds the opaque session id, a `crypto.randomUUID()` value echoed in the `mcp-session-id` header. */
|
|
1051
1041
|
readonly id: string;
|
|
1042
|
+
/**
|
|
1043
|
+
* Registers an open server→client SSE stream, a resumable `GET {path}`, so a later pushed
|
|
1044
|
+
* message reaches it.
|
|
1045
|
+
*/
|
|
1052
1046
|
attach(stream: StreamInterface): void;
|
|
1047
|
+
/** Unregisters a stream — the middleware calls it when the client disconnects. */
|
|
1053
1048
|
detach(stream: StreamInterface): void;
|
|
1049
|
+
/**
|
|
1050
|
+
* Appends a message to the folded replay log under a fresh monotone event id, returns that id,
|
|
1051
|
+
* and fans the message out to every attached stream as one `id:`-tagged SSE event.
|
|
1052
|
+
*/
|
|
1054
1053
|
push(message: JSONRPCMessage): string;
|
|
1054
|
+
/**
|
|
1055
|
+
* Returns every retained log entry strictly after a cursor, in append order; an unknown or
|
|
1056
|
+
* evicted cursor replays nothing.
|
|
1057
|
+
*/
|
|
1055
1058
|
replay(afterId: string): readonly MCPSessionEvent[];
|
|
1056
1059
|
}
|
|
1057
1060
|
|
|
@@ -1060,16 +1063,16 @@ export declare interface MCPSessionInterface {
|
|
|
1060
1063
|
* time-to-live, and the per-session resumable event-log bound.
|
|
1061
1064
|
*
|
|
1062
1065
|
* @remarks
|
|
1063
|
-
* - `path` — the request path the session middleware
|
|
1066
|
+
* - `path` — the request path the session middleware owns (must match the `createMCPRoutes`
|
|
1064
1067
|
* `path` it fronts); a request to any other path passes straight through. Defaults to
|
|
1065
1068
|
* {@link import('./constants.js').DEFAULT_MCP_PATH} (`'/mcp'`).
|
|
1066
1069
|
* - `ttl` — the session idle lifetime in milliseconds: a session not accessed within `ttl`
|
|
1067
|
-
* is treated as
|
|
1070
|
+
* is treated as absent and lazily evicted on the next access (no background timer — the
|
|
1068
1071
|
* `createRateLimiter` lazy-window idiom). Omit it for sessions that live until an explicit
|
|
1069
1072
|
* `DELETE`.
|
|
1070
1073
|
* - `session` — the knobs forwarded to each minted {@link MCPSession}: `capacity` bounds its
|
|
1071
1074
|
* replay log and `ttl` is that log's per-event lifetime. This type's own `ttl` bounds the
|
|
1072
|
-
*
|
|
1075
|
+
* session instead. An omitted leaf takes its {@link MCPSessionOptions} default, and an
|
|
1073
1076
|
* omitted `session.clock` inherits this type's own `clock`, so one injected clock governs
|
|
1074
1077
|
* both the store sweep and the log sweep unless a caller names a different one.
|
|
1075
1078
|
* - `clock` — the `() => number` epoch-ms clock {@link import('./middlewares.js').createMCPSession}
|
|
@@ -1100,7 +1103,7 @@ export declare interface MCPSessionMiddlewareOptions {
|
|
|
1100
1103
|
*
|
|
1101
1104
|
* @remarks
|
|
1102
1105
|
* - `capacity` — the maximum number of pushed server→client messages retained for replay
|
|
1103
|
-
* before the
|
|
1106
|
+
* before the oldest is evicted. Omit it for the {@link
|
|
1104
1107
|
* import('./constants.js').DEFAULT_MCP_SESSION_CAPACITY} default.
|
|
1105
1108
|
* - `ttl` — the PER-EVENT idle lifetime in milliseconds: a log entry older than `ttl` is
|
|
1106
1109
|
* dropped by the lazy sweep `push` and `replay` run, which bounds how far a reconnecting
|
|
@@ -1111,8 +1114,8 @@ export declare interface MCPSessionMiddlewareOptions {
|
|
|
1111
1114
|
* `Date.now`.
|
|
1112
1115
|
*
|
|
1113
1116
|
* The middleware's own knobs — the owned path, the idle-SESSION sweep window, origin
|
|
1114
|
-
* validation, and keepalive — live on {@link MCPSessionMiddlewareOptions}.
|
|
1115
|
-
*
|
|
1117
|
+
* validation, and keepalive — live on {@link MCPSessionMiddlewareOptions}. This type's `ttl`
|
|
1118
|
+
* and that one's measure different things, which is why they sit on different types.
|
|
1116
1119
|
*/
|
|
1117
1120
|
export declare interface MCPSessionOptions {
|
|
1118
1121
|
readonly capacity?: number;
|
|
@@ -1175,7 +1178,7 @@ export declare function readSessionHeader(request: Request): string | undefined;
|
|
|
1175
1178
|
* @remarks
|
|
1176
1179
|
* Returns `Response.json(buildJSONRPCError(undefined, JSONRPC_INVALID_REQUEST, 'Session not
|
|
1177
1180
|
* found'), { status: 404 })`, mirroring `createMCPRoutes`'s `400` transport-failure shape (a
|
|
1178
|
-
* JSON-RPC error
|
|
1181
|
+
* JSON-RPC error body with no id) but at the session-not-found status. Shared by
|
|
1179
1182
|
* every {@link import('./middlewares.js').createMCPSession} validation site — the
|
|
1180
1183
|
* non-`initialize` `POST` path, the resumable `GET {path}` open, and the `DELETE {path}`
|
|
1181
1184
|
* session-end (each a missing / unknown / TTL-evicted id) — so the single `404` envelope
|
|
@@ -1187,16 +1190,16 @@ export declare function rejectUnknownSession(): Response;
|
|
|
1187
1190
|
|
|
1188
1191
|
/**
|
|
1189
1192
|
* Pumps a controlled held-open exchange onto an open SSE stream — one `data:` event per
|
|
1190
|
-
* notification in order, then the terminating response — and
|
|
1193
|
+
* notification in order, then the terminating response — and end the exchange however the
|
|
1191
1194
|
* pump leaves.
|
|
1192
1195
|
*
|
|
1193
1196
|
* @remarks
|
|
1194
1197
|
* The Streamable-HTTP twin of {@link import('@orkestrel/mcp').sendStream}, and it owns exactly what
|
|
1195
|
-
* that owns. The `finally` releases the exchange on
|
|
1198
|
+
* that owns. The `finally` releases the exchange on every exit — the normal terminal, a
|
|
1196
1199
|
* producer that threw, a `write` that threw, and an abort alike — because nothing else will:
|
|
1197
1200
|
* a request whose client vanished cancels nothing by itself, so an exchange this pump walks
|
|
1198
1201
|
* away from keeps its producer, its request lifetime, and its live subscription slot forever.
|
|
1199
|
-
* The exchange is released
|
|
1202
|
+
* The exchange is released before the body ends, so the slot is already back when the response
|
|
1200
1203
|
* completes.
|
|
1201
1204
|
*
|
|
1202
1205
|
* Total — never throws and never rejects. A held-open SSE response has already sent its
|
|
@@ -1230,7 +1233,7 @@ export declare const SSE_BUFFERING_HEADER = "x-accel-buffering";
|
|
|
1230
1233
|
export declare const SSE_KEEPALIVE_COMMENT = "keepalive";
|
|
1231
1234
|
|
|
1232
1235
|
/**
|
|
1233
|
-
* Drives a
|
|
1236
|
+
* Drives a child process MCP server over newline-delimited JSON-RPC on `stdin`/`stdout` —
|
|
1234
1237
|
* a {@link StdioClientTransportInterface}, the stdio sibling of {@link
|
|
1235
1238
|
* import('@orkestrel/mcp').HTTPClientTransport} and {@link
|
|
1236
1239
|
* import('./WebSocketClientTransport.js').WebSocketClientTransport}.
|
|
@@ -1246,14 +1249,14 @@ export declare const SSE_KEEPALIVE_COMMENT = "keepalive";
|
|
|
1246
1249
|
* line is decoded and delivered through the shared {@link dispatchLines} helper — a well-formed
|
|
1247
1250
|
* {@link JSONRPCMessage} emits `message`, a malformed line emits `error` (never throws).
|
|
1248
1251
|
* - **Outbound (`send`).** `send(message)` writes one newline-terminated `JSON.stringify`d line
|
|
1249
|
-
* through the supervisor's `send` and
|
|
1252
|
+
* through the supervisor's `send` and awaits its answer, so this promise settles only after the
|
|
1250
1253
|
* host reports the line handled rather than the moment the write is queued. The supervisor never
|
|
1251
1254
|
* rejects — it answers `false` for a channel that was closed, destroyed, or ended, for a write
|
|
1252
1255
|
* that failed, or for one that remained unconfirmed through `delivery`. A call made without a
|
|
1253
1256
|
* live child rejects as not connected; a `false` answer from a live child rejects as unable to
|
|
1254
1257
|
* deliver. The supervisor does not disclose which cause produced that answer.
|
|
1255
1258
|
* - **`close()`** runs the supervisor's bounded termination and teardown, then fires `close` once
|
|
1256
|
-
* (idempotent). That teardown reaches the child's
|
|
1259
|
+
* (idempotent). That teardown reaches the child's terminal moment, where the supervisor freezes
|
|
1257
1260
|
* `evidence`, ends `lines`, and settles `exit` together, so this transport needs no release of
|
|
1258
1261
|
* its own to get its line pump back: the stream ends under the pump rather than throwing at it.
|
|
1259
1262
|
* A line the supervisor had already framed behind the one being delivered is dropped rather than
|
|
@@ -1266,7 +1269,7 @@ export declare const SSE_KEEPALIVE_COMMENT = "keepalive";
|
|
|
1266
1269
|
* child's own process group `SIGTERM`, waits the grace window, then `SIGKILL`s through the same
|
|
1267
1270
|
* route, so the kill reaches grandchildren rather than orphaning them, while Windows ends the
|
|
1268
1271
|
* tree with `taskkill /F /T`, which nothing in the child can intercept.
|
|
1269
|
-
* - **Evidence.** `evidence` reports that retained stderr tail off the
|
|
1272
|
+
* - **Evidence.** `evidence` reports that retained stderr tail off the held child — its live tail
|
|
1270
1273
|
* while the child runs, and the value the supervisor froze at that child's terminal moment
|
|
1271
1274
|
* afterwards. The reference is held past that moment and replaced only by the next `start()`,
|
|
1272
1275
|
* which is what keeps a post-`close()` read stable without a private copy: the frozen value
|
|
@@ -1274,7 +1277,7 @@ export declare const SSE_KEEPALIVE_COMMENT = "keepalive";
|
|
|
1274
1277
|
* cannot grow it. See {@link StdioClientTransportInterface.evidence} for the readings and the
|
|
1275
1278
|
* byte bound.
|
|
1276
1279
|
* - **Observable.** Owns the `emitter` ({@link MCPMessageTransportEventMap}); the
|
|
1277
|
-
* emitter isolates a listener throw; `error` is a
|
|
1280
|
+
* emitter isolates a listener throw; `error` is a domain event (a transport-level
|
|
1278
1281
|
* fault, including the child spawn cause the supervisor surfaces and the notice that this
|
|
1279
1282
|
* lifetime's `evidence` was cut off at the `drain` bound), distinct from the emitter's own
|
|
1280
1283
|
* listener-error channel.
|
|
@@ -1289,7 +1292,7 @@ export declare const SSE_KEEPALIVE_COMMENT = "keepalive";
|
|
|
1289
1292
|
export declare class StdioClientTransport implements StdioClientTransportInterface {
|
|
1290
1293
|
#private;
|
|
1291
1294
|
constructor(options: StdioClientTransportOptions);
|
|
1292
|
-
get emitter(): EmitterInterface<
|
|
1295
|
+
get emitter(): EmitterInterface<MCPMessageTransportEventMap>;
|
|
1293
1296
|
get session(): string | undefined;
|
|
1294
1297
|
get duplex(): boolean;
|
|
1295
1298
|
get evidence(): string | undefined;
|
|
@@ -1304,7 +1307,7 @@ export declare class StdioClientTransport implements StdioClientTransportInterfa
|
|
|
1304
1307
|
* @throws Thrown with `stdio transport could not deliver the message` when a live child's write
|
|
1305
1308
|
* resolves `false`
|
|
1306
1309
|
*/
|
|
1307
|
-
send(message:
|
|
1310
|
+
send(message: JSONRPCMessage): Promise<void>;
|
|
1308
1311
|
close(): Promise<void>;
|
|
1309
1312
|
}
|
|
1310
1313
|
|
|
@@ -1334,7 +1337,7 @@ export declare interface StdioClientTransportInterface extends MCPMessageTranspo
|
|
|
1334
1337
|
* it exited on its own or `close()` terminated it, and `''` there for a child that ran and
|
|
1335
1338
|
* wrote nothing — an empty tail is a real reading of a silent child, distinct from the
|
|
1336
1339
|
* absent one.
|
|
1337
|
-
* - **Lifetime.** The tail follows the child that produced it. The supervisor
|
|
1340
|
+
* - **Lifetime.** The tail follows the child that produced it. The supervisor freezes it at
|
|
1338
1341
|
* that child's terminal moment — the moment `close()`'s teardown resolves past, and the
|
|
1339
1342
|
* moment the exit that fires this transport's `close` settles at — and this transport keeps
|
|
1340
1343
|
* reading that same child afterwards. The frozen value never moves again, which is what
|
|
@@ -1349,18 +1352,18 @@ export declare interface StdioClientTransportInterface extends MCPMessageTranspo
|
|
|
1349
1352
|
* its teardown barrier while those listeners run, so a `start()` one of them calls parks
|
|
1350
1353
|
* behind it and every later listener reads the ended child's frozen tail. A natural exit
|
|
1351
1354
|
* holds that barrier only across the `error` it reports at that end, so a restart begun
|
|
1352
|
-
*
|
|
1355
|
+
* there parks until `close` has been delivered, while a `close` listener that calls
|
|
1353
1356
|
* `start()` opens the next lifetime itself and replaces the value every listener after it
|
|
1354
1357
|
* would have read.
|
|
1355
1358
|
* - **What the close path carries.** The frozen value is what the supervisor had received by
|
|
1356
1359
|
* that terminal moment, not the child's complete output.
|
|
1357
1360
|
* Windows ends the tree with `taskkill /F /T`, which nothing in the child can intercept: a
|
|
1358
1361
|
* `SIGTERM` handler never runs there, so the bytes it would have written never exist. A
|
|
1359
|
-
* child that ends on its own closes its stderr first, and
|
|
1362
|
+
* child that ends on its own closes its stderr first, and that tail is complete.
|
|
1360
1363
|
* Where that moment arrived at the supervisor's `drain` bound rather than at the child's
|
|
1361
1364
|
* own stream close, the tail stops at the cutoff and later diagnostics may have existed;
|
|
1362
1365
|
* the transport emits an `error` naming that lifetime, so a partial tail reads as partial.
|
|
1363
|
-
* - **Bound.** The supervisor keeps the
|
|
1366
|
+
* - **Bound.** The supervisor keeps the end of the child's raw stderr bytes, at most
|
|
1364
1367
|
* `@orkestrel/process`'s {@link import('@orkestrel/process').PROCESS_EVIDENCE} (2048
|
|
1365
1368
|
* bytes under 0.0.6). A child that writes more than the bound loses its earliest output
|
|
1366
1369
|
* and keeps its last, which is the half that names why it died. The bound counts raw
|
|
@@ -1381,13 +1384,13 @@ export declare interface StdioClientTransportInterface extends MCPMessageTranspo
|
|
|
1381
1384
|
* stdio-framed MCP server (newline-delimited JSON-RPC over `stdin`/`stdout`).
|
|
1382
1385
|
*
|
|
1383
1386
|
* @remarks
|
|
1384
|
-
* - `command` — the executable to spawn (for example, `'node'`, `'./my-mcp-server'`).
|
|
1387
|
+
* - `command` — the executable to spawn (for example, `'node'`, `'./my-mcp-server'`). Required.
|
|
1385
1388
|
* - `args` — the command-line arguments passed to `command`; defaults to none.
|
|
1386
|
-
* - `env` — environment variable overrides
|
|
1389
|
+
* - `env` — environment variable overrides merged over the parent `process.env` for the
|
|
1387
1390
|
* spawned child (the composed `@orkestrel/process` supervisor's merge semantics): when
|
|
1388
|
-
*
|
|
1391
|
+
* omitted the child inherits the full `process.env`, when provided each named key overrides
|
|
1389
1392
|
* the inherited value while every unlisted key is still inherited. This transport cannot
|
|
1390
|
-
*
|
|
1393
|
+
* replace the inherited environment entirely — the supervisor always merges over the parent.
|
|
1391
1394
|
* - `delivery` — the bound in milliseconds on one unconfirmed write to the child's `stdin`;
|
|
1392
1395
|
* an explicit `0` opts out. Defaults to {@link import('./constants.js').DEFAULT_MCP_DELIVERY}.
|
|
1393
1396
|
*/
|
|
@@ -1406,10 +1409,10 @@ export declare interface StdioClientTransportOptions {
|
|
|
1406
1409
|
* that will never read it. Default: {@link import('./constants.js').DEFAULT_MCP_DELIVERY}.
|
|
1407
1410
|
*
|
|
1408
1411
|
* An explicit `0` opts out: the bound is off, and an unconfirmed write stays pending until
|
|
1409
|
-
* the channel faults or teardown settles it. Omission does
|
|
1412
|
+
* the channel faults or teardown settles it. Omission does not opt out here, which is where
|
|
1410
1413
|
* this option diverges from the supervisor's own `delivery` on {@link
|
|
1411
|
-
* import('@orkestrel/process').ProcessOptions} — omitted
|
|
1412
|
-
*
|
|
1414
|
+
* import('@orkestrel/process').ProcessOptions} — omitted there disables the bound, omitted
|
|
1415
|
+
* here selects the default.
|
|
1413
1416
|
*
|
|
1414
1417
|
* An out-of-range value surfaces at `start()` rather than at construction. This transport
|
|
1415
1418
|
* forwards the value verbatim and adds no validator of its own, so the supervisor's own timer
|
|
@@ -1420,25 +1423,28 @@ export declare interface StdioClientTransportOptions {
|
|
|
1420
1423
|
|
|
1421
1424
|
/**
|
|
1422
1425
|
* Arms and tears down the newline-delimited JSON-RPC pump over the {@link StdioServerOptions}
|
|
1423
|
-
* stream pair — the stdio
|
|
1426
|
+
* stream pair — the stdio ingress handle {@link import('./factories.js').createStdioServer}
|
|
1424
1427
|
* returns.
|
|
1425
1428
|
*
|
|
1426
1429
|
* @remarks
|
|
1427
|
-
*
|
|
1428
|
-
*
|
|
1429
|
-
*
|
|
1430
|
-
*
|
|
1431
|
-
* request still draws exactly one reply.
|
|
1432
|
-
* - `stop()` — unbind the pump and close the transport: the listeners `start()` put on
|
|
1433
|
-
* `input` / `output` are removed, every pending `send` rejects, and `input` is released so
|
|
1434
|
-
* the process can exit. The release is complete by the time the call returns, and a
|
|
1435
|
-
* repeated `stop()` does nothing.
|
|
1436
|
-
* - **One lifetime per handle.** `stop()` ends it permanently: a `start()` issued afterwards
|
|
1437
|
-
* arms nothing, and serving again takes a fresh
|
|
1438
|
-
* {@link import('./factories.js').createStdioServer} over a live stream pair.
|
|
1430
|
+
* The subscriptions are attached, and the release complete, by the time each call returns. One
|
|
1431
|
+
* lifetime per handle: the `stop` method ends it permanently, a `start` call issued afterwards
|
|
1432
|
+
* arms nothing, and serving again takes a fresh
|
|
1433
|
+
* {@link import('./factories.js').createStdioServer} over a live stream pair.
|
|
1439
1434
|
*/
|
|
1440
1435
|
export declare interface StdioServerInterface {
|
|
1436
|
+
/**
|
|
1437
|
+
* Arms the pump: subscribes to `input` and dispatches every complete line through the bound
|
|
1438
|
+
* {@link import('@orkestrel/mcp').MCPDispatcherInterface}, writing each defined response back to
|
|
1439
|
+
* `output`. The pump arms once, so a repeated call attaches nothing further and an inbound
|
|
1440
|
+
* request still draws exactly one reply.
|
|
1441
|
+
*/
|
|
1441
1442
|
start(): void;
|
|
1443
|
+
/**
|
|
1444
|
+
* Unbinds the pump and closes the transport: removes the listeners the `start` method put on
|
|
1445
|
+
* `input` and `output`, rejects every pending write, and releases `input` so the process can
|
|
1446
|
+
* exit. A repeated call does nothing.
|
|
1447
|
+
*/
|
|
1442
1448
|
stop(): void;
|
|
1443
1449
|
}
|
|
1444
1450
|
|
|
@@ -1481,7 +1487,7 @@ export declare interface StdioServerOptions {
|
|
|
1481
1487
|
* - **`close()`** removes this transport's input and output subscriptions, rejects every
|
|
1482
1488
|
* pending send, and fires its `close`
|
|
1483
1489
|
* event (idempotent). It pauses the input only when the caller was not already reading
|
|
1484
|
-
* it at `start` (`readableFlowing !== true`)
|
|
1490
|
+
* it at `start` (`readableFlowing !== true`) and no `data` listener remains once this
|
|
1485
1491
|
* transport's own is removed — so a process holding `process.stdin` can exit, and a
|
|
1486
1492
|
* caller's own flow is never stopped underneath it. The transport preserves flowing versus
|
|
1487
1493
|
* non-flowing state and restores every caller-owned listener. A Node stream that had never been
|
|
@@ -1492,13 +1498,13 @@ export declare interface StdioServerOptions {
|
|
|
1492
1498
|
* `process.stdin`/`process.stdout`), so the transport never destroys, ends, or blanket-clears
|
|
1493
1499
|
* them.
|
|
1494
1500
|
* - **Observable.** Owns the `emitter` ({@link MCPMessageTransportEventMap}); the
|
|
1495
|
-
* emitter isolates a listener throw; `error` is a
|
|
1501
|
+
* emitter isolates a listener throw; `error` is a domain event (a transport-level
|
|
1496
1502
|
* fault), distinct from the emitter's own listener-error channel.
|
|
1497
1503
|
*/
|
|
1498
|
-
export declare class StdioServerTransport implements
|
|
1504
|
+
export declare class StdioServerTransport implements MCPMessageTransportInterface {
|
|
1499
1505
|
#private;
|
|
1500
1506
|
constructor(input: NodeJS.ReadableStream, output: NodeJS.WritableStream);
|
|
1501
|
-
get emitter(): EmitterInterface<
|
|
1507
|
+
get emitter(): EmitterInterface<MCPMessageTransportEventMap>;
|
|
1502
1508
|
get session(): string | undefined;
|
|
1503
1509
|
get duplex(): boolean;
|
|
1504
1510
|
start(): Promise<void>;
|
|
@@ -1515,7 +1521,7 @@ export declare class StdioServerTransport implements MCPMessageTransportInterfac
|
|
|
1515
1521
|
* @throws Thrown with `stdio transport is not connected` after the transport closes
|
|
1516
1522
|
* @throws Thrown with the output callback error or synchronous write failure
|
|
1517
1523
|
*/
|
|
1518
|
-
send(message:
|
|
1524
|
+
send(message: JSONRPCMessage): Promise<void>;
|
|
1519
1525
|
close(): Promise<void>;
|
|
1520
1526
|
}
|
|
1521
1527
|
|
|
@@ -1524,7 +1530,7 @@ export declare class StdioServerTransport implements MCPMessageTransportInterfac
|
|
|
1524
1530
|
* the `createWebSocketServer` upgrade-path match.
|
|
1525
1531
|
*
|
|
1526
1532
|
* @remarks
|
|
1527
|
-
* A `node:http` {@link import('node:http').IncomingMessage}'s `url` is the request
|
|
1533
|
+
* A `node:http` {@link import('node:http').IncomingMessage}'s `url` is the request target
|
|
1528
1534
|
* (`'/mcp?x=1'`), narrowed with `isString` (never `as`) and defaulting to `'/'` for an
|
|
1529
1535
|
* absent target; it is parsed against a placeholder base (only the pathname matters for the upgrade
|
|
1530
1536
|
* decision) and the `pathname` returned. The upgrade handler compares this against its
|
|
@@ -1537,7 +1543,7 @@ export declare class StdioServerTransport implements MCPMessageTransportInterfac
|
|
|
1537
1543
|
export declare function upgradeRequestPath(request: IncomingMessage): string;
|
|
1538
1544
|
|
|
1539
1545
|
/**
|
|
1540
|
-
* Drives a
|
|
1546
|
+
* Drives a remote MCP server over a WebSocket — a client
|
|
1541
1547
|
* {@link MCPMessageTransportInterface} for the Model Context Protocol, the
|
|
1542
1548
|
* egress mirror of {@link import('./factories.js').createWebSocketServer} and the WebSocket
|
|
1543
1549
|
* sibling of {@link import('@orkestrel/mcp').HTTPClientTransport}.
|
|
@@ -1547,16 +1553,16 @@ export declare function upgradeRequestPath(request: IncomingMessage): string;
|
|
|
1547
1553
|
* RFC 6455 client handshake: it opens a `node:http`(`s`) `GET` carrying `Connection: Upgrade`
|
|
1548
1554
|
* / `Upgrade: websocket` / a random `Sec-WebSocket-Key` / `Sec-WebSocket-Version: 13` /
|
|
1549
1555
|
* `Sec-WebSocket-Protocol: mcp` (plus any `options.headers`), awaits the client `'upgrade'`
|
|
1550
|
-
* event, and
|
|
1551
|
-
* — a mismatch (or a non-`101` response, or a request error)
|
|
1556
|
+
* event, and validates `Sec-WebSocket-Accept === computeWebSocketAccept(key)` (the D2 helper)
|
|
1557
|
+
* — a mismatch (or a non-`101` response, or a request error) rejects `start()` and the socket
|
|
1552
1558
|
* is destroyed. On success it wraps the raw upgraded socket in `createNodeWebSocket({ socket,
|
|
1553
|
-
* head })` (
|
|
1559
|
+
* head })` (client mode — no key → frames are masked per RFC 6455 §5.3) and bridges its
|
|
1554
1560
|
* `message`.
|
|
1555
1561
|
* - **The arriving socket is RE-ASKED for, never assumed.** `start()` suspends across that
|
|
1556
1562
|
* connect and upgrade, so it re-checks the transport's state before installing anything: a
|
|
1557
1563
|
* concurrent `start()` that already installed a socket, or a {@link close} that ended the
|
|
1558
|
-
* transport while the handshake was on the wire, both
|
|
1559
|
-
*
|
|
1564
|
+
* transport while the handshake was on the wire, both win — the socket that arrives late is
|
|
1565
|
+
* destroyed and never bound, so no orphan is left re-emitting frames at nobody. Both
|
|
1560
1566
|
* `start()` calls still resolve; exactly one socket is ever bound.
|
|
1561
1567
|
* - **Inbound (`message`).** Each decoded text frame runs through the shared `deliverMessage`
|
|
1562
1568
|
* fold (parse, then narrow) — a {@link JSONRPCMessage} re-emits on this transport's `message`
|
|
@@ -1564,14 +1570,14 @@ export declare function upgradeRequestPath(request: IncomingMessage): string;
|
|
|
1564
1570
|
* non-JSON / non-message frame surfaces on `error` and is dropped. The socket's `close`
|
|
1565
1571
|
* / `error` bridge to this transport's events.
|
|
1566
1572
|
* - **Outbound (`send`).** `send(message)` writes one masked text frame. A socket write is not
|
|
1567
|
-
* confirmed, so this transport answers a closed channel from its own state
|
|
1573
|
+
* confirmed, so this transport answers a closed channel from its own state and the socket's
|
|
1568
1574
|
* `readyState`: a `send` with no bound socket — before `start()`, after `close()`, or after the
|
|
1569
|
-
* peer ended the socket — and a `send` on a bound socket that is not `OPEN` both
|
|
1575
|
+
* peer ended the socket — and a `send` on a bound socket that is not `OPEN` both reject with
|
|
1570
1576
|
* `WebSocket transport is not connected`. It neither drops the message nor queues it for a
|
|
1571
1577
|
* connection this transport is not holding — the browser face queues a pre-open send, and this
|
|
1572
1578
|
* one, holding no connection to flush it onto, rejects that too.
|
|
1573
1579
|
* - **`close()`** unsubscribes from the socket, closes it, and fires `close` (idempotent). An
|
|
1574
|
-
* upgrade still on the wire is
|
|
1580
|
+
* upgrade still on the wire is destroyed, so a `close()` during the handshake ends the
|
|
1575
1581
|
* transport at once instead of waiting for a peer that may never answer — the suspended
|
|
1576
1582
|
* `start()` resolves, because the close is the outcome its caller asked for.
|
|
1577
1583
|
* - **URL scheme.** `options.url` accepts a `ws://` / `wss://` URL or an `http://` / `https://`
|
|
@@ -1579,7 +1585,7 @@ export declare function upgradeRequestPath(request: IncomingMessage): string;
|
|
|
1579
1585
|
* → TLS through `node:https`). Either reaches the same endpoint.
|
|
1580
1586
|
* - **Observable.** Owns the `emitter` ({@link MCPMessageTransportEventMap}); every emit
|
|
1581
1587
|
* the emitter isolates a listener throw (a buggy observer never corrupts the transport);
|
|
1582
|
-
* `error` is a
|
|
1588
|
+
* `error` is a domain event (a transport-level fault).
|
|
1583
1589
|
*
|
|
1584
1590
|
* @example
|
|
1585
1591
|
* ```ts
|
|
@@ -1588,14 +1594,14 @@ export declare function upgradeRequestPath(request: IncomingMessage): string;
|
|
|
1588
1594
|
* await client.connect() // start() handshakes, then the MCP initialize runs over WS frames
|
|
1589
1595
|
* ```
|
|
1590
1596
|
*/
|
|
1591
|
-
export declare class WebSocketClientTransport implements
|
|
1597
|
+
export declare class WebSocketClientTransport implements MCPMessageTransportInterface {
|
|
1592
1598
|
#private;
|
|
1593
1599
|
constructor(options: WebSocketClientTransportOptions);
|
|
1594
|
-
get emitter(): EmitterInterface<
|
|
1600
|
+
get emitter(): EmitterInterface<MCPMessageTransportEventMap>;
|
|
1595
1601
|
get session(): string | undefined;
|
|
1596
1602
|
get duplex(): boolean;
|
|
1597
1603
|
start(): Promise<void>;
|
|
1598
|
-
send(message:
|
|
1604
|
+
send(message: JSONRPCMessage): Promise<void>;
|
|
1599
1605
|
close(): Promise<void>;
|
|
1600
1606
|
}
|
|
1601
1607
|
|
|
@@ -1605,15 +1611,15 @@ export declare class WebSocketClientTransport implements MCPMessageTransportInte
|
|
|
1605
1611
|
*
|
|
1606
1612
|
* @remarks
|
|
1607
1613
|
* - `url` — the absolute URL of the remote server's WebSocket endpoint. Accepts a `ws://` /
|
|
1608
|
-
* `wss://` URL
|
|
1614
|
+
* `wss://` URL or an `http://` / `https://` one (a `ws(s)` scheme is converted to `http(s)`
|
|
1609
1615
|
* for the underlying `node:http(s)` upgrade request; either reaches the same endpoint).
|
|
1610
|
-
*
|
|
1616
|
+
* Required.
|
|
1611
1617
|
* - `headers` — extra request headers merged onto the upgrade `GET` (for example, an `Authorization`
|
|
1612
1618
|
* bearer for a guarded server). The transport always sets `Connection: Upgrade`,
|
|
1613
1619
|
* `Upgrade: websocket`, a random `Sec-WebSocket-Key`, `Sec-WebSocket-Version: 13`, and
|
|
1614
1620
|
* `Sec-WebSocket-Protocol: mcp`; a header supplied here is merged on top.
|
|
1615
1621
|
*
|
|
1616
|
-
* **`headers` exists
|
|
1622
|
+
* **`headers` exists here and not on the browser face's `{ url, protocols }`, and that
|
|
1617
1623
|
* divergence is deliberate rather than a lag: the host performs the WebSocket handshake.**
|
|
1618
1624
|
* This face owns its own `node:http(s)` upgrade request, so it can set any header on it. A
|
|
1619
1625
|
* page cannot — the native `WebSocket` constructor takes a URL and subprotocols and nothing
|
|
@@ -1631,21 +1637,21 @@ export declare interface WebSocketClientTransportOptions {
|
|
|
1631
1637
|
*
|
|
1632
1638
|
* @remarks
|
|
1633
1639
|
* - `emitter` — the emitter of the `@orkestrel/server` spine this handler is registered on
|
|
1634
|
-
* (`server.emitter`).
|
|
1640
|
+
* (`server.emitter`). Required: on its `stop` event the handler closes every socket it
|
|
1635
1641
|
* still owns with the RFC 6455 close handshake, so the spine's drain settles at once. An
|
|
1636
1642
|
* upgraded socket is detached from the connection set the spine's own close walks, so
|
|
1637
1643
|
* nothing but the claimant can end it — leave it open and `stop()` spends its whole
|
|
1638
1644
|
* `drain` budget and then cuts the connection mid-protocol.
|
|
1639
|
-
* - `path` — the request path the upgrade handler
|
|
1645
|
+
* - `path` — the request path the upgrade handler claims; defaults to
|
|
1640
1646
|
* {@link import('./constants.js').DEFAULT_MCP_PATH} (`'/mcp'`, the same path the HTTP
|
|
1641
|
-
* transport mounts at). A protocol-upgrade request to any
|
|
1647
|
+
* transport mounts at). A protocol-upgrade request to any other path is declined
|
|
1642
1648
|
* (the handler returns `false`, so the spine fans it to the next handler or destroys it).
|
|
1643
1649
|
* - `subprotocol` — the WebSocket subprotocol selected in the `101` handshake's
|
|
1644
1650
|
* `Sec-WebSocket-Protocol`; defaults to {@link import('@orkestrel/mcp').MCP_WEBSOCKET_SUBPROTOCOL}
|
|
1645
1651
|
* (`'mcp'`). It is sent only when the client's offer contains that token.
|
|
1646
1652
|
*
|
|
1647
|
-
* Auth / origin policy is deliberately
|
|
1648
|
-
* transport is
|
|
1653
|
+
* Auth / origin policy is deliberately absent: like the HTTP transport, the WebSocket
|
|
1654
|
+
* transport is mechanism — compose a guard in front (a `Server.upgrade` handler registered
|
|
1649
1655
|
* before this one can decline an unauthenticated upgrade).
|
|
1650
1656
|
*/
|
|
1651
1657
|
export declare interface WebSocketServerOptions {
|
|
@@ -1656,15 +1662,15 @@ export declare interface WebSocketServerOptions {
|
|
|
1656
1662
|
|
|
1657
1663
|
/**
|
|
1658
1664
|
* Wraps a {@link NodeWebSocketInterface} (the RFC 6455 wire wrapper) as a
|
|
1659
|
-
* {@link MCPMessageTransportInterface} — the per-connection JSON-RPC-over-WebSocket
|
|
1665
|
+
* {@link MCPMessageTransportInterface} — the per-connection JSON-RPC-over-WebSocket server
|
|
1660
1666
|
* bridge, the bidirectional JSON-RPC message channel
|
|
1661
1667
|
* `createWebSocketServer` pumps `mcp.dispatch` over and the egress mirror's
|
|
1662
1668
|
* {@link import('./WebSocketClientTransport.js').WebSocketClientTransport} reuses.
|
|
1663
1669
|
*
|
|
1664
1670
|
* @remarks
|
|
1665
|
-
* - **Reuses `MCPMessageTransportInterface`.** It
|
|
1671
|
+
* - **Reuses `MCPMessageTransportInterface`.** It is the same generic carrier the HTTP
|
|
1666
1672
|
* client transport implements — `emitter` (`message` / `close` / `error`), `start`,
|
|
1667
|
-
* `send`, `close` — so the WebSocket server and client both speak
|
|
1673
|
+
* `send`, `close` — so the WebSocket server and client both speak one transport contract,
|
|
1668
1674
|
* no near-duplicate sibling interface. `session` is `undefined` (the stateless v1; a
|
|
1669
1675
|
* session id is the deferred sessions tier). The name keeps the role explicit even though
|
|
1670
1676
|
* the shape is shared.
|
|
@@ -1672,13 +1678,13 @@ export declare interface WebSocketServerOptions {
|
|
|
1672
1678
|
* frame runs through the shared `deliverMessage` fold (parse, then narrow) — a
|
|
1673
1679
|
* well-formed {@link JSONRPCMessage} is re-emitted on this transport's `message` event (the
|
|
1674
1680
|
* parsed envelope the {@link import('@orkestrel/mcp').MCPServerInterface} pump dispatches), while
|
|
1675
|
-
* a non-JSON or non-message frame is surfaced on `error` and
|
|
1681
|
+
* a non-JSON or non-message frame is surfaced on `error` and dropped, never thrown. It
|
|
1676
1682
|
* also bridges the socket's `close` → this transport's `close`, and the socket's `error`.
|
|
1677
1683
|
* - **Outbound (`send`).** `send(message)` writes one text frame
|
|
1678
1684
|
* (`nodeWs.send(JSON.stringify(message))`). The underlying wrapper no-ops a write on a
|
|
1679
1685
|
* non-open socket and confirms nothing, so this bridge answers a closed channel from its own
|
|
1680
1686
|
* state and the socket's `readyState`: a `send` after `close()`, after the peer's close, or on
|
|
1681
|
-
* a socket that is not `OPEN`
|
|
1687
|
+
* a socket that is not `OPEN` rejects with `WebSocket transport is not connected` rather than
|
|
1682
1688
|
* resolving on a frame nobody wrote. `bindServer` catches that rejection and routes it to the
|
|
1683
1689
|
* dispatcher's `error` event, and it aborts every in-flight request the moment this transport's
|
|
1684
1690
|
* `close` fires — so a peer that disconnects mid-request is answered by no write at all.
|
|
@@ -1689,16 +1695,16 @@ export declare interface WebSocketServerOptions {
|
|
|
1689
1695
|
* releases the same way, so a closed transport is never subscribed to a live socket.
|
|
1690
1696
|
* - **Observable.** Owns the `emitter` ({@link MCPMessageTransportEventMap}); the emitter
|
|
1691
1697
|
* isolates a listener throw (a buggy observer never corrupts the bridge). `error` is a
|
|
1692
|
-
*
|
|
1698
|
+
* domain event (a transport-level fault), distinct from the emitter's listener-error channel.
|
|
1693
1699
|
*/
|
|
1694
|
-
export declare class WebSocketServerTransport implements
|
|
1700
|
+
export declare class WebSocketServerTransport implements MCPMessageTransportInterface {
|
|
1695
1701
|
#private;
|
|
1696
1702
|
constructor(socket: NodeWebSocketInterface);
|
|
1697
|
-
get emitter(): EmitterInterface<
|
|
1703
|
+
get emitter(): EmitterInterface<MCPMessageTransportEventMap>;
|
|
1698
1704
|
get session(): string | undefined;
|
|
1699
1705
|
get duplex(): boolean;
|
|
1700
1706
|
start(): Promise<void>;
|
|
1701
|
-
send(message:
|
|
1707
|
+
send(message: JSONRPCMessage): Promise<void>;
|
|
1702
1708
|
close(): Promise<void>;
|
|
1703
1709
|
}
|
|
1704
1710
|
|
|
@@ -1709,12 +1715,12 @@ export declare class WebSocketServerTransport implements MCPMessageTransportInte
|
|
|
1709
1715
|
* The completion callback is the writable channel's backpressure boundary. A callback error and
|
|
1710
1716
|
* a synchronous `write` throw reject the returned promise with the original value.
|
|
1711
1717
|
*
|
|
1712
|
-
* That callback is the
|
|
1718
|
+
* That callback is the only thing that settles the promise: this helper holds no timer and no
|
|
1713
1719
|
* abort, so an output that neither confirms nor fails the write parks the promise for as long as
|
|
1714
1720
|
* the caller-owned stream holds the callback. A caller wanting a bound races this promise against
|
|
1715
1721
|
* one it owns — {@link import('./transports/StdioServerTransport.js').StdioServerTransport}
|
|
1716
1722
|
* registers such a bound per send and rejects it on `close()`, so closing the transport settles
|
|
1717
|
-
* the
|
|
1723
|
+
* the caller's `send` while the abandoned write stays with the stream that still holds its
|
|
1718
1724
|
* callback, reachable from nothing the transport retains.
|
|
1719
1725
|
*
|
|
1720
1726
|
* @param output - The writable stream that receives the line
|