extracted the config directory access
This commit is contained in:
parent
1dd99edc0d
commit
a32681e154
2 changed files with 169 additions and 98 deletions
146
src/lib/mcp-auth-config.ts
Normal file
146
src/lib/mcp-auth-config.ts
Normal file
|
@ -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<void> {
|
||||||
|
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<T>(
|
||||||
|
serverUrlHash: string,
|
||||||
|
filename: string,
|
||||||
|
schema: any
|
||||||
|
): Promise<T | undefined> {
|
||||||
|
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<void> {
|
||||||
|
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<string> {
|
||||||
|
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<void> {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 open from 'open'
|
||||||
import { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js'
|
import { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js'
|
||||||
import {
|
import {
|
||||||
|
@ -12,13 +8,19 @@ 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 {
|
||||||
|
getServerUrlHash,
|
||||||
|
readJsonFile,
|
||||||
|
writeJsonFile,
|
||||||
|
readTextFile,
|
||||||
|
writeTextFile,
|
||||||
|
} from './mcp-auth-config'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements the OAuthClientProvider interface for Node.js environments.
|
* Implements the OAuthClientProvider interface for Node.js environments.
|
||||||
* Handles OAuth flow and token storage for MCP clients.
|
* Handles OAuth flow and token storage for MCP clients.
|
||||||
*/
|
*/
|
||||||
export class NodeOAuthClientProvider implements OAuthClientProvider {
|
export class NodeOAuthClientProvider implements OAuthClientProvider {
|
||||||
private configDir: string
|
|
||||||
private serverUrlHash: string
|
private serverUrlHash: string
|
||||||
private callbackPath: string
|
private callbackPath: string
|
||||||
private clientName: string
|
private clientName: string
|
||||||
|
@ -29,8 +31,7 @@ export class NodeOAuthClientProvider implements OAuthClientProvider {
|
||||||
* @param options Configuration options for the provider
|
* @param options Configuration options for the provider
|
||||||
*/
|
*/
|
||||||
constructor(readonly options: OAuthProviderOptions) {
|
constructor(readonly options: OAuthProviderOptions) {
|
||||||
this.serverUrlHash = crypto.createHash('md5').update(options.serverUrl).digest('hex')
|
this.serverUrlHash = getServerUrlHash(options.serverUrl)
|
||||||
this.configDir = options.configDir || path.join(os.homedir(), '.mcp-auth')
|
|
||||||
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'
|
||||||
|
@ -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<T>(filename: string, schema: any): Promise<T | undefined> {
|
|
||||||
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<string> {
|
|
||||||
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
|
* Gets the client information if it exists
|
||||||
* @returns The client information or undefined
|
* @returns The client information or undefined
|
||||||
*/
|
*/
|
||||||
async clientInformation(): Promise<OAuthClientInformation | undefined> {
|
async clientInformation(): Promise<OAuthClientInformation | undefined> {
|
||||||
return this.readFile<OAuthClientInformation>('client_info.json', OAuthClientInformationSchema)
|
return readJsonFile<OAuthClientInformation>(
|
||||||
|
this.serverUrlHash,
|
||||||
|
'client_info.json',
|
||||||
|
OAuthClientInformationSchema
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -148,7 +69,7 @@ export class NodeOAuthClientProvider implements OAuthClientProvider {
|
||||||
* @param clientInformation The client information to save
|
* @param clientInformation The client information to save
|
||||||
*/
|
*/
|
||||||
async saveClientInformation(clientInformation: OAuthClientInformationFull): Promise<void> {
|
async saveClientInformation(clientInformation: OAuthClientInformationFull): Promise<void> {
|
||||||
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
|
* @returns The OAuth tokens or undefined
|
||||||
*/
|
*/
|
||||||
async tokens(): Promise<OAuthTokens | undefined> {
|
async tokens(): Promise<OAuthTokens | undefined> {
|
||||||
return this.readFile<OAuthTokens>('tokens.json', OAuthTokensSchema)
|
return readJsonFile<OAuthTokens>(this.serverUrlHash, 'tokens.json', OAuthTokensSchema)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -164,7 +85,7 @@ export class NodeOAuthClientProvider implements OAuthClientProvider {
|
||||||
* @param tokens The tokens to save
|
* @param tokens The tokens to save
|
||||||
*/
|
*/
|
||||||
async saveTokens(tokens: OAuthTokens): Promise<void> {
|
async saveTokens(tokens: OAuthTokens): Promise<void> {
|
||||||
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
|
* @param codeVerifier The code verifier to save
|
||||||
*/
|
*/
|
||||||
async saveCodeVerifier(codeVerifier: string): Promise<void> {
|
async saveCodeVerifier(codeVerifier: string): Promise<void> {
|
||||||
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
|
* @returns The code verifier
|
||||||
*/
|
*/
|
||||||
async codeVerifier(): Promise<string> {
|
async codeVerifier(): Promise<string> {
|
||||||
return await this.readTextFile('code_verifier.txt')
|
return await readTextFile(
|
||||||
|
this.serverUrlHash,
|
||||||
|
'code_verifier.txt',
|
||||||
|
'No code verifier saved for session'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Add table
Add a link
Reference in a new issue