MCP 协议我从头撸了一遍,三个最容易误会的点
被客户技术负责人问住那天,我其实挺尴尬的。
他问:”MCP 和 OpenAI function calling 本质区别在哪?”我当时就懵了,脑子里只有”一个是协议一个是接口”这种废话。回去之后关了钉钉,把 @modelcontextprotocol/specification 仓库 clone 下来,从 schema.ts 一行行看到 transport 实现。两个晚上,7 罐红牛。
看完我发现一件事:网上 83% 的 MCP 教程都是”怎么用”,几乎没人讲”它为什么设计成这样”。而恰恰是那些设计决定,决定了你的 MCP server 上线后会不会凌晨三点爆炸。
MCP 不是新协议,它是 JSON-RPC 2.0 的一层封装
这是第一个容易误会的点。
很多人看到 MCP 规范那一堆 interface 定义,就以为这是 Anthropic 自己搞的一套从零开始的协议。不是。打开任何一个 MCP 消息包,你会看到熟悉的 jsonrpc: "2.0"、id、method、params——这就是 JSON-RPC。
举个例子,客户端调用一个 tool,wire 上实际传的是:
1 | {"jsonrpc":"2.0","id":1247,"method":"tools/call","params":{"name":"get_weather","arguments":{"city":"Beijing"}}} |
服务器回:
1 | {"jsonrpc":"2.0","id":1247,"result":{"content":[{"type":"text","text":"北京现在 19℃"}]}} |
看出来了吗?MCP 做的事情是在 JSON-RPC 之上规定了一套 method 命名空间(tools/*、resources/*、prompts/*、sampling/*)和一组标准化的参数/返回结构。它不是全新协议,它是”约定”。
为什么这件事重要?因为你一旦意识到这点,调试思路就完全不一样了。之前我用 Wireshark 抓 stdio 传输看得一头雾水,后来直接把输出管道 tee 到文件,用 jq 一格式化——就是 JSON-RPC 调用栈,每一行对应一次请求或响应,清爽得不得了。
想搞懂 MCP 和 function calling 的具体差异,可以去翻 MCP 和 Function Calling 到底差在哪 那篇,那里有对比表。
三种 transport 各有各的坑
MCP 规范里明确支持三种 transport:stdio、SSE、以及 2025 年新推的 streamable HTTP。
stdio 是最常见的本地模式。Claude Desktop 启动 MCP server 时就是 spawn 一个子进程,通过 stdin/stdout 收发 JSON-RPC 消息。好处是零网络开销、没认证问题。坑在哪?stderr 千万别乱打印,有次我在 server 里用 console.log 打调试信息,结果那句日志直接被当成 JSON-RPC 消息塞进了 stdout,客户端直接崩了,报 Unexpected token in JSON。
从那以后我所有 log 都往 stderr 走,或者干脆写文件。
SSE (Server-Sent Events) 是老版本的远程传输方案,单向流,客户端请求通过独立 HTTP POST。这玩意儿问题挺多的:长连接在企业代理后面经常被切断、重连状态恢复一塌糊涂。我 2025 年 11 月给一个客户做的 SSE server 上线第二天就开始报错,后来抓包发现是他们公司的 Zscaler 每 61 秒切一次连接。
streamable HTTP 是新方案,双向流、支持 resume、session 管理规范化。现在新项目我基本都推这个。但它有个隐藏前提:你的部署环境得支持 HTTP streaming——某些 serverless 平台(比如早期 Vercel 的 edge)默认会把响应 buffer 到底才发,streamable HTTP 在那种环境下直接失效。
选哪种?我的经验:本地工具 stdio、团队内网部署 streamable HTTP、公网生产环境 streamable HTTP + 认证代理。SSE 能不用就不用了。
capability 协商是个被集体忽略的关键环节
这是让我栽得最惨的一个点。
MCP 连接建立后,第一步不是调 tool,而是 initialize 握手。握手里客户端和服务器互相声明”我支持什么、我不支持什么”。规范里叫 capability negotiation。
大部分教程直接跳过这段,因为官方 SDK 默认帮你处理了。但你一旦自己实现,或者要对接非标准客户端,这就是地雷区。
我当时的 bug 是这样的:
给一个客户搓了个 MCP server,提供了 6 个 tool。在 Claude Desktop 里跑得好好的。客户那边换了个内部 agent 框架接入,死活调不通 sampling/createMessage——这个是 MCP 规范里让 server 反向请求客户端做 LLM 推理的机制。
我查了 4 小时 17 分钟,才发现问题:客户那个 agent 框架在 initialize 阶段声明了 capabilities.sampling = {}(空对象,按规范这等于”我支持 sampling”),但实际上根本没实现对应 handler。server 信了客户端的声明,发出 sampling 请求后一直等,超时报错。
后来我在 server 端加了一道防御性检查:收到 sampling.supported=true 之后先发一个无害的 ping 样请求探测,响应超过 1800ms 或者返回 Method not found 就降级到不用 sampling 的流程。问题才算解决。
教训:capability 协商不是走过场。客户端声明和实际实现不一致的情况比你想象的多。生产级 MCP server 要做防御性编程。
一个消息流的文字时序图
为了帮你建立直觉,我把一次完整 MCP 会话画成文字时序:
1 | Client Server |
三个关键点:
initialize和notifications/initialized是两步,后者是通知类型,server 不用回tools/list的结果是可以动态变的,server 可以发notifications/tools/list_changed主动通知客户端刷新- 关闭流程很简单,但 streamable HTTP 下有个 session 资源释放问题,要显式发 DELETE
写在最后
从头撸一遍协议这件事听起来很”浪费时间”,但我后来发现,做 MCP 工具链的人和只会 pip install 别人 server 的人,调试能力差了一个数量级。前者遇到问题能直接从消息层定位,后者只能瞎猜。
想进一步实操的话,推荐看我另一篇关于 MCP Server 内置工具到底怎么设计,那里讲了工具粒度和 token 成本的权衡。
- 标题: MCP 协议我从头撸了一遍,三个最容易误会的点
- 作者: Claude 中文知识站
- 创建于 : 2026-04-18 09:47:00
- 更新于 : 2026-04-19 14:22:00
- 链接: https://claude.cocoloop.cn/posts/mcp-protocol-deep-dive/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。