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 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
+ ![image](https://github.com/user-attachments/assets/14338bfa-a779-4e8a-a477-71f72cc5d99d)
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
+ };
@@ -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
+ });
@@ -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.1",
3
+ "version": "0.0.2",
4
4
  "type": "module",
5
- "devDependencies": {
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
- "tsx": "^4.19.3"
23
+ "tsup": "^8.4.0",
24
+ "tsx": "^4.19.3",
25
+ "typescript": "^5.8.2"
13
26
  }
14
27
  }
package/.prettierrc DELETED
@@ -1,14 +0,0 @@
1
- {
2
- "printWidth": 140,
3
- "singleQuote": true,
4
- "semi": false,
5
- "useTabs": false,
6
- "overrides": [
7
- {
8
- "files": ["*.jsonc"],
9
- "options": {
10
- "trailingComma": "none"
11
- }
12
- }
13
- ]
14
- }
@@ -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
- }