@orkestrel/mcp 0.0.20 → 0.0.21

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.
@@ -324,22 +324,24 @@ export declare function createReadableStream<T>(pull: (controller: ReadableStrea
324
324
 
325
325
  /**
326
326
  * Creates the stdio CLIENT transport for an {@link import('@orkestrel/mcp').MCPClientInterface}
327
- * — a {@link MCPClientTransportInterface} that spawns and drives a CHILD PROCESS MCP server
327
+ * — a {@link StdioClientTransportInterface} that spawns and drives a CHILD PROCESS MCP server
328
328
  * over newline-delimited JSON-RPC on `stdin`/`stdout`, the stdio sibling of {@link
329
329
  * createHTTPClientTransport} and {@link createWebSocketClientTransport}.
330
330
  *
331
331
  * @remarks
332
332
  * Hand it to `createMCPClient({ transport })`: `start()` (run by `client.connect()`)
333
333
  * spawns `options.command` with `options.args` and `options.env`, piping its
334
- * `stdin`/`stdout` for the JSON-RPC channel (its `stderr` inherits the parent's for
335
- * diagnostics). Each JSON-RPC message the client `send`s is written as one
334
+ * `stdin`/`stdout` for the JSON-RPC channel. The child's `stderr` is piped too, and
335
+ * retained as a bounded tail this transport reports as `evidence` the parent never
336
+ * inherits it. Each JSON-RPC message the client `send`s is written as one
336
337
  * newline-terminated line to the child's `stdin`; each decoded reply line from the
337
338
  * child's `stdout` is surfaced on the transport's `message` event for the client's
338
339
  * id correlation.
339
340
  *
340
341
  * @param options - `command` (the executable to spawn; REQUIRED), optional `args`,
341
342
  * and optional `env`; see {@link StdioClientTransportOptions}
342
- * @returns A working {@link MCPClientTransportInterface} over a child process's stdio
343
+ * @returns A working {@link StdioClientTransportInterface} over a child process's stdio,
344
+ * whose `evidence` carries the supervised child's bounded stderr tail
343
345
  *
344
346
  * @example
345
347
  * ```ts
@@ -353,7 +355,7 @@ export declare function createReadableStream<T>(pull: (controller: ReadableStrea
353
355
  * const tools = await client.tools()
354
356
  * ```
355
357
  */
356
- export declare function createStdioClientTransport(options: StdioClientTransportOptions): MCPClientTransportInterface;
358
+ export declare function createStdioClientTransport(options: StdioClientTransportOptions): StdioClientTransportInterface;
357
359
 
358
360
  /**
359
361
  * Creates the MCP stdio transport INGRESS — pumps a transport-agnostic {@link
@@ -1242,7 +1244,7 @@ export declare const SSE_KEEPALIVE_COMMENT = "keepalive";
1242
1244
 
1243
1245
  /**
1244
1246
  * The stdio CLIENT transport for the Model Context Protocol — a
1245
- * {@link MCPClientTransportInterface} that drives a CHILD PROCESS MCP server over
1247
+ * {@link StdioClientTransportInterface} that drives a CHILD PROCESS MCP server over
1246
1248
  * newline-delimited JSON-RPC on `stdin`/`stdout`, the stdio sibling of {@link
1247
1249
  * import('./HTTPClientTransport.js').HTTPClientTransport} and {@link
1248
1250
  * import('./WebSocketClientTransport.js').WebSocketClientTransport}.
@@ -1263,16 +1265,32 @@ export declare const SSE_KEEPALIVE_COMMENT = "keepalive";
1263
1265
  * rejects — it answers `false` for a channel that was closed, destroyed, or ended, and for a write
1264
1266
  * that failed — so a `false` answer REJECTS here with the same not-connected error a transport
1265
1267
  * that was never started raises. A dead peer surfaces at the caller instead of vanishing.
1266
- * - **`close()`** releases this transport's line pump without waiting for the child's stdout
1267
- * iterator, then runs the supervisor's bounded `SIGTERM` grace `SIGKILL` group-kill and
1268
- * teardown before firing `close` once (idempotent). A descendant can retain an inherited stdout
1269
- * pipe after the child exits; the pump's release barrier keeps that substrate limit from keeping
1270
- * this transport's `close()` pending. On a POSIX host the child leads its own process group, so
1271
- * the group-kill reaches its grandchildren rather than orphaning them.
1268
+ * - **`close()`** runs the supervisor's bounded termination and teardown, then fires `close` once
1269
+ * (idempotent). That teardown reaches the child's TERMINAL MOMENT, where the supervisor freezes
1270
+ * `evidence`, ends `lines`, and settles `exit` together, so this transport needs no release of
1271
+ * its own to get its line pump back: the stream ends under the pump rather than throwing at it.
1272
+ * A line the supervisor had already framed behind the one being delivered is dropped rather than
1273
+ * emitted onto a transport whose teardown has begun. A `close()` issued while that teardown runs
1274
+ * joins it rather than opening a second one, so it resolves only after `close` has fired, and a
1275
+ * `start()` issued while it runs waits behind the same barrier, so lifetimes never overlap. A
1276
+ * descendant can retain an inherited stdout pipe after the child exits; the supervisor's `drain`
1277
+ * bound cuts that wait off, so this transport's `close()` settles within that bound rather than
1278
+ * on the descendant. The termination itself belongs to the host: a POSIX host signals the
1279
+ * child's own process group `SIGTERM`, waits the grace window, then `SIGKILL`s through the same
1280
+ * route, so the kill reaches grandchildren rather than orphaning them, while Windows ends the
1281
+ * tree with `taskkill /F /T`, which nothing in the child can intercept.
1282
+ * - **Evidence.** `evidence` reports that retained stderr tail off the HELD child — its live tail
1283
+ * while the child runs, and the value the supervisor froze at that child's terminal moment
1284
+ * afterwards. The reference is held past that moment and replaced only by the next `start()`,
1285
+ * which is what keeps a post-`close()` read stable without a private copy: the frozen value
1286
+ * never moves again, so a detached descendant writing to the inherited stderr after the cutoff
1287
+ * cannot grow it. See {@link StdioClientTransportInterface.evidence} for the readings and the
1288
+ * byte bound.
1272
1289
  * - **Observable.** Owns the `emitter` ({@link MCPClientTransportEventMap}); the
1273
1290
  * emitter isolates a listener throw; `error` is a DOMAIN event (a transport-level
1274
- * fault, including the child spawn cause the supervisor surfaces), distinct from the emitter's
1275
- * own listener-error channel.
1291
+ * fault, including the child spawn cause the supervisor surfaces and the notice that this
1292
+ * lifetime's `evidence` was cut off at the `drain` bound), distinct from the emitter's own
1293
+ * listener-error channel.
1276
1294
  *
1277
1295
  * @example
1278
1296
  * ```ts
@@ -1281,17 +1299,86 @@ export declare const SSE_KEEPALIVE_COMMENT = "keepalive";
1281
1299
  * await client.connect() // start() spawns the child, then the MCP initialize runs over stdio
1282
1300
  * ```
1283
1301
  */
1284
- export declare class StdioClientTransport implements MCPClientTransportInterface_2 {
1302
+ export declare class StdioClientTransport implements StdioClientTransportInterface {
1285
1303
  #private;
1286
1304
  constructor(options: StdioClientTransportOptions);
1287
1305
  get emitter(): EmitterInterface<MCPClientTransportEventMap_2>;
1288
1306
  get session(): string | undefined;
1289
1307
  get duplex(): boolean;
1308
+ get evidence(): string | undefined;
1290
1309
  start(): Promise<void>;
1291
1310
  send(message: JSONRPCMessage_2): Promise<void>;
1292
1311
  close(): Promise<void>;
1293
1312
  }
1294
1313
 
1314
+ /**
1315
+ * The contract `createStdioClientTransport` returns — a {@link MCPClientTransportInterface}
1316
+ * that also reports the supervised child's stderr tail, the diagnostic a child that dies at
1317
+ * startup leaves behind.
1318
+ *
1319
+ * @remarks
1320
+ * This contract adds `evidence` and changes nothing else: `emitter`, `session`, `duplex`,
1321
+ * `start`, `send`, and `close` are the shared client-transport surface, unchanged. It sits here
1322
+ * rather than on {@link MCPClientTransportInterface} because a transport that supervises no child —
1323
+ * Streamable HTTP, WebSocket, a `MessagePort` pair — has no such tail, and a member every one
1324
+ * of them answers `undefined` to forever is a stdio detail rather than a shared contract. A
1325
+ * consumer that widens this value back to {@link MCPClientTransportInterface}, including by
1326
+ * reading `client.transport`, loses the reader and must keep the original reference.
1327
+ */
1328
+ export declare interface StdioClientTransportInterface extends MCPClientTransportInterface {
1329
+ /**
1330
+ * The supervised child's decoded stderr tail — live while a child is held, and the value
1331
+ * captured at that child's end afterwards.
1332
+ *
1333
+ * @remarks
1334
+ * - **Readings.** `undefined` while no child has run and none has been captured — before the
1335
+ * first `start()`. The live tail while a child is held, which reads `''` from the moment
1336
+ * that child is spawned until it writes. The captured tail after the child ended, whether
1337
+ * it exited on its own or `close()` terminated it, and `''` there for a child that ran and
1338
+ * wrote nothing — an empty tail is a real reading of a silent child, distinct from the
1339
+ * absent one.
1340
+ * - **Lifetime.** The tail follows the child that produced it. The supervisor FREEZES it at
1341
+ * that child's terminal moment — the moment `close()`'s teardown resolves past, and the
1342
+ * moment the exit that fires this transport's `close` settles at — and this transport keeps
1343
+ * reading that same child afterwards. The frozen value never moves again, which is what
1344
+ * makes a late read stable: a detached descendant holding the child's inherited stderr can
1345
+ * still write bytes after `close()` resolves, and those bytes reach no reading this
1346
+ * transport reports. The next `start()` replaces the held child, so a replacement never
1347
+ * reports its predecessor's stderr as current. Lifetimes never overlap: a `start()` issued
1348
+ * while a `close()` is still tearing down waits for that teardown to report `close` before
1349
+ * it opens the next one, so an older tail cannot arrive over a newer one however the calls
1350
+ * interleave. Read the tail before you open a replacement, and read how far it reaches
1351
+ * inside one `close` emit off the way the lifetime ended. An explicit `close()` still holds
1352
+ * its teardown barrier while those listeners run, so a `start()` one of them calls parks
1353
+ * behind it and every later listener reads the ended child's frozen tail. A natural exit
1354
+ * holds that barrier only across the `error` it reports at that end, so a restart begun
1355
+ * THERE parks until `close` has been delivered, while a `close` listener that calls
1356
+ * `start()` opens the next lifetime itself and replaces the value every listener after it
1357
+ * would have read.
1358
+ * - **What the close path carries.** The frozen value is what the supervisor had received by
1359
+ * 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
1361
+ * `SIGTERM` handler never runs there, so the bytes it would have written never exist. A
1362
+ * child that ends on its own closes its stderr first, and THAT tail is complete.
1363
+ * Where that moment arrived at the supervisor's `drain` bound rather than at the child's
1364
+ * own stream close, the tail stops at the cutoff and later diagnostics may have existed;
1365
+ * the transport emits an `error` naming that lifetime, so a partial tail reads as partial.
1366
+ * - **Bound.** The supervisor keeps the END of the child's raw stderr bytes, at most
1367
+ * `@orkestrel/process`'s {@link import('@orkestrel/process').PROCESS_EVIDENCE} (2048
1368
+ * bytes under 0.0.6). A child that writes more than the bound loses its earliest output
1369
+ * and keeps its last, which is the half that names why it died. The bound counts raw
1370
+ * bytes before decoding rather than characters, so multibyte output yields fewer
1371
+ * characters than an ASCII run over the same byte window. The kept bytes never begin
1372
+ * inside a multibyte sequence: where the cut lands mid-character the start retreats to
1373
+ * that character's first byte, so the tail decodes without a replacement character and
1374
+ * can hold a few bytes fewer than the bound.
1375
+ * - **A spawn fault leaves no tail.** A spawn that produced no child wrote no stderr, so
1376
+ * `evidence` reads `''` for that lifetime. Its cause — the host's `ENOENT` for a command
1377
+ * that does not resolve — arrives on the `error` event instead.
1378
+ */
1379
+ readonly evidence: string | undefined;
1380
+ }
1381
+
1295
1382
  /**
1296
1383
  * Options for `createStdioClientTransport` — the child process to spawn as a
1297
1384
  * stdio-framed MCP server (newline-delimited JSON-RPC over `stdin`/`stdout`).
@@ -324,22 +324,24 @@ export declare function createReadableStream<T>(pull: (controller: ReadableStrea
324
324
 
325
325
  /**
326
326
  * Creates the stdio CLIENT transport for an {@link import('@orkestrel/mcp').MCPClientInterface}
327
- * — a {@link MCPClientTransportInterface} that spawns and drives a CHILD PROCESS MCP server
327
+ * — a {@link StdioClientTransportInterface} that spawns and drives a CHILD PROCESS MCP server
328
328
  * over newline-delimited JSON-RPC on `stdin`/`stdout`, the stdio sibling of {@link
329
329
  * createHTTPClientTransport} and {@link createWebSocketClientTransport}.
330
330
  *
331
331
  * @remarks
332
332
  * Hand it to `createMCPClient({ transport })`: `start()` (run by `client.connect()`)
333
333
  * spawns `options.command` with `options.args` and `options.env`, piping its
334
- * `stdin`/`stdout` for the JSON-RPC channel (its `stderr` inherits the parent's for
335
- * diagnostics). Each JSON-RPC message the client `send`s is written as one
334
+ * `stdin`/`stdout` for the JSON-RPC channel. The child's `stderr` is piped too, and
335
+ * retained as a bounded tail this transport reports as `evidence` the parent never
336
+ * inherits it. Each JSON-RPC message the client `send`s is written as one
336
337
  * newline-terminated line to the child's `stdin`; each decoded reply line from the
337
338
  * child's `stdout` is surfaced on the transport's `message` event for the client's
338
339
  * id correlation.
339
340
  *
340
341
  * @param options - `command` (the executable to spawn; REQUIRED), optional `args`,
341
342
  * and optional `env`; see {@link StdioClientTransportOptions}
342
- * @returns A working {@link MCPClientTransportInterface} over a child process's stdio
343
+ * @returns A working {@link StdioClientTransportInterface} over a child process's stdio,
344
+ * whose `evidence` carries the supervised child's bounded stderr tail
343
345
  *
344
346
  * @example
345
347
  * ```ts
@@ -353,7 +355,7 @@ export declare function createReadableStream<T>(pull: (controller: ReadableStrea
353
355
  * const tools = await client.tools()
354
356
  * ```
355
357
  */
356
- export declare function createStdioClientTransport(options: StdioClientTransportOptions): MCPClientTransportInterface;
358
+ export declare function createStdioClientTransport(options: StdioClientTransportOptions): StdioClientTransportInterface;
357
359
 
358
360
  /**
359
361
  * Creates the MCP stdio transport INGRESS — pumps a transport-agnostic {@link
@@ -1242,7 +1244,7 @@ export declare const SSE_KEEPALIVE_COMMENT = "keepalive";
1242
1244
 
1243
1245
  /**
1244
1246
  * The stdio CLIENT transport for the Model Context Protocol — a
1245
- * {@link MCPClientTransportInterface} that drives a CHILD PROCESS MCP server over
1247
+ * {@link StdioClientTransportInterface} that drives a CHILD PROCESS MCP server over
1246
1248
  * newline-delimited JSON-RPC on `stdin`/`stdout`, the stdio sibling of {@link
1247
1249
  * import('./HTTPClientTransport.js').HTTPClientTransport} and {@link
1248
1250
  * import('./WebSocketClientTransport.js').WebSocketClientTransport}.
@@ -1263,16 +1265,32 @@ export declare const SSE_KEEPALIVE_COMMENT = "keepalive";
1263
1265
  * rejects — it answers `false` for a channel that was closed, destroyed, or ended, and for a write
1264
1266
  * that failed — so a `false` answer REJECTS here with the same not-connected error a transport
1265
1267
  * that was never started raises. A dead peer surfaces at the caller instead of vanishing.
1266
- * - **`close()`** releases this transport's line pump without waiting for the child's stdout
1267
- * iterator, then runs the supervisor's bounded `SIGTERM` grace `SIGKILL` group-kill and
1268
- * teardown before firing `close` once (idempotent). A descendant can retain an inherited stdout
1269
- * pipe after the child exits; the pump's release barrier keeps that substrate limit from keeping
1270
- * this transport's `close()` pending. On a POSIX host the child leads its own process group, so
1271
- * the group-kill reaches its grandchildren rather than orphaning them.
1268
+ * - **`close()`** runs the supervisor's bounded termination and teardown, then fires `close` once
1269
+ * (idempotent). That teardown reaches the child's TERMINAL MOMENT, where the supervisor freezes
1270
+ * `evidence`, ends `lines`, and settles `exit` together, so this transport needs no release of
1271
+ * its own to get its line pump back: the stream ends under the pump rather than throwing at it.
1272
+ * A line the supervisor had already framed behind the one being delivered is dropped rather than
1273
+ * emitted onto a transport whose teardown has begun. A `close()` issued while that teardown runs
1274
+ * joins it rather than opening a second one, so it resolves only after `close` has fired, and a
1275
+ * `start()` issued while it runs waits behind the same barrier, so lifetimes never overlap. A
1276
+ * descendant can retain an inherited stdout pipe after the child exits; the supervisor's `drain`
1277
+ * bound cuts that wait off, so this transport's `close()` settles within that bound rather than
1278
+ * on the descendant. The termination itself belongs to the host: a POSIX host signals the
1279
+ * child's own process group `SIGTERM`, waits the grace window, then `SIGKILL`s through the same
1280
+ * route, so the kill reaches grandchildren rather than orphaning them, while Windows ends the
1281
+ * tree with `taskkill /F /T`, which nothing in the child can intercept.
1282
+ * - **Evidence.** `evidence` reports that retained stderr tail off the HELD child — its live tail
1283
+ * while the child runs, and the value the supervisor froze at that child's terminal moment
1284
+ * afterwards. The reference is held past that moment and replaced only by the next `start()`,
1285
+ * which is what keeps a post-`close()` read stable without a private copy: the frozen value
1286
+ * never moves again, so a detached descendant writing to the inherited stderr after the cutoff
1287
+ * cannot grow it. See {@link StdioClientTransportInterface.evidence} for the readings and the
1288
+ * byte bound.
1272
1289
  * - **Observable.** Owns the `emitter` ({@link MCPClientTransportEventMap}); the
1273
1290
  * emitter isolates a listener throw; `error` is a DOMAIN event (a transport-level
1274
- * fault, including the child spawn cause the supervisor surfaces), distinct from the emitter's
1275
- * own listener-error channel.
1291
+ * fault, including the child spawn cause the supervisor surfaces and the notice that this
1292
+ * lifetime's `evidence` was cut off at the `drain` bound), distinct from the emitter's own
1293
+ * listener-error channel.
1276
1294
  *
1277
1295
  * @example
1278
1296
  * ```ts
@@ -1281,17 +1299,86 @@ export declare const SSE_KEEPALIVE_COMMENT = "keepalive";
1281
1299
  * await client.connect() // start() spawns the child, then the MCP initialize runs over stdio
1282
1300
  * ```
1283
1301
  */
1284
- export declare class StdioClientTransport implements MCPClientTransportInterface_2 {
1302
+ export declare class StdioClientTransport implements StdioClientTransportInterface {
1285
1303
  #private;
1286
1304
  constructor(options: StdioClientTransportOptions);
1287
1305
  get emitter(): EmitterInterface<MCPClientTransportEventMap_2>;
1288
1306
  get session(): string | undefined;
1289
1307
  get duplex(): boolean;
1308
+ get evidence(): string | undefined;
1290
1309
  start(): Promise<void>;
1291
1310
  send(message: JSONRPCMessage_2): Promise<void>;
1292
1311
  close(): Promise<void>;
1293
1312
  }
1294
1313
 
1314
+ /**
1315
+ * The contract `createStdioClientTransport` returns — a {@link MCPClientTransportInterface}
1316
+ * that also reports the supervised child's stderr tail, the diagnostic a child that dies at
1317
+ * startup leaves behind.
1318
+ *
1319
+ * @remarks
1320
+ * This contract adds `evidence` and changes nothing else: `emitter`, `session`, `duplex`,
1321
+ * `start`, `send`, and `close` are the shared client-transport surface, unchanged. It sits here
1322
+ * rather than on {@link MCPClientTransportInterface} because a transport that supervises no child —
1323
+ * Streamable HTTP, WebSocket, a `MessagePort` pair — has no such tail, and a member every one
1324
+ * of them answers `undefined` to forever is a stdio detail rather than a shared contract. A
1325
+ * consumer that widens this value back to {@link MCPClientTransportInterface}, including by
1326
+ * reading `client.transport`, loses the reader and must keep the original reference.
1327
+ */
1328
+ export declare interface StdioClientTransportInterface extends MCPClientTransportInterface {
1329
+ /**
1330
+ * The supervised child's decoded stderr tail — live while a child is held, and the value
1331
+ * captured at that child's end afterwards.
1332
+ *
1333
+ * @remarks
1334
+ * - **Readings.** `undefined` while no child has run and none has been captured — before the
1335
+ * first `start()`. The live tail while a child is held, which reads `''` from the moment
1336
+ * that child is spawned until it writes. The captured tail after the child ended, whether
1337
+ * it exited on its own or `close()` terminated it, and `''` there for a child that ran and
1338
+ * wrote nothing — an empty tail is a real reading of a silent child, distinct from the
1339
+ * absent one.
1340
+ * - **Lifetime.** The tail follows the child that produced it. The supervisor FREEZES it at
1341
+ * that child's terminal moment — the moment `close()`'s teardown resolves past, and the
1342
+ * moment the exit that fires this transport's `close` settles at — and this transport keeps
1343
+ * reading that same child afterwards. The frozen value never moves again, which is what
1344
+ * makes a late read stable: a detached descendant holding the child's inherited stderr can
1345
+ * still write bytes after `close()` resolves, and those bytes reach no reading this
1346
+ * transport reports. The next `start()` replaces the held child, so a replacement never
1347
+ * reports its predecessor's stderr as current. Lifetimes never overlap: a `start()` issued
1348
+ * while a `close()` is still tearing down waits for that teardown to report `close` before
1349
+ * it opens the next one, so an older tail cannot arrive over a newer one however the calls
1350
+ * interleave. Read the tail before you open a replacement, and read how far it reaches
1351
+ * inside one `close` emit off the way the lifetime ended. An explicit `close()` still holds
1352
+ * its teardown barrier while those listeners run, so a `start()` one of them calls parks
1353
+ * behind it and every later listener reads the ended child's frozen tail. A natural exit
1354
+ * holds that barrier only across the `error` it reports at that end, so a restart begun
1355
+ * THERE parks until `close` has been delivered, while a `close` listener that calls
1356
+ * `start()` opens the next lifetime itself and replaces the value every listener after it
1357
+ * would have read.
1358
+ * - **What the close path carries.** The frozen value is what the supervisor had received by
1359
+ * 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
1361
+ * `SIGTERM` handler never runs there, so the bytes it would have written never exist. A
1362
+ * child that ends on its own closes its stderr first, and THAT tail is complete.
1363
+ * Where that moment arrived at the supervisor's `drain` bound rather than at the child's
1364
+ * own stream close, the tail stops at the cutoff and later diagnostics may have existed;
1365
+ * the transport emits an `error` naming that lifetime, so a partial tail reads as partial.
1366
+ * - **Bound.** The supervisor keeps the END of the child's raw stderr bytes, at most
1367
+ * `@orkestrel/process`'s {@link import('@orkestrel/process').PROCESS_EVIDENCE} (2048
1368
+ * bytes under 0.0.6). A child that writes more than the bound loses its earliest output
1369
+ * and keeps its last, which is the half that names why it died. The bound counts raw
1370
+ * bytes before decoding rather than characters, so multibyte output yields fewer
1371
+ * characters than an ASCII run over the same byte window. The kept bytes never begin
1372
+ * inside a multibyte sequence: where the cut lands mid-character the start retreats to
1373
+ * that character's first byte, so the tail decodes without a replacement character and
1374
+ * can hold a few bytes fewer than the bound.
1375
+ * - **A spawn fault leaves no tail.** A spawn that produced no child wrote no stderr, so
1376
+ * `evidence` reads `''` for that lifetime. Its cause — the host's `ENOENT` for a command
1377
+ * that does not resolve — arrives on the `error` event instead.
1378
+ */
1379
+ readonly evidence: string | undefined;
1380
+ }
1381
+
1295
1382
  /**
1296
1383
  * Options for `createStdioClientTransport` — the child process to spawn as a
1297
1384
  * stdio-framed MCP server (newline-delimited JSON-RPC over `stdin`/`stdout`).
@@ -1362,7 +1362,7 @@ var WebSocketClientTransport = class {
1362
1362
  //#region src/server/transports/StdioClientTransport.ts
1363
1363
  /**
1364
1364
  * The stdio CLIENT transport for the Model Context Protocol — a
1365
- * {@link MCPClientTransportInterface} that drives a CHILD PROCESS MCP server over
1365
+ * {@link StdioClientTransportInterface} that drives a CHILD PROCESS MCP server over
1366
1366
  * newline-delimited JSON-RPC on `stdin`/`stdout`, the stdio sibling of {@link
1367
1367
  * import('./HTTPClientTransport.js').HTTPClientTransport} and {@link
1368
1368
  * import('./WebSocketClientTransport.js').WebSocketClientTransport}.
@@ -1383,16 +1383,32 @@ var WebSocketClientTransport = class {
1383
1383
  * rejects — it answers `false` for a channel that was closed, destroyed, or ended, and for a write
1384
1384
  * that failed — so a `false` answer REJECTS here with the same not-connected error a transport
1385
1385
  * that was never started raises. A dead peer surfaces at the caller instead of vanishing.
1386
- * - **`close()`** releases this transport's line pump without waiting for the child's stdout
1387
- * iterator, then runs the supervisor's bounded `SIGTERM` grace `SIGKILL` group-kill and
1388
- * teardown before firing `close` once (idempotent). A descendant can retain an inherited stdout
1389
- * pipe after the child exits; the pump's release barrier keeps that substrate limit from keeping
1390
- * this transport's `close()` pending. On a POSIX host the child leads its own process group, so
1391
- * the group-kill reaches its grandchildren rather than orphaning them.
1386
+ * - **`close()`** runs the supervisor's bounded termination and teardown, then fires `close` once
1387
+ * (idempotent). That teardown reaches the child's TERMINAL MOMENT, where the supervisor freezes
1388
+ * `evidence`, ends `lines`, and settles `exit` together, so this transport needs no release of
1389
+ * its own to get its line pump back: the stream ends under the pump rather than throwing at it.
1390
+ * A line the supervisor had already framed behind the one being delivered is dropped rather than
1391
+ * emitted onto a transport whose teardown has begun. A `close()` issued while that teardown runs
1392
+ * joins it rather than opening a second one, so it resolves only after `close` has fired, and a
1393
+ * `start()` issued while it runs waits behind the same barrier, so lifetimes never overlap. A
1394
+ * descendant can retain an inherited stdout pipe after the child exits; the supervisor's `drain`
1395
+ * bound cuts that wait off, so this transport's `close()` settles within that bound rather than
1396
+ * on the descendant. The termination itself belongs to the host: a POSIX host signals the
1397
+ * child's own process group `SIGTERM`, waits the grace window, then `SIGKILL`s through the same
1398
+ * route, so the kill reaches grandchildren rather than orphaning them, while Windows ends the
1399
+ * tree with `taskkill /F /T`, which nothing in the child can intercept.
1400
+ * - **Evidence.** `evidence` reports that retained stderr tail off the HELD child — its live tail
1401
+ * while the child runs, and the value the supervisor froze at that child's terminal moment
1402
+ * afterwards. The reference is held past that moment and replaced only by the next `start()`,
1403
+ * which is what keeps a post-`close()` read stable without a private copy: the frozen value
1404
+ * never moves again, so a detached descendant writing to the inherited stderr after the cutoff
1405
+ * cannot grow it. See {@link StdioClientTransportInterface.evidence} for the readings and the
1406
+ * byte bound.
1392
1407
  * - **Observable.** Owns the `emitter` ({@link MCPClientTransportEventMap}); the
1393
1408
  * emitter isolates a listener throw; `error` is a DOMAIN event (a transport-level
1394
- * fault, including the child spawn cause the supervisor surfaces), distinct from the emitter's
1395
- * own listener-error channel.
1409
+ * fault, including the child spawn cause the supervisor surfaces and the notice that this
1410
+ * lifetime's `evidence` was cut off at the `drain` bound), distinct from the emitter's own
1411
+ * listener-error channel.
1396
1412
  *
1397
1413
  * @example
1398
1414
  * ```ts
@@ -1407,8 +1423,7 @@ var StdioClientTransport = class {
1407
1423
  #args;
1408
1424
  #env;
1409
1425
  #process = void 0;
1410
- #release = Promise.withResolvers();
1411
- #pumping = Promise.resolve();
1426
+ #closing = void 0;
1412
1427
  #closed = false;
1413
1428
  constructor(options) {
1414
1429
  this.#emitter = new Emitter();
@@ -1423,10 +1438,21 @@ var StdioClientTransport = class {
1423
1438
  get duplex() {
1424
1439
  return true;
1425
1440
  }
1441
+ get evidence() {
1442
+ return this.#process?.evidence;
1443
+ }
1426
1444
  async start() {
1427
- if (this.#process !== void 0) return;
1445
+ let closing = this.#closing;
1446
+ while (closing !== void 0) {
1447
+ await closing;
1448
+ if (this.#closing === closing) {
1449
+ this.#closing = void 0;
1450
+ break;
1451
+ }
1452
+ closing = this.#closing;
1453
+ }
1454
+ if (this.#process !== void 0 && !this.#closed) return;
1428
1455
  this.#closed = false;
1429
- this.#release = Promise.withResolvers();
1430
1456
  const child = new Process({
1431
1457
  command: {
1432
1458
  file: this.#command,
@@ -1439,39 +1465,49 @@ var StdioClientTransport = class {
1439
1465
  });
1440
1466
  this.#process = child;
1441
1467
  child.emitter.on("error", (cause) => this.#emitter.emit("error", cause));
1442
- child.exit.then(() => this.#onExit(child));
1443
- this.#pumping = this.#pump(child, this.#release.promise);
1468
+ child.exit.then((exit) => this.#onExit(child, exit));
1469
+ this.#pump(child);
1444
1470
  }
1445
1471
  async send(message) {
1446
- const child = this.#process;
1472
+ const child = this.#closed ? void 0 : this.#process;
1447
1473
  if (!(child === void 0 ? false : await child.send(JSON.stringify(message)))) throw new Error("stdio transport is not connected");
1448
1474
  }
1449
1475
  async close() {
1476
+ if (this.#closed && this.#closing === void 0) return;
1477
+ this.#closing ??= this.#teardown();
1478
+ await this.#closing;
1479
+ }
1480
+ async #teardown() {
1450
1481
  if (this.#closed) return;
1451
1482
  this.#closed = true;
1452
1483
  const child = this.#process;
1453
- const pumping = this.#pumping;
1454
- this.#release.resolve();
1455
- this.#process = void 0;
1456
- if (child !== void 0) await child.destroy();
1457
- await pumping;
1484
+ if (child !== void 0) {
1485
+ await child.destroy();
1486
+ this.#report(await child.exit);
1487
+ }
1458
1488
  this.#emitter.emit("close");
1459
1489
  }
1460
- async #pump(child, release) {
1461
- const iterator = child.lines[Symbol.asyncIterator]();
1462
- while (true) {
1463
- const next = await Promise.race([iterator.next(), release]);
1464
- if (next === void 0 || next.done) return;
1465
- if (this.#process !== child) return;
1466
- dispatchLines(this.#emitter, [next.value]);
1490
+ async #pump(child) {
1491
+ for await (const line of child.lines) {
1492
+ if (this.#closed || this.#process !== child) return;
1493
+ dispatchLines(this.#emitter, [line]);
1467
1494
  }
1468
1495
  }
1469
- #onExit(child) {
1470
- if (this.#closed || this.#process !== child) return;
1496
+ #onExit(child, exit) {
1497
+ if (this.#process !== child) return;
1498
+ if (this.#closed) return;
1471
1499
  this.#closed = true;
1472
- this.#process = void 0;
1500
+ const barrier = Promise.withResolvers();
1501
+ this.#closing ??= barrier.promise;
1502
+ this.#report(exit);
1503
+ barrier.resolve();
1504
+ if (this.#closing === barrier.promise) this.#closing = void 0;
1473
1505
  this.#emitter.emit("close");
1474
1506
  }
1507
+ #report(exit) {
1508
+ if (exit.drained) return;
1509
+ this.#emitter.emit("error", /* @__PURE__ */ new Error("stdio transport evidence may be incomplete: the child streams stayed open past the supervisor drain bound"));
1510
+ }
1475
1511
  };
1476
1512
  //#endregion
1477
1513
  //#region src/server/transports/StdioServerTransport.ts
@@ -1816,22 +1852,24 @@ function createWebSocketClientTransport(options) {
1816
1852
  }
1817
1853
  /**
1818
1854
  * Creates the stdio CLIENT transport for an {@link import('@orkestrel/mcp').MCPClientInterface}
1819
- * — a {@link MCPClientTransportInterface} that spawns and drives a CHILD PROCESS MCP server
1855
+ * — a {@link StdioClientTransportInterface} that spawns and drives a CHILD PROCESS MCP server
1820
1856
  * over newline-delimited JSON-RPC on `stdin`/`stdout`, the stdio sibling of {@link
1821
1857
  * createHTTPClientTransport} and {@link createWebSocketClientTransport}.
1822
1858
  *
1823
1859
  * @remarks
1824
1860
  * Hand it to `createMCPClient({ transport })`: `start()` (run by `client.connect()`)
1825
1861
  * spawns `options.command` with `options.args` and `options.env`, piping its
1826
- * `stdin`/`stdout` for the JSON-RPC channel (its `stderr` inherits the parent's for
1827
- * diagnostics). Each JSON-RPC message the client `send`s is written as one
1862
+ * `stdin`/`stdout` for the JSON-RPC channel. The child's `stderr` is piped too, and
1863
+ * retained as a bounded tail this transport reports as `evidence` the parent never
1864
+ * inherits it. Each JSON-RPC message the client `send`s is written as one
1828
1865
  * newline-terminated line to the child's `stdin`; each decoded reply line from the
1829
1866
  * child's `stdout` is surfaced on the transport's `message` event for the client's
1830
1867
  * id correlation.
1831
1868
  *
1832
1869
  * @param options - `command` (the executable to spawn; REQUIRED), optional `args`,
1833
1870
  * and optional `env`; see {@link StdioClientTransportOptions}
1834
- * @returns A working {@link MCPClientTransportInterface} over a child process's stdio
1871
+ * @returns A working {@link StdioClientTransportInterface} over a child process's stdio,
1872
+ * whose `evidence` carries the supervised child's bounded stderr tail
1835
1873
  *
1836
1874
  * @example
1837
1875
  * ```ts