@diegoaltoworks/localize-remote-mcp-server 1.0.2 → 1.1.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/README.md +11 -3
- package/bin/cli.js +41 -39
- package/package.json +10 -4
- package/src/proxy.js +55 -51
- package/src/stdio.js +68 -43
package/README.md
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Localize Remote MCP Server
|
|
2
2
|
|
|
3
3
|
Bridge any remote HTTP MCP server into Claude Desktop.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## The problem
|
|
6
6
|
|
|
7
|
-
Claude Desktop only supports MCP servers that run locally as commands (stdio transport). It cannot connect to remote MCP servers over HTTP.
|
|
7
|
+
Claude Desktop only supports MCP servers that run locally as commands (stdio transport). It cannot connect to remote MCP servers over HTTP. The two transports are fundamentally incompatible:
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
## The solution
|
|
12
|
+
|
|
13
|
+
This package bridges the 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 — Claude does.
|
|
14
|
+
|
|
15
|
+

|
|
8
16
|
|
|
9
17
|
## Setup
|
|
10
18
|
|
package/bin/cli.js
CHANGED
|
@@ -1,46 +1,48 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
if (
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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.
|
|
23
|
-
|
|
24
|
-
Options:
|
|
25
|
-
-p, --port <port> Local port for HTTP proxy mode
|
|
26
|
-
-h, --help Show this help message
|
|
27
|
-
|
|
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
|
|
34
|
-
`);
|
|
35
|
-
process.exit(values.help ? 0 : 1);
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
var args = process.argv.slice(2);
|
|
6
|
+
var port = null;
|
|
7
|
+
var remoteUrl = null;
|
|
8
|
+
var help = false;
|
|
9
|
+
|
|
10
|
+
for (var i = 0; i < args.length; i++) {
|
|
11
|
+
if (args[i] === "-h" || args[i] === "--help") {
|
|
12
|
+
help = true;
|
|
13
|
+
} else if (args[i] === "-p" || args[i] === "--port") {
|
|
14
|
+
port = parseInt(args[++i], 10);
|
|
15
|
+
} else if (!remoteUrl) {
|
|
16
|
+
remoteUrl = args[i];
|
|
17
|
+
}
|
|
36
18
|
}
|
|
37
19
|
|
|
38
|
-
|
|
20
|
+
if (help || !remoteUrl) {
|
|
21
|
+
console.log("");
|
|
22
|
+
console.log("Usage: localize-remote-mcp-server [options] <remote-url>");
|
|
23
|
+
console.log("");
|
|
24
|
+
console.log("Proxy a remote HTTP MCP server for local use.");
|
|
25
|
+
console.log("");
|
|
26
|
+
console.log("Modes:");
|
|
27
|
+
console.log(" Default (stdio) Bridges stdin/stdout to the remote server.");
|
|
28
|
+
console.log(" Use this with Claude Desktop.");
|
|
29
|
+
console.log(" --port <port> Runs a local HTTP proxy server instead.");
|
|
30
|
+
console.log("");
|
|
31
|
+
console.log("Options:");
|
|
32
|
+
console.log(" -p, --port <port> Local port for HTTP proxy mode");
|
|
33
|
+
console.log(" -h, --help Show this help message");
|
|
34
|
+
console.log("");
|
|
35
|
+
console.log("Examples:");
|
|
36
|
+
console.log(" npx @diegoaltoworks/localize-remote-mcp-server https://my-server.example.com/mcp");
|
|
37
|
+
console.log(" npx @diegoaltoworks/localize-remote-mcp-server --port 6969 https://my-server.example.com/mcp");
|
|
38
|
+
console.log("");
|
|
39
|
+
process.exit(help ? 0 : 1);
|
|
40
|
+
}
|
|
39
41
|
|
|
40
|
-
if (
|
|
41
|
-
|
|
42
|
-
createProxy({ remoteUrl, port:
|
|
42
|
+
if (port) {
|
|
43
|
+
var proxy = require("../src/proxy");
|
|
44
|
+
proxy.createProxy({ remoteUrl: remoteUrl, port: port });
|
|
43
45
|
} else {
|
|
44
|
-
|
|
45
|
-
createStdioBridge({ remoteUrl });
|
|
46
|
+
var stdio = require("../src/stdio");
|
|
47
|
+
stdio.createStdioBridge({ remoteUrl: remoteUrl });
|
|
46
48
|
}
|
package/package.json
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diegoaltoworks/localize-remote-mcp-server",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Proxy a remote HTTP MCP server to localhost",
|
|
5
5
|
"bin": {
|
|
6
6
|
"localize-remote-mcp-server": "./bin/cli.js"
|
|
7
7
|
},
|
|
8
|
-
"type": "module",
|
|
9
8
|
"license": "MIT",
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=12"
|
|
11
|
+
},
|
|
10
12
|
"files": [
|
|
11
|
-
"bin",
|
|
12
|
-
"src"
|
|
13
|
+
"bin/cli.js",
|
|
14
|
+
"src/proxy.js",
|
|
15
|
+
"src/stdio.js"
|
|
13
16
|
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "node --test src/*.test.js bin/*.test.js"
|
|
19
|
+
},
|
|
14
20
|
"keywords": [
|
|
15
21
|
"mcp",
|
|
16
22
|
"proxy",
|
package/src/proxy.js
CHANGED
|
@@ -1,84 +1,88 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
var http = require("http");
|
|
4
|
+
var https = require("https");
|
|
5
|
+
var url = require("url");
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
function createProxy(opts) {
|
|
8
|
+
var remoteUrl = opts.remoteUrl;
|
|
9
|
+
var port = opts.port;
|
|
10
|
+
var parsed = url.parse(remoteUrl);
|
|
11
|
+
var localPath = parsed.pathname;
|
|
12
|
+
var mod = parsed.protocol === "https:" ? https : http;
|
|
13
|
+
|
|
14
|
+
var server = http.createServer(function (req, res) {
|
|
9
15
|
if (req.url !== localPath) {
|
|
10
16
|
res.writeHead(404, { "content-type": "text/plain" });
|
|
11
|
-
res.end(
|
|
17
|
+
res.end("Not found. MCP endpoint is at " + localPath + "\n");
|
|
12
18
|
return;
|
|
13
19
|
}
|
|
14
20
|
|
|
15
|
-
// MCP Streamable HTTP uses POST and GET
|
|
16
21
|
if (req.method !== "POST" && req.method !== "GET") {
|
|
17
22
|
res.writeHead(405, { "content-type": "text/plain" });
|
|
18
23
|
res.end("Method not allowed.\n");
|
|
19
24
|
return;
|
|
20
25
|
}
|
|
21
26
|
|
|
22
|
-
|
|
23
|
-
|
|
27
|
+
var bodyChunks = [];
|
|
28
|
+
|
|
29
|
+
req.on("data", function (chunk) {
|
|
30
|
+
bodyChunks.push(chunk);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
req.on("end", function () {
|
|
34
|
+
var body = Buffer.concat(bodyChunks);
|
|
24
35
|
|
|
25
|
-
|
|
26
|
-
|
|
36
|
+
var reqHeaders = {
|
|
37
|
+
accept:
|
|
38
|
+
req.headers["accept"] || "application/json, text/event-stream",
|
|
39
|
+
};
|
|
27
40
|
if (req.headers["content-type"]) {
|
|
28
|
-
|
|
41
|
+
reqHeaders["content-type"] = req.headers["content-type"];
|
|
29
42
|
}
|
|
30
|
-
// MCP Streamable HTTP requires accepting both JSON and SSE
|
|
31
|
-
headers["accept"] =
|
|
32
|
-
req.headers["accept"] || "application/json, text/event-stream";
|
|
33
43
|
if (req.headers["authorization"]) {
|
|
34
|
-
|
|
44
|
+
reqHeaders["authorization"] = req.headers["authorization"];
|
|
35
45
|
}
|
|
36
46
|
if (req.headers["mcp-session-id"]) {
|
|
37
|
-
|
|
47
|
+
reqHeaders["mcp-session-id"] = req.headers["mcp-session-id"];
|
|
48
|
+
}
|
|
49
|
+
if (req.method === "POST") {
|
|
50
|
+
reqHeaders["content-length"] = body.length;
|
|
38
51
|
}
|
|
39
52
|
|
|
40
|
-
|
|
53
|
+
var reqOpts = {
|
|
54
|
+
hostname: parsed.hostname,
|
|
55
|
+
port: parsed.port,
|
|
56
|
+
path: parsed.path,
|
|
41
57
|
method: req.method,
|
|
42
|
-
headers,
|
|
43
|
-
|
|
58
|
+
headers: reqHeaders,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
var proxyReq = mod.request(reqOpts, function (proxyRes) {
|
|
62
|
+
res.writeHead(proxyRes.statusCode, proxyRes.headers);
|
|
63
|
+
proxyRes.pipe(res);
|
|
44
64
|
});
|
|
45
65
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
res.writeHead(response.status, responseHeaders);
|
|
66
|
+
proxyReq.on("error", function (err) {
|
|
67
|
+
console.error("Proxy error:", err.message);
|
|
68
|
+
res.writeHead(502, { "content-type": "text/plain" });
|
|
69
|
+
res.end("Bad gateway: " + err.message + "\n");
|
|
70
|
+
});
|
|
52
71
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const reader = response.body.getReader();
|
|
56
|
-
while (true) {
|
|
57
|
-
const { done, value } = await reader.read();
|
|
58
|
-
if (done) break;
|
|
59
|
-
res.write(value);
|
|
60
|
-
}
|
|
72
|
+
if (req.method === "POST") {
|
|
73
|
+
proxyReq.write(body);
|
|
61
74
|
}
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
console.error("Proxy error:", err.message);
|
|
65
|
-
res.writeHead(502, { "content-type": "text/plain" });
|
|
66
|
-
res.end(`Bad gateway: ${err.message}\n`);
|
|
67
|
-
}
|
|
75
|
+
proxyReq.end();
|
|
76
|
+
});
|
|
68
77
|
});
|
|
69
78
|
|
|
70
|
-
server.listen(port, ()
|
|
71
|
-
console.log(
|
|
79
|
+
server.listen(port, function () {
|
|
80
|
+
console.log(
|
|
81
|
+
"Proxying " + remoteUrl + " → http://localhost:" + port + localPath
|
|
82
|
+
);
|
|
72
83
|
});
|
|
73
84
|
|
|
74
85
|
return server;
|
|
75
86
|
}
|
|
76
87
|
|
|
77
|
-
|
|
78
|
-
return new Promise((resolve, reject) => {
|
|
79
|
-
const chunks = [];
|
|
80
|
-
req.on("data", (chunk) => chunks.push(chunk));
|
|
81
|
-
req.on("end", () => resolve(Buffer.concat(chunks)));
|
|
82
|
-
req.on("error", reject);
|
|
83
|
-
});
|
|
84
|
-
}
|
|
88
|
+
exports.createProxy = createProxy;
|
package/src/stdio.js
CHANGED
|
@@ -1,85 +1,110 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
var readline = require("readline");
|
|
4
|
+
var https = require("https");
|
|
5
|
+
var http = require("http");
|
|
6
|
+
var url = require("url");
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
function createStdioBridge(opts) {
|
|
9
|
+
var remoteUrl = opts.remoteUrl;
|
|
10
|
+
var sessionId = null;
|
|
11
|
+
var pending = 0;
|
|
12
|
+
var stdinClosed = false;
|
|
13
|
+
|
|
14
|
+
var rl = readline.createInterface({ input: process.stdin });
|
|
9
15
|
|
|
10
16
|
function maybeExit() {
|
|
11
17
|
if (stdinClosed && pending === 0) process.exit(0);
|
|
12
18
|
}
|
|
13
19
|
|
|
14
|
-
rl.on("line",
|
|
20
|
+
rl.on("line", function (line) {
|
|
15
21
|
if (!line.trim()) return;
|
|
16
22
|
|
|
17
|
-
|
|
23
|
+
var message;
|
|
18
24
|
try {
|
|
19
25
|
message = JSON.parse(line);
|
|
20
|
-
} catch {
|
|
26
|
+
} catch (e) {
|
|
21
27
|
return;
|
|
22
28
|
}
|
|
23
29
|
|
|
24
30
|
pending++;
|
|
25
31
|
|
|
26
|
-
|
|
32
|
+
var parsed = url.parse(remoteUrl);
|
|
33
|
+
var mod = parsed.protocol === "https:" ? https : http;
|
|
34
|
+
|
|
35
|
+
var reqHeaders = {
|
|
27
36
|
"content-type": "application/json",
|
|
28
37
|
accept: "application/json, text/event-stream",
|
|
38
|
+
"content-length": Buffer.byteLength(line),
|
|
29
39
|
};
|
|
30
40
|
if (sessionId) {
|
|
31
|
-
|
|
41
|
+
reqHeaders["mcp-session-id"] = sessionId;
|
|
32
42
|
}
|
|
33
43
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
44
|
+
var reqOpts = {
|
|
45
|
+
hostname: parsed.hostname,
|
|
46
|
+
port: parsed.port,
|
|
47
|
+
path: parsed.path,
|
|
48
|
+
method: "POST",
|
|
49
|
+
headers: reqHeaders,
|
|
50
|
+
};
|
|
40
51
|
|
|
41
|
-
|
|
42
|
-
|
|
52
|
+
var req = mod.request(reqOpts, function (res) {
|
|
53
|
+
var newSessionId = res.headers["mcp-session-id"];
|
|
43
54
|
if (newSessionId) {
|
|
44
55
|
sessionId = newSessionId;
|
|
45
56
|
}
|
|
46
57
|
|
|
47
|
-
|
|
58
|
+
var contentType = res.headers["content-type"] || "";
|
|
59
|
+
var chunks = [];
|
|
48
60
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
61
|
+
res.on("data", function (chunk) {
|
|
62
|
+
chunks.push(chunk);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
res.on("end", function () {
|
|
66
|
+
var body = Buffer.concat(chunks).toString();
|
|
67
|
+
|
|
68
|
+
if (contentType.indexOf("text/event-stream") !== -1) {
|
|
69
|
+
var lines = body.split("\n");
|
|
70
|
+
for (var i = 0; i < lines.length; i++) {
|
|
71
|
+
if (lines[i].indexOf("data: ") === 0) {
|
|
72
|
+
var data = lines[i].slice(6).trim();
|
|
73
|
+
if (data) {
|
|
74
|
+
process.stdout.write(data + "\n");
|
|
75
|
+
}
|
|
57
76
|
}
|
|
58
77
|
}
|
|
78
|
+
} else {
|
|
79
|
+
if (body.trim()) {
|
|
80
|
+
process.stdout.write(body.trim() + "\n");
|
|
81
|
+
}
|
|
59
82
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
// Write JSON-RPC error to stdout
|
|
69
|
-
const errorResponse = {
|
|
83
|
+
|
|
84
|
+
pending--;
|
|
85
|
+
maybeExit();
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
req.on("error", function (err) {
|
|
90
|
+
var errorResponse = {
|
|
70
91
|
jsonrpc: "2.0",
|
|
71
|
-
error: { code: -32000, message:
|
|
72
|
-
id: message.id
|
|
92
|
+
error: { code: -32000, message: "Proxy error: " + err.message },
|
|
93
|
+
id: message.id != null ? message.id : null,
|
|
73
94
|
};
|
|
74
95
|
process.stdout.write(JSON.stringify(errorResponse) + "\n");
|
|
75
|
-
} finally {
|
|
76
96
|
pending--;
|
|
77
97
|
maybeExit();
|
|
78
|
-
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
req.write(line);
|
|
101
|
+
req.end();
|
|
79
102
|
});
|
|
80
103
|
|
|
81
|
-
rl.on("close", ()
|
|
104
|
+
rl.on("close", function () {
|
|
82
105
|
stdinClosed = true;
|
|
83
106
|
maybeExit();
|
|
84
107
|
});
|
|
85
108
|
}
|
|
109
|
+
|
|
110
|
+
exports.createStdioBridge = createStdioBridge;
|