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 的情况下验证授权,也没法在没有任务状态的情况下验证恢复。
验收清单
每一条都必须能被一组测试证明,而不是靠"我看了一下没问题":
- 未授权主体看不到敏感工具(清单过滤),也调不动(调用校验),两条路径同一个判定
- 跨租户请求被拒,且失败原因可区分
- 给别的服务签发的凭据(不同 audience)被拒
- 同一个幂等键重试不会重复创建工单或发送通知
- 进程重启后,处于
input_required的任务仍在,且能继续走到completed - 每个诊断结论都有资源引用,或被明确标注为推断
- 一次调用的全部事件共享同一个 trace_id
- 替换教学传输和 HMAC 之后,契约测试仍然通过(说明传输与上层解耦成功)
- 七个兼容维度都有断言,全部进 CI
必做的失败注入
这一份比验收清单更重要——能被演示的失败,才说明防线真的在那:
| 注入 | 期望结果 |
|---|---|
| 用只读主体的凭据调写工具 | 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:
- 换一个宿主,还用重写一遍适配吗?
- 非法参数能不能在进入业务系统之前被拦住?
- 你能回答「这次调用查了哪个租户的什么数据」吗?
- 写操作和读操作有没有明确的区分,并且被强制执行?
四个问题都能给出具体证据(不是"应该没问题"),这门课就真的上完了。