From eee7b1b8d532be0d257eb95f2e7b01b37ebbccaf Mon Sep 17 00:00:00 2001 From: Glen Maddern Date: Mon, 31 Mar 2025 16:29:45 +1100 Subject: [PATCH] Added process.pid to all logs so we can deal with claude code forking mulitple instances --- src/lib/mcp-auth-config.ts | 12 +++---- src/lib/node-oauth-client-provider.ts | 45 +++++++-------------------- src/lib/utils.ts | 40 +++++++++++++----------- src/proxy.ts | 14 ++++----- 4 files changed, 47 insertions(+), 64 deletions(-) diff --git a/src/lib/mcp-auth-config.ts b/src/lib/mcp-auth-config.ts index e35badc..a71782a 100644 --- a/src/lib/mcp-auth-config.ts +++ b/src/lib/mcp-auth-config.ts @@ -2,7 +2,7 @@ import crypto from 'crypto' import path from 'path' import os from 'os' import fs from 'fs/promises' -import { MCP_REMOTE_VERSION } from './utils' +import { log, MCP_REMOTE_VERSION } from './utils' /** * MCP Remote Authentication Configuration @@ -34,7 +34,7 @@ export const knownConfigFiles = ['client_info.json', 'tokens.json', 'code_verifi * @param serverUrlHash The hash of the server URL */ export async function cleanServerConfig(serverUrlHash: string): Promise { - console.error(`Cleaning configuration files for server: ${serverUrlHash}`) + log(`Cleaning configuration files for server: ${serverUrlHash}`) for (const filename of knownConfigFiles) { await deleteConfigFile(serverUrlHash, filename) } @@ -58,7 +58,7 @@ export async function ensureConfigDir(): Promise { const configDir = getConfigDir() await fs.mkdir(configDir, { recursive: true }) } catch (error) { - console.error('Error creating config directory:', error) + log('Error creating config directory:', error) throw error } } @@ -95,7 +95,7 @@ export async function deleteConfigFile(serverUrlHash: string, filename: string): } catch (error) { // Ignore if file doesn't exist if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { - console.error(`Error deleting ${filename}:`, error) + log(`Error deleting ${filename}:`, error) } } } @@ -146,7 +146,7 @@ export async function writeJsonFile(serverUrlHash: string, filename: string, dat const filePath = getConfigFilePath(serverUrlHash, filename) await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8') } catch (error) { - console.error(`Error writing ${filename}:`, error) + log(`Error writing ${filename}:`, error) throw error } } @@ -193,7 +193,7 @@ export async function writeTextFile(serverUrlHash: string, filename: string, tex const filePath = getConfigFilePath(serverUrlHash, filename) await fs.writeFile(filePath, text, 'utf-8') } catch (error) { - console.error(`Error writing ${filename}:`, error) + log(`Error writing ${filename}:`, error) throw error } } diff --git a/src/lib/node-oauth-client-provider.ts b/src/lib/node-oauth-client-provider.ts index ed6caed..0a05d47 100644 --- a/src/lib/node-oauth-client-provider.ts +++ b/src/lib/node-oauth-client-provider.ts @@ -8,14 +8,8 @@ import { OAuthTokensSchema, } from '@modelcontextprotocol/sdk/shared/auth.js' import type { OAuthProviderOptions } from './types' -import { - getServerUrlHash, - readJsonFile, - writeJsonFile, - readTextFile, - writeTextFile, - cleanServerConfig, -} from './mcp-auth-config' +import { getServerUrlHash, readJsonFile, writeJsonFile, readTextFile, writeTextFile, cleanServerConfig } from './mcp-auth-config' +import { log } from './utils' /** * Implements the OAuthClientProvider interface for Node.js environments. @@ -36,11 +30,11 @@ export class NodeOAuthClientProvider implements OAuthClientProvider { this.callbackPath = options.callbackPath || '/oauth/callback' this.clientName = options.clientName || 'MCP CLI Client' this.clientUri = options.clientUri || 'https://github.com/modelcontextprotocol/mcp-cli' - + // If clean flag is set, proactively clean all config files for this server if (options.clean) { - cleanServerConfig(this.serverUrlHash).catch(err => { - console.error('Error cleaning server config:', err) + cleanServerConfig(this.serverUrlHash).catch((err) => { + log('Error cleaning server config:', err) }) } } @@ -65,12 +59,7 @@ export class NodeOAuthClientProvider implements OAuthClientProvider { * @returns The client information or undefined */ async clientInformation(): Promise { - return readJsonFile( - this.serverUrlHash, - 'client_info.json', - OAuthClientInformationSchema, - this.options.clean - ) + return readJsonFile(this.serverUrlHash, 'client_info.json', OAuthClientInformationSchema, this.options.clean) } /** @@ -86,12 +75,7 @@ export class NodeOAuthClientProvider implements OAuthClientProvider { * @returns The OAuth tokens or undefined */ async tokens(): Promise { - return readJsonFile( - this.serverUrlHash, - 'tokens.json', - OAuthTokensSchema, - this.options.clean - ) + return readJsonFile(this.serverUrlHash, 'tokens.json', OAuthTokensSchema, this.options.clean) } /** @@ -107,12 +91,12 @@ export class NodeOAuthClientProvider implements OAuthClientProvider { * @param authorizationUrl The URL to redirect to */ async redirectToAuthorization(authorizationUrl: URL): Promise { - console.error(`\nPlease authorize this client by visiting:\n${authorizationUrl.toString()}\n`) + log(`\nPlease authorize this client by visiting:\n${authorizationUrl.toString()}\n`) try { await open(authorizationUrl.toString()) - console.error('Browser opened automatically.') + log('Browser opened automatically.') } catch (error) { - console.error('Could not open browser automatically. Please copy and paste the URL above into your browser.') + log('Could not open browser automatically. Please copy and paste the URL above into your browser.') } } @@ -129,11 +113,6 @@ export class NodeOAuthClientProvider implements OAuthClientProvider { * @returns The code verifier */ async codeVerifier(): Promise { - return await readTextFile( - this.serverUrlHash, - 'code_verifier.txt', - 'No code verifier saved for session', - this.options.clean - ) + return await readTextFile(this.serverUrlHash, 'code_verifier.txt', 'No code verifier saved for session', this.options.clean) } -} \ No newline at end of file +} diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 3cbb1f6..08a6f5d 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -6,6 +6,10 @@ import express from 'express' import net from 'net' const pid = process.pid +export function log(str: string, ...rest: unknown[]) { + // Using stderr so that it doesn't interfere with stdout + console.error(`[${pid}] ${str}`, ...rest) +} /** * Creates a bidirectional proxy between two transports @@ -17,13 +21,13 @@ export function mcpProxy({ transportToClient, transportToServer }: { transportTo transportToClient.onmessage = (message) => { // @ts-expect-error TODO - console.error('[Local→Remote]', message.method || message.id) + log('[Local→Remote]', message.method || message.id) transportToServer.send(message).catch(onServerError) } transportToServer.onmessage = (message) => { // @ts-expect-error TODO: fix this type - console.error('[Remote→Local]', message.method || message.id) + log('[Remote→Local]', message.method || message.id) transportToClient.send(message).catch(onClientError) } @@ -48,11 +52,11 @@ export function mcpProxy({ transportToClient, transportToServer }: { transportTo transportToServer.onerror = onServerError function onClientError(error: Error) { - console.error('Error from local client:', error) + log('Error from local client:', error) } function onServerError(error: Error) { - console.error('Error from remote server:', error) + log('Error from remote server:', error) } } @@ -68,36 +72,36 @@ export async function connectToRemoteServer( authProvider: OAuthClientProvider, waitForAuthCode: () => Promise, ): Promise { - console.error(`[${pid}] Connecting to remote server: ${serverUrl}`) + log(`[${pid}] Connecting to remote server: ${serverUrl}`) const url = new URL(serverUrl) const transport = new SSEClientTransport(url, { authProvider }) try { await transport.start() - console.error('Connected to remote server') + log('Connected to remote server') return transport } catch (error) { if (error instanceof UnauthorizedError || (error instanceof Error && error.message.includes('Unauthorized'))) { - console.error('Authentication required. Waiting for authorization...') + log('Authentication required. Waiting for authorization...') // Wait for the authorization code from the callback const code = await waitForAuthCode() try { - console.error('Completing authorization...') + log('Completing authorization...') await transport.finishAuth(code) // Create a new transport after auth const newTransport = new SSEClientTransport(url, { authProvider }) await newTransport.start() - console.error('Connected to remote server after authentication') + log('Connected to remote server after authentication') return newTransport } catch (authError) { - console.error('Authorization error:', authError) + log('Authorization error:', authError) throw authError } } else { - console.error('Connection error:', error) + log('Connection error:', error) throw error } } @@ -127,7 +131,7 @@ export function setupOAuthCallbackServer(options: OAuthCallbackServerOptions) { }) const server = app.listen(options.port, () => { - console.error(`OAuth callback server running at http://127.0.0.1:${options.port}`) + log(`OAuth callback server running at http://127.0.0.1:${options.port}`) }) /** @@ -201,7 +205,7 @@ export async function parseCommandLineArgs(args: string[], defaultPort: number, const specifiedPort = args[1] ? parseInt(args[1]) : undefined if (!serverUrl) { - console.error(usage) + log(usage) process.exit(1) } @@ -209,7 +213,7 @@ export async function parseCommandLineArgs(args: string[], defaultPort: number, const isLocalhost = (url.hostname === 'localhost' || url.hostname === '127.0.0.1') && url.protocol === 'http:' if (!(url.protocol == 'https:' || isLocalhost)) { - console.error(usage) + log(usage) process.exit(1) } @@ -217,13 +221,13 @@ export async function parseCommandLineArgs(args: string[], defaultPort: number, const callbackPort = specifiedPort || (await findAvailablePort(defaultPort)) if (specifiedPort) { - console.error(`Using specified callback port: ${callbackPort}`) + log(`Using specified callback port: ${callbackPort}`) } else { - console.error(`Using automatically selected callback port: ${callbackPort}`) + log(`Using automatically selected callback port: ${callbackPort}`) } if (clean) { - console.error('Clean mode enabled: config files will be reset before reading') + log('Clean mode enabled: config files will be reset before reading') } return { serverUrl, callbackPort, clean } @@ -235,7 +239,7 @@ export async function parseCommandLineArgs(args: string[], defaultPort: number, */ export function setupSignalHandlers(cleanup: () => Promise) { process.on('SIGINT', async () => { - console.error('\nShutting down...') + log('\nShutting down...') await cleanup() process.exit(0) }) diff --git a/src/proxy.ts b/src/proxy.ts index dff2e9e..b549018 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -14,7 +14,7 @@ import { EventEmitter } from 'events' import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' -import { connectToRemoteServer, mcpProxy, parseCommandLineArgs, setupOAuthCallbackServer, setupSignalHandlers } from './lib/utils' +import { connectToRemoteServer, log, mcpProxy, parseCommandLineArgs, setupOAuthCallbackServer, setupSignalHandlers } from './lib/utils' import { NodeOAuthClientProvider } from './lib/node-oauth-client-provider' /** @@ -54,9 +54,9 @@ async function runProxy(serverUrl: string, callbackPort: number, clean: boolean // Start the local STDIO server await localTransport.start() - console.error('Local STDIO server running') - console.error('Proxy established successfully between local STDIO and remote SSE') - console.error('Press Ctrl+C to exit') + 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 () => { @@ -66,9 +66,9 @@ async function runProxy(serverUrl: string, callbackPort: number, clean: boolean } setupSignalHandlers(cleanup) } catch (error) { - console.error('Fatal error:', error) + log('Fatal error:', error) if (error instanceof Error && error.message.includes('self-signed certificate in certificate chain')) { - console.error(`You may be behind a VPN! + 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: @@ -100,6 +100,6 @@ parseCommandLineArgs(process.argv.slice(2), 3334, 'Usage: npx tsx proxy.ts [--cl return runProxy(serverUrl, callbackPort, clean) }) .catch((error) => { - console.error('Fatal error:', error) + log('Fatal error:', error) process.exit(1) })