@stackone/mcp-remote
Advanced tools
+2
-2
| { | ||
| "name": "@stackone/mcp-remote", | ||
| "version": "0.0.4", | ||
| "version": "0.0.5", | ||
| "type": "module", | ||
@@ -56,3 +56,3 @@ "main": "./dist/index.js", | ||
| "typecheck": "tsc --noEmit", | ||
| "publish-release": "pnpm publish --access public", | ||
| "publish-release": "pnpm publish --no-git-checks --access public", | ||
| "client": "tsx src/client.ts", | ||
@@ -59,0 +59,0 @@ "proxy": "tsx src/proxy.ts" |
+1
-206
| # MCP Remote | ||
| >Based off this package by @geelen: https://github.com/geelen/mcp-remote | ||
| Please use this package by @geelen: https://github.com/geelen/mcp-remote | ||
| This adds bearer token auth. | ||
| Connect an MCP Client that only supports local (stdio) servers to a Remote MCP Server, with auth support: | ||
| **Note: this is a working proof-of-concept** but should be considered **experimental**. | ||
| ## Why is this necessary? | ||
| So far, the majority of MCP servers in the wild are installed locally, using the stdio transport. This has some benefits: both the client and the server can implicitly trust each other as the user has granted them both permission to run. Adding secrets like API keys can be done using environment variables and never leave your machine. And building on `npx` and `uvx` has allowed users to avoid explicit install steps, too. | ||
| But there's a reason most software that _could_ be moved to the web _did_ get moved to the web: it's so much easier to find and fix bugs & iterate on new features when you can push updates to all your users with a single deploy. | ||
| With the MCP [Authorization specification](https://spec.modelcontextprotocol.io/specification/draft/basic/authorization/) nearing completion, we now have a secure way of sharing our MCP servers with the world _without_ running code on user's laptops. Or at least, you would, if all the popular MCP _clients_ supported it yet. Most are stdio-only, and those that _do_ support HTTP+SSE don't yet support the OAuth flows required. | ||
| That's where `mcp-remote` comes in. As soon as your chosen MCP client supports remote, authorized servers, you can remove it. Until that time, drop in this one liner and dress for the MCP clients you want! | ||
| ## Usage | ||
| All the most popular MCP clients (Claude Desktop, Cursor & Windsurf) use the following config format: | ||
| ```json | ||
| { | ||
| "mcpServers": { | ||
| "remote-example": { | ||
| "command": "npx", | ||
| "args": ["@stackone/mcp-remote", "https://remote.mcp.server/sse"] | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
| ### Authentication Methods | ||
| `mcp-remote` supports two authentication methods: | ||
| 1. **OAuth Authentication** (default) - Uses the standard OAuth flow with browser-based authorization | ||
| 2. **Bearer Token Authentication** - Uses a static bearer token for servers that don't require OAuth | ||
| To use bearer token authentication, add the `--bearer` flag followed by your token: | ||
| ```json | ||
| { | ||
| "mcpServers": { | ||
| "remote-example": { | ||
| "command": "npx", | ||
| "args": [ | ||
| "@stackone/mcp-remote", | ||
| "--bearer", | ||
| "your-bearer-token", | ||
| "https://remote.mcp.server/sse" | ||
| ] | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
| ### Additional Flags | ||
| - If `npx` is producing errors, consider adding `-y` as the first argument to auto-accept the installation of the `mcp-remote` package. | ||
| ```json | ||
| "command": "npx", | ||
| "args": [ | ||
| "-y", | ||
| "@stackone/mcp-remote", | ||
| "https://remote.mcp.server/sse" | ||
| ] | ||
| ``` | ||
| - To force `npx` to always check for an updated version of `mcp-remote`, add the `@latest` flag: | ||
| ```json | ||
| "args": [ | ||
| "mcp-remote@latest", | ||
| "https://remote.mcp.server/sse" | ||
| ] | ||
| ``` | ||
| - To force `mcp-remote` to ignore any existing access tokens and begin the authorization flow anew, pass `--clean`. | ||
| ```json | ||
| "args": [ | ||
| "@stackone/mcp-remote", | ||
| "https://remote.mcp.server/sse", | ||
| "--clean" | ||
| ] | ||
| ``` | ||
| - 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. | ||
| ```json | ||
| "args": [ | ||
| "@stackone/mcp-remote", | ||
| "https://remote.mcp.server/sse", | ||
| "9696" | ||
| ] | ||
| ``` | ||
| ### Claude Desktop | ||
| [Official Docs](https://modelcontextprotocol.io/quickstart/user) | ||
| In order to add an MCP server to Claude Desktop you need to edit the configuration file located at: | ||
| - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json` | ||
| - Windows: `%APPDATA%\Claude\claude_desktop_config.json` | ||
| 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). | ||
| Restart Claude Desktop to pick up the changes in the configuration file. | ||
| Upon restarting, you should see a hammer icon in the bottom right corner | ||
| of the input box. | ||
| ### Cursor | ||
| [Official Docs](https://docs.cursor.com/context/model-context-protocol). The configuration file is located at `~/.cursor/mcp.json`. | ||
| 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`. | ||
| ### Windsurf | ||
| [Official Docs](https://docs.codeium.com/windsurf/mcp). The configuration file is located at `~/.codeium/windsurf/mcp_config.json`. | ||
| ## Troubleshooting | ||
| ### Clear your `~/.mcp-auth` directory | ||
| `mcp-remote` stores OAuth credential information inside `~/.mcp-auth` (or wherever your `MCP_REMOTE_CONFIG_DIR` points to). If you're having persistent issues with OAuth authentication, try running: | ||
| ```sh | ||
| rm -rf ~/.mcp-auth | ||
| ``` | ||
| Then restarting your MCP client. Note: This is not necessary when using bearer token authentication. | ||
| ### Check your Node version | ||
| Make sure that the version of Node you have installed is [18 or | ||
| higher](https://modelcontextprotocol.io/quickstart/server). Claude | ||
| Desktop will use your system version of Node, even if you have a newer | ||
| version installed elsewhere. | ||
| ### Restart Claude | ||
| When modifying `claude_desktop_config.json` it can helpful to completely restart Claude | ||
| ### VPN Certs | ||
| You may run into issues if you are behind a VPN, you can try setting the `NODE_EXTRA_CA_CERTS` | ||
| environment variable to point to the CA certificate file. If using `claude_desktop_config.json`, | ||
| this might look like: | ||
| ```json | ||
| { | ||
| "mcpServers": { | ||
| "remote-example": { | ||
| "command": "npx", | ||
| "args": ["@stackone/mcp-remote", "https://remote.mcp.server/sse"], | ||
| "env": { | ||
| "NODE_EXTRA_CA_CERTS": "{your CA certificate file path}.pem" | ||
| } | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
| ### Check the logs | ||
| - [Follow Claude Desktop logs in real-time](https://modelcontextprotocol.io/docs/tools/debugging#debugging-in-claude-desktop) | ||
| - MacOS / Linux:<br/>`tail -n 20 -F ~/Library/Logs/Claude/mcp*.log` | ||
| - For bash on WSL:<br/>`tail -n 20 -f "C:\Users\YourUsername\AppData\Local\Claude\Logs\mcp.log"` | ||
| - Powershell: <br/>`Get-Content "C:\Users\YourUsername\AppData\Local\Claude\Logs\mcp.log" -Wait -Tail 20` | ||
| ## Debugging | ||
| ### Authentication Issues | ||
| If using OAuth authentication and you encounter the following error, returned by the `/callback` URL: | ||
| ``` | ||
| Authentication Error | ||
| Token exchange failed: HTTP 400 | ||
| ``` | ||
| You can run `rm -rf ~/.mcp-auth` to clear any locally stored state and tokens. | ||
| If using bearer token authentication and you encounter authentication errors, verify that: | ||
| 1. The token is correctly specified in the configuration | ||
| 2. The token is valid and has not expired | ||
| 3. The token has the necessary permissions for the server | ||
| ### "Client" mode | ||
| Run the following on the command line (not from an MCP server): | ||
| ```shell | ||
| # Using OAuth authentication | ||
| npx -p mcp-remote@latest mcp-remote-client https://remote.mcp.server/sse | ||
| # Using bearer token authentication | ||
| npx -p mcp-remote@latest mcp-remote-client --bearer your-token https://remote.mcp.server/sse | ||
| ``` | ||
| This will attempt to connect to the remote URL and list the tools & resources. For OAuth authentication, pair this with `--clean` or after running `rm -rf ~/.mcp-auth` to see if stale credentials are your problem. The logs should help identify any authentication or connection issues. |
| var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, { | ||
| get: (a, b) => (typeof require !== "undefined" ? require : a)[b] | ||
| }) : x)(function(x) { | ||
| if (typeof require !== "undefined") return require.apply(this, arguments); | ||
| throw Error('Dynamic require of "' + x + '" is not supported'); | ||
| }); | ||
| // src/lib/bearer-token-provider.ts | ||
| var BearerTokenProvider = class { | ||
| constructor(options) { | ||
| this.options = options; | ||
| } | ||
| // Required by OAuthClientProvider interface but not used for bearer token auth | ||
| get redirectUrl() { | ||
| return "http://localhost:0/unused"; | ||
| } | ||
| // Required by OAuthClientProvider interface but not used for bearer token auth | ||
| get clientMetadata() { | ||
| return { | ||
| redirect_uris: [this.redirectUrl], | ||
| token_endpoint_auth_method: "none", | ||
| grant_types: [], | ||
| response_types: [], | ||
| client_name: "Bearer Token Client", | ||
| client_uri: "" | ||
| }; | ||
| } | ||
| async clientInformation() { | ||
| return void 0; | ||
| } | ||
| async saveClientInformation() { | ||
| } | ||
| async tokens() { | ||
| return { | ||
| access_token: this.options.bearerToken, | ||
| token_type: "Bearer", | ||
| expires_in: 0, | ||
| // Never expires | ||
| refresh_token: "" | ||
| // No refresh token needed | ||
| }; | ||
| } | ||
| async saveTokens() { | ||
| } | ||
| async redirectToAuthorization() { | ||
| } | ||
| async saveCodeVerifier() { | ||
| } | ||
| async codeVerifier() { | ||
| throw new Error("Code verifier not supported with bearer token authentication"); | ||
| } | ||
| }; | ||
| // src/lib/utils.ts | ||
| import crypto from "node:crypto"; | ||
| import net from "node:net"; | ||
| import { | ||
| UnauthorizedError | ||
| } from "@modelcontextprotocol/sdk/client/auth.js"; | ||
| import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; | ||
| import express from "express"; | ||
| // src/lib/version.ts | ||
| import { readFile } from "node:fs/promises"; | ||
| import { dirname, join } from "node:path"; | ||
| import { fileURLToPath } from "node:url"; | ||
| var __filename = fileURLToPath(import.meta.url); | ||
| var __dirname = dirname(__filename); | ||
| var packageJsonPath = join(__dirname, "../../package.json"); | ||
| var getPackageVersion = async () => { | ||
| const packageJson = JSON.parse(await readFile(packageJsonPath, "utf-8")); | ||
| return packageJson.version; | ||
| }; | ||
| // src/lib/utils.ts | ||
| var MCP_REMOTE_VERSION = "0.0.0"; | ||
| getPackageVersion().then((version) => { | ||
| MCP_REMOTE_VERSION = version; | ||
| }); | ||
| var pid = process.pid; | ||
| function log(str, ...rest) { | ||
| console.error(`[${pid}] ${str}`, ...rest); | ||
| } | ||
| function mcpProxy({ | ||
| transportToClient, | ||
| transportToServer | ||
| }) { | ||
| let transportToClientClosed = false; | ||
| let transportToServerClosed = false; | ||
| transportToClient.onmessage = (message) => { | ||
| log("[Local\u2192Remote]", message.method || message.id); | ||
| transportToServer.send(message).catch(onServerError); | ||
| }; | ||
| transportToServer.onmessage = (message) => { | ||
| log("[Remote\u2192Local]", message.method || message.id); | ||
| transportToClient.send(message).catch(onClientError); | ||
| }; | ||
| transportToClient.onclose = () => { | ||
| if (transportToServerClosed) { | ||
| return; | ||
| } | ||
| transportToClientClosed = true; | ||
| transportToServer.close().catch(onServerError); | ||
| }; | ||
| transportToServer.onclose = () => { | ||
| if (transportToClientClosed) { | ||
| return; | ||
| } | ||
| transportToServerClosed = true; | ||
| transportToClient.close().catch(onClientError); | ||
| }; | ||
| transportToClient.onerror = onClientError; | ||
| transportToServer.onerror = onServerError; | ||
| function onClientError(error) { | ||
| log("Error from local client:", error); | ||
| } | ||
| function onServerError(error) { | ||
| log("Error from remote server:", error); | ||
| } | ||
| } | ||
| async function connectToRemoteServer(serverUrl, authProvider, waitForAuthCode, skipBrowserAuth = false) { | ||
| log(`[${pid}] Connecting to remote server: ${serverUrl}`); | ||
| const url = new URL(serverUrl); | ||
| const transport = new SSEClientTransport(url, { authProvider }); | ||
| try { | ||
| await transport.start(); | ||
| log("Connected to remote server"); | ||
| return transport; | ||
| } catch (error) { | ||
| if (error instanceof UnauthorizedError || error instanceof Error && error.message.includes("Unauthorized")) { | ||
| if (skipBrowserAuth) { | ||
| log("Authentication required but skipping browser auth - using shared auth"); | ||
| } else { | ||
| log("Authentication required. Waiting for authorization..."); | ||
| } | ||
| const code = await waitForAuthCode(); | ||
| try { | ||
| log("Completing authorization..."); | ||
| await transport.finishAuth(code); | ||
| const newTransport = new SSEClientTransport(url, { authProvider }); | ||
| await newTransport.start(); | ||
| log("Connected to remote server after authentication"); | ||
| return newTransport; | ||
| } catch (authError) { | ||
| log("Authorization error:", authError); | ||
| throw authError; | ||
| } | ||
| } else { | ||
| log("Connection error:", error); | ||
| throw error; | ||
| } | ||
| } | ||
| } | ||
| function setupOAuthCallbackServerWithLongPoll(options) { | ||
| let authCode = null; | ||
| const app = express(); | ||
| let authCompletedResolve; | ||
| const authCompletedPromise = new Promise((resolve) => { | ||
| authCompletedResolve = resolve; | ||
| }); | ||
| app.get("/wait-for-auth", (req, res) => { | ||
| if (authCode) { | ||
| log("Auth already completed, returning 200"); | ||
| res.status(200).send("Authentication completed"); | ||
| return; | ||
| } | ||
| if (req.query.poll === "false") { | ||
| log("Client requested no long poll, responding with 202"); | ||
| res.status(202).send("Authentication in progress"); | ||
| return; | ||
| } | ||
| const longPollTimeout = setTimeout(() => { | ||
| log("Long poll timeout reached, responding with 202"); | ||
| res.status(202).send("Authentication in progress"); | ||
| }, 3e4); | ||
| authCompletedPromise.then(() => { | ||
| clearTimeout(longPollTimeout); | ||
| if (!res.headersSent) { | ||
| log("Auth completed during long poll, responding with 200"); | ||
| res.status(200).send("Authentication completed"); | ||
| } | ||
| }).catch(() => { | ||
| clearTimeout(longPollTimeout); | ||
| if (!res.headersSent) { | ||
| log("Auth failed during long poll, responding with 500"); | ||
| res.status(500).send("Authentication failed"); | ||
| } | ||
| }); | ||
| }); | ||
| app.get(options.path, (req, res) => { | ||
| const code = req.query.code; | ||
| if (!code) { | ||
| res.status(400).send("Error: No authorization code received"); | ||
| return; | ||
| } | ||
| authCode = code; | ||
| log("Auth code received, resolving promise"); | ||
| authCompletedResolve(code); | ||
| res.send( | ||
| "Authorization successful! You may close this window and return to the CLI." | ||
| ); | ||
| options.events.emit("auth-code-received", code); | ||
| }); | ||
| const server = app.listen(options.port, () => { | ||
| log(`OAuth callback server running at http://127.0.0.1:${options.port}`); | ||
| }); | ||
| const waitForAuthCode = () => { | ||
| return new Promise((resolve) => { | ||
| if (authCode) { | ||
| resolve(authCode); | ||
| return; | ||
| } | ||
| options.events.once("auth-code-received", (code) => { | ||
| resolve(code); | ||
| }); | ||
| }); | ||
| }; | ||
| return { server, authCode, waitForAuthCode, authCompletedPromise }; | ||
| } | ||
| async function findAvailablePort(preferredPort) { | ||
| return new Promise((resolve, reject) => { | ||
| const server = net.createServer(); | ||
| server.on("error", (err) => { | ||
| if (err.code === "EADDRINUSE") { | ||
| server.listen(0); | ||
| } else { | ||
| reject(err); | ||
| } | ||
| }); | ||
| server.on("listening", () => { | ||
| const { port } = server.address(); | ||
| server.close(() => { | ||
| resolve(port); | ||
| }); | ||
| }); | ||
| server.listen(preferredPort || 0); | ||
| }); | ||
| } | ||
| async function parseCommandLineArgs(args, defaultPort, usage) { | ||
| const cleanIndex = args.indexOf("--clean"); | ||
| const clean = cleanIndex !== -1; | ||
| if (clean) { | ||
| args.splice(cleanIndex, 1); | ||
| } | ||
| const bearerIndex = args.indexOf("--bearer"); | ||
| const hasBearer = bearerIndex !== -1; | ||
| let bearerToken; | ||
| if (hasBearer) { | ||
| if (bearerIndex + 1 >= args.length) { | ||
| log("Error: --bearer flag requires a token value"); | ||
| process.exit(1); | ||
| } | ||
| bearerToken = args[bearerIndex + 1]; | ||
| args.splice(bearerIndex, 2); | ||
| } | ||
| const serverUrl = args[0]; | ||
| const specifiedPort = args[1] ? Number.parseInt(args[1]) : void 0; | ||
| if (!serverUrl) { | ||
| log(usage); | ||
| process.exit(1); | ||
| } | ||
| const url = new URL(serverUrl); | ||
| const isLocalhost = (url.hostname === "localhost" || url.hostname === "127.0.0.1") && url.protocol === "http:"; | ||
| if (!(url.protocol === "https:" || isLocalhost)) { | ||
| log(usage); | ||
| process.exit(1); | ||
| } | ||
| const callbackPort = specifiedPort || await findAvailablePort(defaultPort); | ||
| if (specifiedPort) { | ||
| log(`Using specified callback port: ${callbackPort}`); | ||
| } else { | ||
| log(`Using automatically selected callback port: ${callbackPort}`); | ||
| } | ||
| if (clean) { | ||
| log("Clean mode enabled: config files will be reset before reading"); | ||
| } | ||
| return { serverUrl, callbackPort, clean, bearerToken }; | ||
| } | ||
| function setupSignalHandlers(cleanup) { | ||
| process.on("SIGINT", async () => { | ||
| log("\nShutting down..."); | ||
| await cleanup(); | ||
| process.exit(0); | ||
| }); | ||
| process.stdin.resume(); | ||
| } | ||
| function getServerUrlHash(serverUrl) { | ||
| return crypto.createHash("md5").update(serverUrl).digest("hex"); | ||
| } | ||
| // src/lib/coordination.ts | ||
| import express2 from "express"; | ||
| // src/lib/mcp-auth-config.ts | ||
| import fs from "node:fs/promises"; | ||
| import os from "node:os"; | ||
| import path from "node:path"; | ||
| var knownConfigFiles = [ | ||
| "client_info.json", | ||
| "tokens.json", | ||
| "code_verifier.txt", | ||
| "lock.json" | ||
| ]; | ||
| async function createLockfile(serverUrlHash, pid2, port) { | ||
| const lockData = { | ||
| pid: pid2, | ||
| port, | ||
| timestamp: Date.now() | ||
| }; | ||
| await writeJsonFile(serverUrlHash, "lock.json", lockData); | ||
| } | ||
| async function checkLockfile(serverUrlHash) { | ||
| try { | ||
| const lockfile = await readJsonFile(serverUrlHash, "lock.json", { | ||
| async parseAsync(data) { | ||
| if (typeof data !== "object" || data === null) return null; | ||
| if (typeof data.pid !== "number" || typeof data.port !== "number" || typeof data.timestamp !== "number") { | ||
| return null; | ||
| } | ||
| return data; | ||
| } | ||
| }); | ||
| return lockfile || null; | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
| async function deleteLockfile(serverUrlHash) { | ||
| await deleteConfigFile(serverUrlHash, "lock.json"); | ||
| } | ||
| async function cleanServerConfig(serverUrlHash) { | ||
| log(`Cleaning configuration files for server: ${serverUrlHash}`); | ||
| for (const filename of knownConfigFiles) { | ||
| await deleteConfigFile(serverUrlHash, filename); | ||
| } | ||
| } | ||
| function getConfigDir() { | ||
| const baseConfigDir = process.env.MCP_REMOTE_CONFIG_DIR || path.join(os.homedir(), ".mcp-auth"); | ||
| return path.join(baseConfigDir, `mcp-remote-${MCP_REMOTE_VERSION}`); | ||
| } | ||
| async function ensureConfigDir() { | ||
| try { | ||
| const configDir = getConfigDir(); | ||
| await fs.mkdir(configDir, { recursive: true }); | ||
| } catch (error) { | ||
| log("Error creating config directory:", error); | ||
| throw error; | ||
| } | ||
| } | ||
| function getConfigFilePath(serverUrlHash, filename) { | ||
| const configDir = getConfigDir(); | ||
| return path.join(configDir, `${serverUrlHash}_${filename}`); | ||
| } | ||
| async function deleteConfigFile(serverUrlHash, filename) { | ||
| try { | ||
| const filePath = getConfigFilePath(serverUrlHash, filename); | ||
| await fs.unlink(filePath); | ||
| } catch (error) { | ||
| if (error.code !== "ENOENT") { | ||
| log(`Error deleting ${filename}:`, error); | ||
| } | ||
| } | ||
| } | ||
| async function readJsonFile(serverUrlHash, filename, schema, clean = false) { | ||
| try { | ||
| await ensureConfigDir(); | ||
| if (clean) { | ||
| await deleteConfigFile(serverUrlHash, filename); | ||
| return void 0; | ||
| } | ||
| const filePath = getConfigFilePath(serverUrlHash, filename); | ||
| const content = await fs.readFile(filePath, "utf-8"); | ||
| const result = await schema.parseAsync(JSON.parse(content)); | ||
| return result; | ||
| } catch (error) { | ||
| if (error.code === "ENOENT") { | ||
| return void 0; | ||
| } | ||
| log(`Error reading ${filename}:`, error); | ||
| return void 0; | ||
| } | ||
| } | ||
| async function writeJsonFile(serverUrlHash, filename, data) { | ||
| try { | ||
| await ensureConfigDir(); | ||
| const filePath = getConfigFilePath(serverUrlHash, filename); | ||
| await fs.writeFile(filePath, JSON.stringify(data, null, 2), "utf-8"); | ||
| } catch (error) { | ||
| log(`Error writing ${filename}:`, error); | ||
| throw error; | ||
| } | ||
| } | ||
| async function readTextFile(serverUrlHash, filename, errorMessage, clean = false) { | ||
| try { | ||
| await ensureConfigDir(); | ||
| if (clean) { | ||
| await deleteConfigFile(serverUrlHash, filename); | ||
| throw new Error("File deleted due to clean flag"); | ||
| } | ||
| const filePath = getConfigFilePath(serverUrlHash, filename); | ||
| return await fs.readFile(filePath, "utf-8"); | ||
| } catch (error) { | ||
| throw new Error(errorMessage || `Error reading ${filename}`); | ||
| } | ||
| } | ||
| async function writeTextFile(serverUrlHash, filename, text) { | ||
| try { | ||
| await ensureConfigDir(); | ||
| const filePath = getConfigFilePath(serverUrlHash, filename); | ||
| await fs.writeFile(filePath, text, "utf-8"); | ||
| } catch (error) { | ||
| log(`Error writing ${filename}:`, error); | ||
| throw error; | ||
| } | ||
| } | ||
| // src/lib/coordination.ts | ||
| async function isPidRunning(pid2) { | ||
| try { | ||
| process.kill(pid2, 0); | ||
| return true; | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
| async function isLockValid(lockData) { | ||
| const MAX_LOCK_AGE = 30 * 60 * 1e3; | ||
| if (Date.now() - lockData.timestamp > MAX_LOCK_AGE) { | ||
| log("Lockfile is too old"); | ||
| return false; | ||
| } | ||
| if (!await isPidRunning(lockData.pid)) { | ||
| log("Process from lockfile is not running"); | ||
| return false; | ||
| } | ||
| try { | ||
| const controller = new AbortController(); | ||
| const timeout = setTimeout(() => controller.abort(), 1e3); | ||
| const response = await fetch( | ||
| `http://127.0.0.1:${lockData.port}/wait-for-auth?poll=false`, | ||
| { | ||
| signal: controller.signal | ||
| } | ||
| ); | ||
| clearTimeout(timeout); | ||
| return response.status === 200 || response.status === 202; | ||
| } catch (error) { | ||
| log(`Error connecting to auth server: ${error.message}`); | ||
| return false; | ||
| } | ||
| } | ||
| async function waitForAuthentication(port) { | ||
| log(`Waiting for authentication from the server on port ${port}...`); | ||
| try { | ||
| while (true) { | ||
| const url = `http://127.0.0.1:${port}/wait-for-auth`; | ||
| log(`Querying: ${url}`); | ||
| const response = await fetch(url); | ||
| if (response.status === 200) { | ||
| log("Authentication completed by other instance"); | ||
| return true; | ||
| } | ||
| if (response.status === 202) { | ||
| log("Authentication still in progress"); | ||
| await new Promise((resolve) => setTimeout(resolve, 1e3)); | ||
| } else { | ||
| log(`Unexpected response status: ${response.status}`); | ||
| return false; | ||
| } | ||
| } | ||
| } catch (error) { | ||
| log(`Error waiting for authentication: ${error.message}`); | ||
| return false; | ||
| } | ||
| } | ||
| async function coordinateAuth(serverUrlHash, callbackPort, events) { | ||
| const lockData = process.platform === "win32" ? null : await checkLockfile(serverUrlHash); | ||
| if (lockData && await isLockValid(lockData)) { | ||
| log(`Another instance is handling authentication on port ${lockData.port}`); | ||
| try { | ||
| const authCompleted = await waitForAuthentication(lockData.port); | ||
| if (authCompleted) { | ||
| log("Authentication completed by another instance"); | ||
| const dummyServer = express2().listen(0); | ||
| const dummyWaitForAuthCode = () => { | ||
| log( | ||
| "WARNING: waitForAuthCode called in secondary instance - this is unexpected" | ||
| ); | ||
| return new Promise(() => { | ||
| }); | ||
| }; | ||
| return { | ||
| server: dummyServer, | ||
| waitForAuthCode: dummyWaitForAuthCode, | ||
| skipBrowserAuth: true | ||
| }; | ||
| } | ||
| log("Taking over authentication process..."); | ||
| } catch (error) { | ||
| log(`Error waiting for authentication: ${error}`); | ||
| } | ||
| await deleteLockfile(serverUrlHash); | ||
| } else if (lockData) { | ||
| log("Found invalid lockfile, deleting it"); | ||
| await deleteLockfile(serverUrlHash); | ||
| } | ||
| const { server, waitForAuthCode, authCompletedPromise } = setupOAuthCallbackServerWithLongPoll({ | ||
| port: callbackPort, | ||
| path: "/oauth/callback", | ||
| events | ||
| }); | ||
| const address = server.address(); | ||
| const actualPort = address.port; | ||
| log( | ||
| `Creating lockfile for server ${serverUrlHash} with process ${process.pid} on port ${actualPort}` | ||
| ); | ||
| await createLockfile(serverUrlHash, process.pid, actualPort); | ||
| const cleanupHandler = async () => { | ||
| try { | ||
| log(`Cleaning up lockfile for server ${serverUrlHash}`); | ||
| await deleteLockfile(serverUrlHash); | ||
| } catch (error) { | ||
| log(`Error cleaning up lockfile: ${error}`); | ||
| } | ||
| }; | ||
| process.once("exit", () => { | ||
| try { | ||
| const configPath = getConfigFilePath(serverUrlHash, "lock.json"); | ||
| __require("node:fs").unlinkSync(configPath); | ||
| } catch { | ||
| } | ||
| }); | ||
| process.once("SIGINT", async () => { | ||
| await cleanupHandler(); | ||
| }); | ||
| return { | ||
| server, | ||
| waitForAuthCode, | ||
| skipBrowserAuth: false | ||
| }; | ||
| } | ||
| // src/lib/node-oath-client-provider.ts | ||
| import { | ||
| OAuthClientInformationSchema, | ||
| OAuthTokensSchema | ||
| } from "@modelcontextprotocol/sdk/shared/auth.js"; | ||
| import open from "open"; | ||
| var NodeOAuthClientProvider = class { | ||
| /** | ||
| * Creates a new NodeOAuthClientProvider | ||
| * @param options Configuration options for the provider | ||
| */ | ||
| constructor(options) { | ||
| this.options = options; | ||
| this.serverUrlHash = getServerUrlHash(options.serverUrl); | ||
| this.callbackPath = options.callbackPath || "/oauth/callback"; | ||
| this.clientName = options.clientName || "MCP CLI Client"; | ||
| this.clientUri = options.clientUri || "https://github.com/modelcontextprotocol/mcp-cli"; | ||
| if (options.clean) { | ||
| cleanServerConfig(this.serverUrlHash).catch((err) => { | ||
| log("Error cleaning server config:", err); | ||
| }); | ||
| } | ||
| } | ||
| serverUrlHash; | ||
| callbackPath; | ||
| clientName; | ||
| clientUri; | ||
| get redirectUrl() { | ||
| return `http://127.0.0.1:${this.options.callbackPort}${this.callbackPath}`; | ||
| } | ||
| get clientMetadata() { | ||
| return { | ||
| redirect_uris: [this.redirectUrl], | ||
| token_endpoint_auth_method: "none", | ||
| grant_types: ["authorization_code", "refresh_token"], | ||
| response_types: ["code"], | ||
| client_name: this.clientName, | ||
| client_uri: this.clientUri | ||
| }; | ||
| } | ||
| /** | ||
| * Gets the client information if it exists | ||
| * @returns The client information or undefined | ||
| */ | ||
| async clientInformation() { | ||
| return readJsonFile( | ||
| this.serverUrlHash, | ||
| "client_info.json", | ||
| OAuthClientInformationSchema, | ||
| this.options.clean | ||
| ); | ||
| } | ||
| /** | ||
| * Saves client information | ||
| * @param clientInformation The client information to save | ||
| */ | ||
| async saveClientInformation(clientInformation) { | ||
| await writeJsonFile(this.serverUrlHash, "client_info.json", clientInformation); | ||
| } | ||
| /** | ||
| * Gets the OAuth tokens if they exist | ||
| * @returns The OAuth tokens or undefined | ||
| */ | ||
| async tokens() { | ||
| return readJsonFile( | ||
| this.serverUrlHash, | ||
| "tokens.json", | ||
| OAuthTokensSchema, | ||
| this.options.clean | ||
| ); | ||
| } | ||
| /** | ||
| * Saves OAuth tokens | ||
| * @param tokens The tokens to save | ||
| */ | ||
| async saveTokens(tokens) { | ||
| await writeJsonFile(this.serverUrlHash, "tokens.json", tokens); | ||
| } | ||
| /** | ||
| * Redirects the user to the authorization URL | ||
| * @param authorizationUrl The URL to redirect to | ||
| */ | ||
| async redirectToAuthorization(authorizationUrl) { | ||
| log(` | ||
| Please authorize this client by visiting: | ||
| ${authorizationUrl.toString()} | ||
| `); | ||
| try { | ||
| await open(authorizationUrl.toString()); | ||
| log("Browser opened automatically."); | ||
| } catch (error) { | ||
| log( | ||
| "Could not open browser automatically. Please copy and paste the URL above into your browser." | ||
| ); | ||
| } | ||
| } | ||
| /** | ||
| * Saves the PKCE code verifier | ||
| * @param codeVerifier The code verifier to save | ||
| */ | ||
| async saveCodeVerifier(codeVerifier) { | ||
| await writeTextFile(this.serverUrlHash, "code_verifier.txt", codeVerifier); | ||
| } | ||
| /** | ||
| * Gets the PKCE code verifier | ||
| * @returns The code verifier | ||
| */ | ||
| async codeVerifier() { | ||
| return await readTextFile( | ||
| this.serverUrlHash, | ||
| "code_verifier.txt", | ||
| "No code verifier saved for session", | ||
| this.options.clean | ||
| ); | ||
| } | ||
| }; | ||
| export { | ||
| BearerTokenProvider, | ||
| MCP_REMOTE_VERSION, | ||
| log, | ||
| mcpProxy, | ||
| connectToRemoteServer, | ||
| parseCommandLineArgs, | ||
| setupSignalHandlers, | ||
| getServerUrlHash, | ||
| coordinateAuth, | ||
| NodeOAuthClientProvider | ||
| }; |
| #!/usr/bin/env node |
-141
| #!/usr/bin/env node | ||
| import { | ||
| BearerTokenProvider, | ||
| MCP_REMOTE_VERSION, | ||
| NodeOAuthClientProvider, | ||
| coordinateAuth, | ||
| getServerUrlHash, | ||
| log, | ||
| parseCommandLineArgs, | ||
| setupSignalHandlers | ||
| } from "./chunk-2SYX4XUF.js"; | ||
| // src/client.ts | ||
| import { EventEmitter } from "node:events"; | ||
| import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js"; | ||
| import { Client } from "@modelcontextprotocol/sdk/client/index.js"; | ||
| import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; | ||
| import { | ||
| ListResourcesResultSchema, | ||
| ListToolsResultSchema | ||
| } from "@modelcontextprotocol/sdk/types.js"; | ||
| import express from "express"; | ||
| async function runClient(serverUrl, callbackPort, clean = false, bearerToken) { | ||
| const events = new EventEmitter(); | ||
| const authProvider = bearerToken ? new BearerTokenProvider({ serverUrl, bearerToken }) : new NodeOAuthClientProvider({ | ||
| serverUrl, | ||
| callbackPort, | ||
| clientName: "MCP CLI Client", | ||
| clean | ||
| }); | ||
| const { server, waitForAuthCode, skipBrowserAuth } = bearerToken ? { | ||
| server: express().listen(0), | ||
| // Use imported express instead of require | ||
| waitForAuthCode: () => Promise.resolve(""), | ||
| // Dummy function | ||
| skipBrowserAuth: true | ||
| } : await coordinateAuth(getServerUrlHash(serverUrl), callbackPort, events); | ||
| if (skipBrowserAuth && !bearerToken) { | ||
| log( | ||
| "Authentication was completed by another instance - will use tokens from disk..." | ||
| ); | ||
| await new Promise((res) => setTimeout(res, 1e3)); | ||
| } | ||
| const client = new Client( | ||
| { | ||
| name: "mcp-remote", | ||
| version: MCP_REMOTE_VERSION | ||
| }, | ||
| { | ||
| capabilities: {} | ||
| } | ||
| ); | ||
| const url = new URL(serverUrl); | ||
| function initTransport() { | ||
| const transport2 = new SSEClientTransport(url, { authProvider }); | ||
| transport2.onmessage = (message) => { | ||
| log("Received message:", JSON.stringify(message, null, 2)); | ||
| }; | ||
| transport2.onerror = (error) => { | ||
| log("Transport error:", error); | ||
| }; | ||
| transport2.onclose = () => { | ||
| log("Connection closed."); | ||
| process.exit(0); | ||
| }; | ||
| return transport2; | ||
| } | ||
| const transport = initTransport(); | ||
| const cleanup = async () => { | ||
| log("\nClosing connection..."); | ||
| await client.close(); | ||
| server.close(); | ||
| }; | ||
| setupSignalHandlers(cleanup); | ||
| try { | ||
| log("Connecting to server..."); | ||
| await client.connect(transport); | ||
| log("Connected successfully!"); | ||
| } catch (error) { | ||
| if (error instanceof UnauthorizedError || error instanceof Error && error.message.includes("Unauthorized")) { | ||
| log("Authentication required. Waiting for authorization..."); | ||
| const code = await waitForAuthCode(); | ||
| try { | ||
| log("Completing authorization..."); | ||
| await transport.finishAuth(code); | ||
| log("Connecting after authorization..."); | ||
| await client.connect(initTransport()); | ||
| log("Connected successfully!"); | ||
| log("Requesting tools list..."); | ||
| const tools = await client.request( | ||
| { method: "tools/list" }, | ||
| ListToolsResultSchema | ||
| ); | ||
| log("Tools:", JSON.stringify(tools, null, 2)); | ||
| log("Requesting resource list..."); | ||
| const resources = await client.request( | ||
| { method: "resources/list" }, | ||
| ListResourcesResultSchema | ||
| ); | ||
| log("Resources:", JSON.stringify(resources, null, 2)); | ||
| log("Listening for messages. Press Ctrl+C to exit."); | ||
| } catch (authError) { | ||
| log("Authorization error:", authError); | ||
| server.close(); | ||
| process.exit(1); | ||
| } | ||
| } else { | ||
| log("Connection error:", error); | ||
| server.close(); | ||
| process.exit(1); | ||
| } | ||
| } | ||
| try { | ||
| log("Requesting tools list..."); | ||
| const tools = await client.request({ method: "tools/list" }, ListToolsResultSchema); | ||
| log("Tools:", JSON.stringify(tools, null, 2)); | ||
| } catch (e) { | ||
| log("Error requesting tools list:", e); | ||
| } | ||
| try { | ||
| log("Requesting resource list..."); | ||
| const resources = await client.request( | ||
| { method: "resources/list" }, | ||
| ListResourcesResultSchema | ||
| ); | ||
| log("Resources:", JSON.stringify(resources, null, 2)); | ||
| } catch (e) { | ||
| log("Error requesting resources list:", e); | ||
| } | ||
| log("Listening for messages. Press Ctrl+C to exit."); | ||
| } | ||
| parseCommandLineArgs( | ||
| process.argv.slice(2), | ||
| 3333, | ||
| "Usage: npx tsx client.ts [--clean] [--bearer <token>] <https://server-url> [callback-port]" | ||
| ).then(({ serverUrl, callbackPort, clean, bearerToken }) => { | ||
| return runClient(serverUrl, callbackPort, clean, bearerToken); | ||
| }).catch((error) => { | ||
| console.error("Fatal error:", error); | ||
| process.exit(1); | ||
| }); |
| #!/usr/bin/env node |
| #!/usr/bin/env node | ||
| import { | ||
| BearerTokenProvider, | ||
| NodeOAuthClientProvider, | ||
| connectToRemoteServer, | ||
| coordinateAuth, | ||
| getServerUrlHash, | ||
| log, | ||
| mcpProxy, | ||
| parseCommandLineArgs, | ||
| setupSignalHandlers | ||
| } from "./chunk-2SYX4XUF.js"; | ||
| // src/proxy.ts | ||
| import { EventEmitter } from "node:events"; | ||
| import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; | ||
| import express from "express"; | ||
| async function runProxy(serverUrl, callbackPort, clean = false, bearerToken) { | ||
| const events = new EventEmitter(); | ||
| const authProvider = bearerToken ? new BearerTokenProvider({ serverUrl, bearerToken }) : new NodeOAuthClientProvider({ | ||
| serverUrl, | ||
| callbackPort, | ||
| clientName: "MCP CLI Proxy", | ||
| clean | ||
| }); | ||
| const { server, waitForAuthCode, skipBrowserAuth } = bearerToken ? { | ||
| server: express().listen(0), | ||
| // Use imported express instead of require | ||
| waitForAuthCode: () => Promise.resolve(""), | ||
| // Dummy function | ||
| skipBrowserAuth: true | ||
| } : await coordinateAuth(getServerUrlHash(serverUrl), callbackPort, events); | ||
| if (skipBrowserAuth && !bearerToken) { | ||
| log("Authentication was completed by another instance - will use tokens from disk"); | ||
| await new Promise((res) => setTimeout(res, 1e3)); | ||
| } | ||
| const localTransport = new StdioServerTransport(); | ||
| try { | ||
| const remoteTransport = await connectToRemoteServer( | ||
| serverUrl, | ||
| authProvider, | ||
| waitForAuthCode, | ||
| skipBrowserAuth | ||
| ); | ||
| mcpProxy({ | ||
| transportToClient: localTransport, | ||
| transportToServer: remoteTransport | ||
| }); | ||
| await localTransport.start(); | ||
| log("Local STDIO server running"); | ||
| log("Proxy established successfully between local STDIO and remote SSE"); | ||
| log("Press Ctrl+C to exit"); | ||
| const cleanup = async () => { | ||
| await remoteTransport.close(); | ||
| await localTransport.close(); | ||
| server.close(); | ||
| }; | ||
| setupSignalHandlers(cleanup); | ||
| } catch (error) { | ||
| log("Fatal error:", error); | ||
| if (error instanceof Error && error.message.includes("self-signed certificate in certificate chain")) { | ||
| log(`You may be behind a VPN! | ||
| If you are behind a VPN, you can try setting the NODE_EXTRA_CA_CERTS environment variable to point | ||
| to the CA certificate file. If using claude_desktop_config.json, this might look like: | ||
| { | ||
| "mcpServers": { | ||
| "\${mcpServerName}": { | ||
| "command": "npx", | ||
| "args": [ | ||
| "mcp-remote", | ||
| "https://remote.mcp.server/sse" | ||
| ], | ||
| "env": { | ||
| "NODE_EXTRA_CA_CERTS": "\${your CA certificate file path}.pem" | ||
| } | ||
| } | ||
| } | ||
| } | ||
| `); | ||
| } | ||
| server.close(); | ||
| process.exit(1); | ||
| } | ||
| } | ||
| parseCommandLineArgs( | ||
| process.argv.slice(2), | ||
| 3334, | ||
| "Usage: npx tsx proxy.ts [--clean] [--bearer <token>] <https://server-url> [callback-port]" | ||
| ).then(({ serverUrl, callbackPort, clean, bearerToken }) => { | ||
| return runProxy(serverUrl, callbackPort, clean, bearerToken); | ||
| }).catch((error) => { | ||
| log("Fatal error:", error); | ||
| process.exit(1); | ||
| }); |
Empty package
Supply chain riskPackage does not contain any code. It may be removed, is name squatting, or the result of a faulty package publish.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
0
-100%2
-33.33%1330
-96.37%2
-71.43%0
-100%5
-97.62%