Added process.pid to all logs so we can deal with claude code forking mulitple instances

This commit is contained in:
Glen Maddern 2025-03-31 16:29:45 +11:00
parent 1382827ebd
commit eee7b1b8d5
4 changed files with 47 additions and 64 deletions

View file

@ -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<void> {
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<void> {
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
}
}

View file

@ -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<OAuthClientInformation | undefined> {
return readJsonFile<OAuthClientInformation>(
this.serverUrlHash,
'client_info.json',
OAuthClientInformationSchema,
this.options.clean
)
return readJsonFile<OAuthClientInformation>(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<OAuthTokens | undefined> {
return readJsonFile<OAuthTokens>(
this.serverUrlHash,
'tokens.json',
OAuthTokensSchema,
this.options.clean
)
return readJsonFile<OAuthTokens>(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<void> {
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<string> {
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)
}
}
}

View file

@ -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<string>,
): Promise<SSEClientTransport> {
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<void>) {
process.on('SIGINT', async () => {
console.error('\nShutting down...')
log('\nShutting down...')
await cleanup()
process.exit(0)
})

View file

@ -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)
})