import * as fs from 'node:fs/promises'; import * as path from 'node:path'; import { getHomeDir } from '@main/utils/pathDecoder'; import { createLogger } from '@shared/utils/logger'; import type { InstalledMcpEntry } from '@shared/types/extensions'; const logger = createLogger('Extensions:McpConfigStateReader'); export interface ConfiguredMcpEntry extends InstalledMcpEntry { scope: 'local' | 'user' | 'project'; config: Record; } export class McpConfigStateReader { async readInstalled(projectPath?: string): Promise { const entries: InstalledMcpEntry[] = []; const claudeConfig = await this.readClaudeConfig(); entries.push(...this.readUserMcpServers(claudeConfig)); if (projectPath) { entries.push(...this.readLocalMcpServers(claudeConfig, projectPath)); entries.push(...(await this.readProjectMcpServers(projectPath))); } return entries; } async readConfigured(projectPath?: string): Promise { const entries: ConfiguredMcpEntry[] = []; const claudeConfig = await this.readClaudeConfig(); entries.push(...this.readConfiguredMcpServersFromConfig(claudeConfig?.mcpServers, 'user')); if (projectPath) { entries.push(...this.readLocalConfiguredMcpServers(claudeConfig, projectPath)); entries.push(...(await this.readProjectConfiguredMcpServers(projectPath))); } return entries; } private async readClaudeConfig(): Promise | null> { const configPath = path.join(getHomeDir(), '.claude.json'); try { const raw = await fs.readFile(configPath, 'utf-8'); return JSON.parse(raw) as Record; } catch (err) { if ((err as NodeJS.ErrnoException).code === 'ENOENT') { return null; } logger.error(`Failed to read MCP servers from ${configPath}:`, err); return null; } } private readUserMcpServers(config: Record | null): InstalledMcpEntry[] { return this.readMcpServersFromConfig(config?.mcpServers, 'user'); } private readLocalMcpServers( config: Record | null, projectPath: string ): InstalledMcpEntry[] { return this.readLocalConfiguredMcpServers(config, projectPath).map( ({ config: _config, ...entry }) => entry ); } private readLocalConfiguredMcpServers( config: Record | null, projectPath: string ): ConfiguredMcpEntry[] { const projects = config && typeof config.projects === 'object' && config.projects ? (config.projects as Record) : null; const projectConfig = projects && typeof projects[projectPath] === 'object' && projects[projectPath] ? (projects[projectPath] as Record) : null; return this.readConfiguredMcpServersFromConfig(projectConfig?.mcpServers, 'local'); } private async readProjectMcpServers(projectPath: string): Promise { const configPath = path.join(projectPath, '.mcp.json'); return this.readMcpServersFromFile(configPath, 'project'); } private async readProjectConfiguredMcpServers( projectPath: string ): Promise { const configPath = path.join(projectPath, '.mcp.json'); return this.readConfiguredMcpServersFromFile(configPath, 'project'); } private readMcpServersFromConfig( value: unknown, scope: 'user' | 'project' | 'local' ): InstalledMcpEntry[] { const mcpServers = value && typeof value === 'object' ? (value as Record) : null; if (!mcpServers) { return []; } return Object.entries(mcpServers).map(([name, config]): InstalledMcpEntry => { let transport: string | undefined; if (config.command) transport = 'stdio'; else if (config.url) transport = 'http'; return { name, scope, transport }; }); } private readConfiguredMcpServersFromConfig( value: unknown, scope: 'user' | 'project' | 'local' ): ConfiguredMcpEntry[] { const mcpServers = value && typeof value === 'object' ? (value as Record) : null; if (!mcpServers) { return []; } return Object.entries(mcpServers) .filter((entry): entry is [string, Record] => { const [, config] = entry; return Boolean(config && typeof config === 'object' && !Array.isArray(config)); }) .map(([name, config]): ConfiguredMcpEntry => { let transport: string | undefined; if (typeof config.command === 'string') transport = 'stdio'; else if (typeof config.url === 'string') transport = 'http'; return { name, scope, transport, config: { ...config } }; }); } private async readMcpServersFromFile( filePath: string, scope: 'user' | 'project' ): Promise { return (await this.readConfiguredMcpServersFromFile(filePath, scope)).map( ({ config: _config, ...entry }) => entry ); } private async readConfiguredMcpServersFromFile( filePath: string, scope: 'user' | 'project' ): Promise { try { const raw = await fs.readFile(filePath, 'utf-8'); const json = JSON.parse(raw) as Record; return this.readConfiguredMcpServersFromConfig(json.mcpServers, scope); } catch (err) { if ((err as NodeJS.ErrnoException).code === 'ENOENT') { return []; } logger.error(`Failed to read MCP servers from ${filePath}:`, err); return []; } } }