本文是「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 的角色与通信方式
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 增多时,描述质量对工具选择的影响更大。
工具发现因此成为 Host 需要处理的工程问题:模型需要根据当前任务在已挂载的工具中选择合适的工具。MCP 协议不处理这一选择过程。
第三方 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 可以启动、哪些工具需要确认,并将工具返回内容作为不可信数据处理。
MCP 的适用范围
使用三个月后,我将 MCP 定位为解决工具访问问题的协议;工具选择仍由 Host 和 Agent 处理。
N 乘 M 的适配成本降为 N 加 M,支持 MCP 的工具开始通过统一接口接入。工具接入后,模型能否在几十个工具中选对工具、填对参数和处理错误仍不由 MCP 处理。我观察到,工具数量增加后,bad case 中选错工具的占比超过参数填错;工具选择准确率成为新的瓶颈。
MCP 降低了 Agent 接入工具的成本,工具选择、参数填写和错误处理仍需要由 Agent 与 Host 的工程设计解决。
结论
工具标准化之前,各家 Agent 的差异里有一部分是”我接了哪些别人接不了的系统”。如果 Host 和 server 都实现了相同的协议,这部分接入差异会缩小,但不会自动消失,具体能力仍取决于 server 的实现、权限和部署范围。竞争会回到 Agent 本身的工程设计:上下文怎么管,工具怎么按需提供,权限护栏放在哪一层,选错工具怎么兜底。
前文讨论过的上下文管理、按需提供工具、权限控制和错误恢复,在 MCP 场景中仍需要由 Agent 与 Host 处理。
