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 path from 'path'
import os from 'os' import os from 'os'
import fs from 'fs/promises' import fs from 'fs/promises'
import { MCP_REMOTE_VERSION } from './utils' import { log, MCP_REMOTE_VERSION } from './utils'
/** /**
* MCP Remote Authentication Configuration * 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 * @param serverUrlHash The hash of the server URL
*/ */
export async function cleanServerConfig(serverUrlHash: string): Promise<void> { 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) { for (const filename of knownConfigFiles) {
await deleteConfigFile(serverUrlHash, filename) await deleteConfigFile(serverUrlHash, filename)
} }
@ -58,7 +58,7 @@ export async function ensureConfigDir(): Promise<void> {
const configDir = getConfigDir() const configDir = getConfigDir()
await fs.mkdir(configDir, { recursive: true }) await fs.mkdir(configDir, { recursive: true })
} catch (error) { } catch (error) {
console.error('Error creating config directory:', error) log('Error creating config directory:', error)
throw error throw error
} }
} }
@ -95,7 +95,7 @@ export async function deleteConfigFile(serverUrlHash: string, filename: string):
} catch (error) { } catch (error) {
// Ignore if file doesn't exist // Ignore if file doesn't exist
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { 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) const filePath = getConfigFilePath(serverUrlHash, filename)
await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8') await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8')
} catch (error) { } catch (error) {
console.error(`Error writing ${filename}:`, error) log(`Error writing ${filename}:`, error)
throw error throw error
} }
} }
@ -193,7 +193,7 @@ export async function writeTextFile(serverUrlHash: string, filename: string, tex
const filePath = getConfigFilePath(serverUrlHash, filename) const filePath = getConfigFilePath(serverUrlHash, filename)
await fs.writeFile(filePath, text, 'utf-8') await fs.writeFile(filePath, text, 'utf-8')
} catch (error) { } catch (error) {
console.error(`Error writing ${filename}:`, error) log(`Error writing ${filename}:`, error)
throw error throw error
} }
} }

View file

