adding the --clean flag
This commit is contained in:
parent
a32681e154
commit
027007030e
6 changed files with 125 additions and 25 deletions
|
@ -4,7 +4,10 @@
|
||||||
* MCP Client with OAuth support
|
* MCP Client with OAuth support
|
||||||
* A command-line client that connects to an MCP server using SSE with OAuth authentication.
|
* 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.
|
* 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
|
* 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
|
// Set up event emitter for auth flow
|
||||||
const events = new EventEmitter()
|
const events = new EventEmitter()
|
||||||
|
|
||||||
|
@ -29,6 +32,7 @@ async function runClient(serverUrl: string, callbackPort: number) {
|
||||||
serverUrl,
|
serverUrl,
|
||||||
callbackPort,
|
callbackPort,
|
||||||
clientName: 'MCP CLI Client',
|
clientName: 'MCP CLI Client',
|
||||||
|
clean,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Create the client
|
// Create the client
|
||||||
|
@ -147,9 +151,9 @@ async function runClient(serverUrl: string, callbackPort: number) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse command-line arguments and run the client
|
// Parse command-line arguments and run the client
|
||||||
parseCommandLineArgs(process.argv.slice(2), 3333, 'Usage: npx tsx client.ts <https://server-url> [callback-port]')
|
parseCommandLineArgs(process.argv.slice(2), 3333, 'Usage: npx tsx client.ts [--clean] <https://server-url> [callback-port]')
|
||||||
.then(({ serverUrl, callbackPort }) => {
|
.then(({ serverUrl, callbackPort, clean }) => {
|
||||||
return runClient(serverUrl, callbackPort)
|
return runClient(serverUrl, callbackPort, clean)
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Fatal error:', error)
|
console.error('Fatal error:', error)
|
||||||
|
|
|
@ -23,6 +23,26 @@ import fs from 'fs/promises'
|
||||||
* All JSON files are stored with 2-space indentation for readability.
|
* 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
|
* Gets the configuration directory path
|
||||||
* @returns The path to the configuration directory
|
* @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')
|
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
|
* Reads a JSON file and parses it with the provided schema
|
||||||
* @param serverUrlHash The hash of the server URL
|
* @param serverUrlHash The hash of the server URL
|
||||||
* @param filename The name of the file to read
|
* @param filename The name of the file to read
|
||||||
* @param schema The schema to validate against
|
* @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
|
* @returns The parsed file content or undefined if the file doesn't exist
|
||||||
*/
|
*/
|
||||||
export async function readJsonFile<T>(
|
export async function readJsonFile<T>(
|
||||||
serverUrlHash: string,
|
serverUrlHash: string,
|
||||||
filename: string,
|
filename: string,
|
||||||
schema: any
|
schema: any,
|
||||||
|
clean: boolean = false
|
||||||
): Promise<T | undefined> {
|
): Promise<T | undefined> {
|
||||||
try {
|
try {
|
||||||
await ensureConfigDir()
|
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')
|
const content = await fs.readFile(filePath, 'utf-8')
|
||||||
return await schema.parseAsync(JSON.parse(content))
|
return await schema.parseAsync(JSON.parse(content))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -92,8 +148,7 @@ export async function writeJsonFile(
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await ensureConfigDir()
|
await ensureConfigDir()
|
||||||
const configDir = getConfigDir()
|
const filePath = getConfigFilePath(serverUrlHash, filename)
|
||||||
const filePath = path.join(configDir, `${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)
|
console.error(`Error writing ${filename}:`, error)
|
||||||
|
@ -106,17 +161,25 @@ export async function writeJsonFile(
|
||||||
* @param serverUrlHash The hash of the server URL
|
* @param serverUrlHash The hash of the server URL
|
||||||
* @param filename The name of the file to read
|
* @param filename The name of the file to read
|
||||||
* @param errorMessage Optional custom error message
|
* @param errorMessage Optional custom error message
|
||||||
|
* @param clean Whether to clean (delete) before reading
|
||||||
* @returns The file content as a string
|
* @returns The file content as a string
|
||||||
*/
|
*/
|
||||||
export async function readTextFile(
|
export async function readTextFile(
|
||||||
serverUrlHash: string,
|
serverUrlHash: string,
|
||||||
filename: string,
|
filename: string,
|
||||||
errorMessage?: string
|
errorMessage?: string,
|
||||||
|
clean: boolean = false
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
try {
|
try {
|
||||||
await ensureConfigDir()
|
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')
|
return await fs.readFile(filePath, 'utf-8')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(errorMessage || `Error reading ${filename}`)
|
throw new Error(errorMessage || `Error reading ${filename}`)
|
||||||
|
@ -136,8 +199,7 @@ export async function writeTextFile(
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await ensureConfigDir()
|
await ensureConfigDir()
|
||||||
const configDir = getConfigDir()
|
const filePath = getConfigFilePath(serverUrlHash, filename)
|
||||||
const filePath = path.join(configDir, `${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)
|
console.error(`Error writing ${filename}:`, error)
|
||||||
|
|
|
@ -14,6 +14,7 @@ import {
|
||||||
writeJsonFile,
|
writeJsonFile,
|
||||||
readTextFile,
|
readTextFile,
|
||||||
writeTextFile,
|
writeTextFile,
|
||||||
|
cleanServerConfig,
|
||||||
} from './mcp-auth-config'
|
} from './mcp-auth-config'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -35,6 +36,13 @@ export class NodeOAuthClientProvider implements OAuthClientProvider {
|
||||||
this.callbackPath = options.callbackPath || '/oauth/callback'
|
this.callbackPath = options.callbackPath || '/oauth/callback'
|
||||||
this.clientName = options.clientName || 'MCP CLI Client'
|
this.clientName = options.clientName || 'MCP CLI Client'
|
||||||
this.clientUri = options.clientUri || 'https://github.com/modelcontextprotocol/mcp-cli'
|
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 {
|
get redirectUrl(): string {
|
||||||
|
@ -60,7 +68,8 @@ export class NodeOAuthClientProvider implements OAuthClientProvider {
|
||||||
return readJsonFile<OAuthClientInformation>(
|
return readJsonFile<OAuthClientInformation>(
|
||||||
this.serverUrlHash,
|
this.serverUrlHash,
|
||||||
'client_info.json',
|
'client_info.json',
|
||||||
OAuthClientInformationSchema
|
OAuthClientInformationSchema,
|
||||||
|
this.options.clean
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,7 +86,12 @@ 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>(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(
|
return await readTextFile(
|
||||||
this.serverUrlHash,
|
this.serverUrlHash,
|
||||||
'code_verifier.txt',
|
'code_verifier.txt',
|
||||||
'No code verifier saved for session'
|
'No code verifier saved for session',
|
||||||
|
this.options.clean
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -16,6 +16,8 @@ export interface OAuthProviderOptions {
|
||||||
clientName?: string
|
clientName?: string
|
||||||
/** Client URI to use for OAuth registration */
|
/** Client URI to use for OAuth registration */
|
||||||
clientUri?: string
|
clientUri?: string
|
||||||
|
/** Whether to clean stored configuration before reading */
|
||||||
|
clean?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -185,9 +185,18 @@ export async function findAvailablePort(preferredPort?: number): Promise<number>
|
||||||
* @param args Command line arguments
|
* @param args Command line arguments
|
||||||
* @param defaultPort Default port for the callback server if specified port is unavailable
|
* @param defaultPort Default port for the callback server if specified port is unavailable
|
||||||
* @param usage Usage message to show on error
|
* @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) {
|
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 serverUrl = args[0]
|
||||||
const specifiedPort = args[1] ? parseInt(args[1]) : undefined
|
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}`)
|
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 }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
14
src/proxy.ts
14
src/proxy.ts
|
@ -4,7 +4,10 @@
|
||||||
* MCP Proxy with OAuth support
|
* MCP Proxy with OAuth support
|
||||||
* A bidirectional proxy between a local STDIO MCP server and a remote SSE server with OAuth authentication.
|
* 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.
|
* 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
|
* 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
|
// Set up event emitter for auth flow
|
||||||
const events = new EventEmitter()
|
const events = new EventEmitter()
|
||||||
|
|
||||||
|
@ -26,6 +29,7 @@ async function runProxy(serverUrl: string, callbackPort: number) {
|
||||||
serverUrl,
|
serverUrl,
|
||||||
callbackPort,
|
callbackPort,
|
||||||
clientName: 'MCP CLI Proxy',
|
clientName: 'MCP CLI Proxy',
|
||||||
|
clean,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Create the STDIO transport for local connections
|
// 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
|
// Parse command-line arguments and run the proxy
|
||||||
parseCommandLineArgs(process.argv.slice(2), 3334, 'Usage: npx tsx proxy.ts <https://server-url> [callback-port]')
|
parseCommandLineArgs(process.argv.slice(2), 3334, 'Usage: npx tsx proxy.ts [--clean] <https://server-url> [callback-port]')
|
||||||
.then(({ serverUrl, callbackPort }) => {
|
.then(({ serverUrl, callbackPort, clean }) => {
|
||||||
return runProxy(serverUrl, callbackPort)
|
return runProxy(serverUrl, callbackPort, clean)
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Fatal error:', error)
|
console.error('Fatal error:', error)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue