mcp-proxy 1.2.0 → 1.3.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.
- package/dist/MCPProxy.d.ts +4 -1
- package/dist/MCPProxy.js +1 -1
- package/dist/bin/mcp-proxy.js +1 -1
- package/dist/bin/mcp-proxy.js.map +1 -1
- package/dist/{chunk-XJ6D3A72.js → chunk-3TY6Q6HO.js} +19 -4
- package/dist/chunk-3TY6Q6HO.js.map +1 -0
- package/package.json +1 -1
- package/src/MCPProxy.test.ts +19 -4
- package/src/MCPProxy.ts +24 -2
- package/src/bin/mcp-proxy.ts +0 -1
- package/dist/chunk-XJ6D3A72.js.map +0 -1
package/dist/MCPProxy.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
|
1
2
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
3
|
import { ServerCapabilities, JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js';
|
|
3
4
|
import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
|
@@ -28,10 +29,12 @@ declare const proxyServer: ({ server, client, serverCapabilities, }: {
|
|
|
28
29
|
type SSEServer = {
|
|
29
30
|
close: () => Promise<void>;
|
|
30
31
|
};
|
|
31
|
-
declare const startSSEServer: ({ port, server, endpoint, }: {
|
|
32
|
+
declare const startSSEServer: ({ port, server, endpoint, onConnect, onClose, }: {
|
|
32
33
|
port: number;
|
|
33
34
|
endpoint: string;
|
|
34
35
|
server: Server;
|
|
36
|
+
onConnect?: (transport: SSEServerTransport) => void;
|
|
37
|
+
onClose?: (transport: SSEServerTransport) => void;
|
|
35
38
|
}) => Promise<SSEServer>;
|
|
36
39
|
|
|
37
40
|
export { type SSEServer, proxyServer, startSSEServer, tapTransport };
|
package/dist/MCPProxy.js
CHANGED
package/dist/bin/mcp-proxy.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/bin/mcp-proxy.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { EventSource } from \"eventsource\";\n\n// @ts-expect-error\nglobal.EventSource = EventSource;\n\nimport yargs from \"yargs\";\nimport { hideBin } from \"yargs/helpers\";\nimport { StdioClientTransport } from \"@modelcontextprotocol/sdk/client/stdio.js\";\nimport { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport { proxyServer, startSSEServer } from \"../MCPProxy.js\";\n\
|
|
1
|
+
{"version":3,"sources":["../../src/bin/mcp-proxy.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { EventSource } from \"eventsource\";\n\n// @ts-expect-error\nglobal.EventSource = EventSource;\n\nimport yargs from \"yargs\";\nimport { hideBin } from \"yargs/helpers\";\nimport { StdioClientTransport } from \"@modelcontextprotocol/sdk/client/stdio.js\";\nimport { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport { proxyServer, startSSEServer } from \"../MCPProxy.js\";\n\nconst argv = await yargs(hideBin(process.argv))\n .scriptName(\"mcp-proxy\")\n .command(\"$0 <command> [args...]\", \"Run a command with MCP arguments\")\n .positional(\"command\", {\n type: \"string\",\n describe: \"The command to run\",\n demandOption: true,\n })\n .positional(\"args\", {\n type: \"string\",\n array: true,\n describe: \"The arguments to pass to the command\",\n })\n .options({\n debug: {\n type: \"boolean\",\n describe: \"Enable debug logging\",\n default: false,\n },\n endpoint: {\n type: \"string\",\n describe: \"The endpoint to listen on for SSE\",\n default: \"/sse\",\n },\n port: {\n type: \"number\",\n describe: \"The port to listen on for SSE\",\n default: 8080,\n },\n })\n .help()\n .parseAsync();\n\nconst transport = new StdioClientTransport({\n command: argv.command,\n args: argv.args,\n env: process.env as Record<string, string>,\n});\n\nconst client = new Client(\n {\n name: \"mcp-proxy\",\n version: \"1.0.0\",\n },\n {\n capabilities: {},\n },\n);\n\nawait client.connect(transport);\n\nconst serverVersion = client.getServerVersion() as {\n name: string;\n version: string;\n};\n\nconst serverCapabilities = client.getServerCapabilities() as {};\n\nconst server = new Server(serverVersion, {\n capabilities: serverCapabilities,\n});\n\nproxyServer({\n server,\n client,\n serverCapabilities,\n});\n\nawait startSSEServer({\n server,\n port: argv.port,\n endpoint: argv.endpoint as `/${string}`,\n});\n"],"mappings":";;;;;;;AAEA,SAAS,mBAAmB;AAK5B,OAAO,WAAW;AAClB,SAAS,eAAe;AACxB,SAAS,4BAA4B;AACrC,SAAS,cAAc;AACvB,SAAS,cAAc;AANvB,OAAO,cAAc;AASrB,IAAM,OAAO,MAAM,MAAM,QAAQ,QAAQ,IAAI,CAAC,EAC3C,WAAW,WAAW,EACtB,QAAQ,0BAA0B,kCAAkC,EACpE,WAAW,WAAW;AAAA,EACrB,MAAM;AAAA,EACN,UAAU;AAAA,EACV,cAAc;AAChB,CAAC,EACA,WAAW,QAAQ;AAAA,EAClB,MAAM;AAAA,EACN,OAAO;AAAA,EACP,UAAU;AACZ,CAAC,EACA,QAAQ;AAAA,EACP,OAAO;AAAA,IACL,MAAM;AAAA,IACN,UAAU;AAAA,IACV,SAAS;AAAA,EACX;AAAA,EACA,UAAU;AAAA,IACR,MAAM;AAAA,IACN,UAAU;AAAA,IACV,SAAS;AAAA,EACX;AAAA,EACA,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,UAAU;AAAA,IACV,SAAS;AAAA,EACX;AACF,CAAC,EACA,KAAK,EACL,WAAW;AAEd,IAAM,YAAY,IAAI,qBAAqB;AAAA,EACzC,SAAS,KAAK;AAAA,EACd,MAAM,KAAK;AAAA,EACX,KAAK,QAAQ;AACf,CAAC;AAED,IAAM,SAAS,IAAI;AAAA,EACjB;AAAA,IACE,MAAM;AAAA,IACN,SAAS;AAAA,EACX;AAAA,EACA;AAAA,IACE,cAAc,CAAC;AAAA,EACjB;AACF;AAEA,MAAM,OAAO,QAAQ,SAAS;AAE9B,IAAM,gBAAgB,OAAO,iBAAiB;AAK9C,IAAM,qBAAqB,OAAO,sBAAsB;AAExD,IAAM,SAAS,IAAI,OAAO,eAAe;AAAA,EACvC,cAAc;AAChB,CAAC;AAED,YAAY;AAAA,EACV;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAED,MAAM,eAAe;AAAA,EACnB;AAAA,EACA,MAAM,KAAK;AAAA,EACX,UAAU,KAAK;AACjB,CAAC;","names":[]}
|
|
@@ -146,7 +146,9 @@ var startSending = async (transport) => {
|
|
|
146
146
|
var startSSEServer = async ({
|
|
147
147
|
port,
|
|
148
148
|
server,
|
|
149
|
-
endpoint
|
|
149
|
+
endpoint,
|
|
150
|
+
onConnect,
|
|
151
|
+
onClose
|
|
150
152
|
}) => {
|
|
151
153
|
const activeTransports = {};
|
|
152
154
|
const httpServer = http.createServer(async (req, res) => {
|
|
@@ -158,9 +160,11 @@ var startSSEServer = async ({
|
|
|
158
160
|
const transport = new SSEServerTransport("/messages", res);
|
|
159
161
|
activeTransports[transport.sessionId] = transport;
|
|
160
162
|
await server.connect(transport);
|
|
163
|
+
onConnect?.(transport);
|
|
161
164
|
res.on("close", () => {
|
|
162
165
|
console.log("SSE connection closed");
|
|
163
166
|
delete activeTransports[transport.sessionId];
|
|
167
|
+
onClose?.(transport);
|
|
164
168
|
});
|
|
165
169
|
startSending(transport);
|
|
166
170
|
return;
|
|
@@ -184,13 +188,24 @@ var startSSEServer = async ({
|
|
|
184
188
|
}
|
|
185
189
|
res.writeHead(404).end();
|
|
186
190
|
});
|
|
187
|
-
httpServer.listen(port, "
|
|
191
|
+
httpServer.listen(port, "::");
|
|
188
192
|
console.error(
|
|
189
193
|
`server is running on SSE at http://localhost:${port}${endpoint}`
|
|
190
194
|
);
|
|
191
195
|
return {
|
|
192
196
|
close: async () => {
|
|
193
|
-
|
|
197
|
+
for (const transport of Object.values(activeTransports)) {
|
|
198
|
+
await transport.close();
|
|
199
|
+
}
|
|
200
|
+
return new Promise((resolve, reject) => {
|
|
201
|
+
httpServer.close((error) => {
|
|
202
|
+
if (error) {
|
|
203
|
+
reject(error);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
resolve();
|
|
207
|
+
});
|
|
208
|
+
});
|
|
194
209
|
}
|
|
195
210
|
};
|
|
196
211
|
};
|
|
@@ -200,4 +215,4 @@ export {
|
|
|
200
215
|
proxyServer,
|
|
201
216
|
startSSEServer
|
|
202
217
|
};
|
|
203
|
-
//# sourceMappingURL=chunk-
|
|
218
|
+
//# sourceMappingURL=chunk-3TY6Q6HO.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/MCPProxy.ts"],"sourcesContent":["import { SSEServerTransport } from \"@modelcontextprotocol/sdk/server/sse.js\";\nimport http from \"http\";\nimport { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport {\n CallToolRequestSchema,\n CompleteRequestSchema,\n GetPromptRequestSchema,\n JSONRPCMessage,\n ListPromptsRequestSchema,\n ListResourcesRequestSchema,\n ListResourceTemplatesRequestSchema,\n ListToolsRequestSchema,\n LoggingMessageNotificationSchema,\n ReadResourceRequestSchema,\n ServerCapabilities,\n} from \"@modelcontextprotocol/sdk/types.js\";\nimport { Transport } from \"@modelcontextprotocol/sdk/shared/transport.js\";\nimport { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\n\ntype TransportEvent =\n | {\n type: \"close\";\n }\n | {\n type: \"onclose\";\n }\n | {\n type: \"onerror\";\n error: Error;\n }\n | {\n type: \"onmessage\";\n message: JSONRPCMessage;\n }\n | {\n type: \"send\";\n message: JSONRPCMessage;\n }\n | {\n type: \"start\";\n };\n\nexport const tapTransport = (\n transport: Transport,\n eventHandler: (event: TransportEvent) => void,\n) => {\n const originalClose = transport.close.bind(transport);\n const originalOnClose = transport.onclose?.bind(transport);\n const originalOnError = transport.onerror?.bind(transport);\n const originalOnMessage = transport.onmessage?.bind(transport);\n const originalSend = transport.send.bind(transport);\n const originalStart = transport.start.bind(transport);\n\n transport.close = async () => {\n eventHandler({\n type: \"close\",\n });\n\n return originalClose?.();\n };\n\n transport.onclose = async () => {\n eventHandler({\n type: \"onclose\",\n });\n\n return originalOnClose?.();\n };\n\n transport.onerror = async (error: Error) => {\n eventHandler({\n type: \"onerror\",\n error,\n });\n\n return originalOnError?.(error);\n };\n\n transport.onmessage = async (message: JSONRPCMessage) => {\n eventHandler({\n type: \"onmessage\",\n message,\n });\n\n return originalOnMessage?.(message);\n };\n\n transport.send = async (message: JSONRPCMessage) => {\n eventHandler({\n type: \"send\",\n message,\n });\n\n return originalSend?.(message);\n };\n\n transport.start = async () => {\n eventHandler({\n type: \"start\",\n });\n\n return originalStart?.();\n };\n\n return transport;\n};\n\nexport const proxyServer = async ({\n server,\n client,\n serverCapabilities,\n}: {\n server: Server;\n client: Client;\n serverCapabilities: ServerCapabilities;\n}) => {\n if (serverCapabilities?.logging) {\n server.setNotificationHandler(\n LoggingMessageNotificationSchema,\n async (args) => {\n return client.notification(args);\n },\n );\n }\n\n if (serverCapabilities?.prompts) {\n server.setRequestHandler(GetPromptRequestSchema, async (args) => {\n return client.getPrompt(args.params);\n });\n\n server.setRequestHandler(ListPromptsRequestSchema, async (args) => {\n return client.listPrompts(args.params);\n });\n }\n\n if (serverCapabilities?.resources) {\n server.setRequestHandler(ListResourcesRequestSchema, async (args) => {\n return client.listResources(args.params);\n });\n\n server.setRequestHandler(\n ListResourceTemplatesRequestSchema,\n async (args) => {\n return client.listResourceTemplates(args.params);\n },\n );\n\n server.setRequestHandler(ReadResourceRequestSchema, async (args) => {\n return client.readResource(args.params);\n });\n }\n\n if (serverCapabilities?.tools) {\n server.setRequestHandler(CallToolRequestSchema, async (args) => {\n return client.callTool(args.params);\n });\n\n server.setRequestHandler(ListToolsRequestSchema, async (args) => {\n return client.listTools(args.params);\n });\n }\n\n server.setRequestHandler(CompleteRequestSchema, async (args) => {\n return client.complete(args.params);\n });\n};\n\n/**\n * @author https://dev.classmethod.jp/articles/mcp-sse/\n */\nconst startSending = async (transport: SSEServerTransport) => {\n try {\n await transport.send({\n jsonrpc: \"2.0\",\n method: \"sse/connection\",\n params: { message: \"SSE Connection established\" },\n });\n\n let messageCount = 0;\n const interval = setInterval(async () => {\n messageCount++;\n\n const message = `Message ${messageCount} at ${new Date().toISOString()}`;\n\n try {\n await transport.send({\n jsonrpc: \"2.0\",\n method: \"sse/message\",\n params: { data: message },\n });\n\n console.log(`Sent: ${message}`);\n\n if (messageCount === 10) {\n clearInterval(interval);\n\n await transport.send({\n jsonrpc: \"2.0\",\n method: \"sse/complete\",\n params: { message: \"Stream completed\" },\n });\n console.log(\"Stream completed\");\n }\n } catch (error) {\n console.error(\"Error sending message:\", error);\n clearInterval(interval);\n }\n }, 1000);\n } catch (error) {\n console.error(\"Error in startSending:\", error);\n }\n};\n\nexport type SSEServer = {\n close: () => Promise<void>;\n};\n\nexport const startSSEServer = async ({\n port,\n server,\n endpoint,\n onConnect,\n onClose,\n}: {\n port: number;\n endpoint: string;\n server: Server;\n onConnect?: (transport: SSEServerTransport) => void;\n onClose?: (transport: SSEServerTransport) => void;\n}): Promise<SSEServer> => {\n const activeTransports: Record<string, SSEServerTransport> = {};\n\n /**\n * @author https://dev.classmethod.jp/articles/mcp-sse/\n */\n const httpServer = http.createServer(async (req, res) => {\n if (req.method === \"GET\" && req.url === `/ping`) {\n res.writeHead(200).end(\"pong\");\n\n return;\n }\n\n if (req.method === \"GET\" && req.url === endpoint) {\n const transport = new SSEServerTransport(\"/messages\", res);\n\n activeTransports[transport.sessionId] = transport;\n\n await server.connect(transport);\n\n onConnect?.(transport);\n\n res.on(\"close\", () => {\n console.log(\"SSE connection closed\");\n\n delete activeTransports[transport.sessionId];\n\n onClose?.(transport);\n });\n\n startSending(transport);\n\n return;\n }\n\n if (req.method === \"POST\" && req.url?.startsWith(\"/messages\")) {\n const sessionId = new URL(\n req.url,\n \"https://example.com\",\n ).searchParams.get(\"sessionId\");\n\n if (!sessionId) {\n res.writeHead(400).end(\"No sessionId\");\n\n return;\n }\n\n const activeTransport: SSEServerTransport | undefined =\n activeTransports[sessionId];\n\n if (!activeTransport) {\n res.writeHead(400).end(\"No active transport\");\n\n return;\n }\n\n await activeTransport.handlePostMessage(req, res);\n\n return;\n }\n\n res.writeHead(404).end();\n });\n\n httpServer.listen(port, \"::\");\n\n console.error(\n `server is running on SSE at http://localhost:${port}${endpoint}`,\n );\n\n return {\n close: async () => {\n for (const transport of Object.values(activeTransports)) {\n await transport.close();\n }\n\n return new Promise((resolve, reject) => {\n httpServer.close((error) => {\n if (error) {\n reject(error);\n\n return;\n }\n\n resolve();\n });\n });\n },\n };\n};\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
package/src/MCPProxy.test.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
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
|
-
import { it, expect } from "vitest";
|
|
4
|
+
import { it, expect, vi } from "vitest";
|
|
5
5
|
import { proxyServer, startSSEServer } from "./MCPProxy.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
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
9
10
|
|
|
10
11
|
// @ts-expect-error - figure out how to use --experimental-eventsource with vitest
|
|
11
12
|
global.EventSource = EventSource;
|
|
@@ -35,22 +36,27 @@ it("proxies messages between SSE and stdio servers", async () => {
|
|
|
35
36
|
|
|
36
37
|
const serverCapabilities = stdioClient.getServerCapabilities() as {};
|
|
37
38
|
|
|
38
|
-
const
|
|
39
|
+
const mcpSSEServer = new Server(serverVersion, {
|
|
39
40
|
capabilities: serverCapabilities,
|
|
40
41
|
});
|
|
41
42
|
|
|
42
43
|
proxyServer({
|
|
43
|
-
server:
|
|
44
|
+
server: mcpSSEServer,
|
|
44
45
|
client: stdioClient,
|
|
45
46
|
serverCapabilities,
|
|
46
47
|
});
|
|
47
48
|
|
|
48
49
|
const port = await getRandomPort();
|
|
49
50
|
|
|
51
|
+
const onConnect = vi.fn();
|
|
52
|
+
const onClose = vi.fn();
|
|
53
|
+
|
|
50
54
|
await startSSEServer({
|
|
51
|
-
server:
|
|
55
|
+
server: mcpSSEServer,
|
|
52
56
|
port,
|
|
53
57
|
endpoint: "/sse",
|
|
58
|
+
onConnect,
|
|
59
|
+
onClose,
|
|
54
60
|
});
|
|
55
61
|
|
|
56
62
|
const sseClient = new Client(
|
|
@@ -77,4 +83,13 @@ it("proxies messages between SSE and stdio servers", async () => {
|
|
|
77
83
|
},
|
|
78
84
|
],
|
|
79
85
|
});
|
|
86
|
+
|
|
87
|
+
expect(onConnect).toHaveBeenCalled();
|
|
88
|
+
expect(onClose).not.toHaveBeenCalled();
|
|
89
|
+
|
|
90
|
+
await sseClient.close();
|
|
91
|
+
|
|
92
|
+
await delay(100);
|
|
93
|
+
|
|
94
|
+
expect(onClose).toHaveBeenCalled();
|
|
80
95
|
});
|
package/src/MCPProxy.ts
CHANGED
|
@@ -219,10 +219,14 @@ export const startSSEServer = async ({
|
|
|
219
219
|
port,
|
|
220
220
|
server,
|
|
221
221
|
endpoint,
|
|
222
|
+
onConnect,
|
|
223
|
+
onClose,
|
|
222
224
|
}: {
|
|
223
225
|
port: number;
|
|
224
226
|
endpoint: string;
|
|
225
227
|
server: Server;
|
|
228
|
+
onConnect?: (transport: SSEServerTransport) => void;
|
|
229
|
+
onClose?: (transport: SSEServerTransport) => void;
|
|
226
230
|
}): Promise<SSEServer> => {
|
|
227
231
|
const activeTransports: Record<string, SSEServerTransport> = {};
|
|
228
232
|
|
|
@@ -243,10 +247,14 @@ export const startSSEServer = async ({
|
|
|
243
247
|
|
|
244
248
|
await server.connect(transport);
|
|
245
249
|
|
|
250
|
+
onConnect?.(transport);
|
|
251
|
+
|
|
246
252
|
res.on("close", () => {
|
|
247
253
|
console.log("SSE connection closed");
|
|
248
254
|
|
|
249
255
|
delete activeTransports[transport.sessionId];
|
|
256
|
+
|
|
257
|
+
onClose?.(transport);
|
|
250
258
|
});
|
|
251
259
|
|
|
252
260
|
startSending(transport);
|
|
@@ -283,7 +291,7 @@ export const startSSEServer = async ({
|
|
|
283
291
|
res.writeHead(404).end();
|
|
284
292
|
});
|
|
285
293
|
|
|
286
|
-
httpServer.listen(port, "
|
|
294
|
+
httpServer.listen(port, "::");
|
|
287
295
|
|
|
288
296
|
console.error(
|
|
289
297
|
`server is running on SSE at http://localhost:${port}${endpoint}`,
|
|
@@ -291,7 +299,21 @@ export const startSSEServer = async ({
|
|
|
291
299
|
|
|
292
300
|
return {
|
|
293
301
|
close: async () => {
|
|
294
|
-
|
|
302
|
+
for (const transport of Object.values(activeTransports)) {
|
|
303
|
+
await transport.close();
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return new Promise((resolve, reject) => {
|
|
307
|
+
httpServer.close((error) => {
|
|
308
|
+
if (error) {
|
|
309
|
+
reject(error);
|
|
310
|
+
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
resolve();
|
|
315
|
+
});
|
|
316
|
+
});
|
|
295
317
|
},
|
|
296
318
|
};
|
|
297
319
|
};
|
package/src/bin/mcp-proxy.ts
CHANGED
|
@@ -12,7 +12,6 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
|
12
12
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
13
13
|
import { proxyServer, startSSEServer } from "../MCPProxy.js";
|
|
14
14
|
|
|
15
|
-
|
|
16
15
|
const argv = await yargs(hideBin(process.argv))
|
|
17
16
|
.scriptName("mcp-proxy")
|
|
18
17
|
.command("$0 <command> [args...]", "Run a command with MCP arguments")
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/MCPProxy.ts"],"sourcesContent":["import { SSEServerTransport } from \"@modelcontextprotocol/sdk/server/sse.js\";\nimport http from \"http\";\nimport { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport {\n CallToolRequestSchema,\n CompleteRequestSchema,\n GetPromptRequestSchema,\n JSONRPCMessage,\n ListPromptsRequestSchema,\n ListResourcesRequestSchema,\n ListResourceTemplatesRequestSchema,\n ListToolsRequestSchema,\n LoggingMessageNotificationSchema,\n ReadResourceRequestSchema,\n ServerCapabilities,\n} from \"@modelcontextprotocol/sdk/types.js\";\nimport { Transport } from \"@modelcontextprotocol/sdk/shared/transport.js\";\nimport { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\n\ntype TransportEvent =\n | {\n type: \"close\";\n }\n | {\n type: \"onclose\";\n }\n | {\n type: \"onerror\";\n error: Error;\n }\n | {\n type: \"onmessage\";\n message: JSONRPCMessage;\n }\n | {\n type: \"send\";\n message: JSONRPCMessage;\n }\n | {\n type: \"start\";\n };\n\nexport const tapTransport = (\n transport: Transport,\n eventHandler: (event: TransportEvent) => void,\n) => {\n const originalClose = transport.close.bind(transport);\n const originalOnClose = transport.onclose?.bind(transport);\n const originalOnError = transport.onerror?.bind(transport);\n const originalOnMessage = transport.onmessage?.bind(transport);\n const originalSend = transport.send.bind(transport);\n const originalStart = transport.start.bind(transport);\n\n transport.close = async () => {\n eventHandler({\n type: \"close\",\n });\n\n return originalClose?.();\n };\n\n transport.onclose = async () => {\n eventHandler({\n type: \"onclose\",\n });\n\n return originalOnClose?.();\n };\n\n transport.onerror = async (error: Error) => {\n eventHandler({\n type: \"onerror\",\n error,\n });\n\n return originalOnError?.(error);\n };\n\n transport.onmessage = async (message: JSONRPCMessage) => {\n eventHandler({\n type: \"onmessage\",\n message,\n });\n\n return originalOnMessage?.(message);\n };\n\n transport.send = async (message: JSONRPCMessage) => {\n eventHandler({\n type: \"send\",\n message,\n });\n\n return originalSend?.(message);\n };\n\n transport.start = async () => {\n eventHandler({\n type: \"start\",\n });\n\n return originalStart?.();\n };\n\n return transport;\n};\n\nexport const proxyServer = async ({\n server,\n client,\n serverCapabilities,\n}: {\n server: Server;\n client: Client;\n serverCapabilities: ServerCapabilities;\n}) => {\n if (serverCapabilities?.logging) {\n server.setNotificationHandler(\n LoggingMessageNotificationSchema,\n async (args) => {\n return client.notification(args);\n },\n );\n }\n\n if (serverCapabilities?.prompts) {\n server.setRequestHandler(GetPromptRequestSchema, async (args) => {\n return client.getPrompt(args.params);\n });\n\n server.setRequestHandler(ListPromptsRequestSchema, async (args) => {\n return client.listPrompts(args.params);\n });\n }\n\n if (serverCapabilities?.resources) {\n server.setRequestHandler(ListResourcesRequestSchema, async (args) => {\n return client.listResources(args.params);\n });\n\n server.setRequestHandler(\n ListResourceTemplatesRequestSchema,\n async (args) => {\n return client.listResourceTemplates(args.params);\n },\n );\n\n server.setRequestHandler(ReadResourceRequestSchema, async (args) => {\n return client.readResource(args.params);\n });\n }\n\n if (serverCapabilities?.tools) {\n server.setRequestHandler(CallToolRequestSchema, async (args) => {\n return client.callTool(args.params);\n });\n\n server.setRequestHandler(ListToolsRequestSchema, async (args) => {\n return client.listTools(args.params);\n });\n }\n\n server.setRequestHandler(CompleteRequestSchema, async (args) => {\n return client.complete(args.params);\n });\n};\n\n/**\n * @author https://dev.classmethod.jp/articles/mcp-sse/\n */\nconst startSending = async (transport: SSEServerTransport) => {\n try {\n await transport.send({\n jsonrpc: \"2.0\",\n method: \"sse/connection\",\n params: { message: \"SSE Connection established\" },\n });\n\n let messageCount = 0;\n const interval = setInterval(async () => {\n messageCount++;\n\n const message = `Message ${messageCount} at ${new Date().toISOString()}`;\n\n try {\n await transport.send({\n jsonrpc: \"2.0\",\n method: \"sse/message\",\n params: { data: message },\n });\n\n console.log(`Sent: ${message}`);\n\n if (messageCount === 10) {\n clearInterval(interval);\n\n await transport.send({\n jsonrpc: \"2.0\",\n method: \"sse/complete\",\n params: { message: \"Stream completed\" },\n });\n console.log(\"Stream completed\");\n }\n } catch (error) {\n console.error(\"Error sending message:\", error);\n clearInterval(interval);\n }\n }, 1000);\n } catch (error) {\n console.error(\"Error in startSending:\", error);\n }\n};\n\nexport type SSEServer = {\n close: () => Promise<void>;\n};\n\nexport const startSSEServer = async ({\n port,\n server,\n endpoint,\n}: {\n port: number;\n endpoint: string;\n server: Server;\n}): Promise<SSEServer> => {\n const activeTransports: Record<string, SSEServerTransport> = {};\n\n /**\n * @author https://dev.classmethod.jp/articles/mcp-sse/\n */\n const httpServer = http.createServer(async (req, res) => {\n if (req.method === \"GET\" && req.url === `/ping`) {\n res.writeHead(200).end(\"pong\");\n\n return;\n }\n\n if (req.method === \"GET\" && req.url === endpoint) {\n const transport = new SSEServerTransport(\"/messages\", res);\n\n activeTransports[transport.sessionId] = transport;\n\n await server.connect(transport);\n\n res.on(\"close\", () => {\n console.log(\"SSE connection closed\");\n\n delete activeTransports[transport.sessionId];\n });\n\n startSending(transport);\n\n return;\n }\n\n if (req.method === \"POST\" && req.url?.startsWith(\"/messages\")) {\n const sessionId = new URL(\n req.url,\n \"https://example.com\",\n ).searchParams.get(\"sessionId\");\n\n if (!sessionId) {\n res.writeHead(400).end(\"No sessionId\");\n\n return;\n }\n\n const activeTransport: SSEServerTransport | undefined =\n activeTransports[sessionId];\n\n if (!activeTransport) {\n res.writeHead(400).end(\"No active transport\");\n\n return;\n }\n\n await activeTransport.handlePostMessage(req, res);\n\n return;\n }\n\n res.writeHead(404).end();\n });\n\n httpServer.listen(port, \"0.0.0.0\");\n\n console.error(\n `server is running on SSE at http://localhost:${port}${endpoint}`,\n );\n\n return {\n close: async () => {\n httpServer.close();\n },\n };\n};\n"],"mappings":"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","names":[]}
|