import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, join, resolve } from "node:path"; export const DATABASE_MCP_CONFIG_PATH = ".mcp.json"; export const DB_CONNECT_CONFIG_PATH = ".pi/kd/mcp-db-connect.yml"; export interface DatabaseMcpWriteResult { path: string; changed: boolean; dbConnectConfigPath: string; dbConnectConfigChanged: boolean; servers: string[]; content: string; } interface McpServerEntry { command: string; args?: string[]; env?: Record; lifecycle?: "lazy" | "eager" | "keep-alive"; idleTimeout?: number; exposeResources?: boolean; directTools?: boolean | string[]; } interface McpConfig { settings?: Record; mcpServers?: Record; [key: string]: unknown; } const DB_SERVER_NAMES = ["kcode-postgres", "kcode-sqlserver", "kcode-oracle"]; export function buildDatabaseMcpConfig(packageRoot: string): McpConfig { const postgresLauncher = normalizeForMcp(join(packageRoot, "src", "mcp", "postgres-env-launcher.js")); return { settings: { toolPrefix: "server", idleTimeout: 10, directTools: false, }, mcpServers: { "kcode-postgres": { command: "node", args: [postgresLauncher], env: { KCODE_POSTGRES_URL: "${KCODE_POSTGRES_URL}", KCODE_POSTGRES_DATABASE_SCOPE: "required-business-database", }, lifecycle: "lazy", idleTimeout: 10, exposeResources: true, }, "kcode-sqlserver": { command: "npx", args: ["-y", "@bilims/mcp-sqlserver@2.0.3"], env: { SQLSERVER_HOST: "${KCODE_SQLSERVER_HOST}", SQLSERVER_USER: "${KCODE_SQLSERVER_USER}", SQLSERVER_PASSWORD: "${KCODE_SQLSERVER_PASSWORD}", SQLSERVER_DATABASE: "${KCODE_SQLSERVER_DATABASE}", SQLSERVER_DATABASE_SCOPE: "required-business-database", }, lifecycle: "lazy", idleTimeout: 10, }, "kcode-oracle": { command: "npx", args: ["-y", "mcp-db-connect@0.1.16", "start", "--project", ".", "--config", `./${DB_CONNECT_CONFIG_PATH}`], env: { LOG_LEVEL: "silent", }, lifecycle: "lazy", idleTimeout: 10, }, }, }; } export function writeDatabaseMcpConfig(cwd: string, packageRoot: string): DatabaseMcpWriteResult { const targetPath = resolve(cwd, DATABASE_MCP_CONFIG_PATH); const dbConnectConfigPath = resolve(cwd, DB_CONNECT_CONFIG_PATH); const before = readMcpConfig(targetPath); const next = mergeDatabaseMcpConfig(before, buildDatabaseMcpConfig(packageRoot)); const content = `${JSON.stringify(next, null, 2)}\n`; const previousContent = existsSync(targetPath) ? readFileSync(targetPath, "utf8") : ""; const changed = previousContent !== content; if (changed) writeFileSync(targetPath, content, "utf8"); const dbConnectContent = buildDbConnectConfig(); const previousDbConnectContent = existsSync(dbConnectConfigPath) ? readFileSync(dbConnectConfigPath, "utf8") : ""; const dbConnectConfigChanged = previousDbConnectContent !== dbConnectContent; if (dbConnectConfigChanged) { mkdirSync(dirname(dbConnectConfigPath), { recursive: true }); writeFileSync(dbConnectConfigPath, dbConnectContent, "utf8"); } return { path: targetPath, changed, dbConnectConfigPath, dbConnectConfigChanged, servers: DB_SERVER_NAMES, content }; } export function mergeDatabaseMcpConfig(existing: McpConfig, databaseConfig: McpConfig): McpConfig { return { ...existing, settings: { ...(existing.settings ?? {}), ...(databaseConfig.settings ?? {}), }, mcpServers: { ...(existing.mcpServers ?? {}), ...(databaseConfig.mcpServers ?? {}), }, }; } export function formatDatabaseMcpInstructions(result: DatabaseMcpWriteResult): string { return [ `已${result.changed ? "写入" : "确认"}数据库 MCP 配置:${result.path}`, `已${result.dbConnectConfigChanged ? "写入" : "确认"} Oracle MCP 只读配置:${result.dbConnectConfigPath}`, `已配置服务器:${result.servers.join(", ")}`, "", "凭证必须通过当前终端环境变量提供,禁止写入 .mcp.json、文档、日志或提交记录。", "PostgreSQL:设置 KCODE_POSTGRES_URL,数据库名必须指向真实苍穹业务库;禁止用默认 postgres 库判断 t_meta_entitydesign 是否存在。", "SQL Server:必须设置 KCODE_SQLSERVER_DATABASE 到具体 AIS 业务账套库;禁止使用可跨库枚举的高权限账号。", "Oracle:编辑 .pi/kd/mcp-db-connect.yml 中的 host/port/serviceName/username;serviceName 必须绑定具体业务库或服务,密码只设置 KCODE_ORACLE_PASSWORD。", "", "进入 kcode start 后使用 mcp({ search: \"database table schema\" }) 搜索工具,再按 describe/call 调用。", "执行约束:只允许查询和元数据读取;INSERT/UPDATE/DELETE/DDL/执行存储过程只能生成 SQL 文本,由用户自行执行。", ].join("\n"); } function buildDbConnectConfig(): string { return [ "security:", " defaultMaxRows: 100", " queryTimeoutMs: 10000", " blockMultiStatement: true", " allowWriteOperations: false", " maskColumns:", " - password", " - token", " - secret", " - api_key", " auditLogPath: ./.pi/kd/mcp-db-connect.audit.jsonl", "", "connections:", " oracle_default:", " type: oracle", " host: localhost", " port: 1521", " serviceName: ORCLPDB1", " username: readonly_user", " passwordEnv: KCODE_ORACLE_PASSWORD", " clientMode: thin", " mode: readonly", "", ].join("\n"); } function readMcpConfig(path: string): McpConfig { if (!existsSync(path)) return {}; try { const parsed = JSON.parse(readFileSync(path, "utf8")) as unknown; if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { throw new Error("根节点必须是 JSON object"); } return parsed as McpConfig; } catch (error) { const message = error instanceof Error ? error.message : String(error); throw new Error(`项目 MCP 配置无法解析,已停止写入:${path}\n原因:${message}`); } } function normalizeForMcp(path: string): string { return path.replace(/\\/g, "/"); }