mcp-proxy 2.12.2 → 2.13.1

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.
@@ -1,8 +1,8 @@
1
1
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
3
  import {
4
- ListResourceTemplatesRequestSchema,
5
4
  ListResourcesRequestSchema,
5
+ ListResourceTemplatesRequestSchema,
6
6
  ReadResourceRequestSchema,
7
7
  SubscribeRequestSchema,
8
8
  UnsubscribeRequestSchema,
@@ -24,8 +24,8 @@ server.setRequestHandler(ListResourcesRequestSchema, async () => {
24
24
  return {
25
25
  resources: [
26
26
  {
27
- uri: "file:///example.txt",
28
27
  name: "Example Resource",
28
+ uri: "file:///example.txt",
29
29
  },
30
30
  ],
31
31
  };
@@ -36,9 +36,9 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
36
36
  return {
37
37
  contents: [
38
38
  {
39
- uri: "file:///example.txt",
40
39
  mimeType: "text/plain",
41
40
  text: "This is the content of the example resource.",
41
+ uri: "file:///example.txt",
42
42
  },
43
43
  ],
44
44
  };
@@ -51,9 +51,9 @@ server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
51
51
  return {
52
52
  resourceTemplates: [
53
53
  {
54
- uriTemplate: `file://{filename}`,
55
- name: "Example resource template",
56
54
  description: "Specify the filename to retrieve",
55
+ name: "Example resource template",
56
+ uriTemplate: `file://{filename}`,
57
57
  },
58
58
  ],
59
59
  };
@@ -0,0 +1,130 @@
1
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
3
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
4
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
5
+ import { EventSource } from "eventsource";
6
+ import { getRandomPort } from "get-port-please";
7
+ import { setTimeout as delay } from "node:timers/promises";
8
+ import { expect, it, vi } from "vitest";
9
+
10
+ import { proxyServer } from "./proxyServer.js";
11
+ import { startHTTPStreamServer } from "./startHTTPStreamServer.js";
12
+
13
+ if (!("EventSource" in global)) {
14
+ // @ts-expect-error - figure out how to use --experimental-eventsource with vitest
15
+ global.EventSource = EventSource;
16
+ }
17
+
18
+ it("proxies messages between HTTP stream and stdio servers", async () => {
19
+ const stdioTransport = new StdioClientTransport({
20
+ args: ["src/simple-stdio-server.ts"],
21
+ command: "tsx",
22
+ });
23
+
24
+ const stdioClient = new Client(
25
+ {
26
+ name: "mcp-proxy",
27
+ version: "1.0.0",
28
+ },
29
+ {
30
+ capabilities: {},
31
+ },
32
+ );
33
+
34
+ await stdioClient.connect(stdioTransport);
35
+
36
+ const serverVersion = stdioClient.getServerVersion() as {
37
+ name: string;
38
+ version: string;
39
+ };
40
+
41
+ const serverCapabilities = stdioClient.getServerCapabilities() as {
42
+ capabilities: Record<string, unknown>;
43
+ };
44
+
45
+ const port = await getRandomPort();
46
+
47
+ const onConnect = vi.fn();
48
+ const onClose = vi.fn();
49
+
50
+ await startHTTPStreamServer({
51
+ createServer: async () => {
52
+ const mcpServer = new Server(serverVersion, {
53
+ capabilities: serverCapabilities,
54
+ });
55
+
56
+ await proxyServer({
57
+ client: stdioClient,
58
+ server: mcpServer,
59
+ serverCapabilities,
60
+ });
61
+
62
+ return mcpServer;
63
+ },
64
+ endpoint: "/stream",
65
+ onClose,
66
+ onConnect,
67
+ port,
68
+ });
69
+
70
+ const streamClient = new Client(
71
+ {
72
+ name: "stream-client",
73
+ version: "1.0.0",
74
+ },
75
+ {
76
+ capabilities: {},
77
+ },
78
+ );
79
+
80
+ const transport = new StreamableHTTPClientTransport(
81
+ new URL(`http://localhost:${port}/stream`),
82
+ );
83
+
84
+ await streamClient.connect(transport);
85
+
86
+ const result = await streamClient.listResources();
87
+ expect(result).toEqual({
88
+ resources: [
89
+ {
90
+ name: "Example Resource",
91
+ uri: "file:///example.txt",
92
+ },
93
+ ],
94
+ });
95
+
96
+ expect(
97
+ await streamClient.readResource({ uri: result.resources[0].uri }, {}),
98
+ ).toEqual({
99
+ contents: [
100
+ {
101
+ mimeType: "text/plain",
102
+ text: "This is the content of the example resource.",
103
+ uri: "file:///example.txt",
104
+ },
105
+ ],
106
+ });
107
+ expect(await streamClient.subscribeResource({ uri: "xyz" })).toEqual({});
108
+ expect(await streamClient.unsubscribeResource({ uri: "xyz" })).toEqual({});
109
+ expect(await streamClient.listResourceTemplates()).toEqual({
110
+ resourceTemplates: [
111
+ {
112
+ description: "Specify the filename to retrieve",
113
+ name: "Example resource template",
114
+ uriTemplate: `file://{filename}`,
115
+ },
116
+ ],
117
+ });
118
+
119
+ expect(onConnect).toHaveBeenCalled();
120
+ expect(onClose).not.toHaveBeenCalled();
121
+
122
+ // the transport no requires the function terminateSession to be called but the client does not implement it
123
+ // so we need to call it manually
124
+ await transport.terminateSession();
125
+ await streamClient.close();
126
+
127
+ await delay(1000);
128
+
129
+ expect(onClose).toHaveBeenCalled();
130
+ });
@@ -0,0 +1,281 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import {
3
+ EventStore,
4
+ StreamableHTTPServerTransport,
5
+ } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
6
+ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
7
+ import http from "http";
8
+ import { randomUUID } from "node:crypto";
9
+
10
+ import { InMemoryEventStore } from "./InMemoryEventStore.js";
11
+
12
+ export type SSEServer = {
13
+ close: () => Promise<void>;
14
+ };
15
+
16
+ type ServerLike = {
17
+ close: Server["close"];
18
+ connect: Server["connect"];
19
+ };
20
+
21
+ export const startHTTPStreamServer = async <T extends ServerLike>({
22
+ createServer,
23
+ endpoint,
24
+ eventStore,
25
+ onClose,
26
+ onConnect,
27
+ onUnhandledRequest,
28
+ port,
29
+ }: {
30
+ createServer: (request: http.IncomingMessage) => Promise<T>;
31
+ endpoint: string;
32
+ eventStore?: EventStore;
33
+ onClose?: (server: T) => void;
34
+ onConnect?: (server: T) => void;
35
+ onUnhandledRequest?: (
36
+ req: http.IncomingMessage,
37
+ res: http.ServerResponse,
38
+ ) => Promise<void>;
39
+ port: number;
40
+ }): Promise<SSEServer> => {
41
+ const activeTransports: Record<
42
+ string,
43
+ {
44
+ server: T;
45
+ transport: StreamableHTTPServerTransport;
46
+ }
47
+ > = {};
48
+
49
+ /**
50
+ * @author https://dev.classmethod.jp/articles/mcp-sse/
51
+ */
52
+ const httpServer = http.createServer(async (req, res) => {
53
+ if (req.headers.origin) {
54
+ try {
55
+ const origin = new URL(req.headers.origin);
56
+
57
+ res.setHeader("Access-Control-Allow-Origin", origin.origin);
58
+ res.setHeader("Access-Control-Allow-Credentials", "true");
59
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
60
+ res.setHeader("Access-Control-Allow-Headers", "*");
61
+ } catch (error) {
62
+ console.error("Error parsing origin:", error);
63
+ }
64
+ }
65
+
66
+ if (req.method === "OPTIONS") {
67
+ res.writeHead(204);
68
+ res.end();
69
+ return;
70
+ }
71
+
72
+ if (req.method === "GET" && req.url === `/ping`) {
73
+ res.writeHead(200).end("pong");
74
+ return;
75
+ }
76
+
77
+ if (
78
+ req.method === "POST" &&
79
+ new URL(req.url!, "http://localhost").pathname === endpoint
80
+ ) {
81
+ try {
82
+ const sessionId = Array.isArray(req.headers["mcp-session-id"])
83
+ ? req.headers["mcp-session-id"][0]
84
+ : req.headers["mcp-session-id"];
85
+ let transport: StreamableHTTPServerTransport;
86
+ let server: T;
87
+
88
+ const body = await getBody(req);
89
+
90
+ if (sessionId && activeTransports[sessionId]) {
91
+ transport = activeTransports[sessionId].transport;
92
+ server = activeTransports[sessionId].server;
93
+ } else if (!sessionId && isInitializeRequest(body)) {
94
+ // Create a new transport for the session
95
+ transport = new StreamableHTTPServerTransport({
96
+ eventStore: eventStore || new InMemoryEventStore(),
97
+ onsessioninitialized: (_sessionId) => {
98
+ // add only when the id Sesison id is generated
99
+ activeTransports[_sessionId] = {
100
+ server,
101
+ transport,
102
+ };
103
+ },
104
+ sessionIdGenerator: randomUUID,
105
+ });
106
+
107
+ // Handle the server close event
108
+ transport.onclose = async () => {
109
+ const sid = transport.sessionId;
110
+ if (sid && activeTransports[sid]) {
111
+ onClose?.(server);
112
+ try {
113
+ await server.close();
114
+ } catch (error) {
115
+ console.error("Error closing server:", error);
116
+ }
117
+ delete activeTransports[sid];
118
+ }
119
+ };
120
+
121
+ // Create the server
122
+ try {
123
+ server = await createServer(req);
124
+ } catch (error) {
125
+ if (error instanceof Response) {
126
+ res.writeHead(error.status).end(error.statusText);
127
+ return;
128
+ }
129
+ res.writeHead(500).end("Error creating server");
130
+ return;
131
+ }
132
+
133
+ server.connect(transport);
134
+ onConnect?.(server);
135
+
136
+ await transport.handleRequest(req, res, body);
137
+ return;
138
+ } else {
139
+ // Error if the server is not created but the request is not an initialize request
140
+ res.setHeader("Content-Type", "application/json");
141
+ res.writeHead(400).end(
142
+ JSON.stringify({
143
+ error: {
144
+ code: -32000,
145
+ message: "Bad Request: No valid session ID provided",
146
+ },
147
+ id: null,
148
+ jsonrpc: "2.0",
149
+ }),
150
+ );
151
+
152
+ return;
153
+ }
154
+
155
+ // Handle ther request if the server is already created
156
+ await transport.handleRequest(req, res, body);
157
+ } catch (error) {
158
+ console.error("Error handling request:", error);
159
+ res.setHeader("Content-Type", "application/json");
160
+ res.writeHead(500).end(
161
+ JSON.stringify({
162
+ error: { code: -32603, message: "Internal Server Error" },
163
+ id: null,
164
+ jsonrpc: "2.0",
165
+ }),
166
+ );
167
+ }
168
+ return;
169
+ }
170
+
171
+ if (
172
+ req.method === "GET" &&
173
+ new URL(req.url!, "http://localhost").pathname === endpoint
174
+ ) {
175
+ const sessionId = req.headers["mcp-session-id"] as string | undefined;
176
+ const activeTransport:
177
+ | {
178
+ server: T;
179
+ transport: StreamableHTTPServerTransport;
180
+ }
181
+ | undefined = sessionId ? activeTransports[sessionId] : undefined;
182
+
183
+ if (!sessionId) {
184
+ res.writeHead(400).end("No sessionId");
185
+ return;
186
+ }
187
+
188
+ if (!activeTransport) {
189
+ res.writeHead(400).end("No active transport");
190
+ return;
191
+ }
192
+
193
+ const lastEventId = req.headers["last-event-id"] as string | undefined;
194
+ if (lastEventId) {
195
+ console.log(`Client reconnecting with Last-Event-ID: ${lastEventId}`);
196
+ } else {
197
+ console.log(`Establishing new SSE stream for session ${sessionId}`);
198
+ }
199
+
200
+ await activeTransport.transport.handleRequest(req, res);
201
+ return;
202
+ }
203
+
204
+ if (
205
+ req.method === "DELETE" &&
206
+ new URL(req.url!, "http://localhost").pathname === endpoint
207
+ ) {
208
+ console.log("received delete request");
209
+ const sessionId = req.headers["mcp-session-id"] as string | undefined;
210
+ if (!sessionId) {
211
+ res.writeHead(400).end("Invalid or missing sessionId");
212
+ return;
213
+ }
214
+
215
+ console.log("received delete request for session", sessionId);
216
+
217
+ const { server, transport } = activeTransports[sessionId];
218
+ if (!transport) {
219
+ res.writeHead(400).end("No active transport");
220
+ return;
221
+ }
222
+
223
+ try {
224
+ await transport.handleRequest(req, res);
225
+ onClose?.(server);
226
+ } catch (error) {
227
+ console.error("Error handling delete request:", error);
228
+ res.writeHead(500).end("Error handling delete request");
229
+ }
230
+
231
+ return;
232
+ }
233
+
234
+ if (onUnhandledRequest) {
235
+ await onUnhandledRequest(req, res);
236
+ } else {
237
+ res.writeHead(404).end();
238
+ }
239
+ });
240
+
241
+ await new Promise((resolve) => {
242
+ httpServer.listen(port, "::", () => {
243
+ resolve(undefined);
244
+ });
245
+ });
246
+
247
+ return {
248
+ close: async () => {
249
+ for (const transport of Object.values(activeTransports)) {
250
+ await transport.transport.close();
251
+ }
252
+
253
+ return new Promise((resolve, reject) => {
254
+ httpServer.close((error) => {
255
+ if (error) {
256
+ reject(error);
257
+
258
+ return;
259
+ }
260
+
261
+ resolve();
262
+ });
263
+ });
264
+ },
265
+ };
266
+ };
267
+
268
+ function getBody(request: http.IncomingMessage) {
269
+ return new Promise((resolve) => {
270
+ const bodyParts: Buffer[] = [];
271
+ let body: string;
272
+ request
273
+ .on("data", (chunk) => {
274
+ bodyParts.push(chunk);
275
+ })
276
+ .on("end", () => {
277
+ body = Buffer.concat(bodyParts).toString();
278
+ resolve(JSON.parse(body));
279
+ });
280
+ });
281
+ }
@@ -1,13 +1,14 @@
1
1
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
+ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
2
3
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
3
4
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
- import { it, expect, vi } from "vitest";
5
- import { startSSEServer } from "./startSSEServer.js";
6
- import { getRandomPort } from "get-port-please";
7
- import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
8
5
  import { EventSource } from "eventsource";
6
+ import { getRandomPort } from "get-port-please";
9
7
  import { setTimeout as delay } from "node:timers/promises";
8
+ import { expect, it, vi } from "vitest";
9
+
10
10
  import { proxyServer } from "./proxyServer.js";
11
+ import { startSSEServer } from "./startSSEServer.js";
11
12
 
12
13
  if (!("EventSource" in global)) {
13
14
  // @ts-expect-error - figure out how to use --experimental-eventsource with vitest
@@ -16,8 +17,8 @@ if (!("EventSource" in global)) {
16
17
 
17
18
  it("proxies messages between SSE and stdio servers", async () => {
18
19
  const stdioTransport = new StdioClientTransport({
19
- command: "tsx",
20
20
  args: ["src/simple-stdio-server.ts"],
21
+ command: "tsx",
21
22
  });
22
23
 
23
24
  const stdioClient = new Client(
@@ -37,7 +38,9 @@ it("proxies messages between SSE and stdio servers", async () => {
37
38
  version: string;
38
39
  };
39
40
 
40
- const serverCapabilities = stdioClient.getServerCapabilities() as {};
41
+ const serverCapabilities = stdioClient.getServerCapabilities() as {
42
+ capabilities: Record<string, unknown>;
43
+ };
41
44
 
42
45
  const port = await getRandomPort();
43
46
 
@@ -51,17 +54,17 @@ it("proxies messages between SSE and stdio servers", async () => {
51
54
  });
52
55
 
53
56
  await proxyServer({
54
- server: mcpServer,
55
57
  client: stdioClient,
58
+ server: mcpServer,
56
59
  serverCapabilities,
57
60
  });
58
61
 
59
62
  return mcpServer;
60
63
  },
61
- port,
62
64
  endpoint: "/sse",
63
- onConnect,
64
65
  onClose,
66
+ onConnect,
67
+ port,
65
68
  });
66
69
 
67
70
  const sseClient = new Client(
@@ -84,18 +87,20 @@ it("proxies messages between SSE and stdio servers", async () => {
84
87
  expect(result).toEqual({
85
88
  resources: [
86
89
  {
87
- uri: "file:///example.txt",
88
90
  name: "Example Resource",
91
+ uri: "file:///example.txt",
89
92
  },
90
93
  ],
91
94
  });
92
95
 
93
- expect(await sseClient.readResource({uri: (result.resources[0].uri)},{})).toEqual({
96
+ expect(
97
+ await sseClient.readResource({ uri: result.resources[0].uri }, {}),
98
+ ).toEqual({
94
99
  contents: [
95
100
  {
96
- uri: "file:///example.txt",
97
101
  mimeType: "text/plain",
98
102
  text: "This is the content of the example resource.",
103
+ uri: "file:///example.txt",
99
104
  },
100
105
  ],
101
106
  });
@@ -104,14 +109,13 @@ it("proxies messages between SSE and stdio servers", async () => {
104
109
  expect(await sseClient.listResourceTemplates()).toEqual({
105
110
  resourceTemplates: [
106
111
  {
107
- uriTemplate: `file://{filename}`,
108
- name: "Example resource template",
109
112
  description: "Specify the filename to retrieve",
113
+ name: "Example resource template",
114
+ uriTemplate: `file://{filename}`,
110
115
  },
111
116
  ],
112
117
  });
113
118
 
114
-
115
119
  expect(onConnect).toHaveBeenCalled();
116
120
  expect(onClose).not.toHaveBeenCalled();
117
121
 
@@ -1,33 +1,33 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
1
2
  import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
2
3
  import http from "http";
3
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
4
 
5
5
  export type SSEServer = {
6
6
  close: () => Promise<void>;
7
7
  };
8
8
 
9
9
  type ServerLike = {
10
- connect: Server["connect"];
11
10
  close: Server["close"];
11
+ connect: Server["connect"];
12
12
  };
13
13
 
14
14
  export const startSSEServer = async <T extends ServerLike>({
15
- port,
16
15
  createServer,
17
16
  endpoint,
18
- onConnect,
19
17
  onClose,
18
+ onConnect,
20
19
  onUnhandledRequest,
20
+ port,
21
21
  }: {
22
- port: number;
23
- endpoint: string;
24
22
  createServer: (request: http.IncomingMessage) => Promise<T>;
25
- onConnect?: (server: T) => void;
23
+ endpoint: string;
26
24
  onClose?: (server: T) => void;
25
+ onConnect?: (server: T) => void;
27
26
  onUnhandledRequest?: (
28
27
  req: http.IncomingMessage,
29
28
  res: http.ServerResponse,
30
29
  ) => Promise<void>;
30
+ port: number;
31
31
  }): Promise<SSEServer> => {
32
32
  const activeTransports: Record<string, SSEServerTransport> = {};
33
33
 
@@ -54,13 +54,21 @@ export const startSSEServer = async <T extends ServerLike>({
54
54
  return;
55
55
  }
56
56
 
57
+ if (req.method === "GET" && req.url === "/health") {
58
+ res.writeHead(200, { "Content-Type": "text/plain" }).end("OK");
59
+ return;
60
+ }
61
+
57
62
  if (req.method === "GET" && req.url === `/ping`) {
58
63
  res.writeHead(200).end("pong");
59
64
 
60
65
  return;
61
66
  }
62
67
 
63
- if (req.method === "GET" && new URL(req.url!, "http://localhost").pathname === endpoint) {
68
+ if (
69
+ req.method === "GET" &&
70
+ new URL(req.url!, "http://localhost").pathname === endpoint
71
+ ) {
64
72
  const transport = new SSEServerTransport("/messages", res);
65
73
 
66
74
  let server: T;
@@ -3,22 +3,22 @@ import { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js";
3
3
 
4
4
  type TransportEvent =
5
5
  | {
6
- type: "close";
6
+ error: Error;
7
+ type: "onerror";
7
8
  }
8
9
  | {
9
- type: "onclose";
10
+ message: JSONRPCMessage;
11
+ type: "onmessage";
10
12
  }
11
13
  | {
12
- type: "onerror";
13
- error: Error;
14
+ message: JSONRPCMessage;
15
+ type: "send";
14
16
  }
15
17
  | {
16
- type: "onmessage";
17
- message: JSONRPCMessage;
18
+ type: "close";
18
19
  }
19
20
  | {
20
- type: "send";
21
- message: JSONRPCMessage;
21
+ type: "onclose";
22
22
  }
23
23
  | {
24
24
  type: "start";
@@ -53,8 +53,8 @@ export const tapTransport = (
53
53
 
54
54
  transport.onerror = async (error: Error) => {
55
55
  eventHandler({
56
- type: "onerror",
57
56
  error,
57
+ type: "onerror",
58
58
  });
59
59
 
60
60
  return originalOnError?.(error);
@@ -62,8 +62,8 @@ export const tapTransport = (
62
62
 
63
63
  transport.onmessage = async (message: JSONRPCMessage) => {
64
64
  eventHandler({
65
- type: "onmessage",
66
65
  message,
66
+ type: "onmessage",
67
67
  });
68
68
 
69
69
  return originalOnMessage?.(message);
@@ -71,8 +71,8 @@ export const tapTransport = (
71
71
 
72
72
  transport.send = async (message: JSONRPCMessage) => {
73
73
  eventHandler({
74
- type: "send",
75
74
  message,
75
+ type: "send",
76
76
  });
77
77
 
78
78
  return originalSend?.(message);