removing --clean flag as it sent things into a reauth loop

This commit is contained in:
Glen Maddern 2025-04-11 13:45:03 +10:00 committed by Glen Maddern
parent 14da1b44d4
commit c542f78288
7 changed files with 19 additions and 100 deletions

View file

@ -77,16 +77,6 @@ To bypass authentication, or to emit custom headers on all requests to your remo
] ]
``` ```
* To force `mcp-remote` to ignore any existing access tokens and begin the authorization flow anew, pass `--clean`.
```json
"args": [
"mcp-remote",
"https://remote.mcp.server/sse",
"--clean"
]
```
* To change which port `mcp-remote` listens for an OAuth redirect (by default `3334`), add an additional argument after the server URL. Note that whatever port you specify, if it is unavailable an open port will be chosen at random. * To change which port `mcp-remote` listens for an OAuth redirect (by default `3334`), add an additional argument after the server URL. Note that whatever port you specify, if it is unavailable an open port will be chosen at random.
```json ```json
@ -211,4 +201,4 @@ Run the following on the command line (not from an MCP server):
npx -p mcp-remote@latest mcp-remote-client https://remote.mcp.server/sse npx -p mcp-remote@latest mcp-remote-client https://remote.mcp.server/sse
``` ```
This will run through the entire authorization flow and attempt to list the tools & resources at the remote URL. Pair this with `--clean` or after running `rm -rf ~/.mcp-auth` to see if stale credentials are your problem, otherwise hopefully the issue will be more obvious in these logs than those in your MCP client. This will run through the entire authorization flow and attempt to list the tools & resources at the remote URL. Try this after running `rm -rf ~/.mcp-auth` to see if stale credentials are your problem, otherwise hopefully the issue will be more obvious in these logs than those in your MCP client.

View file

