Files
oh-my-opencode-free-fork/src/features/claude-code-mcp-loader/loader.ts
YeonGyu-Kim e6ffdc4352 feat(claude-code-mcp-loader): auto-disable builtin skills with overlapping MCP servers
Add getSystemMcpServerNames() sync function to detect system-configured MCP
servers from .mcp.json files (user, project, and local scopes). Builtin skills
like playwright are now automatically excluded when their MCP server is already
configured in system config, preventing duplicate MCP server registration.

Also adds comprehensive test suite with 5 BDD-style tests covering empty config,
project/local scopes, disabled servers, and merged configurations.

Changes:
- loader.ts: Add getSystemMcpServerNames() function + readFileSync import
- loader.test.ts: Add 5 tests for getSystemMcpServerNames() edge cases
- index.ts: Filter builtin skills to exclude those with overlapping MCP names

🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
2026-01-02 10:55:54 +09:00

114 lines
2.9 KiB
TypeScript

import { existsSync, readFileSync } from "fs"
import { join } from "path"
import { getClaudeConfigDir } from "../../shared"
import type {
ClaudeCodeMcpConfig,
LoadedMcpServer,
McpLoadResult,
McpScope,
} from "./types"
import { transformMcpServer } from "./transformer"
import { log } from "../../shared/logger"
interface McpConfigPath {
path: string
scope: McpScope
}
function getMcpConfigPaths(): McpConfigPath[] {
const claudeConfigDir = getClaudeConfigDir()
const cwd = process.cwd()
return [
{ path: join(claudeConfigDir, ".mcp.json"), scope: "user" },
{ 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
}
}
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
}
export async function loadMcpConfigs(): Promise<McpLoadResult> {
const servers: McpLoadResult["servers"] = {}
const loadedServers: LoadedMcpServer[] = []
const paths = getMcpConfigPaths()
for (const { path, scope } of paths) {
const config = await loadMcpConfigFile(path)
if (!config?.mcpServers) continue
for (const [name, serverConfig] of Object.entries(config.mcpServers)) {
if (serverConfig.disabled) {
log(`Skipping disabled MCP server "${name}"`, { path })
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(", ")
}