EP02. "MCP 进阶:STDIO 与 StreamableHTTP 两种传输方式"
🔒 登录后可标记已读- 承接 EP01,这篇讲 MCP 底层怎么传消息:本地开发常用的 STDIO、正式上线常用的 StreamableHTTP,以及为什么 HTTP 这种「单向」协议要靠一套 Server-Sent Events(SSE)的 workaround,才能撑起「Server 主动通知 Client」这件事
- 覆盖课程「Transports and communication」这个大分类,对应 EP01 讲的 Sampling / Notifications 这些功能,全都要靠「Server 能主动发消息给 Client」才能运作,选错传输方式会直接让这些功能失效
- 前置知识:学过 EP01,理解 Sampling / Notifications 这些依赖「Server 主动发消息」的功能实际长什么样
重点内容
MCP 的消息全部是 JSON:两大类
MCP 完全靠 JSON 消息通信,每种消息类型各司其职——从调用工具、列资源清单,到系统事件通知都算。举例:Claude 要用 Server 提供的某个工具时,Client 发一个 Call Tool Request 消息;Server 执行完,回一个包含结果的 Call Tool Result 消息。
📌 权威的消息类型定义在 MCP 官方 spec 仓库(GitHub),跟各语言的 SDK(Python、TypeScript 等)分开维护;消息类型用 TypeScript 记法描述数据结构,只是用来说明格式,不是能直接跑的代码。
消息分两大类:
| 类型 | 特点 | 例子 |
|---|---|---|
| Request-Result(双向配对) | 发出去一定要有对应的回应 | Call Tool Request → Call Tool Result;List Prompts Request → List Prompts Result;Read Resource Request → Read Resource Result;Initialize Request → Initialize Result |
| Notification(单向,不需要回应) | 发出去就结束,不等对方回话 | Progress Notification、Logging Message Notification、Tool List Changed Notification、Resource Updated Notification |
规范里还按「谁发的」把消息分成 Client 发给 Server 的、和 Server 发给 Client 的两种——这个双向设计,正是后面选传输方式时最关键的考量点。
STDIO Transport:开发阶段的「理想情况」
Client 把 MCP Server 当作子进程启动,两边靠标准输入输出(stdin/stdout)通信:
- Client 发消息走
stdin - Server 回消息走
stdout - 双方都能随时主动发起通信
- 要求 Client 和 Server 跑在同一台机器上
每次连接都要先走三段握手:
- Initialize Request——Client 先发
- Initialize Result——Server 回应,附带自己的能力清单
- Initialized Notification——Client 确认收到,这一步不需要 Server 再回应
握手走完,才能发后续的 tool call、list prompts 这些请求。
📌 STDIO 之所以重要,是因为它代表了「双向沟通完全没有阻碍」的理想情况——先搞懂这个基准,才看得懂后面 HTTP 传输为什么会遇到各种限制。
测试方式:可以直接在终端机里把 JSON 消息贴进 stdin,看 stdout 的回应,不用真的写一个完整的 Client 应用。
StreamableHTTP Transport:要公开上线,但 HTTP 天生不对等
要让 Server 能被远端访问、给公开用户使用,通常得走 HTTP。但普通 HTTP 的通信模式本质上是不对等的:
- Client 能发请求给 Server(因为 Server 有一个已知的 URL)
- Server 却没办法轻松主动发请求给 Client(Client 没有已知的 URL)
这个限制直接影响到一批 MCP 消息类型:
- Server 主动发起的请求——Create Message(也就是 Sampling)、List Roots
- 各种 Notification——progress、logging、initialized、cancelled
如果传输设置得比较严格,进度条、日志功能、Server 主动发起的 Sampling 请求都会失效——这也是为什么 EP01 讲的 Sampling / Notifications 要放在讲传输方式之前先理解,不然会搞不清楚为什么这些功能"在 HTTP 上用不了"。
SSE Workaround:怎么在 HTTP 里让 Server 也能「主动说话」
StreamableHTTP 用 Server-Sent Events(SSE) 撑出一条 Server 能持续向 Client 推送消息的通道,绕开 HTTP「只有 Client 能发起请求」的限制。
连接建立流程:
- Client 发
Initialize Request给 Server - Server 回
Initialize Result,里面带一个mcp-session-id请求头 - Client 发
Initialized Notification,带上这个 session id
这个 session id 是之后每个请求都要带的必要信息。
握手完成后,Client 会另外开一个长连的 GET 请求,打开一条持续存在的 SSE 通道,让 Server 能透过它持续往 Client 推消息。
Tool Call 时会同时用到两条 SSE 连接:
| 连接 | 作用 | 生命周期 |
|---|---|---|
| 主 SSE 连接 | 处理 Server 主动发起的请求 | 一直保持开启 |
| 工具专属 SSE 连接 | 每次 tool call 各自开一条 | 拿到结果就自动关闭 |
消息路由规则:progress notification 走主连接;logging 消息和 tool 执行结果走工具专属连接。
stateless_http 与 json_response:两个会削弱功能的开关
| 开关 | 默认值 | 打开(True)之后 |
|---|---|---|
stateless_http | false | 不再发 session id;Server 不能主动发请求给 Client;不能用 Sampling;不能报进度;不能订阅 Resource 更新。代价换来的好处:Client 不用再走初始化握手,可以直接发请求 |
json_response | false | 关掉 POST 响应的流式推送——tool 执行只会拿到最终的 JSON 结果,中间的 progress/logging 消息全部收不到 |
什么时候该开 stateless_http:典型场景是单一 Server 撑不住几千个并发 Client,要做水平扩展——在 load balancer 后面挂多台 Server 实例。但正常的 MCP Client 要同时维护两条独立连接(一条 GET SSE 收 Server 主动发来的请求,一条 POST 发 tool call 拿结果),水平扩展下多台 Server 实例要协调这些连接会很麻烦。开了 stateless_http=True,就不用维护 session,Client 可以直接发请求,省掉初始化握手,代价是放弃所有依赖「Server 主动通信」的功能。
适合开 stateless_http 的场景:要做水平扩展、本来就不需要 Server 主动跟 Client 说话、工具不需要用到 Sampling、想把连接开销降到最低。
适合开 json_response 的场景:不需要流式响应、系统本来就只期待拿到一份纯 JSON 结果。
📌 开发阶段最好直接用打算实际部署的那种传输方式来测——stateful 跟 stateless 两种模式的行为差异不小,图方便先用 STDIO 或 stateful HTTP 开发,上线前才临时切到 stateless,很容易踩到开发阶段完全没测过的坑。
学完你会
- ✅ 分得清 Request-Result 和 Notification 两类消息的差别,理解「Server 能不能主动发消息」这件事其实是跟传输方式绑在一起的
- ✅ 讲得出 STDIO 作为本地开发「理想情况」的原因——双向沟通没有限制,是理解后续限制的参照基准
- ✅ 讲得清 StreamableHTTP 为什么要靠 SSE 撑出 Server 主动推送,以及 tool call 时那两条 SSE 连接各自负责什么
- ✅ 分得清
stateless_http和json_response这两个开关各自关掉了哪些功能,知道什么场景才该开
常见错误
- ❌ 开发阶段随便切换
stateless_http测试,以为跟 stateful 模式行为差不多——两种模式差别很大(session、进度、Sampling 全部受影响),上线前务必用打算实际部署的那种模式完整测过一遍 - ❌ 想让 Server 支持 Sampling / Progress Notification,却把服务部署成没有 SSE 的纯 HTTP、或者开了
stateless_http=True——这些功能全部要靠「Server 能主动发消息给 Client」,跟 stateless 模式互斥 - ❌ 为了应付更大并发直接开
stateless_http,却没意识到这会连带砍掉 Sampling、进度回报、Resource 订阅这些功能——先确认自己的 Server 到底用不用得到这些功能,再决定要不要开 - 💡 STDIO 是本地开发的黄金标准——先在 STDIO 下把逻辑写对、验证过,再迁移到 HTTP,遇到问题时比较容易分清楚是「传输方式本身的限制」还是「自己代码写错了」
Sources
官方文档:
- Model Context Protocol: Advanced Topics(课程总览)— https://academy.claude.com/courses/model-context-protocol-advanced-topics
- JSON message types — https://academy.claude.com/courses/model-context-protocol-advanced-topics/json-message-types
- The STDIO transport — https://academy.claude.com/courses/model-context-protocol-advanced-topics/the-stdio-transport
- The StreamableHTTP transport — https://academy.claude.com/courses/model-context-protocol-advanced-topics/the-streamablehttp-transport
- StreamableHTTP in depth — https://academy.claude.com/courses/model-context-protocol-advanced-topics/streamablehttp-in-depth
- State and the StreamableHTTP transport — https://academy.claude.com/courses/model-context-protocol-advanced-topics/state-and-the-streamablehttp-transport