@ -4,10 +4,7 @@
* 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 [--clean] https://example.remote/server [callback-port] * Run with: npx tsx client.ts 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.
*/ */
@ -24,7 +21,7 @@ import { coordinateAuth } from './lib/coordination'
/** /**
* Main function to run the client * Main function to run the client
*/ */
async function runClient(serverUrl: string, callbackPort: number, headers: Record<string, string>, clean: boolean = false) { async function runClient(serverUrl: string, callbackPort: number, headers: Record<string, string>) {
// Set up event emitter for auth flow // Set up event emitter for auth flow
const events = new EventEmitter() const events = new EventEmitter()
@ -39,7 +36,6 @@ async function runClient(serverUrl: string, callbackPort: number, headers: Recor
serverUrl, serverUrl,
callbackPort, callbackPort,
clientName: 'MCP CLI Client', clientName: 'MCP CLI Client',
clean,
}) })
// If auth was completed by another instance, just log that we'll use the auth from disk // If auth was completed by another instance, just log that we'll use the auth from disk
@ -159,9 +155,9 @@ async function runClient(serverUrl: string, callbackPort: number, headers: Recor
} }
// 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 [--clean] <https://server-url> [callback-port]') parseCommandLineArgs(process.argv.slice(2), 3333, 'Usage: npx tsx client.ts <https://server-url> [callback-port]')
.then(({ serverUrl, callbackPort, clean, headers }) => { .then(({ serverUrl, callbackPort, headers }) => {
return runClient(serverUrl, callbackPort, headers, clean) return runClient(serverUrl, callbackPort, headers)
}) })
.catch((error) => { .catch((error) => {
console.error('Fatal error:', error) console.error('Fatal error:', error)

View file

@ -23,11 +23,6 @@ import { log, MCP_REMOTE_VERSION } from './utils'
* 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', 'lock.json']
/** /**
* Lockfile data structure * Lockfile data structure
*/ */
@ -82,17 +77,6 @@ export async function deleteLockfile(serverUrlHash: string): Promise<void> {
await deleteConfigFile(serverUrlHash, 'lock.json') await deleteConfigFile(serverUrlHash, 'lock.json')
} }
/**
* 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> {
log(`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
@ -149,24 +133,12 @@ export async function deleteConfigFile(serverUrlHash: string, filename: string):
* @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, filename: string, schema: any): Promise<T | undefined> {
serverUrlHash: string,
filename: string,
schema: any,
clean: boolean = false,
): Promise<T | undefined> {
try { try {
await ensureConfigDir() await ensureConfigDir()
// 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 filePath = getConfigFilePath(serverUrlHash, filename)
const content = await fs.readFile(filePath, 'utf-8') const content = await fs.readFile(filePath, 'utf-8')
const result = await schema.parseAsync(JSON.parse(content)) const result = await schema.parseAsync(JSON.parse(content))
@ -204,24 +176,11 @@ export async function writeJsonFile(serverUrlHash: string, filename: string, dat
* @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, filename: string, errorMessage?: string): Promise<string> {
serverUrlHash: string,
filename: string,
errorMessage?: string,
clean: boolean = false,
): Promise<string> {
try { try {
await ensureConfigDir() await ensureConfigDir()
// 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) const filePath = getConfigFilePath(serverUrlHash, filename)
return await fs.readFile(filePath, 'utf-8') return await fs.readFile(filePath, 'utf-8')
} catch (error) { } catch (error) {

View file

@ -8,7 +8,7 @@ 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 { readJsonFile, writeJsonFile, readTextFile, writeTextFile, cleanServerConfig } from './mcp-auth-config' import { readJsonFile, writeJsonFile, readTextFile, writeTextFile } from './mcp-auth-config'
import { getServerUrlHash, log } from './utils' import { getServerUrlHash, log } from './utils'
/** /**
@ -30,13 +30,6 @@ 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) => {
log('Error cleaning server config:', err)
})
}
} }
get redirectUrl(): string { get redirectUrl(): string {
@ -60,7 +53,7 @@ export class NodeOAuthClientProvider implements OAuthClientProvider {
*/ */
async clientInformation(): Promise<OAuthClientInformation | undefined> { async clientInformation(): Promise<OAuthClientInformation | undefined> {
// log('Reading client info') // log('Reading client info')
return readJsonFile<OAuthClientInformation>(this.serverUrlHash, 'client_info.json', OAuthClientInformationSchema, this.options.clean) return readJsonFile<OAuthClientInformation>(this.serverUrlHash, 'client_info.json', OAuthClientInformationSchema)
} }
/** /**
@ -79,7 +72,7 @@ export class NodeOAuthClientProvider implements OAuthClientProvider {
async tokens(): Promise<OAuthTokens | undefined> { async tokens(): Promise<OAuthTokens | undefined> {
// log('Reading tokens') // log('Reading tokens')
// console.log(new Error().stack) // console.log(new Error().stack)
return readJsonFile<OAuthTokens>(this.serverUrlHash, 'tokens.json', OAuthTokensSchema, this.options.clean) return readJsonFile<OAuthTokens>(this.serverUrlHash, 'tokens.json', OAuthTokensSchema)
} }
/** /**
@ -120,6 +113,6 @@ export class NodeOAuthClientProvider implements OAuthClientProvider {
*/ */
async codeVerifier(): Promise<string> { async codeVerifier(): Promise<string> {
// log('Reading code verifier') // log('Reading code verifier')
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')
} }
} }

View file

@ -16,8 +16,6 @@ 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
} }
/** /**

View file

@ -275,18 +275,9 @@ 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, callbackPort, clean flag, and headers * @returns A promise that resolves to an object with parsed serverUrl, callbackPort and headers
*/ */
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)
}
// Process headers // Process headers
const headers: Record<string, string> = {} const headers: Record<string, string> = {}
args.forEach((arg, i) => { args.forEach((arg, i) => {
@ -327,10 +318,6 @@ export async function parseCommandLineArgs(args: string[], defaultPort: number,
log(`Using automatically selected callback port: ${callbackPort}`) log(`Using automatically selected callback port: ${callbackPort}`)
} }
if (clean) {
log('Clean mode enabled: config files will be reset before reading')
}
if (Object.keys(headers).length > 0) { if (Object.keys(headers).length > 0) {
log(`Using custom headers: ${JSON.stringify(headers)}`) log(`Using custom headers: ${JSON.stringify(headers)}`)
} }
@ -350,7 +337,7 @@ export async function parseCommandLineArgs(args: string[], defaultPort: number,
}) })
} }
return { serverUrl, callbackPort, clean, headers } return { serverUrl, callbackPort, headers }
} }
/** /**

View file

@ -4,10 +4,7 @@
* 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 [--clean] https://example.remote/server [callback-port] * Run with: npx tsx proxy.ts 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.
*/ */
@ -21,7 +18,7 @@ import { coordinateAuth } from './lib/coordination'
/** /**
* Main function to run the proxy * Main function to run the proxy
*/ */
async function runProxy(serverUrl: string, callbackPort: number, headers: Record<string, string>, clean: boolean = false) { async function runProxy(serverUrl: string, callbackPort: number, headers: Record<string, string>) {
// Set up event emitter for auth flow // Set up event emitter for auth flow
const events = new EventEmitter() const events = new EventEmitter()
@ -36,7 +33,6 @@ async function runProxy(serverUrl: string, callbackPort: number, headers: Record
serverUrl, serverUrl,
callbackPort, callbackPort,
clientName: 'MCP CLI Proxy', clientName: 'MCP CLI Proxy',
clean,
}) })
// If auth was completed by another instance, just log that we'll use the auth from disk // If auth was completed by another instance, just log that we'll use the auth from disk
@ -103,9 +99,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 [--clean] <https://server-url> [callback-port]') parseCommandLineArgs(process.argv.slice(2), 3334, 'Usage: npx tsx proxy.ts <https://server-url> [callback-port]')
.then(({ serverUrl, callbackPort, clean, headers }) => { .then(({ serverUrl, callbackPort, headers }) => {
return runProxy(serverUrl, callbackPort, headers, clean) return runProxy(serverUrl, callbackPort, headers)
}) })
.catch((error) => { .catch((error) => {
log('Fatal error:', error) log('Fatal error:', error)