@orkestrel/mcp 0.0.22 → 0.0.24

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.
@@ -2300,6 +2300,11 @@ export declare interface MCPClientTransportInterface {
2300
2300
  * throwing. The client cannot enforce this from its side — the throw and the write are
2301
2301
  * indistinguishable to it — so it is stated here, on the contract that owns it.
2302
2302
  *
2303
+ * A transport whose channel confirms the write rejects on its failure. A transport whose
2304
+ * exchange reports through the emitter resolves. A transport whose channel cannot confirm a
2305
+ * write answers a closed channel from its own state, and each states which in its own
2306
+ * remarks.
2307
+ *
2303
2308
  * @param message - The message to write to the wire
2304
2309
  * @returns Resolves once the message has been sent, and rejects — never throws — when the
2305
2310
  * write fails
@@ -2300,6 +2300,11 @@ export declare interface MCPClientTransportInterface {
2300
2300
  * throwing. The client cannot enforce this from its side — the throw and the write are
2301
2301
  * indistinguishable to it — so it is stated here, on the contract that owns it.
2302
2302
  *
2303
+ * A transport whose channel confirms the write rejects on its failure. A transport whose
2304
+ * exchange reports through the emitter resolves. A transport whose channel cannot confirm a
2305
+ * write answers a closed channel from its own state, and each states which in its own
2306
+ * remarks.
2307
+ *
2303
2308
  * @param message - The message to write to the wire
2304
2309
  * @returns Resolves once the message has been sent, and rejects — never throws — when the
2305
2310
  * write fails
@@ -365,6 +365,42 @@ function extractLines(buffer, chunk) {
365
365
  };
366
366
  }
367
367
  /**
368
+ * Writes one line to a Node writable stream and waits for its completion callback.
369
+ *
370
+ * @remarks
371
+ * The completion callback is the writable channel's backpressure boundary. A callback error and
372
+ * a synchronous `write` throw reject the returned promise with the original value.
373
+ *
374
+ * That callback is the ONLY thing that settles the promise: this helper holds no timer and no
375
+ * abort, so an output that neither confirms nor fails the write parks the promise for as long as
376
+ * the caller-owned stream holds the callback. A caller wanting a bound races this promise against
377
+ * one it owns — {@link import('./transports/StdioServerTransport.js').StdioServerTransport}
378
+ * registers such a bound per send and rejects it on `close()`, so closing the transport settles
379
+ * the CALLER's `send` while the abandoned write stays with the stream that still holds its
380
+ * callback, reachable from nothing the transport retains.
381
+ *
382
+ * @param output - The writable stream that receives the line
383
+ * @param line - The complete line to write
384
+ * @returns Resolves when the stream confirms the write; rejects when the write fails
385
+ *
386
+ * @example
387
+ * ```ts
388
+ * await writeLine(process.stdout, '{"jsonrpc":"2.0","method":"ping"}\n')
389
+ * ```
390
+ */
391
+ function writeLine(output, line) {
392
+ return new Promise((resolve, reject) => {
393
+ try {
394
+ output.write(line, (error) => {
395
+ if (error === void 0 || error === null) resolve();
396
+ else reject(error);
397
+ });
398
+ } catch (error) {
399
+ reject(error);
400
+ }
401
+ });
402
+ }
403
+ /**
368
404
  * Decodes and delivers each complete newline-framed line onto a {@link
369
405
  * MCPClientTransportEventMap} emitter — the shared per-chunk dispatch step both stdio
370
406
  * transports run their framed lines through: the server transport frames with {@link
@@ -1212,7 +1248,11 @@ var WebSocketServerTransport = class {
1212
1248
  * event (the reply the {@link import('@orkestrel/mcp').MCPClientInterface} correlates by `id`); a
1213
1249
  * non-JSON / non-message frame surfaces on `error` and is dropped. The socket's `close`
1214
1250
  * / `error` bridge to this transport's events.
1215
- * - **Outbound (`send`).** `send(message)` writes one masked text frame.
1251
+ * - **Outbound (`send`).** `send(message)` writes one masked text frame. A socket write is not
1252
+ * confirmed, so this transport answers a closed channel from its OWN state: a `send` with no
1253
+ * bound socket — before `start()`, after `close()`, or after the peer ended the socket —
1254
+ * REJECTS with `WebSocket transport is not connected`. It neither drops the message (the
1255
+ * browser face's posture) nor queues it for a connection this transport is not holding.
1216
1256
  * - **`close()`** unsubscribes from the socket, closes it, and fires `close` (idempotent). An
1217
1257
  * upgrade still on the wire is DESTROYED, so a `close()` during the handshake ends the
1218
1258
  * transport at once instead of waiting for a peer that may never answer — the suspended
@@ -1559,8 +1599,10 @@ var StdioClientTransport = class {
1559
1599
  * emits `error` (never throws). `input`'s `close` bridges to this
1560
1600
  * transport's `close`.
1561
1601
  * - **Outbound (`send`).** `send(message)` writes one newline-terminated
1562
- * `JSON.stringify`d line to `output`.
1563
- * - **`close()`** removes this transport's input subscriptions and fires its `close`
1602
+ * `JSON.stringify`d line to `output` and awaits the writable completion callback. The
1603
+ * callback is the backpressure boundary and its error rejects the send.
1604
+ * - **`close()`** removes this transport's input and output subscriptions, rejects every
1605
+ * pending send, and fires its `close`
1564
1606
  * event (idempotent). It pauses the input only when the caller was not already reading
1565
1607
  * it at `start` (`readableFlowing !== true`) AND no `data` listener remains once this
1566
1608
  * transport's own is removed — so a process holding `process.stdin` can exit, and a
@@ -1583,6 +1625,7 @@ var StdioServerTransport = class {
1583
1625
  #data = (chunk) => this.#receive(chunk.toString());
1584
1626
  #ending = () => this.#onClose();
1585
1627
  #failure = (error) => this.#emitter.emit("error", error);
1628
+ #pending = /* @__PURE__ */ new Set();
1586
1629
  #buffer = "";
1587
1630
  #started = false;
1588
1631
  #closed = false;
@@ -1606,9 +1649,30 @@ var StdioServerTransport = class {
1606
1649
  this.#input.on("data", this.#data);
1607
1650
  this.#input.on("close", this.#ending);
1608
1651
  this.#input.on("error", this.#failure);
1652
+ this.#output.on("error", this.#failure);
1609
1653
  }
1654
+ /**
1655
+ * Sends one newline-delimited JSON-RPC message through the caller-owned output stream.
1656
+ *
1657
+ * @remarks
1658
+ * The writable completion callback is the backpressure boundary. This method awaits that
1659
+ * callback rather than adding a `drain` listener. Closing the transport rejects every send
1660
+ * whose callback has not settled.
1661
+ *
1662
+ * @param message - The message to serialize and write
1663
+ * @returns Resolves when the output confirms the write
1664
+ * @throws Thrown with `stdio transport is not connected` after the transport closes
1665
+ * @throws Thrown with the output callback error or synchronous write failure
1666
+ */
1610
1667
  async send(message) {
1611
- this.#output.write(`${JSON.stringify(message)}\n`);
1668
+ if (this.#closed) throw new Error("stdio transport is not connected");
1669
+ const pending = Promise.withResolvers();
1670
+ this.#pending.add(pending);
1671
+ try {
1672
+ await Promise.race([writeLine(this.#output, `${JSON.stringify(message)}\n`), pending.promise]);
1673
+ } finally {
1674
+ this.#pending.delete(pending);
1675
+ }
1612
1676
  }
1613
1677
  async close() {
1614
1678
  if (this.#closed) return;
@@ -1631,6 +1695,9 @@ var StdioServerTransport = class {
1631
1695
  this.#input.removeListener("data", this.#data);
1632
1696
  this.#input.removeListener("close", this.#ending);
1633
1697
  this.#input.removeListener("error", this.#failure);
1698
+ this.#output.removeListener("error", this.#failure);
1699
+ for (const pending of this.#pending) pending.reject(/* @__PURE__ */ new Error("stdio transport is not connected"));
1700
+ this.#pending.clear();
1634
1701
  if (!this.#flowing && this.#input.listenerCount("data") === 0) this.#input.pause();
1635
1702
  }
1636
1703
  };
@@ -2183,5 +2250,6 @@ exports.readSessionHeader = readSessionHeader;
2183
2250
  exports.rejectUnknownSession = rejectUnknownSession;
2184
2251
  exports.sendEventStream = sendEventStream;
2185
2252
  exports.upgradeRequestPath = upgradeRequestPath;
2253
+ exports.writeLine = writeLine;
2186
2254
 
2187
2255
  //# sourceMappingURL=index.cjs.map