From a32681e154d7d443ed6a154edffdea8d0ab217b6 Mon Sep 17 00:00:00 2001 From: Glen Maddern Date: Mon, 31 Mar 2025 14:42:19 +1100 Subject: [PATCH] extracted the config directory access --- src/lib/mcp-auth-config.ts | 146 ++++++++++++++++++++++++++ src/lib/node-oauth-client-provider.ts | 121 ++++----------------- 2 files changed, 169 insertions(+), 98 deletions(-) create mode 100644 src/lib/mcp-auth-config.ts diff --git a/src/lib/mcp-auth-config.ts b/src/lib/mcp-auth-config.ts new file mode 100644 index 0000000..8f06d41 --- /dev/null +++ b/src/lib/mcp-auth-config.ts @@ -0,0 +1,146 @@ +import crypto from 'crypto' +import path from 'path' +import os from 'os' +import fs from 'fs/promises' + +/** + * MCP Remote Authentication Configuration + * + * This module handles the storage and retrieval of authentication-related data for MCP Remote. + * + * Configuration directory structure: + * - 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 + * + * Files stored in the config directory: + * - {server_hash}_client_info.json: Contains OAuth client registration information + * - Format: OAuthClientInformation object with client_id and other registration details + * - {server_hash}_tokens.json: Contains OAuth access and refresh tokens + * - 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 + * - Format: Plain text string used for PKCE verification + * + * All JSON files are stored with 2-space indentation for readability. + */ + +/** + * Gets the configuration directory path + * @returns The path to the configuration directory + */ +export function getConfigDir(): string { + return process.env.MCP_REMOTE_CONFIG_DIR || path.join(os.homedir(), '.mcp-auth') +} + +/** + * Ensures the configuration directory exists + */ +export async function ensureConfigDir(): Promise { + try { + const configDir = getConfigDir() + await fs.mkdir(configDir, { recursive: true }) + } catch (error) { + console.error('Error creating config directory:', error) + throw error + } +} + +/** + * Generates a hash for the server URL to use in filenames + * @param serverUrl The server URL to hash + * @returns The hashed server URL + */ +export function getServerUrlHash(serverUrl: string): string { + return crypto.createHash('md5').update(serverUrl).digest('hex') +} + +/** + * Reads a JSON file and parses it with the provided schema + * @param serverUrlHash The hash of the server URL + * @param filename The name of the file to read + * @param schema The schema to validate against + * @returns The parsed file content or undefined if the file doesn't exist + */ +export async function readJsonFile( + serverUrlHash: string, + filename: string, + schema: any +): Promise { + try { + await ensureConfigDir() + const configDir = getConfigDir() + const filePath = path.join(configDir, `${serverUrlHash}_${filename}`) + const content = await fs.readFile(filePath, 'utf-8') + return await schema.parseAsync(JSON.parse(content)) + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + return undefined + } + return undefined + } +} + +/** + * Writes a JSON object to a file + * @param serverUrlHash The hash of the server URL + * @param filename The name of the file to write + * @param data The data to write + */ +export async function writeJsonFile( + serverUrlHash: string, + filename: string, + data: any +): Promise { + try { + await ensureConfigDir() + const configDir = getConfigDir() + const filePath = path.join(configDir, `${serverUrlHash}_${filename}`) + await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8') + } catch (error) { + console.error(`Error writing ${filename}:`, error) + throw error + } +} + +/** + * Reads a text file + * @param serverUrlHash The hash of the server URL + * @param filename The name of the file to read + * @param errorMessage Optional custom error message + * @returns The file content as a string + */ +export async function readTextFile( + serverUrlHash: string, + filename: string, + errorMessage?: string +): Promise { + try { + await ensureConfigDir() + const configDir = getConfigDir() + const filePath = path.join(configDir, `${serverUrlHash}_${filename}`) + return await fs.readFile(filePath, 'utf-8') + } catch (error) { + throw new Error(errorMessage || `Error reading ${filename}`) + } +} + +/** + * Writes a text string to a file + * @param serverUrlHash The hash of the server URL + * @param filename The name of the file to write + * @param text The text to write + */ +export async function writeTextFile( + serverUrlHash: string, + filename: string, + text: string +): Promise { + try { + await ensureConfigDir() + const configDir = getConfigDir() + const filePath = path.join(configDir, `${serverUrlHash}_${filename}`) + await fs.writeFile(filePath, text, 'utf-8') + } catch (error) { + console.error(`Error writing ${filename}:`, error) + throw error + } +} \ No newline at end of file diff --git a/src/lib/node-oauth-client-provider.ts b/src/lib/node-oauth-client-provider.ts index 920b8e5..c97743e 100644 --- a/src/lib/node-oauth-client-provider.ts +++ b/src/lib/node-oauth-client-provider.ts @@ -1,7 +1,3 @@ -import crypto from 'crypto' -import path from 'path' -import os from 'os' -import fs from 'fs/promises' import open from 'open' import { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js' import { @@ -12,13 +8,19 @@ import { OAuthTokensSchema, } from '@modelcontextprotocol/sdk/shared/auth.js' import type { OAuthProviderOptions } from './types' +import { + getServerUrlHash, + readJsonFile, + writeJsonFile, + readTextFile, + writeTextFile, +} from './mcp-auth-config' /** * Implements the OAuthClientProvider interface for Node.js environments. * Handles OAuth flow and token storage for MCP clients. */ export class NodeOAuthClientProvider implements OAuthClientProvider { - private configDir: string private serverUrlHash: string private callbackPath: string private clientName: string @@ -29,8 +31,7 @@ export class NodeOAuthClientProvider implements OAuthClientProvider { * @param options Configuration options for the provider */ constructor(readonly options: OAuthProviderOptions) { - this.serverUrlHash = crypto.createHash('md5').update(options.serverUrl).digest('hex') - this.configDir = options.configDir || path.join(os.homedir(), '.mcp-auth') + this.serverUrlHash = getServerUrlHash(options.serverUrl) this.callbackPath = options.callbackPath || '/oauth/callback' this.clientName = options.clientName || 'MCP CLI Client' this.clientUri = options.clientUri || 'https://github.com/modelcontextprotocol/mcp-cli' @@ -51,96 +52,16 @@ export class NodeOAuthClientProvider implements OAuthClientProvider { } } - /** - * Ensures the configuration directory exists - * @private - */ - private async ensureConfigDir() { - try { - await fs.mkdir(this.configDir, { recursive: true }) - } catch (error) { - console.error('Error creating config directory:', error) - throw error - } - } - - /** - * Reads a JSON file and parses it with the provided schema - * @param filename The name of the file to read - * @param schema The schema to validate against - * @returns The parsed file content or undefined if the file doesn't exist - * @private - */ - private async readFile(filename: string, schema: any): Promise { - try { - await this.ensureConfigDir() - const filePath = path.join(this.configDir, `${this.serverUrlHash}_${filename}`) - const content = await fs.readFile(filePath, 'utf-8') - return await schema.parseAsync(JSON.parse(content)) - } catch (error) { - if ((error as NodeJS.ErrnoException).code === 'ENOENT') { - return undefined - } - return undefined - } - } - - /** - * Writes a JSON object to a file - * @param filename The name of the file to write - * @param data The data to write - * @private - */ - private async writeFile(filename: string, data: any) { - try { - await this.ensureConfigDir() - const filePath = path.join(this.configDir, `${this.serverUrlHash}_${filename}`) - await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8') - } catch (error) { - console.error(`Error writing ${filename}:`, error) - throw error - } - } - - /** - * Writes a text string to a file - * @param filename The name of the file to write - * @param text The text to write - * @private - */ - private async writeTextFile(filename: string, text: string) { - try { - await this.ensureConfigDir() - const filePath = path.join(this.configDir, `${this.serverUrlHash}_${filename}`) - await fs.writeFile(filePath, text, 'utf-8') - } catch (error) { - console.error(`Error writing ${filename}:`, error) - throw error - } - } - - /** - * Reads text from a file - * @param filename The name of the file to read - * @returns The file content as a string - * @private - */ - private async readTextFile(filename: string): Promise { - try { - await this.ensureConfigDir() - const filePath = path.join(this.configDir, `${this.serverUrlHash}_${filename}`) - return await fs.readFile(filePath, 'utf-8') - } catch (error) { - throw new Error('No code verifier saved for session') - } - } - /** * Gets the client information if it exists * @returns The client information or undefined */ async clientInformation(): Promise { - return this.readFile('client_info.json', OAuthClientInformationSchema) + return readJsonFile( + this.serverUrlHash, + 'client_info.json', + OAuthClientInformationSchema + ) } /** @@ -148,7 +69,7 @@ export class NodeOAuthClientProvider implements OAuthClientProvider { * @param clientInformation The client information to save */ async saveClientInformation(clientInformation: OAuthClientInformationFull): Promise { - await this.writeFile('client_info.json', clientInformation) + await writeJsonFile(this.serverUrlHash, 'client_info.json', clientInformation) } /** @@ -156,7 +77,7 @@ export class NodeOAuthClientProvider implements OAuthClientProvider { * @returns The OAuth tokens or undefined */ async tokens(): Promise { - return this.readFile('tokens.json', OAuthTokensSchema) + return readJsonFile(this.serverUrlHash, 'tokens.json', OAuthTokensSchema) } /** @@ -164,7 +85,7 @@ export class NodeOAuthClientProvider implements OAuthClientProvider { * @param tokens The tokens to save */ async saveTokens(tokens: OAuthTokens): Promise { - await this.writeFile('tokens.json', tokens) + await writeJsonFile(this.serverUrlHash, 'tokens.json', tokens) } /** @@ -186,7 +107,7 @@ export class NodeOAuthClientProvider implements OAuthClientProvider { * @param codeVerifier The code verifier to save */ async saveCodeVerifier(codeVerifier: string): Promise { - await this.writeTextFile('code_verifier.txt', codeVerifier) + await writeTextFile(this.serverUrlHash, 'code_verifier.txt', codeVerifier) } /** @@ -194,6 +115,10 @@ export class NodeOAuthClientProvider implements OAuthClientProvider { * @returns The code verifier */ async codeVerifier(): Promise { - return await this.readTextFile('code_verifier.txt') + return await readTextFile( + this.serverUrlHash, + 'code_verifier.txt', + 'No code verifier saved for session' + ) } -} +} \ No newline at end of file