Versioning storage based on mcp-auth version number to let us iterate on storage format

This commit is contained in:
Glen Maddern 2025-03-31 16:21:34 +11:00
parent a97fd9e5c6
commit 1382827ebd
3 changed files with 28 additions and 35 deletions

View file

@ -18,7 +18,7 @@ import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'
import { ListResourcesResultSchema, ListToolsResultSchema } from '@modelcontextprotocol/sdk/types.js' import { ListResourcesResultSchema, ListToolsResultSchema } from '@modelcontextprotocol/sdk/types.js'
import { UnauthorizedError } from '@modelcontextprotocol/sdk/client/auth.js' import { UnauthorizedError } from '@modelcontextprotocol/sdk/client/auth.js'
import { NodeOAuthClientProvider } from './lib/node-oauth-client-provider' import { NodeOAuthClientProvider } from './lib/node-oauth-client-provider'
import { parseCommandLineArgs, setupOAuthCallbackServer, setupSignalHandlers } from './lib/utils' import { parseCommandLineArgs, setupOAuthCallbackServer, setupSignalHandlers, MCP_REMOTE_VERSION } from './lib/utils'
/** /**
* Main function to run the client * Main function to run the client
@ -39,7 +39,7 @@ async function runClient(serverUrl: string, callbackPort: number, clean: boolean
const client = new Client( const client = new Client(
{ {
name: 'mcp-remote', name: 'mcp-remote',
version: require('../package.json').version, version: MCP_REMOTE_VERSION,
}, },
{ {
capabilities: {}, capabilities: {},

View file

@ -2,16 +2,17 @@ import crypto from 'crypto'
import path from 'path' import path from 'path'
import os from 'os' import os from 'os'
import fs from 'fs/promises' import fs from 'fs/promises'
import { MCP_REMOTE_VERSION } from './utils'
/** /**
* MCP Remote Authentication Configuration * MCP Remote Authentication Configuration
* *
* This module handles the storage and retrieval of authentication-related data for MCP Remote. * This module handles the storage and retrieval of authentication-related data for MCP Remote.
* *
* Configuration directory structure: * Configuration directory structure:
* - The config directory is determined by MCP_REMOTE_CONFIG_DIR env var or defaults to ~/.mcp-auth * - The config directory is determined by MCP_REMOTE_CONFIG_DIR env var or defaults to ~/.mcp-auth
* - Each file is prefixed with a hash of the server URL to separate configurations for different servers * - Each file is prefixed with a hash of the server URL to separate configurations for different servers
* *
* Files stored in the config directory: * Files stored in the config directory:
* - {server_hash}_client_info.json: Contains OAuth client registration information * - {server_hash}_client_info.json: Contains OAuth client registration information
* - Format: OAuthClientInformation object with client_id and other registration details * - Format: OAuthClientInformation object with client_id and other registration details
@ -19,18 +20,14 @@ import fs from 'fs/promises'
* - Format: OAuthTokens object with access_token, refresh_token, and expiration information * - Format: OAuthTokens object with access_token, refresh_token, and expiration information
* - {server_hash}_code_verifier.txt: Contains the PKCE code verifier for the current OAuth flow * - {server_hash}_code_verifier.txt: Contains the PKCE code verifier for the current OAuth flow
* - Format: Plain text string used for PKCE verification * - Format: Plain text string used for PKCE verification
* *
* 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 * Known configuration file names that might need to be cleaned
*/ */
export const knownConfigFiles = [ export const knownConfigFiles = ['client_info.json', 'tokens.json', 'code_verifier.txt']
'client_info.json',
'tokens.json',
'code_verifier.txt',
];
/** /**
* Deletes all known configuration files for a specific server * Deletes all known configuration files for a specific server
@ -48,7 +45,9 @@ export async function cleanServerConfig(serverUrlHash: string): Promise<void> {
* @returns The path to the configuration directory * @returns The path to the configuration directory
*/ */
export function getConfigDir(): string { export function getConfigDir(): string {
return process.env.MCP_REMOTE_CONFIG_DIR || path.join(os.homedir(), '.mcp-auth') const baseConfigDir = process.env.MCP_REMOTE_CONFIG_DIR || path.join(os.homedir(), '.mcp-auth')
// Add a version subdirectory so we don't need to worry about backwards/forwards compatibility yet
return path.join(baseConfigDir, `mcp-remote-${MCP_REMOTE_VERSION}`)
} }
/** /**
@ -110,20 +109,20 @@ export async function deleteConfigFile(serverUrlHash: string, filename: string):
* @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 clean: boolean = false,
): Promise<T | undefined> { ): Promise<T | undefined> {
try { try {
await ensureConfigDir() await ensureConfigDir()
// If clean flag is set, delete the file before trying to read it // If clean flag is set, delete the file before trying to read it
if (clean) { if (clean) {
await deleteConfigFile(serverUrlHash, filename) await deleteConfigFile(serverUrlHash, filename)
return undefined 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')
return await schema.parseAsync(JSON.parse(content)) return await schema.parseAsync(JSON.parse(content))
@ -141,11 +140,7 @@ export async function readJsonFile<T>(
* @param filename The name of the file to write * @param filename The name of the file to write
* @param data The data to write * @param data The data to write
*/ */
export async function writeJsonFile( export async function writeJsonFile(serverUrlHash: string, filename: string, data: any): Promise<void> {
serverUrlHash: string,
filename: string,
data: any
): Promise<void> {
try { try {
await ensureConfigDir() await ensureConfigDir()
const filePath = getConfigFilePath(serverUrlHash, filename) const filePath = getConfigFilePath(serverUrlHash, filename)
@ -165,20 +160,20 @@ export async function writeJsonFile(
* @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 clean: boolean = false,
): Promise<string> { ): Promise<string> {
try { try {
await ensureConfigDir() await ensureConfigDir()
// If clean flag is set, delete the file before trying to read it // If clean flag is set, delete the file before trying to read it
if (clean) { if (clean) {
await deleteConfigFile(serverUrlHash, filename) await deleteConfigFile(serverUrlHash, filename)
throw new Error('File deleted due to clean flag') 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) {
@ -192,11 +187,7 @@ export async function readTextFile(
* @param filename The name of the file to write * @param filename The name of the file to write
* @param text The text to write * @param text The text to write
*/ */
export async function writeTextFile( export async function writeTextFile(serverUrlHash: string, filename: string, text: string): Promise<void> {
serverUrlHash: string,
filename: string,
text: string
): Promise<void> {
try { try {
await ensureConfigDir() await ensureConfigDir()
const filePath = getConfigFilePath(serverUrlHash, filename) const filePath = getConfigFilePath(serverUrlHash, filename)
@ -205,4 +196,4 @@ export async function writeTextFile(
console.error(`Error writing ${filename}:`, error) console.error(`Error writing ${filename}:`, error)
throw error throw error
} }
} }

View file

@ -191,12 +191,12 @@ export async function parseCommandLineArgs(args: string[], defaultPort: number,
// Check for --clean flag // Check for --clean flag
const cleanIndex = args.indexOf('--clean') const cleanIndex = args.indexOf('--clean')
const clean = cleanIndex !== -1 const clean = cleanIndex !== -1
// Remove the flag from args if it exists // Remove the flag from args if it exists
if (clean) { if (clean) {
args.splice(cleanIndex, 1) 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
@ -221,7 +221,7 @@ export async function parseCommandLineArgs(args: string[], defaultPort: number,
} else { } else {
console.error(`Using automatically selected callback port: ${callbackPort}`) console.error(`Using automatically selected callback port: ${callbackPort}`)
} }
if (clean) { if (clean) {
console.error('Clean mode enabled: config files will be reset before reading') console.error('Clean mode enabled: config files will be reset before reading')
} }
@ -243,3 +243,5 @@ export function setupSignalHandlers(cleanup: () => Promise<void>) {
// Keep the process alive // Keep the process alive
process.stdin.resume() process.stdin.resume()
} }
export const MCP_REMOTE_VERSION = require('../../package.json').version