@orkestrel/mcp 0.0.34 → 0.0.36

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.
@@ -181,6 +181,8 @@ export declare function createMCPContinuation(secret: TokenSecret): MCPContinuat
181
181
  * handler through `MCPDispatchOptions.signal`. After every transport validation and immediately
182
182
  * before dispatch, the optional synchronous `caller` extractor reads front-middleware state; a
183
183
  * defined value is added to `MCPDispatchOptions`, while `undefined` is omitted.
184
+ * For legacy `initialize`, the dispatch response is recorded as `initialization` before
185
+ * JSON or SSE framing only when consumer state is an object with `'session' in context.state`.
184
186
  *
185
187
  * @typeParam TState - The consumer's opaque per-request route state type
186
188
  * @param mcp - The transport-agnostic MCP dispatcher to dispatch through
@@ -286,8 +288,12 @@ export declare function createMCPRoutes<TState = unknown>(mcp: MCPDispatcherInte
286
288
  * live-session request. It then
287
289
  * forwards a fresh `Request` carrying the buffered `text` (`next(forwarded)`) — never the
288
290
  * already-consumed original — so the route re-reads the same body, and stamps the response
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
291
+ * with {@link MCP_SESSION_HEADER}. A candidate entry is stored and advertised only when
292
+ * `context.state.initialization` carries a result. An `initialize` that would mint a session
293
+ * stores none and advertises none when refused; a live session's header is returned unchanged.
294
+ * The POST handler records that dispatch response before JSON or SSE framing.
295
+ * The entry's `touched` instant is read after the downstream response, because it
296
+ * means the last access: a request slower than `ttl` would
291
297
  * otherwise store a session that is already expired, and the write-back RE-ASKS the store, so
292
298
  * a `DELETE` arriving while the request was suspended is not undone.
293
299
  * - **`GET {path}`.** Resolves the session the same way (no mint — only `initialize` mints);
@@ -548,7 +554,7 @@ export declare const DEFAULT_MCP_SESSION_TTL = 300000;
548
554
  * Decodes and delivers each complete newline-framed line onto a {@link
549
555
  * MCPMessageTransportEventMap} emitter — the shared per-chunk dispatch step both stdio
550
556
  * transports run their framed lines through: the server transport frames with {@link
551
- * extractLines}, the client transport takes its lines from the process supervisor.
557
+ * extractLines}, the client transport frames the supervisor's stdout with Node's `readline`.
552
558
  *
553
559
  * @remarks
554
560
  * A blank line is skipped (a stray trailing newline). Every other line runs through the
@@ -855,6 +861,16 @@ export declare interface LineExtraction {
855
861
  readonly remainder: string;
856
862
  }
857
863
 
864
+ /**
865
+ * Sets the bound in milliseconds for a stdio server to exit after the client ends its input.
866
+ *
867
+ * @remarks
868
+ * Gives EOF cleanup half the process supervisor's 5,000 ms signal grace before escalation gets
869
+ * its existing full window. This bounds the MCP lifecycle's reasonable-time wait without spending
870
+ * another full signal grace before termination. Pending input flushes share this bound.
871
+ */
872
+ export declare const MCP_STDIO_GRACE: number;
873
+
858
874
  /**
859
875
  * Extracts consumer-asserted caller context synchronously from an HTTP request after the
860
876
  * transport has validated it for dispatch.
@@ -1130,13 +1146,16 @@ export declare interface MCPSessionOptions {
1130
1146
  * a server-initiated message onto the session's resumable stream.
1131
1147
  *
1132
1148
  * @remarks
1133
- * `session` is set on `initialize` (the minted session) and on every validated
1149
+ * `session` is set on `initialize` (the candidate or resolved live session) and on every validated
1134
1150
  * non-`initialize` `POST` (the resolved one); absent when the request never
1135
1151
  * reached a resolved session (the middleware short-circuits those as a `404`
1136
- * before calling `next`).
1152
+ * before calling `next`). A refused candidate remains in request state but is never
1153
+ * stored or advertised. A live session's response header is returned unchanged.
1137
1154
  */
1138
1155
  export declare interface MCPSessionState {
1139
1156
  readonly session?: MCPSessionInterface;
1157
+ /** Holds the initialization dispatch response before HTTP framing, used to admit a session. */
1158
+ readonly initialization?: JSONRPCResponse;
1140
1159
  }
1141
1160
 
