@orkestrel/mcp 0.0.1

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.
@@ -0,0 +1,1044 @@
1
+ import { ClientTransportEventMap } from '../core/index.ts';
2
+ import { ClientTransportEventMap as ClientTransportEventMap_2 } from '../../core/index.ts';
3
+ import { ClientTransportInterface } from '../../core/index.ts';
4
+ import { ClientTransportInterface as ClientTransportInterface_2 } from '../core/index.ts';
5
+ import { EmitterInterface } from '@orkestrel/emitter';
6
+ import { IncomingMessage } from 'node:http';
7
+ import { JSONRPCMessage } from '../core/index.ts';
8
+ import { JSONRPCMessage as JSONRPCMessage_2 } from '../../core/index.ts';
9
+ import { MCPServerInterface } from '../core/index.ts';
10
+ import { MiddlewareHandler } from '@orkestrel/server';
11
+ import { NodeWebSocketInterface } from '@orkestrel/websocket';
12
+ import { RouteInput } from '@orkestrel/router';
13
+ import { StreamInterface } from '@orkestrel/server';
14
+ import { UpgradeHandler } from '@orkestrel/server/server';
15
+
16
+ /**
17
+ * Whether the request's `Accept` header opts into a Server-Sent-Events response.
18
+ *
19
+ * @remarks
20
+ * Reads the fetch-standard `Request.headers.get('accept')` and returns `true` when it
21
+ * contains `text/event-stream` (case-insensitive). The MCP `POST` handler uses it
22
+ * (together with the `streaming` option) to pick the Streamable-HTTP SSE response
23
+ * framing over a plain JSON body; the JSON-RPC envelope is identical either way. Total
24
+ * — an absent / unmatched header returns `false`.
25
+ *
26
+ * @param request - The fetch-standard `Request`
27
+ * @returns `true` when the client `Accept`s `text/event-stream`, else `false`
28
+ */
29
+ export declare function acceptsEventStream(request: Request): boolean;
30
+
31
+ /**
32
+ * Create the HTTP CLIENT transport for an {@link import('@src/core').MCPClientInterface}
33
+ * — a {@link ClientTransportInterface} that drives a REMOTE Streamable-HTTP MCP server
34
+ * over `fetch`. The egress mirror of {@link createMCPRoutes}.
35
+ *
36
+ * @remarks
37
+ * Hand it to `createMCPClient({ transport })`: each JSON-RPC message the client sends is
38
+ * `POST`ed to `options.url` with `content-type: application/json` and an `Accept` of
39
+ * both `application/json` and `text/event-stream` (the server answers with EITHER — a
40
+ * plain JSON envelope or a Streamable-HTTP SSE `data:` event, decoded via `@orkestrel/sse`),
41
+ * and the reply is surfaced on the transport's `message` event for the client's id
42
+ * correlation. Add `options.headers` (e.g. an `Authorization` bearer) to reach a guarded
43
+ * server. `start` / `close` hold no connection; against a STATEFUL server it captures the
44
+ * `mcp-session-id` from `initialize` and echoes it on later requests, so the same
45
+ * `MCPClient` passes session validation (a stateless server sends none).
46
+ *
47
+ * @param options - `url` (the remote endpoint; REQUIRED), optional `headers` merged onto
48
+ * every request, optional `fetch` (default `globalThis.fetch`), and optional `timeout`
49
+ * (ms, applied via `AbortSignal.timeout`); see {@link HTTPClientTransportOptions}
50
+ * @returns A working {@link ClientTransportInterface} over `fetch`
51
+ *
52
+ * @example
53
+ * ```ts
54
+ * import { createMCPClient } from '@src/core'
55
+ * import { createHTTPClientTransport } from '@src/server'
56
+ *
57
+ * const client = createMCPClient({
58
+ * transport: createHTTPClientTransport({ url: 'http://localhost:3000/mcp' }),
59
+ * })
60
+ * await client.connect()
61
+ * const tools = await client.tools()
62
+ * ```
63
+ */
64
+ export declare function createHTTPClientTransport(options: HTTPClientTransportOptions): ClientTransportInterface_2;
65
+
66
+ /**
67
+ * Create the MCP Streamable-HTTP transport routes — mounts a transport-agnostic
68
+ * {@link MCPServerInterface} (the `@src/core` dispatch core) on the fetch-standard router
69
+ * spine, pumping each `POST` body through `mcp.dispatch`. Returns the {@link RouteInput}s to
70
+ * hand to `router.add(...)`.
71
+ *
72
+ * @remarks
73
+ * A SINGLE `POST {path}` route — `createMCPRoutes` is STATELESS. The handler reads its own
74
+ * request body (its own JSON parse try/catch), so it works with or without a session
75
+ * middleware mounted in front. It draws a sharp line between TRANSPORT-level and
76
+ * DISPATCH-level outcomes:
77
+ *
78
+ * - A **transport** failure — a malformed JSON body, or a parsed value that is not a
79
+ * JSON-RPC REQUEST — is an HTTP `400` carrying a JSON-RPC error BODY (`-32700` Parse
80
+ * error / `-32600` Invalid Request, id `null`).
81
+ * - A **dispatch** result — a success OR an IN-BAND JSON-RPC error from `mcp.dispatch`
82
+ * (e.g. `-32601` method-not-found) — is an HTTP `200` carrying the JSON-RPC response
83
+ * envelope (the error is in-band per JSON-RPC, NOT an HTTP error).
84
+ * - A **notification** (a request with no `id`, which `dispatch` resolves to
85
+ * `undefined`) is a `202 Accepted` with no body.
86
+ *
87
+ * When `streaming` is enabled (the default) and the client `Accept`s `text/event-stream`,
88
+ * the `200` reply is framed as a Streamable-HTTP SSE response (one `data:` event carrying
89
+ * the JSON-RPC envelope, then the stream ends) via `@orkestrel/server`'s generic
90
+ * {@link import('@orkestrel/server').openStream} seam; otherwise it is a plain JSON body.
91
+ *
92
+ * **Sessions are a SEPARATE, plug-and-play middleware.** `createMCPRoutes` mints / reads no
93
+ * session id. To make the transport STATEFUL, mount {@link
94
+ * import('./middlewares.js').createMCPSession} IN FRONT — it owns the same `path`, mints +
95
+ * validates the `mcp-session-id`, and serves the resumable `GET {path}` + `DELETE {path}`,
96
+ * leaving this route to dispatch the validated `POST`.
97
+ *
98
+ * This is MECHANISM, not policy: compose auth / CORS / rate-limiting (and the session
99
+ * middleware) IN FRONT as ordinary middleware — the transport route adds none.
100
+ *
101
+ * @typeParam TState - The consumer's opaque per-request state type
102
+ * @param mcp - The transport-agnostic {@link MCPServerInterface} to expose over HTTP
103
+ * @param options - Optional `path` (default {@link DEFAULT_MCP_PATH}) and `streaming`
104
+ * (default `true`); see {@link HTTPTransportOptions}
105
+ * @returns The {@link RouteInput}s to register with the router
106
+ *
107
+ * @example
108
+ * ```ts
109
+ * import { createMCPServer, createToolManager } from '@src/core'
110
+ * import { createMCPRoutes } from '@src/server'
111
+ *
112
+ * const mcp = createMCPServer({ name: 'docs', version: '1.0.0', tools: createToolManager() })
113
+ * const routes = createMCPRoutes(mcp) // POST /mcp dispatches JSON-RPC (JSON or SSE per Accept)
114
+ * ```
115
+ */
116
+ export declare function createMCPRoutes<TState = unknown>(mcp: MCPServerInterface, options?: HTTPTransportOptions): readonly RouteInput<string, TState>[];
117
+
118
+ /**
119
+ * Create the native MCP session {@link MiddlewareHandler} — the plug-and-play stateful layer
120
+ * that fronts a session-agnostic {@link import('./factories.js').createMCPRoutes}. Compose it
121
+ * via `router.use(createMCPSession())` (or the equivalent middleware seam), mirroring any
122
+ * other closure-scoped stateful middleware. Has NO dependency on `@orkestrel/middleware` — the
123
+ * session store, mint-on-`initialize`, and resumable stream are all native to this package.
124
+ *
125
+ * @remarks
126
+ * Owns a closure `Map<string, MCPSessionEntry>` keyed by session id, and a single request
127
+ * `path` (default {@link DEFAULT_MCP_PATH}); a request to any other path passes straight
128
+ * through (`next()`).
129
+ *
130
+ * - **`POST {path}`.** Buffers `const text = await request.text()` (so the downstream route
131
+ * can re-read it via a freshly-built forwarded `Request`). Resolves a session via {@link
132
+ * readSessionHeader}: a VALID id touches the entry and sets `context.state.session`; an
133
+ * ABSENT / unknown id whose (guarded) body parses to an `initialize` request ({@link
134
+ * isInitializeRequest}) MINTS a fresh {@link MCPSession} (`crypto.randomUUID()`, `capacity`)
135
+ * and sets `context.state.session`; neither → {@link rejectUnknownSession} (`404`). It then
136
+ * FORWARDS a fresh `Request` carrying the buffered `text` (`next(forwarded)`) — never the
137
+ * already-consumed original — so the route re-reads the same body, and stamps the response
138
+ * with {@link MCP_SESSION_HEADER}.
139
+ * - **`GET {path}`.** Resolves the session the same way (no mint — only `initialize` mints);
140
+ * an invalid / unknown id is the same `404`. A valid session opens the resumable
141
+ * server→client stream via `@orkestrel/server`'s {@link import('@orkestrel/server').openStream}:
142
+ * replays every event after the client's `Last-Event-ID` ({@link readLastEventId}) BEFORE
143
+ * attaching the stream for live pushes, then attaches; a client disconnect (`request.signal`)
144
+ * detaches it. Long-lived — never `end()`ed here.
145
+ * - **`DELETE {path}`.** Resolves the session; a valid id deletes it from the store and answers
146
+ * `204`; an invalid / unknown id is the same `404`.
147
+ *
148
+ * It is MECHANISM, not policy, and ADDITIVE: omit it entirely for the stateless default
149
+ * ({@link import('./factories.js').createMCPRoutes}'s only behavior). The `path` MUST match the
150
+ * `createMCPRoutes` `path` it fronts. The WebSocket transport is inherently one session per
151
+ * connection (the socket IS the session), so this middleware does not apply to it.
152
+ *
153
+ * @typeParam TState - The consumer's `TState`, which MUST extend {@link MCPSessionState} so
154
+ * the resolved session can be threaded through `context.state.session`
155
+ * @param options - Optional `path` (default {@link DEFAULT_MCP_PATH}), `ttl` (idle-session
156
+ * sweep window, ms — omit for sessions that live until an explicit `DELETE`), `capacity`
157
+ * (the folded per-session replay-log bound), and `clock` (the deterministic epoch-ms clock;
158
+ * defaults to `Date.now`); see {@link MCPSessionOptions}
159
+ * @returns A {@link MiddlewareHandler} that mints / validates sessions + serves the resumable
160
+ * `GET` / `DELETE`
161
+ *
162
+ * @example
163
+ * ```ts
164
+ * import { createMCPServer, createToolManager } from '@src/core'
165
+ * import { createMCPRoutes, createMCPSession } from '@src/server'
166
+ *
167
+ * const mcp = createMCPServer({ name: 'docs', version: '1.0.0', tools: createToolManager() })
168
+ * router.use(createMCPSession({ ttl: 60_000 })) // stateful: mint + validate + resumable GET / DELETE
169
+ * router.add(createMCPRoutes(mcp)) // the route stays session-agnostic
170
+ * ```
171
+ */
172
+ export declare function createMCPSession<TState extends MCPSessionState>(options?: MCPSessionOptions): MiddlewareHandler<TState>;
173
+
174
+ /**
175
+ * Create the stdio CLIENT transport for an {@link import('@src/core').MCPClientInterface}
176
+ * — a {@link ClientTransportInterface} that spawns and drives a CHILD PROCESS MCP server
177
+ * over newline-delimited JSON-RPC on `stdin`/`stdout`, the stdio sibling of {@link
178
+ * createHTTPClientTransport} and {@link createWebSocketClientTransport}.
179
+ *
180
+ * @remarks
181
+ * Hand it to `createMCPClient({ transport })`: `start()` (run by `client.connect()`)
182
+ * spawns `options.command` with `options.args` and `options.env`, piping its
183
+ * `stdin`/`stdout` for the JSON-RPC channel (its `stderr` inherits the parent's for
184
+ * diagnostics). Each JSON-RPC message the client `send`s is written as one
185
+ * newline-terminated line to the child's `stdin`; each decoded reply line from the
186
+ * child's `stdout` is surfaced on the transport's `message` event for the client's
187
+ * id correlation.
188
+ *
189
+ * @param options - `command` (the executable to spawn; REQUIRED), optional `args`,
190
+ * and optional `env`; see {@link StdioClientTransportOptions}
191
+ * @returns A working {@link ClientTransportInterface} over a child process's stdio
192
+ *
193
+ * @example
194
+ * ```ts
195
+ * import { createMCPClient } from '@src/core'
196
+ * import { createStdioClientTransport } from '@src/server'
197
+ *
198
+ * const client = createMCPClient({
199
+ * transport: createStdioClientTransport({ command: 'node', args: ['./server.js'] }),
200
+ * })
201
+ * await client.connect()
202
+ * const tools = await client.tools()
203
+ * ```
204
+ */
205
+ export declare function createStdioClientTransport(options: StdioClientTransportOptions): ClientTransportInterface_2;
206
+
207
+ /**
208
+ * Create the MCP stdio transport INGRESS — pumps a transport-agnostic {@link
209
+ * MCPServerInterface} over newline-delimited JSON-RPC on `stdin`/`stdout` (or an
210
+ * injected stream pair), the stdio mirror of {@link createWebSocketServer}.
211
+ *
212
+ * @remarks
213
+ * Wraps `options.input` (default `process.stdin`) / `options.output` (default
214
+ * `process.stdout`) in a {@link import('./transports/StdioServerTransport.js').StdioServerTransport}
215
+ * and PUMPS: each inbound {@link import('@src/core').JSONRPCMessage} that is a
216
+ * REQUEST runs through `mcp.dispatch`, and a defined response is written back as a
217
+ * newline-terminated line — a NOTIFICATION (`dispatch` → `undefined`) writes
218
+ * nothing. A non-request message is ignored. The dispatch is guarded so a
219
+ * `dispatch` / `send` fault surfaces on the transport's `error` event rather than
220
+ * escaping the (async) message listener.
221
+ *
222
+ * @param mcp - The transport-agnostic {@link MCPServerInterface} to expose over stdio
223
+ * @param options - Optional injectable `input` / `output` streams; see
224
+ * {@link StdioServerOptions}
225
+ * @returns A `{ start(): void; stop(): void }` handle to arm / tear down the pump
226
+ *
227
+ * @example
228
+ * ```ts
229
+ * import { createMCPServer, createToolManager } from '@src/core'
230
+ * import { createStdioServer } from '@src/server'
231
+ *
232
+ * const mcp = createMCPServer({ name: 'docs', version: '1.0.0', tools: createToolManager() })
233
+ * createStdioServer(mcp).start() // an MCP client now connects over this process's stdio
234
+ * ```
235
+ */
236
+ export declare function createStdioServer(mcp: MCPServerInterface, options?: StdioServerOptions): {
237
+ start(): void;
238
+ stop(): void;
239
+ };
240
+
241
+ /**
242
+ * Create the WebSocket CLIENT transport for an {@link import('@src/core').MCPClientInterface}
243
+ * — a {@link ClientTransportInterface} that drives a REMOTE MCP server over a WebSocket. The
244
+ * egress mirror of {@link createWebSocketServer} and the WebSocket sibling of {@link
245
+ * createHTTPClientTransport}.
246
+ *
247
+ * @remarks
248
+ * Hand it to `createMCPClient({ transport })`: `start()` (run by `client.connect()`) performs
249
+ * the RFC 6455 client handshake against `options.url` (accepting a `ws://` / `wss://` or an
250
+ * `http://` / `https://` URL — a `ws(s)` scheme is converted to `http(s)` for the underlying
251
+ * upgrade request), validates the `Sec-WebSocket-Accept` (via `@orkestrel/websocket`'s
252
+ * `computeWebSocketAccept`), and opens a persistent bidirectional frame channel; each JSON-RPC
253
+ * message the client `send`s is written as one masked text frame, and each decoded reply is
254
+ * surfaced on the transport's `message` event for the client's id correlation. Add
255
+ * `options.headers` (e.g. an `Authorization` bearer) to reach a guarded server.
256
+ *
257
+ * @param options - `url` (the remote WebSocket endpoint; REQUIRED) and optional `headers`
258
+ * merged onto the upgrade request; see {@link WebSocketClientTransportOptions}
259
+ * @returns A working {@link ClientTransportInterface} over a WebSocket
260
+ *
261
+ * @example
262
+ * ```ts
263
+ * import { createMCPClient } from '@src/core'
264
+ * import { createWebSocketClientTransport } from '@src/server'
265
+ *
266
+ * const client = createMCPClient({
267
+ * transport: createWebSocketClientTransport({ url: 'ws://localhost:3000/mcp' }),
268
+ * })
269
+ * await client.connect()
270
+ * const tools = await client.tools()
271
+ * ```
272
+ */
273
+ export declare function createWebSocketClientTransport(options: WebSocketClientTransportOptions): ClientTransportInterface_2;
274
+
275
+ /**
276
+ * Create the MCP WebSocket transport INGRESS — an {@link UpgradeHandler} that exposes a
277
+ * transport-agnostic {@link MCPServerInterface} over a WebSocket, the WebSocket mirror of
278
+ * {@link createMCPRoutes}. Register it on the spine's upgrade seam.
279
+ *
280
+ * @remarks
281
+ * It composes the lean RFC 6455 `@orkestrel/websocket` wrapper over `@orkestrel/server`'s
282
+ * generic upgrade seam — the spine speaks no WebSocket, this handler does.
283
+ *
284
+ * - **Declines (returns `false`)** when the upgrade is not for it, so the spine fans the
285
+ * socket to the next handler (or destroys an unclaimed one): the `Upgrade` header is not
286
+ * `websocket`, the request path is not `options.path` (default {@link DEFAULT_MCP_PATH},
287
+ * `'/mcp'`), the `Sec-WebSocket-Key` is absent, or the `Sec-WebSocket-Version` is not `13`.
288
+ * A decline NEVER writes to the socket (it is not yet ours) — the spine owns the unclaimed
289
+ * outcome.
290
+ * - **Claims (returns `true`)** otherwise: it builds `createNodeWebSocket({ socket, key, head,
291
+ * protocol })` (SERVER mode → writes the `101` handshake, echoing the `subprotocol`, default
292
+ * {@link MCP_WEBSOCKET_SUBPROTOCOL} `'mcp'`, and sends UNMASKED frames), wraps it in a
293
+ * {@link WebSocketServerTransport}, and PUMPS: each inbound {@link
294
+ * import('@src/core').JSONRPCMessage} that is a REQUEST runs through `mcp.dispatch`, and a
295
+ * defined response is written back as a frame — a NOTIFICATION (`dispatch` → `undefined`)
296
+ * sends nothing. A non-request message (a stray response) is ignored. The dispatch is
297
+ * guarded so a `dispatch` / `send` fault surfaces on the transport's `error` event rather
298
+ * than escaping the (async) message listener.
299
+ *
300
+ * It is MECHANISM, not policy: compose an auth guard IN FRONT by registering an upgrade
301
+ * handler BEFORE this one — that handler can claim (decline + destroy) an unauthenticated
302
+ * upgrade so it never reaches this pump.
303
+ *
304
+ * @param mcp - The transport-agnostic {@link MCPServerInterface} to expose over WebSocket
305
+ * @param options - Optional `path` (default {@link DEFAULT_MCP_PATH}) and `subprotocol`
306
+ * (default {@link MCP_WEBSOCKET_SUBPROTOCOL}); see {@link WebSocketServerOptions}
307
+ * @returns An {@link UpgradeHandler} to register with the spine's `upgrade` seam
308
+ *
309
+ * @example
310
+ * ```ts
311
+ * import { createMCPServer, createToolManager } from '@src/core'
312
+ * import { createWebSocketServer } from '@src/server'
313
+ *
314
+ * const mcp = createMCPServer({ name: 'docs', version: '1.0.0', tools: createToolManager() })
315
+ * server.upgrade(createWebSocketServer(mcp)) // an MCP client now connects over ws://…/mcp
316
+ * ```
317
+ */
318
+ export declare function createWebSocketServer(mcp: MCPServerInterface, options?: WebSocketServerOptions): UpgradeHandler;
319
+
320
+ /**
321
+ * Decode one SSE event's `data` string into a {@link JSONRPCMessage}, or `undefined`
322
+ * when it is not one — the per-event step {@link readEventStream} folds over.
323
+ *
324
+ * @remarks
325
+ * `JSON.parse`s the `data` (the server serializes the JSON-RPC envelope as the event's
326
+ * `data`) inside a try/catch and narrows the parsed value with `parseJSONRPCMessage`.
327
+ * Total (§14): malformed JSON or a non-message value yields `undefined`, never throws.
328
+ *
329
+ * @param data - One SSE event's `data` payload
330
+ * @returns The decoded {@link JSONRPCMessage}, or `undefined`
331
+ */
332
+ export declare function decodeEvent(data: string): JSONRPCMessage | undefined;
333
+
334
+ /** The default request path `createMCPRoutes` mounts the transport's `POST` route at. */
335
+ export declare const DEFAULT_MCP_PATH = "/mcp";
336
+
337
+ /**
338
+ * The default capacity of a session's FOLDED resumable event log (the per-{@link
339
+ * import('./MCPSession.js').MCPSession} replay log) — the maximum number of pushed
340
+ * server→client messages retained for replay before the OLDEST is evicted.
341
+ *
342
+ * @remarks
343
+ * Bounds the replay log's memory: only the most-recent {@link DEFAULT_MCP_SESSION_CAPACITY}
344
+ * pushes are retained, so a client reconnecting with a `Last-Event-ID` older than that window
345
+ * replays nothing (its cursor fell off the back). Override per `createMCPSession`'s `capacity`
346
+ * for a deeper / shallower window.
347
+ */
348
+ export declare const DEFAULT_MCP_SESSION_CAPACITY = 1024;
349
+
350
+ /**
351
+ * The default per-event idle lifetime (ms) of a session's folded resumable event log — an
352
+ * entry older than this is lazily evicted on the next access (no background timer), bounding
353
+ * how far back a reconnecting client may replay.
354
+ *
355
+ * @remarks
356
+ * Five minutes — a generous reconnection window for a dropped SSE stream without retaining
357
+ * stale pushes indefinitely. The session's own idle TTL is the `createMCPSession` `ttl` knob;
358
+ * this bounds the replay log paired with it.
359
+ */
360
+ export declare const DEFAULT_MCP_SESSION_TTL = 300000;
361
+
362
+ /**
363
+ * Decode and deliver each complete newline-framed line onto a {@link
364
+ * ClientTransportEventMap} emitter — the shared per-chunk dispatch step both stdio
365
+ * transports (client and server) run their {@link extractLines} output through.
366
+ *
367
+ * @remarks
368
+ * A blank line is skipped (a stray trailing newline). Every other line is decoded
369
+ * with {@link decodeEvent} (`JSON.parse` + `parseJSONRPCMessage`, guarded); a
370
+ * well-formed {@link JSONRPCMessage} emits `message`, a malformed / non-message line
371
+ * emits `error` (§14 — total, never throws). Pure w.r.t. its own state — the emit is
372
+ * the caller-owned side effect.
373
+ *
374
+ * @param emitter - The transport's {@link EmitterInterface} to emit `message` / `error` onto
375
+ * @param lines - The complete lines (from {@link extractLines}) to decode and deliver
376
+ */
377
+ export declare function dispatchLines(emitter: EmitterInterface<ClientTransportEventMap>, lines: readonly string[]): void;
378
+
379
+ /**
380
+ * One entry of an {@link MCPSessionInterface}'s folded replay log — a single pushed {@link
381
+ * JSONRPCMessage} tagged with the monotone event `id` the session assigned and the `timestamp`
382
+ * it was appended at (for the lazy-TTL replay window).
383
+ *
384
+ * @remarks
385
+ * - `id` — the session-assigned, monotonically-increasing event id (a base36 string), the
386
+ * value a resumable client echoes back as its `Last-Event-ID` to replay from here.
387
+ * - `message` — the server→client {@link JSONRPCMessage} that was pushed.
388
+ * - `timestamp` — the epoch-ms instant the entry was appended, read by the TTL eviction.
389
+ *
390
+ * A plain value record (no behavior, §4.5) — the unit {@link MCPSessionInterface.replay}
391
+ * returns.
392
+ */
393
+ export declare interface EventStoreEntry {
394
+ readonly id: string;
395
+ readonly message: JSONRPCMessage;
396
+ readonly timestamp: number;
397
+ }
398
+
399
+ /**
400
+ * Fold one more chunk of raw stdio bytes into a newline-framed buffer — the shared
401
+ * line-framing step both stdio transports (client and server) read their inbound
402
+ * newline-delimited JSON-RPC messages through.
403
+ *
404
+ * @remarks
405
+ * Concatenates `buffer` (the carried-forward partial line from the previous call)
406
+ * with `chunk`, splits on `'\n'`, and returns every COMPLETE line (a `'\r'` trailing
407
+ * a line, from a CRLF-framed peer, is trimmed) plus the final, possibly-empty
408
+ * fragment as the new `remainder` — the caller threads it back in as the next call's
409
+ * `buffer`. A chunk containing no `'\n'` yields no lines and the whole (buffer +
410
+ * chunk) as `remainder`. Pure — no I/O, no instance state.
411
+ *
412
+ * @param buffer - The partial line carried forward from the previous chunk (`''` initially)
413
+ * @param chunk - The newly-read raw bytes (already decoded to a string)
414
+ * @returns The complete `lines` extracted (in order) and the trailing `remainder`
415
+ */
416
+ export declare function extractLines(buffer: string, chunk: string): LineExtraction;
417
+
418
+ /**
419
+ * The HTTP CLIENT transport for the Model Context Protocol — a
420
+ * {@link ClientTransportInterface} that drives a REMOTE Streamable-HTTP MCP server over
421
+ * `fetch`, the egress mirror of the server's `createMCPRoutes`.
422
+ *
423
+ * @remarks
424
+ * - **Request/response over `fetch`.** `send(message)` POSTs the JSON-serialized
425
+ * message (or batch) to `options.url` with `content-type: application/json` and an
426
+ * `Accept` of BOTH `application/json` and `text/event-stream` (so the server may
427
+ * answer with either framing) — plus any `options.headers` (e.g. an `Authorization`
428
+ * bearer). It then decodes the reply and emits each decoded {@link JSONRPCMessage} on
429
+ * the `message` event the {@link import('@src/core').MCPClientInterface} subscribes
430
+ * to.
431
+ * - **Both reply framings.** A `200` with an `application/json` body is parsed with
432
+ * `parseJSONRPCMessage`; a `200` with a `text/event-stream` body is decoded via the
433
+ * `@orkestrel/sse` {@link import('@orkestrel/sse').SSEParserInterface} ({@link
434
+ * readEventStream}) — the inverse of the server's `openStream` seam, so the wire
435
+ * round-trips. A `202`
436
+ * Accepted (a notification) carries no body and emits nothing.
437
+ * - **Session echo.** `start()` / `close()` are no-ops (a request/response transport
438
+ * holds no long-lived connection). The `mcp-session-id` response header, when a
439
+ * STATEFUL server sends one (on `initialize`), is captured into `session` and then
440
+ * ECHOED as the `mcp-session-id` request header on every SUBSEQUENT request — so an
441
+ * `MCPClient` passes a stateful server's session validation. Before initialize returns
442
+ * an id, `session` is `undefined` and no header is sent (safe against a stateless
443
+ * server, which neither sends nor expects one).
444
+ * - **Total at the boundary (§14).** Every reply is narrowed (`parseJSONRPCMessage`,
445
+ * the SSE decoder) — a non-message reply is dropped, never asserted; a `fetch` /
446
+ * decode failure surfaces on the `error` event rather than escaping `send`.
447
+ * - **Observable (§13).** Owns the `emitter` ({@link ClientTransportEventMap}); fires
448
+ * `message` per decoded reply, `error` on a fault, and `close` on `close()`.
449
+ *
450
+ * @example
451
+ * ```ts
452
+ * const transport = new HTTPClientTransport({ url: 'http://localhost:3000/mcp' })
453
+ * const client = new MCPClient({ transport })
454
+ * await client.connect()
455
+ * ```
456
+ */
457
+ export declare class HTTPClientTransport implements ClientTransportInterface {
458
+ #private;
459
+ constructor(options: HTTPClientTransportOptions);
460
+ get emitter(): EmitterInterface<ClientTransportEventMap_2>;
461
+ get session(): string | undefined;
462
+ start(): Promise<void>;
463
+ send(message: JSONRPCMessage_2 | readonly JSONRPCMessage_2[]): Promise<void>;
464
+ close(): Promise<void>;
465
+ }
466
+
467
+ /**
468
+ * Options for `createHTTPClientTransport` — the remote MCP server's URL and any extra
469
+ * request headers.
470
+ *
471
+ * @remarks
472
+ * - `url` — the absolute URL of the remote server's Streamable-HTTP endpoint (the
473
+ * `POST` target every JSON-RPC message is written to, e.g.
474
+ * `http://localhost:3000/mcp`). REQUIRED.
475
+ * - `headers` — extra request headers merged onto every `POST` (e.g. an
476
+ * `Authorization` bearer for a guarded server). The transport always sets
477
+ * `content-type: application/json` and an `Accept` of both `application/json` and
478
+ * `text/event-stream` (so the server may answer with either framing); a key supplied
479
+ * here is merged on top.
480
+ * - `fetch` — the `fetch` implementation to issue each `POST` with; defaults to
481
+ * `globalThis.fetch`. Injectable for a test double or a non-global `fetch`.
482
+ * - `timeout` — an optional per-request timeout in milliseconds; when set, each
483
+ * `fetch` call is issued with `signal: AbortSignal.timeout(timeout)`. Omit for no
484
+ * transport-level deadline.
485
+ */
486
+ export declare interface HTTPClientTransportOptions {
487
+ readonly url: string;
488
+ readonly headers?: Readonly<Record<string, string>>;
489
+ readonly fetch?: typeof fetch;
490
+ readonly timeout?: number;
491
+ }
492
+
493
+ /**
494
+ * Options for `createMCPRoutes` — the path the transport is mounted at and whether an SSE
495
+ * response is allowed. `createMCPRoutes` is STATELESS; sessions are a separate middleware
496
+ * ({@link import('./middlewares.js').createMCPSession}), composed via `server.use`.
497
+ *
498
+ * @remarks
499
+ * - `path` — the request path the single `POST` route answers; defaults to
500
+ * {@link import('./constants.js').DEFAULT_MCP_PATH} (`'/mcp'`). `GET` / `DELETE` to this
501
+ * path get the spine's automatic `405` unless a {@link
502
+ * import('./middlewares.js').createMCPSession} middleware (which owns the same `path`) is
503
+ * mounted IN FRONT to serve them.
504
+ * - `streaming` — when `true` (the DEFAULT) the transport MAY answer with a
505
+ * Server-Sent-Events response (one `data:` event carrying the JSON-RPC reply, then
506
+ * the stream ends) whenever the client's `Accept` header includes
507
+ * `text/event-stream`; when `false` it always answers with a plain JSON body. Either
508
+ * mode carries the SAME JSON-RPC response envelope — the choice is purely the wire
509
+ * framing the Streamable-HTTP spec lets the client negotiate.
510
+ */
511
+ export declare interface HTTPTransportOptions {
512
+ readonly path?: string;
513
+ readonly streaming?: boolean;
514
+ }
515
+
516
+ /**
517
+ * The result of folding one more chunk of raw stdio bytes into a newline-framed
518
+ * buffer — every COMPLETE line extracted (newline-terminated in the wire bytes) plus
519
+ * the trailing partial line carried forward as the new `remainder`.
520
+ *
521
+ * @remarks
522
+ * A plain value record (no behavior, §4.5) {@link import('./helpers.js').extractLines}
523
+ * returns; the caller threads `remainder` back in as the next call's `buffer`.
524
+ */
525
+ export declare interface LineExtraction {
526
+ readonly lines: readonly string[];
527
+ readonly remainder: string;
528
+ }
529
+
530
+ /**
531
+ * The Streamable-HTTP transport header that carries the negotiated MCP protocol version
532
+ * on a subsequent request. The version is negotiated in the `initialize` JSON-RPC result
533
+ * body; a stateful transport MAY additionally read this header to pin the per-request
534
+ * protocol version (optional — the result body remains the source of truth).
535
+ */
536
+ export declare const MCP_PROTOCOL_VERSION_HEADER = "mcp-protocol-version";
537
+
538
+ /**
539
+ * The Streamable-HTTP transport header that carries the MCP session id. When a {@link
540
+ * import('./middlewares.js').createMCPSession} middleware is mounted, it SETS this header on
541
+ * the `initialize` response (the minted id) and READS it on every subsequent request
542
+ * (validating the session); the stateless `createMCPRoutes` default neither sets nor reads it.
543
+ */
544
+ export declare const MCP_SESSION_HEADER = "mcp-session-id";
545
+
546
+ /**
547
+ * The WebSocket subprotocol the MCP-over-WebSocket transports negotiate — sent by the
548
+ * client in `Sec-WebSocket-Protocol`, echoed by the server in its `101` handshake.
549
+ *
550
+ * @remarks
551
+ * `createWebSocketServer` echoes it in the upgrade response and `createWebSocketClientTransport`
552
+ * requests it, so an MCP WebSocket endpoint is distinguishable from any other WebSocket on the
553
+ * same path. The default WebSocket upgrade path is {@link DEFAULT_MCP_PATH} (the same `'/mcp'`
554
+ * the HTTP transport mounts at) — the upgrade is selected by the `Upgrade: websocket` header,
555
+ * not a separate path.
556
+ */
557
+ export declare const MCP_WEBSOCKET_SUBPROTOCOL = "mcp";
558
+
559
+ /**
560
+ * One MCP transport session — the per-session entity a {@link
561
+ * import('./middlewares.js').createMCPSession} middleware owns, keyed by its `id`, carrying the
562
+ * resumable server→client push channel with its bounded replay log FOLDED IN.
563
+ *
564
+ * @remarks
565
+ * The single session entity (the old `SessionState` + `EventStore` merged): it holds the
566
+ * session `id`, its OWN bounded, replayable log of pushed server→client messages (the
567
+ * resumable GET-SSE channel — a private `#events` `Map` + a monotone `#counter`, with
568
+ * `capacity` / `ttl` eviction, NOT a separate store), and the set of currently OPEN
569
+ * server→client SSE streams (a resumable `GET {path}` registers via `attach`, unregisters via
570
+ * `detach` on disconnect). Still a small entity (not a record), built minimal + extensible.
571
+ *
572
+ * - **`push` is the server-initiated primitive.** It APPENDS the message to the log (assigning
573
+ * a monotone base36 event id) and FANS it out to every attached stream as one `id:`-tagged
574
+ * SSE event (`stream.write({ id, data })`). A push with NO attached stream is still logged,
575
+ * so a client that connects (or reconnects with a `Last-Event-ID`) LATER replays it from the
576
+ * log. A `write` to a closed stream is a safe no-op (the {@link
577
+ * `@orkestrel/server`'s `openStream` contract), so a just-disconnected stream that
578
+ * has not yet been `detach`ed never throws. A replayed event and the live one carry the
579
+ * IDENTICAL id (the log assigns it once).
580
+ *
581
+ * - **`replay(afterId)` is strictly-after.** It returns every retained log entry whose id sorts
582
+ * AFTER `afterId` in append order — the missed-events list the `GET {path}` handler writes
583
+ * before attaching the stream for live pushes. The decision for an UNKNOWN / already-evicted
584
+ * `afterId` (the client's cursor fell off the back of the capacity window, or never existed):
585
+ * replay NOTHING. Replaying the whole retained log would re-deliver events the client never
586
+ * lost (its cursor is OLDER than everything retained); returning `[]` lets the handler then
587
+ * stream only the fresh pushes that follow `attach` — the spec-sane resume.
588
+ *
589
+ * - **Bounded, append-ordered, plain `Map` (§21).** The log lives in ONE insertion-ordered
590
+ * `Map<id, entry>` — insertion order IS append order IS id order, so `replay` and capacity
591
+ * eviction both walk the map directly. NO database mirror — the log is process-local
592
+ * transport mechanics, not durable state. `push` first drops every entry older than `ttl`
593
+ * (lazy TTL — no background timer, the middleware's lazy-window idiom), appends, then evicts
594
+ * the OLDEST entries until at most `capacity` remain; `replay` also runs the lazy TTL sweep
595
+ * first, so a stale entry is never replayed.
596
+ *
597
+ * - **No transport coupling beyond the SSE seam.** It holds session state + the generic {@link
598
+ * StreamInterface} handles `attach` was handed — never a raw socket, request, or response.
599
+ * The middleware opens the stream (the spine seam) and registers it here; this class only
600
+ * serializes a message onto the already-open streams.
601
+ *
602
+ * - **Injected clock.** `push` / `replay` accept an optional `now` (epoch ms), defaulting to
603
+ * `Date.now()` — so a test drives TTL eviction with an elapsed clock rather than a real timer
604
+ * (AGENTS §16).
605
+ *
606
+ * @example
607
+ * ```ts
608
+ * const session = new MCPSession(crypto.randomUUID())
609
+ * session.attach(stream) // an open resumable GET-SSE stream
610
+ * session.push({ jsonrpc: '2.0', method: 'notifications/message', params: { text: 'hi' } })
611
+ * // → logged AND written to `stream` as an `id:`-tagged event; a reconnect replays it
612
+ * ```
613
+ */
614
+ export declare class MCPSession implements MCPSessionInterface {
615
+ #private;
616
+ constructor(id: string, options?: MCPSessionOptions);
617
+ get id(): string;
618
+ attach(stream: StreamInterface): void;
619
+ detach(stream: StreamInterface): void;
620
+ push(message: JSONRPCMessage, now?: number): string;
621
+ replay(afterId: string, now?: number): readonly EventStoreEntry[];
622
+ }
623
+
624
+ /**
625
+ * The closure store entry a {@link import('./middlewares.js').createMCPSession} middleware
626
+ * keeps per minted session — the live {@link MCPSession} entity plus the epoch-ms instant it
627
+ * was last touched (the lazy-TTL sweep's idle clock, independent of the session's own
628
+ * replay-log TTL).
629
+ *
630
+ * @remarks
631
+ * - `session` — the live {@link MCPSession} entity the store keys by session id.
632
+ * - `touched` — the epoch-ms instant of the last access; mutated (not replaced) on every
633
+ * resolved request so the middleware's lazy sweep can evict an idle entry past `ttl`.
634
+ */
635
+ export declare interface MCPSessionEntry {
636
+ readonly session: MCPSession;
637
+ touched: number;
638
+ }
639
+
640
+ /**
641
+ * One MCP transport session — the per-session entity a {@link
642
+ * import('./middlewares.js').createMCPSession} middleware owns (the {@link
643
+ * import('./MCPSession.js').MCPSession} entity), carrying the resumable server→client push
644
+ * channel with its bounded replay log FOLDED IN.
645
+ *
646
+ * @remarks
647
+ * - `id` — the opaque session id (a `crypto.randomUUID()`), echoed in the `mcp-session-id`
648
+ * header. The app reads it off `context.state.session` (the {@link MCPSessionState} slice
649
+ * {@link import('./middlewares.js').createMCPSession} sets) to address a push.
650
+ * - `attach(stream)` — register an OPEN server→client SSE stream (a resumable `GET {path}`)
651
+ * so future {@link push}es reach it; `detach(stream)` unregisters it (the middleware calls
652
+ * it when the client disconnects).
653
+ * - `push(message)` — APPEND `message` to the session's folded replay log (assigning a
654
+ * monotone event id, RETURNED) and FAN it out to every attached stream as one `id:`-tagged
655
+ * SSE event — the server-initiated push primitive an in-request handler calls. A push with
656
+ * no attached stream is still logged, so a later-connecting / reconnecting client replays it.
657
+ * - `replay(afterId)` — the missed-events list (every retained log entry STRICTLY AFTER
658
+ * `afterId`, in append order) the resumable `GET {path}` handler writes ahead of live pushes;
659
+ * an unknown / evicted cursor replays NOTHING (the spec-sane resume).
660
+ */
661
+ export declare interface MCPSessionInterface {
662
+ readonly id: string;
663
+ attach(stream: StreamInterface): void;
664
+ detach(stream: StreamInterface): void;
665
+ push(message: JSONRPCMessage): string;
666
+ replay(afterId: string): readonly EventStoreEntry[];
667
+ }
668
+
669
+ /**
670
+ * Options for `createMCPSession` — the path the session middleware owns, the session idle
671
+ * time-to-live, and the per-session resumable event-log bound.
672
+ *
673
+ * @remarks
674
+ * - `path` — the request path the session middleware OWNS (must match the `createMCPRoutes`
675
+ * `path` it fronts); a request to any other path passes straight through. Defaults to
676
+ * {@link import('./constants.js').DEFAULT_MCP_PATH} (`'/mcp'`).
677
+ * - `ttl` — the session idle lifetime in milliseconds: a session not accessed within `ttl`
678
+ * is treated as ABSENT and lazily evicted on the next access (no background timer — the
679
+ * `createRateLimiter` lazy-window idiom). Omit it for sessions that live until an explicit
680
+ * `DELETE`.
681
+ * - `capacity` — the FOLDED event-log bound per session: the maximum number of pushed
682
+ * server→client messages retained for replay before the OLDEST is evicted, paired with a
683
+ * per-event idle lifetime ({@link import('./constants.js').DEFAULT_MCP_SESSION_TTL}) that
684
+ * bounds how far a reconnecting client may replay. Omit it for the {@link
685
+ * import('./constants.js').DEFAULT_MCP_SESSION_CAPACITY} default. (The session `ttl` bounds
686
+ * the session; this `capacity` bounds its replay log — independent knobs.)
687
+ * - `clock` — the `() => number` epoch-ms clock {@link import('./middlewares.js').createMCPSession}
688
+ * uses directly for its own session-touch / TTL-sweep bookkeeping; defaults to `Date.now`. The
689
+ * deterministic clock a TTL test advances explicitly instead of racing a real idle window
690
+ * against wall-clock (AGENTS §16). Production never sets it.
691
+ */
692
+ export declare interface MCPSessionOptions {
693
+ readonly path?: string;
694
+ readonly ttl?: number;
695
+ readonly capacity?: number;
696
+ readonly clock?: () => number;
697
+ }
698
+
699
+ /**
700
+ * The `context.state` slice a {@link import('./middlewares.js').createMCPSession}
701
+ * middleware sets on a validated / minted request — a consumer's `TState` extends
702
+ * this so the downstream route handler can read `context.state.session` to `push`
703
+ * a server-initiated message onto the session's resumable stream.
704
+ *
705
+ * @remarks
706
+ * `session` is set on `initialize` (the minted session) and on every validated
707
+ * non-`initialize` `POST` (the resolved one); absent when the request never
708
+ * reached a resolved session (the middleware short-circuits those as a `404`
709
+ * before calling `next`).
710
+ */
711
+ export declare interface MCPSessionState {
712
+ session?: MCPSessionInterface;
713
+ }
714
+
715
+ /**
716
+ * Decode a `fetch` Response's Server-Sent-Events body into the JSON-RPC messages it
717
+ * carried — the CLIENT-side inverse of the server's Streamable-HTTP SSE response.
718
+ *
719
+ * @remarks
720
+ * Reads the whole `response.body` stream chunk-by-chunk through a `TextDecoder({
721
+ * stream: true })` (handling a multi-byte char split across reads) and `@orkestrel/sse`'s
722
+ * {@link SSEParserInterface} (handling a partial line / in-progress event split across
723
+ * reads), then narrows each dispatched event's `data` to a {@link JSONRPCMessage} via
724
+ * `parseJSONRPCMessage` (so a non-message / non-JSON `data:` event is DROPPED, never
725
+ * thrown — total, §14). It reuses the SAME `SSEParser` the server's `openStream` seam
726
+ * serializes against, so the wire round-trips. A `null` body (no stream) yields no
727
+ * messages; the {@link import('./transports/HTTPClientTransport.js').HTTPClientTransport}
728
+ * reads a request/response SSE reply (the server sends one `data:` event then ends), so
729
+ * this drains to completion.
730
+ *
731
+ * @param response - The SSE `fetch` Response to decode (its `body` is read to completion)
732
+ * @returns Every {@link JSONRPCMessage} the stream carried, in order
733
+ */
734
+ export declare function readEventStream(response: Response): Promise<readonly JSONRPCMessage[]>;
735
+
736
+ /**
737
+ * Read the request's `Last-Event-ID` header — the SSE resume cursor a client sends when it
738
+ * reconnects to the resumable `GET {path}` stream, or `undefined` when absent.
739
+ *
740
+ * @remarks
741
+ * Reads `request.headers.get('last-event-id')` — a fetch-standard `Headers` lookup — so a
742
+ * missing header reads as `undefined` (no resume, the stream starts fresh). The resumable
743
+ * `GET` handler in {@link import('./middlewares.js').createMCPSession} passes a present value
744
+ * to the session's {@link import('./types.js').MCPSessionInterface.replay} to re-deliver the
745
+ * missed events before attaching the stream for live pushes. Total — never throws.
746
+ *
747
+ * @param request - The fetch-standard `Request`
748
+ * @returns The last-event-id, or `undefined` when the header is absent
749
+ */
750
+ export declare function readLastEventId(request: Request): string | undefined;
751
+
752
+ /**
753
+ * Read the request's `mcp-session-id` header — the session id a stateful transport
754
+ * validates, or `undefined` when absent.
755
+ *
756
+ * @remarks
757
+ * Reads `request.headers.get(MCP_SESSION_HEADER)` — a fetch-standard `Headers` lookup
758
+ * (single-valued by construction, never an array) — so a missing header reads as
759
+ * `undefined` (no session). {@link import('./middlewares.js').createMCPSession} uses it on
760
+ * every `POST` / `GET` / `DELETE` to look the session up in its closure store; an
761
+ * `undefined` id is treated exactly like an unknown one (a `404`). Total — never throws.
762
+ *
763
+ * @param request - The fetch-standard `Request`
764
+ * @returns The session id, or `undefined` when the header is absent
765
+ */
766
+ export declare function readSessionHeader(request: Request): string | undefined;
767
+
768
+ /**
769
+ * Build the stateful transport's "unknown session" rejection — an HTTP `404` carrying a
770
+ * JSON-RPC error body.
771
+ *
772
+ * @remarks
773
+ * Returns `Response.json(jsonRPCError(null, JSONRPC_INVALID_REQUEST, 'Session not found'),
774
+ * { status: 404 })`, mirroring `createMCPRoutes`'s `400` transport-failure shape (a
775
+ * JSON-RPC error BODY with a `null` id) but at the session-not-found status. Shared by
776
+ * every {@link import('./middlewares.js').createMCPSession} validation site — the
777
+ * non-`initialize` `POST` path, the resumable `GET {path}` open, and the `DELETE {path}`
778
+ * session-end (each a missing / unknown / TTL-evicted id) — so the single `404` envelope
779
+ * is defined once. Total — never throws.
780
+ *
781
+ * @returns The `404` JSON-RPC error `Response`
782
+ */
783
+ export declare function rejectUnknownSession(): Response;
784
+
785
+ /**
786
+ * The stdio CLIENT transport for the Model Context Protocol — a
787
+ * {@link ClientTransportInterface} that drives a CHILD PROCESS MCP server over
788
+ * newline-delimited JSON-RPC on `stdin`/`stdout`, the stdio sibling of {@link
789
+ * import('./HTTPClientTransport.js').HTTPClientTransport} and {@link
790
+ * import('./WebSocketClientTransport.js').WebSocketClientTransport}.
791
+ *
792
+ * @remarks
793
+ * - **Spawns the server.** `start()` runs `node:child_process`'s `spawn(options.command,
794
+ * options.args, { env: options.env, stdio: ['pipe', 'pipe', 'inherit'] })` — the
795
+ * child's `stdin`/`stdout` are piped for the JSON-RPC channel, its `stderr` inherits
796
+ * the parent's (diagnostics pass through, never parsed as protocol).
797
+ * - **Inbound (`message`).** Each `stdout` chunk is folded through the shared
798
+ * {@link extractLines} line-framing helper (buffering a partial trailing line
799
+ * across reads); every complete line is decoded and delivered via the shared
800
+ * {@link dispatchLines} helper — a well-formed {@link JSONRPCMessage} emits
801
+ * `message`, a malformed line emits `error` (§14, never throws). The child's
802
+ * `close` bridges to this transport's `close`.
803
+ * - **Outbound (`send`).** `send(message | messages)` writes ONE newline-terminated
804
+ * `JSON.stringify`d line per message to the child's `stdin`.
805
+ * - **`close()`** kills the child process and fires `close` (idempotent).
806
+ * - **Observable (§13).** Owns the `emitter` ({@link ClientTransportEventMap}); the
807
+ * emitter isolates a listener throw; `error` is a DOMAIN event (a transport-level
808
+ * fault), distinct from the emitter's own listener-error channel.
809
+ *
810
+ * @example
811
+ * ```ts
812
+ * const transport = new StdioClientTransport({ command: 'node', args: ['./server.js'] })
813
+ * const client = new MCPClient({ transport })
814
+ * await client.connect() // start() spawns the child, then the MCP initialize runs over stdio
815
+ * ```
816
+ */
817
+ export declare class StdioClientTransport implements ClientTransportInterface {
818
+ #private;
819
+ constructor(options: StdioClientTransportOptions);
820
+ get emitter(): EmitterInterface<ClientTransportEventMap_2>;
821
+ get session(): string | undefined;
822
+ start(): Promise<void>;
823
+ send(message: JSONRPCMessage_2 | readonly JSONRPCMessage_2[]): Promise<void>;
824
+ close(): Promise<void>;
825
+ }
826
+
827
+ /**
828
+ * Options for `createStdioClientTransport` — the child process to spawn as a
829
+ * stdio-framed MCP server (newline-delimited JSON-RPC over `stdin`/`stdout`).
830
+ *
831
+ * @remarks
832
+ * - `command` — the executable to spawn (e.g. `'node'`, `'./my-mcp-server'`). REQUIRED.
833
+ * - `args` — the command-line arguments passed to `command`; defaults to none.
834
+ * - `env` — the environment variables for the spawned child, passed straight to
835
+ * `node:child_process`'s `spawn`; when OMITTED the child inherits the full
836
+ * `process.env` (the `spawn` default), when PROVIDED it REPLACES the inherited
837
+ * environment entirely (`spawn` semantics) — a caller wanting to extend rather
838
+ * than replace spreads `process.env` into `env` themselves.
839
+ */
840
+ export declare interface StdioClientTransportOptions {
841
+ readonly command: string;
842
+ readonly args?: readonly string[];
843
+ readonly env?: Readonly<Record<string, string>>;
844
+ }
845
+
846
+ /**
847
+ * Options for `createStdioServer` — the injectable stdin/stdout streams the server
848
+ * transport reads newline-delimited JSON-RPC requests from and writes responses to.
849
+ *
850
+ * @remarks
851
+ * - `input` — the readable stream carrying newline-delimited JSON-RPC requests;
852
+ * defaults to `process.stdin`. Injectable for a test double.
853
+ * - `output` — the writable stream newline-delimited JSON-RPC responses are written
854
+ * to; defaults to `process.stdout`. Injectable for a test double.
855
+ */
856
+ export declare interface StdioServerOptions {
857
+ readonly input?: NodeJS.ReadableStream;
858
+ readonly output?: NodeJS.WritableStream;
859
+ }
860
+
861
+ /**
862
+ * The stdio SERVER transport for the Model Context Protocol — wraps an injectable
863
+ * readable/writable stream pair (`process.stdin`/`process.stdout` in production, a
864
+ * test double in tests) as a {@link ClientTransportInterface}, the newline-delimited
865
+ * JSON-RPC channel {@link import('../factories.js').createStdioServer} pumps
866
+ * `mcp.dispatch` over, the stdio mirror of {@link
867
+ * import('./WebSocketServerTransport.js').WebSocketServerTransport}.
868
+ *
869
+ * @remarks
870
+ * - **Reuses `ClientTransportInterface` (§21).** The same generic carrier the HTTP
871
+ * and WebSocket server transports implement — `emitter` (`message` / `close` /
872
+ * `error`), `start`, `send`, `close`. `session` is `undefined` (the stateless v1).
873
+ * - **Inbound (`message`).** `start()` subscribes to `input`'s `data` event; each
874
+ * chunk is folded through the shared {@link extractLines} line-framing helper
875
+ * (buffering a partial trailing line across reads), and every complete line is
876
+ * decoded and delivered via the shared {@link dispatchLines} helper — a
877
+ * well-formed {@link JSONRPCMessage} re-emits on `message`, a malformed line
878
+ * emits `error` (§14, never throws). `input`'s `close` bridges to this
879
+ * transport's `close`.
880
+ * - **Outbound (`send`).** `send(message | messages)` writes ONE newline-terminated
881
+ * `JSON.stringify`d line per message to `output`.
882
+ * - **`close()`** fires this transport's `close` (idempotent) — the injected streams
883
+ * are owned by the caller (typically `process.stdin`/`process.stdout`, which must
884
+ * never be closed out from under the process) and are not torn down here.
885
+ * - **Observable (§13).** Owns the `emitter` ({@link ClientTransportEventMap}); the
886
+ * emitter isolates a listener throw; `error` is a DOMAIN event (a transport-level
887
+ * fault), distinct from the emitter's own listener-error channel.
888
+ */
889
+ export declare class StdioServerTransport implements ClientTransportInterface {
890
+ #private;
891
+ constructor(input: NodeJS.ReadableStream, output: NodeJS.WritableStream);
892
+ get emitter(): EmitterInterface<ClientTransportEventMap_2>;
893
+ get session(): string | undefined;
894
+ start(): Promise<void>;
895
+ send(message: JSONRPCMessage_2 | readonly JSONRPCMessage_2[]): Promise<void>;
896
+ close(): Promise<void>;
897
+ }
898
+
899
+ /**
900
+ * Read the path (without the query string) of a raw `node:http` protocol-upgrade request —
901
+ * the `createWebSocketServer` upgrade-path match.
902
+ *
903
+ * @remarks
904
+ * A `node:http` {@link import('node:http').IncomingMessage}'s `url` is the request TARGET
905
+ * (`'/mcp?x=1'`), narrowed with `isString` (§14, never `as`) and defaulting to `'/'` for an
906
+ * absent target; it is parsed against a dummy base (only the pathname matters for the upgrade
907
+ * decision) and the `pathname` returned. The upgrade handler compares this against its
908
+ * configured `path` to decide whether to claim the socket. Total — never throws on an
909
+ * adversarial / absent target.
910
+ *
911
+ * @param request - The raw upgrade {@link import('node:http').IncomingMessage}
912
+ * @returns The request's path (the `pathname`, no query), or `'/'` when the target is absent
913
+ */
914
+ export declare function upgradeRequestPath(request: IncomingMessage): string;
915
+
916
+ /**
917
+ * The WebSocket CLIENT transport for the Model Context Protocol — a
918
+ * {@link ClientTransportInterface} that drives a REMOTE MCP server over a WebSocket, the
919
+ * egress mirror of {@link import('./factories.js').createWebSocketServer} and the WebSocket
920
+ * sibling of {@link import('./HTTPClientTransport.js').HTTPClientTransport}.
921
+ *
922
+ * @remarks
923
+ * - **Persistent bidirectional channel (unlike the HTTP transport).** `start()` performs the
924
+ * RFC 6455 client handshake: it opens a `node:http`(`s`) `GET` carrying `Connection: Upgrade`
925
+ * / `Upgrade: websocket` / a random `Sec-WebSocket-Key` / `Sec-WebSocket-Version: 13` /
926
+ * `Sec-WebSocket-Protocol: mcp` (plus any `options.headers`), awaits the client `'upgrade'`
927
+ * event, and VALIDATES `Sec-WebSocket-Accept === computeWebSocketAccept(key)` (the D2 helper)
928
+ * — a mismatch (or a non-`101` response, or a request error) REJECTS `start()` and the socket
929
+ * is destroyed. On success it wraps the raw upgraded socket in `createNodeWebSocket({ socket,
930
+ * head })` (CLIENT mode — no key → frames are MASKED per §5.3) and bridges its `message`.
931
+ * - **Inbound (`message`).** Each decoded text frame is `JSON.parse`d (guarded) and narrowed
932
+ * with `parseJSONRPCMessage` — a {@link JSONRPCMessage} re-emits on this transport's `message`
933
+ * event (the reply the {@link import('@src/core').MCPClientInterface} correlates by `id`); a
934
+ * non-JSON / non-message frame surfaces on `error` and is dropped (§14). The socket's `close`
935
+ * / `error` bridge to this transport's events.
936
+ * - **Outbound (`send`).** `send(message | messages)` writes ONE masked text frame per message.
937
+ * - **`close()`** closes the underlying socket and fires `close` (idempotent).
938
+ * - **URL scheme.** `options.url` accepts a `ws://` / `wss://` URL or an `http://` / `https://`
939
+ * one; a `ws(s)` scheme is converted to `http(s)` for the underlying upgrade request (`wss`
940
+ * → TLS via `node:https`). Either reaches the same endpoint.
941
+ * - **Observable (§13).** Owns the `emitter` ({@link ClientTransportEventMap}); every emit
942
+ * the emitter isolates a listener throw (a buggy observer never corrupts the transport);
943
+ * `error` is a DOMAIN event (a transport-level fault).
944
+ *
945
+ * @example
946
+ * ```ts
947
+ * const transport = new WebSocketClientTransport({ url: 'ws://localhost:3000/mcp' })
948
+ * const client = new MCPClient({ transport })
949
+ * await client.connect() // start() handshakes, then the MCP initialize runs over WS frames
950
+ * ```
951
+ */
952
+ export declare class WebSocketClientTransport implements ClientTransportInterface {
953
+ #private;
954
+ constructor(options: WebSocketClientTransportOptions);
955
+ get emitter(): EmitterInterface<ClientTransportEventMap_2>;
956
+ get session(): string | undefined;
957
+ start(): Promise<void>;
958
+ send(message: JSONRPCMessage_2 | readonly JSONRPCMessage_2[]): Promise<void>;
959
+ close(): Promise<void>;
960
+ }
961
+
962
+ /**
963
+ * Options for `createWebSocketClientTransport` — the remote MCP WebSocket endpoint and any
964
+ * extra handshake headers.
965
+ *
966
+ * @remarks
967
+ * - `url` — the absolute URL of the remote server's WebSocket endpoint. Accepts a `ws://` /
968
+ * `wss://` URL OR an `http://` / `https://` one (a `ws(s)` scheme is converted to `http(s)`
969
+ * for the underlying `node:http(s)` upgrade request; either reaches the same endpoint).
970
+ * REQUIRED.
971
+ * - `headers` — extra request headers merged onto the upgrade `GET` (e.g. an `Authorization`
972
+ * bearer for a guarded server). The transport always sets `Connection: Upgrade`,
973
+ * `Upgrade: websocket`, a random `Sec-WebSocket-Key`, `Sec-WebSocket-Version: 13`, and
974
+ * `Sec-WebSocket-Protocol: mcp`; a header supplied here is merged on top.
975
+ */
976
+ export declare interface WebSocketClientTransportOptions {
977
+ readonly url: string;
978
+ readonly headers?: Readonly<Record<string, string>>;
979
+ }
980
+
981
+ /**
982
+ * Options for `createWebSocketServer` — where the WebSocket upgrade is accepted and the
983
+ * subprotocol negotiated.
984
+ *
985
+ * @remarks
986
+ * - `path` — the request path the upgrade handler CLAIMS; defaults to
987
+ * {@link import('./constants.js').DEFAULT_MCP_PATH} (`'/mcp'`, the same path the HTTP
988
+ * transport mounts at). A protocol-upgrade request to any OTHER path is DECLINED
989
+ * (the handler returns `false`, so the spine fans it to the next handler or destroys it).
990
+ * - `subprotocol` — the WebSocket subprotocol echoed in the `101` handshake's
991
+ * `Sec-WebSocket-Protocol`; defaults to {@link import('./constants.js').MCP_WEBSOCKET_SUBPROTOCOL}
992
+ * (`'mcp'`). It is echoed unconditionally (the client requests it), so an MCP WebSocket
993
+ * endpoint is distinguishable from another WebSocket on the same path.
994
+ *
995
+ * Auth / origin policy is deliberately ABSENT: like the HTTP transport, the WebSocket
996
+ * transport is MECHANISM — compose a guard IN FRONT (a `Server.upgrade` handler registered
997
+ * before this one can decline an unauthenticated upgrade).
998
+ */
999
+ export declare interface WebSocketServerOptions {
1000
+ readonly path?: string;
1001
+ readonly subprotocol?: string;
1002
+ }
1003
+
1004
+ /**
1005
+ * The per-connection JSON-RPC-over-WebSocket SERVER bridge — wraps a
1006
+ * {@link NodeWebSocketInterface} (the RFC 6455 wire wrapper) as a
1007
+ * {@link ClientTransportInterface}, the bidirectional JSON-RPC message channel
1008
+ * `createWebSocketServer` pumps `mcp.dispatch` over and the egress mirror's
1009
+ * {@link import('./WebSocketClientTransport.js').WebSocketClientTransport} reuses.
1010
+ *
1011
+ * @remarks
1012
+ * - **Reuses `ClientTransportInterface` (§21).** It IS the same generic carrier the HTTP
1013
+ * client transport implements — `emitter` (`message` / `close` / `error`), `start`,
1014
+ * `send`, `close` — so the WebSocket server and client both speak ONE transport contract,
1015
+ * no near-duplicate sibling interface. `session` is `undefined` (the stateless v1; a
1016
+ * session id is the deferred sessions tier). The name keeps the role explicit even though
1017
+ * the shape is shared.
1018
+ * - **Inbound (`message`).** `start()` subscribes to the socket's `message` event; each text
1019
+ * frame is `JSON.parse`d inside a try/catch and narrowed with `parseJSONRPCMessage` — a
1020
+ * well-formed {@link JSONRPCMessage} is re-emitted on this transport's `message` event (the
1021
+ * parsed envelope the {@link import('@src/core').MCPServerInterface} pump dispatches), while
1022
+ * a non-JSON or non-message frame is surfaced on `error` and DROPPED, never thrown (§14). It
1023
+ * also bridges the socket's `close` → this transport's `close`, and the socket's `error`.
1024
+ * - **Outbound (`send`).** `send(message | messages)` writes ONE text frame per message
1025
+ * (`nodeWs.send(JSON.stringify(...))`); the underlying wrapper no-ops a write on a
1026
+ * non-open socket, so a closed connection drops silently rather than throwing.
1027
+ * - **`close()`** closes the underlying socket (the RFC 6455 close handshake) and fires the
1028
+ * transport's `close` event (idempotent — a second `close`, or a socket-driven close, emits
1029
+ * once).
1030
+ * - **Observable (§13).** Owns the `emitter` ({@link ClientTransportEventMap}); the emitter
1031
+ * isolates a listener throw (a buggy observer never corrupts the bridge). `error` is a
1032
+ * DOMAIN event (a transport-level fault), distinct from the emitter's listener-error channel.
1033
+ */
1034
+ export declare class WebSocketServerTransport implements ClientTransportInterface {
1035
+ #private;
1036
+ constructor(socket: NodeWebSocketInterface);
1037
+ get emitter(): EmitterInterface<ClientTransportEventMap_2>;
1038
+ get session(): string | undefined;
1039
+ start(): Promise<void>;
1040
+ send(message: JSONRPCMessage_2 | readonly JSONRPCMessage_2[]): Promise<void>;
1041
+ close(): Promise<void>;
1042
+ }
1043
+
1044
+ export { }