@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 CHANGED
@@ -1,10 +1,18 @@
1
- # @diegoaltoworks/localize-remote-mcp-server
1
+ # Localize Remote MCP Server
2
2
 
3
3
  Bridge any remote HTTP MCP server into Claude Desktop.
4
4
 
5
- ## Why
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. 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.
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
+ ![The broken approach — Claude Desktop cannot connect directly to a remote HTTP/SSE MCP server](docs/mcp_broken_approach.svg)
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
+ ![The working solution — localize-remote-mcp-server acts as a local stdio bridge](docs/mcp_working_solution.svg)
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
- import { parseArgs } from "node:util";
4
-
5
- const { values, positionals } = parseArgs({
6
- options: {
7
- port: { type: "string", short: "p" },
8
- help: { type: "boolean", short: "h", default: false },
9
- },
10
- allowPositionals: true,
11
- });
12
-
13
- if (values.help || positionals.length === 0) {
14
- console.log(`
15
- Usage: localize-remote-mcp-server [options] <remote-url>
16
-
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.
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
- const remoteUrl = positionals[0];
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 (values.port) {
41
- const { createProxy } = await import("../src/proxy.js");
42
- createProxy({ remoteUrl, port: parseInt(values.port, 10) });
42
+ if (port) {
43
+ var proxy = require("../src/proxy");
44
+ proxy.createProxy({ remoteUrl: remoteUrl, port: port });
43
45
  } else {
44
- const { createStdioBridge } = await import("../src/stdio.js");
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.2",
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
- import { createServer } from "node:http";
1
+ "use strict";
2
2
 
3
- export function createProxy({ remoteUrl, port }) {
4
- const remote = new URL(remoteUrl);
5
- const localPath = remote.pathname;
3
+ var http = require("http");
4
+ var https = require("https");
5
+ var url = require("url");
6
6
 
7
- const server = createServer(async (req, res) => {
8
- // Only proxy requests to the MCP path
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(`Not found. MCP endpoint is at ${localPath}\n`);
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
- try {
23
- const body = req.method === "POST" ? await readBody(req) : undefined;
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
- // Forward all relevant headers
26
- const headers = {};
36
+ var reqHeaders = {
37
+ accept:
38
+ req.headers["accept"] || "application/json, text/event-stream",
39
+ };
27
40
  if (req.headers["content-type"]) {
28
- headers["content-type"] = req.headers["content-type"];
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
- headers["authorization"] = req.headers["authorization"];
44
+ reqHeaders["authorization"] = req.headers["authorization"];
35
45
  }
36
46
  if (req.headers["mcp-session-id"]) {
37
- headers["mcp-session-id"] = req.headers["mcp-session-id"];
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
- const response = await fetch(remoteUrl, {
53
+ var reqOpts = {
54
+ hostname: parsed.hostname,
55
+ port: parsed.port,
56
+ path: parsed.path,
41
57
  method: req.method,
42
- headers,
43
- body,
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
- // Forward status and headers back
47
- const responseHeaders = {};
48
- for (const [key, value] of response.headers) {
49
- responseHeaders[key] = value;
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
- // Stream the response body back
54
- if (response.body) {
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
- res.end();
63
- } catch (err) {
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(`Proxying ${remoteUrl} → http://localhost:${port}${localPath}`);
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
- function readBody(req) {
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
- import { createInterface } from "node:readline";
1
+ "use strict";
2
2
 
3
- export function createStdioBridge({ remoteUrl }) {
4
- let sessionId = null;
5
- let pending = 0;
6
- let stdinClosed = false;
3
+ var readline = require("readline");
4
+ var https = require("https");
5
+ var http = require("http");
6
+ var url = require("url");
7
7
 
8
- const rl = createInterface({ input: process.stdin });
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", async (line) => {
20
+ rl.on("line", function (line) {
15
21
  if (!line.trim()) return;
16
22
 
17
- let message;
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
- const headers = {
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
- headers["mcp-session-id"] = sessionId;
41
+ reqHeaders["mcp-session-id"] = sessionId;
32
42
  }
33
43
 
34
- try {
35
- const response = await fetch(remoteUrl, {
36
- method: "POST",
37
- headers,
38
- body: line,
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
- // Capture session ID from response
42
- const newSessionId = response.headers.get("mcp-session-id");
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
- const contentType = response.headers.get("content-type") || "";
58
+ var contentType = res.headers["content-type"] || "";
59
+ var chunks = [];
48
60
 
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");
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
- } 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 = {
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: `Proxy error: ${err.message}` },
72
- id: message.id ?? null,
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;