import { ChildProcess, spawn } from "node:child_process"; import { ReadBuffer, serializeMessage } from "../shared/stdio.js"; import { JSONRPCMessage } from "../types.js"; import { Transport } from "../shared/transport.js"; export type StdioServerParameters = { /** * The executable to run to start the server. */ command: string; /** * Command line arguments to pass to the executable. */ args?: string[]; /** * The environment to use when spawning the process. * * The environment is NOT inherited from the parent process by default. */ env?: object; }; /** * Client transport for stdio: this will connect to a server by spawning a process and communicating with it over stdin/stdout. * * This transport is only available in Node.js environments. */ export class StdioClientTransport implements Transport { private _process?: ChildProcess; private _abortController: AbortController = new AbortController(); private _readBuffer: ReadBuffer = new ReadBuffer(); onclose?: () => void; onerror?: (error: Error) => void; onmessage?: (message: JSONRPCMessage) => void; /** * Spawns the server process and prepare to communicate with it. */ spawn(server: StdioServerParameters): Promise { return new Promise((resolve, reject) => { this._process = spawn(server.command, server.args ?? [], { // The parent process may have sensitive secrets in its env, so don't inherit it automatically. env: server.env === undefined ? {} : { ...server.env }, stdio: ["pipe", "pipe", "inherit"], signal: this._abortController.signal, }); this._process.on("error", (error) => { if (error.name === "AbortError") { // Expected when close() is called. this.onclose?.(); return; } reject(error); this.onerror?.(error); }); this._process.on("spawn", () => { resolve(); }); this._process.on("close", (_code) => { this._process = undefined; this.onclose?.(); }); this._process.stdin?.on("error", (error) => { this.onerror?.(error); }); this._process.stdout?.on("data", (chunk) => { this._readBuffer.append(chunk); this.processReadBuffer(); }); this._process.stdout?.on("error", (error) => { this.onerror?.(error); }); }); } private processReadBuffer() { while (true) { try { const message = this._readBuffer.readMessage(); if (message === null) { break; } this.onmessage?.(message); } catch (error) { this.onerror?.(error as Error); } } } async close(): Promise { this._abortController.abort(); this._process = undefined; this._readBuffer.clear(); } send(message: JSONRPCMessage): Promise { return new Promise((resolve) => { if (!this._process?.stdin) { throw new Error("Not connected"); } const json = serializeMessage(message); if (this._process.stdin.write(json)) { resolve(); } else { this._process.stdin.once("drain", resolve); } }); } }