Skip to content

Commit 171ae9e

Browse files
committed
feat: add logs for every event
1 parent 8e727f0 commit 171ae9e

File tree

5 files changed

+235
-18
lines changed

5 files changed

+235
-18
lines changed

src/StdioClientTransport.ts

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
/**
2+
* Forked from https://github.com/modelcontextprotocol/typescript-sdk/blob/66e1508162d37c0b83b0637ebcd7f07946e3d210/src/client/stdio.ts#L90
3+
*/
4+
5+
import {
6+
ReadBuffer,
7+
serializeMessage,
8+
} from "@modelcontextprotocol/sdk/shared/stdio.js";
9+
import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
10+
import { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js";
11+
import { ChildProcess, IOType, spawn } from "node:child_process";
12+
import { Stream } from "node:stream";
13+
14+
type TransportEvent =
15+
| {
16+
type: "close";
17+
}
18+
| {
19+
type: "error";
20+
error: Error;
21+
}
22+
| {
23+
type: "data";
24+
chunk: string;
25+
}
26+
| {
27+
type: "message";
28+
message: JSONRPCMessage;
29+
};
30+
31+
export type StdioServerParameters = {
32+
/**
33+
* The executable to run to start the server.
34+
*/
35+
command: string;
36+
37+
/**
38+
* Command line arguments to pass to the executable.
39+
*/
40+
args?: string[];
41+
42+
/**
43+
* The environment to use when spawning the process.
44+
*
45+
* If not specified, the result of getDefaultEnvironment() will be used.
46+
*/
47+
env: Record<string, string>;
48+
49+
/**
50+
* How to handle stderr of the child process. This matches the semantics of Node's `child_process.spawn`.
51+
*
52+
* The default is "inherit", meaning messages to stderr will be printed to the parent process's stderr.
53+
*/
54+
stderr?: IOType | Stream | number;
55+
56+
/**
57+
* The working directory to use when spawning the process.
58+
*
59+
* If not specified, the current working directory will be inherited.
60+
*/
61+
cwd?: string;
62+
63+
/**
64+
* A function to call when an event occurs.
65+
*/
66+
onEvent?: (event: TransportEvent) => void;
67+
};
68+
69+
/**
70+
* Client transport for stdio: this will connect to a server by spawning a process and communicating with it over stdin/stdout.
71+
*
72+
* This transport is only available in Node.js environments.
73+
*/
74+
export class StdioClientTransport implements Transport {
75+
private process?: ChildProcess;
76+
private abortController: AbortController = new AbortController();
77+
private readBuffer: ReadBuffer = new ReadBuffer();
78+
private serverParams: StdioServerParameters;
79+
private onEvent?: (event: TransportEvent) => void;
80+
81+
onclose?: () => void;
82+
onerror?: (error: Error) => void;
83+
onmessage?: (message: JSONRPCMessage) => void;
84+
85+
constructor(server: StdioServerParameters) {
86+
this.serverParams = server;
87+
this.onEvent = server.onEvent;
88+
}
89+
90+
/**
91+
* Starts the server process and prepares to communicate with it.
92+
*/
93+
async start(): Promise<void> {
94+
if (this.process) {
95+
throw new Error(
96+
"StdioClientTransport already started! If using Client class, note that connect() calls start() automatically.",
97+
);
98+
}
99+
100+
return new Promise((resolve, reject) => {
101+
this.process = spawn(
102+
this.serverParams.command,
103+
this.serverParams.args ?? [],
104+
{
105+
env: this.serverParams.env,
106+
stdio: ["pipe", "pipe", this.serverParams.stderr ?? "inherit"],
107+
shell: false,
108+
signal: this.abortController.signal,
109+
cwd: this.serverParams.cwd,
110+
},
111+
);
112+
113+
this.process.on("error", (error) => {
114+
if (error.name === "AbortError") {
115+
// Expected when close() is called.
116+
this.onclose?.();
117+
return;
118+
}
119+
120+
reject(error);
121+
this.onerror?.(error);
122+
});
123+
124+
this.process.on("spawn", () => {
125+
resolve();
126+
});
127+
128+
this.process.on("close", (_code) => {
129+
this.onEvent?.({
130+
type: "close",
131+
});
132+
133+
this.process = undefined;
134+
this.onclose?.();
135+
});
136+
137+
this.process.stdin?.on("error", (error) => {
138+
this.onEvent?.({
139+
type: "error",
140+
error,
141+
});
142+
143+
this.onerror?.(error);
144+
});
145+
146+
this.process.stdout?.on("data", (chunk) => {
147+
this.onEvent?.({
148+
type: "data",
149+
chunk: chunk.toString(),
150+
});
151+
152+
this.readBuffer.append(chunk);
153+
this.processReadBuffer();
154+
});
155+
156+
this.process.stdout?.on("error", (error) => {
157+
this.onEvent?.({
158+
type: "error",
159+
error,
160+
});
161+
162+
this.onerror?.(error);
163+
});
164+
});
165+
}
166+
167+
/**
168+
* The stderr stream of the child process, if `StdioServerParameters.stderr` was set to "pipe" or "overlapped".
169+
*
170+
* This is only available after the process has been started.
171+
*/
172+
get stderr(): Stream | null {
173+
return this.process?.stderr ?? null;
174+
}
175+
176+
private processReadBuffer() {
177+
while (true) {
178+
try {
179+
const message = this.readBuffer.readMessage();
180+
181+
if (message === null) {
182+
break;
183+
}
184+
185+
this.onEvent?.({
186+
type: "message",
187+
message,
188+
});
189+
190+
this.onmessage?.(message);
191+
} catch (error) {
192+
this.onEvent?.({
193+
type: "error",
194+
error: error as Error,
195+
});
196+
197+
this.onerror?.(error as Error);
198+
}
199+
}
200+
}
201+
202+
async close(): Promise<void> {
203+
this.onEvent?.({
204+
type: "close",
205+
});
206+
207+
this.abortController.abort();
208+
this.process = undefined;
209+
this.readBuffer.clear();
210+
}
211+
212+
send(message: JSONRPCMessage): Promise<void> {
213+
return new Promise((resolve) => {
214+
if (!this.process?.stdin) {
215+
throw new Error("Not connected");
216+
}
217+
218+
const json = serializeMessage(message);
219+
if (this.process.stdin.write(json)) {
220+
resolve();
221+
} else {
222+
this.process.stdin.once("drain", resolve);
223+
}
224+
});
225+
}
226+
}

src/bin/mcp-proxy.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
66
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
77
import { EventSource } from "eventsource";
88
import { setTimeout } from "node:timers/promises";
9-
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
9+
import { StdioClientTransport } from "../StdioClientTransport.js";
1010
import util from "node:util";
1111
import { startSSEServer } from "../startSSEServer.js";
1212
import { proxyServer } from "../proxyServer.js";
@@ -57,13 +57,12 @@ const connect = async (client: Client) => {
5757
args: argv.args,
5858
env: process.env as Record<string, string>,
5959
stderr: "pipe",
60+
onEvent: (event) => {
61+
console.debug("transport event", event);
62+
},
6063
});
6164

62-
console.info("connecting to the MCP server...");
63-
6465
await client.connect(transport);
65-
66-
console.info("connected to the MCP server");
6766
};
6867

6968
const proxy = async () => {

src/index.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,3 @@
1-
export {
2-
tapTransport,
3-
} from './tapTransport.js';
4-
export {
5-
proxyServer,
6-
} from './proxyServer.js';
7-
export {
8-
startSSEServer,
9-
} from './startSSEServer.js';
1+
export { tapTransport } from "./tapTransport.js";
2+
export { proxyServer } from "./proxyServer.js";
3+
export { startSSEServer } from "./startSSEServer.js";

src/proxyServer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,4 @@ export const proxyServer = async ({
7171
server.setRequestHandler(CompleteRequestSchema, async (args) => {
7272
return client.complete(args.params);
7373
});
74-
};
74+
};

src/tapTransport.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,6 @@ type TransportEvent =
2424
type: "start";
2525
};
2626

27-
28-
2927
export const tapTransport = (
3028
transport: Transport,
3129
eventHandler: (event: TransportEvent) => void,
@@ -89,4 +87,4 @@ export const tapTransport = (
8987
};
9088

9189
return transport;
92-
};
90+
};

0 commit comments

Comments
 (0)