当我写了一个 MCP Server 之后
写 MCP Server 的经历让我意识到,工具的 Schema 描述比实现更重要——描述写得好,Agent 才知道什么时候该用、什么时候不该用。
一个"适配层"如何改变了我对 Agent 工具生态的看法
上个月我在做一个多 Agent 知识库系统,遇到一个很恼火的问题。
系统里要用到好几个工具:查本地文件、调 GitHub API、搜网页、写数据库。每个工具本身不复杂,但问题在于——我的 Agent 一开始用的 Claude,后来想换成 DeepSeek,再后来又加了 GPT 做辅助判断。
每换一次模型,工具调用那段代码就要重写一遍。
不是说改参数,是整块逻辑都得换。OpenAI 的 Function Calling 格式和 Anthropic 的 Tool Use 长得像,但细节差很多。参数名、返回格式、错误处理方式全不一样。我一个工具接三次,写了三份适配。
干了三次之后我意识到:这件事不对。这不是技术问题,是架构问题。
后来我选了 MCP。
不是因为它是 Anthropic 出的,也不是因为圈子里吹得凶——而是它的设计恰好打在了我那个痛点上。这篇文章不讲 MCP 的基础概念(网上已经有很多了),聊的是,真正上手写了一个 MCP Server 之后,我发现它解决的不止是"统一接口"这个表面问题。
它逼我重新思考了一件事:一个 Agent 的工具到底该怎么组织。
第一个认知:工具描述写得好不好,直接决定 Agent 的智商
MCP 设计里有个很容易被忽略的细节:工具的 Schema 定义。
很多人上手写 MCP Server 的时候,会直接把函数签名翻译成 JSON Schema,觉得"参数类型写对了就行"。我一开始也这么干。
比如我写了一个查询慢 SQL 的工具:
{
"name": "query_slow_sql",
"description": "查询慢SQL日志",
"inputSchema": {
"type": "object",
"properties": {
"service_name": { "type": "string" },
"time_range": { "type": "string" }
}
}
}
跑起来之后,Agent 经常在不需要的时候调这个工具。用户问"服务是不是挂了",它不去看监控,先跑一遍慢 SQL 查询。结果是空的,再回头查监控。浪费一轮。
后来我改了 description:
"查指定微服务在特定时间段的慢 SQL 日志。服务响应慢、数据库超时、CPU 飙升的时候用这个。如果用户问的是网络或内存问题,别调这个。"
加入了一句"什么时候别用",效果明显不一样。Agent 选工具的准确率上了一个台阶。
这事让我明白一个道理:工具的 Schema 不是给开发者看的,是给 LLM 看的。 description 写得好不好,直接决定了 Agent 的"判断力"。你写"查询慢SQL日志"它只能知道这个工具能干什么,但不知道什么时候该干、什么时候不该干。而后者才是 LLM 真正需要的信息。
MCP 协议本身不管你怎么写 description,但它把工具描述标准化了——你只要把这份 Schema 写好,任何支持 MCP 的 Host 都能理解。这意味着你只需要写一次好的 description,而不是给每个模型分别写一份。
第二个认知:传输方式的选择比看起来复杂
MCP 支持两种传输方式:stdio 和 Streamable HTTP。
官方文档说得很简单:本地用 stdio,远程用 HTTP。但我真正部署的时候才发现,这个选择背后牵扯的东西不少。
我一开始在本地开发,用 stdio 非常爽。启动 Server 作为子进程,通过 stdin/stdout 通信,零网络开销,调试日志直接看终端。Claude Desktop 也是这种方式,所以出问题可以跟官方行为做对照。
但一上远程服务器就出事了。
stdio 模式假定 Host 和 Server 在同一台机器上。而我的场景是:Agent 跑在服务器 A,工具 Server 因为要访问内网数据库,得放在服务器 B。中间隔了一层网络,stdio 根本用不了。
换成 Streamable HTTP 之后,又发现了一个问题:鉴权。
之前的 HTTP+SSE 模式(MCP 2025 年 3 月之前的版本)只在建立 SSE 连接时做一次鉴权,之后的所有消息都复用这个长连接。看起来省事,但对负载均衡器不友好——长连接会一直挂在某台机器上,没法优雅地做滚动更新。
Streamable HTTP 改成了每个请求独立鉴权,代价是每次调用多了一次 Token 验证的开销。好处是终于能跟标准 HTTP 基础设施(Nginx、Kong、AWS ALB)好好相处了。
选型上我的经验是:
- 纯本地开发:用 stdio,简单到极致
- 内网服务间通信:Streamable HTTP,用 mTLS 或者内部 Token 做鉴权
- 暴露到外网的服务:Streamable HTTP + OAuth2 / API Key,每条请求都要鉴
别想一上来就用 HTTP 走天下。如果你的 Server 只服务本机的 Claude Desktop 或者 VS Code 插件,stdio 就是最优雅的方案。
第三个认知:MCP 最大的隐藏价值是"工具可发现"
这个是我用了两周之后才后知后觉感受到的。
传统的 Function Calling 模式里,工具的发现是编译期的事。你在代码里写死了有哪些工具,注册进去。换了环境,加一个新工具,得改代码、重新部署。
MCP 不一样。
它有一个叫做"工具发现"(Tool Discovery)的机制。Host 启动时会向 MCP Server 发一个 tools/list 请求,Server 返回目前可用的工具列表。这就意味着:工具的增删可以在运行时完成,不需要改 Host 的代码。
我后来做了一个实验。写了一个动态 MCP Server,它连接了一个配置中心(Nacos),配置中心里改了某个工具的开关状态,Server 下次返回的 tools/list 里就不包含它了。整个过程 Agent 端完全无感。
这对生产环境太有用了。比如凌晨发现某个数据源不稳定,直接关掉对应的工具,Agent 就自动绕开了。不用重启服务,不用改代码。
后来我反过来想,Function Calling 的"编译期绑定"其实暴露了一个深层问题:工具和 Agent 的耦合太紧。MCP 用一套运行时的协议把这种耦合松开了。Agent 不需要知道工具在哪里、怎么部署、什么语言写的,它只要知道:我连上这个 Server,它告诉我有什么工具可以用,我就用。
这才是"USB-C"这个比喻真正值钱的地方——不是统一接口,而是即插即用。
第四个认知:安全问题比想象中棘手
写 MCP Server 写到第三周,我遇到了一个真正的噩梦。
我的一个 MCP Server 暴露了一个 execute_command 工具,用来在服务器上跑 Shell 命令做运维诊断。本意是好的——让 Agent 能查磁盘、看进程、分析日志。结果测试的时候,Agent 自己"脑补"了一个参数组合:rm -rf /tmp/*。幸好是在沙箱环境跑的。
这件事让我出了一身冷汗。
MCP 协议本身不做权限控制。它只管通信的格式,不管通信的内容安不安全。安全是 Server 开发者自己的事。
几个我在实战中踩坑后总结的原则:
路径遍历一定要防。 read_file 这类工具,如果路径参数没做校验,Agent 可以传 ../../etc/passwd 读到不该读的东西。最简单的做法:把可读路径限定在一个白名单目录里,任何跳出这个目录的请求直接拒绝。
危险操作要分层。 读日志、查监控这类只读操作可以放开。写文件、执行命令、删数据这类操作,要么分到单独的 Server(权限更小),要么加入人工确认环节。别把所有工具塞到一个 Server 里用一个 Token。
不要把 Token 写死在 config 里。 MCP Server 的 Streamable HTTP 模式下,每条请求都会带 Authorization header。你的 Token 管理最好是动态注入的,而不是写在代码里。不然换个环境就得改代码,安全性和可维护性都差。
MCP 还在快速演进,安全这块很多最佳实践还在形成。但我建议:在项目初期就把安全当成架构的一部分来考虑,而不是等到出事了再补。 因为 MCP 的存在本身就是让工具更"容易被调用"——被容易调用的另一面就是更容易被滥用。
最后聊两句开头说的那个"适配三份"的问题。
用上 MCP 之后,我换模型就变成了一行配置的事:换个 Host 端的 API 地址和 Key,Server 完全不用动。工具还是那些工具,Schema 还是那些 Schema。
更关键的是,我的项目里后来增加了两个新的数据源——Elasticsearch 和 Redis。我只需要写两个新的 MCP Server,在 Agent 的配置里加上连接信息,新工具就自动可用了。
整个过程不需要改 Agent 的核心逻辑。工具和 Agent 之间真的解耦了。
MCP 不是一个完美的协议——它的 JSON-RPC 通信有性能开销,生态还在成长,安全规范还在摸索。但它做了一个非常正确的设计选择:把"工具"定义为 AI 应用的一等公民,而不是 LLM 调用的附庸。
就冲这一点,它值得你花一个下午写个 Server 试试。
评论
发表评论