Compare commits
2 commits
main
...
remove-cle
Author | SHA1 | Date | |
---|---|---|---|
|
aa91270ddc | ||
|
e5d0ac8fe9 |
8 changed files with 20 additions and 101 deletions
12
README.md
12
README.md
|
@ -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.
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "mcp-remote",
|
"name": "mcp-remote",
|
||||||
"version": "0.0.18",
|
"version": "0.0.19",
|
||||||
"description": "Remote proxy for Model Context Protocol, allowing local-only clients to connect to remote servers using oAuth",
|
"description": "Remote proxy for Model Context Protocol, allowing local-only clients to connect to remote servers using oAuth",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"mcp",
|
"mcp",
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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 }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
14
src/proxy.ts
14
src/proxy.ts
|
@ -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)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue