From c1fe647a4899e6c169bc76128bbe293cd37bad68 Mon Sep 17 00:00:00 2001 From: stevehuynh Date: Wed, 14 May 2025 20:40:43 +1000 Subject: [PATCH 1/3] feat: short timeout has now been added for clients like cursor --- src/lib/utils.ts | 56 ++++++++++++++++++++++++++++++++++++++++++++---- src/proxy.ts | 7 +++--- 2 files changed, 56 insertions(+), 7 deletions(-) diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 572550c..b11c9b3 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -3,10 +3,14 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js' import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js' import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js' import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js' +import fs from 'fs' +import path from 'path' +import os from 'os' // Connection constants export const REASON_AUTH_NEEDED = 'authentication-needed' export const REASON_TRANSPORT_FALLBACK = 'falling-back-to-alternate-transport' +export const SHORT_TIMEOUT_DURATION = 50000 // Transport strategy types export type TransportStrategy = 'sse-only' | 'http-only' | 'sse-first' | 'http-first' @@ -24,6 +28,27 @@ export function log(str: string, ...rest: unknown[]) { console.error(`[${pid}] ${str}`, ...rest) } +/** + * Clears MCP auth files after short timeout + * Used for clients like Cursor or Claude Desktop with short timeouts + */ +function clearMcpAuthFiles() { + try { + const mcpDir = path.join(os.homedir(), '.mcp-auth') + log('Short timeout reached, clearing MCP auth files') + // Check if the directory exists + if (fs.existsSync(mcpDir)) { + // Delete the entire directory and its contents + fs.rmSync(mcpDir, { recursive: true, force: true }) + log('MCP auth directory cleared successfully') + } else { + log('No MCP directory found, nothing to clear') + } + } catch (error) { + log('Error clearing MCP auth files:', error) + } +} + /** * Creates a bidirectional proxy between two transports * @param params The transport connections to proxy between @@ -97,6 +122,7 @@ export type AuthInitializer = () => Promise<{ * @param authInitializer Function to initialize authentication when needed * @param transportStrategy Strategy for selecting transport type ('sse-only', 'http-only', 'sse-first', 'http-first') * @param recursionReasons Set of reasons for recursive calls (internal use) + * @param shortTimeout Whether to use a short timeout (for clients like Cursor or Claude Desktop) * @returns The connected transport */ export async function connectToRemoteServer( @@ -106,7 +132,8 @@ export async function connectToRemoteServer( headers: Record, authInitializer: AuthInitializer, transportStrategy: TransportStrategy = 'http-first', - recursionReasons: Set = new Set(), + shortTimeout: boolean = false, + recursionReasons: Set = new Set() ): Promise { log(`[${pid}] Connecting to remote server: ${serverUrl}`) const url = new URL(serverUrl) @@ -196,7 +223,8 @@ export async function connectToRemoteServer( headers, authInitializer, sseTransport ? 'http-only' : 'sse-only', - recursionReasons, + shortTimeout, + recursionReasons ) } else if (error instanceof UnauthorizedError || (error instanceof Error && error.message.includes('Unauthorized'))) { log('Authentication required. Initializing auth...') @@ -204,6 +232,15 @@ export async function connectToRemoteServer( // Initialize authentication on-demand const { waitForAuthCode, skipBrowserAuth } = await authInitializer() + // Set up short timeout if enabled + let shortTimeoutTimer: NodeJS.Timeout | null = null + if (shortTimeout) { + log(`Short timeout enabled, will clear auth files after ${SHORT_TIMEOUT_DURATION / 1000} seconds`) + shortTimeoutTimer = setTimeout(() => { + clearMcpAuthFiles() + }, SHORT_TIMEOUT_DURATION) + } + if (skipBrowserAuth) { log('Authentication required but skipping browser auth - using shared auth') } else { @@ -214,6 +251,11 @@ export async function connectToRemoteServer( const code = await waitForAuthCode() try { + // Clear the timeout if auth completes successfully + if (shortTimeoutTimer) { + clearTimeout(shortTimeoutTimer) + } + log('Completing authorization...') await transport.finishAuth(code) @@ -228,8 +270,13 @@ export async function connectToRemoteServer( log(`Recursively reconnecting for reason: ${REASON_AUTH_NEEDED}`) // Recursively call connectToRemoteServer with the updated recursion tracking - return connectToRemoteServer(client, serverUrl, authProvider, headers, authInitializer, transportStrategy, recursionReasons) + return connectToRemoteServer(client, serverUrl, authProvider, headers, authInitializer, transportStrategy, shortTimeout) } catch (authError) { + // Clear the timeout if auth fails + if (shortTimeoutTimer) { + clearTimeout(shortTimeoutTimer) + } + log('Authorization error:', authError) throw authError } @@ -412,6 +459,7 @@ export async function parseCommandLineArgs(args: string[], defaultPort: number, const serverUrl = args[0] const specifiedPort = args[1] ? parseInt(args[1]) : undefined const allowHttp = args.includes('--allow-http') + const shortTimeout = args.includes('--short-timeout') // Parse transport strategy let transportStrategy: TransportStrategy = 'http-first' // Default @@ -468,7 +516,7 @@ export async function parseCommandLineArgs(args: string[], defaultPort: number, }) } - return { serverUrl, callbackPort, headers, transportStrategy } + return { serverUrl, callbackPort, headers, transportStrategy, shortTimeout } } /** diff --git a/src/proxy.ts b/src/proxy.ts index 7263a95..58e29f3 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -32,6 +32,7 @@ async function runProxy( callbackPort: number, headers: Record, transportStrategy: TransportStrategy = 'http-first', + shortTimeout: boolean = false, ) { // Set up event emitter for auth flow const events = new EventEmitter() @@ -78,7 +79,7 @@ async function runProxy( try { // Connect to remote server with lazy authentication - const remoteTransport = await connectToRemoteServer(null, serverUrl, authProvider, headers, authInitializer, transportStrategy) + const remoteTransport = await connectToRemoteServer(null, serverUrl, authProvider, headers, authInitializer, transportStrategy, shortTimeout) // Set up bidirectional proxy between local and remote transports mcpProxy({ @@ -136,8 +137,8 @@ to the CA certificate file. If using claude_desktop_config.json, this might look // Parse command-line arguments and run the proxy parseCommandLineArgs(process.argv.slice(2), 3334, 'Usage: npx tsx proxy.ts [callback-port]') - .then(({ serverUrl, callbackPort, headers, transportStrategy }) => { - return runProxy(serverUrl, callbackPort, headers, transportStrategy) + .then(({ serverUrl, callbackPort, headers, transportStrategy, shortTimeout }) => { + return runProxy(serverUrl, callbackPort, headers, transportStrategy, shortTimeout) }) .catch((error) => { log('Fatal error:', error) From 437c47680a77928cfe0e22d5810f45932e2c9c30 Mon Sep 17 00:00:00 2001 From: stevehuynh Date: Wed, 14 May 2025 20:57:14 +1000 Subject: [PATCH 2/3] chore: remove whitespace --- src/lib/utils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 7727f68..c580769 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -8,7 +8,6 @@ import path from 'path' import os from 'os' import { OAuthClientInformationFull, OAuthClientInformationFullSchema } from '@modelcontextprotocol/sdk/shared/auth.js' - // Connection constants export const REASON_AUTH_NEEDED = 'authentication-needed' export const REASON_TRANSPORT_FALLBACK = 'falling-back-to-alternate-transport' From ea80a7c663c638a837d58c4d29d84bb5d01d9447 Mon Sep 17 00:00:00 2001 From: stevehuynh Date: Wed, 14 May 2025 21:48:05 +1000 Subject: [PATCH 3/3] fix: now deletes only the current version of MCP-remote --- src/lib/utils.ts | 22 ++++++++++++---------- src/proxy.ts | 2 +- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 7a36bda..d0d5be2 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -3,7 +3,7 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js' import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js' import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js' import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js' -import fs from 'fs' +import * as fsSync from 'fs' import path from 'path' import os from 'os' import { OAuthClientInformationFull, OAuthClientInformationFullSchema } from '@modelcontextprotocol/sdk/shared/auth.js' @@ -12,7 +12,7 @@ import { getConfigFilePath, readJsonFile } from './mcp-auth-config' import express from 'express' import net from 'net' import crypto from 'crypto' -import fs from 'fs/promises' +import * as fs from 'fs/promises' // Connection constants export const REASON_AUTH_NEEDED = 'authentication-needed' @@ -37,15 +37,17 @@ export function log(str: string, ...rest: unknown[]) { */ function clearMcpAuthFiles() { try { - const mcpDir = path.join(os.homedir(), '.mcp-auth') - log('Short timeout reached, clearing MCP auth files') - // Check if the directory exists - if (fs.existsSync(mcpDir)) { - // Delete the entire directory and its contents - fs.rmSync(mcpDir, { recursive: true, force: true }) - log('MCP auth directory cleared successfully') + const baseConfigDir = process.env.MCP_REMOTE_CONFIG_DIR || path.join(os.homedir(), '.mcp-auth') + const versionDir = path.join(baseConfigDir, `mcp-remote-${MCP_REMOTE_VERSION}`) + + log('Short timeout reached, clearing MCP auth files for current version') + // Check if the version directory exists + if (fsSync.existsSync(versionDir)) { + // Delete only the current version directory and its contents + fsSync.rmSync(versionDir, { recursive: true, force: true }) + log(`MCP auth directory for version ${MCP_REMOTE_VERSION} cleared successfully`) } else { - log('No MCP directory found, nothing to clear') + log(`No MCP directory found for version ${MCP_REMOTE_VERSION}, nothing to clear`) } } catch (error) { log('Error clearing MCP auth files:', error) diff --git a/src/proxy.ts b/src/proxy.ts index 58e29f3..d087eb7 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -136,7 +136,7 @@ to the CA certificate file. If using claude_desktop_config.json, this might look } // Parse command-line arguments and run the proxy -parseCommandLineArgs(process.argv.slice(2), 3334, 'Usage: npx tsx proxy.ts [callback-port]') +parseCommandLineArgs(process.argv.slice(2), 'Usage: npx tsx proxy.ts [callback-port]') .then(({ serverUrl, callbackPort, headers, transportStrategy, shortTimeout }) => { return runProxy(serverUrl, callbackPort, headers, transportStrategy, shortTimeout) })