import { dirname, join, normalize } from 'pathe'; import { resolveKimiHome } from '#/app/bootstrap/bootstrap'; import { findGitWorkTree } from '#/app/git/workTree'; import { resolvePath } from '#/_base/utils/paths'; import { ErrorCodes, Error2 } from '#/errors'; import { McpServerConfigSchema, type McpServerConfig } from '#/mcpCore/config-schema'; import type { IHostFileSystem } from '#/os/interface/hostFileSystem'; import { OsFsErrors, HostFsError } from '#/os/interface/hostFsErrors'; export interface McpJsonPaths { readonly user: string; readonly projectRoot: string; readonly project: string; } export interface ResolveMcpJsonPathsInput { readonly fs: IHostFileSystem; readonly cwd: string; readonly homeDir?: string; } export async function resolveMcpJsonPaths(input: ResolveMcpJsonPathsInput): Promise { const start = normalize(input.cwd); const projectRoot = (await findGitWorkTree(input.fs, start))?.root ?? start; return { user: join(resolveKimiHome(input.homeDir), 'mcp.json'), projectRoot: join(projectRoot, '.mcp.json'), project: join(input.cwd, '.kimi-code', 'mcp.json'), }; } export interface LoadMcpServersInput { readonly fs: IHostFileSystem; readonly cwd: string; readonly homeDir?: string; readonly includeProject?: boolean; } export interface LoadMcpServersDetailedResult { readonly servers: Record; readonly origins: Record; } export async function loadMcpServers( input: LoadMcpServersInput, ): Promise> { return (await loadMcpServersDetailed(input)).servers; } export async function loadMcpServersDetailed( input: LoadMcpServersInput, ): Promise { const paths = await resolveMcpJsonPaths(input); if (input.includeProject === false) { const user = await readMcpJson(input.fs, paths.user); return { servers: user, origins: mapValuesToPath(user, paths.user) }; } const layers: readonly [path: string, servers: Record][] = await Promise.all([ readMcpJson(input.fs, paths.user), readMcpJson(input.fs, paths.projectRoot, { stdioCwdBase: dirname(paths.projectRoot) }), readMcpJson(input.fs, paths.project), ]).then(([user, projectRoot, project]) => [ [paths.user, user], [paths.projectRoot, projectRoot], [paths.project, project], ]); const servers: Record = Object.create(null); const origins: Record = Object.create(null); for (const [path, layer] of layers) { for (const [name, config] of Object.entries(layer)) { servers[name] = config; origins[name] = path; } } return { servers, origins }; } interface ReadMcpJsonOptions { readonly stdioCwdBase?: string; } async function readMcpJson( fs: IHostFileSystem, filePath: string, options: ReadMcpJsonOptions = {}, ): Promise> { let text: string; try { text = await fs.readText(filePath); } catch (error: unknown) { if (isFileNotFound(error)) return {}; throw new Error2( ErrorCodes.CONFIG_INVALID, `Failed to read ${filePath}: ${describeError(error)}`, { cause: error, }, ); } if (text.trim().length === 0) return {}; let data: unknown; try { data = JSON.parse(text); } catch (error: unknown) { throw new Error2( ErrorCodes.CONFIG_INVALID, `Invalid JSON in ${filePath}: ${describeError(error)}`, { cause: error, }, ); } try { return normalizeMcpServers(parseMcpJsonServers(data), options); } catch (error: unknown) { throw new Error2( ErrorCodes.CONFIG_INVALID, `Invalid MCP server config in ${filePath}: ${describeError(error)}`, { cause: error, }, ); } } function parseMcpJsonServers(data: unknown): Record { if (!isRecord(data)) { throw new Error('expected a JSON object'); } if (!('mcpServers' in data)) return {}; const raw = data['mcpServers']; if (!isRecord(raw)) { throw new Error('"mcpServers" must be an object'); } return Object.fromEntries( Object.entries(raw).map(([name, value]) => [name, McpServerConfigSchema.parse(value)]), ); } function normalizeMcpServers( servers: Record, options: ReadMcpJsonOptions, ): Record { const stdioCwdBase = options.stdioCwdBase; if (stdioCwdBase === undefined) return servers; return Object.fromEntries( Object.entries(servers).map(([name, config]) => [ name, normalizeStdioCwd(config, stdioCwdBase), ]), ); } function normalizeStdioCwd(config: McpServerConfig, cwdBase: string): McpServerConfig { if (config.transport !== 'stdio') return config; const cwd = config.cwd === undefined ? cwdBase : resolvePath(cwdBase, config.cwd); return { ...config, cwd }; } function mapValuesToPath( servers: Record, path: string, ): Record { const origins: Record = Object.create(null); for (const name of Object.keys(servers)) { origins[name] = path; } return origins; } function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value); } function isFileNotFound(error: unknown): boolean { return error instanceof HostFsError && error.code === OsFsErrors.codes.OS_FS_NOT_FOUND; } function describeError(error: unknown): string { return error instanceof Error ? error.message : String(error); }