diff --git a/src/client/stdio.ts b/src/client/stdio.ts index b83bf27c..9e35293d 100644 --- a/src/client/stdio.ts +++ b/src/client/stdio.ts @@ -1,7 +1,7 @@ import { ChildProcess, IOType } from "node:child_process"; import spawn from "cross-spawn"; import process from "node:process"; -import { Stream } from "node:stream"; +import { Stream, PassThrough } from "node:stream"; import { ReadBuffer, serializeMessage } from "../shared/stdio.js"; import { Transport } from "../shared/transport.js"; import { JSONRPCMessage } from "../types.js"; @@ -93,6 +93,7 @@ export class StdioClientTransport implements Transport { private _abortController: AbortController = new AbortController(); private _readBuffer: ReadBuffer = new ReadBuffer(); private _serverParams: StdioServerParameters; + private _stderrStream: PassThrough | null = null; onclose?: () => void; onerror?: (error: Error) => void; @@ -100,6 +101,9 @@ export class StdioClientTransport implements Transport { constructor(server: StdioServerParameters) { this._serverParams = server; + if (server.stderr === "pipe" || server.stderr === "overlapped") { + this._stderrStream = new PassThrough(); + } } /** @@ -158,15 +162,25 @@ export class StdioClientTransport implements Transport { this._process.stdout?.on("error", (error) => { this.onerror?.(error); }); + + if (this._stderrStream && this._process.stderr) { + this._process.stderr.pipe(this._stderrStream); + } }); } /** * The stderr stream of the child process, if `StdioServerParameters.stderr` was set to "pipe" or "overlapped". * - * This is only available after the process has been started. + * If stderr piping was requested, a PassThrough stream is returned _immediately_, allowing callers to + * attach listeners before the start method is invoked. This prevents loss of any early + * error output emitted by the child process. */ get stderr(): Stream | null { + if (this._stderrStream) { + return this._stderrStream; + } + return this._process?.stderr ?? null; }