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 吗?把这两个接口包成两个函数传给它不就行了?」

三个月后,这个方案留下了四个具体的坑:

  1. 换一个宿主就得重写一遍。助手先跑在内部 Web,后来客户要求接进 IDE 插件。同一套「两个函数」在 Web 里叫 search_incident,在 IDE 里因为命名冲突改成了 search_incident_v2,两边参数字段名也不同。业务逻辑没变,胶水代码写了两遍。
  2. 参数错了没人拦。模型把服务名写成「支付服务」,而不是后端要的 payments。请求发出去了,返回 400,助手把错误信息原样念给用户。有用的校验本应发生在发请求之前。
  3. 没人知道是谁批准的这次调用。事后复盘问「这次查询查了哪个租户的数据?」,答案是不知道——数据集有人在服务端做 ACL,健康接口没有,全靠调用方自觉。
  4. 只读查询和一个写操作混在一起。某次迭代偷偷加了一个可以创建工单的函数,模型和前两个函数一起看到它。一次误触发创建了 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。

精确定义:三个角色,两种能力方向

规范对角色的划分是固定的:

能力在两个方向上流动,方向很重要:

方向 内容 谁控制
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 负责"连接",不负责"理解"和"治理"。 它把能力变成可发现、可协商、可授权调用的协议消息;它不管模型怎么想,也不替你设计审批流。

失败注入:四种边界错误

下面四种是真实项目里最常见的越界方式,先看能不能自己说出它们分别踩了哪条边界:

  1. 只把函数名和一句意自然语言描述发给模型,没有 input schema —— 非法参数直达业务系统。
  2. Tool 的返回结果里直接带上内部资源 URI/地址,用户可以拿着它绕过租户和权限再取一次。
  3. 把 Server 返回 success 当成外部副作用已完成——HTTP 200 只说明响应回来了。
  4. 因为"MCP 是标准"就把自家模型的 Function Calling 格式整套搬过来,换模型时同步崩。

它们分别踩的边界是:

  1. 没有 input schema → MCP 责任。Tool 必须携带 inputSchema,且服务端必须在执行前校验。少了这一步,脏数据会直达业务系统。
  2. 结果里塞裸地址 → MCP 与资源治理共同责任。Tool 结果应该返回引用(第 05 章会做),而不是可直接二次使用的内部 URI。
  3. success ≠ 副作用完成 → 语义分层错误。协议层收到合法响应,和业务动作真的做完了,是两件事(第 05 章的三层成功模型)。
  4. 照搬 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、审计系统六个节点,画出每条箭头的归属层。验收标准:任何一条你画不出归属的箭头,就是下一章需要澄清的责任——把它单独列出来。

本章检查点

现在能解释什么

你现在能说清「为什么不能只用 Function Calling」:它解决意图表达,不解决发现、授权、结果协议、生命周期这四件在真实接入里必然要回答的事。你也能说清 MCP 不是什么——它不负责模型思考、不负责上下文裁剪、不负责 Agent 循环。下一章把角色分工落到具体消息上:一条连接到底是怎么建立和收尾的。

进入 keel 阅读