Files
UHD_Media_Tool/docs/runbook/mcp.md
T
rootandClaude Opus 5.5 ceb66f8c77 feat: v0.3.8–v0.3.14 工作区汇总(依赖升级、整理来源、手动入库、原地入库等)
生产 v0.3.8~v0.3.13 均由 8013196 加工作区修改构建部署,此前未提交;本提交补齐源码,
并含未部署的 v0.3.14 原地入库。中间各版本源码状态未保存,无法逐版拆分,详见
docs/plan/17-full-upgrade.md 与 docs/plan/18-organize-release.md。

- v0.3.8 全面升级:Deno workspace 依赖、Router 8 / Vite 8、camofox 1.18.0 / Node 24、
  PostgreSQL 18.6、备份客户端 18.6、演示 qB 5.2.4
- v0.3.9 媒体整理:分类规则、整理来源与目录快照(0059)、文件管理整理入口
- v0.3.10 手动下载与入库:独立手动下载、详情页资源下载、TMDB / Local 元数据兜底
  (metadata.resolve_organize)、正常多版本 / 一次性洗版(0060)
- v0.3.11 搜索详情与桌面黑边修复
- v0.3.12 列表分页、搜索历史、首页概况
- v0.3.13 统一整理弹窗与队列
- v0.3.14 原地入库(0061,未部署)

同批审计修复:
- 刮削入队:确认原记录存在后才删重放产生的新记录;写回 run 关联失败时保持 queued,
  由处理器补关联(对齐 MP 先登记、只在入队失败时记失败)
- 识别升级链只在结果更确定时采用,不再用 no_match 顶替原有歧义候选
- bluray_dir 资产以原盘目录本身为刮削根(MP 同做法)
- metadata.resolve_organize 登记到入库排障 Skill 与能力地图
- db/src/types.ts 按迁移重新生成;contracts 整理动作枚举顺序与数据库对齐
- 测试:原地入库用例改为直接查记录表;domain 测试模板补 version
- 前端:通知铃铛弹层与用户菜单统一样式;站点卡片共用卡头、底栏贴底、分页间距

依赖升级证据只提交摘要文件,registry-snapshot.json 移至 .local/(SHA-256 记于文档)。

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-02 15:47:17 +08:00

88 lines
6.8 KiB
Markdown

