本文是「Agent 开发实践与思考」系列第 9 篇。系列目录:
- 2024
- 2025
- 01-28 长任务不失忆:上下文压缩、交接文档与子任务隔离
- 02-25 MCP 用了三个月:工具标准化之后 Agent 设计变了什么(本篇)
MCP 解决的是什么问题
假设你有 N 个 Agent 应用,比如自己写的 Agent、Claude Desktop、Cursor,要接 M 个系统,比如工单、数据库、文件系统、内部 API。没有标准协议时,每个应用都要为每个系统写一遍适配代码,工作量是 N 乘 M。MCP 把工具接口标准化之后,每个系统只写一个 server,每个应用只实现一次 client,工作量变成 N 加 M。MCP就是一套开源的协议,自带 SDK 和一批参考实现,所以在我观察到的发布后三个月里,传播速度很快。这里说的是当时的实现和生态,不是协议已经解决了所有互操作问题。
架构三分钟版
MCP 有三个角色:
- Host:用户直接用的 Agent 应用,比如 Claude Desktop,或者你自己写的 Agent。
- Client:Host 内部的连接器,一个 client 对一个 server,维持一条会话。
- Server:一个轻量进程,通过统一协议向外暴露三样东西:tools(可调用的函数)、resources(可读取的数据,比如文件、日志)、prompts(预设的提示词模板)。
在本文记录的版本里,协议消息使用 JSON-RPC 2.0。连接建立时会进行能力协商,server 声明自己提供哪些 tools、resources、prompts,client 再按需拉取工具列表和描述。规范、TypeScript 和 Python 的 SDK 以及一批参考 server 是一起开源的,看完文档当天就能跑通一个 demo。具体字段、能力和生命周期行为仍应以使用时对应版本的规范和 SDK 为准。
当时常见的传输方式有两种。stdio:Host 把 server 作为子进程拉起来,走标准输入输出通信,适合本地工具,配置里写一条 npx some-mcp-server 就能跑。SSE(Server-Sent Events):server 是独立的 HTTP 服务,请求走 POST,服务端推送走 SSE 长连接,适合远程部署。传输方式本身不替 Host 解决认证、授权、超时和网络隔离问题,远程部署时这些都要由具体实现补上。
做过编辑器插件的人会觉得这个结构眼熟。它和 LSP(Language Server Protocol)的思路基本一样:语言服务器实现一次,VS Code、Vim、JetBrains 都能用;MCP server 实现一次,所有支持 MCP 的 Host 都能用。LSP 把编辑器从”每个编辑器为每种语言各写一套支持”里解放出来,MCP 想对工具做同样的事。
三个月里我实际怎么用
最直接的用法是把内部系统包成 MCP server。工单查询那个工具包,原来只在我自己的 Agent 里用,格式是我自定义的。花了一个下午按 MCP 的 SDK 重写了一遍,tool 的 schema 和描述基本照搬,只是把注册和调用换成了协议规定的 JSON-RPC 消息。
改完之后的收益很直接:
- 我自己的 Agent 继续用,改成调 MCP server,不再直接 import 函数。
- Claude Desktop 的配置里加两行,同一个 server 直接能用,写周报查数据顺手很多。
- Cursor 也支持 MCP 了,写代码的时候可以让它直接查内部接口的返回结构,不用手动贴示例。
举个具体的场景。上周排查一个线上问题,我在自研的Agent里直接让它查相关工单和最近的变更记录,它调了几次 server,把结果汇总成一段结论。放在以前,这个流程是我登内部系统、查数据、导出、再贴给模型,中间的搬运全是手工。现在搬运这步没了,我只负责看结论对不对。
一份工具代码,三个宿主复用,中间没有适配层。这就是协议标准化的全部意义,说出来不新鲜,但省掉的是实打实的重复劳动。
另一个变化是生态。发布时官方给了一批参考 server,文件系统、GitHub、Postgres、Puppeteer 这些都有。到我记录这篇文章时,社区已经出现了不少第三方 server,但它们的质量、维护状态和权限模型并不一致。接新系统有时能从”写一周适配”变成”找个现成 server,配两行”,前提是先检查实现和权限。
新矛盾一:工具描述吃掉上下文
server 接得多了,第一个问题来得比预想快。
在我使用的 Host 实现里,模型的工具列表会从已连接 server 拉取工具描述并拼接起来,每个工具带名字、功能描述、参数 schema。一个 server 少则几个、多则二三十个工具。我最多的时候挂了二十几个 server,粗算下来光工具描述就超过一万 token。这一万 token 在每一轮对话里都要原样发出去,不管这次任务用不用得上。
这带来两个具体后果。一是成本和延迟。工具定义在消息前缀里,占上下文,也占首 token 的等待时间。从缓存角度看更麻烦,我在上下文窗口的成本课里写过,前缀稳定缓存才能命中,而频繁切换挂载的 server 组合会改变前缀,缓存直接作废。二是准确率。几十个名字相近的工具摆在一起,模型挑错的概率明显上升,这和只挂五六个工具的时候不是一个量级。
一个我实际遇到的 bad case:同时挂了”查工单”和”查订单”两个描述相近的工具,用户报了工单号,模型把工单号当订单号去查,返回空之后没有回头检查自己是不是选错了工具,而是换了一个更不相干的工具再试。工具少的时候这类错误很少见,工具一多,描述之间的区分度不够,错误就来了。
目前的对策都比较朴素:
- 按需挂载。按任务类型启用 server 子集,查数据的任务不挂浏览器类 server。麻烦的是现在多数 Host 的开关粒度很粗,得手动维护几套配置。
- 工具描述继续按老手艺写。MCP 没有改变”工具描述是写给模型看的产品文档”这个事实,名字动词化、写清什么时候用和什么时候别用,这些在工具调用踩坑记里写过的原则一条都没过时。server 一多,描述质量的影响反而被放大了。
本质上,这是把”工具发现”变成了新的工程问题:工具就挂在那里,模型怎么知道这次该用哪个。这个问题MCP协议没处理,留给了 Host 的工程设计。
新矛盾二:第三方 server 的权限
第二个问题是 MCP server 可能是别人写的、最终跑在你机器上、拿着你的凭据干活的代码。
官方参考实现里有一个文件系统 server,功能是让模型读写本地文件。它按你配置的目录参数工作,给什么目录就能读写什么目录。我看到不少人配置时图省事,直接给了 home 目录。这意味着模型只要愿意,可以读到 SSH 私钥、浏览器 cookie、各种云服务的本地凭据。如果 server 本身或者工具返回的内容里混进了恶意指令,数据出去就是一次工具调用的事。
第三方 server 的风险链路大致是这样:server 代码本身可能有漏洞或恶意逻辑;server 会拿到你配置的 API key;模型可能被工具返回的内容注入指令,被诱导调用越权的工具。每一环都不是 MCP 特有的问题,但 MCP 让”装一个来路不明的工具进程”变得非常方便,风险跟着一起普及了。
我现在的几条规矩:
- server 当不可信依赖对待。只装读过代码或者来源可靠的,锁定版本,不追 latest。
- 目录白名单。文件系统类 server 只给工作目录,绝不给 home,敏感目录显式排除。
- 凭据最小化。给 server 的 API key 单独申请,权限收窄到够用,不用个人主账号的 key。
- 敏感操作二次确认。删除、转账、对外发送这类工具调用,在 Host 层强制人工确认,不靠模型自觉。
顺带说一句,自己写的 server 也要按同样的标准约束。权限事故不一定来自恶意代码,自己代码里的 bug 造成越权读写,后果是一样的。我给内部 server 也加了目录白名单,而不是因为它是我自己写的就放行。
这些都不是 MCP 协议自动提供的完整安全边界。协议主要描述消息和能力交互,具体的权限确认、凭据隔离、目录限制、审计和用户审批要看 Host、server 及部署环境的实现。Host 不能只把工具列表交给模型,还要决定哪些 server 可以启动、哪些工具需要确认,以及工具返回的内容只能作为不可信数据处理。权限这层规矩得 Host 和用户自己立。
自己的一些判断
三个月用下来,我对 MCP 的定位是:它解决”访问”问题,不解决”选择”问题。
可访问性确实是真问题。N 乘 M 的适配成本被消掉了,工具生态第一次在事实上有了统一接口,这件事的价值会随时间越来越大。但工具接进来之后,模型能不能在几十个工具里挑对那个、填对参数、处理对错误,MCP 一概不管。我观察到的现状是:工具数量上来之后,选错工具成了 bad case 里的主要来源,占比超过了参数填错。工具选择的准确率,是标准化之后浮出水面的新瓶颈。
所以我的判断是,MCP 不会让 Agent 自动变强,它让 Agent 的工具供给变便宜了。供给便宜之后,瓶颈转移到别处。
收尾
工具标准化之前,各家 Agent 的差异里有一部分是”我接了哪些别人接不了的系统”。如果 Host 和 server 都实现了相同的协议,这部分接入差异会缩小,但不会自动消失,具体能力仍取决于 server 的实现、权限和部署范围。竞争会回到 Agent 本身的工程设计:上下文怎么管,工具怎么按需提供,权限护栏放在哪一层,选错工具怎么兜底。
这些问题我在前面的篇目里已经碰到过一轮,现在它们换了个位置重新出现。工具这一层的故事,后面应该还要再总结一些文章。

