import { z } from "zod"; import { CapabilityDescriptor, type CapabilityKind } from "./capability.ts"; import { type ApiScope, Id, Timestamp } from "./common.ts"; // 对外 MCP 服务端与能力可视化(REQ R13、§10.3、§12.3)。 // MCP 每个 tool 对应一个已注册能力(工具名 `域__动作`),调用走同一个 registry.invoke(授权 / 审计 / 幂等键 / 操作账本)。 /** MCP 端点(相对 API_PREFIX);Streamable HTTP,POST 发消息、GET 开服务端推送流、DELETE 结束会话 */ export const MCP_PATH = "/mcp"; /** MCP 协议里声明的服务端名 */ export const MCP_SERVER_NAME = "uhd-media-tool"; /** 调用来源归类:内置 Agent(含后台任务)、MCP、REST(控制台 / API Key 直接调用) */ export const CapabilityCaller = z.enum(["agent", "mcp", "rest", "other"]); export type CapabilityCaller = z.infer; /** 审计 payload.source → 调用来源 */ export function callerOfSource(source: string | null | undefined): CapabilityCaller { if (source === "agent" || source === "task") return "agent"; if (source === "mcp") return "mcp"; if (source === "web" || source === "cli") return "rest"; return "other"; } /** * 能力当前的授权状态(服务端按 automation_grant 判定): * - read_only:read / preview,只看 scope,不需要授权; * - granted:有覆盖它的有效持续授权(自动来源仍受自动化总开关约束); * - needs_approval:没有持续授权,需要单次审批(或控制台手动执行); * - agent_only:只在 Agent 会话内可用(agent.* 能力,例如派生子会话)。 */ export const CapabilityAuthState = z.enum(["read_only", "granted", "needs_approval", "agent_only"]); export type CapabilityAuthState = z.infer; export const CapabilityStatsQuery = z.object({ days: z.coerce.number().int().min(1).max(90).default(7), }); export type CapabilityStatsQuery = z.infer; const CallerCounts = z.object({ agent: z.number().int().nonnegative(), mcp: z.number().int().nonnegative(), rest: z.number().int().nonnegative(), other: z.number().int().nonnegative(), }); /** 单个能力在统计窗口内的调用汇总(来自 audit_event 的 capability.invoke / capability.denied) */ export const CapabilityStatsItem = z.object({ capabilityId: z.string(), calls: z.number().int().nonnegative(), /** outcome = error / invalid */ failures: z.number().int().nonnegative(), /** 授权或白名单拒绝 */ denied: z.number().int().nonnegative(), avgDurationMs: z.number().nonnegative().nullable(), lastCalledAt: Timestamp.nullable(), lastOutcome: z.string().nullable(), lastCaller: CapabilityCaller.nullable(), byCaller: CallerCounts, }); export type CapabilityStatsItem = z.infer; export const CapabilityStatsResponse = z.object({ days: z.number().int(), since: Timestamp, items: z.array(CapabilityStatsItem), }); export type CapabilityStatsResponse = z.infer; /** 能力清单(给工具页与会话页工具卡片):描述符 + 工具名 + JSON Schema + 授权状态 */ export const CapabilityToolInfo = CapabilityDescriptor.extend({ toolName: z.string(), inputJsonSchema: z.record(z.string(), z.unknown()), outputJsonSchema: z.record(z.string(), z.unknown()), authState: CapabilityAuthState, /** 覆盖它的有效持续授权数 */ activeGrants: z.number().int().nonnegative(), /** 能否经 MCP 暴露(agent.* 只在 Agent 会话内) */ mcpExposed: z.boolean(), }); export type CapabilityToolInfo = z.infer; export const CapabilityToolsResponse = z.object({ /** 自动化总开关:关着时 execute 类能力对自动来源(Agent / MCP / API Key)一律拒绝 */ automationEnabled: z.boolean(), readOnlyReconcile: z.boolean(), items: z.array(CapabilityToolInfo), }); export type CapabilityToolsResponse = z.infer; /** MCP 服务端配置(mcp_server_config 单行) */ export const McpServerConfig = z.object({ enabled: z.boolean(), /** 允许连接的 API Key;null = 所有未撤销的 Key */ allowedApiKeyIds: z.array(Id).nullable(), updatedAt: Timestamp.nullable(), }); export type McpServerConfig = z.infer; export const McpServerConfigUpdate = z .object({ enabled: z.boolean(), allowedApiKeyIds: z.array(Id).max(200).nullable(), }) .partial(); export type McpServerConfigUpdate = z.infer; export const McpSessionInfo = z.object({ /** 会话 ID 前 8 位(完整 ID 等同会话凭据,不回显) */ idPrefix: z.string(), apiKeyId: z.string(), apiKeyName: z.string(), clientName: z.string().nullable(), createdAt: Timestamp, lastSeenAt: Timestamp, toolCalls: z.number().int().nonnegative(), }); export type McpSessionInfo = z.infer; export const McpRecentCall = z.object({ at: Timestamp, capabilityId: z.string(), outcome: z.string(), actorId: z.string().nullable(), durationMs: z.number().nullable(), errorCode: z.string().nullable(), }); export type McpRecentCall = z.infer; export const McpServerStatus = McpServerConfig.extend({ /** 相对站点根的端点路径(/api/v1/mcp) */ path: z.string(), transport: z.literal("streamable-http"), serverName: z.string(), serverVersion: z.string(), /** 每个 API Key 每分钟的工具调用上限 */ rateLimitPerMinute: z.number().int().positive(), /** 每个 API Key 同时保持的会话上限 */ maxSessionsPerKey: z.number().int().positive(), sessions: z.array(McpSessionInfo), recentCalls: z.array(McpRecentCall), /** 常见 Key 组合能看到的工具数(read / read + organize.execute / admin) */ toolsByScope: z.object({ read: z.number().int().nonnegative(), readExecute: z.number().int().nonnegative(), admin: z.number().int().nonnegative(), }), }); export type McpServerStatus = z.infer; /** MCP 工具可见性:能力 scope 被 Key 覆盖;没有 organize.execute(或 admin)时只给 read / preview 类 */ export function mcpToolVisible( d: Pick, scopes: readonly string[], ): boolean { if (d.id.startsWith("agent.")) return false; const has = (s: ApiScope) => scopes.includes("admin") || scopes.includes(s) || (s === "organize.read" && scopes.includes("organize.execute")); if (!has(d.scope)) return false; const readish: readonly CapabilityKind[] = ["read", "preview"]; return readish.includes(d.kind) || has("organize.execute"); }