mcp-remote 0.0.13 → 0.0.15

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.
@@ -1,5 +1,11 @@
1
1
  var __getOwnPropNames = Object.getOwnPropertyNames;
2
- var __commonJS = (cb, mod) => function __require() {
2
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
3
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
4
+ }) : x)(function(x) {
5
+ if (typeof require !== "undefined") return require.apply(this, arguments);
6
+ throw Error('Dynamic require of "' + x + '" is not supported');
7
+ });
8
+ var __commonJS = (cb, mod) => function __require2() {
3
9
  return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
4
10
  };
5
11
 
@@ -8,7 +14,7 @@ var require_package = __commonJS({
8
14
  "package.json"(exports, module) {
9
15
  module.exports = {
10
16
  name: "mcp-remote",
11
- version: "0.0.13",
17
+ version: "0.0.14",
12
18
  description: "Remote proxy for Model Context Protocol, allowing local-only clients to connect to remote servers using oAuth",
13
19
  keywords: [
14
20
  "mcp",
@@ -18,7 +24,7 @@ var require_package = __commonJS({
18
24
  "oauth"
19
25
  ],
20
26
  author: "Glen Maddern <glen@cloudflare.com>",
21
- repository: "https://github.com/geelen/remote-mcp",
27
+ repository: "https://github.com/geelen/mcp-remote",
22
28
  type: "module",
23
29
  files: [
24
30
  "dist",
@@ -74,6 +80,8 @@ import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js";
74
80
  import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
75
81
  import express from "express";
76
82
  import net from "net";
83
+ import crypto from "crypto";
84
+ var MCP_REMOTE_VERSION = require_package().version;
77
85
  var pid = process.pid;
78
86
  function log(str, ...rest) {
79
87
  console.error(`[${pid}] ${str}`, ...rest);
@@ -112,7 +120,7 @@ function mcpProxy({ transportToClient, transportToServer }) {
112
120
  log("Error from remote server:", error);
113
121
  }
114
122
  }
115
- async function connectToRemoteServer(serverUrl, authProvider, waitForAuthCode) {
123
+ async function connectToRemoteServer(serverUrl, authProvider, waitForAuthCode, skipBrowserAuth = false) {
116
124
  log(`[${pid}] Connecting to remote server: ${serverUrl}`);
117
125
  const url = new URL(serverUrl);
118
126
  const transport = new SSEClientTransport(url, { authProvider });
@@ -122,7 +130,11 @@ async function connectToRemoteServer(serverUrl, authProvider, waitForAuthCode) {
122
130
  return transport;
123
131
  } catch (error) {
124
132
  if (error instanceof UnauthorizedError || error instanceof Error && error.message.includes("Unauthorized")) {
125
- log("Authentication required. Waiting for authorization...");
133
+ if (skipBrowserAuth) {
134
+ log("Authentication required but skipping browser auth - using shared auth");
135
+ } else {
136
+ log("Authentication required. Waiting for authorization...");
137
+ }
126
138
  const code = await waitForAuthCode();
127
139
  try {
128
140
  log("Completing authorization...");
@@ -141,9 +153,42 @@ async function connectToRemoteServer(serverUrl, authProvider, waitForAuthCode) {
141
153
  }
142
154
  }
143
155
  }
144
- function setupOAuthCallbackServer(options) {
156
+ function setupOAuthCallbackServerWithLongPoll(options) {
145
157
  let authCode = null;
146
158
  const app = express();
159
+ let authCompletedResolve;
160
+ const authCompletedPromise = new Promise((resolve) => {
161
+ authCompletedResolve = resolve;
162
+ });
163
+ app.get("/wait-for-auth", (req, res) => {
164
+ if (authCode) {
165
+ log("Auth already completed, returning 200");
166
+ res.status(200).send("Authentication completed");
167
+ return;
168
+ }
169
+ if (req.query.poll === "false") {
170
+ log("Client requested no long poll, responding with 202");
171
+ res.status(202).send("Authentication in progress");
172
+ return;
173
+ }
174
+ const longPollTimeout = setTimeout(() => {
175
+ log("Long poll timeout reached, responding with 202");
176
+ res.status(202).send("Authentication in progress");
177
+ }, 3e4);
178
+ authCompletedPromise.then(() => {
179
+ clearTimeout(longPollTimeout);
180
+ if (!res.headersSent) {
181
+ log("Auth completed during long poll, responding with 200");
182
+ res.status(200).send("Authentication completed");
183
+ }
184
+ }).catch(() => {
185
+ clearTimeout(longPollTimeout);
186
+ if (!res.headersSent) {
187
+ log("Auth failed during long poll, responding with 500");
188
+ res.status(500).send("Authentication failed");
189
+ }
190
+ });
191
+ });
147
192
  app.get(options.path, (req, res) => {
148
193
  const code = req.query.code;
149
194
  if (!code) {
@@ -151,6 +196,8 @@ function setupOAuthCallbackServer(options) {
151
196
  return;
152
197
  }
153
198
  authCode = code;
199
+ log("Auth code received, resolving promise");
200
+ authCompletedResolve(code);
154
201
  res.send("Authorization successful! You may close this window and return to the CLI.");
155
202
  options.events.emit("auth-code-received", code);
156
203
  });
@@ -168,7 +215,7 @@ function setupOAuthCallbackServer(options) {
168
215
  });
169
216
  });
170
217
  };
171
- return { server, authCode, waitForAuthCode };
218
+ return { server, authCode, waitForAuthCode, authCompletedPromise };
172
219
  }
173
220
  async function findAvailablePort(preferredPort) {
174
221
  return new Promise((resolve, reject) => {
@@ -226,7 +273,9 @@ function setupSignalHandlers(cleanup) {
226
273
  });
227
274
  process.stdin.resume();
228
275
  }
229
- var MCP_REMOTE_VERSION = require_package().version;
276
+ function getServerUrlHash(serverUrl) {
277
+ return crypto.createHash("md5").update(serverUrl).digest("hex");
278
+ }
230
279
 
231
280
  // src/lib/node-oauth-client-provider.ts
232
281
  import open from "open";
@@ -236,11 +285,37 @@ import {
236
285
  } from "@modelcontextprotocol/sdk/shared/auth.js";
237
286
 
238
287
  // src/lib/mcp-auth-config.ts
239
- import crypto from "crypto";
240
288
  import path from "path";
241
289
  import os from "os";
242
290
  import fs from "fs/promises";
243
- var knownConfigFiles = ["client_info.json", "tokens.json", "code_verifier.txt"];
291
+ var knownConfigFiles = ["client_info.json", "tokens.json", "code_verifier.txt", "lock.json"];
292
+ async function createLockfile(serverUrlHash, pid2, port) {
293
+ const lockData = {
294
+ pid: pid2,
295
+ port,
296
+ timestamp: Date.now()
297
+ };
298
+ await writeJsonFile(serverUrlHash, "lock.json", lockData);
299
+ }
300
+ async function checkLockfile(serverUrlHash) {
301
+ try {
302
+ const lockfile = await readJsonFile(serverUrlHash, "lock.json", {
303
+ async parseAsync(data) {
304
+ if (typeof data !== "object" || data === null) return null;
305
+ if (typeof data.pid !== "number" || typeof data.port !== "number" || typeof data.timestamp !== "number") {
306
+ return null;
307
+ }
308
+ return data;
309
+ }
310
+ });
311
+ return lockfile || null;
312
+ } catch {
313
+ return null;
314
+ }
315
+ }
316
+ async function deleteLockfile(serverUrlHash) {
317
+ await deleteConfigFile(serverUrlHash, "lock.json");
318
+ }
244
319
  async function cleanServerConfig(serverUrlHash) {
245
320
  log(`Cleaning configuration files for server: ${serverUrlHash}`);
246
321
  for (const filename of knownConfigFiles) {
@@ -260,9 +335,6 @@ async function ensureConfigDir() {
260
335
  throw error;
261
336
  }
262
337
  }
263
- function getServerUrlHash(serverUrl) {
264
- return crypto.createHash("md5").update(serverUrl).digest("hex");
265
- }
266
338
  function getConfigFilePath(serverUrlHash, filename) {
267
339
  const configDir = getConfigDir();
268
340
  return path.join(configDir, `${serverUrlHash}_${filename}`);
@@ -286,11 +358,13 @@ async function readJsonFile(serverUrlHash, filename, schema, clean = false) {
286
358
  }
287
359
  const filePath = getConfigFilePath(serverUrlHash, filename);
288
360
  const content = await fs.readFile(filePath, "utf-8");
289
- return await schema.parseAsync(JSON.parse(content));
361
+ const result = await schema.parseAsync(JSON.parse(content));
362
+ return result;
290
363
  } catch (error) {
291
364
  if (error.code === "ENOENT") {
292
365
  return void 0;
293
366
  }
367
+ log(`Error reading ${filename}:`, error);
294
368
  return void 0;
295
369
  }
296
370
  }
@@ -423,13 +497,134 @@ ${authorizationUrl.toString()}
423
497
  }
424
498
  };
425
499
 
500
+ // src/lib/coordination.ts
501
+ import express2 from "express";
502
+ async function isPidRunning(pid2) {
503
+ try {
504
+ process.kill(pid2, 0);
505
+ return true;
506
+ } catch {
507
+ return false;
508
+ }
509
+ }
510
+ async function isLockValid(lockData) {
511
+ const MAX_LOCK_AGE = 30 * 60 * 1e3;
512
+ if (Date.now() - lockData.timestamp > MAX_LOCK_AGE) {
513
+ log("Lockfile is too old");
514
+ return false;
515
+ }
516
+ if (!await isPidRunning(lockData.pid)) {
517
+ log("Process from lockfile is not running");
518
+ return false;
519
+ }
520
+ try {
521
+ const controller = new AbortController();
522
+ const timeout = setTimeout(() => controller.abort(), 1e3);
523
+ const response = await fetch(`http://127.0.0.1:${lockData.port}/wait-for-auth?poll=false`, {
524
+ signal: controller.signal
525
+ });
526
+ clearTimeout(timeout);
527
+ return response.status === 200 || response.status === 202;
528
+ } catch (error) {
529
+ log(`Error connecting to auth server: ${error.message}`);
530
+ return false;
531
+ }
532
+ }
533
+ async function waitForAuthentication(port) {
534
+ log(`Waiting for authentication from the server on port ${port}...`);
535
+ try {
536
+ while (true) {
537
+ const url = `http://127.0.0.1:${port}/wait-for-auth`;
538
+ log(`Querying: ${url}`);
539
+ const response = await fetch(url);
540
+ if (response.status === 200) {
541
+ log(`Authentication completed by other instance`);
542
+ return true;
543
+ } else if (response.status === 202) {
544
+ log(`Authentication still in progress`);
545
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
546
+ } else {
547
+ log(`Unexpected response status: ${response.status}`);
548
+ return false;
549
+ }
550
+ }
551
+ } catch (error) {
552
+ log(`Error waiting for authentication: ${error.message}`);
553
+ return false;
554
+ }
555
+ }
556
+ async function coordinateAuth(serverUrlHash, callbackPort, events) {
557
+ const lockData = await checkLockfile(serverUrlHash);
558
+ if (lockData && await isLockValid(lockData)) {
559
+ log(`Another instance is handling authentication on port ${lockData.port}`);
560
+ try {
561
+ const authCompleted = await waitForAuthentication(lockData.port);
562
+ if (authCompleted) {
563
+ log("Authentication completed by another instance");
564
+ const dummyServer = express2().listen(0);
565
+ const dummyWaitForAuthCode = () => {
566
+ log("WARNING: waitForAuthCode called in secondary instance - this is unexpected");
567
+ return new Promise(() => {
568
+ });
569
+ };
570
+ return {
571
+ server: dummyServer,
572
+ waitForAuthCode: dummyWaitForAuthCode,
573
+ skipBrowserAuth: true
574
+ };
575
+ } else {
576
+ log("Taking over authentication process...");
577
+ }
578
+ } catch (error) {
579
+ log(`Error waiting for authentication: ${error}`);
580
+ }
581
+ await deleteLockfile(serverUrlHash);
582
+ } else if (lockData) {
583
+ log("Found invalid lockfile, deleting it");
584
+ await deleteLockfile(serverUrlHash);
585
+ }
586
+ const { server, waitForAuthCode, authCompletedPromise } = setupOAuthCallbackServerWithLongPoll({
587
+ port: callbackPort,
588
+ path: "/oauth/callback",
589
+ events
590
+ });
591
+ const address = server.address();
592
+ const actualPort = address.port;
593
+ log(`Creating lockfile for server ${serverUrlHash} with process ${process.pid} on port ${actualPort}`);
594
+ await createLockfile(serverUrlHash, process.pid, actualPort);
595
+ const cleanupHandler = async () => {
596
+ try {
597
+ log(`Cleaning up lockfile for server ${serverUrlHash}`);
598
+ await deleteLockfile(serverUrlHash);
599
+ } catch (error) {
600
+ log(`Error cleaning up lockfile: ${error}`);
601
+ }
602
+ };
603
+ process.once("exit", () => {
604
+ try {
605
+ const configPath = getConfigFilePath(serverUrlHash, "lock.json");
606
+ __require("fs").unlinkSync(configPath);
607
+ } catch {
608
+ }
609
+ });
610
+ process.once("SIGINT", async () => {
611
+ await cleanupHandler();
612
+ });
613
+ return {
614
+ server,
615
+ waitForAuthCode,
616
+ skipBrowserAuth: false
617
+ };
618
+ }
619
+
426
620
  export {
621
+ MCP_REMOTE_VERSION,
427
622
  log,
428
623
  mcpProxy,
429
624
  connectToRemoteServer,
430
- setupOAuthCallbackServer,
431
625
  parseCommandLineArgs,
432
626
  setupSignalHandlers,
433
- MCP_REMOTE_VERSION,
434
- NodeOAuthClientProvider
627
+ getServerUrlHash,
628
+ NodeOAuthClientProvider,
629
+ coordinateAuth
435
630
  };
package/dist/client.js CHANGED
@@ -2,10 +2,12 @@
2
2
  import {
3
3
  MCP_REMOTE_VERSION,
4
4
  NodeOAuthClientProvider,
5
+ coordinateAuth,
6
+ getServerUrlHash,
7
+ log,
5
8
  parseCommandLineArgs,
6
- setupOAuthCallbackServer,
7
9
  setupSignalHandlers
8
- } from "./chunk-UL4C5N2N.js";
10
+ } from "./chunk-R4LSJHXI.js";
9
11
 
10
12
  // src/client.ts
11
13
  import { EventEmitter } from "events";
@@ -15,12 +17,18 @@ import { ListResourcesResultSchema, ListToolsResultSchema } from "@modelcontextp
15
17
  import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js";
16
18
  async function runClient(serverUrl, callbackPort, clean = false) {
17
19
  const events = new EventEmitter();
20
+ const serverUrlHash = getServerUrlHash(serverUrl);
21
+ const { server, waitForAuthCode, skipBrowserAuth } = await coordinateAuth(serverUrlHash, callbackPort, events);
18
22
  const authProvider = new NodeOAuthClientProvider({
19
23
  serverUrl,
20
24
  callbackPort,
21
25
  clientName: "MCP CLI Client",
22
26
  clean
23
27
  });
28
+ if (skipBrowserAuth) {
29
+ log("Authentication was completed by another instance - will use tokens from disk...");
30
+ await new Promise((res) => setTimeout(res, 1e3));
31
+ }
24
32
  const client = new Client(
25
33
  {
26
34
  name: "mcp-remote",
@@ -34,76 +42,71 @@ async function runClient(serverUrl, callbackPort, clean = false) {
34
42
  function initTransport() {
35
43
  const transport2 = new SSEClientTransport(url, { authProvider });
36
44
  transport2.onmessage = (message) => {
37
- console.log("Received message:", JSON.stringify(message, null, 2));
45
+ log("Received message:", JSON.stringify(message, null, 2));
38
46
  };
39
47
  transport2.onerror = (error) => {
40
- console.error("Transport error:", error);
48
+ log("Transport error:", error);
41
49
  };
42
50
  transport2.onclose = () => {
43
- console.log("Connection closed.");
51
+ log("Connection closed.");
44
52
  process.exit(0);
45
53
  };
46
54
  return transport2;
47
55
  }
48
56
  const transport = initTransport();
49
- const { server, waitForAuthCode } = setupOAuthCallbackServer({
50
- port: callbackPort,
51
- path: "/oauth/callback",
52
- events
53
- });
54
57
  const cleanup = async () => {
55
- console.log("\nClosing connection...");
58
+ log("\nClosing connection...");
56
59
  await client.close();
57
60
  server.close();
58
61
  };
59
62
  setupSignalHandlers(cleanup);
60
63
  try {
61
- console.log("Connecting to server...");
64
+ log("Connecting to server...");
62
65
  await client.connect(transport);
63
- console.log("Connected successfully!");
66
+ log("Connected successfully!");
64
67
  } catch (error) {
65
68
  if (error instanceof UnauthorizedError || error instanceof Error && error.message.includes("Unauthorized")) {
66
- console.log("Authentication required. Waiting for authorization...");
69
+ log("Authentication required. Waiting for authorization...");
67
70
  const code = await waitForAuthCode();
68
71
  try {
69
- console.log("Completing authorization...");
72
+ log("Completing authorization...");
70
73
  await transport.finishAuth(code);
71
- console.log("Connecting after authorization...");
74
+ log("Connecting after authorization...");
72
75
  await client.connect(initTransport());
73
- console.log("Connected successfully!");
74
- console.log("Requesting tools list...");
76
+ log("Connected successfully!");
77
+ log("Requesting tools list...");
75
78
  const tools = await client.request({ method: "tools/list" }, ListToolsResultSchema);
76
- console.log("Tools:", JSON.stringify(tools, null, 2));
77
- console.log("Requesting resource list...");
79
+ log("Tools:", JSON.stringify(tools, null, 2));
80
+ log("Requesting resource list...");
78
81
  const resources = await client.request({ method: "resources/list" }, ListResourcesResultSchema);
79
- console.log("Resources:", JSON.stringify(resources, null, 2));
80
- console.log("Listening for messages. Press Ctrl+C to exit.");
82
+ log("Resources:", JSON.stringify(resources, null, 2));
83
+ log("Listening for messages. Press Ctrl+C to exit.");
81
84
  } catch (authError) {
82
- console.error("Authorization error:", authError);
85
+ log("Authorization error:", authError);
83
86
  server.close();
84
87
  process.exit(1);
85
88
  }
86
89
  } else {
87
- console.error("Connection error:", error);
90
+ log("Connection error:", error);
88
91
  server.close();
89
92
  process.exit(1);
90
93
  }
91
94
  }
92
95
  try {
93
- console.log("Requesting tools list...");
96
+ log("Requesting tools list...");
94
97
  const tools = await client.request({ method: "tools/list" }, ListToolsResultSchema);
95
- console.log("Tools:", JSON.stringify(tools, null, 2));
98
+ log("Tools:", JSON.stringify(tools, null, 2));
96
99
  } catch (e) {
97
- console.log("Error requesting tools list:", e);
100
+ log("Error requesting tools list:", e);
98
101
  }
99
102
  try {
100
- console.log("Requesting resource list...");
103
+ log("Requesting resource list...");
101
104
  const resources = await client.request({ method: "resources/list" }, ListResourcesResultSchema);
102
- console.log("Resources:", JSON.stringify(resources, null, 2));
105
+ log("Resources:", JSON.stringify(resources, null, 2));
103
106
  } catch (e) {
104
- console.log("Error requesting resources list:", e);
107
+ log("Error requesting resources list:", e);
105
108
  }
106
- console.log("Listening for messages. Press Ctrl+C to exit.");
109
+ log("Listening for messages. Press Ctrl+C to exit.");
107
110
  }
108
111
  parseCommandLineArgs(process.argv.slice(2), 3333, "Usage: npx tsx client.ts [--clean] <https://server-url> [callback-port]").then(({ serverUrl, callbackPort, clean }) => {
109
112
  return runClient(serverUrl, callbackPort, clean);
package/dist/proxy.js CHANGED
@@ -2,32 +2,34 @@
2
2
  import {
3
3
  NodeOAuthClientProvider,
4
4
  connectToRemoteServer,
5
+ coordinateAuth,
6
+ getServerUrlHash,
5
7
  log,
6
8
  mcpProxy,
7
9
  parseCommandLineArgs,
8
- setupOAuthCallbackServer,
9
10
  setupSignalHandlers
10
- } from "./chunk-UL4C5N2N.js";
11
+ } from "./chunk-R4LSJHXI.js";
11
12
 
12
13
  // src/proxy.ts
13
14
  import { EventEmitter } from "events";
14
15
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
15
16
  async function runProxy(serverUrl, callbackPort, clean = false) {
16
17
  const events = new EventEmitter();
18
+ const serverUrlHash = getServerUrlHash(serverUrl);
19
+ const { server, waitForAuthCode, skipBrowserAuth } = await coordinateAuth(serverUrlHash, callbackPort, events);
17
20
  const authProvider = new NodeOAuthClientProvider({
18
21
  serverUrl,
19
22
  callbackPort,
20
23
  clientName: "MCP CLI Proxy",
21
24
  clean
22
25
  });
26
+ if (skipBrowserAuth) {
27
+ log("Authentication was completed by another instance - will use tokens from disk");
28
+ await new Promise((res) => setTimeout(res, 1e3));
29
+ }
23
30
  const localTransport = new StdioServerTransport();
24
- const { server, waitForAuthCode } = setupOAuthCallbackServer({
25
- port: callbackPort,
26
- path: "/oauth/callback",
27
- events
28
- });
29
31
  try {
30
- const remoteTransport = await connectToRemoteServer(serverUrl, authProvider, waitForAuthCode);
32
+ const remoteTransport = await connectToRemoteServer(serverUrl, authProvider, waitForAuthCode, skipBrowserAuth);
31
33
  mcpProxy({
32
34
  transportToClient: localTransport,
33
35
  transportToServer: remoteTransport
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-remote",
3
- "version": "0.0.13",
3
+ "version": "0.0.15",
4
4
  "description": "Remote proxy for Model Context Protocol, allowing local-only clients to connect to remote servers using oAuth",
5
5
  "keywords": [
6
6
  "mcp",
@@ -10,7 +10,7 @@
10
10
  "oauth"
11
11
  ],
12
12
  "author": "Glen Maddern <glen@cloudflare.com>",
13
- "repository": "https://github.com/geelen/remote-mcp",
13
+ "repository": "https://github.com/geelen/mcp-remote",
14
14
  "type": "module",
15
15
  "files": [
16
16
  "dist",