@stackone/mcp-remote
Advanced tools
| 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); | ||
| }); |
+2
-2
| { | ||
| "name": "@stackone/mcp-remote", | ||
| "version": "0.0.1", | ||
| "version": "0.0.4", | ||
| "type": "module", | ||
@@ -56,3 +56,3 @@ "main": "./dist/index.js", | ||
| "typecheck": "tsc --noEmit", | ||
| "publish-release": "pnpm publish --no-git-checks --access public", | ||
| "publish-release": "pnpm publish --access public", | ||
| "client": "tsx src/client.ts", | ||
@@ -59,0 +59,0 @@ "proxy": "tsx src/proxy.ts" |
+6
-6
@@ -30,3 +30,3 @@ # MCP Remote | ||
| "command": "npx", | ||
| "args": ["mcp-remote", "https://remote.mcp.server/sse"] | ||
| "args": ["@stackone/mcp-remote", "https://remote.mcp.server/sse"] | ||
| } | ||
@@ -52,3 +52,3 @@ } | ||
| "args": [ | ||
| "mcp-remote", | ||
| "@stackone/mcp-remote", | ||
| "--bearer", | ||
@@ -71,3 +71,3 @@ "your-bearer-token", | ||
| "-y", | ||
| "mcp-remote", | ||
| "@stackone/mcp-remote", | ||
| "https://remote.mcp.server/sse" | ||
@@ -90,3 +90,3 @@ ] | ||
| "args": [ | ||
| "mcp-remote", | ||
| "@stackone/mcp-remote", | ||
| "https://remote.mcp.server/sse", | ||
@@ -101,3 +101,3 @@ "--clean" | ||
| "args": [ | ||
| "mcp-remote", | ||
| "@stackone/mcp-remote", | ||
| "https://remote.mcp.server/sse", | ||
@@ -167,3 +167,3 @@ "9696" | ||
| "command": "npx", | ||
| "args": ["mcp-remote", "https://remote.mcp.server/sse"], | ||
| "args": ["@stackone/mcp-remote", "https://remote.mcp.server/sse"], | ||
| "env": { | ||
@@ -170,0 +170,0 @@ "NODE_EXTRA_CA_CERTS": "{your CA certificate file path}.pem" |
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
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
36642
316.95%7
250%897
Infinity%2
Infinity%3
200%