import type { CapabilityAuthState, CapabilityCaller, CapabilityKind, CapabilityStatsItem, CapabilityToolInfo, McpServerStatus, RiskLevel, } from "@umt/contracts"; import { Check, Copy } from "lucide-react"; import { useMemo, useState } from "react"; import { toast } from "sonner"; import { useApiKeys } from "~/api/api-keys.ts"; import { mcpClientConfig, useCapabilityStats, useCapabilityTools, useMcpStatus, useUpdateMcpConfig, } from "~/api/mcp.ts"; import { DataTable } from "~/components/console/data-table.tsx"; import { DetailSection, DetailSheet } from "~/components/console/detail-sheet.tsx"; import { ErrorState, LoadingState } from "~/components/console/empty-state.tsx"; import { JsonView } from "~/components/console/json-view.tsx"; import { KvList } from "~/components/console/kv-list.tsx"; import { StatusBadge, type StatusTone } from "~/components/console/status-badge.tsx"; import { Button } from "~/components/ui/button.tsx"; import { Switch } from "~/components/ui/switch.tsx"; import { errorMessage } from "~/lib/api.ts"; import { formatRelative } from "~/lib/labels.ts"; import { formatDateTime } from "~/lib/utils.ts"; import { SettingsBlock } from "./settings-block.tsx"; // 设置 → Agent → 工具与 MCP:对外 MCP 服务端(端点、启用、允许的 API Key、会话、最近调用、接入配置) // + 能力清单(作用域 / 风险 / 幂等与重放 / 授权状态 / 7 天调用与失败率 / 最近一次,点开看 schema)。 export const KIND_LABEL: Record = { read: "只读", preview: "预览", propose: "方案", approve: "审批", execute: "执行", }; const RISK: Record = { low: { label: "低风险", tone: "neutral" }, medium: { label: "中风险", tone: "warning" }, high: { label: "高风险", tone: "destructive" }, }; const AUTH: Record = { read_only: { label: "不需要授权", tone: "neutral", hint: "只读或预览,不改数据;Key 有对应 scope 就能调用" }, granted: { label: "有持续授权", tone: "success", hint: "智能体、MCP、API Key 调用按持续授权放行" }, needs_approval: { label: "需审批", tone: "warning", hint: "没有覆盖它的持续授权,智能体、MCP、API Key 调用需要单次审批", }, agent_only: { label: "仅智能体会话", tone: "neutral", hint: "只在智能体会话内可用,不经 MCP 暴露" }, }; export const CALLER_LABEL: Record = { agent: "内置智能体", mcp: "MCP", rest: "REST", other: "其他", }; const percent = (n: number, d: number) => (d > 0 ? `${Math.round((n / d) * 100)}%` : "—"); export function AgentToolsBlock() { return (
); } // ── MCP 服务端 ── function McpServerBlock() { const status = useMcpStatus(); return ( {status.isPending ? ( ) : status.error ? ( status.refetch()} plain /> ) : ( )} ); } function McpServerBody({ s }: { s: McpServerStatus }) { const update = useUpdateMcpConfig(); const keys = useApiKeys(); const origin = typeof window !== "undefined" ? window.location.origin : ""; const endpoint = `${origin}${s.path}`; const config = useMemo(() => mcpClientConfig(origin, s.path, s.serverName), [origin, s.path, s.serverName]); const activeKeys = (keys.data?.items ?? []).filter((k) => !k.revokedAt); const restricted = s.allowedApiKeyIds !== null; const save = (patch: Parameters[0], ok: string) => update.mutate(patch, { onSuccess: () => toast.success(ok), onError: (e) => toast.error(errorMessage(e, "保存失败")), }); const toggleKey = (id: string, on: boolean) => { const cur = new Set(s.allowedApiKeyIds ?? []); if (on) cur.add(id); else cur.delete(id); save({ allowedApiKeyIds: [...cur] }, "已更新允许的 API Key"); }; return (
save({ enabled: v }, v ? "MCP 服务端已启用" : "MCP 服务端已停用,现有会话已断开")} aria-labelledby="mcp-enabled-label" /> {s.enabled ? "已启用" : "已停用"}
0 ? "success" : "neutral"}>{s.sessions.length} 个会话在线
, wide: true }, { label: "传输", value: "Streamable HTTP(POST / GET / DELETE)" }, { label: "鉴权", value: "Authorization: Bearer " }, { label: "限速", value: `每个 Key 每分钟 ${s.rateLimitPerMinute} 次工具调用,最多 ${s.maxSessionsPerKey} 个会话`, }, { label: "可见工具", value: `read ${s.toolsByScope.read} 个 · read + organize.execute ${s.toolsByScope.readExecute} 个 · admin ${s.toolsByScope.admin} 个`, }, ]} />
{restricted ? ( activeKeys.length === 0 ? (

还没有 API Key。在「API Key」分区新建。

) : (
    {activeKeys.map((k) => (
  • ))}
) ) : null}

