@diegoaltoworks/localize-remote-mcp-server 1.0.1 → 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,40 +1,68 @@
1
- # @diegoaltoworks/localize-remote-mcp-server
1
+ # Localize Remote MCP Server
2
2
 
3
- Proxy a remote HTTP MCP server to localhost.
3
+ Bridge any remote HTTP MCP server into Claude Desktop.
4
4
 
5
- ## Why
5
+ ## The problem
6
6
 
7
- Claude Desktop currently only supports connecting to local MCP servers. If your MCP server is hosted remotely over HTTP, Claude Desktop can't reach it directly. This project bridges that gap — it runs a lightweight local proxy that forwards all MCP traffic to your remote server, letting Claude Desktop (and any other client with the same limitation) work with remote MCP servers as if they were local.
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
8
 
9
- ## Usage
9
+ ![The broken approach — Claude Desktop cannot connect directly to a remote HTTP/SSE MCP server](docs/mcp_broken_approach.svg)
10
10
 
11
- ```bash
12
- npx @diegoaltoworks/localize-remote-mcp-server --port 6969 https://example.com/mcp
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)
16
+
17
+ ## Setup
18
+
19
+ 1. Open your Claude Desktop config file `claude_desktop_config.json`:
20
+ - **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
21
+ - **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
22
+
23
+ 2. Add your remote MCP server to the `mcpServers` section. Replace the URL with your remote server's MCP endpoint:
24
+
25
+ ```json
26
+ {
27
+ "mcpServers": {
28
+ "my-remote-server": {
29
+ "command": "npx",
30
+ "args": [
31
+ "@diegoaltoworks/localize-remote-mcp-server",
32
+ "https://my-remote-server.example.com/mcp"
33
+ ]
34
+ }
35
+ }
36
+ }
13
37
  ```
14
38
 
15
- This starts a local server at `http://localhost:6969/mcp` that proxies all MCP requests to the remote server.
39
+ 3. Restart Claude Desktop.
16
40
 
17
- ## Options
41
+ That's it. Claude Desktop will launch the bridge process in the background and your remote server's tools will appear in Claude.
18
42
 
19
- | Flag | Description | Default |
20
- |------|-------------|---------|
21
- | `-p, --port` | Local port to listen on | `3000` |
22
- | `-h, --help` | Show help message | |
43
+ ## How it works
23
44
 
24
- ## Example
45
+ When Claude Desktop starts, it runs the `npx` command from your config. The bridge process:
25
46
 
26
- ```bash
27
- # Proxy a remote MCP server to localhost:6969
28
- npx @diegoaltoworks/localize-remote-mcp-server --port 6969 https://london-toll-checker.diegoalto.app/mcp
47
+ 1. Receives JSON-RPC messages from Claude Desktop over stdin
48
+ 2. Forwards them as HTTP POST requests to your remote MCP server
49
+ 3. Parses the response (JSON or SSE) and writes it back to stdout
50
+
51
+ Claude Desktop sees it as a normal local MCP server. Your remote server receives normal HTTP MCP requests. Neither side knows about the bridge.
29
52
 
30
- # Proxying https://london-toll-checker.diegoalto.app/mcp -> http://localhost:6969/mcp
53
+ ## HTTP proxy mode
54
+
55
+ If you need a local HTTP proxy instead of stdio (for other MCP clients), you can run it directly with `--port`:
56
+
57
+ ```bash
58
+ npx @diegoaltoworks/localize-remote-mcp-server --port 6969 https://my-remote-server.example.com/mcp
31
59
  ```
32
60
 
33
- Then point your MCP client at `http://localhost:6969/mcp` instead of the remote URL.
61
+ This is not needed for Claude Desktop.
34
62
 
35
63
  ## What it does
36
64
 
37
- - Proxies POST and GET requests to the remote MCP server
38
- - Streams SSE responses back to the client
65
+ - Bridges stdio and HTTP MCP transports
66
+ - Parses SSE responses from remote servers
39
67
  - Forwards authorization headers and MCP session IDs
40
68
  - Zero dependencies — just Node.js
