Added process.pid to all logs so we can deal with claude code forking mulitple instances
This commit is contained in:
parent
1382827ebd
commit
eee7b1b8d5
4 changed files with 47 additions and 64 deletions
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
|
|
14
src/proxy.ts
14
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)
|
||||
})
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue