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.
- package/.github/workflows/feature.yaml +37 -0
- package/.github/workflows/main.yaml +48 -0
- package/LICENSE +24 -0
- package/README.md +81 -0
- package/dist/MCPProxy.d.ts +36 -0
- package/dist/MCPProxy.js +11 -0
- package/dist/MCPProxy.js.map +1 -0
- package/dist/bin/mcp-proxy.d.ts +1 -0
- package/dist/bin/mcp-proxy.js +68 -0
- package/dist/bin/mcp-proxy.js.map +1 -0
- package/dist/chunk-4AZHVXNQ.js +199 -0
- package/dist/chunk-4AZHVXNQ.js.map +1 -0
- package/eslint.config.js +3 -0
- package/package.json +71 -0
- package/src/MCPProxy.test.ts +80 -0
- package/src/MCPProxy.ts +287 -0
- package/src/bin/mcp-proxy.ts +82 -0
- package/src/simple-stdio-server.ts +49 -0
- package/tsconfig.json +8 -0
|
@@ -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 };
|
package/dist/MCPProxy.js
ADDED
|
@@ -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":[]}
|
package/eslint.config.js
ADDED
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
|
+
});
|
package/src/MCPProxy.ts
ADDED
|
@@ -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);
|