mcp-proxy 1.0.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,37 @@
1
+ name: Run Tests
2
+ on:
3
+ pull_request:
4
+ branches:
5
+ - main
6
+ types:
7
+ - opened
8
+ - synchronize
9
+ - reopened
10
+ - ready_for_review
11
+ jobs:
12
+ test:
13
+ runs-on: ubuntu-latest
14
+ name: Test
15
+ strategy:
16
+ fail-fast: true
17
+ matrix:
18
+ node:
19
+ - 22
20
+ steps:
21
+ - name: Checkout repository
22
+ uses: actions/checkout@v4
23
+ with:
24
+ fetch-depth: 0
25
+ - uses: pnpm/action-setup@v4
26
+ with:
27
+ version: 9
28
+ - name: Setup NodeJS ${{ matrix.node }}
29
+ uses: actions/setup-node@v4
30
+ with:
31
+ node-version: ${{ matrix.node }}
32
+ cache: "pnpm"
33
+ cache-dependency-path: "**/pnpm-lock.yaml"
34
+ - name: Install dependencies
35
+ run: pnpm install
36
+ - name: Run tests
37
+ run: pnpm test
@@ -0,0 +1,48 @@
1
+ name: Release
2
+ on:
3
+ push:
4
+ branches:
5
+ - main
6
+ jobs:
7
+ test:
8
+ environment: release
9
+ name: Test
10
+ strategy:
11
+ fail-fast: true
12
+ matrix:
13
+ node:
14
+ - 22
15
+ runs-on: ubuntu-latest
16
+ permissions:
17
+ contents: write
18
+ id-token: write
19
+ steps:
20
+ - name: setup repository
21
+ uses: actions/checkout@v4
22
+ with:
23
+ fetch-depth: 0
24
+ - uses: pnpm/action-setup@v4
25
+ with:
26
+ version: 9
27
+ - name: setup node.js
28
+ uses: actions/setup-node@v4
29
+ with:
30
+ cache: "pnpm"
31
+ node-version: ${{ matrix.node }}
32
+ - name: Setup NodeJS ${{ matrix.node }}
33
+ uses: actions/setup-node@v4
34
+ with:
35
+ node-version: ${{ matrix.node }}
36
+ cache: "pnpm"
37
+ cache-dependency-path: "**/pnpm-lock.yaml"
38
+ - name: Install dependencies
39
+ run: pnpm install
40
+ - name: Run tests
41
+ run: pnpm test
42
+ - name: Build
43
+ run: pnpm build
44
+ - name: Release
45
+ run: pnpm semantic-release
46
+ env:
47
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
48
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
package/LICENSE ADDED
@@ -0,0 +1,24 @@
1
+ BSD 2-Clause License
2
+
3
+ Copyright (c) 2024, Frank Fiegel <frank@glama.ai>
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
19
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
21
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
22
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
23
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/README.md ADDED
@@ -0,0 +1,81 @@
1
+ # MCP Proxy
2
+
3
+ A TypeScript SSE proxy for [MCP](https://modelcontextprotocol.io/) servers that use `stdio` transport.
4
+
5
+ > [!NOTE]
6
+ > For a Python implementation, see [mcp-proxy](https://github.com/sparfenyuk/mcp-proxy).
7
+
8
+ > [!NOTE]
9
+ > MCP Proxy is what [FastMCP](https://github.com/punkpeye/fastmcp) uses to enable SSE.
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ npm install mcp-proxy
15
+ ```
16
+
17
+ ## Quickstart
18
+
19
+ ### Command-line
20
+
21
+ ```bash
22
+ npx mcp-proxy --port 8080 --endpoint /sse tsx server.js
23
+ ```
24
+
25
+ This starts an SSE server and `stdio` server (`tsx server.js`). The SSE server listens on port 8080 and endpoint `/sse`, and forwards messages to the `stdio` server.
26
+
27
+ ### Node.js SDK
28
+
29
+ The Node.js SDK provides several utilities that are used to create a proxy.
30
+
31
+ #### `proxyServer`
32
+
33
+ Sets up a proxy between a server and a client.
34
+
35
+ ```ts
36
+ const transport = new StdioClientTransport();
37
+ const client = new Client();
38
+
39
+ const server = new Server(serverVersion, {
40
+ capabilities: {},
41
+ });
42
+
43
+ proxyServer({
44
+ server,
45
+ client,
46
+ capabilities: {},
47
+ });
48
+ ```
49
+
50
+ In this example, the server will proxy all requests to the client and vice versa.
51
+
52
+ #### `startSseServer`
53
+
54
+ Starts a proxy that listens on a `port` and `endpoint`, and sends messages to the attached server via `SSEServerTransport`.
55
+
56
+ ```ts
57
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
58
+ import { startSseServer } from "mcp-proxy";
59
+
60
+ const server = new Server();
61
+
62
+ const { close } = startSseServer({
63
+ port: 8080,
64
+ endpoint: "/sse",
65
+ server,
66
+ });
67
+
68
+ close();
69
+ ```
70
+
71
+ #### `tapTransport`
72
+
73
+ Taps into a transport and logs events.
74
+
75
+ ```ts
76
+ import { tapTransport } from "mcp-proxy";
77
+
78
+ const transport = tapTransport(new StdioClientTransport(), (event) => {
79
+ console.log(event);
80
+ });
81
+ ```
@@ -0,0 +1,36 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { ServerCapabilities, JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js';
3
+ import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
4
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
5
+
6
+ type TransportEvent = {
7
+ type: "close";
8
+ } | {
9
+ type: "onclose";
10
+ } | {
11
+ type: "onerror";
12
+ error: Error;
13
+ } | {
14
+ type: "onmessage";
15
+ message: JSONRPCMessage;
16
+ } | {
17
+ type: "send";
18
+ message: JSONRPCMessage;
19
+ } | {
20
+ type: "start";
21
+ };
22
+ declare const tapTransport: (transport: Transport, eventHandler: (event: TransportEvent) => void) => Transport;
23
+ declare const proxyServer: ({ server, client, serverCapabilities, }: {
24
+ server: Server;
25
+ client: Client;
26
+ serverCapabilities: ServerCapabilities;
27
+ }) => Promise<void>;
28
+ declare const startSseServer: ({ port, server, endpoint, }: {
29
+ port: number;
30
+ endpoint: string;
31
+ server: Server;
32
+ }) => {
33
+ close: () => void;
34
+ };
35
+
36
+ export { proxyServer, startSseServer, tapTransport };
@@ -0,0 +1,11 @@
1
+ import {
2
+ proxyServer,
3
+ startSseServer,
4
+ tapTransport
5
+ } from "./chunk-4AZHVXNQ.js";
6
+ export {
7
+ proxyServer,
8
+ startSseServer,
9
+ tapTransport
10
+ };
11
+ //# sourceMappingURL=MCPProxy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ proxyServer,
4
+ startSseServer
5
+ } from "../chunk-4AZHVXNQ.js";
6
+
7
+ // src/bin/mcp-proxy.ts
8
+ import yargs from "yargs";
9
+ import { hideBin } from "yargs/helpers";
10
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
11
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
12
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
13
+ var argv = await yargs(hideBin(process.argv)).scriptName("mcp-proxy").command("$0 <command> [args...]", "Run a command with MCP arguments").positional("command", {
14
+ type: "string",
15
+ describe: "The command to run",
16
+ demandOption: true
17
+ }).positional("args", {
18
+ type: "string",
19
+ array: true,
20
+ describe: "The arguments to pass to the command"
21
+ }).options({
22
+ debug: {
23
+ type: "boolean",
24
+ describe: "Enable debug logging",
25
+ default: false
26
+ },
27
+ endpoint: {
28
+ type: "string",
29
+ describe: "The endpoint to listen on for SSE",
30
+ default: "/sse"
31
+ },
32
+ port: {
33
+ type: "number",
34
+ describe: "The port to listen on for SSE",
35
+ default: 8080
36
+ }
37
+ }).help().parseAsync();
38
+ var transport = new StdioClientTransport({
39
+ command: argv.command,
40
+ args: argv.args,
41
+ env: process.env
42
+ });
43
+ var client = new Client(
44
+ {
45
+ name: "mcp-proxy",
46
+ version: "1.0.0"
47
+ },
48
+ {
49
+ capabilities: {}
50
+ }
51
+ );
52
+ await client.connect(transport);
53
+ var serverVersion = client.getServerVersion();
54
+ var serverCapabilities = client.getServerCapabilities();
55
+ var server = new Server(serverVersion, {
56
+ capabilities: serverCapabilities
57
+ });
58
+ proxyServer({
59
+ server,
60
+ client,
61
+ serverCapabilities
62
+ });
63
+ await startSseServer({
64
+ server,
65
+ port: argv.port,
66
+ endpoint: argv.endpoint
67
+ });
68
+ //# sourceMappingURL=mcp-proxy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/bin/mcp-proxy.ts"],"sourcesContent":["#!/usr/bin/env node\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,OAAO,WAAW;AAClB,SAAS,eAAe;AACxB,SAAS,4BAA4B;AACrC,SAAS,cAAc;AACvB,SAAS,cAAc;AAGvB,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":[]}
@@ -0,0 +1,199 @@
1
+ // src/MCPProxy.ts
2
+ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
3
+ import http from "http";
4
+ import {
5
+ CallToolRequestSchema,
6
+ CompleteRequestSchema,
7
+ GetPromptRequestSchema,
8
+ ListPromptsRequestSchema,
9
+ ListResourcesRequestSchema,
10
+ ListResourceTemplatesRequestSchema,
11
+ ListToolsRequestSchema,
12
+ LoggingMessageNotificationSchema,
13
+ ReadResourceRequestSchema
14
+ } from "@modelcontextprotocol/sdk/types.js";
15
+ var tapTransport = (transport, eventHandler) => {
16
+ const originalClose = transport.close.bind(transport);
17
+ const originalOnClose = transport.onclose?.bind(transport);
18
+ const originalOnError = transport.onerror?.bind(transport);
19
+ const originalOnMessage = transport.onmessage?.bind(transport);
20
+ const originalSend = transport.send.bind(transport);
21
+ const originalStart = transport.start.bind(transport);
22
+ transport.close = async () => {
23
+ eventHandler({
24
+ type: "close"
25
+ });
26
+ return originalClose?.();
27
+ };
28
+ transport.onclose = async () => {
29
+ eventHandler({
30
+ type: "onclose"
31
+ });
32
+ return originalOnClose?.();
33
+ };
34
+ transport.onerror = async (error) => {
35
+ eventHandler({
36
+ type: "onerror",
37
+ error
38
+ });
39
+ return originalOnError?.(error);
40
+ };
41
+ transport.onmessage = async (message) => {
42
+ eventHandler({
43
+ type: "onmessage",
44
+ message
45
+ });
46
+ return originalOnMessage?.(message);
47
+ };
48
+ transport.send = async (message) => {
49
+ eventHandler({
50
+ type: "send",
51
+ message
52
+ });
53
+ return originalSend?.(message);
54
+ };
55
+ transport.start = async () => {
56
+ eventHandler({
57
+ type: "start"
58
+ });
59
+ return originalStart?.();
60
+ };
61
+ return transport;
62
+ };
63
+ var proxyServer = async ({
64
+ server,
65
+ client,
66
+ serverCapabilities
67
+ }) => {
68
+ if (serverCapabilities?.logging) {
69
+ server.setNotificationHandler(
70
+ LoggingMessageNotificationSchema,
71
+ async (args) => {
72
+ return client.notification(args);
73
+ }
74
+ );
75
+ }
76
+ if (serverCapabilities?.prompts) {
77
+ server.setRequestHandler(GetPromptRequestSchema, async (args) => {
78
+ return client.getPrompt(args.params);
79
+ });
80
+ server.setRequestHandler(ListPromptsRequestSchema, async (args) => {
81
+ return client.listPrompts(args.params);
82
+ });
83
+ }
84
+ if (serverCapabilities?.resources) {
85
+ server.setRequestHandler(ListResourcesRequestSchema, async (args) => {
86
+ return client.listResources(args.params);
87
+ });
88
+ server.setRequestHandler(
89
+ ListResourceTemplatesRequestSchema,
90
+ async (args) => {
91
+ return client.listResourceTemplates(args.params);
92
+ }
93
+ );
94
+ server.setRequestHandler(ReadResourceRequestSchema, async (args) => {
95
+ return client.readResource(args.params);
96
+ });
97
+ }
98
+ if (serverCapabilities?.tools) {
99
+ server.setRequestHandler(CallToolRequestSchema, async (args) => {
100
+ return client.callTool(args.params);
101
+ });
102
+ server.setRequestHandler(ListToolsRequestSchema, async (args) => {
103
+ return client.listTools(args.params);
104
+ });
105
+ }
106
+ server.setRequestHandler(CompleteRequestSchema, async (args) => {
107
+ return client.complete(args.params);
108
+ });
109
+ };
110
+ var startSending = async (transport) => {
111
+ try {
112
+ await transport.send({
113
+ jsonrpc: "2.0",
114
+ method: "sse/connection",
115
+ params: { message: "SSE Connection established" }
116
+ });
117
+ let messageCount = 0;
118
+ const interval = setInterval(async () => {
119
+ messageCount++;
120
+ const message = `Message ${messageCount} at ${(/* @__PURE__ */ new Date()).toISOString()}`;
121
+ try {
122
+ await transport.send({
123
+ jsonrpc: "2.0",
124
+ method: "sse/message",
125
+ params: { data: message }
126
+ });
127
+ console.log(`Sent: ${message}`);
128
+ if (messageCount === 10) {
129
+ clearInterval(interval);
130
+ await transport.send({
131
+ jsonrpc: "2.0",
132
+ method: "sse/complete",
133
+ params: { message: "Stream completed" }
134
+ });
135
+ console.log("Stream completed");
136
+ }
137
+ } catch (error) {
138
+ console.error("Error sending message:", error);
139
+ clearInterval(interval);
140
+ }
141
+ }, 1e3);
142
+ } catch (error) {
143
+ console.error("Error in startSending:", error);
144
+ }
145
+ };
146
+ var startSseServer = ({
147
+ port,
148
+ server,
149
+ endpoint
150
+ }) => {
151
+ const activeTransports = {};
152
+ const httpServer = http.createServer(async (req, res) => {
153
+ if (req.method === "GET" && req.url === endpoint) {
154
+ const transport = new SSEServerTransport("/messages", res);
155
+ activeTransports[transport.sessionId] = transport;
156
+ await server.connect(transport);
157
+ res.on("close", () => {
158
+ console.log("SSE connection closed");
159
+ delete activeTransports[transport.sessionId];
160
+ });
161
+ startSending(transport);
162
+ return;
163
+ }
164
+ if (req.method === "POST" && req.url?.startsWith("/messages")) {
165
+ const sessionId = new URL(
166
+ req.url,
167
+ "https://example.com"
168
+ ).searchParams.get("sessionId");
169
+ if (!sessionId) {
170
+ res.writeHead(400).end("No sessionId");
171
+ return;
172
+ }
173
+ const activeTransport = activeTransports[sessionId];
174
+ if (!activeTransport) {
175
+ res.writeHead(400).end("No active transport");
176
+ return;
177
+ }
178
+ await activeTransport.handlePostMessage(req, res);
179
+ return;
180
+ }
181
+ res.writeHead(404).end();
182
+ });
183
+ httpServer.listen(port, "0.0.0.0");
184
+ console.error(
185
+ `server is running on SSE at http://localhost:${port}${endpoint}`
186
+ );
187
+ return {
188
+ close: () => {
189
+ httpServer.close();
190
+ }
191
+ };
192
+ };
193
+
194
+ export {
195
+ tapTransport,
196
+ proxyServer,
197
+ startSseServer
198
+ };
199
+ //# sourceMappingURL=chunk-4AZHVXNQ.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 const startSseServer = ({\n port,\n server,\n endpoint,\n}: {\n port: number;\n endpoint: string;\n server: Server;\n}) => {\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 === 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: () => {\n httpServer.close();\n },\n };\n};\n"],"mappings":"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","names":[]}
@@ -0,0 +1,3 @@
1
+ import perfectionist from "eslint-plugin-perfectionist";
2
+
3
+ export default [perfectionist.configs["recommended-alphabetical"]];
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "mcp-proxy",
3
+ "version": "1.0.0",
4
+ "main": "dist/MCPProxy.js",
5
+ "scripts": {
6
+ "build": "tsup",
7
+ "test": "vitest run && tsc",
8
+ "format": "prettier --write . && eslint --fix ."
9
+ },
10
+ "bin": {
11
+ "mcp-proxy": "dist/bin/mcp-proxy.js"
12
+ },
13
+ "keywords": [
14
+ "MCP",
15
+ "SSE",
16
+ "proxy"
17
+ ],
18
+ "type": "module",
19
+ "author": "Frank Fiegel <frank@glama.ai>",
20
+ "license": "MIT",
21
+ "description": "A TypeScript SSE proxy for MCP servers that use stdio transport.",
22
+ "module": "dist/MCPProxy.js",
23
+ "types": "dist/MCPProxy.d.ts",
24
+ "dependencies": {
25
+ "@modelcontextprotocol/sdk": "^1.0.4",
26
+ "fastmcp": "^1.5.9",
27
+ "yargs": "^17.7.2"
28
+ },
29
+ "repository": {
30
+ "url": "https://github.com/punkpeye/mcp-proxy"
31
+ },
32
+ "release": {
33
+ "branches": [
34
+ "main"
35
+ ],
36
+ "plugins": [
37
+ "@semantic-release/commit-analyzer",
38
+ "@semantic-release/release-notes-generator",
39
+ "@semantic-release/npm",
40
+ "@semantic-release/github"
41
+ ]
42
+ },
43
+ "devDependencies": {
44
+ "@sebbo2002/semantic-release-jsr": "^2.0.2",
45
+ "@tsconfig/node22": "^22.0.0",
46
+ "@types/node": "^22.10.2",
47
+ "@types/yargs": "^17.0.33",
48
+ "eslint": "^9.17.0",
49
+ "eslint-plugin-perfectionist": "^4.4.0",
50
+ "eventsource": "^3.0.2",
51
+ "get-port-please": "^3.1.2",
52
+ "prettier": "^3.4.2",
53
+ "semantic-release": "^24.2.0",
54
+ "tsup": "^8.3.5",
55
+ "typescript": "^5.7.2",
56
+ "vitest": "^2.1.8"
57
+ },
58
+ "tsup": {
59
+ "entry": [
60
+ "src/MCPProxy.ts",
61
+ "src/bin/mcp-proxy.ts"
62
+ ],
63
+ "format": [
64
+ "esm"
65
+ ],
66
+ "dts": true,
67
+ "splitting": true,
68
+ "sourcemap": true,
69
+ "clean": true
70
+ }
71
+ }
@@ -0,0 +1,80 @@
1
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
3
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
+ import { it, expect } from "vitest";
5
+ import { proxyServer, startSseServer } from "./MCPProxy.js";
6
+ import { getRandomPort } from "get-port-please";
7
+ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
8
+ import { EventSource } from "eventsource";
9
+
10
+ // @ts-expect-error - figure out how to use --experimental-eventsource with vitest
11
+ global.EventSource = EventSource;
12
+
13
+ it("proxies messages between SSE and stdio servers", async () => {
14
+ const stdioTransport = new StdioClientTransport({
15
+ command: "tsx",
16
+ args: ["src/simple-stdio-server.ts"],
17
+ });
18
+
19
+ const stdioClient = new Client(
20
+ {
21
+ name: "mcp-proxy",
22
+ version: "1.0.0",
23
+ },
24
+ {
25
+ capabilities: {},
26
+ },
27
+ );
28
+
29
+ await stdioClient.connect(stdioTransport);
30
+
31
+ const serverVersion = stdioClient.getServerVersion() as {
32
+ name: string;
33
+ version: string;
34
+ };
35
+
36
+ const serverCapabilities = stdioClient.getServerCapabilities() as {};
37
+
38
+ const sseServer = new Server(serverVersion, {
39
+ capabilities: serverCapabilities,
40
+ });
41
+
42
+ proxyServer({
43
+ server: sseServer,
44
+ client: stdioClient,
45
+ serverCapabilities,
46
+ });
47
+
48
+ const port = await getRandomPort();
49
+
50
+ await startSseServer({
51
+ server: sseServer,
52
+ port,
53
+ endpoint: "/sse",
54
+ });
55
+
56
+ const sseClient = new Client(
57
+ {
58
+ name: "sse-client",
59
+ version: "1.0.0",
60
+ },
61
+ {
62
+ capabilities: {},
63
+ },
64
+ );
65
+
66
+ const transport = new SSEClientTransport(
67
+ new URL(`http://localhost:${port}/sse`),
68
+ );
69
+
70
+ await sseClient.connect(transport);
71
+
72
+ expect(await sseClient.listResources()).toEqual({
73
+ resources: [
74
+ {
75
+ uri: "file:///example.txt",
76
+ name: "Example Resource",
77
+ },
78
+ ],
79
+ });
80
+ });
@@ -0,0 +1,287 @@
1
+ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
2
+ import http from "http";
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
+
168
+ /**
169
+ * @author https://dev.classmethod.jp/articles/mcp-sse/
170
+ */
171
+ const startSending = async (transport: SSEServerTransport) => {
172
+ try {
173
+ await transport.send({
174
+ jsonrpc: "2.0",
175
+ method: "sse/connection",
176
+ params: { message: "SSE Connection established" },
177
+ });
178
+
179
+ let messageCount = 0;
180
+ const interval = setInterval(async () => {
181
+ messageCount++;
182
+
183
+ const message = `Message ${messageCount} at ${new Date().toISOString()}`;
184
+
185
+ try {
186
+ await transport.send({
187
+ jsonrpc: "2.0",
188
+ method: "sse/message",
189
+ params: { data: message },
190
+ });
191
+
192
+ console.log(`Sent: ${message}`);
193
+
194
+ if (messageCount === 10) {
195
+ clearInterval(interval);
196
+
197
+ await transport.send({
198
+ jsonrpc: "2.0",
199
+ method: "sse/complete",
200
+ params: { message: "Stream completed" },
201
+ });
202
+ console.log("Stream completed");
203
+ }
204
+ } catch (error) {
205
+ console.error("Error sending message:", error);
206
+ clearInterval(interval);
207
+ }
208
+ }, 1000);
209
+ } catch (error) {
210
+ console.error("Error in startSending:", error);
211
+ }
212
+ };
213
+
214
+ export const startSseServer = ({
215
+ port,
216
+ server,
217
+ endpoint,
218
+ }: {
219
+ port: number;
220
+ endpoint: string;
221
+ server: Server;
222
+ }) => {
223
+ const activeTransports: Record<string, SSEServerTransport> = {};
224
+
225
+ /**
226
+ * @author https://dev.classmethod.jp/articles/mcp-sse/
227
+ */
228
+ const httpServer = http.createServer(async (req, res) => {
229
+ if (req.method === "GET" && req.url === endpoint) {
230
+ const transport = new SSEServerTransport("/messages", res);
231
+
232
+ activeTransports[transport.sessionId] = transport;
233
+
234
+ await server.connect(transport);
235
+
236
+ res.on("close", () => {
237
+ console.log("SSE connection closed");
238
+
239
+ delete activeTransports[transport.sessionId];
240
+ });
241
+
242
+ startSending(transport);
243
+
244
+ return;
245
+ }
246
+
247
+ if (req.method === "POST" && req.url?.startsWith("/messages")) {
248
+ const sessionId = new URL(
249
+ req.url,
250
+ "https://example.com",
251
+ ).searchParams.get("sessionId");
252
+
253
+ if (!sessionId) {
254
+ res.writeHead(400).end("No sessionId");
255
+
256
+ return;
257
+ }
258
+
259
+ const activeTransport: SSEServerTransport | undefined =
260
+ activeTransports[sessionId];
261
+
262
+ if (!activeTransport) {
263
+ res.writeHead(400).end("No active transport");
264
+
265
+ return;
266
+ }
267
+
268
+ await activeTransport.handlePostMessage(req, res);
269
+
270
+ return;
271
+ }
272
+
273
+ res.writeHead(404).end();
274
+ });
275
+
276
+ httpServer.listen(port, "0.0.0.0");
277
+
278
+ console.error(
279
+ `server is running on SSE at http://localhost:${port}${endpoint}`,
280
+ );
281
+
282
+ return {
283
+ close: () => {
284
+ httpServer.close();
285
+ },
286
+ };
287
+ };
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env node
2
+
3
+ import yargs from "yargs";
4
+ import { hideBin } from "yargs/helpers";
5
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
6
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
7
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
8
+ import { proxyServer, startSseServer } from "../MCPProxy.js";
9
+
10
+ const argv = await yargs(hideBin(process.argv))
11
+ .scriptName("mcp-proxy")
12
+ .command("$0 <command> [args...]", "Run a command with MCP arguments")
13
+ .positional("command", {
14
+ type: "string",
15
+ describe: "The command to run",
16
+ demandOption: true,
17
+ })
18
+ .positional("args", {
19
+ type: "string",
20
+ array: true,
21
+ describe: "The arguments to pass to the command",
22
+ })
23
+ .options({
24
+ debug: {
25
+ type: "boolean",
26
+ describe: "Enable debug logging",
27
+ default: false,
28
+ },
29
+ endpoint: {
30
+ type: "string",
31
+ describe: "The endpoint to listen on for SSE",
32
+ default: "/sse",
33
+ },
34
+ port: {
35
+ type: "number",
36
+ describe: "The port to listen on for SSE",
37
+ default: 8080,
38
+ },
39
+ })
40
+ .help()
41
+ .parseAsync();
42
+
43
+ const transport = new StdioClientTransport({
44
+ command: argv.command,
45
+ args: argv.args,
46
+ env: process.env as Record<string, string>,
47
+ });
48
+
49
+ const client = new Client(
50
+ {
51
+ name: "mcp-proxy",
52
+ version: "1.0.0",
53
+ },
54
+ {
55
+ capabilities: {},
56
+ },
57
+ );
58
+
59
+ await client.connect(transport);
60
+
61
+ const serverVersion = client.getServerVersion() as {
62
+ name: string;
63
+ version: string;
64
+ };
65
+
66
+ const serverCapabilities = client.getServerCapabilities() as {};
67
+
68
+ const server = new Server(serverVersion, {
69
+ capabilities: serverCapabilities,
70
+ });
71
+
72
+ proxyServer({
73
+ server,
74
+ client,
75
+ serverCapabilities,
76
+ });
77
+
78
+ await startSseServer({
79
+ server,
80
+ port: argv.port,
81
+ endpoint: argv.endpoint as `/${string}`,
82
+ });
@@ -0,0 +1,49 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import {
4
+ ListResourcesRequestSchema,
5
+ ReadResourceRequestSchema,
6
+ } from "@modelcontextprotocol/sdk/types.js";
7
+
8
+ const server = new Server(
9
+ {
10
+ name: "example-server",
11
+ version: "1.0.0",
12
+ },
13
+ {
14
+ capabilities: {
15
+ resources: {},
16
+ },
17
+ },
18
+ );
19
+
20
+ server.setRequestHandler(ListResourcesRequestSchema, async () => {
21
+ return {
22
+ resources: [
23
+ {
24
+ uri: "file:///example.txt",
25
+ name: "Example Resource",
26
+ },
27
+ ],
28
+ };
29
+ });
30
+
31
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
32
+ if (request.params.uri === "file:///example.txt") {
33
+ return {
34
+ contents: [
35
+ {
36
+ uri: "file:///example.txt",
37
+ mimeType: "text/plain",
38
+ text: "This is the content of the example resource.",
39
+ },
40
+ ],
41
+ };
42
+ } else {
43
+ throw new Error("Resource not found");
44
+ }
45
+ });
46
+
47
+ const transport = new StdioServerTransport();
48
+
49
+ await server.connect(transport);
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "@tsconfig/node22/tsconfig.json",
3
+ "compilerOptions": {
4
+ "noEmit": true,
5
+ "noUnusedLocals": true,
6
+ "noUnusedParameters": true
7
+ }
8
+ }