2026-01-02 10:55:54 +09:00
|
|
|
import { existsSync, readFileSync } from "fs"
|
2025-12-09 16:27:46 +09:00
|
|
|
import { join } from "path"
|
2026-02-07 20:01:16 +09:00
|
|
|
import { homedir } from "os"
|
2025-12-26 23:28:33 +09:00
|
|
|
import { getClaudeConfigDir } from "../../shared"
|
2025-12-09 16:27:46 +09:00
|
|
|
import type {
|
|
|
|
|
ClaudeCodeMcpConfig,
|
|
|
|
|
LoadedMcpServer,
|
|
|
|
|
McpLoadResult,
|
|
|
|
|
McpScope,
|
|
|
|
|
} from "./types"
|
|
|
|
|
import { transformMcpServer } from "./transformer"
|
|
|
|
|
import { log } from "../../shared/logger"
|
2026-03-29 04:53:36 +09:00
|
|
|
import { shouldLoadMcpServer } from "./scope-filter"
|
2025-12-09 16:27:46 +09:00
|
|
|
|
|
|
|
|
interface McpConfigPath {
|
|
|
|
|
path: string
|
|
|
|
|
scope: McpScope
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getMcpConfigPaths(): McpConfigPath[] {
|
2026-02-07 22:29:51 +09:00
|
|
|
const claudeConfigDir = getClaudeConfigDir()
|
2025-12-09 16:27:46 +09:00
|
|
|
const cwd = process.cwd()
|
|
|
|
|
|
|
|
|
|
return [
|
2026-02-07 20:01:16 +09:00
|
|
|
{ path: join(homedir(), ".claude.json"), scope: "user" },
|
2026-02-07 22:29:51 +09:00
|
|
|
{ path: join(claudeConfigDir, ".mcp.json"), scope: "user" },
|
2025-12-09 16:27:46 +09:00
|
|
|
{ path: join(cwd, ".mcp.json"), scope: "project" },
|
|
|
|
|
{ path: join(cwd, ".claude", ".mcp.json"), scope: "local" },
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadMcpConfigFile(
|
|
|
|
|
filePath: string
|
|
|
|
|
): Promise<ClaudeCodeMcpConfig | null> {
|
|
|
|
|
if (!existsSync(filePath)) {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const content = await Bun.file(filePath).text()
|
|
|
|
|
return JSON.parse(content) as ClaudeCodeMcpConfig
|
|
|
|
|
} catch (error) {
|
|
|
|
|
log(`Failed to load MCP config from ${filePath}`, error)
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-02 10:55:54 +09:00
|
|
|
export function getSystemMcpServerNames(): Set<string> {
|
|
|
|
|
const names = new Set<string>()
|
|
|
|
|
const paths = getMcpConfigPaths()
|
|
|
|
|
|
|
|
|
|
for (const { path } of paths) {
|
|
|
|
|
if (!existsSync(path)) continue
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const content = readFileSync(path, "utf-8")
|
|
|
|
|
const config = JSON.parse(content) as ClaudeCodeMcpConfig
|
|
|
|
|
if (!config?.mcpServers) continue
|
|
|
|
|
|
|
|
|
|
for (const [name, serverConfig] of Object.entries(config.mcpServers)) {
|
|
|
|
|
if (serverConfig.disabled) continue
|
|
|
|
|
names.add(name)
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return names
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-10 15:26:17 +01:00
|
|
|
export async function loadMcpConfigs(
|
|
|
|
|
disabledMcps: string[] = []
|
|
|
|
|
): Promise<McpLoadResult> {
|
2025-12-09 16:27:46 +09:00
|
|
|
const servers: McpLoadResult["servers"] = {}
|
|
|
|
|
const loadedServers: LoadedMcpServer[] = []
|
|
|
|
|
const paths = getMcpConfigPaths()
|
2026-02-10 15:26:17 +01:00
|
|
|
const disabledSet = new Set(disabledMcps)
|
2026-03-29 04:53:36 +09:00
|
|
|
const cwd = process.cwd()
|
2025-12-09 16:27:46 +09:00
|
|
|
|
|
|
|
|
for (const { path, scope } of paths) {
|
|
|
|
|
const config = await loadMcpConfigFile(path)
|
|
|
|
|
if (!config?.mcpServers) continue
|
|
|
|
|
|
|
|
|
|
for (const [name, serverConfig] of Object.entries(config.mcpServers)) {
|
2026-02-10 15:26:17 +01:00
|
|
|
if (disabledSet.has(name)) {
|
|
|
|
|
log(`Skipping MCP "${name}" (in disabled_mcps)`, { path })
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-29 04:53:36 +09:00
|
|
|
if (!shouldLoadMcpServer(serverConfig, cwd)) {
|
|
|
|
|
log(`Skipping MCP server "${name}" because local scope does not match cwd`, {
|
|
|
|
|
path,
|
|
|
|
|
projectPath: serverConfig.projectPath,
|
|
|
|
|
cwd,
|
|
|
|
|
})
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-09 16:27:46 +09:00
|
|
|
if (serverConfig.disabled) {
|
2026-01-24 14:55:59 +08:00
|
|
|
log(`Disabling MCP server "${name}"`, { path })
|
|
|
|
|
delete servers[name]
|
|
|
|
|
const existingIndex = loadedServers.findIndex((s) => s.name === name)
|
|
|
|
|
if (existingIndex !== -1) {
|
|
|
|
|
loadedServers.splice(existingIndex, 1)
|
|
|
|
|
log(`Removed previously loaded MCP server "${name}"`, { path })
|
|
|
|
|
}
|
2025-12-09 16:27:46 +09:00
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const transformed = transformMcpServer(name, serverConfig)
|
|
|
|
|
servers[name] = transformed
|
|
|
|
|
|
|
|
|
|
const existingIndex = loadedServers.findIndex((s) => s.name === name)
|
|
|
|
|
if (existingIndex !== -1) {
|
|
|
|
|
loadedServers.splice(existingIndex, 1)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
loadedServers.push({ name, scope, config: transformed })
|
|
|
|
|
|
|
|
|
|
log(`Loaded MCP server "${name}" from ${scope}`, { path })
|
|
|
|
|
} catch (error) {
|
|
|
|
|
log(`Failed to transform MCP server "${name}"`, error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { servers, loadedServers }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function formatLoadedServersForToast(
|
|
|
|
|
loadedServers: LoadedMcpServer[]
|
|
|
|
|
): string {
|
|
|
|
|
if (loadedServers.length === 0) return ""
|
|
|
|
|
|
|
|
|
|
return loadedServers
|
|
|
|
|
.map((server) => `${server.name} (${server.scope})`)
|
|
|
|
|
.join(", ")
|
|
|
|
|
}
|