KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA

10 · Capstone:把 Incident Bridge 提升为可交付形态 — keel 龙骨

前九章各自独立地解决了问题。本章把它们合并成一个有具体边界、可执行、可验收的交付物。

前九章各自独立地解决了问题。本章把它们合并成一个有具体边界、可执行、可验收的交付物。

业务目标与硬约束

为一个企业事件诊断助手提供 MCP 接入。硬约束一句话:助手只能做只读诊断,除工单创建以外的任何写操作都不允许,且工单创建必须幂等。

具体到 API 层面:

约束 含义
只读优先 incident.search、service.health 只读;新增能力默认 readonly=True 并在调用路径上真的检查它
数据隔离 工具按 tenant + scope 过滤,跨租户请求必须失败
引用可溯 每个诊断结论必须带资源引用,或明确标注为推断
长任务可恢复 Diagnosis 支持 input_required、轮询、取消、重启恢复
幂等 同一幂等键重试不会重复创建工单或发送通知
可解释 每次调用的事件能被同一个 trace_id 串起来

最小交付范围

对应到项目里已有的 API 与本章要新增的部分:

# 交付物 现成的 API 你要补的
1 incident.search ✅ call_tool 租户与时间范围校验、返回条数上限
2 service.health ✅ call_tool 输出脱敏:不返回内部地址
3 knowledge://runbook/{id} 声明在 discover 里 实现 ACL 校验与引用返回
4 diagnose 长任务 ✅ start_diagnosis / provide_diagnosis_input / complete_diagnosis 补 trace_id 串联与持久化路径
5 兼容矩阵 ✅ examples/08_compatibility.py 扩到覆盖第 09 章七维表

注意第 4 项:这三个方法在本项目中已经存在,配套的 TaskStore 也支持持久化和幂等键。你要做的不是重写它们,而是把它们接进真实部署(传 path)、贯穿 trace_id、并写出验收测试。

分阶段实施

建议按下面四步走,每一步都要求「测试从红变绿」:

① 收紧清单(第 03、04 章)
   把 discover 的可见列表做成按 principal 过滤 —— 已有 list_tools 实现
   断言:只读主体看不到任何写工具;猜名字调用也被拒
        ↓
② 打通授权(第 07 章)
   把 shared secret 换成按 Server 注入的 audience,去掉 DEFAULT_AUDIENCE 共用常量
   断言:给 runbook-server 签的票不能在 incident-bridge 上用
        ↓
③ 长任务可存活(第 08 章)
   TaskStore 接真实路径;重启后可以走到 completed
   断言:模拟重启后 input_required 任务仍在,且能继续到 completed
        ↓
④ 兼容与观测(第 09 章)
   七维矩阵 + trace_id 串联
   断言:一次调用的所有事件共享 trace_id

每一步失败时会暴露前一层的漏洞。这个顺序不是随意的:你没法在没有 permission filtering 的情况下验证授权,也没法在没有任务状态的情况下验证恢复。

验收清单

每一条都必须能被一组测试证明,而不是靠"我看了一下没问题":

必做的失败注入

这一份比验收清单更重要——能被演示的失败,才说明防线真的在那:

注入 期望结果
用只读主体的凭据调写工具 PermissionError: missing scope
把 token 的租户改成 globex PermissionError: tenant mismatch
用 ttl_seconds=-1 造一个过期 token PermissionError: expired token
用别的服务 audience 签发的票据 PermissionError: audience mismatch
缺必填字段调用 返回 isError=True 的结果,不是抛异常
对 working 状态的任务提交输入 ValueError: task is not waiting for input
已完成的任务再调 complete 静默返回既有结果,不被改写
用一个服务于两个 scope 的工具 + 只持一个 scope 的凭据 必须被拒(第 04 章那个真实漏洞)
关闭传输后继续发送 ConnectionError,不静默丢弃

生产替换清单(全课汇总)

把本课程所有教学替身收在一张表里。交付评审时这份清单就是自查依据:

教学实现 生产替换 章节
内存注册表 版本化 server manifest / registry 01
进程内函数调用 stdio 或 Streamable HTTP 02、06
只在握手时校验版本 每请求校验 _meta,返回 -32022 + supported 03
extensions=("tasks",) 数组 {"io.modelcontextprotocol/tasks": {}} 映射 03
固定 required_scopes 策略引擎动态计算 04
文本结果 content / structuredContent / isError 三通道 05
input_required 轮询 Client 的 Elicitation 能力 + UI 确认组件 05
无重连与退避 受控重试 + 区分可重试请求 06
HMAC bearer + 共享密钥 OAuth 2.1 + 短期 token + JWKS 轮转 07
DEFAULT_AUDIENCE 常量 每 Server 规范 URI,配置注入 07
内存 + 整体写 JSON 任务表 + 事务 + 租约 + TTL 08
EventRecorder 列表 OpenTelemetry + SIEM + Trace Context 传播 09

完成后往哪走

做完 Capstone,你能负责任地说自己懂 MCP 的连接这一层了。但请把边界再在墙上刻一遍——下面这些不是 MCP 的职责,它们是独立的能力:

你还需要的 属于哪一层
让 Agent 能持续运行几十步而不跑偏 Agent Harness
决定上下文窗口里放什么、怎么压 Context Engineering
防止提示词注入、危险命令、数据外泄 Safety Controls
让长时延、可阻塞的任务在生产里稳定运行 Real-world Execution
多个 Agent 之间怎么分工和交付 多 Agent 协作协议

MCP 解决的是连接,不是系统。 一位工程师接好了 MCP,得到的是一条干净、可发现、可授权的通道;通道那头的东西稳不稳定、通道前面的 Agent 聪不聪明,是另外的问题。

这也是本课程的最后一句提醒:当你发现某个需求「MCP 好像能解决但总差点意思」时,先问它属于上表的哪一行——大概率它在别的层。

交付自检

最后一步,用第 01 章开头那四个坑对照你的 Capstone:

  1. 换一个宿主,还用重写一遍适配吗?
  2. 非法参数能不能在进入业务系统之前被拦住?
  3. 你能回答「这次调用查了哪个租户的什么数据」吗?
  4. 写操作和读操作有没有明确的区分,并且被强制执行?

四个问题都能给出具体证据(不是"应该没问题"),这门课就真的上完了。

进入 keel 阅读