1142
1161
  /**
@@ -1240,32 +1259,37 @@ export declare const SSE_KEEPALIVE_COMMENT = "keepalive";
1240
1259
  *
1241
1260
  * @remarks
1242
1261
  * - **Composes `@orkestrel/process`.** `start()` builds one supervised
1243
- * {@link import('@orkestrel/process/server').Process} with `writable: true`, so the child's
1262
+ * {@link import('@orkestrel/process/server').Supervisor} with `writable: true`, so the child's
1244
1263
  * `stdin`/`stdout` are the JSON-RPC channel and its `stderr` is retained as bounded evidence
1245
- * rather than parsed as protocol. The supervisor owns spawn, framing, and termination.
1246
- * - **Inbound (`message`).** Standard output is drained eagerly through the supervisor's
1247
- * `readline`-framed `lines` iterable, so a multi-byte UTF-8 sequence split across two reads is
1264
+ * rather than parsed as protocol. The supervisor owns spawn and termination.
1265
+ * - **Inbound (`message`).** Standard output is drained eagerly through Node's `readline`,
1266
+ * so a multi-byte UTF-8 sequence split across two reads is
1248
1267
  * decoded whole and a final line written without a trailing newline still arrives. Each framed
1249
1268
  * line is decoded and delivered through the shared {@link dispatchLines} helper — a well-formed
1250
1269
  * {@link JSONRPCMessage} emits `message`, a malformed line emits `error` (never throws).
1251
1270
  * - **Outbound (`send`).** `send(message)` writes one newline-terminated `JSON.stringify`d line
1252
- * through the supervisor's `send` and awaits its answer, so this promise settles only after the
1271
+ * through the supervisor's `deliver` and awaits its answer, so this promise settles only after the
1253
1272
  * host reports the line handled rather than the moment the write is queued. The supervisor never
1254
1273
  * rejects — it answers `false` for a channel that was closed, destroyed, or ended, for a write
1255
1274
  * that failed, or for one that remained unconfirmed through `delivery`. A call made without a
1256
1275
  * live child rejects as not connected; a `false` answer from a live child rejects as unable to
1257
1276
  * deliver. The supervisor does not disclose which cause produced that answer.
1258
- * - **`close()`** runs the supervisor's bounded termination and teardown, then fires `close` once
1259
- * (idempotent). That teardown reaches the child's terminal moment, where the supervisor freezes
1260
- * `evidence`, ends `lines`, and settles `exit` together, so this transport needs no release of
1261
- * its own to get its line pump back: the stream ends under the pump rather than throwing at it.
1262
- * A line the supervisor had already framed behind the one being delivered is dropped rather than
1277
+ * - **`close()`** ends the child's input and waits up to {@link MCP_STDIO_GRACE} for native exit,
1278
+ * including any pending input flush. Only after that wait does the supervisor terminate a child
1279
+ * that remains alive. Teardown freezes `evidence`, closes the reader, and settles `exit`, then
1280
+ * fires `close` once (idempotent).
1281
+ * A child that exits on input end within the grace receives no termination signal, so it must
1282
+ * end its own child processes on input end; only escalation reaches its process tree.
1283
+ * If an `MCPClient` request `timeout` is shorter than the grace and the close outlasts that
1284
+ * timeout, `disconnect()` rejects with `MCP transport close timed out after <timeout>ms`.
1285
+ * The close keeps running, and a later caller joins it while it remains pending.
1286
+ * A line already framed behind the one being delivered is dropped rather than
1263
1287
  * emitted onto a transport whose teardown has begun. A `close()` issued while that teardown runs
1264
1288
  * joins it rather than opening a second one, so it resolves only after `close` has fired, and a
1265
1289
  * `start()` issued while it runs waits behind the same barrier, so lifetimes never overlap. A
1266
1290
  * descendant can retain an inherited stdout pipe after the child exits; the supervisor's `drain`
1267
- * bound cuts that wait off, so this transport's `close()` settles within that bound rather than
1268
- * on the descendant. The termination itself belongs to the host: a POSIX host signals the
1291
+ * bound cuts that wait off independently of the input grace. Escalation belongs to the host:
1292
+ * a POSIX host signals the
1269
1293
  * child's own process group `SIGTERM`, waits the grace window, then `SIGKILL`s through the same
1270
1294
  * route, so the kill reaches grandchildren rather than orphaning them, while Windows ends the
1271
1295
  * tree with `taskkill /F /T`, which nothing in the child can intercept.
@@ -1324,6 +1348,14 @@ export declare class StdioClientTransport implements StdioClientTransportInterfa
1324
1348
  * of them answers `undefined` to forever is a stdio detail rather than a shared contract. A
1325
1349
  * consumer that widens this value back to {@link MCPMessageTransportInterface}, including by
1326
1350
  * reading `client.transport`, loses the reader and must keep the original reference.
1351
+ * Closing ends the child's input, waits up to {@link import('./constants.js').MCP_STDIO_GRACE}
1352
+ * for native exit, and then uses the supervisor's bounded termination if the child remains alive.
1353
+ * The input flush shares that bound; the supervisor's stream-drain bound follows native exit.
1354
+ * A child that exits on input end within the grace receives no termination signal and must end
1355
+ * its own child processes on input end; only escalation reaches its process tree. If an
1356
+ * `MCPClient` request `timeout` is shorter than the grace and the close outlasts that timeout,
1357
+ * `disconnect()` rejects with `MCP transport close timed out after <timeout>ms`. The close keeps
1358
+ * running, and a later caller joins it while it remains pending.
1327
1359
  */
