From 027007030e6400beadbdeab94e4f17243fd2ff29 Mon Sep 17 00:00:00 2001 From: Glen Maddern Date: Mon, 31 Mar 2025 15:02:27 +1100 Subject: [PATCH] adding the --clean flag --- src/client.ts | 14 +++-- src/lib/mcp-auth-config.ts | 82 +++++++++++++++++++++++---- src/lib/node-oauth-client-provider.ts | 21 ++++++- src/lib/types.ts | 2 + src/lib/utils.ts | 17 +++++- src/proxy.ts | 14 +++-- 6 files changed, 125 insertions(+), 25 deletions(-) diff --git a/src/client.ts b/src/client.ts index 48d6ea4..451325c 100644 --- a/src/client.ts +++ b/src/client.ts @@ -4,7 +4,10 @@ * MCP Client with OAuth support * A command-line client that connects to an MCP server using SSE with OAuth authentication. * - * Run with: npx tsx client.ts https://example.remote/server [callback-port] + * Run with: npx tsx client.ts [--clean] https://example.remote/server [callback-port] + * + * Options: + * --clean: Deletes stored configuration before reading, ensuring a fresh session * * If callback-port is not specified, an available port will be automatically selected. */ @@ -20,7 +23,7 @@ import { parseCommandLineArgs, setupOAuthCallbackServer, setupSignalHandlers } f /** * Main function to run the client */ -async function runClient(serverUrl: string, callbackPort: number) { +async function runClient(serverUrl: string, callbackPort: number, clean: boolean = false) { // Set up event emitter for auth flow const events = new EventEmitter() @@ -29,6 +32,7 @@ async function runClient(serverUrl: string, callbackPort: number) { serverUrl, callbackPort, clientName: 'MCP CLI Client', + clean, }) // Create the client @@ -147,9 +151,9 @@ async function runClient(serverUrl: string, callbackPort: number) { } // Parse command-line arguments and run the client -parseCommandLineArgs(process.argv.slice(2), 3333, 'Usage: npx tsx client.ts [callback-port]') - .then(({ serverUrl, callbackPort }) => { - return runClient(serverUrl, callbackPort) +parseCommandLineArgs(process.argv.slice(2), 3333, 'Usage: npx tsx client.ts [--clean] [callback-port]') + .then(({ serverUrl, callbackPort, clean }) => { + return runClient(serverUrl, callbackPort, clean) }) .catch((error) => { console.error('Fatal error:', error) diff --git a/src/lib/mcp-auth-config.ts b/src/lib/mcp-auth-config.ts index 8f06d41..f52fc64 100644 --- a/src/lib/mcp-auth-config.ts +++ b/src/lib/mcp-auth-config.ts @@ -23,6 +23,26 @@ import fs from 'fs/promises' * All JSON files are stored with 2-space indentation for readability. */ +/** + * Known configuration file names that might need to be cleaned + */ +export const knownConfigFiles = [ + 'client_info.json', + 'tokens.json', + 'code_verifier.txt', +]; + +/** + * Deletes all known configuration files for a specific server + * @param serverUrlHash The hash of the server URL + */ +export async function cleanServerConfig(serverUrlHash: string): Promise { + console.error(`Cleaning configuration files for server: ${serverUrlHash}`) + for (const filename of knownConfigFiles) { + await deleteConfigFile(serverUrlHash, filename) + } +} + /** * Gets the configuration directory path * @returns The path to the configuration directory @@ -53,22 +73,58 @@ export function getServerUrlHash(serverUrl: string): string { return crypto.createHash('md5').update(serverUrl).digest('hex') } +/** + * Gets the file path for a config file + * @param serverUrlHash The hash of the server URL + * @param filename The name of the file + * @returns The absolute file path + */ +export function getConfigFilePath(serverUrlHash: string, filename: string): string { + const configDir = getConfigDir() + return path.join(configDir, `${serverUrlHash}_${filename}`) +} + +/** + * Deletes a config file if it exists + * @param serverUrlHash The hash of the server URL + * @param filename The name of the file to delete + */ +export async function deleteConfigFile(serverUrlHash: string, filename: string): Promise { + try { + const filePath = getConfigFilePath(serverUrlHash, filename) + await fs.unlink(filePath) + } catch (error) { + // Ignore if file doesn't exist + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { + console.error(`Error deleting ${filename}:`, error) + } + } +} + /** * Reads a JSON file and parses it with the provided schema * @param serverUrlHash The hash of the server URL * @param filename The name of the file to read * @param schema The schema to validate against + * @param clean Whether to clean (delete) before reading * @returns The parsed file content or undefined if the file doesn't exist */ export async function readJsonFile( serverUrlHash: string, filename: string, - schema: any + schema: any, + clean: boolean = false ): Promise { try { await ensureConfigDir() - const configDir = getConfigDir() - const filePath = path.join(configDir, `${serverUrlHash}_${filename}`) + + // If clean flag is set, delete the file before trying to read it + if (clean) { + await deleteConfigFile(serverUrlHash, filename) + return undefined + } + + const filePath = getConfigFilePath(serverUrlHash, filename) const content = await fs.readFile(filePath, 'utf-8') return await schema.parseAsync(JSON.parse(content)) } catch (error) { @@ -92,8 +148,7 @@ export async function writeJsonFile( ): Promise { try { await ensureConfigDir() - const configDir = getConfigDir() - const filePath = path.join(configDir, `${serverUrlHash}_${filename}`) + const filePath = getConfigFilePath(serverUrlHash, filename) await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8') } catch (error) { console.error(`Error writing ${filename}:`, error) @@ -106,17 +161,25 @@ export async function writeJsonFile( * @param serverUrlHash The hash of the server URL * @param filename The name of the file to read * @param errorMessage Optional custom error message + * @param clean Whether to clean (delete) before reading * @returns The file content as a string */ export async function readTextFile( serverUrlHash: string, filename: string, - errorMessage?: string + errorMessage?: string, + clean: boolean = false ): Promise { try { await ensureConfigDir() - const configDir = getConfigDir() - const filePath = path.join(configDir, `${serverUrlHash}_${filename}`) + + // If clean flag is set, delete the file before trying to read it + if (clean) { + await deleteConfigFile(serverUrlHash, filename) + throw new Error('File deleted due to clean flag') + } + + const filePath = getConfigFilePath(serverUrlHash, filename) return await fs.readFile(filePath, 'utf-8') } catch (error) { throw new Error(errorMessage || `Error reading ${filename}`) @@ -136,8 +199,7 @@ export async function writeTextFile( ): Promise { try { await ensureConfigDir() - const configDir = getConfigDir() - const filePath = path.join(configDir, `${serverUrlHash}_${filename}`) + const filePath = getConfigFilePath(serverUrlHash, filename) await fs.writeFile(filePath, text, 'utf-8') } catch (error) { console.error(`Error writing ${filename}:`, error) diff --git a/src/lib/node-oauth-client-provider.ts b/src/lib/node-oauth-client-provider.ts index c97743e..ed6caed 100644 --- a/src/lib/node-oauth-client-provider.ts +++ b/src/lib/node-oauth-client-provider.ts @@ -14,6 +14,7 @@ import { writeJsonFile, readTextFile, writeTextFile, + cleanServerConfig, } from './mcp-auth-config' /** @@ -35,6 +36,13 @@ 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) + }) + } } get redirectUrl(): string { @@ -60,7 +68,8 @@ export class NodeOAuthClientProvider implements OAuthClientProvider { return readJsonFile( this.serverUrlHash, 'client_info.json', - OAuthClientInformationSchema + OAuthClientInformationSchema, + this.options.clean ) } @@ -77,7 +86,12 @@ export class NodeOAuthClientProvider implements OAuthClientProvider { * @returns The OAuth tokens or undefined */ async tokens(): Promise { - return readJsonFile(this.serverUrlHash, 'tokens.json', OAuthTokensSchema) + return readJsonFile( + this.serverUrlHash, + 'tokens.json', + OAuthTokensSchema, + this.options.clean + ) } /** @@ -118,7 +132,8 @@ export class NodeOAuthClientProvider implements OAuthClientProvider { return await readTextFile( this.serverUrlHash, 'code_verifier.txt', - 'No code verifier saved for session' + 'No code verifier saved for session', + this.options.clean ) } } \ No newline at end of file diff --git a/src/lib/types.ts b/src/lib/types.ts index 188fccb..e719905 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -16,6 +16,8 @@ export interface OAuthProviderOptions { clientName?: string /** Client URI to use for OAuth registration */ clientUri?: string + /** Whether to clean stored configuration before reading */ + clean?: boolean } /** diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 33e8685..e9b4bf3 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -185,9 +185,18 @@ export async function findAvailablePort(preferredPort?: number): Promise * @param args Command line arguments * @param defaultPort Default port for the callback server if specified port is unavailable * @param usage Usage message to show on error - * @returns A promise that resolves to an object with parsed serverUrl and callbackPort + * @returns A promise that resolves to an object with parsed serverUrl, callbackPort, and clean flag */ export async function parseCommandLineArgs(args: string[], defaultPort: number, usage: string) { + // Check for --clean flag + const cleanIndex = args.indexOf('--clean') + const clean = cleanIndex !== -1 + + // Remove the flag from args if it exists + if (clean) { + args.splice(cleanIndex, 1) + } + const serverUrl = args[0] const specifiedPort = args[1] ? parseInt(args[1]) : undefined @@ -212,8 +221,12 @@ export async function parseCommandLineArgs(args: string[], defaultPort: number, } else { console.error(`Using automatically selected callback port: ${callbackPort}`) } + + if (clean) { + console.error('Clean mode enabled: config files will be reset before reading') + } - return { serverUrl, callbackPort } + return { serverUrl, callbackPort, clean } } /** diff --git a/src/proxy.ts b/src/proxy.ts index 23eaafe..dff2e9e 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -4,7 +4,10 @@ * MCP Proxy with OAuth support * A bidirectional proxy between a local STDIO MCP server and a remote SSE server with OAuth authentication. * - * Run with: npx tsx proxy.ts https://example.remote/server [callback-port] + * Run with: npx tsx proxy.ts [--clean] https://example.remote/server [callback-port] + * + * Options: + * --clean: Deletes stored configuration before reading, ensuring a fresh session * * If callback-port is not specified, an available port will be automatically selected. */ @@ -17,7 +20,7 @@ import { NodeOAuthClientProvider } from './lib/node-oauth-client-provider' /** * Main function to run the proxy */ -async function runProxy(serverUrl: string, callbackPort: number) { +async function runProxy(serverUrl: string, callbackPort: number, clean: boolean = false) { // Set up event emitter for auth flow const events = new EventEmitter() @@ -26,6 +29,7 @@ async function runProxy(serverUrl: string, callbackPort: number) { serverUrl, callbackPort, clientName: 'MCP CLI Proxy', + clean, }) // Create the STDIO transport for local connections @@ -91,9 +95,9 @@ to the CA certificate file. If using claude_desktop_config.json, this might look } // Parse command-line arguments and run the proxy -parseCommandLineArgs(process.argv.slice(2), 3334, 'Usage: npx tsx proxy.ts [callback-port]') - .then(({ serverUrl, callbackPort }) => { - return runProxy(serverUrl, callbackPort) +parseCommandLineArgs(process.argv.slice(2), 3334, 'Usage: npx tsx proxy.ts [--clean] [callback-port]') + .then(({ serverUrl, callbackPort, clean }) => { + return runProxy(serverUrl, callbackPort, clean) }) .catch((error) => { console.error('Fatal error:', error)