agent-ecosystem/src/main/services/extensions/runtime/McpConfigStateReader.ts

101 lines
3.3 KiB
TypeScript

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 class McpConfigStateReader {
async readInstalled(projectPath?: string): Promise<InstalledMcpEntry[]> {
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;
}
private async readClaudeConfig(): Promise<Record<string, unknown> | null> {
const configPath = path.join(getHomeDir(), '.claude.json');
try {
const raw = await fs.readFile(configPath, 'utf-8');
return JSON.parse(raw) as Record<string, unknown>;
} 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<string, unknown> | null): InstalledMcpEntry[] {
return this.readMcpServersFromConfig(config?.mcpServers, 'user');
}
private readLocalMcpServers(
config: Record<string, unknown> | null,
projectPath: string
): InstalledMcpEntry[] {
const projects =
config && typeof config.projects === 'object' && config.projects
? (config.projects as Record<string, unknown>)
: null;
const projectConfig =
projects && typeof projects[projectPath] === 'object' && projects[projectPath]
? (projects[projectPath] as Record<string, unknown>)
: null;
return this.readMcpServersFromConfig(projectConfig?.mcpServers, 'local');
}
private async readProjectMcpServers(projectPath: string): Promise<InstalledMcpEntry[]> {
const configPath = path.join(projectPath, '.mcp.json');
return this.readMcpServersFromFile(configPath, 'project');
}
private readMcpServersFromConfig(
value: unknown,
scope: 'user' | 'project' | 'local'
): InstalledMcpEntry[] {
const mcpServers =
value && typeof value === 'object'
? (value as Record<string, { command?: string; url?: string }>)
: 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 async readMcpServersFromFile(
filePath: string,
scope: 'user' | 'project'
): Promise<InstalledMcpEntry[]> {
try {
const raw = await fs.readFile(filePath, 'utf-8');
const json = JSON.parse(raw) as Record<string, unknown>;
return this.readMcpServersFromConfig(json.mcpServers, scope);
} catch (err) {
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
return [];
}
logger.error(`Failed to read MCP servers from ${filePath}:`, err);
return [];
}
}
}