adding the --clean flag

This commit is contained in:
Glen Maddern 2025-03-31 15:02:27 +11:00
parent a32681e154
commit 027007030e
6 changed files with 125 additions and 25 deletions

View file

@ -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 <https://server-url> [callback-port]')
.then(({ serverUrl, callbackPort }) => {
return runClient(serverUrl, callbackPort)
parseCommandLineArgs(process.argv.slice(2), 3333, 'Usage: npx tsx client.ts [--clean] <https://server-url> [callback-port]')
.then(({ serverUrl, callbackPort, clean }) => {
return runClient(serverUrl, callbackPort, clean)
})
.catch((error) => {
console.error('Fatal error:', error)

View file

@ -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<void> {
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<void> {
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<T>(
serverUrlHash: string,
filename: string,
schema: any
schema: any,
clean: boolean = false
): Promise<T | undefined> {
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<void> {
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<string> {
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<void> {
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)

View file

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

View file

@ -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
}
/**

View file

@ -185,9 +185,18 @@ export async function findAvailablePort(preferredPort?: number): Promise<number>
* @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
@ -213,7 +222,11 @@ export async function parseCommandLineArgs(args: string[], defaultPort: number,
console.error(`Using automatically selected callback port: ${callbackPort}`)
}
return { serverUrl, callbackPort }
if (clean) {
console.error('Clean mode enabled: config files will be reset before reading')
}
return { serverUrl, callbackPort, clean }
}
/**

View file

@ -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 <https://server-url> [callback-port]')
.then(({ serverUrl, callbackPort }) => {
return runProxy(serverUrl, callbackPort)
parseCommandLineArgs(process.argv.slice(2), 3334, 'Usage: npx tsx proxy.ts [--clean] <https://server-url> [callback-port]')
.then(({ serverUrl, callbackPort, clean }) => {
return runProxy(serverUrl, callbackPort, clean)
})
.catch((error) => {
console.error('Fatal error:', error)