@orkestrel/mcp 0.0.20 → 0.0.22

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.
@@ -85,6 +85,18 @@ var DEFAULT_MCP_SESSION_CAPACITY = 1024;
85
85
  * this bounds the replay log paired with it.
86
86
  */
87
87
  var DEFAULT_MCP_SESSION_TTL = 3e5;
88
+ /**
89
+ * The default bound in milliseconds on one unconfirmed write to a stdio client transport's
90
+ * child `stdin` — the `delivery` a `createStdioClientTransport` caller who supplies none gets.
91
+ *
92
+ * @remarks
93
+ * Ten seconds. The load-bearing property is the ordering, not the magnitude: this bound stays
94
+ * BELOW {@link import('@orkestrel/mcp').DEFAULT_MCP_REQUEST_TIMEOUT}, so a write the child never
95
+ * reads fails as an undeliverable message while the request that carried it is still open,
96
+ * rather than being masked by that request's own deadline expiring first. Override per
97
+ * transport with `delivery`; an explicit `0` there removes the bound.
98
+ */
99
+ var DEFAULT_MCP_DELIVERY = 1e4;
88
100
  //#endregion
89
101
  //#region src/server/helpers.ts
90
102
  /**
@@ -1363,7 +1375,7 @@ var WebSocketClientTransport = class {
1363
1375
  //#region src/server/transports/StdioClientTransport.ts
1364
1376
  /**
1365
1377
  * The stdio CLIENT transport for the Model Context Protocol — a
1366
- * {@link MCPClientTransportInterface} that drives a CHILD PROCESS MCP server over
1378
+ * {@link StdioClientTransportInterface} that drives a CHILD PROCESS MCP server over
1367
1379
  * newline-delimited JSON-RPC on `stdin`/`stdout`, the stdio sibling of {@link
1368
1380
  * import('./HTTPClientTransport.js').HTTPClientTransport} and {@link
1369
1381
  * import('./WebSocketClientTransport.js').WebSocketClientTransport}.
@@ -1381,19 +1393,36 @@ var WebSocketClientTransport = class {
1381
1393
  * - **Outbound (`send`).** `send(message)` writes one newline-terminated `JSON.stringify`d line
1382
1394
  * through the supervisor's `send` and AWAITS its answer, so this promise settles only after the
1383
1395
  * host reports the line handled rather than the moment the write is queued. The supervisor never
1384
- * rejects — it answers `false` for a channel that was closed, destroyed, or ended, and for a write
1385
- * that failed so a `false` answer REJECTS here with the same not-connected error a transport
1386
- * that was never started raises. A dead peer surfaces at the caller instead of vanishing.
1387
- * - **`close()`** releases this transport's line pump without waiting for the child's stdout
1388
- * iterator, then runs the supervisor's bounded `SIGTERM` grace `SIGKILL` group-kill and
1389
- * teardown before firing `close` once (idempotent). A descendant can retain an inherited stdout
1390
- * pipe after the child exits; the pump's release barrier keeps that substrate limit from keeping
1391
- * this transport's `close()` pending. On a POSIX host the child leads its own process group, so
1392
- * the group-kill reaches its grandchildren rather than orphaning them.
1396
+ * rejects — it answers `false` for a channel that was closed, destroyed, or ended, for a write
1397
+ * that failed, or for one that remained unconfirmed through `delivery`. A call made without a
1398
+ * live child rejects as not connected; a `false` answer from a live child rejects as unable to
1399
+ * deliver. The supervisor does not disclose which cause produced that answer.
1400
+ * - **`close()`** runs the supervisor's bounded termination and teardown, then fires `close` once
1401
+ * (idempotent). That teardown reaches the child's TERMINAL MOMENT, where the supervisor freezes
1402
+ * `evidence`, ends `lines`, and settles `exit` together, so this transport needs no release of
1403
+ * its own to get its line pump back: the stream ends under the pump rather than throwing at it.
1404
+ * A line the supervisor had already framed behind the one being delivered is dropped rather than
1405
+ * emitted onto a transport whose teardown has begun. A `close()` issued while that teardown runs
1406
+ * joins it rather than opening a second one, so it resolves only after `close` has fired, and a
1407
+ * `start()` issued while it runs waits behind the same barrier, so lifetimes never overlap. A
1408
+ * descendant can retain an inherited stdout pipe after the child exits; the supervisor's `drain`
1409
+ * bound cuts that wait off, so this transport's `close()` settles within that bound rather than
1410
+ * on the descendant. The termination itself belongs to the host: a POSIX host signals the
1411
+ * child's own process group `SIGTERM`, waits the grace window, then `SIGKILL`s through the same
1412
+ * route, so the kill reaches grandchildren rather than orphaning them, while Windows ends the
1413
+ * tree with `taskkill /F /T`, which nothing in the child can intercept.
1414
+ * - **Evidence.** `evidence` reports that retained stderr tail off the HELD child — its live tail
1415
+ * while the child runs, and the value the supervisor froze at that child's terminal moment
1416
+ * afterwards. The reference is held past that moment and replaced only by the next `start()`,
1417
+ * which is what keeps a post-`close()` read stable without a private copy: the frozen value
1418
+ * never moves again, so a detached descendant writing to the inherited stderr after the cutoff
1419
+ * cannot grow it. See {@link StdioClientTransportInterface.evidence} for the readings and the
1420
+ * byte bound.
1393
1421
  * - **Observable.** Owns the `emitter` ({@link MCPClientTransportEventMap}); the
1394
1422
  * emitter isolates a listener throw; `error` is a DOMAIN event (a transport-level
1395
- * fault, including the child spawn cause the supervisor surfaces), distinct from the emitter's
1396
- * own listener-error channel.
1423
+ * fault, including the child spawn cause the supervisor surfaces and the notice that this
1424
+ * lifetime's `evidence` was cut off at the `drain` bound), distinct from the emitter's own
1425
+ * listener-error channel.
1397
1426
  *
1398
1427
  * @example
1399
1428
  * ```ts
@@ -1407,15 +1436,16 @@ var StdioClientTransport = class {
1407
1436
  #command;
1408
1437
  #args;
1409
1438
  #env;
1439
+ #delivery;
1410
1440
  #process = void 0;
1411
- #release = Promise.withResolvers();
1412
- #pumping = Promise.resolve();
1441
+ #closing = void 0;
1413
1442
  #closed = false;
1414
1443
  constructor(options) {
1415
1444
  this.#emitter = new _orkestrel_emitter.Emitter();
1416
1445
  this.#command = options.command;
1417
1446
  this.#args = options.args ?? [];
1418
1447
  this.#env = options.env;
1448
+ this.#delivery = options.delivery ?? 1e4;
1419
1449
  }
1420
1450
  get emitter() {
1421
1451
  return this.#emitter;
@@ -1424,10 +1454,21 @@ var StdioClientTransport = class {
1424
1454
  get duplex() {
1425
1455
  return true;
1426
1456
  }
1457
+ get evidence() {
1458
+ return this.#process?.evidence;
1459
+ }
1427
1460
  async start() {
1428
- if (this.#process !== void 0) return;
1461
+ let closing = this.#closing;
1462
+ while (closing !== void 0) {
1463
+ await closing;
1464
+ if (this.#closing === closing) {
1465
+ this.#closing = void 0;
1466
+ break;
1467
+ }
1468
+ closing = this.#closing;
1469
+ }
1470
+ if (this.#process !== void 0 && !this.#closed) return;
1429
1471
  this.#closed = false;
1430
- this.#release = Promise.withResolvers();
1431
1472
  const child = new _orkestrel_process_server.Process({
1432
1473
  command: {
1433
1474
  file: this.#command,
@@ -1436,43 +1477,65 @@ var StdioClientTransport = class {
1436
1477
  },
1437
1478
  workspace: process.cwd(),
1438
1479
  grace: _orkestrel_process.PROCESS_GRACE,
1480
+ delivery: this.#delivery,
1439
1481
  writable: true
1440
1482
  });
1441
1483
  this.#process = child;
1442
1484
  child.emitter.on("error", (cause) => this.#emitter.emit("error", cause));
1443
- child.exit.then(() => this.#onExit(child));
1444
- this.#pumping = this.#pump(child, this.#release.promise);
1485
+ child.exit.then((exit) => this.#onExit(child, exit));
1486
+ this.#pump(child);
1445
1487
  }
1488
+ /**
1489
+ * Sends one newline-delimited JSON-RPC message to the live child.
1490
+ *
1491
+ * @param message - The message to write to the child's `stdin`
1492
+ * @returns Resolves when the supervisor confirms the write
1493
+ * @throws Thrown with `stdio transport is not connected` when no live child is available before
1494
+ * the write
1495
+ * @throws Thrown with `stdio transport could not deliver the message` when a live child's write
1496
+ * resolves `false`
1497
+ */
1446
1498
  async send(message) {
1447
- const child = this.#process;
1448
- if (!(child === void 0 ? false : await child.send(JSON.stringify(message)))) throw new Error("stdio transport is not connected");
1499
+ const child = this.#closed ? void 0 : this.#process;
1500
+ if (child === void 0) throw new Error("stdio transport is not connected");
1501
+ if (!await child.send(JSON.stringify(message))) throw new Error("stdio transport could not deliver the message");
1449
1502
  }
