import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs"; import { dirname } from "node:path"; const DISABLED_SERVERS_KEY = "mcpServersDisabled"; type McpServerEntry = Record; export interface McpToolSettingsEntry { server: string; name: string; description: string; parameters: string[]; required: string[]; enabled: boolean; } export interface McpServerSettingsEntry { name: string; configured: boolean; enabled: boolean; cached: boolean; toolCount: number; enabledToolCount: number; resourceCount: number; cachedAt: string; tools: McpToolSettingsEntry[]; } function readRawConfig(filePath: string): Record { if (!existsSync(filePath)) return { mcpServers: {} }; try { const raw = JSON.parse(readFileSync(filePath, "utf8")); return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : { mcpServers: {} }; } catch { return { mcpServers: {} }; } } function writeRawConfig(filePath: string, raw: Record): void { mkdirSync(dirname(filePath), { recursive: true }); const tmpPath = `${filePath}.${process.pid}.tmp`; writeFileSync(tmpPath, `${JSON.stringify(raw, null, 2)}\n`, "utf8"); renameSync(tmpPath, filePath); } function getServersObject(raw: Record): Record { const existing = raw.mcpServers ?? raw["mcp-servers"]; if (!existing || typeof existing !== "object" || Array.isArray(existing)) { return {}; } return existing as Record; } function getDisabledServersObject(raw: Record): Record { const existing = raw[DISABLED_SERVERS_KEY]; if (!existing || typeof existing !== "object" || Array.isArray(existing)) { return {}; } return existing as Record; } function setServersObject(raw: Record, servers: Record): void { delete raw["mcp-servers"]; raw.mcpServers = servers; } function setDisabledServersObject(raw: Record, servers: Record): void { if (Object.keys(servers).length === 0) { delete raw[DISABLED_SERVERS_KEY]; return; } raw[DISABLED_SERVERS_KEY] = servers; } function normalizeToolName(value: string): string { return value.replace(/-/g, "_"); } function isToolExcluded(toolName: string, serverName: string, excludeTools: unknown): boolean { if (!Array.isArray(excludeTools) || excludeTools.length === 0) return false; const candidates = new Set([ normalizeToolName(toolName), normalizeToolName(`${serverName}_${toolName}`), normalizeToolName(`${serverName.replace(/-/g, "_")}_${toolName}`), ]); for (const excluded of excludeTools) { if (typeof excluded !== "string") continue; if (candidates.has(normalizeToolName(excluded))) { return true; } } return false; } function removeToolFromExcludeList(excludeTools: string[], toolName: string, serverName: string): string[] { return excludeTools.filter((entry) => !isToolExcluded(toolName, serverName, [entry])); } function readJsonFile(filePath: string): any | null { if (!existsSync(filePath)) return null; try { return JSON.parse(readFileSync(filePath, "utf8")); } catch { return null; } } function normalizeMcpTool(serverName: string, tool: any, serverEntry: McpServerEntry): McpToolSettingsEntry { const schema = tool?.inputSchema && typeof tool.inputSchema === "object" ? tool.inputSchema : {}; const properties = schema && typeof schema.properties === "object" ? Object.keys(schema.properties) : []; const required = Array.isArray(schema.required) ? schema.required.map(String) : []; const name = String(tool?.name || ""); return { server: serverName, name, description: String(tool?.description || ""), parameters: properties, required, enabled: name ? !isToolExcluded(name, serverName, serverEntry.excludeTools) : false, }; } export function listMcpSettings(configPath: string, cachePath: string): McpServerSettingsEntry[] { const raw = readRawConfig(configPath); const activeServers = getServersObject(raw); const disabledServers = getDisabledServersObject(raw); const cache = readJsonFile(cachePath); const cachedServers = cache?.servers && typeof cache.servers === "object" ? cache.servers : {}; const serverNames = Array.from( new Set([...Object.keys(activeServers), ...Object.keys(disabledServers), ...Object.keys(cachedServers)]), ).sort(); return serverNames.map((serverName) => { const enabled = Object.prototype.hasOwnProperty.call(activeServers, serverName); const serverEntry = (enabled ? activeServers[serverName] : disabledServers[serverName]) || {}; const entry = cachedServers[serverName] || {}; const tools = Array.isArray(entry.tools) ? entry.tools .map((tool: any) => normalizeMcpTool(serverName, tool, serverEntry)) .filter((tool: McpToolSettingsEntry) => tool.name) .sort((a: McpToolSettingsEntry, b: McpToolSettingsEntry) => a.name.localeCompare(b.name)) : []; const enabledTools = enabled ? tools.filter((tool) => tool.enabled) : []; const resources = Array.isArray(entry.resources) ? entry.resources : []; return { name: serverName, configured: enabled || Object.prototype.hasOwnProperty.call(disabledServers, serverName), enabled, cached: Boolean(cachedServers[serverName]), toolCount: tools.length, enabledToolCount: enabledTools.length, resourceCount: resources.length, cachedAt: entry.cachedAt ? new Date(entry.cachedAt).toISOString() : "", tools, }; }); } export function setMcpServerEnabled( configPath: string, cachePath: string, serverName: string, enabled: boolean, ): McpServerSettingsEntry { const raw = readRawConfig(configPath); const activeServers = getServersObject(raw); const disabledServers = getDisabledServersObject(raw); if (enabled) { const entry = disabledServers[serverName]; if (!entry) { const current = listMcpSettings(configPath, cachePath).find((server) => server.name === serverName); if (current?.enabled) return current; throw new Error(`未找到已禁用的 MCP Server:${serverName}`); } activeServers[serverName] = entry; delete disabledServers[serverName]; } else { const entry = activeServers[serverName]; if (!entry) { const current = listMcpSettings(configPath, cachePath).find((server) => server.name === serverName); if (current && !current.enabled) return current; throw new Error(`未找到 MCP Server:${serverName}`); } disabledServers[serverName] = entry; delete activeServers[serverName]; } setServersObject(raw, activeServers); setDisabledServersObject(raw, disabledServers); writeRawConfig(configPath, raw); const updated = listMcpSettings(configPath, cachePath).find((server) => server.name === serverName); if (!updated) { throw new Error(`更新 MCP Server 状态失败:${serverName}`); } return updated; } export function setMcpToolEnabled( configPath: string, cachePath: string, serverName: string, toolName: string, enabled: boolean, ): McpToolSettingsEntry { const raw = readRawConfig(configPath); const activeServers = getServersObject(raw); const serverEntry = activeServers[serverName]; if (!serverEntry) { throw new Error(`MCP Server 未启用:${serverName}`); } const excludeTools = Array.isArray(serverEntry.excludeTools) ? serverEntry.excludeTools.filter((value): value is string => typeof value === "string") : []; let nextExclude = excludeTools; if (enabled) { nextExclude = removeToolFromExcludeList(excludeTools, toolName, serverName); } else if (!isToolExcluded(toolName, serverName, excludeTools)) { nextExclude = [...excludeTools, toolName]; } if (nextExclude.length > 0) { serverEntry.excludeTools = nextExclude; } else { delete serverEntry.excludeTools; } activeServers[serverName] = serverEntry; setServersObject(raw, activeServers); writeRawConfig(configPath, raw); const server = listMcpSettings(configPath, cachePath).find((item) => item.name === serverName); const tool = server?.tools.find((item) => item.name === toolName); if (!tool) { throw new Error(`未找到 MCP工具:${serverName}/${toolName}`); } return tool; }