@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.
- package/dist/src/server/index.cjs +73 -35
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +102 -15
- package/dist/src/server/index.d.ts +102 -15
- package/dist/src/server/index.js +73 -35
- package/dist/src/server/index.js.map +1 -1
- package/package.json +11 -9
|
@@ -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
|
|
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
|
|
335
|
-
*
|
|
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
|
|
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):
|
|
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
|
|
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()`**
|
|
1267
|
-
*
|
|
1268
|
-
*
|
|
1269
|
-
*
|
|
1270
|
-
*
|
|
1271
|
-
*
|
|
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
|
|
1275
|
-
*
|
|
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
|
|
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
|
|
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
|
|
335
|
-
*
|
|
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
|
|
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):
|
|
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
|
|
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()`**
|
|
1267
|
-
*
|
|
1268
|
-
*
|
|
1269
|
-
*
|
|
1270
|
-
*
|
|
1271
|
-
*
|
|
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
|
|
1275
|
-
*
|
|
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
|
|
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`).
|
package/dist/src/server/index.js
CHANGED
|
@@ -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
|
|
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()`**
|
|
1387
|
-
*
|
|
1388
|
-
*
|
|
1389
|
-
*
|
|
1390
|
-
*
|
|
1391
|
-
*
|
|
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
|
|
1395
|
-
*
|
|
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
|
-
#
|
|
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
|
-
|
|
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.#
|
|
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
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
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
|
|
1461
|
-
const
|
|
1462
|
-
|
|
1463
|
-
|
|
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.#
|
|
1496
|
+
#onExit(child, exit) {
|
|
1497
|
+
if (this.#process !== child) return;
|
|
1498
|
+
if (this.#closed) return;
|
|
1471
1499
|
this.#closed = true;
|
|
1472
|
-
|
|
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
|
|
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
|
|
1827
|
-
*
|
|
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
|
|
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
|