1328
1360
  export declare interface StdioClientTransportInterface extends MCPMessageTransportInterface {
1329
1361
  /**
@@ -1357,7 +1389,8 @@ export declare interface StdioClientTransportInterface extends MCPMessageTranspo
1357
1389
  * would have read.
1358
1390
  * - **What the close path carries.** The frozen value is what the supervisor had received by
1359
1391
  * that terminal moment, not the child's complete output.
1360
- * Windows ends the tree with `taskkill /F /T`, which nothing in the child can intercept: a
1392
+ * After the input grace expires, Windows ends the tree with `taskkill /F /T`, which nothing
1393
+ * in the child can intercept: a
1361
1394
  * `SIGTERM` handler never runs there, so the bytes it would have written never exist. A
1362
1395
  * child that ends on its own closes its stderr first, and that tail is complete.
1363
1396
  * Where that moment arrived at the supervisor's `drain` bound rather than at the child's
@@ -181,6 +181,8 @@ export declare function createMCPContinuation(secret: TokenSecret): MCPContinuat
181
181
  * handler through `MCPDispatchOptions.signal`. After every transport validation and immediately
182
182
  * before dispatch, the optional synchronous `caller` extractor reads front-middleware state; a
183
183
  * defined value is added to `MCPDispatchOptions`, while `undefined` is omitted.
184
+ * For legacy `initialize`, the dispatch response is recorded as `initialization` before
185
+ * JSON or SSE framing only when consumer state is an object with `'session' in context.state`.
184
186
  *
185
187
  * @typeParam TState - The consumer's opaque per-request route state type
186
188
  * @param mcp - The transport-agnostic MCP dispatcher to dispatch through
@@ -286,8 +288,12 @@ export declare function createMCPRoutes<TState = unknown>(mcp: MCPDispatcherInte
286
288
  * live-session request. It then
287
289
  * forwards a fresh `Request` carrying the buffered `text` (`next(forwarded)`) — never the
288
290
  * already-consumed original — so the route re-reads the same body, and stamps the response
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
291
+ * with {@link MCP_SESSION_HEADER}. A candidate entry is stored and advertised only when
292
+ * `context.state.initialization` carries a result. An `initialize` that would mint a session
293
+ * stores none and advertises none when refused; a live session's header is returned unchanged.
294
+ * The POST handler records that dispatch response before JSON or SSE framing.
295
+ * The entry's `touched` instant is read after the downstream response, because it
296
+ * means the last access: a request slower than `ttl` would
291
297
  * otherwise store a session that is already expired, and the write-back RE-ASKS the store, so
292
298
  * a `DELETE` arriving while the request was suspended is not undone.
293
299
  * - **`GET {path}`.** Resolves the session the same way (no mint — only `initialize` mints);
@@ -548,7 +554,7 @@ export declare const DEFAULT_MCP_SESSION_TTL = 300000;
548
554
  * Decodes and delivers each complete newline-framed line onto a {@link
549
555
  * MCPMessageTransportEventMap} emitter — the shared per-chunk dispatch step both stdio
550
556
  * transports run their framed lines through: the server transport frames with {@link
551
- * extractLines}, the client transport takes its lines from the process supervisor.
557
+ * extractLines}, the client transport frames the supervisor's stdout with Node's `readline`.
552
558
  *
553
559
  * @remarks
554
560
  * A blank line is skipped (a stray trailing newline). Every other line runs through the
@@ -855,6 +861,16 @@ export declare interface LineExtraction {
855
861
  readonly remainder: string;
856
862
  }
857
863
 
864
+ /**
865
+ * Sets the bound in milliseconds for a stdio server to exit after the client ends its input.
866
+ *
867
+ * @remarks
868
+ * Gives EOF cleanup half the process supervisor's 5,000 ms signal grace before escalation gets
869
+ * its existing full window. This bounds the MCP lifecycle's reasonable-time wait without spending
870
+ * another full signal grace before termination. Pending input flushes share this bound.
871
+ */
872
+ export declare const MCP_STDIO_GRACE: number;
873
+
858
874
  /**
859
875
  * Extracts consumer-asserted caller context synchronously from an HTTP request after the
860
876
  * transport has validated it for dispatch.
@@ -1130,13 +1146,16 @@ export declare interface MCPSessionOptions {
1130
1146
  * a server-initiated message onto the session's resumable stream.
1131
1147
  *
1132
1148
  * @remarks
1133
- * `session` is set on `initialize` (the minted session) and on every validated
1149
+ * `session` is set on `initialize` (the candidate or resolved live session) and on every validated
1134
1150
  * non-`initialize` `POST` (the resolved one); absent when the request never
1135
1151
  * reached a resolved session (the middleware short-circuits those as a `404`
1136
- * before calling `next`).
1152
+ * before calling `next`). A refused candidate remains in request state but is never
1153
+ * stored or advertised. A live session's response header is returned unchanged.
1137
1154
  */
1138
1155
  export declare interface MCPSessionState {
1139
1156
  readonly session?: MCPSessionInterface;
1157
+ /** Holds the initialization dispatch response before HTTP framing, used to admit a session. */
1158
+ readonly initialization?: JSONRPCResponse;
1140
1159
  }
1141
1160
 
1142
1161
  /**
@@ -1240,32 +1259,37 @@ export declare const SSE_KEEPALIVE_COMMENT = "keepalive";
1240
1259
  *
1241
1260
  * @remarks
1242
1261
  * - **Composes `@orkestrel/process`.** `start()` builds one supervised
1243
- * {@link import('@orkestrel/process/server').Process} with `writable: true`, so the child's
1262
+ * {@link import('@orkestrel/process/server').Supervisor} with `writable: true`, so the child's
1244
1263
  * `stdin`/`stdout` are the JSON-RPC channel and its `stderr` is retained as bounded evidence
1245
- * rather than parsed as protocol. The supervisor owns spawn, framing, and termination.
1246
- * - **Inbound (`message`).** Standard output is drained eagerly through the supervisor's
1247
- * `readline`-framed `lines` iterable, so a multi-byte UTF-8 sequence split across two reads is
1264
+ * rather than parsed as protocol. The supervisor owns spawn and termination.
1265
+ * - **Inbound (`message`).** Standard output is drained eagerly through Node's `readline`,
1266
+ * so a multi-byte UTF-8 sequence split across two reads is
1248
1267
  * decoded whole and a final line written without a trailing newline still arrives. Each framed
1249
1268
  * line is decoded and delivered through the shared {@link dispatchLines} helper — a well-formed
1250
1269
  * {@link JSONRPCMessage} emits `message`, a malformed line emits `error` (never throws).
1251
1270
  * - **Outbound (`send`).** `send(message)` writes one newline-terminated `JSON.stringify`d line
1252
- * through the supervisor's `send` and awaits its answer, so this promise settles only after the
1271
+ * through the supervisor's `deliver` and awaits its answer, so this promise settles only after the
1253
1272
  * host reports the line handled rather than the moment the write is queued. The supervisor never
1254
1273
  * rejects — it answers `false` for a channel that was closed, destroyed, or ended, for a write
1255
1274
  * that failed, or for one that remained unconfirmed through `delivery`. A call made without a
1256
1275
  * live child rejects as not connected; a `false` answer from a live child rejects as unable to
1257
1276
  * deliver. The supervisor does not disclose which cause produced that answer.
1258
- * - **`close()`** runs the supervisor's bounded termination and teardown, then fires `close` once
1259
- * (idempotent). That teardown reaches the child's terminal moment, where the supervisor freezes
1260
- * `evidence`, ends `lines`, and settles `exit` together, so this transport needs no release of
1261
- * its own to get its line pump back: the stream ends under the pump rather than throwing at it.
1262
- * A line the supervisor had already framed behind the one being delivered is dropped rather than
1277
+ * - **`close()`** ends the child's input and waits up to {@link MCP_STDIO_GRACE} for native exit,
1278
+ * including any pending input flush. Only after that wait does the supervisor terminate a child
1279
+ * that remains alive. Teardown freezes `evidence`, closes the reader, and settles `exit`, then
1280
+ * fires `close` once (idempotent).
1281
+ * A child that exits on input end within the grace receives no termination signal, so it must
1282
+ * end its own child processes on input end; only escalation reaches its process tree.
1283
+ * If an `MCPClient` request `timeout` is shorter than the grace and the close outlasts that
1284
+ * timeout, `disconnect()` rejects with `MCP transport close timed out after <timeout>ms`.
1285
+ * The close keeps running, and a later caller joins it while it remains pending.
1286
+ * A line already framed behind the one being delivered is dropped rather than
1263
1287
  * emitted onto a transport whose teardown has begun. A `close()` issued while that teardown runs
1264
1288
  * joins it rather than opening a second one, so it resolves only after `close` has fired, and a
1265
1289
  * `start()` issued while it runs waits behind the same barrier, so lifetimes never overlap. A
1266
1290
  * descendant can retain an inherited stdout pipe after the child exits; the supervisor's `drain`
1267
- * bound cuts that wait off, so this transport's `close()` settles within that bound rather than
1268
- * on the descendant. The termination itself belongs to the host: a POSIX host signals the
1291
+ * bound cuts that wait off independently of the input grace. Escalation belongs to the host:
1292
+ * a POSIX host signals the
1269
1293
  * child's own process group `SIGTERM`, waits the grace window, then `SIGKILL`s through the same
1270
1294
  * route, so the kill reaches grandchildren rather than orphaning them, while Windows ends the
1271
1295
  * tree with `taskkill /F /T`, which nothing in the child can intercept.
@@ -1324,6 +1348,14 @@ export declare class StdioClientTransport implements StdioClientTransportInterfa
1324
1348
  * of them answers `undefined` to forever is a stdio detail rather than a shared contract. A
1325
1349
  * consumer that widens this value back to {@link MCPMessageTransportInterface}, including by
1326
1350
  * reading `client.transport`, loses the reader and must keep the original reference.
1351
+ * Closing ends the child's input, waits up to {@link import('./constants.js').MCP_STDIO_GRACE}
1352
+ * for native exit, and then uses the supervisor's bounded termination if the child remains alive.
1353
+ * The input flush shares that bound; the supervisor's stream-drain bound follows native exit.
1354
+ * A child that exits on input end within the grace receives no termination signal and must end
1355
+ * its own child processes on input end; only escalation reaches its process tree. If an
1356
+ * `MCPClient` request `timeout` is shorter than the grace and the close outlasts that timeout,
1357
+ * `disconnect()` rejects with `MCP transport close timed out after <timeout>ms`. The close keeps
1358
+ * running, and a later caller joins it while it remains pending.
1327
1359
  */
1328
1360
  export declare interface StdioClientTransportInterface extends MCPMessageTransportInterface {
1329
1361
  /**
@@ -1357,7 +1389,8 @@ export declare interface StdioClientTransportInterface extends MCPMessageTranspo
1357
1389
  * would have read.
1358
1390
  * - **What the close path carries.** The frozen value is what the supervisor had received by
1359
1391
  * that terminal moment, not the child's complete output.
1360
- * Windows ends the tree with `taskkill /F /T`, which nothing in the child can intercept: a
1392
+ * After the input grace expires, Windows ends the tree with `taskkill /F /T`, which nothing
1393
+ * in the child can intercept: a
1361
1394
  * `SIGTERM` handler never runs there, so the bytes it would have written never exist. A
1362
1395
  * child that ends on its own closes its stderr first, and that tail is complete.
1363
1396
  * Where that moment arrived at the supervisor's `drain` bound rather than at the child's
@@ -1,15 +1,25 @@
1
+ import { PROCESS_GRACE } from "@orkestrel/process";
1
2
  import { HTTPClientTransport, JSONRPC_INVALID_PARAMS, JSONRPC_INVALID_REQUEST, JSONRPC_METHOD_NOT_FOUND, JSONRPC_PARSE_ERROR, MCP_HANDSHAKE_VERSION, MCP_HEADER_MISMATCH, MCP_LOOKUP_PAGES, MCP_META_VERSION, MCP_METHOD_HEADER, MCP_MISSING_CAPABILITY, MCP_NAME_HEADER, MCP_PARAM_PREFIX, MCP_PROTOCOL_VERSION_HEADER, MCP_SESSION_HEADER, MCP_UNSUPPORTED_VERSION, MCP_WEBSOCKET_SUBPROTOCOL, SUPPORTED_LEGACY_PROTOCOL_VERSIONS, bindServer, buildHeaderParameters, buildJSONRPCError, decodeEvent, decodeSentinel, deliverMessage, extractToolSchema, inferRequestEra, isInitializeRequest, isJSONRPCInvocation, isMCPLegacyVersion, isMCPModernVersion, isModernRequest, parseJSONRPCMessage, parseRequestContext, renderHeaderValue } from "../core/index.js";
2
- import { isError, isRecord, isString, parseJSON, sanitizeBudget } from "@orkestrel/contract";
3
+ import { isError, isObject, isRecord, isString, parseJSON, sanitizeBudget } from "@orkestrel/contract";
3
4
  import { createStream, signToken, verifyToken } from "@orkestrel/server";
4
5
  import { Emitter } from "@orkestrel/emitter";
5
6
  import { WEBSOCKET_READY_OPEN, WEBSOCKET_VERSION, computeWebSocketAccept, createNodeWebSocket } from "@orkestrel/websocket";
6
7
  import { randomBytes } from "node:crypto";
7
8
  import { request } from "node:http";
8
9
  import { request as request$1 } from "node:https";
9
- import { Process } from "@orkestrel/process/server";
10
- import { PROCESS_GRACE } from "@orkestrel/process";
10
+ import { createInterface } from "node:readline";
11
+ import { Supervisor } from "@orkestrel/process/server";
11
12
  import { Readable } from "node:stream";
12
13
  //#region src/server/constants.ts
14
+ /**
15
+ * Sets the bound in milliseconds for a stdio server to exit after the client ends its input.
16
+ *
17
+ * @remarks
18
+ * Gives EOF cleanup half the process supervisor's 5,000 ms signal grace before escalation gets
19
+ * its existing full window. This bounds the MCP lifecycle's reasonable-time wait without spending
20
+ * another full signal grace before termination. Pending input flushes share this bound.
21
+ */
22
+ var MCP_STDIO_GRACE = PROCESS_GRACE / 2;
13
23
  /** Names the reverse-proxy response header controlling buffering of an SSE response. */
14
24
  var SSE_BUFFERING_HEADER = "x-accel-buffering";
15
25
  /** Names the `X-Accel-Buffering` value that disables reverse-proxy buffering. */
@@ -297,7 +307,7 @@ function writeLine(output, line) {
297
307
  * Decodes and delivers each complete newline-framed line onto a {@link
298
308
  * MCPMessageTransportEventMap} emitter — the shared per-chunk dispatch step both stdio
299
309
  * transports run their framed lines through: the server transport frames with {@link
300
- * extractLines}, the client transport takes its lines from the process supervisor.
310
+ * extractLines}, the client transport frames the supervisor's stdout with Node's `readline`.
301
311
  *
302
312
  * @remarks
303
313
  * A blank line is skipped (a stray trailing newline). Every other line runs through the
@@ -719,6 +729,8 @@ var HTTPDisconnect = class {
719
729
  * handler through `MCPDispatchOptions.signal`. After every transport validation and immediately
720
730
  * before dispatch, the optional synchronous `caller` extractor reads front-middleware state; a
721
731
  * defined value is added to `MCPDispatchOptions`, while `undefined` is omitted.
732
+ * For legacy `initialize`, the dispatch response is recorded as `initialization` before
733
+ * JSON or SSE framing only when consumer state is an object with `'session' in context.state`.
722
734
  *
723
735
  * @typeParam TState - The consumer's opaque per-request route state type
724
736
  * @param mcp - The transport-agnostic MCP dispatcher to dispatch through
@@ -811,6 +823,9 @@ function createMCPPostHandler(mcp, options) {
811
823
  queueMicrotask(() => void sendEventStream(response, stream));
812
824
  return disconnect.bridge(stream);
813
825
  }
826
+ if (era === "legacy" && invocation.method === "initialize" && isObject(context?.state) && "session" in context.state) {
827
+ if (!Reflect.set(context.state, "initialization", response)) throw new Error("MCP initialization state is not writable");
828
+ }
814
829
  const status = inferStatus(response, era);
815
830
  if (response === void 0) return new Response(null, { status });
816
831
  if (status === 200 && streaming && acceptsEventStream(request)) {
@@ -1228,32 +1243,37 @@ var WebSocketClientTransport = class {
1228
1243
  *
1229
1244
  * @remarks
1230
1245
  * - **Composes `@orkestrel/process`.** `start()` builds one supervised
1231
- * {@link import('@orkestrel/process/server').Process} with `writable: true`, so the child's
1246
+ * {@link import('@orkestrel/process/server').Supervisor} with `writable: true`, so the child's
1232
1247
  * `stdin`/`stdout` are the JSON-RPC channel and its `stderr` is retained as bounded evidence
1233
- * rather than parsed as protocol. The supervisor owns spawn, framing, and termination.
1234
- * - **Inbound (`message`).** Standard output is drained eagerly through the supervisor's
1235
- * `readline`-framed `lines` iterable, so a multi-byte UTF-8 sequence split across two reads is
1248
+ * rather than parsed as protocol. The supervisor owns spawn and termination.
1249
+ * - **Inbound (`message`).** Standard output is drained eagerly through Node's `readline`,
1250
+ * so a multi-byte UTF-8 sequence split across two reads is
1236
1251
  * decoded whole and a final line written without a trailing newline still arrives. Each framed
1237
1252
  * line is decoded and delivered through the shared {@link dispatchLines} helper — a well-formed
1238
1253
  * {@link JSONRPCMessage} emits `message`, a malformed line emits `error` (never throws).
1239
1254
  * - **Outbound (`send`).** `send(message)` writes one newline-terminated `JSON.stringify`d line
1240
- * through the supervisor's `send` and awaits its answer, so this promise settles only after the
1255
+ * through the supervisor's `deliver` and awaits its answer, so this promise settles only after the
1241
1256
  * host reports the line handled rather than the moment the write is queued. The supervisor never
1242
1257
  * rejects — it answers `false` for a channel that was closed, destroyed, or ended, for a write
1243
1258
  * that failed, or for one that remained unconfirmed through `delivery`. A call made without a
1244
1259
  * live child rejects as not connected; a `false` answer from a live child rejects as unable to
1245
1260
  * deliver. The supervisor does not disclose which cause produced that answer.
1246
- * - **`close()`** runs the supervisor's bounded termination and teardown, then fires `close` once
1247
- * (idempotent). That teardown reaches the child's terminal moment, where the supervisor freezes
1248
- * `evidence`, ends `lines`, and settles `exit` together, so this transport needs no release of
1249
- * its own to get its line pump back: the stream ends under the pump rather than throwing at it.
1250
- * A line the supervisor had already framed behind the one being delivered is dropped rather than
1261
+ * - **`close()`** ends the child's input and waits up to {@link MCP_STDIO_GRACE} for native exit,
1262
+ * including any pending input flush. Only after that wait does the supervisor terminate a child
1263
+ * that remains alive. Teardown freezes `evidence`, closes the reader, and settles `exit`, then
1264
+ * fires `close` once (idempotent).
1265
+ * A child that exits on input end within the grace receives no termination signal, so it must
1266
+ * end its own child processes on input end; only escalation reaches its process tree.
1267
+ * If an `MCPClient` request `timeout` is shorter than the grace and the close outlasts that
1268
+ * timeout, `disconnect()` rejects with `MCP transport close timed out after <timeout>ms`.
1269
+ * The close keeps running, and a later caller joins it while it remains pending.
1270
+ * A line already framed behind the one being delivered is dropped rather than
1251
1271
  * emitted onto a transport whose teardown has begun. A `close()` issued while that teardown runs
1252
1272
  * joins it rather than opening a second one, so it resolves only after `close` has fired, and a
1253
1273
  * `start()` issued while it runs waits behind the same barrier, so lifetimes never overlap. A
1254
1274
  * descendant can retain an inherited stdout pipe after the child exits; the supervisor's `drain`
1255
- * bound cuts that wait off, so this transport's `close()` settles within that bound rather than
1256
- * on the descendant. The termination itself belongs to the host: a POSIX host signals the
1275
+ * bound cuts that wait off independently of the input grace. Escalation belongs to the host:
1276
+ * a POSIX host signals the
1257
1277
  * child's own process group `SIGTERM`, waits the grace window, then `SIGKILL`s through the same
1258
1278
  * route, so the kill reaches grandchildren rather than orphaning them, while Windows ends the
1259
1279
  * tree with `taskkill /F /T`, which nothing in the child can intercept.
@@ -1315,7 +1335,7 @@ var StdioClientTransport = class {
1315
1335
  }
1316
1336
  if (this.#process !== void 0 && !this.#closed) return;
1317
1337
  this.#closed = false;
1318
- const child = new Process({
1338
+ const child = new Supervisor({
1319
1339
  command: {
1320
1340
  file: this.#command,
1321
1341
  arguments: [...this.#args],
@@ -1325,11 +1345,23 @@ var StdioClientTransport = class {
1325
1345
  grace: PROCESS_GRACE,
1326
1346
  delivery: this.#delivery,
1327
1347
  writable: true
1348
+ }, {
1349
+ chunk: () => void 0,
1350
+ fault: (cause) => this.#emitter.emit("error", cause),
1351
+ close: () => reader.close(),
1352
+ terminal: () => void 0,
1353
+ teardown: () => reader.close()
1354
+ });
1355
+ const reader = createInterface({
1356
+ input: child.stdout,
1357
+ crlfDelay: Infinity
1328
1358
  });
1329
1359
  this.#process = child;
1330
- child.emitter.on("error", (cause) => this.#emitter.emit("error", cause));
1360
+ reader.on("line", (line) => {
1361
+ if (this.#closed || this.#process !== child) return;
1362
+ dispatchLines(this.#emitter, [line]);
1363
+ });
1331
1364
  child.exit.then((exit) => this.#onExit(child, exit));
1332
- this.#pump(child);
1333
1365
  }
1334
1366
  /**
1335
1367
  * Sends one newline-delimited JSON-RPC message to the live child.
@@ -1344,7 +1376,7 @@ var StdioClientTransport = class {
1344
1376
  async send(message) {
1345
1377
  const child = this.#closed ? void 0 : this.#process;
1346
1378
  if (child === void 0) throw new Error("stdio transport is not connected");
1347
- if (!await child.send(JSON.stringify(message))) throw new Error("stdio transport could not deliver the message");
1379
+ if (!await child.deliver(Buffer.from(`${JSON.stringify(message)}\n`, "utf8"))) throw new Error("stdio transport could not deliver the message");
1348
1380
  }
1349
1381
  async close() {
1350
1382
  if (this.#closed && this.#closing === void 0) return;
@@ -1356,17 +1388,19 @@ var StdioClientTransport = class {
1356
1388
  this.#closed = true;
1357
1389
  const child = this.#process;
1358
1390
  if (child !== void 0) {
1391
+ const expired = Promise.withResolvers();
1392
+ const timer = setTimeout(expired.resolve, MCP_STDIO_GRACE);
1393
+ try {
1394
+ child.end();
1395
+ await Promise.race([child.ending, expired.promise]);
1396
+ } finally {
1397
+ clearTimeout(timer);
1398
+ }
1359
1399
  await child.destroy();
1360
1400
  this.#report(await child.exit);
1361
1401
  }
1362
1402
  this.#emitter.emit("close");
1363
1403
  }
1364
- async #pump(child) {
1365
- for await (const line of child.lines) {
1366
- if (this.#closed || this.#process !== child) return;
1367
- dispatchLines(this.#emitter, [line]);
1368
- }
1369
- }
1370
1404
  #onExit(child, exit) {
1371
1405
  if (this.#process !== child) return;
1372
1406
  if (this.#closed) return;
@@ -1950,8 +1984,12 @@ function createStdioServer(mcp, options) {
1950
1984
  * live-session request. It then
1951
1985
  * forwards a fresh `Request` carrying the buffered `text` (`next(forwarded)`) — never the
1952
1986
  * already-consumed original — so the route re-reads the same body, and stamps the response
1953
- * with {@link MCP_SESSION_HEADER}. The entry's `touched` instant is read after that
1954
- * downstream response, because it means the last access: a request slower than `ttl` would
1987
+ * with {@link MCP_SESSION_HEADER}. A candidate entry is stored and advertised only when
1988
+ * `context.state.initialization` carries a result. An `initialize` that would mint a session
1989
+ * stores none and advertises none when refused; a live session's header is returned unchanged.
1990
+ * The POST handler records that dispatch response before JSON or SSE framing.
1991
+ * The entry's `touched` instant is read after the downstream response, because it
1992
+ * means the last access: a request slower than `ttl` would
1955
1993
  * otherwise store a session that is already expired, and the write-back RE-ASKS the store, so
1956
1994
  * a `DELETE` arriving while the request was suspended is not undone.
1957
1995
  * - **`GET {path}`.** Resolves the session the same way (no mint — only `initialize` mints);
@@ -2088,7 +2126,7 @@ function createMCPSession(options) {
2088
2126
  signal: request.signal
2089
2127
  }));
2090
2128
  if (created !== void 0) {
2091
- if (!response.ok) return response;
2129
+ if (!response.ok || context.state.initialization?.result === void 0) return response;
2092
2130
  store.set(created.session.id, {
2093
2131
  ...created,
2094
2132
  touched: clock()
@@ -2102,6 +2140,6 @@ function createMCPSession(options) {
2102
2140
  };
2103
2141
  }
2104
2142
  //#endregion
2105
- export { DEFAULT_MCP_DELIVERY, DEFAULT_MCP_KEEPALIVE_INTERVAL, DEFAULT_MCP_PATH, DEFAULT_MCP_SESSION_CAPACITY, DEFAULT_MCP_SESSION_TTL, HTTPDisconnect, MCPSession, SSE_BUFFERING_DISABLED, SSE_BUFFERING_HEADER, SSE_KEEPALIVE_COMMENT, StdioClientTransport, StdioServerTransport, WebSocketClientTransport, WebSocketServerTransport, acceptsEventStream, allowsOrigin, createDuplexServerTransport, createHTTPClientTransport, createMCPContinuation, createMCPPostHandler, createMCPRoutes, createMCPSession, createStdioClientTransport, createStdioServer, createWebSocketClientTransport, createWebSocketServer, dispatchLines, extractLines, inferHeaderIssue, inferHeaderTarget, inferLegacyVersion, inferParameterRefusal, inferSessionHeaderIssue, inferStatus, readLastEventId, readSessionHeader, rejectUnknownSession, sendEventStream, upgradeRequestPath, writeLine };
2143
+ export { DEFAULT_MCP_DELIVERY, DEFAULT_MCP_KEEPALIVE_INTERVAL, DEFAULT_MCP_PATH, DEFAULT_MCP_SESSION_CAPACITY, DEFAULT_MCP_SESSION_TTL, HTTPDisconnect, MCPSession, MCP_STDIO_GRACE, SSE_BUFFERING_DISABLED, SSE_BUFFERING_HEADER, SSE_KEEPALIVE_COMMENT, StdioClientTransport, StdioServerTransport, WebSocketClientTransport, WebSocketServerTransport, acceptsEventStream, allowsOrigin, createDuplexServerTransport, createHTTPClientTransport, createMCPContinuation, createMCPPostHandler, createMCPRoutes, createMCPSession, createStdioClientTransport, createStdioServer, createWebSocketClientTransport, createWebSocketServer, dispatchLines, extractLines, inferHeaderIssue, inferHeaderTarget, inferLegacyVersion, inferParameterRefusal, inferSessionHeaderIssue, inferStatus, readLastEventId, readSessionHeader, rejectUnknownSession, sendEventStream, upgradeRequestPath, writeLine };
2106
2144
 
2107
2145
  //# sourceMappingURL=index.js.map