MCP 协议与工具生态
上一篇讲了「工具怎么写」。但这里有个尴尬的现状:每家 Agent 平台各搞一套工具协议。 同一个「查订单」能力,你要给 Claude 写一遍、给 Cursor 写一遍、给 OpenClaw 再写一遍——每一次都要重新适配接口、鉴权、数据格式。
这一篇回答一个核心问题:怎么让一个工具写一次,就能被所有 Agent 平台复用。 MCP(Model Context Protocol)就是答案。读完你会发现,MCP 的本质是一个协议标准,它解决的是「互操作」而非「实现」,而用好它的关键,藏在几个容易被忽视的工程细节里。
1. MCP 是什么:工具侧的「USB-C」
1.1 一个「N×M 问题」
在 MCP 出现之前,AI 工具生态是一个典型的 N×M 问题:
- 有 N 家 Agent 平台(Claude Desktop、Cursor、OpenClaw、Coze、以及无数内部平台)
- 有 M 个工具(查天气、查订单、建歌单、发邮件……)
- 每个工具要给每个平台写一遍适配 → 一共 N×M 个适配器
N 和 M 都在涨,适配器数量就指数级爆炸。这正是当年手机充电接口乱象的重演——每家厂商一个接口,一根线一个样。
1.2 MCP 做的事,和 USB-C 一模一样
MCP(Model Context Protocol)——「Model」指大模型,「Context」指它需要的上下文能力。它做的事,和 USB-C 对手机接口做的事完全相同:统一协议,一次开发、处处可用。
| 之前 | 之后 | |
|---|---|---|
| 手机接口 | 各家各搞一套,一款手机配一堆线 | USB-C 统一,一根线走天下 |
| AI 工具 | 每个工具给每个平台写一遍适配 | MCP 统一,一个工具多平台复用 |
一句关键认知,务必记住:MCP 解决的是「工具的互操作」,不是「工具怎么实现」。 你写一个 MCP Server 暴露工具,任何支持 MCP 的平台都能直接调用——平台不再关心你的工具用什么语言写、部署在哪台机器、靠什么鉴权。协议一统一,这些「实现细节」就被隔离在 Server 内部了。
1.3 Skill 和 MCP Server 的关系
这里澄清一对常被混淆的概念:
- 协议层:MCP —— 统一的通信规范(本文的主角)。
- 封装层:Skill —— 各家平台对「一个可上架的 MCP Server」的元数据封装(名字、简介、权限声明、图标等)。
对开发者的含义浓缩成一句:写 MCP Server = 写 Skill 的 90%,剩下 10% 是各平台的上架元数据。 你把最核心的「能力暴露」这一件事做对(标准 MCP 协议),剩下就是套各平台的发布壳。
2. MCP 三件套:Tool / Resource / Prompt
MCP 规定 Server 可以向 Client 暴露三种能力,三者的职责是正交的、不可混用的。这是理解 MCP 的核心,先看分界:
| 成员 | 职责 | 谁决定调用 | 有无副作用 |
|---|---|---|---|
| Tool | 让 LLM「做事」 | LLM 自主(看 description) | ✅ 有 |
| Resource | 让 LLM「读数据」 | 按 URI 读取 | ❌ 只读 |
| Prompt | 让用户「选模板」 | 用户主动触发 | 参数化模板 |
2.1 三者为什么不能混
很多人一开始把所有能力都堆成 Tool。这是错的,因为三者的调用模式根本不同:
- Tool 有副作用,触发权在模型——模型看 description 自己决定调不调;
- Resource 无副作用、是数据,触发方式是按 URI 引用——客户端可以缓存它、在多个地方复用;
- Prompt 是给用户用的模板,触发权在人——用户主动选一个模板来快速开始。
混用的后果很直接:把「查订单状态」(只读)写成 Tool,客户端就没法缓存它、每次都当成「有副作用的动作」重新执行;把「发邮件」(有副作用)写成 Resource,模型可能错误地以为它「只是读个数据、可以随便调」。
2.2 一个选型决策树
新增一个能力时,按下面顺序问自己,几秒就能定:
会修改外部状态?(发邮件、建歌单、写数据库)
→ 是:放 Tool
→ 否:是静态/半静态数据?(读日志、读配置、读某行)
→ 是:放 Resource
→ 否:是可复用的 Prompt 模板?
→ 是:放 Prompt
→ 否:可能不属于 MCP,放回应用层一个常见错误:把「查订单状态」写成 Tool。查询是只读的,放 Resource 能让客户端做缓存、按 URI 引用。判断技巧很实用:一个能力调用 100 次、100 次结果都一样,通常该放 Resource;结果随调用改变(有副作用或依赖实时状态),才放 Tool。
3. 一次 MCP 调用的完整报文
MCP 底层是 JSON-RPC 2.0。把最常用的 tools/call 一次完整交互拆开看,你就摸清了它的「语言」:
Client 发起调用:
{
"jsonrpc": "2.0",
"id": 42,
"method": "tools/call",
"params": {
"name": "create_playlist",
"arguments": {"artist": "周杰伦", "size": 15}
}
}Server 返回结果:
{
"jsonrpc": "2.0",
"id": 42,
"result": {
"content": [{"type": "text", "text": "歌单已创建:https://..."}],
"isError": false
}
}3.1 一个被低估的关键细节
注意上面返回里的 isError 字段。这里藏着 MCP 工程里最容易被忽略、也最容易踩坑的细节:
业务失败,要用 result.content + isError: true 回传,而不是抛异常让 Client 看到协议层的 error。
这两条路,对「人」来说都是「出错了」,但对模型来说天差地别:
- 走
result.content+isError: true→ 模型能读懂业务错误信息(「歌单服务繁忙,请稍后重试」),它能据此对用户说人话、或决定重试; - 走抛异常 → Client 看到的是协议层错误(「JSON-RPC error -32000」),模型只会一脸懵,因为它拿到的是「通信故障」而非「业务失败」,它不知道该跟用户说什么。
一句话:isError 是「给模型看的业务信号」,协议 error 是「给开发者看的通信故障」。两者要分清,业务失败永远走前者。
4. 怎么守安全 + 可靠(本篇命门)
MCP 的使命是「开放互联」,而开放天然带来风险。一个能被任意平台调用的工具,一旦鉴权出问题、幂等没做好,后果就是真实的(串号、重复扣款、拖垮下游)。六条工程规约,核心就三件事:
4.1 安全:鉴权隔离
这是 MCP 工具最致命的坑,因为它直接影响数据和资金安全。
反例(血的教训):Skill 进程全局持有一个 API Key——所有用户的操作用同一个凭证。结果就是:所有人的歌单都建在了开发者自己的账号下;或者一个人的越权操作,会以「开发者账号」的权限被执行,破坏范围被放大到不可控。
正例:从用户上下文里取各自的凭证,按用户隔离。 每个用户的操作,用他自己的身份和权限去执行。同时,权限声明做在平台侧——比如声明「本 Skill 只能访问 music.163.com」,平台据此做沙箱,把工具的网络/文件能力限制在最小范围。这和第 10 篇「权限最小化」是同一套逻辑。
4.2 可靠:错误结构化 + 幂等 + 熔断
三条,每一条对应的都是真实的生产事故:
一、错误结构化回传。 业务失败走 return(isError: true),协议失败才走 throw——就是上一节讲的那个区分。做对这步,模型才知道「业务错了」和「通信断了」是两回事。
二、幂等。 创建类工具必须幂等。原因很具体:模型在网络重试、用户追问「你确定建好了吗?」时,会重复发起同参数调用。如果你的「建歌单」不幂等,就会出现「问一次、建了 3 个歌单」,更严重的是「重复扣款」。幂等的标准做法:同参数先查缓存/去重表,已存在则直接返回已有结果,不重复创建。
三、熔断 + 限流。 下游 API 挂了,你的 Skill 要能降级(返回友好错误、或走缓存),而不是傻等超时、或疯狂重试把下游打爆,拖死整个链路。工具是「被模型高频调用」的,一旦不设限,一个坏掉的工具能拖垮一整个 Agent。
4.3 高效:描述写「使用时机」
这一条直接决定「模型调不调你的工具」,值得单独强调:
LLM 决定调不调你的工具,唯一依据就是工具的 description。 模型看不到你的实现代码,它只看到你写的描述文字。
所以 description 要写「使用时机 + 输入示例 + 不适用场景」:
❌ 差:「调用网易云音乐 API 创建歌单」 ← 这是给"人"看的,模型不知道啥时候该调
✅ 好:「当用户想创建/生成一个歌单时调用。
输入:artist(歌手名,如"周杰伦")、size(歌单曲目数,如 15)。
不适用:用户只是想"听歌"而不是"建歌单"时不要调用。」差描述说「我是谁」(调用什么 API),好描述说「何时用我、怎么用、何时别用」。 前者是废话,后者才真正帮助模型在正确时机做出正确的调用决策。
5. 最常踩的几个坑
| 现象 | 根因 | 正确姿势 |
|---|---|---|
| 客户端连上就断 | stdio 模式下,stdout 被日志/Banner 污染(协议信道被非协议输出打断) | 关 Banner,日志走 stderr 或文件 |
| LLM 不调我的工具 | description 太抽象,模型不知道何时该用 | 写「使用时机 + 示例 + 不适用场景」 |
| 重复建了 3 个歌单 | 没做幂等,重试触发重复创建 | 同参走缓存/去重 |
| 多用户共用账号 | 全局共享一个 API Key | 按用户隔离鉴权 |
| 模型报「通信失败」而非「业务失败」 | 业务失败抛异常,没走 isError | 业务失败 return + isError: true |
另有一个容易漏的点:stdio 模式下的 stdout 是「协议信道」,绝不能用来打印日志。 你打印的一行 [INFO] server started,在客户端看来就是一段「非法 JSON-RPC」,会导致连接直接断开。所以日志一律走 stderr,把 stdout 干干净净留给协议。
6. 记住这一句
MCP = LLM 世界的 USB-C;Tool/Resource/Prompt 三件套各司其职;Skill = 打包好的 MCP Server。生产级工具的三条命脉:描述写「使用时机」(高效)、鉴权按用户隔离(安全)、错误结构化 + 幂等 + 熔断(可靠)。
下一篇预告:工具、生态都齐了,但真实任务往往复杂到「一个 Agent 干不完」——要拆成一队分工、还要保证状态中途不丢。这就到了 07 · Agent 编排与状态持久化。