






















@@ -0,0 +1,140 @@
1+import { spawn, type ChildProcess } from "node:child_process";
2+import process from "node:process";
3+import { PassThrough } from "node:stream";
4+import { getDefaultEnvironment } from "@modelcontextprotocol/sdk/client/stdio.js";
5+import { ReadBuffer, serializeMessage } from "@modelcontextprotocol/sdk/shared/stdio.js";
6+import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
7+import type { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js";
8+import { killProcessTree } from "../process/kill-tree.js";
9+10+export type OpenClawStdioServerParameters = {
11+command: string;
12+args?: string[];
13+env?: Record<string, string>;
14+cwd?: string;
15+stderr?: "pipe" | "overlapped" | "inherit" | "ignore";
16+};
17+18+const CLOSE_TIMEOUT_MS = 2000;
19+20+function delay(ms: number) {
21+return new Promise<void>((resolve) => {
22+setTimeout(resolve, ms).unref();
23+});
24+}
25+26+export class OpenClawStdioClientTransport implements Transport {
27+onclose?: () => void;
28+onerror?: (error: Error) => void;
29+onmessage?: (message: JSONRPCMessage) => void;
30+31+private readonly readBuffer = new ReadBuffer();
32+private readonly stderrStream: PassThrough | null = null;
33+private process?: ChildProcess;
34+35+constructor(private readonly serverParams: OpenClawStdioServerParameters) {
36+if (serverParams.stderr === "pipe" || serverParams.stderr === "overlapped") {
37+this.stderrStream = new PassThrough();
38+}
39+}
40+41+async start(): Promise<void> {
42+if (this.process) {
43+throw new Error(
44+"OpenClawStdioClientTransport already started; Client.connect() starts transports automatically.",
45+);
46+}
47+48+await new Promise<void>((resolve, reject) => {
49+const child = spawn(this.serverParams.command, this.serverParams.args ?? [], {
50+cwd: this.serverParams.cwd,
51+detached: process.platform !== "win32",
52+env: {
53+ ...getDefaultEnvironment(),
54+ ...this.serverParams.env,
55+},
56+shell: false,
57+stdio: ["pipe", "pipe", this.serverParams.stderr ?? "inherit"],
58+windowsHide: process.platform === "win32",
59+});
60+this.process = child;
61+62+child.on("error", (error: Error) => {
63+reject(error);
64+this.onerror?.(error);
65+});
66+child.on("spawn", () => resolve());
67+child.on("close", () => {
68+this.process = undefined;
69+this.onclose?.();
70+});
71+child.stdin?.on("error", (error: Error) => this.onerror?.(error));
72+child.stdout?.on("data", (chunk: Buffer) => {
73+this.readBuffer.append(chunk);
74+this.processReadBuffer();
75+});
76+child.stdout?.on("error", (error: Error) => this.onerror?.(error));
77+if (this.stderrStream && child.stderr) {
78+child.stderr.pipe(this.stderrStream);
79+}
80+});
81+}
82+83+get stderr() {
84+return this.stderrStream ?? this.process?.stderr ?? null;
85+}
86+87+get pid() {
88+return this.process?.pid ?? null;
89+}
90+91+private processReadBuffer() {
92+while (true) {
93+try {
94+const message = this.readBuffer.readMessage();
95+if (message === null) {
96+break;
97+}
98+this.onmessage?.(message);
99+} catch (error) {
100+this.onerror?.(error instanceof Error ? error : new Error(String(error)));
101+}
102+}
103+}
104+105+async close(): Promise<void> {
106+const processToClose = this.process;
107+this.process = undefined;
108+if (processToClose) {
109+const closePromise = new Promise<void>((resolve) => {
110+processToClose.once("close", () => resolve());
111+});
112+try {
113+processToClose.stdin?.end();
114+} catch {
115+// best-effort
116+}
117+await Promise.race([closePromise, delay(CLOSE_TIMEOUT_MS)]);
118+if (processToClose.exitCode === null && processToClose.pid) {
119+killProcessTree(processToClose.pid);
120+await Promise.race([closePromise, delay(CLOSE_TIMEOUT_MS)]);
121+}
122+}
123+this.readBuffer.clear();
124+}
125+126+send(message: JSONRPCMessage): Promise<void> {
127+return new Promise((resolve) => {
128+const stdin = this.process?.stdin;
129+if (!stdin) {
130+throw new Error("Not connected");
131+}
132+const json = serializeMessage(message);
133+if (stdin.write(json)) {
134+resolve();
135+} else {
136+stdin.once("drain", resolve);
137+}
138+});
139+}
140+}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。