1450
1503
  async close() {
1504
+ if (this.#closed && this.#closing === void 0) return;
1505
+ this.#closing ??= this.#teardown();
1506
+ await this.#closing;
1507
+ }
1508
+ async #teardown() {
1451
1509
  if (this.#closed) return;
1452
1510
  this.#closed = true;
1453
1511
  const child = this.#process;
1454
- const pumping = this.#pumping;
1455
- this.#release.resolve();
1456
- this.#process = void 0;
1457
- if (child !== void 0) await child.destroy();
1458
- await pumping;
1512
+ if (child !== void 0) {
1513
+ await child.destroy();
1514
+ this.#report(await child.exit);
1515
+ }
1459
1516
  this.#emitter.emit("close");
1460
1517
  }
1461
- async #pump(child, release) {
1462
- const iterator = child.lines[Symbol.asyncIterator]();
1463
- while (true) {
1464
- const next = await Promise.race([iterator.next(), release]);
1465
- if (next === void 0 || next.done) return;
1466
- if (this.#process !== child) return;
1467
- dispatchLines(this.#emitter, [next.value]);
1518
+ async #pump(child) {
1519
+ for await (const line of child.lines) {
1520
+ if (this.#closed || this.#process !== child) return;
1521
+ dispatchLines(this.#emitter, [line]);
1468
1522
  }
1469
1523
  }
1470
- #onExit(child) {
1471
- if (this.#closed || this.#process !== child) return;
1524
+ #onExit(child, exit) {
1525
+ if (this.#process !== child) return;
1526
+ if (this.#closed) return;
1472
1527
  this.#closed = true;
1473
- this.#process = void 0;
1528
+ const barrier = Promise.withResolvers();
1529
+ this.#closing ??= barrier.promise;
1530
+ this.#report(exit);
1531
+ barrier.resolve();
1532
+ if (this.#closing === barrier.promise) this.#closing = void 0;
1474
1533
  this.#emitter.emit("close");
1475
1534
  }
1535
+ #report(exit) {
1536
+ if (exit.drained) return;
1537
+ this.#emitter.emit("error", /* @__PURE__ */ new Error("stdio transport evidence may be incomplete: the child streams stayed open past the supervisor drain bound"));
1538
+ }
1476
1539
  };
1477
1540
  //#endregion
1478
1541
  //#region src/server/transports/StdioServerTransport.ts
@@ -1817,22 +1880,28 @@ function createWebSocketClientTransport(options) {
1817
1880
  }
1818
1881
  /**
1819
1882
  * Creates the stdio CLIENT transport for an {@link import('@orkestrel/mcp').MCPClientInterface}
1820
- * — a {@link MCPClientTransportInterface} that spawns and drives a CHILD PROCESS MCP server
1883
+ * — a {@link StdioClientTransportInterface} that spawns and drives a CHILD PROCESS MCP server
1821
1884
  * over newline-delimited JSON-RPC on `stdin`/`stdout`, the stdio sibling of {@link
1822
1885
  * createHTTPClientTransport} and {@link createWebSocketClientTransport}.
1823
1886
  *
1824
1887
  * @remarks
1825
1888
  * Hand it to `createMCPClient({ transport })`: `start()` (run by `client.connect()`)
1826
1889
  * spawns `options.command` with `options.args` and `options.env`, piping its
1827
- * `stdin`/`stdout` for the JSON-RPC channel (its `stderr` inherits the parent's for
1828
- * diagnostics). Each JSON-RPC message the client `send`s is written as one
1890
+ * `stdin`/`stdout` for the JSON-RPC channel. The child's `stderr` is piped too, and
1891
+ * retained as a bounded tail this transport reports as `evidence` the parent never
1892
+ * inherits it. Each JSON-RPC message the client `send`s is written as one
1829
1893
  * newline-terminated line to the child's `stdin`; each decoded reply line from the
1830
1894
  * child's `stdout` is surfaced on the transport's `message` event for the client's
1831
- * id correlation.
1895
+ * id correlation. That write is bounded: a child that stays alive without ever reading
1896
+ * its `stdin` fills the pipe, and `options.delivery` is how long the unconfirmed write
1897
+ * waits before the `send` rejects. An omitted `delivery` selects {@link
1898
+ * import('./constants.js').DEFAULT_MCP_DELIVERY}; an explicit `0` removes the bound.
1832
1899
  *
1833
1900
  * @param options - `command` (the executable to spawn; REQUIRED), optional `args`,
1834
- * and optional `env`; see {@link StdioClientTransportOptions}
1835
- * @returns A working {@link MCPClientTransportInterface} over a child process's stdio
1901
+ * optional `env`, and an optional `delivery` bound in milliseconds on an unconfirmed
1902
+ * `stdin` write; see {@link StdioClientTransportOptions}
1903
+ * @returns A working {@link StdioClientTransportInterface} over a child process's stdio,
1904
+ * whose `evidence` carries the supervised child's bounded stderr tail
1836
1905
  *
1837
1906
  * @example
1838
1907
  * ```ts
@@ -2069,6 +2138,7 @@ function createMCPSession(options) {
2069
2138
  };
2070
2139
  }
2071
2140
  //#endregion
2141
+ exports.DEFAULT_MCP_DELIVERY = DEFAULT_MCP_DELIVERY;
2072
2142
  exports.DEFAULT_MCP_KEEPALIVE_INTERVAL = DEFAULT_MCP_KEEPALIVE_INTERVAL;
2073
2143
  exports.DEFAULT_MCP_PATH = DEFAULT_MCP_PATH;
2074
2144
  exports.DEFAULT_MCP_SESSION_CAPACITY = DEFAULT_MCP_SESSION_CAPACITY;