mcp-remote 0.0.10-3 → 0.0.11
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 +78 -40
- package/dist/chunk-W4HND3T7.js +382 -0
- package/dist/{cli/client.js → client.js} +76 -11
- package/dist/proxy.js +77 -0
- package/package.json +16 -13
- package/dist/chunk-S3VO5IXF.js +0 -265
- package/dist/cli/proxy.js +0 -144
- package/dist/react/index.d.ts +0 -94
- package/dist/react/index.js +0 -827
- /package/dist/{cli/client.d.ts → client.d.ts} +0 -0
- /package/dist/{cli/proxy.d.ts → proxy.d.ts} +0 -0
package/README.md
CHANGED
|
@@ -16,17 +16,7 @@ That's where `mcp-remote` comes in. As soon as your chosen MCP client supports r
|
|
|
16
16
|
|
|
17
17
|
## Usage
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
[Official Docs](https://modelcontextprotocol.io/quickstart/user)
|
|
22
|
-
|
|
23
|
-
In order to add an MCP server to Claude Desktop you need to edit the configuration file located at:
|
|
24
|
-
|
|
25
|
-
macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
26
|
-
|
|
27
|
-
Windows: `%APPDATA%\Claude\claude_desktop_config.json`
|
|
28
|
-
|
|
29
|
-
If it does not exist yet, [you may need to enable it under Settings > Developer](https://modelcontextprotocol.io/quickstart/user#2-add-the-filesystem-mcp-server).
|
|
19
|
+
All the most popular MCP clients (Claude Desktop, Cursor & Windsurf) use the following config format:
|
|
30
20
|
|
|
31
21
|
```json
|
|
32
22
|
{
|
|
@@ -34,7 +24,6 @@ If it does not exist yet, [you may need to enable it under Settings > Developer]
|
|
|
34
24
|
"remote-example": {
|
|
35
25
|
"command": "npx",
|
|
36
26
|
"args": [
|
|
37
|
-
"-y",
|
|
38
27
|
"mcp-remote",
|
|
39
28
|
"https://remote.mcp.server/sse"
|
|
40
29
|
]
|
|
@@ -43,52 +32,75 @@ If it does not exist yet, [you may need to enable it under Settings > Developer]
|
|
|
43
32
|
}
|
|
44
33
|
```
|
|
45
34
|
|
|
46
|
-
|
|
47
|
-
Upon restarting, you should see a hammer icon in the bottom right corner
|
|
48
|
-
of the input box.
|
|
49
|
-
|
|
50
|
-
### Cursor
|
|
51
|
-
|
|
52
|
-
[Official Docs](https://docs.cursor.com/context/model-context-protocol)
|
|
35
|
+
### Flags
|
|
53
36
|
|
|
54
|
-
|
|
37
|
+
* If `npx` is producing errors, consider adding `-y` as the first argument to auto-accept the installation of the `mcp-remote` package.
|
|
55
38
|
|
|
56
39
|
```json
|
|
57
|
-
{
|
|
58
|
-
"mcpServers": {
|
|
59
|
-
"remote-example": {
|
|
60
40
|
"command": "npx",
|
|
61
41
|
"args": [
|
|
62
|
-
"-y"
|
|
42
|
+
"-y"
|
|
63
43
|
"mcp-remote",
|
|
64
44
|
"https://remote.mcp.server/sse"
|
|
65
45
|
]
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
46
|
```
|
|
70
47
|
|
|
71
|
-
|
|
48
|
+
* To force `npx` to always check for an updated version of `mcp-remote`, add the `@latest` flag:
|
|
72
49
|
|
|
73
|
-
|
|
50
|
+
```json
|
|
51
|
+
"args": [
|
|
52
|
+
"mcp-remote@latest",
|
|
53
|
+
"https://remote.mcp.server/sse"
|
|
54
|
+
]
|
|
55
|
+
```
|
|
74
56
|
|
|
75
|
-
|
|
57
|
+
* To force `mcp-remote` to ignore any existing access tokens and begin the authorization flow anew, pass `--clean`.
|
|
76
58
|
|
|
77
59
|
```json
|
|
78
|
-
{
|
|
79
|
-
"mcpServers": {
|
|
80
|
-
"remote-example": {
|
|
81
|
-
"command": "npx",
|
|
82
60
|
"args": [
|
|
83
|
-
"-y",
|
|
84
61
|
"mcp-remote",
|
|
85
|
-
"https://remote.mcp.server/sse"
|
|
62
|
+
"https://remote.mcp.server/sse",
|
|
63
|
+
"--clean"
|
|
64
|
+
]
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
* To change which port `mcp-remote` listens for an OAuth redirect (by default `3334`), add an additional argument after the server URL. Note that whatever port you specify, if it is unavailable an open port will be chosen at random.
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
"args": [
|
|
71
|
+
"mcp-remote",
|
|
72
|
+
"https://remote.mcp.server/sse",
|
|
73
|
+
"9696"
|
|
86
74
|
]
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
75
|
```
|
|
91
76
|
|
|
77
|
+
* To ensure that no
|
|
78
|
+
|
|
79
|
+
### Claude Desktop
|
|
80
|
+
|
|
81
|
+
[Official Docs](https://modelcontextprotocol.io/quickstart/user)
|
|
82
|
+
|
|
83
|
+
In order to add an MCP server to Claude Desktop you need to edit the configuration file located at:
|
|
84
|
+
|
|
85
|
+
* macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
86
|
+
* Windows: `%APPDATA%\Claude\claude_desktop_config.json`
|
|
87
|
+
|
|
88
|
+
If it does not exist yet, [you may need to enable it under Settings > Developer](https://modelcontextprotocol.io/quickstart/user#2-add-the-filesystem-mcp-server).
|
|
89
|
+
|
|
90
|
+
Restart Claude Desktop to pick up the changes in the configuration file.
|
|
91
|
+
Upon restarting, you should see a hammer icon in the bottom right corner
|
|
92
|
+
of the input box.
|
|
93
|
+
|
|
94
|
+
### Cursor
|
|
95
|
+
|
|
96
|
+
[Official Docs](https://docs.cursor.com/context/model-context-protocol). The configuration file is located at `~/.cursor/mcp.json`.
|
|
97
|
+
|
|
98
|
+
As of version `0.48.0`, Cursor supports unauthed SSE servers directly. If your MCP server is using the official MCP OAuth authorization protocol, you still need to add a **"command"** server and call `mcp-remote`.
|
|
99
|
+
|
|
100
|
+
### Windsurf
|
|
101
|
+
|
|
102
|
+
[Official Docs](https://docs.codeium.com/windsurf/mcp). The configuration file is located at `~/.codeium/windsurf/mcp_config.json`.
|
|
103
|
+
|
|
92
104
|
## Building Remote MCP Servers
|
|
93
105
|
|
|
94
106
|
For instructions on building & deploying remote MCP servers, including acting as a valid OAuth client, see the following resources:
|
|
@@ -106,7 +118,17 @@ For more information about testing these servers, see also:
|
|
|
106
118
|
|
|
107
119
|
Know of more resources you'd like to share? Please add them to this Readme and send a PR!
|
|
108
120
|
|
|
109
|
-
##
|
|
121
|
+
## Troubleshooting
|
|
122
|
+
|
|
123
|
+
### Wipe your `~/.mcp-auth` directory
|
|
124
|
+
|
|
125
|
+
`mcp-remote` stores all the credential information inside `~/.mcp-auth` (or wherever your `MCP_REMOTE_CONFIG_DIR` points to). If you're having persistent issues, try running:
|
|
126
|
+
|
|
127
|
+
```sh
|
|
128
|
+
rm -rf ~/.mcp-auth
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Then restarting your MCP client.
|
|
110
132
|
|
|
111
133
|
### Check your Node version
|
|
112
134
|
|
|
@@ -141,3 +163,19 @@ this might look like:
|
|
|
141
163
|
}
|
|
142
164
|
}
|
|
143
165
|
```
|
|
166
|
+
|
|
167
|
+
### Check the logs
|
|
168
|
+
|
|
169
|
+
[Follow Claude Desktop logs in real-time](https://modelcontextprotocol.io/docs/tools/debugging#debugging-in-claude-desktop)
|
|
170
|
+
|
|
171
|
+
MacOS / Linux:
|
|
172
|
+
|
|
173
|
+
`tail -n 20 -F ~/Library/Logs/Claude/mcp*.log`
|
|
174
|
+
|
|
175
|
+
For bash on WSL:
|
|
176
|
+
|
|
177
|
+
`tail -n 20 -f "C:\Users\YourUsername\AppData\Local\Claude\Logs\mcp.log"`
|
|
178
|
+
|
|
179
|
+
or Powershell:
|
|
180
|
+
|
|
181
|
+
`Get-Content "C:\Users\YourUsername\AppData\Local\Claude\Logs\mcp.log" -Wait -Tail 20`
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
2
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
3
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
// src/lib/node-oauth-client-provider.ts
|
|
7
|
+
import open from "open";
|
|
8
|
+
import {
|
|
9
|
+
OAuthClientInformationSchema,
|
|
10
|
+
OAuthTokensSchema
|
|
11
|
+
} from "@modelcontextprotocol/sdk/shared/auth.js";
|
|
12
|
+
|
|
13
|
+
// src/lib/mcp-auth-config.ts
|
|
14
|
+
import crypto from "crypto";
|
|
15
|
+
import path from "path";
|
|
16
|
+
import os from "os";
|
|
17
|
+
import fs from "fs/promises";
|
|
18
|
+
var knownConfigFiles = [
|
|
19
|
+
"client_info.json",
|
|
20
|
+
"tokens.json",
|
|
21
|
+
"code_verifier.txt"
|
|
22
|
+
];
|
|
23
|
+
async function cleanServerConfig(serverUrlHash) {
|
|
24
|
+
console.error(`Cleaning configuration files for server: ${serverUrlHash}`);
|
|
25
|
+
for (const filename of knownConfigFiles) {
|
|
26
|
+
await deleteConfigFile(serverUrlHash, filename);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function getConfigDir() {
|
|
30
|
+
return process.env.MCP_REMOTE_CONFIG_DIR || path.join(os.homedir(), ".mcp-auth");
|
|
31
|
+
}
|
|
32
|
+
async function ensureConfigDir() {
|
|
33
|
+
try {
|
|
34
|
+
const configDir = getConfigDir();
|
|
35
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.error("Error creating config directory:", error);
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function getServerUrlHash(serverUrl) {
|
|
42
|
+
return crypto.createHash("md5").update(serverUrl).digest("hex");
|
|
43
|
+
}
|
|
44
|
+
function getConfigFilePath(serverUrlHash, filename) {
|
|
45
|
+
const configDir = getConfigDir();
|
|
46
|
+
return path.join(configDir, `${serverUrlHash}_${filename}`);
|
|
47
|
+
}
|
|
48
|
+
async function deleteConfigFile(serverUrlHash, filename) {
|
|
49
|
+
try {
|
|
50
|
+
const filePath = getConfigFilePath(serverUrlHash, filename);
|
|
51
|
+
await fs.unlink(filePath);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if (error.code !== "ENOENT") {
|
|
54
|
+
console.error(`Error deleting ${filename}:`, error);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function readJsonFile(serverUrlHash, filename, schema, clean = false) {
|
|
59
|
+
try {
|
|
60
|
+
await ensureConfigDir();
|
|
61
|
+
if (clean) {
|
|
62
|
+
await deleteConfigFile(serverUrlHash, filename);
|
|
63
|
+
return void 0;
|
|
64
|
+
}
|
|
65
|
+
const filePath = getConfigFilePath(serverUrlHash, filename);
|
|
66
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
67
|
+
return await schema.parseAsync(JSON.parse(content));
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (error.code === "ENOENT") {
|
|
70
|
+
return void 0;
|
|
71
|
+
}
|
|
72
|
+
return void 0;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async function writeJsonFile(serverUrlHash, filename, data) {
|
|
76
|
+
try {
|
|
77
|
+
await ensureConfigDir();
|
|
78
|
+
const filePath = getConfigFilePath(serverUrlHash, filename);
|
|
79
|
+
await fs.writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.error(`Error writing ${filename}:`, error);
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async function readTextFile(serverUrlHash, filename, errorMessage, clean = false) {
|
|
86
|
+
try {
|
|
87
|
+
await ensureConfigDir();
|
|
88
|
+
if (clean) {
|
|
89
|
+
await deleteConfigFile(serverUrlHash, filename);
|
|
90
|
+
throw new Error("File deleted due to clean flag");
|
|
91
|
+
}
|
|
92
|
+
const filePath = getConfigFilePath(serverUrlHash, filename);
|
|
93
|
+
return await fs.readFile(filePath, "utf-8");
|
|
94
|
+
} catch (error) {
|
|
95
|
+
throw new Error(errorMessage || `Error reading ${filename}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async function writeTextFile(serverUrlHash, filename, text) {
|
|
99
|
+
try {
|
|
100
|
+
await ensureConfigDir();
|
|
101
|
+
const filePath = getConfigFilePath(serverUrlHash, filename);
|
|
102
|
+
await fs.writeFile(filePath, text, "utf-8");
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.error(`Error writing ${filename}:`, error);
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// src/lib/node-oauth-client-provider.ts
|
|
110
|
+
var NodeOAuthClientProvider = class {
|
|
111
|
+
/**
|
|
112
|
+
* Creates a new NodeOAuthClientProvider
|
|
113
|
+
* @param options Configuration options for the provider
|
|
114
|
+
*/
|
|
115
|
+
constructor(options) {
|
|
116
|
+
this.options = options;
|
|
117
|
+
this.serverUrlHash = getServerUrlHash(options.serverUrl);
|
|
118
|
+
this.callbackPath = options.callbackPath || "/oauth/callback";
|
|
119
|
+
this.clientName = options.clientName || "MCP CLI Client";
|
|
120
|
+
this.clientUri = options.clientUri || "https://github.com/modelcontextprotocol/mcp-cli";
|
|
121
|
+
if (options.clean) {
|
|
122
|
+
cleanServerConfig(this.serverUrlHash).catch((err) => {
|
|
123
|
+
console.error("Error cleaning server config:", err);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
serverUrlHash;
|
|
128
|
+
callbackPath;
|
|
129
|
+
clientName;
|
|
130
|
+
clientUri;
|
|
131
|
+
get redirectUrl() {
|
|
132
|
+
return `http://127.0.0.1:${this.options.callbackPort}${this.callbackPath}`;
|
|
133
|
+
}
|
|
134
|
+
get clientMetadata() {
|
|
135
|
+
return {
|
|
136
|
+
redirect_uris: [this.redirectUrl],
|
|
137
|
+
token_endpoint_auth_method: "none",
|
|
138
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
139
|
+
response_types: ["code"],
|
|
140
|
+
client_name: this.clientName,
|
|
141
|
+
client_uri: this.clientUri
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Gets the client information if it exists
|
|
146
|
+
* @returns The client information or undefined
|
|
147
|
+
*/
|
|
148
|
+
async clientInformation() {
|
|
149
|
+
return readJsonFile(
|
|
150
|
+
this.serverUrlHash,
|
|
151
|
+
"client_info.json",
|
|
152
|
+
OAuthClientInformationSchema,
|
|
153
|
+
this.options.clean
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Saves client information
|
|
158
|
+
* @param clientInformation The client information to save
|
|
159
|
+
*/
|
|
160
|
+
async saveClientInformation(clientInformation) {
|
|
161
|
+
await writeJsonFile(this.serverUrlHash, "client_info.json", clientInformation);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Gets the OAuth tokens if they exist
|
|
165
|
+
* @returns The OAuth tokens or undefined
|
|
166
|
+
*/
|
|
167
|
+
async tokens() {
|
|
168
|
+
return readJsonFile(
|
|
169
|
+
this.serverUrlHash,
|
|
170
|
+
"tokens.json",
|
|
171
|
+
OAuthTokensSchema,
|
|
172
|
+
this.options.clean
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Saves OAuth tokens
|
|
177
|
+
* @param tokens The tokens to save
|
|
178
|
+
*/
|
|
179
|
+
async saveTokens(tokens) {
|
|
180
|
+
await writeJsonFile(this.serverUrlHash, "tokens.json", tokens);
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Redirects the user to the authorization URL
|
|
184
|
+
* @param authorizationUrl The URL to redirect to
|
|
185
|
+
*/
|
|
186
|
+
async redirectToAuthorization(authorizationUrl) {
|
|
187
|
+
console.error(`
|
|
188
|
+
Please authorize this client by visiting:
|
|
189
|
+
${authorizationUrl.toString()}
|
|
190
|
+
`);
|
|
191
|
+
try {
|
|
192
|
+
await open(authorizationUrl.toString());
|
|
193
|
+
console.error("Browser opened automatically.");
|
|
194
|
+
} catch (error) {
|
|
195
|
+
console.error("Could not open browser automatically. Please copy and paste the URL above into your browser.");
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Saves the PKCE code verifier
|
|
200
|
+
* @param codeVerifier The code verifier to save
|
|
201
|
+
*/
|
|
202
|
+
async saveCodeVerifier(codeVerifier) {
|
|
203
|
+
await writeTextFile(this.serverUrlHash, "code_verifier.txt", codeVerifier);
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Gets the PKCE code verifier
|
|
207
|
+
* @returns The code verifier
|
|
208
|
+
*/
|
|
209
|
+
async codeVerifier() {
|
|
210
|
+
return await readTextFile(
|
|
211
|
+
this.serverUrlHash,
|
|
212
|
+
"code_verifier.txt",
|
|
213
|
+
"No code verifier saved for session",
|
|
214
|
+
this.options.clean
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// src/lib/utils.ts
|
|
220
|
+
import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js";
|
|
221
|
+
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
222
|
+
import express from "express";
|
|
223
|
+
import net from "net";
|
|
224
|
+
var pid = process.pid;
|
|
225
|
+
function mcpProxy({ transportToClient, transportToServer }) {
|
|
226
|
+
let transportToClientClosed = false;
|
|
227
|
+
let transportToServerClosed = false;
|
|
228
|
+
transportToClient.onmessage = (message) => {
|
|
229
|
+
console.error("[Local\u2192Remote]", message.method || message.id);
|
|
230
|
+
transportToServer.send(message).catch(onServerError);
|
|
231
|
+
};
|
|
232
|
+
transportToServer.onmessage = (message) => {
|
|
233
|
+
console.error("[Remote\u2192Local]", message.method || message.id);
|
|
234
|
+
transportToClient.send(message).catch(onClientError);
|
|
235
|
+
};
|
|
236
|
+
transportToClient.onclose = () => {
|
|
237
|
+
if (transportToServerClosed) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
transportToClientClosed = true;
|
|
241
|
+
transportToServer.close().catch(onServerError);
|
|
242
|
+
};
|
|
243
|
+
transportToServer.onclose = () => {
|
|
244
|
+
if (transportToClientClosed) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
transportToServerClosed = true;
|
|
248
|
+
transportToClient.close().catch(onClientError);
|
|
249
|
+
};
|
|
250
|
+
transportToClient.onerror = onClientError;
|
|
251
|
+
transportToServer.onerror = onServerError;
|
|
252
|
+
function onClientError(error) {
|
|
253
|
+
console.error("Error from local client:", error);
|
|
254
|
+
}
|
|
255
|
+
function onServerError(error) {
|
|
256
|
+
console.error("Error from remote server:", error);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
async function connectToRemoteServer(serverUrl, authProvider, waitForAuthCode) {
|
|
260
|
+
console.error(`[${pid}] Connecting to remote server: ${serverUrl}`);
|
|
261
|
+
const url = new URL(serverUrl);
|
|
262
|
+
const transport = new SSEClientTransport(url, { authProvider });
|
|
263
|
+
try {
|
|
264
|
+
await transport.start();
|
|
265
|
+
console.error("Connected to remote server");
|
|
266
|
+
return transport;
|
|
267
|
+
} catch (error) {
|
|
268
|
+
if (error instanceof UnauthorizedError || error instanceof Error && error.message.includes("Unauthorized")) {
|
|
269
|
+
console.error("Authentication required. Waiting for authorization...");
|
|
270
|
+
const code = await waitForAuthCode();
|
|
271
|
+
try {
|
|
272
|
+
console.error("Completing authorization...");
|
|
273
|
+
await transport.finishAuth(code);
|
|
274
|
+
const newTransport = new SSEClientTransport(url, { authProvider });
|
|
275
|
+
await newTransport.start();
|
|
276
|
+
console.error("Connected to remote server after authentication");
|
|
277
|
+
return newTransport;
|
|
278
|
+
} catch (authError) {
|
|
279
|
+
console.error("Authorization error:", authError);
|
|
280
|
+
throw authError;
|
|
281
|
+
}
|
|
282
|
+
} else {
|
|
283
|
+
console.error("Connection error:", error);
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
function setupOAuthCallbackServer(options) {
|
|
289
|
+
let authCode = null;
|
|
290
|
+
const app = express();
|
|
291
|
+
app.get(options.path, (req, res) => {
|
|
292
|
+
const code = req.query.code;
|
|
293
|
+
if (!code) {
|
|
294
|
+
res.status(400).send("Error: No authorization code received");
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
authCode = code;
|
|
298
|
+
res.send("Authorization successful! You may close this window and return to the CLI.");
|
|
299
|
+
options.events.emit("auth-code-received", code);
|
|
300
|
+
});
|
|
301
|
+
const server = app.listen(options.port, () => {
|
|
302
|
+
console.error(`OAuth callback server running at http://127.0.0.1:${options.port}`);
|
|
303
|
+
});
|
|
304
|
+
const waitForAuthCode = () => {
|
|
305
|
+
return new Promise((resolve) => {
|
|
306
|
+
if (authCode) {
|
|
307
|
+
resolve(authCode);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
options.events.once("auth-code-received", (code) => {
|
|
311
|
+
resolve(code);
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
};
|
|
315
|
+
return { server, authCode, waitForAuthCode };
|
|
316
|
+
}
|
|
317
|
+
async function findAvailablePort(preferredPort) {
|
|
318
|
+
return new Promise((resolve, reject) => {
|
|
319
|
+
const server = net.createServer();
|
|
320
|
+
server.on("error", (err) => {
|
|
321
|
+
if (err.code === "EADDRINUSE") {
|
|
322
|
+
server.listen(0);
|
|
323
|
+
} else {
|
|
324
|
+
reject(err);
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
server.on("listening", () => {
|
|
328
|
+
const { port } = server.address();
|
|
329
|
+
server.close(() => {
|
|
330
|
+
resolve(port);
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
server.listen(preferredPort || 0);
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
async function parseCommandLineArgs(args, defaultPort, usage) {
|
|
337
|
+
const cleanIndex = args.indexOf("--clean");
|
|
338
|
+
const clean = cleanIndex !== -1;
|
|
339
|
+
if (clean) {
|
|
340
|
+
args.splice(cleanIndex, 1);
|
|
341
|
+
}
|
|
342
|
+
const serverUrl = args[0];
|
|
343
|
+
const specifiedPort = args[1] ? parseInt(args[1]) : void 0;
|
|
344
|
+
if (!serverUrl) {
|
|
345
|
+
console.error(usage);
|
|
346
|
+
process.exit(1);
|
|
347
|
+
}
|
|
348
|
+
const url = new URL(serverUrl);
|
|
349
|
+
const isLocalhost = (url.hostname === "localhost" || url.hostname === "127.0.0.1") && url.protocol === "http:";
|
|
350
|
+
if (!(url.protocol == "https:" || isLocalhost)) {
|
|
351
|
+
console.error(usage);
|
|
352
|
+
process.exit(1);
|
|
353
|
+
}
|
|
354
|
+
const callbackPort = specifiedPort || await findAvailablePort(defaultPort);
|
|
355
|
+
if (specifiedPort) {
|
|
356
|
+
console.error(`Using specified callback port: ${callbackPort}`);
|
|
357
|
+
} else {
|
|
358
|
+
console.error(`Using automatically selected callback port: ${callbackPort}`);
|
|
359
|
+
}
|
|
360
|
+
if (clean) {
|
|
361
|
+
console.error("Clean mode enabled: config files will be reset before reading");
|
|
362
|
+
}
|
|
363
|
+
return { serverUrl, callbackPort, clean };
|
|
364
|
+
}
|
|
365
|
+
function setupSignalHandlers(cleanup) {
|
|
366
|
+
process.on("SIGINT", async () => {
|
|
367
|
+
console.error("\nShutting down...");
|
|
368
|
+
await cleanup();
|
|
369
|
+
process.exit(0);
|
|
370
|
+
});
|
|
371
|
+
process.stdin.resume();
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export {
|
|
375
|
+
__commonJS,
|
|
376
|
+
NodeOAuthClientProvider,
|
|
377
|
+
mcpProxy,
|
|
378
|
+
connectToRemoteServer,
|
|
379
|
+
setupOAuthCallbackServer,
|
|
380
|
+
parseCommandLineArgs,
|
|
381
|
+
setupSignalHandlers
|
|
382
|
+
};
|
|
@@ -1,33 +1,98 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
NodeOAuthClientProvider,
|
|
4
|
+
__commonJS,
|
|
4
5
|
parseCommandLineArgs,
|
|
5
6
|
setupOAuthCallbackServer,
|
|
6
7
|
setupSignalHandlers
|
|
7
|
-
} from "
|
|
8
|
+
} from "./chunk-W4HND3T7.js";
|
|
8
9
|
|
|
9
|
-
//
|
|
10
|
+
// package.json
|
|
11
|
+
var require_package = __commonJS({
|
|
12
|
+
"package.json"(exports, module) {
|
|
13
|
+
module.exports = {
|
|
14
|
+
name: "mcp-remote",
|
|
15
|
+
version: "0.0.10",
|
|
16
|
+
description: "Remote proxy for Model Context Protocol, allowing local-only clients to connect to remote servers using oAuth",
|
|
17
|
+
keywords: [
|
|
18
|
+
"mcp",
|
|
19
|
+
"stdio",
|
|
20
|
+
"sse",
|
|
21
|
+
"remote",
|
|
22
|
+
"oauth"
|
|
23
|
+
],
|
|
24
|
+
author: "Glen Maddern <glen@cloudflare.com>",
|
|
25
|
+
repository: "https://github.com/geelen/remote-mcp",
|
|
26
|
+
type: "module",
|
|
27
|
+
files: [
|
|
28
|
+
"dist",
|
|
29
|
+
"README.md",
|
|
30
|
+
"LICENSE"
|
|
31
|
+
],
|
|
32
|
+
main: "dist/index.js",
|
|
33
|
+
bin: {
|
|
34
|
+
"mcp-remote": "dist/cli/proxy.js"
|
|
35
|
+
},
|
|
36
|
+
scripts: {
|
|
37
|
+
dev: "tsup --watch",
|
|
38
|
+
build: "tsup",
|
|
39
|
+
check: "prettier --check . && tsc"
|
|
40
|
+
},
|
|
41
|
+
dependencies: {
|
|
42
|
+
"@modelcontextprotocol/sdk": "^1.7.0",
|
|
43
|
+
express: "^4.21.2",
|
|
44
|
+
open: "^10.1.0"
|
|
45
|
+
},
|
|
46
|
+
devDependencies: {
|
|
47
|
+
"@types/express": "^5.0.0",
|
|
48
|
+
"@types/node": "^22.13.10",
|
|
49
|
+
"@types/react": "^19.0.12",
|
|
50
|
+
prettier: "^3.5.3",
|
|
51
|
+
react: "^19.0.0",
|
|
52
|
+
tsup: "^8.4.0",
|
|
53
|
+
tsx: "^4.19.3",
|
|
54
|
+
typescript: "^5.8.2"
|
|
55
|
+
},
|
|
56
|
+
tsup: {
|
|
57
|
+
entry: [
|
|
58
|
+
"src/client.ts",
|
|
59
|
+
"src/proxy.ts"
|
|
60
|
+
],
|
|
61
|
+
format: [
|
|
62
|
+
"esm"
|
|
63
|
+
],
|
|
64
|
+
dts: true,
|
|
65
|
+
clean: true,
|
|
66
|
+
outDir: "dist",
|
|
67
|
+
external: [
|
|
68
|
+
"react"
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// src/client.ts
|
|
10
76
|
import { EventEmitter } from "events";
|
|
11
77
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
12
78
|
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
13
79
|
import { ListResourcesResultSchema, ListToolsResultSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
14
80
|
import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js";
|
|
15
|
-
async function runClient(serverUrl, callbackPort) {
|
|
81
|
+
async function runClient(serverUrl, callbackPort, clean = false) {
|
|
16
82
|
const events = new EventEmitter();
|
|
17
83
|
const authProvider = new NodeOAuthClientProvider({
|
|
18
84
|
serverUrl,
|
|
19
85
|
callbackPort,
|
|
20
|
-
clientName: "MCP CLI Client"
|
|
86
|
+
clientName: "MCP CLI Client",
|
|
87
|
+
clean
|
|
21
88
|
});
|
|
22
89
|
const client = new Client(
|
|
23
90
|
{
|
|
24
|
-
name: "mcp-
|
|
25
|
-
version:
|
|
91
|
+
name: "mcp-remote",
|
|
92
|
+
version: require_package().version
|
|
26
93
|
},
|
|
27
94
|
{
|
|
28
|
-
capabilities: {
|
|
29
|
-
sampling: {}
|
|
30
|
-
}
|
|
95
|
+
capabilities: {}
|
|
31
96
|
}
|
|
32
97
|
);
|
|
33
98
|
const url = new URL(serverUrl);
|
|
@@ -105,8 +170,8 @@ async function runClient(serverUrl, callbackPort) {
|
|
|
105
170
|
}
|
|
106
171
|
console.log("Listening for messages. Press Ctrl+C to exit.");
|
|
107
172
|
}
|
|
108
|
-
parseCommandLineArgs(process.argv.slice(2), 3333, "Usage: npx tsx client.ts <https://server-url> [callback-port]").then(({ serverUrl, callbackPort }) => {
|
|
109
|
-
return runClient(serverUrl, callbackPort);
|
|
173
|
+
parseCommandLineArgs(process.argv.slice(2), 3333, "Usage: npx tsx client.ts [--clean] <https://server-url> [callback-port]").then(({ serverUrl, callbackPort, clean }) => {
|
|
174
|
+
return runClient(serverUrl, callbackPort, clean);
|
|
110
175
|
}).catch((error) => {
|
|
111
176
|
console.error("Fatal error:", error);
|
|
112
177
|
process.exit(1);
|