Key 的 scope 决定能看到哪些工具:read 只有只读和预览类,organize.execute 或 admin 才有执行类。

{s.sessions.length === 0 ? (

没有在线会话。客户端连上后出现在这里,空闲 30 分钟自动断开。

) : (
    {s.sessions.map((x) => (
  • {x.idPrefix} {x.apiKeyName} {x.clientName ?? "未知客户端"} {x.toolCalls} 次调用 · 最近 {formatRelative(x.lastSeenAt)}
  • ))}
)}
{s.recentCalls.length === 0 ? (

还没有经 MCP 的调用。

) : (
    {s.recentCalls.slice(0, 8).map((c) => (
  • {c.capabilityId} {formatRelative(c.at)} {c.durationMs !== null ? ` · ${c.durationMs} ms` : ""}
  • ))}
)}
} >

适用于 Claude Code 等支持 Streamable HTTP 的客户端。密钥用环境变量 UMT_API_KEY 占位,配置里不含明文。

); } function OutcomeBadge({ outcome }: { outcome: string | null }) { if (!outcome) return null; const map: Record = { ok: { label: "成功", tone: "success" }, denied: { label: "被拒绝", tone: "warning" }, invalid: { label: "参数不符", tone: "destructive" }, error: { label: "失败", tone: "destructive" }, }; const m = map[outcome] ?? { label: outcome, tone: "neutral" as const }; return {m.label}; } function CopyText({ text }: { text: string }) { return ( {text} ); } function CopyButton({ text, label }: { text: string; label: string }) { const [copied, setCopied] = useState(false); return ( ); } // ── 能力清单 ── type Row = CapabilityToolInfo & { stats: CapabilityStatsItem | null }; function CapabilitiesBlock() { const tools = useCapabilityTools(); const stats = useCapabilityStats(7); const [openId, setOpenId] = useState(null); const byId = useMemo(() => new Map((stats.data?.items ?? []).map((s) => [s.capabilityId, s])), [stats.data]); const rows: Row[] | undefined = tools.data?.items.map((t) => ({ ...t, stats: byId.get(t.id) ?? null })); const open = rows?.find((r) => r.id === openId) ?? null; const automationOff = tools.data && (!tools.data.automationEnabled || tools.data.readOnlyReconcile); return ( {automationOff ? (

{tools.data?.readOnlyReconcile ? "只读对账模式" : "自动化总开关已关闭"} :执行类能力对智能体、MCP、API Key 一律拒绝,持续授权暂不生效。

) : null} r.id} loading={tools.isPending} error={tools.error} onRetry={() => tools.refetch()} onRowClick={(r) => setOpenId(r.id)} empty={{ title: "没有已注册的能力" }} columns={[ { key: "id", header: "能力", cell: (r) => (

{r.title}

{r.id}
), }, { key: "kind", header: "类别 / scope", hideBelow: "md", cell: (r) => (
{KIND_LABEL[r.kind]} {r.scope}
), }, { key: "risk", header: "风险", hideBelow: "sm", cell: (r) => {RISK[r.riskLevel].label}, }, { key: "idem", header: "幂等 / 重放", hideBelow: "lg", cell: (r) => ( {r.idempotent ? "幂等" : "非幂等"} · {r.replay === "safe" ? "可重放" : "不重放"} ), }, { key: "auth", header: "授权状态", cell: (r) => ( {AUTH[r.authState].label} ), }, { key: "calls", header: "7 天调用", align: "right", hideBelow: "sm", cell: (r) => r.stats ? (
{r.stats.calls} 0 ? "text-meta text-warning" : "text-meta text-muted-foreground" } > 失败 {percent(r.stats.failures, r.stats.calls)} {r.stats.denied > 0 ? ` · 拒绝 ${r.stats.denied}` : ""}
) : ( 0 ), }, { key: "last", header: "最近一次", hideBelow: "md", cell: (r) => r.stats?.lastCalledAt ? (
{formatRelative(r.stats.lastCalledAt)} {r.stats.lastCaller ? CALLER_LABEL[r.stats.lastCaller] : ""}
) : ( — ), }, ]} /> setOpenId(null)} />
); } function CapabilitySheet({ row, onClose }: { row: Row | null; onClose: () => void }) { return ( !o && onClose()} title={row?.title ?? ""} description={row?.id} width="lg" > {row ? ( <>

{row.description}

{KIND_LABEL[row.kind]} scope · {row.scope} {RISK[row.riskLevel].label} {AUTH[row.authState].label}
{row.stats ? ( row.stats!.byCaller[k] > 0) .map((k) => `${CALLER_LABEL[k]} ${row.stats!.byCaller[k]}`) .join(" · "), wide: true, }, ]} /> ) : null} ) : null}
); }