05 · 传输层:stdio 与 Streamable HTTP
传输层只负责把 JSON-RPC 消息搬过去——消息内容(第 01 章那套)在两种管子上完全一样。MCP 定义两种标准传输:本地用 stdio(启动子进程),远程用 Streamable HTTP。draft 对 HTTP 传输做了一次“瘦身”。
5.1 stdio:把服务器当子进程跑
最简单的传输:客户端把服务器作为子进程启动,两边通过子进程的标准流通信(docs/specification/draft/basic/transports/stdio.mdx:7)。
Client Server(子进程)
│ 写 JSON-RPC ──► stdin │
│ │ 读 stdin、干活
│ stdout ◄── 写 JSON-RPC │
│ stderr ◄── 写日志(随意) │
规则简单到优雅(docs/specification/draft/basic/transports/stdio.mdx:12):
- 每条消息是一行换行分隔的 JSON,不得含内嵌换行。
- 服务器只能往 stdout 写合法 MCP 消息;日志走 stderr(客户端不应把 stderr 输出当成错误信号)。
- 所有消息共用 stdout 这一根信道,没有“每请求一个流”。
无状态原则在这里很显眼(docs/specification/draft/basic/transports/stdio.mdx:110):一个开着的 stdio 进程不是会话。进程意外退出?客户端直接重启重试——因为协议无状态,在途请求丢了也无所谓,对新进程重发即可(活跃的 subscriptions/listen 流需要重新建立)。
关停遵循“关 stdin → 等退出 → 超时则强杀”三步(docs/specification/draft/basic/transports/stdio.mdx:88)。取消在途请求用 notifications/cancelled(stdio 是单信道,没有“关流”这种动作可用)。
5.2 Streamable HTTP:每条消息一次 POST
远程服务器用这个。核心形态(docs/specification/draft/basic/transports/streamable-http.mdx:27):服务器开一个 HTTP 端点(MCP endpoint),客户端把每条 JSON-RPC 请求/通知都作为独立的一次 POST 发过去。
服务器对每条请求二选一回复:
Content-Type: application/json— 一个 JSON 对象(简单一问一答)。Content-Type: text/event-stream— 一个 SSE 流,只限于这条请求:可先发若干notifications/progress,最后发最终响应(docs/specification/draft/basic/transports/streamable-http.mdx:88)。
简单响应: POST tools/call ──► 200 application/json(一个 JSON)
流式响应: POST tools/call ──► 200 text/event-stream
◄── SSE: notifications/progress
◄── SSE: notifications/progress
◄── SSE: 最终 JSON-RPC 响应 (流随之关闭)
通知: POST notification ──► 202 Accepted(无体)
draft 给 HTTP 传输的“瘦身”
2026-07-28 版本对 Streamable HTTP 砍了一刀(docs/specification/draft/basic/transports/streamable-http.mdx:14、changelog docs/specification/draft/changelog.mdx:11):
| 砍掉的东西 | 原来干嘛 | 为什么砍 |
|---|---|---|
Mcp-Session-Id 头 | 维持协议级会话 | 协议改无状态了,不需要 |
| GET 流端点 | 服务器主动推消息的常驻流 | 改用 subscriptions/listen |
| 服务器在 SSE 上发请求 | 服务器→客户端的独立请求 | 改用 MRTR(04 章) |
Last-Event-ID 可恢复 SSE | 断流后续传、消息重放 | 砍掉:断流即丢,客户端拿新 id 重发 |
关键头与“头体一致”校验
每个 POST 必须带 MCP-Protocol-Version 头,且必须和请求体 _meta 里的 protocolVersion 一致,不一致就 400 + HeaderMismatch(-32020)(docs/specification/draft/basic/transports/streamable-http.mdx:252)。还必须带 Mcp-Method,以及 tools/call/resources/read/prompts/get 时的 Mcp-Name(docs/specification/draft/basic/transports/streamable-http.mdx:288)。
这套“把请求体字段镜像进头”的设计,是为了让中间件(负载均衡、网关、观测)不必解析请求体就能路由(呼应 03 章的 x-mcp-header)。但带来一个安全要求:任何处理请求体的服务器必须校验头值与体值一致,不一致就拒——否则会出现“负载均衡按头路由、MCP 服务器按体执行”的错位攻击面(docs/specification/draft/basic/transports/streamable-http.mdx:587)。
HTTP 传输的安全红线
- 服务器必须校验
Origin头,防 DNS rebinding 攻击;非法 Origin 回403(docs/specification/draft/basic/transports/streamable-http.mdx:56)。 - 本地运行时应只绑
127.0.0.1,别绑0.0.0.0。
这两条加起来防的是:一个恶意网页通过浏览器偷偷连到你本机跑着的 MCP 服务器。
5.3 subscriptions/listen:变更通知的新归宿
砍掉 GET 流后,“服务器主动推变更通知”改由 subscriptions/listen 承担(docs/specification/draft/basic/patterns/subscriptions.mdx:7)。它本质是一个长寿命请求:客户端发一个带通知过滤器的请求,服务器的响应是一个一直开着的 SSE 流,只推客户端显式订阅的那些通知类型。
过滤器字段(docs/specification/draft/basic/patterns/subscriptions.mdx:42):
| 字段 | 类型 | 订阅什么 |
|---|---|---|
toolsListChanged | boolean | 工具清单变了 |
promptsListChanged | boolean | 提示词清单变了 |
resourcesListChanged | boolean | 资源清单变了 |
resourceSubscriptions | string[] | 这些 URI 的资源内容更新了 |
机制要点:
- 服务器必须先发
notifications/subscriptions/acknowledged作为流上第一条消息,里面回执它实际同意承担的子集(不支持的类型会被省略)(docs/specification/draft/basic/patterns/subscriptions.mdx:53)。 - 流上每条通知都带
io.modelcontextprotocol/subscriptionId(值是subscriptions/listen请求的 JSON-RPC id),客户端靠它关联到对应订阅——在 stdio 单信道上尤其必要(docs/specification/draft/basic/patterns/subscriptions.mdx:78)。 - 请求级通知(
notifications/progress、notifications/message)不走这条订阅流——它们只在“与之相关的那条请求”的响应流上流动(docs/specification/draft/basic/transports/streamable-http.mdx:127)。这条区分容易搞混:变更通知走订阅流,进度/日志走原请求流。
优雅关闭:服务器主动收尾时应先对原 subscriptions/listen 请求回一个空 result(表示干净结束),再关流——以区别于“突然断线”(docs/specification/draft/basic/patterns/subscriptions.mdx:123)。
代码地图
| 主题 | 文件路径 | 符号 / 锚点 |
|---|---|---|
| 传输总览 | docs/specification/draft/basic/transports/index.mdx | — |
| stdio 收发/关停/兼容 | docs/specification/draft/basic/transports/stdio.mdx | Shutdown / Backward Compatibility |
| Streamable HTTP 全规则 | docs/specification/draft/basic/transports/streamable-http.mdx | Request Metadata / Server Validation |
| 订阅流机制 | docs/specification/draft/basic/patterns/subscriptions.mdx | Notification Filter / Graceful Closure |
| 订阅请求类型 | schema/draft/schema.ts | SubscriptionsListenRequest、ResourceUpdatedNotification |
| 头不匹配错误 | schema/draft/schema.ts | HeaderMismatchError |