@ -8,14 +8,8 @@ import {
OAuthTokensSchema, OAuthTokensSchema,
} from '@modelcontextprotocol/sdk/shared/auth.js' } from '@modelcontextprotocol/sdk/shared/auth.js'
import type { OAuthProviderOptions } from './types' import type { OAuthProviderOptions } from './types'
import { import { getServerUrlHash, readJsonFile, writeJsonFile, readTextFile, writeTextFile, cleanServerConfig } from './mcp-auth-config'
getServerUrlHash, import { log } from './utils'
readJsonFile,
writeJsonFile,
readTextFile,
writeTextFile,
cleanServerConfig,
} from './mcp-auth-config'
/** /**
* Implements the OAuthClientProvider interface for Node.js environments. * Implements the OAuthClientProvider interface for Node.js environments.
@ -39,8 +33,8 @@ export class NodeOAuthClientProvider implements OAuthClientProvider {
// If clean flag is set, proactively clean all config files for this server // If clean flag is set, proactively clean all config files for this server
if (options.clean) { if (options.clean) {
cleanServerConfig(this.serverUrlHash).catch(err => { cleanServerConfig(this.serverUrlHash).catch((err) => {
console.error('Error cleaning server config:', err) log('Error cleaning server config:', err)
}) })
} }
} }
@ -65,12 +59,7 @@ export class NodeOAuthClientProvider implements OAuthClientProvider {
* @returns The client information or undefined * @returns The client information or undefined
*/ */
async clientInformation(): Promise<OAuthClientInformation | undefined> { async clientInformation(): Promise<OAuthClientInformation | undefined> {
return readJsonFile<OAuthClientInformation>( return readJsonFile<OAuthClientInformation>(this.serverUrlHash, 'client_info.json', OAuthClientInformationSchema, this.options.clean)
this.serverUrlHash,
'client_info.json',
OAuthClientInformationSchema,
this.options.clean
)
} }
/** /**
@ -86,12 +75,7 @@ export class NodeOAuthClientProvider implements OAuthClientProvider {
* @returns The OAuth tokens or undefined * @returns The OAuth tokens or undefined
*/ */
async tokens(): Promise<OAuthTokens | undefined> { async tokens(): Promise<OAuthTokens | undefined> {
return readJsonFile<OAuthTokens>( return readJsonFile<OAuthTokens>(this.serverUrlHash, 'tokens.json', OAuthTokensSchema, this.options.clean)
this.serverUrlHash,
'tokens.json',
OAuthTokensSchema,
this.options.clean
)
} }
/** /**
@ -107,12 +91,12 @@ export class NodeOAuthClientProvider implements OAuthClientProvider {
* @param authorizationUrl The URL to redirect to * @param authorizationUrl The URL to redirect to
*/ */
async redirectToAuthorization(authorizationUrl: URL): Promise<void> { 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 { try {
await open(authorizationUrl.toString()) await open(authorizationUrl.toString())
console.error('Browser opened automatically.') log('Browser opened automatically.')
} catch (error) { } 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 * @returns The code verifier
*/ */
async codeVerifier(): Promise<string> { async codeVerifier(): Promise<string> {
return await readTextFile( return await readTextFile(this.serverUrlHash, 'code_verifier.txt', 'No code verifier saved for session', this.options.clean)
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' import net from 'net'
const pid = process.pid 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 * Creates a bidirectional proxy between two transports
@ -17,13 +21,13 @@ export function mcpProxy({ transportToClient, transportToServer }: { transportTo
transportToClient.onmessage = (message) => { transportToClient.onmessage = (message) => {
// @ts-expect-error TODO // @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.send(message).catch(onServerError)
} }
transportToServer.onmessage = (message) => { transportToServer.onmessage = (message) => {
// @ts-expect-error TODO: fix this type // @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) transportToClient.send(message).catch(onClientError)
} }
@ -48,11 +52,11 @@ export function mcpProxy({ transportToClient, transportToServer }: { transportTo
transportToServer.onerror = onServerError transportToServer.onerror = onServerError
function onClientError(error: Error) { function onClientError(error: Error) {
console.error('Error from local client:', error) log('Error from local client:', error)
} }
function onServerError(error: 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, authProvider: OAuthClientProvider,
waitForAuthCode: () => Promise<string>, waitForAuthCode: () => Promise<string>,
): Promise<SSEClientTransport> { ): Promise<SSEClientTransport> {
console.error(`[${pid}] Connecting to remote server: ${serverUrl}`) log(`[${pid}] Connecting to remote server: ${serverUrl}`)
const url = new URL(serverUrl) const url = new URL(serverUrl)
const transport = new SSEClientTransport(url, { authProvider }) const transport = new SSEClientTransport(url, { authProvider })
try { try {
await transport.start() await transport.start()
console.error('Connected to remote server') log('Connected to remote server')
return transport return transport
} catch (error) { } catch (error) {
if (error instanceof UnauthorizedError || (error instanceof Error && error.message.includes('Unauthorized'))) { 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 // Wait for the authorization code from the callback
const code = await waitForAuthCode() const code = await waitForAuthCode()
try { try {
console.error('Completing authorization...') log('Completing authorization...')
await transport.finishAuth(code) await transport.finishAuth(code)
// Create a new transport after auth // Create a new transport after auth
const newTransport = new SSEClientTransport(url, { authProvider }) const newTransport = new SSEClientTransport(url, { authProvider })
await newTransport.start() await newTransport.start()
console.error('Connected to remote server after authentication') log('Connected to remote server after authentication')
return newTransport return newTransport
} catch (authError) { } catch (authError) {
console.error('Authorization error:', authError) log('Authorization error:', authError)
throw authError throw authError
} }
} else { } else {
console.error('Connection error:', error) log('Connection error:', error)
throw error throw error
} }
} }
@ -127,7 +131,7 @@ export function setupOAuthCallbackServer(options: OAuthCallbackServerOptions) {
}) })
const server = app.listen(options.port, () => { 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 const specifiedPort = args[1] ? parseInt(args[1]) : undefined
if (!serverUrl) { if (!serverUrl) {
console.error(usage) log(usage)
process.exit(1) 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:' const isLocalhost = (url.hostname === 'localhost' || url.hostname === '127.0.0.1') && url.protocol === 'http:'
if (!(url.protocol == 'https:' || isLocalhost)) { if (!(url.protocol == 'https:' || isLocalhost)) {
console.error(usage) log(usage)
process.exit(1) process.exit(1)
} }
@ -217,13 +221,13 @@ export async function parseCommandLineArgs(args: string[], defaultPort: number,
const callbackPort = specifiedPort || (await findAvailablePort(defaultPort)) const callbackPort = specifiedPort || (await findAvailablePort(defaultPort))
if (specifiedPort) { if (specifiedPort) {
console.error(`Using specified callback port: ${callbackPort}`) log(`Using specified callback port: ${callbackPort}`)
} else { } else {
console.error(`Using automatically selected callback port: ${callbackPort}`) log(`Using automatically selected callback port: ${callbackPort}`)
} }
if (clean) { 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 } return { serverUrl, callbackPort, clean }
@ -235,7 +239,7 @@ export async function parseCommandLineArgs(args: string[], defaultPort: number,
*/ */
export function setupSignalHandlers(cleanup: () => Promise<void>) { export function setupSignalHandlers(cleanup: () => Promise<void>) {
process.on('SIGINT', async () => { process.on('SIGINT', async () => {
console.error('\nShutting down...') log('\nShutting down...')
await cleanup() await cleanup()
process.exit(0) process.exit(0)
}) })

View file

@ -14,7 +14,7 @@
import { EventEmitter } from 'events' import { EventEmitter } from 'events'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' 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' 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 // Start the local STDIO server
await localTransport.start() await localTransport.start()
console.error('Local STDIO server running') log('Local STDIO server running')
console.error('Proxy established successfully between local STDIO and remote SSE') log('Proxy established successfully between local STDIO and remote SSE')
console.error('Press Ctrl+C to exit') log('Press Ctrl+C to exit')
// Setup cleanup handler // Setup cleanup handler
const cleanup = async () => { const cleanup = async () => {
@ -66,9 +66,9 @@ async function runProxy(serverUrl: string, callbackPort: number, clean: boolean
} }
setupSignalHandlers(cleanup) setupSignalHandlers(cleanup)
} catch (error) { } catch (error) {
console.error('Fatal error:', error) log('Fatal error:', error)
if (error instanceof Error && error.message.includes('self-signed certificate in certificate chain')) { 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 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: 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) return runProxy(serverUrl, callbackPort, clean)
}) })
.catch((error) => { .catch((error) => {
console.error('Fatal error:', error) log('Fatal error:', error)
process.exit(1) process.exit(1)
}) })