mcp-proxy 2.8.1 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
+ }
@@ -2,13 +2,16 @@
2
2
 
3
3
  import yargs from "yargs";
4
4
  import { hideBin } from "yargs/helpers";
5
- import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
6
5
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
7
6
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
8
- import { proxyServer, startSSEServer } from "../MCPProxy.js";
9
7
  import { EventSource } from "eventsource";
10
8
  import { setTimeout } from "node:timers/promises";
11
- import multiline from 'multiline-ts';
9
+ import { StdioClientTransport } from "../StdioClientTransport.js";
10
+ import util from "node:util";
11
+ import { startSSEServer } from "../startSSEServer.js";
12
+ import { proxyServer } from "../proxyServer.js";
13
+
14
+ util.inspect.defaultOptions.depth = 8;
12
15
 
13
16
  if (!("EventSource" in global)) {
14
17
  // @ts-expect-error - figure out how to use --experimental-eventsource with vitest
@@ -48,85 +51,71 @@ const argv = await yargs(hideBin(process.argv))
48
51
  .help()
49
52
  .parseAsync();
50
53
 
51
- const transport = new StdioClientTransport({
52
- command: argv.command,
53
- args: argv.args,
54
- env: process.env as Record<string, string>,
55
- stderr: 'pipe',
56
- });
57
-
58
- const client = new Client(
59
- {
60
- name: "mcp-proxy",
61
- version: "1.0.0",
62
- },
63
- {
64
- capabilities: {},
65
- },
66
- );
67
-
68
-
69
- let stderrOutput = '';
70
-
71
- try {
72
- console.info('connecting to the MCP server...');
73
-
74
- const connectionPromise = client.connect(transport);
75
-
76
- transport?.stderr?.on('data', (chunk) => {
77
- stderrOutput += chunk.toString();
54
+ const connect = async (client: Client) => {
55
+ const transport = new StdioClientTransport({
56
+ command: argv.command,
57
+ args: argv.args,
58
+ env: process.env as Record<string, string>,
59
+ stderr: "pipe",
60
+ onEvent: (event) => {
61
+ console.debug("transport event", event);
62
+ },
78
63
  });
79
64
 
80
- await connectionPromise;
81
-
82
- console.info('connected to the MCP server');
83
- } catch (error) {
84
- console.error(multiline`
85
- could not connect to the MCP server
86
-
87
- --- error ---
88
- ${String(error)}
89
-
90
- --- stderr output ---
91
- ${stderrOutput}
92
- `);
65
+ await client.connect(transport);
66
+ };
93
67
 
94
- await setTimeout(1000);
68
+ const proxy = async () => {
69
+ const client = new Client(
70
+ {
71
+ name: "mcp-proxy",
72
+ version: "1.0.0",
73
+ },
74
+ {
75
+ capabilities: {},
76
+ },
77
+ );
95
78
 
96
- process.exit(1);
97
- }
79
+ await connect(client);
98
80
 
99
- const serverVersion = client.getServerVersion() as {
100
- name: string;
101
- version: string;
102
- };
81
+ const serverVersion = client.getServerVersion() as {
82
+ name: string;
83
+ version: string;
84
+ };
103
85
 
104
- const serverCapabilities = client.getServerCapabilities() as {};
86
+ const serverCapabilities = client.getServerCapabilities() as {};
105
87
 
106
- try {
107
- console.info('starting the SSE server on port %d', argv.port);
88
+ console.info("starting the SSE server on port %d", argv.port);
108
89
 
109
90
  await startSSEServer({
110
91
  createServer: async () => {
111
92
  const server = new Server(serverVersion, {
112
93
  capabilities: serverCapabilities,
113
94
  });
114
-
95
+
115
96
  proxyServer({
116
97
  server,
117
98
  client,
118
99
  serverCapabilities,
119
100
  });
120
-
101
+
121
102
  return server;
122
103
  },
123
104
  port: argv.port,
124
105
  endpoint: argv.endpoint as `/${string}`,
125
106
  });
126
- } catch (error) {
127
- console.error('could not start the SSE server', error);
107
+ };
128
108
 
129
- await setTimeout(1000);
109
+ const main = async () => {
110
+ try {
111
+ await proxy();
112
+ } catch (error) {
113
+ console.error("could not start the proxy", error);
130
114
 
131
- process.exit(1);
132
- }
115
+ await setTimeout(1000);
116
+
117
+ process.exit(1);
118
+ }
119
+ };
120
+
121
+ await main();
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export { tapTransport } from "./tapTransport.js";
2
+ export { proxyServer } from "./proxyServer.js";
3
+ export { startSSEServer } from "./startSSEServer.js";
@@ -0,0 +1,74 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import {
3
+ CallToolRequestSchema,
4
+ CompleteRequestSchema,
5
+ GetPromptRequestSchema,
6
+ ListPromptsRequestSchema,
7
+ ListResourcesRequestSchema,
8
+ ListResourceTemplatesRequestSchema,
9
+ ListToolsRequestSchema,
10
+ LoggingMessageNotificationSchema,
11
+ ReadResourceRequestSchema,
12
+ ServerCapabilities,
13
+ } from "@modelcontextprotocol/sdk/types.js";
14
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
15
+
16
+ export const proxyServer = async ({
17
+ server,
18
+ client,
19
+ serverCapabilities,
20
+ }: {
21
+ server: Server;
22
+ client: Client;
23
+ serverCapabilities: ServerCapabilities;
24
+ }) => {
25
+ if (serverCapabilities?.logging) {
26
+ server.setNotificationHandler(
27
+ LoggingMessageNotificationSchema,
28
+ async (args) => {
29
+ return client.notification(args);
30
+ },
31
+ );
32
+ }
33
+
34
+ if (serverCapabilities?.prompts) {
35
+ server.setRequestHandler(GetPromptRequestSchema, async (args) => {
36
+ return client.getPrompt(args.params);
37
+ });
38
+
39
+ server.setRequestHandler(ListPromptsRequestSchema, async (args) => {
40
+ return client.listPrompts(args.params);
41
+ });
42
+ }
43
+
44
+ if (serverCapabilities?.resources) {
45
+ server.setRequestHandler(ListResourcesRequestSchema, async (args) => {
46
+ return client.listResources(args.params);
47
+ });
48
+
49
+ server.setRequestHandler(
50
+ ListResourceTemplatesRequestSchema,
51
+ async (args) => {
52
+ return client.listResourceTemplates(args.params);
53
+ },
54
+ );
55
+
56
+ server.setRequestHandler(ReadResourceRequestSchema, async (args) => {
57
+ return client.readResource(args.params);
58
+ });
59
+ }
60
+
61
+ if (serverCapabilities?.tools) {
62
+ server.setRequestHandler(CallToolRequestSchema, async (args) => {
63
+ return client.callTool(args.params);
64
+ });
65
+
66
+ server.setRequestHandler(ListToolsRequestSchema, async (args) => {
67
+ return client.listTools(args.params);
68
+ });
69
+ }
70
+
71
+ server.setRequestHandler(CompleteRequestSchema, async (args) => {
72
+ return client.complete(args.params);
73
+ });
74
+ };
@@ -2,11 +2,12 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
2
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
3
3
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
4
  import { it, expect, vi } from "vitest";
5
- import { proxyServer, startSSEServer } from "./MCPProxy.js";
5
+ import { startSSEServer } from "./startSSEServer.js";
6
6
  import { getRandomPort } from "get-port-please";
7
7
  import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
8
8
  import { EventSource } from "eventsource";
9
9
  import { setTimeout as delay } from "node:timers/promises";
10
+ import { proxyServer } from "./proxyServer.js";
10
11
 
11
12
  if (!("EventSource" in global)) {
12
13
  // @ts-expect-error - figure out how to use --experimental-eventsource with vitest
@@ -1,169 +1,6 @@
1
1
  import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
2
2
  import http from "http";
3
3
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
- import {
5
- CallToolRequestSchema,
6
- CompleteRequestSchema,
7
- GetPromptRequestSchema,
8
- JSONRPCMessage,
9
- ListPromptsRequestSchema,
10
- ListResourcesRequestSchema,
11
- ListResourceTemplatesRequestSchema,
12
- ListToolsRequestSchema,
13
- LoggingMessageNotificationSchema,
14
- ReadResourceRequestSchema,
15
- ServerCapabilities,
16
- } from "@modelcontextprotocol/sdk/types.js";
17
- import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
18
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
19
-
20
- type TransportEvent =
21
- | {
22
- type: "close";
23
- }
24
- | {
25
- type: "onclose";
26
- }
27
- | {
28
- type: "onerror";
29
- error: Error;
30
- }
31
- | {
32
- type: "onmessage";
33
- message: JSONRPCMessage;
34
- }
35
- | {
36
- type: "send";
37
- message: JSONRPCMessage;
38
- }
39
- | {
40
- type: "start";
41
- };
42
-
43
- export const tapTransport = (
44
- transport: Transport,
45
- eventHandler: (event: TransportEvent) => void,
46
- ) => {
47
- const originalClose = transport.close.bind(transport);
48
- const originalOnClose = transport.onclose?.bind(transport);
49
- const originalOnError = transport.onerror?.bind(transport);
50
- const originalOnMessage = transport.onmessage?.bind(transport);
51
- const originalSend = transport.send.bind(transport);
52
- const originalStart = transport.start.bind(transport);
53
-
54
- transport.close = async () => {
55
- eventHandler({
56
- type: "close",
57
- });
58
-
59
- return originalClose?.();
60
- };
61
-
62
- transport.onclose = async () => {
63
- eventHandler({
64
- type: "onclose",
65
- });
66
-
67
- return originalOnClose?.();
68
- };
69
-
70
- transport.onerror = async (error: Error) => {
71
- eventHandler({
72
- type: "onerror",
73
- error,
74
- });
75
-
76
- return originalOnError?.(error);
77
- };
78
-
79
- transport.onmessage = async (message: JSONRPCMessage) => {
80
- eventHandler({
81
- type: "onmessage",
82
- message,
83
- });
84
-
85
- return originalOnMessage?.(message);
86
- };
87
-
88
- transport.send = async (message: JSONRPCMessage) => {
89
- eventHandler({
90
- type: "send",
91
- message,
92
- });
93
-
94
- return originalSend?.(message);
95
- };
96
-
97
- transport.start = async () => {
98
- eventHandler({
99
- type: "start",
100
- });
101
-
102
- return originalStart?.();
103
- };
104
-
105
- return transport;
106
- };
107
-
108
- export const proxyServer = async ({
109
- server,
110
- client,
111
- serverCapabilities,
112
- }: {
113
- server: Server;
114
- client: Client;
115
- serverCapabilities: ServerCapabilities;
116
- }) => {
117
- if (serverCapabilities?.logging) {
118
- server.setNotificationHandler(
119
- LoggingMessageNotificationSchema,
120
- async (args) => {
121
- return client.notification(args);
122
- },
123
- );
124
- }
125
-
126
- if (serverCapabilities?.prompts) {
127
- server.setRequestHandler(GetPromptRequestSchema, async (args) => {
128
- return client.getPrompt(args.params);
129
- });
130
-
131
- server.setRequestHandler(ListPromptsRequestSchema, async (args) => {
132
- return client.listPrompts(args.params);
133
- });
134
- }
135
-
136
- if (serverCapabilities?.resources) {
137
- server.setRequestHandler(ListResourcesRequestSchema, async (args) => {
138
- return client.listResources(args.params);
139
- });
140
-
141
- server.setRequestHandler(
142
- ListResourceTemplatesRequestSchema,
143
- async (args) => {
144
- return client.listResourceTemplates(args.params);
145
- },
146
- );
147
-
148
- server.setRequestHandler(ReadResourceRequestSchema, async (args) => {
149
- return client.readResource(args.params);
150
- });
151
- }
152
-
153
- if (serverCapabilities?.tools) {
154
- server.setRequestHandler(CallToolRequestSchema, async (args) => {
155
- return client.callTool(args.params);
156
- });
157
-
158
- server.setRequestHandler(ListToolsRequestSchema, async (args) => {
159
- return client.listTools(args.params);
160
- });
161
- }
162
-
163
- server.setRequestHandler(CompleteRequestSchema, async (args) => {
164
- return client.complete(args.params);
165
- });
166
- };
167
4
 
168
5
  export type SSEServer = {
169
6
  close: () => Promise<void>;
@@ -268,7 +105,7 @@ export const startSSEServer = async <T extends ServerLike>({
268
105
  } catch (error) {
269
106
  if (!closed) {
270
107
  console.error("Error connecting to server:", error);
271
-
108
+
272
109
  res.writeHead(500).end("Error connecting to server");
273
110
  }
274
111
  }