package/bin/cli.js CHANGED
@@ -1,33 +1,48 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { parseArgs } from "node:util";
4
- import { createProxy } from "../src/proxy.js";
5
-
6
- const { values, positionals } = parseArgs({
7
- options: {
8
- port: { type: "string", short: "p", default: "3000" },
9
- help: { type: "boolean", short: "h", default: false },
10
- },
11
- allowPositionals: true,
12
- });
13
-
14
- if (values.help || positionals.length === 0) {
15
- console.log(`
16
- Usage: localize-remote-mcp-server [options] <remote-url>
17
-
18
- Proxy a remote HTTP MCP server to localhost.
19
-
20
- Options:
21
- -p, --port <port> Local port to listen on (default: 3000)
22
- -h, --help Show this help message
23
-
24
- Example:
25
- npx @diegoaltoworks/localize-remote-mcp-server --port 6969 https://example.com/mcp
26
- `);
27
- 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
+ }
28
18
  }
29
19
 
30
- const remoteUrl = positionals[0];
31
- const port = parseInt(values.port, 10);
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
+ }
32
41
 
33
- createProxy({ remoteUrl, port });
42
+ if (port) {
43
+ var proxy = require("../src/proxy");
44
+ proxy.createProxy({ remoteUrl: remoteUrl, port: port });
45
+ } else {
46
+ var stdio = require("../src/stdio");
47
+ stdio.createStdioBridge({ remoteUrl: remoteUrl });
48
+ }
package/package.json CHANGED
@@ -1,16 +1,22 @@
1
1
  {
2
2
  "name": "@diegoaltoworks/localize-remote-mcp-server",
3
- "version": "1.0.1",
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 ADDED
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+
3
+ var readline = require("readline");
4
+ var https = require("https");
5
+ var http = require("http");
6
+ var url = require("url");
7
+
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 });
15
+
16
+ function maybeExit() {
17
+ if (stdinClosed && pending === 0) process.exit(0);
18
+ }
19
+
20
+ rl.on("line", function (line) {
21
+ if (!line.trim()) return;
22
+
23
+ var message;
24
+ try {
25
+ message = JSON.parse(line);
26
+ } catch (e) {
27
+ return;
28
+ }
29
+
30
+ pending++;
31
+
32
+ var parsed = url.parse(remoteUrl);
33
+ var mod = parsed.protocol === "https:" ? https : http;
34
+
35
+ var reqHeaders = {
36
+ "content-type": "application/json",
37
+ accept: "application/json, text/event-stream",
38
+ "content-length": Buffer.byteLength(line),
39
+ };
40
+ if (sessionId) {
41
+ reqHeaders["mcp-session-id"] = sessionId;
42
+ }
43
+
44
+ var reqOpts = {
45
+ hostname: parsed.hostname,
46
+ port: parsed.port,
47
+ path: parsed.path,
48
+ method: "POST",
49
+ headers: reqHeaders,
50
+ };
51
+
52
+ var req = mod.request(reqOpts, function (res) {
53
+ var newSessionId = res.headers["mcp-session-id"];
54
+ if (newSessionId) {
55
+ sessionId = newSessionId;
56
+ }
57
+
58
+ var contentType = res.headers["content-type"] || "";
59
+ var chunks = [];
60
+
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
+ }
76
+ }
77
+ }
78
+ } else {
79
+ if (body.trim()) {
80
+ process.stdout.write(body.trim() + "\n");
81
+ }
82
+ }
83
+
84
+ pending--;
85
+ maybeExit();
86
+ });
87
+ });
88
+
89
+ req.on("error", function (err) {
90
+ var errorResponse = {
91
+ jsonrpc: "2.0",
92
+ error: { code: -32000, message: "Proxy error: " + err.message },
93
+ id: message.id != null ? message.id : null,
94
+ };
95
+ process.stdout.write(JSON.stringify(errorResponse) + "\n");
96
+ pending--;
97
+ maybeExit();
98
+ });
99
+
100
+ req.write(line);
101
+ req.end();
102
+ });
103
+
104
+ rl.on("close", function () {
105
+ stdinClosed = true;
106
+ maybeExit();
107
+ });
108
+ }
109
+
110
+ exports.createStdioBridge = createStdioBridge;