- contracts/mcp.ts: capability stats/tools, MCP server status/config schemas, mcpToolVisible - api.ts: GET /capabilities/stats, GET /capabilities/tools, GET /mcp/status, PATCH /mcp/config - migration 0039: mcp_server_config single row + audit_event capability index - deno.json: npm:@modelcontextprotocol/[email protected].1 Co-Authored-By: Claude Opus 5.5 <[email protected]>
163 lines
6.5 KiB
TypeScript
163 lines
6.5 KiB
TypeScript
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<typeof CapabilityCaller>;
|
|
|
|
/** 审计 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<typeof CapabilityAuthState>;
|
|
|
|
export const CapabilityStatsQuery = z.object({
|
|
days: z.coerce.number().int().min(1).max(90).default(7),
|
|
});
|
|
export type CapabilityStatsQuery = z.infer<typeof CapabilityStatsQuery>;
|
|
|
|
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<typeof CapabilityStatsItem>;
|
|
|
|
export const CapabilityStatsResponse = z.object({
|
|
days: z.number().int(),
|
|
since: Timestamp,
|
|
items: z.array(CapabilityStatsItem),
|
|
});
|
|
export type CapabilityStatsResponse = z.infer<typeof CapabilityStatsResponse>;
|
|
|
|
/** 能力清单(给工具页与会话页工具卡片):描述符 + 工具名 + 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<typeof CapabilityToolInfo>;
|
|
|
|
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<typeof CapabilityToolsResponse>;
|
|
|
|
/** 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<typeof McpServerConfig>;
|
|
|
|
export const McpServerConfigUpdate = z
|
|
.object({
|
|
enabled: z.boolean(),
|
|
allowedApiKeyIds: z.array(Id).max(200).nullable(),
|
|
})
|
|
.partial();
|
|
export type McpServerConfigUpdate = z.infer<typeof McpServerConfigUpdate>;
|
|
|
|
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<typeof McpSessionInfo>;
|
|
|
|
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<typeof McpRecentCall>;
|
|
|
|
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<typeof McpServerStatus>;
|
|
|
|
/** MCP 工具可见性:能力 scope 被 Key 覆盖;没有 organize.execute(或 admin)时只给 read / preview 类 */
|
|
export function mcpToolVisible(
|
|
d: Pick<CapabilityDescriptor, "id" | "kind" | "scope">,
|
|
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");
|
|
}
|