import { useCallback, useEffect, useMemo, useState } from "react"; import { Check, Copy, ExternalLink, RefreshCw } from "lucide-react"; import { getMcpInstall, getMcpPolicy, saveMcpPolicy, type McpInstallInfo, type McpPolicy, } from "./api"; import { useWorkspaceStore } from "./store"; import { openExternal } from "./openExternal"; const MCP_DOCS_URL = "http://127.0.0.1:8788/docs/mcp"; const NPM_PKG = "@rlwj-lab/opsxdb-mcp"; type ClientTab = "cursor" | "claude-code" | "vscode" | "windsurf" | "codex" | "claude-desktop"; const CLIENT_TABS: Array<{ id: ClientTab; label: string }> = [ { id: "cursor", label: "Cursor" }, { id: "claude-code", label: "Claude Code" }, { id: "vscode", label: "VS Code" }, { id: "windsurf", label: "Windsurf" }, { id: "codex", label: "Codex" }, { id: "claude-desktop", label: "Claude Desktop" }, ]; function buildClientSnippet(tab: ClientTab, goBin: string): { path: string; json: string } { const env: Record = {}; if (goBin) env.OPSXDB_MCP_BIN = goBin; const npxServer = { command: "npx", args: ["-y", NPM_PKG], ...(Object.keys(env).length ? { env } : {}), }; const absServer = goBin ? { command: goBin, args: [] as string[] } : npxServer; switch (tab) { case "claude-code": return { path: "项目根目录 .mcp.json", json: JSON.stringify({ mcpServers: { opsxdb: npxServer } }, null, 2), }; case "cursor": return { path: "~/.cursor/mcp.json 或项目 .cursor/mcp.json", json: JSON.stringify({ mcpServers: { opsxdb: npxServer } }, null, 2), }; case "vscode": return { path: ".vscode/mcp.json", json: JSON.stringify( { servers: { opsxdb: { type: "stdio", command: npxServer.command, args: npxServer.args, ...(npxServer.env ? { env: npxServer.env } : {}), }, }, }, null, 2, ), }; case "windsurf": return { path: "~/.codeium/windsurf/mcp_config.json", json: JSON.stringify({ mcpServers: { opsxdb: npxServer } }, null, 2), }; case "codex": return { path: "~/.codex/config.toml(MCP 段)或对应 mcp.json", json: JSON.stringify({ mcpServers: { opsxdb: absServer } }, null, 2), }; case "claude-desktop": return { path: "Claude Desktop → Developer → Edit Config", json: JSON.stringify({ mcpServers: { opsxdb: npxServer } }, null, 2), }; } } function StatusBadge({ status }: { status: string }) { const map: Record = { checking: { label: "检测中", color: "var(--ops-muted)", bg: "var(--ops-surface-2)" }, ready: { label: "已就绪", color: "var(--ops-accent)", bg: "color-mix(in srgb, var(--ops-accent) 14%, transparent)" }, partial: { label: "部分就绪", color: "var(--ops-warning, #d97706)", bg: "color-mix(in srgb, #d97706 14%, transparent)" }, not_installed: { label: "未安装", color: "var(--ops-warning, #d97706)", bg: "color-mix(in srgb, #d97706 14%, transparent)" }, }; const m = map[status] ?? map.not_installed; return ( {m.label} ); } /** 设置 → MCP:权限状态、授权连接、客户端接入 */ export function McpSettings() { const notify = useWorkspaceStore((s) => s.notify); const connections = useWorkspaceStore((s) => s.connections); const [install, setInstall] = useState(null); const [policy, setPolicy] = useState(null); const [loading, setLoading] = useState(true); const [installLoading, setInstallLoading] = useState(true); const [installError, setInstallError] = useState(""); const [saving, setSaving] = useState(false); const [error, setError] = useState(""); const [clientTab, setClientTab] = useState("cursor"); const [copied, setCopied] = useState<"install" | "snippet" | "">(""); const [tick, setTick] = useState(0); const reload = useCallback(() => setTick((n) => n + 1), []); useEffect(() => { let alive = true; setLoading(true); setError(""); getMcpPolicy() .then((pol) => { if (!alive) return; setPolicy(pol.policy); }) .catch((e) => { if (alive) setError(e instanceof Error ? e.message : String(e)); }) .finally(() => { if (alive) setLoading(false); }); setInstallLoading(true); setInstallError(""); getMcpInstall() .then((inst) => { if (alive) setInstall(inst); }) .catch((e) => { if (alive) setInstallError(e instanceof Error ? e.message : String(e)); }) .finally(() => { if (alive) setInstallLoading(false); }); return () => { alive = false; }; }, [tick]); const readOnly = !policy || policy.mode !== "read_write"; const allowDangerous = !!policy?.allow_dangerous; const allowed = new Set(policy?.allowed_connection_ids ?? []); const snippet = useMemo( () => buildClientSnippet(clientTab, install?.goBinaryPath || ""), [clientTab, install?.goBinaryPath], ); const persist = async (next: McpPolicy) => { setSaving(true); try { const res = await saveMcpPolicy(next); setPolicy(res.policy); notify({ kind: "success", title: "MCP", message: "策略已保存", mode: "toast", durationMs: 1400 }); } catch (e) { notify({ kind: "error", title: "MCP", message: e instanceof Error ? e.message : String(e), mode: "modal" }); } finally { setSaving(false); } }; const copyText = async (text: string, which: "install" | "snippet") => { try { await navigator.clipboard.writeText(text); setCopied(which); setTimeout(() => setCopied(""), 1600); notify({ kind: "success", title: "已复制", message: which === "install" ? "安装命令" : "配置片段", mode: "toast", durationMs: 1200 }); } catch { notify({ kind: "error", title: "复制失败", message: "请手动选中复制", mode: "toast", durationMs: 2000 }); } }; const toggleConn = (id: string) => { if (!policy) return; const set = new Set(policy.allowed_connection_ids ?? []); if (set.has(id)) set.delete(id); else set.add(id); void persist({ ...policy, allowed_connection_ids: Array.from(set) }); }; if (loading) return

正在读取 MCP 设置…

; if (error) { return (

{error}

); } const installCmd = install?.installCommand || `npm install -g ${NPM_PKG}@latest`; return (
{/* Header */}

MCP Server

{installError &&

安装检测失败:{installError}。连接白名单与权限设置仍可使用。

} {/* Version grid */}
{[ ["当前版本", install?.currentVersion || (installLoading ? "检测中…" : install?.goBinaryFound ? "native" : "未检测到全局安装")], ["最新版本", install?.latestVersion ? `v${install.latestVersion}` : "—"], ["Node.js", install?.nodeVersion || (installLoading ? "检测中…" : "未检测到")], ["npm", install?.npmAvailable ? (install.npmVersion || "可用") : installLoading ? "检测中…" : "不可用"], ].map(([k, v]) => (
{k}
{v}
))}
{install?.goBinaryPath && (

Go 二进制:{install.goBinaryPath}

)} {install?.hint && (

{install.hint}

)} {/* Install command */}
安装命令
{installCmd}

npm 包是 Node 启动器;还需本机 Go 二进制(go build -o bin/opsxdb-mcp ./cmd/opsxdb-mcp), 并在客户端配置里设置 OPSXDB_MCP_BIN。

{readOnly ? "只读模式" : "读写模式"} {!readOnly && allowDangerous && 允许危险 SQL} {(readOnly || !allowDangerous) && }

Agent 写入前确认具体命令、连接与数据库。

{/* Whitelist */}
连接白名单

仅可访问勾选的连接。

{connections.length === 0 ? (

暂无已保存连接。请先在侧栏新建连接。

) : (
{connections.map((c) => ( ))}
)}
{/* Client configs */}
MCP 配置
{CLIENT_TABS.map((t) => ( ))}

写入位置:{snippet.path}

            {snippet.json}
          
); }