mcp-remote 0.0.1 → 0.0.2
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/LICENSE +21 -0
- package/README.md +26 -0
- package/dist/chunk-SB5B4PZV.js +326 -0
- package/dist/client.d.ts +1 -0
- package/dist/client.js +113 -0
- package/dist/proxy.d.ts +1 -0
- package/dist/proxy.js +54 -0
- package/package.json +18 -5
- package/.prettierrc +0 -14
- package/sse-auth-client.ts +0 -307
- package/sse-auth-proxy.ts +0 -323
- package/tsconfig.json +0 -17
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Cloudflare, Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# `mcp-remote`
|
|
2
|
+
|
|
3
|
+
**EXPERIMENTAL PROOF OF CONCEPT**
|
|
4
|
+
|
|
5
|
+
Connect an MCP Client that only supports local (stdio) servers to a Remote MCP Server, with auth support:
|
|
6
|
+
|
|
7
|
+
E.g: Claude Desktop or Windsurf
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"mcpServers": {
|
|
12
|
+
"remote-example": {
|
|
13
|
+
"command": "npx",
|
|
14
|
+
"args": [
|
|
15
|
+
"mcp-remote",
|
|
16
|
+
"https://remote.mcp.server/sse"
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Cursor:
|
|
24
|
+
|
|
25
|
+

|
|
26
|
+
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
// src/shared.ts
|
|
2
|
+
import express from "express";
|
|
3
|
+
import open from "open";
|
|
4
|
+
import fs from "fs/promises";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import os from "os";
|
|
7
|
+
import crypto from "crypto";
|
|
8
|
+
import net from "net";
|
|
9
|
+
import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js";
|
|
10
|
+
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
11
|
+
import {
|
|
12
|
+
OAuthClientInformationSchema,
|
|
13
|
+
OAuthTokensSchema
|
|
14
|
+
} from "@modelcontextprotocol/sdk/shared/auth.js";
|
|
15
|
+
var NodeOAuthClientProvider = class {
|
|
16
|
+
/**
|
|
17
|
+
* Creates a new NodeOAuthClientProvider
|
|
18
|
+
* @param options Configuration options for the provider
|
|
19
|
+
*/
|
|
20
|
+
constructor(options) {
|
|
21
|
+
this.options = options;
|
|
22
|
+
this.serverUrlHash = crypto.createHash("md5").update(options.serverUrl).digest("hex");
|
|
23
|
+
this.configDir = options.configDir || path.join(os.homedir(), ".mcp-auth");
|
|
24
|
+
this.callbackPath = options.callbackPath || "/oauth/callback";
|
|
25
|
+
this.clientName = options.clientName || "MCP CLI Client";
|
|
26
|
+
this.clientUri = options.clientUri || "https://github.com/modelcontextprotocol/mcp-cli";
|
|
27
|
+
}
|
|
28
|
+
configDir;
|
|
29
|
+
serverUrlHash;
|
|
30
|
+
callbackPath;
|
|
31
|
+
clientName;
|
|
32
|
+
clientUri;
|
|
33
|
+
get redirectUrl() {
|
|
34
|
+
return `http://localhost:${this.options.callbackPort}${this.callbackPath}`;
|
|
35
|
+
}
|
|
36
|
+
get clientMetadata() {
|
|
37
|
+
return {
|
|
38
|
+
redirect_uris: [this.redirectUrl],
|
|
39
|
+
token_endpoint_auth_method: "none",
|
|
40
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
41
|
+
response_types: ["code"],
|
|
42
|
+
client_name: this.clientName,
|
|
43
|
+
client_uri: this.clientUri
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Ensures the configuration directory exists
|
|
48
|
+
* @private
|
|
49
|
+
*/
|
|
50
|
+
async ensureConfigDir() {
|
|
51
|
+
try {
|
|
52
|
+
await fs.mkdir(this.configDir, { recursive: true });
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error("Error creating config directory:", error);
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Reads a JSON file and parses it with the provided schema
|
|
60
|
+
* @param filename The name of the file to read
|
|
61
|
+
* @param schema The schema to validate against
|
|
62
|
+
* @returns The parsed file content or undefined if the file doesn't exist
|
|
63
|
+
* @private
|
|
64
|
+
*/
|
|
65
|
+
async readFile(filename, schema) {
|
|
66
|
+
try {
|
|
67
|
+
await this.ensureConfigDir();
|
|
68
|
+
const filePath = path.join(this.configDir, `${this.serverUrlHash}_${filename}`);
|
|
69
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
70
|
+
return await schema.parseAsync(JSON.parse(content));
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (error.code === "ENOENT") {
|
|
73
|
+
return void 0;
|
|
74
|
+
}
|
|
75
|
+
return void 0;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Writes a JSON object to a file
|
|
80
|
+
* @param filename The name of the file to write
|
|
81
|
+
* @param data The data to write
|
|
82
|
+
* @private
|
|
83
|
+
*/
|
|
84
|
+
async writeFile(filename, data) {
|
|
85
|
+
try {
|
|
86
|
+
await this.ensureConfigDir();
|
|
87
|
+
const filePath = path.join(this.configDir, `${this.serverUrlHash}_${filename}`);
|
|
88
|
+
await fs.writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
|
|
89
|
+
} catch (error) {
|
|
90
|
+
console.error(`Error writing ${filename}:`, error);
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Writes a text string to a file
|
|
96
|
+
* @param filename The name of the file to write
|
|
97
|
+
* @param text The text to write
|
|
98
|
+
* @private
|
|
99
|
+
*/
|
|
100
|
+
async writeTextFile(filename, text) {
|
|
101
|
+
try {
|
|
102
|
+
await this.ensureConfigDir();
|
|
103
|
+
const filePath = path.join(this.configDir, `${this.serverUrlHash}_${filename}`);
|
|
104
|
+
await fs.writeFile(filePath, text, "utf-8");
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.error(`Error writing ${filename}:`, error);
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Reads text from a file
|
|
112
|
+
* @param filename The name of the file to read
|
|
113
|
+
* @returns The file content as a string
|
|
114
|
+
* @private
|
|
115
|
+
*/
|
|
116
|
+
async readTextFile(filename) {
|
|
117
|
+
try {
|
|
118
|
+
await this.ensureConfigDir();
|
|
119
|
+
const filePath = path.join(this.configDir, `${this.serverUrlHash}_${filename}`);
|
|
120
|
+
return await fs.readFile(filePath, "utf-8");
|
|
121
|
+
} catch (error) {
|
|
122
|
+
throw new Error("No code verifier saved for session");
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Gets the client information if it exists
|
|
127
|
+
* @returns The client information or undefined
|
|
128
|
+
*/
|
|
129
|
+
async clientInformation() {
|
|
130
|
+
return this.readFile("client_info.json", OAuthClientInformationSchema);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Saves client information
|
|
134
|
+
* @param clientInformation The client information to save
|
|
135
|
+
*/
|
|
136
|
+
async saveClientInformation(clientInformation) {
|
|
137
|
+
await this.writeFile("client_info.json", clientInformation);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Gets the OAuth tokens if they exist
|
|
141
|
+
* @returns The OAuth tokens or undefined
|
|
142
|
+
*/
|
|
143
|
+
async tokens() {
|
|
144
|
+
return this.readFile("tokens.json", OAuthTokensSchema);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Saves OAuth tokens
|
|
148
|
+
* @param tokens The tokens to save
|
|
149
|
+
*/
|
|
150
|
+
async saveTokens(tokens) {
|
|
151
|
+
await this.writeFile("tokens.json", tokens);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Redirects the user to the authorization URL
|
|
155
|
+
* @param authorizationUrl The URL to redirect to
|
|
156
|
+
*/
|
|
157
|
+
async redirectToAuthorization(authorizationUrl) {
|
|
158
|
+
console.error(`
|
|
159
|
+
Please authorize this client by visiting:
|
|
160
|
+
${authorizationUrl.toString()}
|
|
161
|
+
`);
|
|
162
|
+
try {
|
|
163
|
+
await open(authorizationUrl.toString());
|
|
164
|
+
console.error("Browser opened automatically.");
|
|
165
|
+
} catch (error) {
|
|
166
|
+
console.error("Could not open browser automatically. Please copy and paste the URL above into your browser.");
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Saves the PKCE code verifier
|
|
171
|
+
* @param codeVerifier The code verifier to save
|
|
172
|
+
*/
|
|
173
|
+
async saveCodeVerifier(codeVerifier) {
|
|
174
|
+
await this.writeTextFile("code_verifier.txt", codeVerifier);
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Gets the PKCE code verifier
|
|
178
|
+
* @returns The code verifier
|
|
179
|
+
*/
|
|
180
|
+
async codeVerifier() {
|
|
181
|
+
return await this.readTextFile("code_verifier.txt");
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
function setupOAuthCallbackServer(options) {
|
|
185
|
+
let authCode = null;
|
|
186
|
+
const app = express();
|
|
187
|
+
app.get(options.path, (req, res) => {
|
|
188
|
+
const code = req.query.code;
|
|
189
|
+
if (!code) {
|
|
190
|
+
res.status(400).send("Error: No authorization code received");
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
authCode = code;
|
|
194
|
+
res.send("Authorization successful! You may close this window and return to the CLI.");
|
|
195
|
+
options.events.emit("auth-code-received", code);
|
|
196
|
+
});
|
|
197
|
+
const server = app.listen(options.port, () => {
|
|
198
|
+
console.error(`OAuth callback server running at http://localhost:${options.port}`);
|
|
199
|
+
});
|
|
200
|
+
const waitForAuthCode = () => {
|
|
201
|
+
return new Promise((resolve) => {
|
|
202
|
+
if (authCode) {
|
|
203
|
+
resolve(authCode);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
options.events.once("auth-code-received", (code) => {
|
|
207
|
+
resolve(code);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
};
|
|
211
|
+
return { server, authCode, waitForAuthCode };
|
|
212
|
+
}
|
|
213
|
+
async function connectToRemoteServer(serverUrl, authProvider, waitForAuthCode) {
|
|
214
|
+
console.error("Connecting to remote server:", serverUrl);
|
|
215
|
+
const url = new URL(serverUrl);
|
|
216
|
+
const transport = new SSEClientTransport(url, { authProvider });
|
|
217
|
+
try {
|
|
218
|
+
await transport.start();
|
|
219
|
+
console.error("Connected to remote server");
|
|
220
|
+
return transport;
|
|
221
|
+
} catch (error) {
|
|
222
|
+
if (error instanceof UnauthorizedError || error instanceof Error && error.message.includes("Unauthorized")) {
|
|
223
|
+
console.error("Authentication required. Waiting for authorization...");
|
|
224
|
+
const code = await waitForAuthCode();
|
|
225
|
+
try {
|
|
226
|
+
console.error("Completing authorization...");
|
|
227
|
+
await transport.finishAuth(code);
|
|
228
|
+
const newTransport = new SSEClientTransport(url, { authProvider });
|
|
229
|
+
await newTransport.start();
|
|
230
|
+
console.error("Connected to remote server after authentication");
|
|
231
|
+
return newTransport;
|
|
232
|
+
} catch (authError) {
|
|
233
|
+
console.error("Authorization error:", authError);
|
|
234
|
+
throw authError;
|
|
235
|
+
}
|
|
236
|
+
} else {
|
|
237
|
+
console.error("Connection error:", error);
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function mcpProxy({ transportToClient, transportToServer }) {
|
|
243
|
+
let transportToClientClosed = false;
|
|
244
|
+
let transportToServerClosed = false;
|
|
245
|
+
transportToClient.onmessage = (message) => {
|
|
246
|
+
console.error("[Local\u2192Remote]", message.method || message.id);
|
|
247
|
+
transportToServer.send(message).catch(onServerError);
|
|
248
|
+
};
|
|
249
|
+
transportToServer.onmessage = (message) => {
|
|
250
|
+
console.error("[Remote\u2192Local]", message.method || message.id);
|
|
251
|
+
transportToClient.send(message).catch(onClientError);
|
|
252
|
+
};
|
|
253
|
+
transportToClient.onclose = () => {
|
|
254
|
+
if (transportToServerClosed) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
transportToClientClosed = true;
|
|
258
|
+
transportToServer.close().catch(onServerError);
|
|
259
|
+
};
|
|
260
|
+
transportToServer.onclose = () => {
|
|
261
|
+
if (transportToClientClosed) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
transportToServerClosed = true;
|
|
265
|
+
transportToClient.close().catch(onClientError);
|
|
266
|
+
};
|
|
267
|
+
transportToClient.onerror = onClientError;
|
|
268
|
+
transportToServer.onerror = onServerError;
|
|
269
|
+
function onClientError(error) {
|
|
270
|
+
console.error("Error from local client:", error);
|
|
271
|
+
}
|
|
272
|
+
function onServerError(error) {
|
|
273
|
+
console.error("Error from remote server:", error);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
async function findAvailablePort(preferredPort) {
|
|
277
|
+
return new Promise((resolve, reject) => {
|
|
278
|
+
const server = net.createServer();
|
|
279
|
+
server.on("error", (err) => {
|
|
280
|
+
if (err.code === "EADDRINUSE") {
|
|
281
|
+
server.listen(0);
|
|
282
|
+
} else {
|
|
283
|
+
reject(err);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
server.on("listening", () => {
|
|
287
|
+
const { port } = server.address();
|
|
288
|
+
server.close(() => {
|
|
289
|
+
resolve(port);
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
server.listen(preferredPort || 0);
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
async function parseCommandLineArgs(args, defaultPort, usage) {
|
|
296
|
+
const serverUrl = args[0];
|
|
297
|
+
const specifiedPort = args[1] ? parseInt(args[1]) : void 0;
|
|
298
|
+
if (!serverUrl || !serverUrl.startsWith("https://")) {
|
|
299
|
+
console.error(usage);
|
|
300
|
+
process.exit(1);
|
|
301
|
+
}
|
|
302
|
+
const callbackPort = specifiedPort || await findAvailablePort(defaultPort);
|
|
303
|
+
if (specifiedPort) {
|
|
304
|
+
console.error(`Using specified callback port: ${callbackPort}`);
|
|
305
|
+
} else {
|
|
306
|
+
console.error(`Using automatically selected callback port: ${callbackPort}`);
|
|
307
|
+
}
|
|
308
|
+
return { serverUrl, callbackPort };
|
|
309
|
+
}
|
|
310
|
+
function setupSignalHandlers(cleanup) {
|
|
311
|
+
process.on("SIGINT", async () => {
|
|
312
|
+
console.error("\nShutting down...");
|
|
313
|
+
await cleanup();
|
|
314
|
+
process.exit(0);
|
|
315
|
+
});
|
|
316
|
+
process.stdin.resume();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export {
|
|
320
|
+
NodeOAuthClientProvider,
|
|
321
|
+
setupOAuthCallbackServer,
|
|
322
|
+
connectToRemoteServer,
|
|
323
|
+
mcpProxy,
|
|
324
|
+
parseCommandLineArgs,
|
|
325
|
+
setupSignalHandlers
|
|
326
|
+
};
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
NodeOAuthClientProvider,
|
|
4
|
+
parseCommandLineArgs,
|
|
5
|
+
setupOAuthCallbackServer,
|
|
6
|
+
setupSignalHandlers
|
|
7
|
+
} from "./chunk-SB5B4PZV.js";
|
|
8
|
+
|
|
9
|
+
// src/client.ts
|
|
10
|
+
import { EventEmitter } from "events";
|
|
11
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
12
|
+
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
13
|
+
import { ListResourcesResultSchema, ListToolsResultSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
14
|
+
import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js";
|
|
15
|
+
async function runClient(serverUrl, callbackPort) {
|
|
16
|
+
const events = new EventEmitter();
|
|
17
|
+
const authProvider = new NodeOAuthClientProvider({
|
|
18
|
+
serverUrl,
|
|
19
|
+
callbackPort,
|
|
20
|
+
clientName: "MCP CLI Client"
|
|
21
|
+
});
|
|
22
|
+
const client = new Client(
|
|
23
|
+
{
|
|
24
|
+
name: "mcp-cli",
|
|
25
|
+
version: "0.1.0"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
capabilities: {
|
|
29
|
+
sampling: {}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
);
|
|
33
|
+
const url = new URL(serverUrl);
|
|
34
|
+
function initTransport() {
|
|
35
|
+
const transport2 = new SSEClientTransport(url, { authProvider });
|
|
36
|
+
transport2.onmessage = (message) => {
|
|
37
|
+
console.log("Received message:", JSON.stringify(message, null, 2));
|
|
38
|
+
};
|
|
39
|
+
transport2.onerror = (error) => {
|
|
40
|
+
console.error("Transport error:", error);
|
|
41
|
+
};
|
|
42
|
+
transport2.onclose = () => {
|
|
43
|
+
console.log("Connection closed.");
|
|
44
|
+
process.exit(0);
|
|
45
|
+
};
|
|
46
|
+
return transport2;
|
|
47
|
+
}
|
|
48
|
+
const transport = initTransport();
|
|
49
|
+
const { server, waitForAuthCode } = setupOAuthCallbackServer({
|
|
50
|
+
port: callbackPort,
|
|
51
|
+
path: "/oauth/callback",
|
|
52
|
+
events
|
|
53
|
+
});
|
|
54
|
+
const cleanup = async () => {
|
|
55
|
+
console.log("\nClosing connection...");
|
|
56
|
+
await client.close();
|
|
57
|
+
server.close();
|
|
58
|
+
};
|
|
59
|
+
setupSignalHandlers(cleanup);
|
|
60
|
+
try {
|
|
61
|
+
console.log("Connecting to server...");
|
|
62
|
+
await client.connect(transport);
|
|
63
|
+
console.log("Connected successfully!");
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (error instanceof UnauthorizedError || error instanceof Error && error.message.includes("Unauthorized")) {
|
|
66
|
+
console.log("Authentication required. Waiting for authorization...");
|
|
67
|
+
const code = await waitForAuthCode();
|
|
68
|
+
try {
|
|
69
|
+
console.log("Completing authorization...");
|
|
70
|
+
await transport.finishAuth(code);
|
|
71
|
+
console.log("Connecting after authorization...");
|
|
72
|
+
await client.connect(initTransport());
|
|
73
|
+
console.log("Connected successfully!");
|
|
74
|
+
console.log("Requesting tools list...");
|
|
75
|
+
const tools = await client.request({ method: "tools/list" }, ListToolsResultSchema);
|
|
76
|
+
console.log("Tools:", JSON.stringify(tools, null, 2));
|
|
77
|
+
console.log("Requesting resource list...");
|
|
78
|
+
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.");
|
|
81
|
+
} catch (authError) {
|
|
82
|
+
console.error("Authorization error:", authError);
|
|
83
|
+
server.close();
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
console.error("Connection error:", error);
|
|
88
|
+
server.close();
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
console.log("Requesting tools list...");
|
|
94
|
+
const tools = await client.request({ method: "tools/list" }, ListToolsResultSchema);
|
|
95
|
+
console.log("Tools:", JSON.stringify(tools, null, 2));
|
|
96
|
+
} catch (e) {
|
|
97
|
+
console.log("Error requesting tools list:", e);
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
console.log("Requesting resource list...");
|
|
101
|
+
const resources = await client.request({ method: "resources/list" }, ListResourcesResultSchema);
|
|
102
|
+
console.log("Resources:", JSON.stringify(resources, null, 2));
|
|
103
|
+
} catch (e) {
|
|
104
|
+
console.log("Error requesting resources list:", e);
|
|
105
|
+
}
|
|
106
|
+
console.log("Listening for messages. Press Ctrl+C to exit.");
|
|
107
|
+
}
|
|
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);
|
|
110
|
+
}).catch((error) => {
|
|
111
|
+
console.error("Fatal error:", error);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
});
|
package/dist/proxy.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/proxy.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
NodeOAuthClientProvider,
|
|
4
|
+
connectToRemoteServer,
|
|
5
|
+
mcpProxy,
|
|
6
|
+
parseCommandLineArgs,
|
|
7
|
+
setupOAuthCallbackServer,
|
|
8
|
+
setupSignalHandlers
|
|
9
|
+
} from "./chunk-SB5B4PZV.js";
|
|
10
|
+
|
|
11
|
+
// src/proxy.ts
|
|
12
|
+
import { EventEmitter } from "events";
|
|
13
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
14
|
+
async function runProxy(serverUrl, callbackPort) {
|
|
15
|
+
const events = new EventEmitter();
|
|
16
|
+
const authProvider = new NodeOAuthClientProvider({
|
|
17
|
+
serverUrl,
|
|
18
|
+
callbackPort,
|
|
19
|
+
clientName: "MCP CLI Proxy"
|
|
20
|
+
});
|
|
21
|
+
const localTransport = new StdioServerTransport();
|
|
22
|
+
const { server, waitForAuthCode } = setupOAuthCallbackServer({
|
|
23
|
+
port: callbackPort,
|
|
24
|
+
path: "/oauth/callback",
|
|
25
|
+
events
|
|
26
|
+
});
|
|
27
|
+
try {
|
|
28
|
+
const remoteTransport = await connectToRemoteServer(serverUrl, authProvider, waitForAuthCode);
|
|
29
|
+
mcpProxy({
|
|
30
|
+
transportToClient: localTransport,
|
|
31
|
+
transportToServer: remoteTransport
|
|
32
|
+
});
|
|
33
|
+
await localTransport.start();
|
|
34
|
+
console.error("Local STDIO server running");
|
|
35
|
+
console.error("Proxy established successfully between local STDIO and remote SSE");
|
|
36
|
+
console.error("Press Ctrl+C to exit");
|
|
37
|
+
const cleanup = async () => {
|
|
38
|
+
await remoteTransport.close();
|
|
39
|
+
await localTransport.close();
|
|
40
|
+
server.close();
|
|
41
|
+
};
|
|
42
|
+
setupSignalHandlers(cleanup);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error("Fatal error:", error);
|
|
45
|
+
server.close();
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
parseCommandLineArgs(process.argv.slice(2), 3334, "Usage: npx tsx proxy.ts <https://server-url> [callback-port]").then(({ serverUrl, callbackPort }) => {
|
|
50
|
+
return runProxy(serverUrl, callbackPort);
|
|
51
|
+
}).catch((error) => {
|
|
52
|
+
console.error("Fatal error:", error);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
});
|
package/package.json
CHANGED
|
@@ -1,14 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-remote",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"
|
|
5
|
+
"bin": "dist/proxy.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist",
|
|
8
|
+
"README.md",
|
|
9
|
+
"LICENSE"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsup"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
6
15
|
"@modelcontextprotocol/sdk": "^1.7.0",
|
|
16
|
+
"express": "^4.21.2",
|
|
17
|
+
"open": "^10.1.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
7
20
|
"@types/express": "^5.0.0",
|
|
8
21
|
"@types/node": "^22.13.10",
|
|
9
|
-
"express": "^4.21.2",
|
|
10
|
-
"open": "^10.1.0",
|
|
11
22
|
"prettier": "^3.5.3",
|
|
12
|
-
"
|
|
23
|
+
"tsup": "^8.4.0",
|
|
24
|
+
"tsx": "^4.19.3",
|
|
25
|
+
"typescript": "^5.8.2"
|
|
13
26
|
}
|
|
14
27
|
}
|
package/.prettierrc
DELETED
package/sse-auth-client.ts
DELETED
|
@@ -1,307 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// sse-auth-client.ts - MCP Client with OAuth support
|
|
4
|
-
// Run with: npx tsx sse-auth-client.ts sse-auth-client.ts https://example.remote/server [callback-port]
|
|
5
|
-
|
|
6
|
-
import express from 'express'
|
|
7
|
-
import open from 'open'
|
|
8
|
-
import fs from 'fs/promises'
|
|
9
|
-
import path from 'path'
|
|
10
|
-
import os from 'os'
|
|
11
|
-
import crypto from 'crypto'
|
|
12
|
-
import { EventEmitter } from 'events'
|
|
13
|
-
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
|
14
|
-
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'
|
|
15
|
-
import { OAuthClientProvider, auth, UnauthorizedError } from '@modelcontextprotocol/sdk/client/auth.js'
|
|
16
|
-
import { ListResourcesResultSchema, ListToolsResultSchema } from '@modelcontextprotocol/sdk/types.js'
|
|
17
|
-
import {
|
|
18
|
-
OAuthClientInformation,
|
|
19
|
-
OAuthClientInformationFull,
|
|
20
|
-
OAuthClientInformationSchema,
|
|
21
|
-
OAuthTokens,
|
|
22
|
-
OAuthTokensSchema,
|
|
23
|
-
} from '@modelcontextprotocol/sdk/shared/auth.js'
|
|
24
|
-
|
|
25
|
-
// Implement OAuth client provider for Node.js environment
|
|
26
|
-
class NodeOAuthClientProvider implements OAuthClientProvider {
|
|
27
|
-
private configDir: string
|
|
28
|
-
private serverUrlHash: string
|
|
29
|
-
|
|
30
|
-
constructor(
|
|
31
|
-
private serverUrl: string,
|
|
32
|
-
private callbackPort: number = 3333,
|
|
33
|
-
private callbackPath: string = '/oauth/callback',
|
|
34
|
-
) {
|
|
35
|
-
this.serverUrlHash = crypto.createHash('md5').update(serverUrl).digest('hex')
|
|
36
|
-
this.configDir = path.join(os.homedir(), '.mcp-auth')
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
get redirectUrl(): string {
|
|
40
|
-
return `http://localhost:${this.callbackPort}${this.callbackPath}`
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
get clientMetadata() {
|
|
44
|
-
return {
|
|
45
|
-
redirect_uris: [this.redirectUrl],
|
|
46
|
-
token_endpoint_auth_method: 'none',
|
|
47
|
-
grant_types: ['authorization_code', 'refresh_token'],
|
|
48
|
-
response_types: ['code'],
|
|
49
|
-
client_name: 'MCP CLI Client',
|
|
50
|
-
client_uri: 'https://github.com/modelcontextprotocol/mcp-cli',
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
private async ensureConfigDir() {
|
|
55
|
-
try {
|
|
56
|
-
await fs.mkdir(this.configDir, { recursive: true })
|
|
57
|
-
} catch (error) {
|
|
58
|
-
console.error('Error creating config directory:', error)
|
|
59
|
-
throw error
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
private async readFile<T>(filename: string, schema: any): Promise<T | undefined> {
|
|
64
|
-
try {
|
|
65
|
-
await this.ensureConfigDir()
|
|
66
|
-
const filePath = path.join(this.configDir, `${this.serverUrlHash}_${filename}`)
|
|
67
|
-
const content = await fs.readFile(filePath, 'utf-8')
|
|
68
|
-
return await schema.parseAsync(JSON.parse(content))
|
|
69
|
-
} catch (error) {
|
|
70
|
-
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
71
|
-
return undefined
|
|
72
|
-
}
|
|
73
|
-
return undefined
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
private async writeFile(filename: string, data: any) {
|
|
78
|
-
try {
|
|
79
|
-
await this.ensureConfigDir()
|
|
80
|
-
const filePath = path.join(this.configDir, `${this.serverUrlHash}_${filename}`)
|
|
81
|
-
await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8')
|
|
82
|
-
} catch (error) {
|
|
83
|
-
console.error(`Error writing ${filename}:`, error)
|
|
84
|
-
throw error
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
private async writeTextFile(filename: string, text: string) {
|
|
89
|
-
try {
|
|
90
|
-
await this.ensureConfigDir()
|
|
91
|
-
const filePath = path.join(this.configDir, `${this.serverUrlHash}_${filename}`)
|
|
92
|
-
await fs.writeFile(filePath, text, 'utf-8')
|
|
93
|
-
} catch (error) {
|
|
94
|
-
console.error(`Error writing ${filename}:`, error)
|
|
95
|
-
throw error
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
private async readTextFile(filename: string): Promise<string> {
|
|
100
|
-
try {
|
|
101
|
-
await this.ensureConfigDir()
|
|
102
|
-
const filePath = path.join(this.configDir, `${this.serverUrlHash}_${filename}`)
|
|
103
|
-
return await fs.readFile(filePath, 'utf-8')
|
|
104
|
-
} catch (error) {
|
|
105
|
-
throw new Error('No code verifier saved for session')
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
async clientInformation(): Promise<OAuthClientInformation | undefined> {
|
|
110
|
-
return this.readFile<OAuthClientInformation>('client_info.json', OAuthClientInformationSchema)
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
async saveClientInformation(clientInformation: OAuthClientInformationFull): Promise<void> {
|
|
114
|
-
await this.writeFile('client_info.json', clientInformation)
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
async tokens(): Promise<OAuthTokens | undefined> {
|
|
118
|
-
return this.readFile<OAuthTokens>('tokens.json', OAuthTokensSchema)
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
async saveTokens(tokens: OAuthTokens): Promise<void> {
|
|
122
|
-
await this.writeFile('tokens.json', tokens)
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
async redirectToAuthorization(authorizationUrl: URL): Promise<void> {
|
|
126
|
-
console.log(`\nPlease authorize this client by visiting:\n${authorizationUrl.toString()}\n`)
|
|
127
|
-
try {
|
|
128
|
-
await open(authorizationUrl.toString())
|
|
129
|
-
console.log('Browser opened automatically.')
|
|
130
|
-
} catch (error) {
|
|
131
|
-
console.log('Could not open browser automatically. Please copy and paste the URL above into your browser.')
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
async saveCodeVerifier(codeVerifier: string): Promise<void> {
|
|
136
|
-
await this.writeTextFile('code_verifier.txt', codeVerifier)
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
async codeVerifier(): Promise<string> {
|
|
140
|
-
return await this.readTextFile('code_verifier.txt')
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Main function to run the client
|
|
145
|
-
async function runClient(serverUrl: string, callbackPort: number) {
|
|
146
|
-
// Set up event emitter for auth flow
|
|
147
|
-
const events = new EventEmitter()
|
|
148
|
-
|
|
149
|
-
// Create the OAuth client provider
|
|
150
|
-
const authProvider = new NodeOAuthClientProvider(serverUrl, callbackPort)
|
|
151
|
-
|
|
152
|
-
// Create the client
|
|
153
|
-
const client = new Client(
|
|
154
|
-
{
|
|
155
|
-
name: 'mcp-cli',
|
|
156
|
-
version: '0.1.0',
|
|
157
|
-
},
|
|
158
|
-
{
|
|
159
|
-
capabilities: {
|
|
160
|
-
sampling: {},
|
|
161
|
-
},
|
|
162
|
-
},
|
|
163
|
-
)
|
|
164
|
-
|
|
165
|
-
// Create the transport
|
|
166
|
-
const url = new URL(serverUrl)
|
|
167
|
-
|
|
168
|
-
function initTransport() {
|
|
169
|
-
const transport = new SSEClientTransport(url, { authProvider })
|
|
170
|
-
|
|
171
|
-
// Set up message and error handlers
|
|
172
|
-
transport.onmessage = (message) => {
|
|
173
|
-
console.log('Received message:', JSON.stringify(message, null, 2))
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
transport.onerror = (error) => {
|
|
177
|
-
console.error('Transport error:', error)
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
transport.onclose = () => {
|
|
181
|
-
console.log('Connection closed.')
|
|
182
|
-
process.exit(0)
|
|
183
|
-
}
|
|
184
|
-
return transport
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const transport = initTransport()
|
|
188
|
-
|
|
189
|
-
// Set up an HTTP server to handle OAuth callback
|
|
190
|
-
let authCode: string | null = null
|
|
191
|
-
const app = express()
|
|
192
|
-
|
|
193
|
-
app.get('/oauth/callback', (req, res) => {
|
|
194
|
-
const code = req.query.code as string | undefined
|
|
195
|
-
if (!code) {
|
|
196
|
-
res.status(400).send('Error: No authorization code received')
|
|
197
|
-
return
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
authCode = code
|
|
201
|
-
res.send('Authorization successful! You may close this window and return to the CLI.')
|
|
202
|
-
|
|
203
|
-
// Notify main flow that auth code is available
|
|
204
|
-
events.emit('auth-code-received', code)
|
|
205
|
-
})
|
|
206
|
-
|
|
207
|
-
const server = app.listen(callbackPort, () => {
|
|
208
|
-
console.log(`OAuth callback server running at http://localhost:${callbackPort}`)
|
|
209
|
-
})
|
|
210
|
-
|
|
211
|
-
// Function to wait for auth code
|
|
212
|
-
const waitForAuthCode = (): Promise<string> => {
|
|
213
|
-
return new Promise((resolve) => {
|
|
214
|
-
if (authCode) {
|
|
215
|
-
resolve(authCode)
|
|
216
|
-
return
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
events.once('auth-code-received', (code) => {
|
|
220
|
-
resolve(code)
|
|
221
|
-
})
|
|
222
|
-
})
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Try to connect
|
|
226
|
-
try {
|
|
227
|
-
console.log('Connecting to server...')
|
|
228
|
-
await client.connect(transport)
|
|
229
|
-
console.log('Connected successfully!')
|
|
230
|
-
|
|
231
|
-
// Send a resources/list request
|
|
232
|
-
// console.log("Requesting resource list...");
|
|
233
|
-
// const result = await client.request({ method: "resources/list" }, ListResourcesResultSchema);
|
|
234
|
-
// console.log("Resources:", JSON.stringify(result, null, 2));
|
|
235
|
-
|
|
236
|
-
console.log('Request tools list...')
|
|
237
|
-
const tools = await client.request({ method: 'tools/list' }, ListToolsResultSchema)
|
|
238
|
-
console.log('Tools:', JSON.stringify(tools, null, 2))
|
|
239
|
-
|
|
240
|
-
console.log('Listening for messages. Press Ctrl+C to exit.')
|
|
241
|
-
} catch (error) {
|
|
242
|
-
if (error instanceof UnauthorizedError || (error instanceof Error && error.message.includes('Unauthorized'))) {
|
|
243
|
-
console.log('Authentication required. Waiting for authorization...')
|
|
244
|
-
|
|
245
|
-
// Wait for the authorization code from the callback
|
|
246
|
-
const code = await waitForAuthCode()
|
|
247
|
-
|
|
248
|
-
try {
|
|
249
|
-
console.log('Completing authorization...')
|
|
250
|
-
await transport.finishAuth(code)
|
|
251
|
-
|
|
252
|
-
// Start a new transport here? Ok cause it's going to write to the file maybe?
|
|
253
|
-
|
|
254
|
-
// Reconnect after authorization
|
|
255
|
-
console.log('Connecting after authorization...')
|
|
256
|
-
await client.connect(initTransport())
|
|
257
|
-
|
|
258
|
-
console.log('Connected successfully!')
|
|
259
|
-
|
|
260
|
-
// // Send a resources/list request
|
|
261
|
-
// console.log("Requesting resource list...");
|
|
262
|
-
// const result = await client.request({ method: "resources/list" }, ListResourcesResultSchema);
|
|
263
|
-
// console.log("Resources:", JSON.stringify(result, null, 2));2));
|
|
264
|
-
|
|
265
|
-
console.log('Request tools list...')
|
|
266
|
-
const tools = await client.request({ method: 'tools/list' }, ListToolsResultSchema)
|
|
267
|
-
console.log('Tools:', JSON.stringify(tools, null, 2))
|
|
268
|
-
|
|
269
|
-
console.log('Listening for messages. Press Ctrl+C to exit.')
|
|
270
|
-
} catch (authError) {
|
|
271
|
-
console.error('Authorization error:', authError)
|
|
272
|
-
server.close()
|
|
273
|
-
process.exit(1)
|
|
274
|
-
}
|
|
275
|
-
} else {
|
|
276
|
-
console.error('Connection error:', error)
|
|
277
|
-
server.close()
|
|
278
|
-
process.exit(1)
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// Handle shutdown
|
|
283
|
-
process.on('SIGINT', async () => {
|
|
284
|
-
console.log('\nClosing connection...')
|
|
285
|
-
await client.close()
|
|
286
|
-
server.close()
|
|
287
|
-
process.exit(0)
|
|
288
|
-
})
|
|
289
|
-
|
|
290
|
-
// Keep the process alive
|
|
291
|
-
process.stdin.resume()
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// Parse command-line arguments
|
|
295
|
-
const args = process.argv.slice(2)
|
|
296
|
-
const serverUrl = args[0]
|
|
297
|
-
const callbackPort = args[1] ? parseInt(args[1]) : 3333
|
|
298
|
-
|
|
299
|
-
if (!serverUrl || !serverUrl.startsWith('https://')) {
|
|
300
|
-
console.error('Usage: node --experimental-strip-types sse-auth-client.ts <https://server-url> [callback-port]')
|
|
301
|
-
process.exit(1)
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
runClient(serverUrl, callbackPort).catch((error) => {
|
|
305
|
-
console.error('Fatal error:', error)
|
|
306
|
-
process.exit(1)
|
|
307
|
-
})
|
package/sse-auth-proxy.ts
DELETED
|
@@ -1,323 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// sse-auth-proxy.ts - MCP Proxy with OAuth support
|
|
4
|
-
// Run with: npx tsx sse-auth-proxy.ts https://example.remote/server [callback-port]
|
|
5
|
-
|
|
6
|
-
import express from 'express'
|
|
7
|
-
import open from 'open'
|
|
8
|
-
import fs from 'fs/promises'
|
|
9
|
-
import path from 'path'
|
|
10
|
-
import crypto from 'crypto'
|
|
11
|
-
import { EventEmitter } from 'events'
|
|
12
|
-
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
|
13
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
14
|
-
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'
|
|
15
|
-
import { OAuthClientProvider, auth, UnauthorizedError } from '@modelcontextprotocol/sdk/client/auth.js'
|
|
16
|
-
import {
|
|
17
|
-
OAuthClientInformation,
|
|
18
|
-
OAuthClientInformationFull,
|
|
19
|
-
OAuthClientInformationSchema,
|
|
20
|
-
OAuthTokens,
|
|
21
|
-
OAuthTokensSchema,
|
|
22
|
-
} from '@modelcontextprotocol/sdk/shared/auth.js'
|
|
23
|
-
import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'
|
|
24
|
-
import os from 'os'
|
|
25
|
-
|
|
26
|
-
// Implement OAuth client provider for Node.js environment
|
|
27
|
-
class NodeOAuthClientProvider implements OAuthClientProvider {
|
|
28
|
-
private configDir: string
|
|
29
|
-
private serverUrlHash: string
|
|
30
|
-
|
|
31
|
-
constructor(
|
|
32
|
-
private serverUrl: string,
|
|
33
|
-
private callbackPort: number = 3334,
|
|
34
|
-
private callbackPath: string = '/oauth/callback',
|
|
35
|
-
) {
|
|
36
|
-
this.serverUrlHash = crypto.createHash('md5').update(serverUrl).digest('hex')
|
|
37
|
-
this.configDir = path.join(os.homedir(), '.mcp-auth')
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
get redirectUrl(): string {
|
|
41
|
-
return `http://localhost:${this.callbackPort}${this.callbackPath}`
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
get clientMetadata() {
|
|
45
|
-
return {
|
|
46
|
-
redirect_uris: [this.redirectUrl],
|
|
47
|
-
token_endpoint_auth_method: 'none',
|
|
48
|
-
grant_types: ['authorization_code', 'refresh_token'],
|
|
49
|
-
response_types: ['code'],
|
|
50
|
-
client_name: 'MCP CLI Proxy',
|
|
51
|
-
client_uri: 'https://github.com/modelcontextprotocol/mcp-cli',
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
private async ensureConfigDir() {
|
|
56
|
-
try {
|
|
57
|
-
await fs.mkdir(this.configDir, { recursive: true })
|
|
58
|
-
} catch (error) {
|
|
59
|
-
console.error('Error creating config directory:', error)
|
|
60
|
-
throw error
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
private async readFile<T>(filename: string, schema: any): Promise<T | undefined> {
|
|
65
|
-
try {
|
|
66
|
-
await this.ensureConfigDir()
|
|
67
|
-
const filePath = path.join(this.configDir, `${this.serverUrlHash}_${filename}`)
|
|
68
|
-
const content = await fs.readFile(filePath, 'utf-8')
|
|
69
|
-
return await schema.parseAsync(JSON.parse(content))
|
|
70
|
-
} catch (error) {
|
|
71
|
-
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
72
|
-
return undefined
|
|
73
|
-
}
|
|
74
|
-
return undefined
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
private async writeFile(filename: string, data: any) {
|
|
79
|
-
try {
|
|
80
|
-
await this.ensureConfigDir()
|
|
81
|
-
const filePath = path.join(this.configDir, `${this.serverUrlHash}_${filename}`)
|
|
82
|
-
await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8')
|
|
83
|
-
} catch (error) {
|
|
84
|
-
console.error(`Error writing ${filename}:`, error)
|
|
85
|
-
throw error
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
private async writeTextFile(filename: string, text: string) {
|
|
90
|
-
try {
|
|
91
|
-
await this.ensureConfigDir()
|
|
92
|
-
const filePath = path.join(this.configDir, `${this.serverUrlHash}_${filename}`)
|
|
93
|
-
await fs.writeFile(filePath, text, 'utf-8')
|
|
94
|
-
} catch (error) {
|
|
95
|
-
console.error(`Error writing ${filename}:`, error)
|
|
96
|
-
throw error
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
private async readTextFile(filename: string): Promise<string> {
|
|
101
|
-
try {
|
|
102
|
-
await this.ensureConfigDir()
|
|
103
|
-
const filePath = path.join(this.configDir, `${this.serverUrlHash}_${filename}`)
|
|
104
|
-
return await fs.readFile(filePath, 'utf-8')
|
|
105
|
-
} catch (error) {
|
|
106
|
-
throw new Error('No code verifier saved for session')
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
async clientInformation(): Promise<OAuthClientInformation | undefined> {
|
|
111
|
-
return this.readFile<OAuthClientInformation>('client_info.json', OAuthClientInformationSchema)
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
async saveClientInformation(clientInformation: OAuthClientInformationFull): Promise<void> {
|
|
115
|
-
await this.writeFile('client_info.json', clientInformation)
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
async tokens(): Promise<OAuthTokens | undefined> {
|
|
119
|
-
return this.readFile<OAuthTokens>('tokens.json', OAuthTokensSchema)
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
async saveTokens(tokens: OAuthTokens): Promise<void> {
|
|
123
|
-
await this.writeFile('tokens.json', tokens)
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
async redirectToAuthorization(authorizationUrl: URL): Promise<void> {
|
|
127
|
-
console.error(`\nPlease authorize this client by visiting:\n${authorizationUrl.toString()}\n`)
|
|
128
|
-
try {
|
|
129
|
-
await open(authorizationUrl.toString())
|
|
130
|
-
console.error('Browser opened automatically.')
|
|
131
|
-
} catch (error) {
|
|
132
|
-
console.error('Could not open browser automatically. Please copy and paste the URL above into your browser.')
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
async saveCodeVerifier(codeVerifier: string): Promise<void> {
|
|
137
|
-
await this.writeTextFile('code_verifier.txt', codeVerifier)
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
async codeVerifier(): Promise<string> {
|
|
141
|
-
return await this.readTextFile('code_verifier.txt')
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Function to proxy messages between two transports
|
|
146
|
-
function mcpProxy({ transportToClient, transportToServer }: { transportToClient: Transport; transportToServer: Transport }) {
|
|
147
|
-
let transportToClientClosed = false
|
|
148
|
-
let transportToServerClosed = false
|
|
149
|
-
|
|
150
|
-
transportToClient.onmessage = (message) => {
|
|
151
|
-
console.error('[Local→Remote]', message.method || message.id)
|
|
152
|
-
transportToServer.send(message).catch(onServerError)
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
transportToServer.onmessage = (message) => {
|
|
156
|
-
console.error('[Remote→Local]', message.method || message.id)
|
|
157
|
-
transportToClient.send(message).catch(onClientError)
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
transportToClient.onclose = () => {
|
|
161
|
-
if (transportToServerClosed) {
|
|
162
|
-
return
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
transportToClientClosed = true
|
|
166
|
-
transportToServer.close().catch(onServerError)
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
transportToServer.onclose = () => {
|
|
170
|
-
if (transportToClientClosed) {
|
|
171
|
-
return
|
|
172
|
-
}
|
|
173
|
-
transportToServerClosed = true
|
|
174
|
-
transportToClient.close().catch(onClientError)
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
transportToClient.onerror = onClientError
|
|
178
|
-
transportToServer.onerror = onServerError
|
|
179
|
-
|
|
180
|
-
function onClientError(error: Error) {
|
|
181
|
-
console.error('Error from local client:', error)
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function onServerError(error: Error) {
|
|
185
|
-
console.error('Error from remote server:', error)
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Main function to run the proxy
|
|
190
|
-
async function runProxy(serverUrl: string, callbackPort: number) {
|
|
191
|
-
// Set up event emitter for auth flow
|
|
192
|
-
const events = new EventEmitter()
|
|
193
|
-
|
|
194
|
-
// Create the OAuth client provider
|
|
195
|
-
const authProvider = new NodeOAuthClientProvider(serverUrl, callbackPort)
|
|
196
|
-
|
|
197
|
-
// Create the STDIO transport
|
|
198
|
-
const localTransport = new StdioServerTransport()
|
|
199
|
-
|
|
200
|
-
// Set up an HTTP server to handle OAuth callback
|
|
201
|
-
let authCode: string | null = null
|
|
202
|
-
const app = express()
|
|
203
|
-
|
|
204
|
-
app.get('/oauth/callback', (req, res) => {
|
|
205
|
-
const code = req.query.code as string | undefined
|
|
206
|
-
if (!code) {
|
|
207
|
-
res.status(400).send('Error: No authorization code received')
|
|
208
|
-
return
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
authCode = code
|
|
212
|
-
res.send('Authorization successful! You may close this window and return to the CLI.')
|
|
213
|
-
|
|
214
|
-
// Notify main flow that auth code is available
|
|
215
|
-
events.emit('auth-code-received', code)
|
|
216
|
-
})
|
|
217
|
-
|
|
218
|
-
const httpServer = app.listen(callbackPort, () => {
|
|
219
|
-
console.error(`OAuth callback server running at http://localhost:${callbackPort}`)
|
|
220
|
-
})
|
|
221
|
-
|
|
222
|
-
// Function to wait for auth code
|
|
223
|
-
const waitForAuthCode = (): Promise<string> => {
|
|
224
|
-
return new Promise((resolve) => {
|
|
225
|
-
if (authCode) {
|
|
226
|
-
resolve(authCode)
|
|
227
|
-
return
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
events.once('auth-code-received', (code) => {
|
|
231
|
-
resolve(code)
|
|
232
|
-
})
|
|
233
|
-
})
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// Function to create and connect to remote server, handling auth
|
|
237
|
-
const connectToRemoteServer = async (): Promise<SSEClientTransport> => {
|
|
238
|
-
console.error('Connecting to remote server:', serverUrl)
|
|
239
|
-
const url = new URL(serverUrl)
|
|
240
|
-
const transport = new SSEClientTransport(url, { authProvider })
|
|
241
|
-
|
|
242
|
-
try {
|
|
243
|
-
await transport.start()
|
|
244
|
-
console.error('Connected to remote server')
|
|
245
|
-
return transport
|
|
246
|
-
} catch (error) {
|
|
247
|
-
if (error instanceof UnauthorizedError || (error instanceof Error && error.message.includes('Unauthorized'))) {
|
|
248
|
-
console.error('Authentication required. Waiting for authorization...')
|
|
249
|
-
|
|
250
|
-
// Wait for the authorization code from the callback
|
|
251
|
-
const code = await waitForAuthCode()
|
|
252
|
-
|
|
253
|
-
try {
|
|
254
|
-
console.error('Completing authorization...')
|
|
255
|
-
await transport.finishAuth(code)
|
|
256
|
-
|
|
257
|
-
// Create a new transport after auth
|
|
258
|
-
const newTransport = new SSEClientTransport(url, { authProvider })
|
|
259
|
-
await newTransport.start()
|
|
260
|
-
console.error('Connected to remote server after authentication')
|
|
261
|
-
return newTransport
|
|
262
|
-
} catch (authError) {
|
|
263
|
-
console.error('Authorization error:', authError)
|
|
264
|
-
throw authError
|
|
265
|
-
}
|
|
266
|
-
} else {
|
|
267
|
-
console.error('Connection error:', error)
|
|
268
|
-
throw error
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
try {
|
|
274
|
-
// Start local server
|
|
275
|
-
// await server.connect(serverTransport)
|
|
276
|
-
|
|
277
|
-
// Connect to remote server
|
|
278
|
-
const remoteTransport = await connectToRemoteServer()
|
|
279
|
-
|
|
280
|
-
// Set up bidirectional proxy
|
|
281
|
-
mcpProxy({
|
|
282
|
-
transportToClient: localTransport,
|
|
283
|
-
transportToServer: remoteTransport,
|
|
284
|
-
})
|
|
285
|
-
|
|
286
|
-
await localTransport.start()
|
|
287
|
-
console.error('Local STDIO server running')
|
|
288
|
-
|
|
289
|
-
console.error('Proxy established successfully')
|
|
290
|
-
console.error('Press Ctrl+C to exit')
|
|
291
|
-
|
|
292
|
-
// Handle shutdown
|
|
293
|
-
process.on('SIGINT', async () => {
|
|
294
|
-
console.error('\nShutting down proxy...')
|
|
295
|
-
await remoteTransport.close()
|
|
296
|
-
await localTransport.close()
|
|
297
|
-
httpServer.close()
|
|
298
|
-
process.exit(0)
|
|
299
|
-
})
|
|
300
|
-
|
|
301
|
-
// Keep the process alive
|
|
302
|
-
process.stdin.resume()
|
|
303
|
-
} catch (error) {
|
|
304
|
-
console.error('Fatal error:', error)
|
|
305
|
-
httpServer.close()
|
|
306
|
-
process.exit(1)
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// Parse command-line arguments
|
|
311
|
-
const args = process.argv.slice(2)
|
|
312
|
-
const serverUrl = args[0]
|
|
313
|
-
const callbackPort = args[1] ? parseInt(args[1]) : 3334
|
|
314
|
-
|
|
315
|
-
if (!serverUrl || !serverUrl.startsWith('https://')) {
|
|
316
|
-
console.error('Usage: npx tsx sse-auth-proxy.ts <https://server-url> [callback-port]')
|
|
317
|
-
process.exit(1)
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
runProxy(serverUrl, callbackPort).catch((error) => {
|
|
321
|
-
console.error('Fatal error:', error)
|
|
322
|
-
process.exit(1)
|
|
323
|
-
})
|
package/tsconfig.json
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "Node16",
|
|
5
|
-
"moduleResolution": "Node16",
|
|
6
|
-
"outDir": "./build",
|
|
7
|
-
"rootDir": "./src",
|
|
8
|
-
"strict": true,
|
|
9
|
-
"esModuleInterop": true,
|
|
10
|
-
"lib": ["ES2022"],
|
|
11
|
-
"types": ["node"],
|
|
12
|
-
"forceConsistentCasingInFileNames": true,
|
|
13
|
-
"resolveJsonModule": true
|
|
14
|
-
},
|
|
15
|
-
"include": ["*.ts","src/**/*"],
|
|
16
|
-
"exclude": ["node_modules", "packages", "**/*.spec.ts"]
|
|
17
|
-
}
|