跳到主要内容

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-Namedocs/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 回 403docs/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):

字段类型订阅什么
toolsListChangedboolean工具清单变了
promptsListChangedboolean提示词清单变了
resourcesListChangedboolean资源清单变了
resourceSubscriptionsstring[]这些 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/progressnotifications/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.mdxShutdown / Backward Compatibility
Streamable HTTP 全规则docs/specification/draft/basic/transports/streamable-http.mdxRequest Metadata / Server Validation
订阅流机制docs/specification/draft/basic/patterns/subscriptions.mdxNotification Filter / Graceful Closure
订阅请求类型schema/draft/schema.tsSubscriptionsListenRequestResourceUpdatedNotification
头不匹配错误schema/draft/schema.tsHeaderMismatchError