diff --git a/package.json b/package.json index 11022f6..6f914d7 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "open": "^10.1.0" }, "devDependencies": { - "@modelcontextprotocol/sdk": "^1.12.1", + "@modelcontextprotocol/sdk": "https://pkg.pr.new/geelen/typescript-sdk/@modelcontextprotocol/sdk@cdf3508", "@types/express": "^5.0.0", "@types/node": "^22.13.10", "prettier": "^3.5.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d66fc53..76f511d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,8 +16,8 @@ importers: version: 10.1.0 devDependencies: '@modelcontextprotocol/sdk': - specifier: ^1.12.1 - version: 1.12.1 + specifier: https://pkg.pr.new/geelen/typescript-sdk/@modelcontextprotocol/sdk@cdf3508 + version: https://pkg.pr.new/geelen/typescript-sdk/@modelcontextprotocol/sdk@cdf3508 '@types/express': specifier: ^5.0.0 version: 5.0.0 @@ -211,8 +211,9 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} - '@modelcontextprotocol/sdk@1.12.1': - resolution: {integrity: sha512-KG1CZhZfWg+u8pxeM/mByJDScJSrjjxLc8fwQqbsS8xCjBmQfMNEBTotYdNanKekepnfRI85GtgQlctLFpcYPw==} + '@modelcontextprotocol/sdk@https://pkg.pr.new/geelen/typescript-sdk/@modelcontextprotocol/sdk@cdf3508': + resolution: {tarball: https://pkg.pr.new/geelen/typescript-sdk/@modelcontextprotocol/sdk@cdf3508} + version: 1.12.2 engines: {node: '>=18'} '@pkgjs/parseargs@0.11.0': @@ -1221,7 +1222,7 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 - '@modelcontextprotocol/sdk@1.12.1': + '@modelcontextprotocol/sdk@https://pkg.pr.new/geelen/typescript-sdk/@modelcontextprotocol/sdk@cdf3508': dependencies: ajv: 6.12.6 content-type: 1.0.5 diff --git a/src/lib/coordination.ts b/src/lib/coordination.ts index 00a3808..8b729f8 100644 --- a/src/lib/coordination.ts +++ b/src/lib/coordination.ts @@ -18,10 +18,10 @@ export type AuthCoordinator = { export async function isPidRunning(pid: number): Promise { try { process.kill(pid, 0) // Doesn't kill the process, just checks if it exists - if (DEBUG) await debugLog(global.currentServerUrlHash!, `Process ${pid} is running`) + if (DEBUG) debugLog(`Process ${pid} is running`) return true } catch (err) { - if (DEBUG) await debugLog(global.currentServerUrlHash!, `Process ${pid} is not running`, err) + if (DEBUG) debugLog(`Process ${pid} is not running`, err) return false } } @@ -32,14 +32,14 @@ export async function isPidRunning(pid: number): Promise { * @returns True if the lockfile is valid, false otherwise */ export async function isLockValid(lockData: LockfileData): Promise { - if (DEBUG) await debugLog(global.currentServerUrlHash!, 'Checking if lockfile is valid', lockData) + if (DEBUG) debugLog('Checking if lockfile is valid', lockData) // Check if the lockfile is too old (over 30 minutes) const MAX_LOCK_AGE = 30 * 60 * 1000 // 30 minutes if (Date.now() - lockData.timestamp > MAX_LOCK_AGE) { log('Lockfile is too old') if (DEBUG) - await debugLog(global.currentServerUrlHash!, 'Lockfile is too old', { + debugLog('Lockfile is too old', { age: Date.now() - lockData.timestamp, maxAge: MAX_LOCK_AGE, }) @@ -49,13 +49,13 @@ export async function isLockValid(lockData: LockfileData): Promise { // Check if the process is still running if (!(await isPidRunning(lockData.pid))) { log('Process from lockfile is not running') - if (DEBUG) await debugLog(global.currentServerUrlHash!, 'Process from lockfile is not running', { pid: lockData.pid }) + if (DEBUG) debugLog('Process from lockfile is not running', { pid: lockData.pid }) return false } // Check if the endpoint is accessible try { - if (DEBUG) await debugLog(global.currentServerUrlHash!, 'Checking if endpoint is accessible', { port: lockData.port }) + if (DEBUG) debugLog('Checking if endpoint is accessible', { port: lockData.port }) const controller = new AbortController() const timeout = setTimeout(() => controller.abort(), 1000) @@ -67,12 +67,11 @@ export async function isLockValid(lockData: LockfileData): Promise { clearTimeout(timeout) const isValid = response.status === 200 || response.status === 202 - if (DEBUG) - await debugLog(global.currentServerUrlHash!, `Endpoint check result: ${isValid ? 'valid' : 'invalid'}`, { status: response.status }) + if (DEBUG) debugLog(`Endpoint check result: ${isValid ? 'valid' : 'invalid'}`, { status: response.status }) return isValid } catch (error) { log(`Error connecting to auth server: ${(error as Error).message}`) - if (DEBUG) await debugLog(global.currentServerUrlHash!, 'Error connecting to auth server', error) + if (DEBUG) debugLog('Error connecting to auth server', error) return false } } @@ -84,7 +83,6 @@ export async function isLockValid(lockData: LockfileData): Promise { */ export async function waitForAuthentication(port: number): Promise { log(`Waiting for authentication from the server on port ${port}...`) - if (DEBUG) await debugLog(global.currentServerUrlHash!, `Waiting for authentication from server on port ${port}`) try { let attempts = 0 @@ -92,36 +90,34 @@ export async function waitForAuthentication(port: number): Promise { attempts++ const url = `http://127.0.0.1:${port}/wait-for-auth` log(`Querying: ${url}`) - if (DEBUG) await debugLog(global.currentServerUrlHash!, `Poll attempt ${attempts}: ${url}`) + if (DEBUG) debugLog(`Poll attempt ${attempts}`) try { const response = await fetch(url) - if (DEBUG) await debugLog(global.currentServerUrlHash!, `Poll response status: ${response.status}`) + if (DEBUG) debugLog(`Poll response status: ${response.status}`) if (response.status === 200) { // Auth completed, but we don't return the code anymore log(`Authentication completed by other instance`) - if (DEBUG) await debugLog(global.currentServerUrlHash!, `Authentication completed by other instance`) return true } else if (response.status === 202) { // Continue polling log(`Authentication still in progress`) - if (DEBUG) await debugLog(global.currentServerUrlHash!, `Authentication still in progress, will retry in 1s`) + if (DEBUG) debugLog(`Will retry in 1s`) await new Promise((resolve) => setTimeout(resolve, 1000)) } else { log(`Unexpected response status: ${response.status}`) - if (DEBUG) await debugLog(global.currentServerUrlHash!, `Unexpected response status`, { status: response.status }) return false } } catch (fetchError) { - if (DEBUG) await debugLog(global.currentServerUrlHash!, `Fetch error during poll`, fetchError) + if (DEBUG) debugLog(`Fetch error during poll`, fetchError) // If we can't connect, we'll try again after a delay await new Promise((resolve) => setTimeout(resolve, 2000)) } } } catch (error) { log(`Error waiting for authentication: ${(error as Error).message}`) - if (DEBUG) await debugLog(global.currentServerUrlHash!, `Error waiting for authentication`, error) + if (DEBUG) debugLog(`Error waiting for authentication`, error) return false } } @@ -140,16 +136,16 @@ export function createLazyAuthCoordinator(serverUrlHash: string, callbackPort: n initializeAuth: async () => { // If auth has already been initialized, return the existing state if (authState) { - if (DEBUG) await debugLog(serverUrlHash, 'Auth already initialized, reusing existing state') + if (DEBUG) debugLog('Auth already initialized, reusing existing state') return authState } log('Initializing auth coordination on-demand') - if (DEBUG) await debugLog(serverUrlHash, 'Initializing auth coordination on-demand', { serverUrlHash, callbackPort }) + if (DEBUG) debugLog('Initializing auth coordination on-demand', { serverUrlHash, callbackPort }) // Initialize auth using the existing coordinateAuth logic authState = await coordinateAuth(serverUrlHash, callbackPort, events) - if (DEBUG) await debugLog(serverUrlHash, 'Auth coordination completed', { skipBrowserAuth: authState.skipBrowserAuth }) + if (DEBUG) debugLog('Auth coordination completed', { skipBrowserAuth: authState.skipBrowserAuth }) return authState }, } @@ -167,42 +163,39 @@ export async function coordinateAuth( callbackPort: number, events: EventEmitter, ): Promise<{ server: Server; waitForAuthCode: () => Promise; skipBrowserAuth: boolean }> { - if (DEBUG) await debugLog(serverUrlHash, 'Coordinating authentication', { serverUrlHash, callbackPort }) + if (DEBUG) debugLog('Coordinating authentication', { serverUrlHash, callbackPort }) // Check for a lockfile (disabled on Windows for the time being) const lockData = process.platform === 'win32' ? null : await checkLockfile(serverUrlHash) if (DEBUG) { if (process.platform === 'win32') { - await debugLog(serverUrlHash, 'Skipping lockfile check on Windows') + debugLog('Skipping lockfile check on Windows') } else { - await debugLog(serverUrlHash, 'Lockfile check result', { found: !!lockData, lockData }) + debugLog('Lockfile check result', { found: !!lockData, lockData }) } } // If there's a valid lockfile, try to use the existing auth process if (lockData && (await isLockValid(lockData))) { - log(`Another instance is handling authentication on port ${lockData.port}`) - if (DEBUG) await debugLog(serverUrlHash, 'Another instance is handling authentication', { port: lockData.port, pid: lockData.pid }) + log(`Another instance is handling authentication on port ${lockData.port} (pid: ${lockData.pid})`) try { // Try to wait for the authentication to complete - if (DEBUG) await debugLog(serverUrlHash, 'Waiting for authentication from other instance') + if (DEBUG) debugLog('Waiting for authentication from other instance') const authCompleted = await waitForAuthentication(lockData.port) if (authCompleted) { - log('Authentication completed by another instance') - if (DEBUG) await debugLog(serverUrlHash, 'Authentication completed by another instance, will use tokens from disk') + log('Authentication completed by another instance. Using tokens from disk') // Setup a dummy server - the client will use tokens directly from disk const dummyServer = express().listen(0) // Listen on any available port const dummyPort = (dummyServer.address() as AddressInfo).port - if (DEBUG) await debugLog(serverUrlHash, 'Started dummy server', { port: dummyPort }) + if (DEBUG) debugLog('Started dummy server', { port: dummyPort }) // This shouldn't actually be called in normal operation, but provide it for API compatibility const dummyWaitForAuthCode = () => { log('WARNING: waitForAuthCode called in secondary instance - this is unexpected') - if (DEBUG) debugLog(serverUrlHash, 'WARNING: waitForAuthCode called in secondary instance - this is unexpected').catch(() => {}) // Return a promise that never resolves - the client should use the tokens from disk instead return new Promise(() => {}) } @@ -214,25 +207,23 @@ export async function coordinateAuth( } } else { log('Taking over authentication process...') - if (DEBUG) await debugLog(serverUrlHash, 'Taking over authentication process') } } catch (error) { log(`Error waiting for authentication: ${error}`) - if (DEBUG) await debugLog(serverUrlHash, 'Error waiting for authentication', error) + if (DEBUG) debugLog('Error waiting for authentication', error) } // If we get here, the other process didn't complete auth successfully - if (DEBUG) await debugLog(serverUrlHash, 'Other instance did not complete auth successfully, deleting lockfile') + if (DEBUG) debugLog('Other instance did not complete auth successfully, deleting lockfile') await deleteLockfile(serverUrlHash) } else if (lockData) { // Invalid lockfile, delete it log('Found invalid lockfile, deleting it') - if (DEBUG) await debugLog(serverUrlHash, 'Found invalid lockfile, deleting it') await deleteLockfile(serverUrlHash) } // Create our own lockfile - if (DEBUG) await debugLog(serverUrlHash, 'Setting up OAuth callback server', { port: callbackPort }) + if (DEBUG) debugLog('Setting up OAuth callback server', { port: callbackPort }) const { server, waitForAuthCode, authCompletedPromise } = setupOAuthCallbackServerWithLongPoll({ port: callbackPort, path: '/oauth/callback', @@ -242,21 +233,19 @@ export async function coordinateAuth( // Get the actual port the server is running on const address = server.address() as AddressInfo const actualPort = address.port - if (DEBUG) await debugLog(serverUrlHash, 'OAuth callback server running', { port: actualPort }) + if (DEBUG) debugLog('OAuth callback server running', { port: actualPort }) log(`Creating lockfile for server ${serverUrlHash} with process ${process.pid} on port ${actualPort}`) - if (DEBUG) await debugLog(serverUrlHash, 'Creating lockfile', { serverUrlHash, pid: process.pid, port: actualPort }) await createLockfile(serverUrlHash, process.pid, actualPort) // Make sure lockfile is deleted on process exit const cleanupHandler = async () => { try { log(`Cleaning up lockfile for server ${serverUrlHash}`) - if (DEBUG) await debugLog(serverUrlHash, 'Cleaning up lockfile') await deleteLockfile(serverUrlHash) } catch (error) { log(`Error cleaning up lockfile: ${error}`) - if (DEBUG) await debugLog(serverUrlHash, 'Error cleaning up lockfile', error) + if (DEBUG) debugLog('Error cleaning up lockfile', error) } } @@ -273,11 +262,11 @@ export async function coordinateAuth( // Also handle SIGINT separately process.once('SIGINT', async () => { - if (DEBUG) await debugLog(serverUrlHash, 'Received SIGINT signal, cleaning up') + if (DEBUG) debugLog('Received SIGINT signal, cleaning up') await cleanupHandler() }) - if (DEBUG) await debugLog(serverUrlHash, 'Auth coordination complete, returning primary instance handlers') + if (DEBUG) debugLog('Auth coordination complete, returning primary instance handlers') return { server, waitForAuthCode, diff --git a/src/lib/node-oauth-client-provider.ts b/src/lib/node-oauth-client-provider.ts index de0c593..732bbd2 100644 --- a/src/lib/node-oauth-client-provider.ts +++ b/src/lib/node-oauth-client-provider.ts @@ -7,7 +7,7 @@ import { OAuthTokensSchema, } from '@modelcontextprotocol/sdk/shared/auth.js' import type { OAuthProviderOptions, StaticOAuthClientMetadata } from './types' -import { readJsonFile, writeJsonFile, readTextFile, writeTextFile } from './mcp-auth-config' +import { readJsonFile, writeJsonFile, readTextFile, writeTextFile, deleteConfigFile } from './mcp-auth-config' import { StaticOAuthClientInformationFull } from './types' import { getServerUrlHash, log, debugLog, DEBUG, MCP_REMOTE_VERSION } from './utils' import { randomUUID } from 'node:crypto' @@ -70,9 +70,9 @@ export class NodeOAuthClientProvider implements OAuthClientProvider { * @returns The client information or undefined */ async clientInformation(): Promise { - if (DEBUG) await debugLog(this.serverUrlHash, 'Reading client info') + if (DEBUG) debugLog('Reading client info') if (this.staticOAuthClientInfo) { - if (DEBUG) await debugLog(this.serverUrlHash, 'Returning static client info') + if (DEBUG) debugLog('Returning static client info') return this.staticOAuthClientInfo } const clientInfo = await readJsonFile( @@ -80,7 +80,7 @@ export class NodeOAuthClientProvider implements OAuthClientProvider { 'client_info.json', OAuthClientInformationFullSchema, ) - if (DEBUG) await debugLog(this.serverUrlHash, 'Client info result:', clientInfo ? 'Found' : 'Not found') + if (DEBUG) debugLog('Client info result:', clientInfo ? 'Found' : 'Not found') return clientInfo } @@ -89,7 +89,7 @@ export class NodeOAuthClientProvider implements OAuthClientProvider { * @param clientInformation The client information to save */ async saveClientInformation(clientInformation: OAuthClientInformationFull): Promise { - if (DEBUG) await debugLog(this.serverUrlHash, 'Saving client info', { client_id: clientInformation.client_id }) + if (DEBUG) debugLog('Saving client info', { client_id: clientInformation.client_id }) await writeJsonFile(this.serverUrlHash, 'client_info.json', clientInformation) } @@ -99,8 +99,8 @@ export class NodeOAuthClientProvider implements OAuthClientProvider { */ async tokens(): Promise { if (DEBUG) { - await debugLog(this.serverUrlHash, 'Reading OAuth tokens') - await debugLog(this.serverUrlHash, 'Token request stack trace:', new Error().stack) + debugLog('Reading OAuth tokens') + debugLog('Token request stack trace:', new Error().stack) } const tokens = await readJsonFile(this.serverUrlHash, 'tokens.json', OAuthTokensSchema) @@ -111,14 +111,14 @@ export class NodeOAuthClientProvider implements OAuthClientProvider { // Alert if expires_in is invalid if (typeof tokens.expires_in !== 'number' || tokens.expires_in < 0) { - await debugLog(this.serverUrlHash, '⚠️ WARNING: Invalid expires_in detected while reading tokens ⚠️', { + debugLog('⚠️ WARNING: Invalid expires_in detected while reading tokens ⚠️', { expiresIn: tokens.expires_in, tokenObject: JSON.stringify(tokens), stack: new Error('Invalid expires_in value').stack, }) } - await debugLog(this.serverUrlHash, 'Token result:', { + debugLog('Token result:', { found: true, hasAccessToken: !!tokens.access_token, hasRefreshToken: !!tokens.refresh_token, @@ -127,7 +127,7 @@ export class NodeOAuthClientProvider implements OAuthClientProvider { expiresInValue: tokens.expires_in, }) } else { - await debugLog(this.serverUrlHash, 'Token result: Not found') + debugLog('Token result: Not found') } } @@ -144,14 +144,14 @@ export class NodeOAuthClientProvider implements OAuthClientProvider { // Alert if expires_in is invalid if (typeof tokens.expires_in !== 'number' || tokens.expires_in < 0) { - await debugLog(this.serverUrlHash, '⚠️ WARNING: Invalid expires_in detected in tokens ⚠️', { + debugLog('⚠️ WARNING: Invalid expires_in detected in tokens ⚠️', { expiresIn: tokens.expires_in, tokenObject: JSON.stringify(tokens), stack: new Error('Invalid expires_in value').stack, }) } - await debugLog(this.serverUrlHash, 'Saving tokens', { + debugLog('Saving tokens', { hasAccessToken: !!tokens.access_token, hasRefreshToken: !!tokens.refresh_token, expiresIn: `${timeLeft} seconds`, @@ -169,15 +169,14 @@ export class NodeOAuthClientProvider implements OAuthClientProvider { async redirectToAuthorization(authorizationUrl: URL): Promise { log(`\nPlease authorize this client by visiting:\n${authorizationUrl.toString()}\n`) - if (DEBUG) await debugLog(this.serverUrlHash, 'Redirecting to authorization URL', authorizationUrl.toString()) + if (DEBUG) debugLog('Redirecting to authorization URL', authorizationUrl.toString()) try { await open(authorizationUrl.toString()) log('Browser opened automatically.') - if (DEBUG) await debugLog(this.serverUrlHash, 'Browser opened automatically') } catch (error) { log('Could not open browser automatically. Please copy and paste the URL above into your browser.') - if (DEBUG) await debugLog(this.serverUrlHash, 'Failed to open browser', error) + if (DEBUG) debugLog('Failed to open browser', error) } } @@ -186,7 +185,7 @@ export class NodeOAuthClientProvider implements OAuthClientProvider { * @param codeVerifier The code verifier to save */ async saveCodeVerifier(codeVerifier: string): Promise { - if (DEBUG) await debugLog(this.serverUrlHash, 'Saving code verifier') + if (DEBUG) debugLog('Saving code verifier') await writeTextFile(this.serverUrlHash, 'code_verifier.txt', codeVerifier) } @@ -195,9 +194,46 @@ export class NodeOAuthClientProvider implements OAuthClientProvider { * @returns The code verifier */ async codeVerifier(): Promise { - if (DEBUG) await debugLog(this.serverUrlHash, 'Reading code verifier') + if (DEBUG) debugLog('Reading code verifier') const verifier = await readTextFile(this.serverUrlHash, 'code_verifier.txt', 'No code verifier saved for session') - if (DEBUG) await debugLog(this.serverUrlHash, 'Code verifier found:', !!verifier) + if (DEBUG) debugLog('Code verifier found:', !!verifier) return verifier } + + /** + * Invalidates the specified credentials + * @param scope The scope of credentials to invalidate + */ + async invalidateCredentials(scope: 'all' | 'client' | 'tokens' | 'verifier'): Promise { + if (DEBUG) debugLog(`Invalidating credentials: ${scope}`) + + switch (scope) { + case 'all': + await Promise.all([ + deleteConfigFile(this.serverUrlHash, 'client_info.json'), + deleteConfigFile(this.serverUrlHash, 'tokens.json'), + deleteConfigFile(this.serverUrlHash, 'code_verifier.txt'), + ]) + if (DEBUG) debugLog('All credentials invalidated') + break + + case 'client': + await deleteConfigFile(this.serverUrlHash, 'client_info.json') + if (DEBUG) debugLog('Client information invalidated') + break + + case 'tokens': + await deleteConfigFile(this.serverUrlHash, 'tokens.json') + if (DEBUG) debugLog('OAuth tokens invalidated') + break + + case 'verifier': + await deleteConfigFile(this.serverUrlHash, 'code_verifier.txt') + if (DEBUG) debugLog('Code verifier invalidated') + break + + default: + throw new Error(`Unknown credential scope: ${scope}`) + } + } } diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 3b83016..c5598d5 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -3,15 +3,16 @@ 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 { OAuthError } from '@modelcontextprotocol/sdk/server/auth/errors.js' import { OAuthClientInformationFull, OAuthClientInformationFullSchema } from '@modelcontextprotocol/sdk/shared/auth.js' import { OAuthCallbackServerOptions, StaticOAuthClientInformationFull, StaticOAuthClientMetadata } from './types' -import { getConfigFilePath, readJsonFile } from './mcp-auth-config' +import { getConfigDir, getConfigFilePath, readJsonFile } from './mcp-auth-config' import express from 'express' import net from 'net' import crypto from 'crypto' -import fs, { readFile } from 'fs/promises' +import fs from 'fs' +import { readFile, rm } from 'fs/promises' import path from 'path' -import os from 'os' import { version as MCP_REMOTE_VERSION } from '../../package.json' // Global type declaration for typescript @@ -38,7 +39,7 @@ function getTimestamp(): string { } // Debug logging function -export async function debugLog(message: string, ...args: any[]): Promise { +export function debugLog(message: string, ...args: any[]) { if (!DEBUG) return const serverUrlHash = global.currentServerUrlHash @@ -55,14 +56,14 @@ export async function debugLog(message: string, ...args: any[]): Promise { console.error(formattedMessage, ...args) // Ensure config directory exists - const configDir = process.env.MCP_REMOTE_CONFIG_DIR || path.join(os.homedir(), '.mcp-auth') - await fs.mkdir(configDir, { recursive: true }) + const configDir = getConfigDir() + fs.mkdirSync(configDir, { recursive: true }) // Append to log file const logPath = path.join(configDir, `${serverUrlHash}_debug.log`) const logMessage = `${formattedMessage} ${args.map((arg) => (typeof arg === 'object' ? JSON.stringify(arg) : String(arg))).join(' ')}\n` - await fs.appendFile(logPath, logMessage, { encoding: 'utf8' }) + fs.appendFileSync(logPath, logMessage, { encoding: 'utf8' }) } catch (error) { // Fallback to console if file logging fails console.error(`[DEBUG LOG ERROR] ${error}`) @@ -75,7 +76,7 @@ export function log(str: string, ...rest: unknown[]) { // If debug mode is on, also log to debug file if (DEBUG && global.currentServerUrlHash) { - debugLog(str, ...rest).catch(() => {}) + debugLog(str, ...rest) } } @@ -97,7 +98,7 @@ export function mcpProxy({ transportToClient, transportToServer }: { transportTo method: message.method, id: message.id, params: message.params ? JSON.stringify(message.params).substring(0, 500) : undefined, - }).catch(() => {}) + }) } if (message.method === 'initialize') { @@ -106,7 +107,7 @@ export function mcpProxy({ transportToClient, transportToServer }: { transportTo log(JSON.stringify(message, null, 2)) if (DEBUG) { - debugLog('Initialize message with modified client info', { clientInfo }).catch(() => {}) + debugLog('Initialize message with modified client info', { clientInfo }) } } @@ -124,7 +125,7 @@ export function mcpProxy({ transportToClient, transportToServer }: { transportTo id: message.id, result: message.result ? 'result-present' : undefined, error: message.error, - }).catch(() => {}) + }) } transportToClient.send(message).catch(onClientError) @@ -136,7 +137,7 @@ export function mcpProxy({ transportToClient, transportToServer }: { transportTo } transportToClientClosed = true - if (DEBUG) debugLog('Local transport closed, closing remote transport').catch(() => {}) + if (DEBUG) debugLog('Local transport closed, closing remote transport') transportToServer.close().catch(onServerError) } @@ -145,7 +146,7 @@ export function mcpProxy({ transportToClient, transportToServer }: { transportTo return } transportToServerClosed = true - if (DEBUG) debugLog('Remote transport closed, closing local transport').catch(() => {}) + if (DEBUG) debugLog('Remote transport closed, closing local transport') transportToClient.close().catch(onClientError) } @@ -154,12 +155,12 @@ export function mcpProxy({ transportToClient, transportToServer }: { transportTo function onClientError(error: Error) { log('Error from local client:', error) - if (DEBUG) debugLog('Error from local client', { errorMessage: error.message, stack: error.stack }).catch(() => {}) + if (DEBUG) debugLog('Error from local client', { stack: error.stack }) } function onServerError(error: Error) { log('Error from remote server:', error) - if (DEBUG) debugLog('Error from remote server', { errorMessage: error.message, stack: error.stack }).catch(() => {}) + if (DEBUG) debugLog('Error from remote server', { stack: error.stack }) } } @@ -230,27 +231,26 @@ export async function connectToRemoteServer( }) try { - if (DEBUG) await debugLog('Attempting to connect to remote server', { sseTransport }) + if (DEBUG) debugLog('Attempting to connect to remote server', { sseTransport }) if (client) { - if (DEBUG) await debugLog('Connecting client to transport') + if (DEBUG) debugLog('Connecting client to transport') await client.connect(transport) } else { - if (DEBUG) await debugLog('Starting transport directly') + if (DEBUG) debugLog('Starting transport directly') await transport.start() if (!sseTransport) { // Extremely hacky, but we didn't actually send a request when calling transport.start() above, so we don't // know if we're even talking to an HTTP server. But if we forced that now we'd get an error later saying that // the client is already connected. So let's just create a one-off client to make a single request and figure // out if we're actually talking to an HTTP server or not. - if (DEBUG) await debugLog('Creating test transport for HTTP-only connection test') + if (DEBUG) debugLog('Creating test transport for HTTP-only connection test') const testTransport = new StreamableHTTPClientTransport(url, { authProvider, requestInit: { headers } }) const testClient = new Client({ name: 'mcp-remote-fallback-test', version: '0.0.0' }, { capabilities: {} }) await testClient.connect(testTransport) } } log(`Connected to remote server using ${transport.constructor.name}`) - if (DEBUG) await debugLog(`Connected to remote server successfully`, { transportType: transport.constructor.name }) return transport } catch (error: any) { @@ -290,58 +290,63 @@ export async function connectToRemoteServer( } else if (error instanceof UnauthorizedError || (error instanceof Error && error.message.includes('Unauthorized'))) { log('Authentication required. Initializing auth...') if (DEBUG) { - await debugLog('Authentication required, initializing auth process', { + debugLog('Authentication error detected', { + errorCode: error instanceof OAuthError ? error.errorCode : undefined, errorMessage: error.message, stack: error.stack, }) } // Initialize authentication on-demand - if (DEBUG) await debugLog('Calling authInitializer to start auth flow') + if (DEBUG) debugLog('Calling authInitializer to start auth flow') const { waitForAuthCode, skipBrowserAuth } = await authInitializer() if (skipBrowserAuth) { log('Authentication required but skipping browser auth - using shared auth') - if (DEBUG) await debugLog('Authentication required but skipping browser auth - using shared auth') } else { log('Authentication required. Waiting for authorization...') - if (DEBUG) await debugLog('Authentication required. Waiting for authorization...') } // Wait for the authorization code from the callback - if (DEBUG) await debugLog('Waiting for auth code from callback server') + if (DEBUG) debugLog('Waiting for auth code from callback server') const code = await waitForAuthCode() - if (DEBUG) await debugLog('Received auth code from callback server') + if (DEBUG) debugLog('Received auth code from callback server') try { log('Completing authorization...') - if (DEBUG) await debugLog('Completing authorization with transport.finishAuth') await transport.finishAuth(code) - if (DEBUG) await debugLog('Authorization completed successfully') + if (DEBUG) debugLog('Authorization completed successfully') if (recursionReasons.has(REASON_AUTH_NEEDED)) { const errorMessage = `Already attempted reconnection for reason: ${REASON_AUTH_NEEDED}. Giving up.` log(errorMessage) - if (DEBUG) await debugLog('Already attempted auth reconnection, giving up', { recursionReasons: Array.from(recursionReasons) }) + if (DEBUG) + debugLog('Already attempted auth reconnection, giving up', { + recursionReasons: Array.from(recursionReasons), + }) throw new Error(errorMessage) } // Track this reason for recursion recursionReasons.add(REASON_AUTH_NEEDED) log(`Recursively reconnecting for reason: ${REASON_AUTH_NEEDED}`) - if (DEBUG) await debugLog('Recursively reconnecting after auth', { recursionReasons: Array.from(recursionReasons) }) + if (DEBUG) debugLog('Recursively reconnecting after auth', { recursionReasons: Array.from(recursionReasons) }) // Recursively call connectToRemoteServer with the updated recursion tracking return connectToRemoteServer(client, serverUrl, authProvider, headers, authInitializer, transportStrategy, recursionReasons) } catch (authError: any) { log('Authorization error:', authError) - if (DEBUG) await debugLog('Authorization error during finishAuth', { errorMessage: authError.message, stack: authError.stack }) + if (DEBUG) + debugLog('Authorization error during finishAuth', { + errorMessage: authError.message, + stack: authError.stack, + }) throw authError } } else { log('Connection error:', error) if (DEBUG) - await debugLog('Connection error', { + debugLog('Connection error', { errorMessage: error.message, stack: error.stack, transportType: transport.constructor.name, @@ -623,7 +628,7 @@ export async function parseCommandLineArgs(args: string[], usage: string) { global.currentServerUrlHash = serverUrlHash if (DEBUG) { - debugLog(`Starting mcp-remote with server URL: ${serverUrl}`).catch(() => {}) + debugLog(`Starting mcp-remote with server URL: ${serverUrl}`) } const defaultPort = calculateDefaultPort(serverUrlHash) @@ -637,7 +642,7 @@ export async function parseCommandLineArgs(args: string[], usage: string) { log( `Warning! Specified callback port of ${specifiedPort}, which conflicts with existing client registration port ${existingClientPort}. Deleting existing client data to force reregistration.`, ) - await fs.rm(getConfigFilePath(serverUrlHash, 'client_info.json')) + await rm(getConfigFilePath(serverUrlHash, 'client_info.json')) } log(`Using specified callback port: ${specifiedPort}`) callbackPort = specifiedPort