git-subtree-dir: packages/mcp-typescript git-subtree-split: 2cc7dd104307d48bab8d27760f16c63c119d8a88
122 lines
3.1 KiB
TypeScript
122 lines
3.1 KiB
TypeScript
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<void> {
|
|
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<void> {
|
|
this._abortController.abort();
|
|
this._process = undefined;
|
|
this._readBuffer.clear();
|
|
}
|
|
|
|
send(message: JSONRPCMessage): Promise<void> {
|
|
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);
|
|
}
|
|
});
|
|
}
|
|
}
|