#!/usr/bin/env node /// /** * MCP Proxy with OAuth support * A bidirectional proxy between a local STDIO MCP server and a remote SSE server with OAuth authentication. * * Run with: deno run --allow-net --allow-env --allow-read --allow-run src/proxy.ts https://example.remote/server [callback-port] * * If callback-port is not specified, an available port will be automatically selected. */ import { EventEmitter } from "node:events"; import { StdioServerTransport } from "npm:@modelcontextprotocol/sdk/server/stdio.js"; import { connectToRemoteServer, getServerUrlHash, log, mcpProxy, parseCommandLineArgs, setupSignalHandlers, } from "./lib/utils.ts"; import { NodeOAuthClientProvider } from "./lib/node-oauth-client-provider.ts"; import { coordinateAuth } from "./lib/coordination.ts"; /** * Main function to run the proxy */ async function runProxy( serverUrl: string, callbackPort: number, headers: Record, ) { // Set up event emitter for auth flow const events = new EventEmitter(); // Get the server URL hash for lockfile operations const serverUrlHash = getServerUrlHash(serverUrl); // Coordinate authentication with other instances const { server, waitForAuthCode, skipBrowserAuth } = await coordinateAuth( serverUrlHash, callbackPort, events, ); // Create the OAuth client provider const authProvider = new NodeOAuthClientProvider({ serverUrl, callbackPort, clientName: "MCP CLI Proxy", }); // If auth was completed by another instance, just log that we'll use the auth from disk if (skipBrowserAuth) { log( "Authentication was completed by another instance - will use tokens from disk", ); // TODO: remove, the callback is happening before the tokens are exchanged // so we're slightly too early await new Promise((res) => setTimeout(res, 1_000)); } // Create the STDIO transport for local connections const localTransport = new StdioServerTransport(); try { // Connect to remote server with authentication const remoteTransport = await connectToRemoteServer( serverUrl, authProvider, headers, waitForAuthCode, skipBrowserAuth, ); // Set up bidirectional proxy between local and remote transports mcpProxy({ transportToClient: localTransport, transportToServer: remoteTransport, }); // Start the local STDIO server await localTransport.start(); log("Local STDIO server running"); log("Proxy established successfully between local STDIO and remote SSE"); log("Press Ctrl+C to exit"); // Setup cleanup handler 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(); Deno.exit(1); } } // Parse command-line arguments and run the proxy parseCommandLineArgs( Deno.args, 3334, "Usage: deno run src/proxy.ts [callback-port]", ) .then(({ serverUrl, callbackPort, headers }) => { return runProxy(serverUrl, callbackPort, headers); }) .catch((error) => { log("Fatal error:", error); Deno.exit(1); });