Versioning storage based on mcp-auth version number to let us iterate on storage format
This commit is contained in:
parent
a97fd9e5c6
commit
1382827ebd
3 changed files with 28 additions and 35 deletions
|
@ -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: {},
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue