KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA
01 · MCP 解决什么,不解决什么? — keel 龙骨
## 现场:一次接入评审会
现场:一次接入评审会
Acme 有一个内部「事件诊断助手」:值班工程师在对话框里问「payments 服务现在健康吗」,助手自己去查数据回答。今天评审的是把它接到两个内部系统:
| 系统 | 现有接口 | 谁在用 |
|---|---|---|
| 事件库 | GET /v1/incidents?service=&since= |
诊断助手 |
| 集群健康 | GET /v1/health/{service} |
诊断助手、巡检脚本 |
| 内部 runbook | wiki 页面,需 SSO | 诊断助手、人工值班 |
会上有人提议:「模型不是支持 Function Calling 吗?把这两个接口包成两个函数传给它不就行了?」
三个月后,这个方案留下了四个具体的坑:
- 换一个宿主就得重写一遍。助手先跑在内部 Web,后来客户要求接进 IDE 插件。同一套「两个函数」在 Web 里叫
search_incident,在 IDE 里因为命名冲突改成了search_incident_v2,两边参数字段名也不同。业务逻辑没变,胶水代码写了两遍。 - 参数错了没人拦。模型把服务名写成「支付服务」,而不是后端要的
payments。请求发出去了,返回 400,助手把错误信息原样念给用户。有用的校验本应发生在发请求之前。 - 没人知道是谁批准的这次调用。事后复盘问「这次查询查了哪个租户的数据?」,答案是不知道——数据集有人在服务端做 ACL,健康接口没有,全靠调用方自觉。
- 只读查询和一个写操作混在一起。某次迭代偷偷加了一个可以创建工单的函数,模型和前两个函数一起看到它。一次误触发创建了 6 张重复工单。
注意这四个坑的共同点:它们都不是"模型不会调函数"的问题。模型从一开始就能生成正确的调用意图。失败发生在意图之后。
直觉模型:把 Adapter 挪到哪一层?
先建立一张图,理解 MCP 在系统里的位置:
没有 MCP 时(每接入一个宿主,写一次适配):
Web 助手 ──adapter A──► 事件库 / 健康 API / wiki
IDE 插件 ──adapter B──► 事件库 / 健康 API / wiki
巡检机器人 ──adapter C──► 事件库
M × N 问题:M 个宿主 × N 个数据源,要写 M×N 份胶水。
有 MCP 时(适配成本收敛到两侧):
宿主 ──► MCP Client ──► MCP Server ──► 真实系统
(统一协议) (只写一次)
官方给的类比非常直白,值得记住:MCP 之于 AI 应用,就像 USB-C 之于电子设备——一个标准化的连接口,让任意 AI 应用能连到任意外部系统,而不是为每一对组合定制一根线(来源:modelcontextprotocol.io 入门页,检索于 2026-09-29)。
于是 M×N 变成 M+N:数据源侧实现一次 Server,宿主侧实现一次 Client。
精确定义:三个角色,两种能力方向
规范对角色的划分是固定的:
- Host(宿主):发起连接的 LLM 应用,例如上面的 Web 助手、IDE 插件。负责用户体验、安全策略和多连接编排。
- Client(客户端):宿主内部的一条连接。一个宿主可以同时开多个 Client,每个 Client 只对一个 Server。生命周期由 Client 管。
- Server(服务端):暴露能力的一方,例如本课程的
incident-bridge。
能力在两个方向上流动,方向很重要:
| 方向 | 内容 | 谁控制 |
|---|---|---|
| Server → Client | Resources(上下文与数据)、Prompts(模板化流程)、Tools(可执行函数) | Server 决定"提供什么",但调用需用户同意 |
| Client → Server | Elicitation(服务端向用户请求补充信息) | Client 决定"给不给",用户决定"答不答" |
有一条架构约束容易被忽略,但它决定了你能做什么样的安全设计:Server 不应读取整段对话,也看不到同宿主下其他 Server 的数据。每条 Client–Server 连接是隔离的。
这条不是建议而是设计原则。它带来的直接结论是:你可以把内部 runbook Server 和第三方搜索 Server 挂在同一宿主下,第三方无法通过协议得知内部 Server 返回了什么。
它不是什么:五层边界
回到现场那四个坑。它们里没有一个应该由 MCP 修,分清边界才能知道该去哪一层改:
| 问题 | 归属层 | 说明 |
|---|---|---|
| 一次运行怎么继续、暂停、恢复 | Agent Harness | MCP 不定义 Agent 循环 |
| 当前模型怎么表达"我想调某个能力" | Tool Calling | 模型侧能力,MCP 不干涉 |
| 哪些信息该放进上下文窗口 | Context Engineering | MCP 只负责搬运,不负责裁剪 |
| 外部能力怎么被发现、授权、调用 | MCP | 本课程范围 |
| 两个独立 Agent 怎么互相派任务 | A2A 等 Agent 间协议 | 另一个问题域 |
对照现场:坑 3(不知道查了谁的数据)一部分属于 MCP——租户和 Scope 应该在调用边界上校验;另一部分属于权限治理。坑 4(写操作混进只读)属于你把什么注册进了 Server,是设计决策,不是协议缺陷。坑 2(参数错)落在 MCP——因为 Tool 必须声明 input schema。
一个可靠的判断口诀:MCP 负责"连接",不负责"理解"和"治理"。 它把能力变成可发现、可协商、可授权调用的协议消息;它不管模型怎么想,也不替你设计审批流。
失败注入:四种边界错误
下面四种是真实项目里最常见的越界方式,先看能不能自己说出它们分别踩了哪条边界:
- 只把函数名和一句意自然语言描述发给模型,没有 input schema —— 非法参数直达业务系统。
- Tool 的返回结果里直接带上内部资源 URI/地址,用户可以拿着它绕过租户和权限再取一次。
- 把 Server 返回
success当成外部副作用已完成——HTTP 200 只说明响应回来了。 - 因为"MCP 是标准"就把自家模型的 Function Calling 格式整套搬过来,换模型时同步崩。
它们分别踩的边界是:
- 没有 input schema → MCP 责任。Tool 必须携带
inputSchema,且服务端必须在执行前校验。少了这一步,脏数据会直达业务系统。 - 结果里塞裸地址 → MCP 与资源治理共同责任。Tool 结果应该返回引用(第 05 章会做),而不是可直接二次使用的内部 URI。
success≠ 副作用完成 → 语义分层错误。协议层收到合法响应,和业务动作真的做完了,是两件事(第 05 章的三层成功模型)。- 照搬 Function Calling 格式 → 分层没做干净。MCP 是连接协议,供应商 SDK 是模型接口,两者可以叠加但不能合并。
项目动作:先认识五个稳定对象
打开 project/src/mcp_bridge/contracts.py。这个文件定义了后续九章都会引用的四个对象:
@dataclass(frozen=True)
class CapabilitySet:
tools: bool = False
resources: bool = False
prompts: bool = False
tasks: bool = False
extensions: tuple[str, ...] = ()
@dataclass(frozen=True)
class ToolDefinition:
name: str
description: str
input_schema: dict[str, Any]
required_scopes: tuple[str, ...] = ()
readonly: bool = True
CapabilitySet 回答"这一端愿意处理什么",ToolDefinition 回答"这个工具是什么、谁可以调、要什么参数",JsonRpcRequest / JsonRpcError 回答"这条消息是请求还是通知、错了怎么表达",ToolResult 回答"结果给模型看什么、给程序看什么"。
传输、SDK、供应商适配都应该围绕这四个对象变化,而不是反过来。 记牢这一点,后面六章你会反复验证它。
生产替换点
| 教学实现 | 生产替换 |
|---|---|
内存注册表 self.tools |
版本化的 server manifest / registry |
固定 required_scopes |
策略引擎(OPA / Cedar / 自研),按租户动态计算 |
| 进程内函数调用 | stdio 或 Streamable HTTP server |
| 两个硬编码工具 | 灰度清单 + 按环境开关的工具发布流程 |
练习与验收
练习(有可观察结果):画一张你所在项目的边界图,至少标出 Host、MCP Client、MCP Server、模型、业务 API、审计系统六个节点,画出每条箭头的归属层。验收标准:任何一条你画不出归属的箭头,就是下一章需要澄清的责任——把它单独列出来。
本章检查点
- 现场四个坑里,哪几个是 MCP 应该解决的,哪几个不是?说出判断依据。
- 为什么 Server 看不到整段对话这条约束,能让你在同一宿主下安全地混挂内部和第三方 Server?
ToolDefinition.readonly是协议要求的,还是本课程的设计决策?在server.py里搜索它被读取的位置——你会发现没有任何一处用它做判断。一个声明了却没人检查的字段,等于没有约束。这说明什么?
现在能解释什么
你现在能说清「为什么不能只用 Function Calling」:它解决意图表达,不解决发现、授权、结果协议、生命周期这四件在真实接入里必然要回答的事。你也能说清 MCP 不是什么——它不负责模型思考、不负责上下文裁剪、不负责 Agent 循环。下一章把角色分工落到具体消息上:一条连接到底是怎么建立和收尾的。