# MCP 服务端运行手册(AG-1)
实现:`apps/server/src/modules/mcp/`(Streamable HTTP 端点、会话表、资源与提示模板、能力统计)、
`packages/contracts/src/mcp.ts`(合同)、迁移 `0039_capability_stats`(`mcp_server_config` + `audit_event` 能力索引)、
界面 `设置 → Agent → MCP 服务端 / 能力清单`(`apps/web/app/components/settings/agent-tools.tsx`)。
依据:REQ R13、§10.3、§12.3。SDK:`npm:@modelcontextprotocol/[email protected]`,传输用 `WebStandardStreamableHTTPServerTransport`(Deno 下直接吃 `Request` / 返回 `Response`)。
SDK 的子路径类型靠 `typesVersions` 映射,`deno check` 不认,所以每个 `@modelcontextprotocol/sdk/…js` import 上方带 `// @ts-types="…"`(去掉 `.js` 的同名子路径)。
## 接入
端点:`<站点根>/api/v1/mcp`,Streamable HTTP(`POST` 发消息,`GET` 开服务端推送流,`DELETE` 结束会话)。
鉴权:`Authorization: Bearer <API Key>`。在「设置 → API Key」新建 Key,明文只显示一次。
Claude Code(或其它支持 Streamable HTTP 的客户端)配置,界面上「复制接入配置」给的就是这一段,密钥用环境变量占位:
```json
{
"mcpServers": {
"uhd-media-tool": {
"type": "http",
"url": "https://umt.example.lan/api/v1/mcp",
"headers": { "Authorization": "Bearer ${UMT_API_KEY}" }
}
}
}
```
命令行等价写法:`claude mcp add --transport http uhd-media-tool https://umt.example.lan/api/v1/mcp --header "Authorization: Bearer $UMT_API_KEY"`。
自检(不需要客户端):
```sh
curl -s -X POST https://umt.example.lan/api/v1/mcp \
-H "Authorization: Bearer $UMT_API_KEY" -H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}' -i
```
响应头里的 `mcp-session-id` 就是会话 ID,之后的请求都要带上。
## 暴露什么
| MCP | 来源 |
|---|---|
| tools | 能力注册中心里对这个 Key 可见的能力。工具名是能力 ID 把 `.` 换成 `__`(`download.submit` → `download__submit`);描述、inputSchema 取描述符与输入 zod schema;annotations 标 readOnly / destructive / idempotent |
| resources | `umt://settings/summary`(自动化开关、预算、Agent 上限,不含凭证)、`umt://decisions/pending`(未结束的待处理,最多 50 条)、`umt://subscriptions/overview`(各状态计数 + 最近 20 个订阅);需要 `read` scope |
| prompts | `triage_pending`、`acquire_title(title, year?)`、`diagnose_task(taskId)` |
工具可见性(`mcpToolVisible`):Key 的 scope 覆盖能力的 scope;没有 `organize.execute` / `admin` 时只给 read / preview 类。`agent.*`(派生子会话等)只在 Agent 会话内可用,不经 MCP 暴露。当前 41 个能力下:`read` 18 个,`read + organize.execute` 28 个,`admin` 37 个(2026-10-01 按 `mcpToolVisible` 核对;界面「可见工具」按实时注册表计算)。
## 安全边界:MCP 不绕过授权
- 每次工具调用都走 `ctx.capabilities.invoke(source="mcp", actorId="api_key:<id>")`,和 Web、内置 Agent 是同一条路径:输入 schema 校验 → 授权回调 → 服务端幂等键 → 操作账本 → 输出 schema → 审计。
- 授权只看库里的记录:propose / approve / execute 类要有覆盖它的**持续授权**(`automation_grant`)或有效的**单次审批**。API Key 不享有控制台管理员的"手动即授权",客户端或模型在参数里声称"已同意"不起作用。缺授权时工具返回 `isError` + `{"error":{"code":"unauthorized",...}}`,不执行。
- 单次审批只能在控制台登录后申请和批准(`POST/PATCH /approvals` 只接受管理员会话),MCP 客户端不能给自己开审批。
- 自动化总开关关闭或只读对账时,execute 类一律拒绝。
- 幂等键按会话归属(`id:v版本:mcp:<sessionId>:sha256(输入)`):同一会话同参数的有副作用调用只执行一次,重复调用直接返回账本里的结果。
- 输出与资源内容出服务端前再过一遍 `redactForModel`(cookie / passkey / token / apiKey 等键一律遮盖)。
- 审计:`audit_event` 里 `capability.invoke` / `capability.denied`,`payload.source = "mcp"`;会话建立写 `mcp.session.open`,配置变更写 `mcp.config.update`。
## 会话、限速与开关
- 会话与建立它的 API Key 绑定:换一个 Key 带同一个 `mcp-session-id` 返回 403。控制台 cookie 会话不能连 MCP(403)。
- 空闲 30 分钟回收;每个 Key 最多 8 个会话,全进程 256 个;超出返回 429 / 503。
- 限速(每个 Key、滑动一分钟):工具调用 60 次,HTTP 请求 600 次。工具调用超限返回 `rate_limited`,请求超限返回 HTTP 429。站点实时搜索、TMDB 等外部 IO 能力另受进程级并发与 15 s 超时约束(与 REST 共用 `sharedInvokeLimiter`)。
- 启用开关与允许的 API Key 在 `mcp_server_config`(`PATCH /mcp/config`,admin scope)。停用或把某个 Key 移出允许列表时,它的现有会话立即断开。列表为 `null` 表示所有未撤销的 Key;撤销 Key 后下一个请求即 401。
- 会话表在 API 进程内存里,重启后客户端需要重新 initialize(按规范收到 404 即重建会话)。多 API 进程部署时需要按会话粘滞路由。
## 能力统计与清单
| 端点 | 说明 |
|---|---|
| `GET /capabilities/stats?days=7` | 按能力汇总 `audit_event`:调用数、失败(error / invalid)、被拒绝、平均耗时、最近一次与调用来源(内置 Agent = agent / task,MCP,REST = web / cli)。`capability_invocation` 只记有副作用的调用,不用于统计 |
| `GET /capabilities/tools` | 描述符 + 工具名 + 输入 / 输出 JSON Schema + 授权状态(无需授权 / 有持续授权 / 需审批 / 仅 Agent 会话)+ 有效持续授权数 |
| `GET /mcp/status` | 启用、允许的 Key、在线会话(会话 ID 只回前 8 位)、最近 20 次 MCP 调用、限速、各 Key 组合可见工具数(admin scope) |
## 排障
| 现象 | 处理 |
|---|---|
| initialize 返回 403「MCP 服务端已停用」/「不在 MCP 允许列表里」 | 设置 → Agent → MCP 服务端,打开开关或勾选这个 Key |
| 工具列表里没有 execute 类工具 | Key 缺 `organize.execute`(或 `admin`)scope |
| 工具返回 `unauthorized` | 没有覆盖该能力的持续授权。发布收录策略会生成持续授权,或在控制台为计划申请单次审批后带 `approvalId` 调用 |
| 404「会话不存在或已过期」 | 空闲超时或进程重启,重新 initialize |
| 429 | 超过每分钟上限或会话数上限,稍后重试 / 关闭不用的会话 |
测试:`apps/server/test/mcp/mcp.test.ts`(官方 SDK client 经 Streamable HTTP 实连:工具列表按 scope 过滤、read 能力调用与脱敏、execute 无授权被拒与有授权后记账本、资源与提示模板、会话绑定、cookie / 停用 / 允许列表拒绝、统计与清单端点)。