@diegoaltoworks/localize-remote-mcp-server 1.0.1 → 1.0.2
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/README.md +39 -19
- package/bin/cli.js +21 -8
- package/package.json +1 -1
- package/src/stdio.js +85 -0
package/README.md
CHANGED
|
@@ -1,40 +1,60 @@
|
|
|
1
1
|
# @diegoaltoworks/localize-remote-mcp-server
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Bridge any remote HTTP MCP server into Claude Desktop.
|
|
4
4
|
|
|
5
5
|
## Why
|
|
6
6
|
|
|
7
|
-
Claude Desktop
|
|
7
|
+
Claude Desktop only supports MCP servers that run locally as commands (stdio transport). It cannot connect to remote MCP servers over HTTP. This package bridges that gap — you configure it in Claude Desktop's config file, Claude launches it automatically, and it translates between stdio and your remote server's HTTP endpoint. You never run this yourself.
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## Setup
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
1. Open your Claude Desktop config file `claude_desktop_config.json`:
|
|
12
|
+
- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
13
|
+
- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
|
|
14
|
+
|
|
15
|
+
2. Add your remote MCP server to the `mcpServers` section. Replace the URL with your remote server's MCP endpoint:
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"mcpServers": {
|
|
20
|
+
"my-remote-server": {
|
|
21
|
+
"command": "npx",
|
|
22
|
+
"args": [
|
|
23
|
+
"@diegoaltoworks/localize-remote-mcp-server",
|
|
24
|
+
"https://my-remote-server.example.com/mcp"
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
13
29
|
```
|
|
14
30
|
|
|
15
|
-
|
|
31
|
+
3. Restart Claude Desktop.
|
|
16
32
|
|
|
17
|
-
|
|
33
|
+
That's it. Claude Desktop will launch the bridge process in the background and your remote server's tools will appear in Claude.
|
|
18
34
|
|
|
19
|
-
|
|
20
|
-
|------|-------------|---------|
|
|
21
|
-
| `-p, --port` | Local port to listen on | `3000` |
|
|
22
|
-
| `-h, --help` | Show help message | |
|
|
35
|
+
## How it works
|
|
23
36
|
|
|
24
|
-
|
|
37
|
+
When Claude Desktop starts, it runs the `npx` command from your config. The bridge process:
|
|
25
38
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
39
|
+
1. Receives JSON-RPC messages from Claude Desktop over stdin
|
|
40
|
+
2. Forwards them as HTTP POST requests to your remote MCP server
|
|
41
|
+
3. Parses the response (JSON or SSE) and writes it back to stdout
|
|
29
42
|
|
|
30
|
-
|
|
43
|
+
Claude Desktop sees it as a normal local MCP server. Your remote server receives normal HTTP MCP requests. Neither side knows about the bridge.
|
|
44
|
+
|
|
45
|
+
## HTTP proxy mode
|
|
46
|
+
|
|
47
|
+
If you need a local HTTP proxy instead of stdio (for other MCP clients), you can run it directly with `--port`:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npx @diegoaltoworks/localize-remote-mcp-server --port 6969 https://my-remote-server.example.com/mcp
|
|
31
51
|
```
|
|
32
52
|
|
|
33
|
-
|
|
53
|
+
This is not needed for Claude Desktop.
|
|
34
54
|
|
|
35
55
|
## What it does
|
|
36
56
|
|
|
37
|
-
-
|
|
38
|
-
-
|
|
57
|
+
- Bridges stdio and HTTP MCP transports
|
|
58
|
+
- Parses SSE responses from remote servers
|
|
39
59
|
- Forwards authorization headers and MCP session IDs
|
|
40
60
|
- Zero dependencies — just Node.js
|
package/bin/cli.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { parseArgs } from "node:util";
|
|
4
|
-
import { createProxy } from "../src/proxy.js";
|
|
5
4
|
|
|
6
5
|
const { values, positionals } = parseArgs({
|
|
7
6
|
options: {
|
|
8
|
-
port: { type: "string", short: "p"
|
|
7
|
+
port: { type: "string", short: "p" },
|
|
9
8
|
help: { type: "boolean", short: "h", default: false },
|
|
10
9
|
},
|
|
11
10
|
allowPositionals: true,
|
|
@@ -15,19 +14,33 @@ if (values.help || positionals.length === 0) {
|
|
|
15
14
|
console.log(`
|
|
16
15
|
Usage: localize-remote-mcp-server [options] <remote-url>
|
|
17
16
|
|
|
18
|
-
Proxy a remote HTTP MCP server
|
|
17
|
+
Proxy a remote HTTP MCP server for local use.
|
|
18
|
+
|
|
19
|
+
Modes:
|
|
20
|
+
Default (stdio) Bridges stdin/stdout to the remote server.
|
|
21
|
+
Use this with Claude Desktop.
|
|
22
|
+
--port <port> Runs a local HTTP proxy server instead.
|
|
19
23
|
|
|
20
24
|
Options:
|
|
21
|
-
-p, --port <port> Local port
|
|
25
|
+
-p, --port <port> Local port for HTTP proxy mode
|
|
22
26
|
-h, --help Show this help message
|
|
23
27
|
|
|
24
|
-
|
|
25
|
-
|
|
28
|
+
Examples:
|
|
29
|
+
# stdio mode (for Claude Desktop)
|
|
30
|
+
npx @diegoaltoworks/localize-remote-mcp-server https://my-remote-server.example.com/mcp
|
|
31
|
+
|
|
32
|
+
# HTTP proxy mode
|
|
33
|
+
npx @diegoaltoworks/localize-remote-mcp-server --port 6969 https://my-remote-server.example.com/mcp
|
|
26
34
|
`);
|
|
27
35
|
process.exit(values.help ? 0 : 1);
|
|
28
36
|
}
|
|
29
37
|
|
|
30
38
|
const remoteUrl = positionals[0];
|
|
31
|
-
const port = parseInt(values.port, 10);
|
|
32
39
|
|
|
33
|
-
|
|
40
|
+
if (values.port) {
|
|
41
|
+
const { createProxy } = await import("../src/proxy.js");
|
|
42
|
+
createProxy({ remoteUrl, port: parseInt(values.port, 10) });
|
|
43
|
+
} else {
|
|
44
|
+
const { createStdioBridge } = await import("../src/stdio.js");
|
|
45
|
+
createStdioBridge({ remoteUrl });
|
|
46
|
+
}
|
package/package.json
CHANGED
package/src/stdio.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { createInterface } from "node:readline";
|
|
2
|
+
|
|
3
|
+
export function createStdioBridge({ remoteUrl }) {
|
|
4
|
+
let sessionId = null;
|
|
5
|
+
let pending = 0;
|
|
6
|
+
let stdinClosed = false;
|
|
7
|
+
|
|
8
|
+
const rl = createInterface({ input: process.stdin });
|
|
9
|
+
|
|
10
|
+
function maybeExit() {
|
|
11
|
+
if (stdinClosed && pending === 0) process.exit(0);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
rl.on("line", async (line) => {
|
|
15
|
+
if (!line.trim()) return;
|
|
16
|
+
|
|
17
|
+
let message;
|
|
18
|
+
try {
|
|
19
|
+
message = JSON.parse(line);
|
|
20
|
+
} catch {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
pending++;
|
|
25
|
+
|
|
26
|
+
const headers = {
|
|
27
|
+
"content-type": "application/json",
|
|
28
|
+
accept: "application/json, text/event-stream",
|
|
29
|
+
};
|
|
30
|
+
if (sessionId) {
|
|
31
|
+
headers["mcp-session-id"] = sessionId;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const response = await fetch(remoteUrl, {
|
|
36
|
+
method: "POST",
|
|
37
|
+
headers,
|
|
38
|
+
body: line,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Capture session ID from response
|
|
42
|
+
const newSessionId = response.headers.get("mcp-session-id");
|
|
43
|
+
if (newSessionId) {
|
|
44
|
+
sessionId = newSessionId;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const contentType = response.headers.get("content-type") || "";
|
|
48
|
+
|
|
49
|
+
if (contentType.includes("text/event-stream")) {
|
|
50
|
+
// Parse SSE and extract JSON-RPC messages
|
|
51
|
+
const text = await response.text();
|
|
52
|
+
for (const sseLine of text.split("\n")) {
|
|
53
|
+
if (sseLine.startsWith("data: ")) {
|
|
54
|
+
const data = sseLine.slice(6).trim();
|
|
55
|
+
if (data) {
|
|
56
|
+
process.stdout.write(data + "\n");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
// Plain JSON response
|
|
62
|
+
const text = await response.text();
|
|
63
|
+
if (text.trim()) {
|
|
64
|
+
process.stdout.write(text.trim() + "\n");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
} catch (err) {
|
|
68
|
+
// Write JSON-RPC error to stdout
|
|
69
|
+
const errorResponse = {
|
|
70
|
+
jsonrpc: "2.0",
|
|
71
|
+
error: { code: -32000, message: `Proxy error: ${err.message}` },
|
|
72
|
+
id: message.id ?? null,
|
|
73
|
+
};
|
|
74
|
+
process.stdout.write(JSON.stringify(errorResponse) + "\n");
|
|
75
|
+
} finally {
|
|
76
|
+
pending--;
|
|
77
|
+
maybeExit();
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
rl.on("close", () => {
|
|
82
|
+
stdinClosed = true;
|
|
83
|
+
maybeExit();
|
|
84
|
+
});
|
|
85
|
+
}
|