MCP Transport 完整指南:stdio 与 Streamable HTTP 到底怎样传消息
本文根据 MCP
2026-07-28的 Transport Overview、stdio 和 Streamable HTTP 三篇官方规范整理。
第一次学习 MCP Transport 时,最容易混淆以下问题:
- Transport 是不是一种新协议?
- JSON-RPC、JSON、HTTP 和 SSE 是什么关系?
- Streamable HTTP 返回 SSE 后,还算不算 HTTP?
- stdio 为什么不能随便使用
print()? - stdio 只有一条
stdout,怎样同时处理多个 Request? - Streamable HTTP 为什么有时返回 JSON,有时返回 SSE?
- 每个 HTTP POST 都会建立一条永久 SSE 连接吗?
subscriptions/listen和普通 Request-scoped SSE 有什么区别?- 新版 MCP 为什么不再允许 Server 直接向 Client 发起 Request?
这些问题的根源,是把"消息含义""消息格式"和"消息怎样运输"混在了一起。
本文先建立协议分层,再分别解释 stdio 和 Streamable HTTP 的完整消息流程。
一、先理解 Transport:它只决定"怎么送"
MCP Transport 可以翻译为"传输绑定"或"传输方式"。
它负责定义:
- 一条 MCP Message 从哪里发送到哪里;
- 多条 Message 怎样分隔;
- Request Metadata 放在哪里;
- 怎样取消正在执行的 Request;
- 怎样关闭连接或进程;
- 连接异常断开时怎样处理。
Transport 不负责定义:
tools/call是什么意思;- Tool 应该接收什么参数;
resources/read应该返回什么;resultType: "input_required"应该怎样继续;- JSON-RPC Error Code 表示什么错误。
这些属于 MCP Core Protocol 和具体 Feature 的语义。
可以用快递作类比:
text
包裹里的信件内容 = MCP Method 和业务数据
包裹的统一填写格式 = JSON-RPC
包裹具体走哪条路线 = Transport
无论通过本地 stdio 还是远程 HTTP 运输:
text
tools/call 仍然是 tools/call
JSON-RPC id 仍然负责关联 Response
resultType 仍然表达 Result 类型
只有消息的"送法"发生变化。
二、MCP、JSON-RPC、JSON、SSE 和 HTTP 的分层关系
这几个概念处于不同层级:
text
MCP Method 与 Message Pattern
↓ 决定消息的业务含义
JSON-RPC 2.0
↓ 决定 Request、Response、Notification 的结构
JSON 文本 / SSE Event
↓ 决定数据在承载通道中怎样组织
stdio 或 HTTP
↓ 决定字节怎样在进程或网络之间传输
操作系统 Pipe / TCP / TLS / QUIC
↓ 更底层的通信能力
最重要的结论是:
SSE 不是脱离 HTTP 的另一条路。SSE 是 HTTP Response Body 的一种流式组织格式。
返回普通 JSON 时:
http
HTTP/1.1 200 OK
Content-Type: application/json
{"jsonrpc":"2.0","id":1,"result":{"resultType":"complete"}}
返回 SSE 时:
http
HTTP/1.1 200 OK
Content-Type: text/event-stream
data: {"jsonrpc":"2.0","method":"notifications/progress","params":{"progress":50}}
data: {"jsonrpc":"2.0","id":1,"result":{"resultType":"complete"}}
两种情况的最外层都是 HTTP Response:
text
HTTP Response
├─ application/json
│ └─ 一个 JSON-RPC Message
│
└─ text/event-stream
├─ SSE Event → 一个 JSON-RPC Message
├─ SSE Event → 一个 JSON-RPC Message
└─ SSE Event → 最终 JSON-RPC Response
所以不存在"请求走 HTTP,返回就不走 HTTP"这种情况。
三、MCP 2026-07-28 的消息方向
MCP 使用 UTF-8 编码的 JSON-RPC Message。
这一版核心协议只允许以下方向:
text
Client → Server
├─ JSON-RPC Request
└─ JSON-RPC Notification
Server → Client
├─ JSON-RPC Response
└─ JSON-RPC Notification
不允许:
text
Server → Client:独立 JSON-RPC Request
Client → Server:JSON-RPC Response
旧版 MCP 曾允许 Server 向 Client 发起 Sampling、Elicitation、Roots 等 Request。2026-07-28 改为 Multi Round-Trip Requests,简称 MRTR。
现在 Server 如果需要 Client 补充内容,不会反向发送 Request,而是返回:
json
{
"resultType": "input_required",
"inputRequests": [
{
"type": "elicitation",
"message": "是否确认继续?"
}
]
}
Client 取得输入后,重试原 Request,并携带 inputResponses。
这一语义在 stdio 和 Streamable HTTP 上完全相同。
四、MCP 的两种标准 Transport
MCP 2026-07-28 定义两种标准 Transport:
| Transport | 主要场景 | 消息怎样传输 |
|---|---|---|
| stdio | 本地 MCP Server | 子进程的 stdin、stdout |
| Streamable HTTP | 远程或共享 MCP Server | HTTP POST,Response 为 JSON 或 SSE |
还可以实现 Custom Transport,但必须保留:
- JSON-RPC Message 格式;
- MCP Message Pattern;
- 每个 Request 自带 Metadata 的模型;
- 明确的 Message Framing;
- 明确的 Cancellation 和 Termination 规则。
如果 Custom Transport 本身是可靠的双向 Byte Stream,例如 Unix Domain Socket 或 TCP,官方建议复用 stdio 的"每行一个 JSON-RPC Message"规则,而不是重新设计 Message Framing。
第一部分:stdio Transport
五、stdio 的整体工作方式
在 stdio Transport 中,MCP Client 会启动 MCP Server 子进程:
text
MCP Host / Client
│
│ 启动子进程
▼
MCP Server Process
两端通过三个标准流通信:
text
Client ──写入──> Server stdin
Client <─读取─── Server stdout
Client <─可选读取 Server stderr
三者职责必须严格区分:
| Stream | 方向 | 内容 |
|---|---|---|
stdin |
Client → Server | MCP JSON-RPC Message |
stdout |
Server → Client | MCP JSON-RPC Message |
stderr |
Server → Client/日志系统 | 普通日志文本 |
这是一种本地进程通信方式,不需要监听 TCP Port,也不需要 HTTP Server。
六、stdio 怎样划分一条条 Message
stdio 本质上是一条连续的 Byte Stream。
如果连续写入:
text
{...}{...}{...}
接收方不知道第一条 Message 在哪里结束。
因此 MCP stdio 使用非常简单的 Framing 规则:
一行只能放一个完整的 JSON-RPC Message,每条 Message 用换行符分隔。
例如 Client 向 Server 的 stdin 写入:
jsonl
{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_weather","arguments":{"location":"Shanghai"},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}
Server 逐行读取:
text
第 1 行 → Request id=1
第 2 行 → Request id=2
每一行必须:
- 是一个完整 JSON-RPC Message;
- 使用 UTF-8 编码;
- 结尾使用换行符;
- Message 本身不能包含真实的嵌入换行。
JSON String 中的转义字符 \n 没问题:
json
{"text":"line1\nline2"}
因为文件中仍然只有一条物理行。不能把它格式化成跨多行的 Pretty JSON:
json
{
"jsonrpc": "2.0",
"id": 1
}
这会被接收方误认为多条不完整 Message。
七、为什么 stdio Server 不能随便 print()
stdio 最大的工程规则是:
Server 的
stdout只能输出合法 MCP Message。
假设 Python Server 写了:
python
print("server started")
这段文本会进入 stdout。Client 会尝试把它解析为 JSON-RPC Message:
text
server started
结果自然是 JSON Parse Error,整个连接可能因此损坏。
普通日志应该写入 stderr:
python
import sys
print("server started", file=sys.stderr)
或者使用配置为输出到 stderr 的 Logging Framework。
需要注意:
stderr是日志通道,不代表日志一定是 Error Level。
Server 可以将 Info、Debug、Warning 和 Error Log 都写入 stderr。Client 可以选择显示、转发或忽略它,不能因为出现 stderr 输出就自动判断 Server 执行失败。
同样,Client 写入 Server stdin 的内容也必须全部是合法 MCP Message,不能混入普通命令或调试文本。
八、stdio 如何同时处理多个 Request
stdio 的 stdout 是所有 Server Message 共用的一条通道,没有"每个 Request 单独一条 Stream"。
假设 Client 连续发送:
text
Request id=1:生成报告
Request id=2:查询天气
Server 可能先完成更快的天气查询:
text
stdout 第 1 条:Response id=2
stdout 第 2 条:Request id=1 的 Progress Notification
stdout 第 3 条:Response id=1
Client 不能依赖消息顺序,而要根据字段关联:
- 普通 Response 使用 JSON-RPC
id; - Progress 通常通过 Request 相关 Token 关联;
- Subscription Notification 使用
_meta.io.modelcontextprotocol/subscriptionId。
可以把 stdio 看成一条共享公路:
text
stdout
├─ Response id=2
├─ Progress for id=1
├─ Subscription notification A
└─ Response id=1
每辆车都走同一条路,因此必须携带自己的识别信息。
九、stdio 中的 Request Metadata 放在哪里
stdio 没有 HTTP Header。
协议版本、Client Capabilities 和可选 Client Identity 都直接放进 JSON-RPC Body 的 _meta:
json
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get_weather",
"arguments": {
"location": "Shanghai"
},
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": {
"name": "ExampleClient",
"version": "1.0.0"
},
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}
对 stdio 而言:
text
Method → JSON-RPC method
Arguments → params
Protocol Info → params._meta
不存在额外 Header Layer。
十、stdio 怎样取消 Request
stdio 中所有 Request 共用同一条 stdout,不能通过关闭"某个 Request 的 Stream"取消,因为根本没有独立的 Request Stream。
Client 需要发送:
text
notifications/cancelled
并指定要取消的 Request ID。
流程是:
text
Client ── Request id=17 ─────────> Server
Client ── notifications/cancelled
requestId=17 ──────────> Server
Server 收到后应该尽快停止相关工作,并且不得继续为该 Request 发送消息。
这里取消的是一个 Request,而不是关闭整个 Server Process。
十一、stdio Server 怎样正常关闭
Client 通常按照以下顺序关闭 Server:
text
1. 关闭写向 Server 的 stdin
2. 等待 Server 自行退出
3. 超时仍未退出,再强制终止 Process
stdin 被关闭后,Server 的读取会得到 EOF。Server 应把 EOF 当作最主要、最可移植的 Graceful Shutdown Signal,然后尽快退出。
如果进程迟迟不退出:
- POSIX 系统通常先发送
SIGTERM; - 仍不退出时再使用
SIGKILL; - Windows 使用对应的 Process Termination 机制。
Server 也可以主动关闭 stdout 并退出,以发起 Shutdown。
需要区分:
text
取消一个 Request ≠ 关闭 stdin
关闭 stdin = 请求整个 Server Process 退出
十二、stdio Server 意外退出怎么办
如果 Server Process 崩溃或意外退出,Client 应该考虑重新启动它。
由于新版 MCP Protocol 是无状态的:
- 旧进程中的 In-flight Request 会丢失;
- Client 可以在新进程上重新请求;
subscriptions/listen需要重新建立。
工程上仍要注意:
非幂等操作不能仅因为没收到 Response 就无条件重试。
例如 Server 可能已经创建订单,但在返回 Response 前崩溃。Client 如果直接重试,可能创建两次。此类 Tool 应使用业务 Idempotency Key。
十三、stdio 的版本兼容探测
旧 MCP 版本要求先执行 initialize Handshake,新版则使用每请求 Metadata。
同时支持新旧版本的 stdio Client,可以先调用:
text
server/discover
可能出现三种结果:
- 返回
DiscoverResult:对方是新版 Server,从双方支持的版本中选择一个; - 返回可识别的新版 Version Error:对方仍是新版,只是版本不匹配,应选择其支持版本,不能退回
initialize; - 返回其他错误或超时:对方可能是旧版,再退回
initialize。
不要仅根据某一个错误码判断旧版,因为不同旧 Server 对未知 Request 的处理可能不同。
十四、stdio 的优点、限制和适用场景
优点
- 不需要开放网络端口;
- Host 可以直接管理 Server Process;
- 本地开发和个人工具配置简单;
- 权限可以借助操作系统用户和 Process Environment;
- 调试时消息流相对直接。
限制
- 主要适用于本地进程;
- Process Lifecycle 由 Client 管理;
- 所有 Request 共用一条 Message Channel;
stdout被普通日志污染后协议就会损坏;- 不适合直接作为多用户共享的远程服务。
常见场景
- 本地文件访问;
- IDE 启动的开发工具;
- Desktop AI Application;
- 本地数据库或命令行工具封装;
- 单用户、本机运行的 MCP Server。
第二部分:Streamable HTTP Transport
十五、Streamable HTTP 的整体工作方式
Streamable HTTP 主要用于远程或共享 MCP Server。
Server 提供一个统一 MCP Endpoint,例如:
text
https://example.com/mcp
Client 发送的每一条 JSON-RPC Request 都是一个新的 HTTP POST:
text
POST /mcp → Request 1
POST /mcp → Request 2
POST /mcp → Request 3
Server 针对每次 Request 选择一种 Response:
text
HTTP POST
├─ application/json
│ └─ 一个最终 JSON-RPC Response
│
└─ text/event-stream
├─ 零个或多个相关 Notification
└─ 一个最终 JSON-RPC Response
所以"Streamable HTTP"不是说每个 Request 都必须流式返回。
它的真正含义是:
同一个 MCP HTTP Endpoint 既支持普通 JSON Response,也支持需要时使用 SSE 的流式 Response。
十六、每个 Client Message 都是独立 HTTP POST
Client 发送 Request 时:
http
POST /mcp HTTP/1.1
Content-Type: application/json
Accept: application/json, text/event-stream
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: get_weather
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get_weather",
"arguments": {
"location": "Shanghai"
},
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}
几个关键要求:
- HTTP Method 必须是 POST;
- Body 是一个 JSON-RPC Request 或 Notification;
- 一个 POST 不能塞入多条 Client Message;
- Client 不能在 Body 里发送 JSON-RPC Response;
Accept必须同时声明支持 JSON 和 SSE;- 每个 POST 都要携带相应 Request Metadata。
Accept 同时声明两种类型,是为了允许 Server 针对不同 Request 决定最合适的返回方式。
十七、返回普通 JSON 的情况
如果 Server 可以直接完成请求,可以返回:
http
HTTP/1.1 200 OK
Content-Type: application/json
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"resultType": "complete",
"content": [
{
"type": "text",
"text": "Shanghai: 31°C"
}
]
}
}
流程是:
text
Client ── HTTP POST ─────────> Server
Client <─ 一个 JSON Response ─ Server
当前 HTTP Request 结束
这种方式适合:
tools/list;- 快速
tools/call; resources/read;- 不需要中间进度的普通 Request。
十八、返回 Request-scoped SSE 的情况
如果 Server 希望在最终结果前持续发送进度或日志,可以返回:
http
HTTP/1.1 200 OK
Content-Type: text/event-stream
X-Accel-Buffering: no
data: {"jsonrpc":"2.0","method":"notifications/progress","params":{"progress":20}}
data: {"jsonrpc":"2.0","method":"notifications/progress","params":{"progress":80}}
data: {"jsonrpc":"2.0","id":1,"result":{"resultType":"complete","content":[]}}
流程是:
text
Client ── HTTP POST ────────────────> Server
Client <─ SSE:Progress Notification ─ Server
Client <─ SSE:Log Notification ───── Server
Client <─ SSE:最终 Response ───────── Server
Server 关闭当前 Response Stream
这里仍然只有:
text
一个 HTTP Request
一个 HTTP Response
区别只是 HTTP Response Body 没有一次性返回,而是保持打开并逐步追加 SSE Event。
每个 SSE Event 的 data 中装的是 JSON-RPC Message。
十九、Request-scoped 到底是什么意思
Request-scoped 表示:
这条 SSE Response Stream 只能携带与发起它的 HTTP POST Request 有关的 Message。
例如 Request 是:
text
tools/call:生成销售报告
它的 SSE Stream 可以包含:
text
正在读取数据
已经完成 50%
正在生成图表
最终报告结果
不能包含:
text
另一个用户的 Request Result
另一个 Tool Call 的 Progress
与当前 Request 无关的全局通知
Server 主动发起的独立 JSON-RPC Request
普通 Request 的最终 JSON-RPC Response 发出后,Server 应结束这条 SSE Stream。
这与 stdio 很不同:
text
stdio
所有 Request 共用一条 stdout
Streamable HTTP
每个 Request 有自己的 HTTP Response
需要时该 Response 可以成为独立 SSE Stream
二十、SSE 不是 WebSocket,也不是脱离 HTTP
SSE 的完整名称是 Server-Sent Events。
在这里,它表示 Server 可以通过一个保持打开的 HTTP Response,持续向 Client 发送文本事件。
text
HTTP Response
└─ Content-Type: text/event-stream
├─ data: {...}
├─ data: {...}
└─ data: {...}
与 WebSocket 不同:
- SSE 仍然是 HTTP Response;
- 在这条 Response 中主要是 Server 向 Client 推送;
- Client 若要发送新的 MCP Message,需要再发新的 HTTP POST;
- 不会把当前 Response 升级成任意双向消息通道。
因此:
text
JSON Response = 同一个 HTTP Response 一次性返回 Body
SSE Response = 同一个 HTTP Response 分多次返回 Body
二十一、subscriptions/listen 为什么也是 SSE
普通 Request-scoped SSE 最后会有一个最终 Response,然后关闭。
但 Client 如果想长期接收以下变化:
- Tool List 变化;
- Prompt List 变化;
- Resource List 变化;
- Resource 内容更新;
可以发送:
text
subscriptions/listen
Server 对这个 Request 返回一条长期保持的 SSE Stream:
text
Client ── POST subscriptions/listen ──> Server
Client <─ SSE:tools/list_changed ────── Server
Client <─ SSE:resources/updated ─────── Server
Client <─ SSE:prompts/list_changed ──── Server
...保持打开...
这条 Stream 只传递 Client 选择订阅的 Change Notification。
普通 Request 的 Progress 或 Log Notification 不应该混入 subscriptions/listen:
text
Request Progress
→ 跟随该 Request 自己的 Response Stream
List / Resource Change
→ subscriptions/listen Stream
二十二、HTTP Notification 为什么返回 202 Accepted
从 Transport Mechanism 来看,如果 Client POST 的 Body 是 JSON-RPC Notification:
json
{
"jsonrpc": "2.0",
"method": "some/notification",
"params": {}
}
Server 接受后返回:
http
HTTP/1.1 202 Accepted
Response Body 为空。
原因是 JSON-RPC Notification 本来就不允许返回 JSON-RPC Response。
如果 Server 无法接受,可以返回 400 Bad Request 等 HTTP Error。
需要注意:
2026-07-28Core Protocol 当前没有定义通过 Streamable HTTP 发送的 Client-to-Server Notification。
唯一的核心 Client Notification notifications/cancelled 只用于 stdio。在 HTTP 中,取消通过关闭 Response Stream 表达。
规范仍然描述 Notification POST 的 Transport Rule,是为了让 Transport Mechanism 本身完整,也方便 Extension 或未来能力采用。
二十三、Streamable HTTP 怎样处理 MRTR
在旧版 Streamable HTTP 中,Server 曾经可以通过 SSE 向 Client 发送独立 JSON-RPC Request。
2026-07-28 已不允许这样做。
如果 Tool 执行到一半需要用户确认:
text
Server 不会通过 SSE 发送 elicitation/create Request
而是返回:
json
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"resultType": "input_required",
"inputRequests": [
{
"type": "elicitation",
"message": "该操作会删除数据,是否继续?"
}
]
}
}
Client 取得输入后,发送一个新的 HTTP POST,重试原 MCP Request:
text
第一次 POST
→ input_required
Client 获取用户输入
第二次 POST
→ 原参数 + inputResponses
→ complete
所以 MRTR 的"多轮"是多个 HTTP Request/Response Round Trip,不是 Server 在一条 SSE 中开启反向调用。
二十四、Streamable HTTP 怎样取消 Request
stdio 使用 notifications/cancelled,因为所有请求共用一条通道。
Streamable HTTP 中,每个 Request 都有自己的 Response,因此 Client 通过关闭或中止该 Request 的 Response Stream 来取消:
text
Client ── POST Request ─────────> Server
Client <─ SSE Progress ────────── Server
Client 关闭当前 Response Stream
Server 将它视为取消信号
Server 应该:
- 尽快停止相关工作;
- 不再为该 Request 发送任何 Message。
因为每个 Request 的 Stream 是独立的,所以 Server 能明确知道被取消的是哪一个 Request。
二十五、为什么 HTTP Header 和 Body 都有 Metadata
新版 MCP 要求 Request 在 Body _meta 中携带:
text
Protocol Version
Client Capabilities
Optional Client Info
Body 是 Source of Truth。
Streamable HTTP 还会把一些字段镜像到 HTTP Header:
text
JSON-RPC Body HTTP Header
method → Mcp-Method
params.name / params.uri → Mcp-Name
protocolVersion → MCP-Protocol-Version
为什么重复?
因为 API Gateway、Load Balancer、Rate Limiter 和 WAF 通常更容易读取 Header,而不希望解析完整 JSON Body。
例如:
http
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: get_weather
Gateway 只看 Header 就能:
- 按 Tool 路由;
- 对不同 Tool 限流;
- 记录调用指标;
- 拦截敏感 Tool;
- 应用不同授权策略。
二十六、标准 MCP HTTP Header
每个 POST 必须包含:
http
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
以下 Request 还必须包含 Mcp-Name:
| Request | Mcp-Name 来源 |
|---|---|
tools/call |
params.name |
prompts/get |
params.name |
resources/read |
params.uri |
例如:
http
Mcp-Method: resources/read
Mcp-Name: file:///projects/myapp/config.json
Header 和 Body 中的值必须匹配。
例如下面的 Request 不合法:
http
Mcp-Name: safe_tool
json
{
"method": "tools/call",
"params": {
"name": "dangerous_tool"
}
}
否则 Gateway 可能按 safe_tool 放行,而 MCP Server 实际执行 dangerous_tool。
Server 检测到 Header 与 Body 不一致时返回:
text
HTTP 400 Bad Request
JSON-RPC -32020 HeaderMismatch
这就是为什么 Body 虽然是 Source of Truth,镜像 Header 仍必须严格校验。
二十七、Protocol Version Header
每个 POST 必须包含:
http
MCP-Protocol-Version: 2026-07-28
它必须与 Body 中的:
text
_meta.io.modelcontextprotocol/protocolVersion
一致。
常见失败情况:
| 情况 | Server Response |
|---|---|
| Header 与 Body 版本不同 | 400 + HeaderMismatch |
| Server 不支持请求版本 | 400 + UnsupportedProtocolVersionError |
| Server 不实现该 RPC Method | 404 + JSON-RPC -32601 Method not found |
HTTP Status 表达 HTTP 层结果,JSON-RPC Error 表达 MCP/JSON-RPC 层的具体原因。两者可以同时存在。
二十八、Tool 参数怎样镜像为自定义 Header
有时 Gateway 不仅需要知道 Tool Name,还需要读取某个参数,例如 Region 或 Tenant。
Server 可以在 Tool inputSchema 中使用:
text
x-mcp-header
例如:
json
{
"name": "execute_sql",
"inputSchema": {
"type": "object",
"properties": {
"region": {
"type": "string",
"x-mcp-header": "Region"
},
"query": {
"type": "string"
}
},
"required": ["region", "query"]
}
}
Client 调用:
json
{
"name": "execute_sql",
"arguments": {
"region": "us-west1",
"query": "SELECT * FROM users"
}
}
在 HTTP Request 中额外添加:
http
Mcp-Param-Region: us-west1
重要规则可以简化为:
- 只适合 String、Integer、Boolean 等简单值;
- 参数不存在或为
null时不发送 Header; - Header 值仍必须与 Body 参数一致;
- Server 校验失败时返回
HeaderMismatch; - stdio 等其他 Transport 可以忽略
x-mcp-header。
x-mcp-header 的目的不是把参数从 Body 移走,而是复制一份给中间基础设施使用。Body 中仍然保留原始参数。
二十九、非 ASCII Header 怎样编码
HTTP Header 不适合直接承载任意 Unicode、换行符或首尾空格。
如果 Mcp-Name 或 Mcp-Param-* 的值不能安全表示成普通 ASCII,Client 使用 UTF-8 Base64,并加上 Sentinel:
text
=?base64?{Base64EncodedValue}?=
例如:
text
原值:Hello, 世界
Header:=?base64?SGVsbG8sIOS4lueVjA==?=
Server 比较 Header 与 Body 之前,先完成 Base64 Decode。
初学阶段只需理解:
Header 里不适合直接放的值需要安全编码,不能把换行等危险字符原样写入 Header。
通常这些细节由 MCP SDK 处理。
三十、HTTP Header 的大小写规则
需要区分 Header Name 和 Header Value:
text
Header Name 不区分大小写
Header Value 通常区分大小写
因此以下 Header Name 等价:
text
Mcp-Method
mcp-method
MCP-METHOD
但 Method Value:
text
tools/call
Tools/Call
不是同一个 MCP Method。
三十一、Streamable HTTP 的 SSE 运维细节
1. 禁止代理缓冲
Server 返回 SSE 时,建议添加:
http
X-Accel-Buffering: no
它告诉 Nginx 等 Reverse Proxy 不要先积累一批 Event 再一起转发,否则实时 Progress 可能延迟很久。
2. 长连接 Keep-alive
长期没有 Notification 时,中间代理可能认为连接空闲并关闭。
subscriptions/listen 等 Long-lived Stream 可以定期发送 SSE Comment:
text
:
以冒号开头的行是 SSE Comment,Client 应忽略其业务含义,但它可以保持连接活跃。
3. 不支持 Last-Event-ID 恢复
2026-07-28 不支持通过:
http
Last-Event-ID
恢复断开的 SSE Stream,也不支持 Event Redelivery。
Request Stream 断开后:
- 当前 In-flight Request 丢失;
- Client 重新发送新 Request;
- 使用新的 JSON-RPC Request ID;
- 长期 Subscription 需要重新建立。
三十二、Streamable HTTP 的基础安全要求
1. 验证 Origin
Server 应验证传入请求的 Origin Header,防止 DNS Rebinding Attack。
存在但不合法的 Origin 应返回:
http
403 Forbidden
2. 本地 Server 只绑定 Loopback
如果 HTTP MCP Server 只用于本机,应优先绑定:
text
127.0.0.1
而不是:
text
0.0.0.0
后者会监听所有 Network Interface,可能让局域网或外部设备访问。
3. Remote Server 应进行认证与授权
远程 MCP Server 不应仅凭知道 Endpoint URL 就允许调用敏感 Tool。
需要根据场景实施:
- Authentication;
- Authorization;
- Tenant Isolation;
- Tool-level Policy;
- Audit Log;
- Rate Limit。
三十三、Streamable HTTP 的版本演进
1. 当前 2026-07-28
当前形态是:
- 单一 POST Endpoint;
- 无协议级 Session;
- 无单独 GET Stream;
- Server 不发送独立 JSON-RPC Request;
- SSE 不支持恢复;
- 每个 Request 自带版本和 Capabilities。
2. 2025-03-26 到 2025-11-25
早期 Streamable HTTP 曾支持:
Mcp-Session-Id;- GET 打开独立 SSE Stream;
- Server 在 SSE 中发送 JSON-RPC Request;
Last-Event-ID恢复 Stream。
这些机制都不属于 2026-07-28。
3. 更早的 HTTP+SSE
2024-11-05 的 HTTP+SSE Transport 已被废弃。新实现不应采用,应迁移到 Streamable HTTP。
因此阅读旧教程时,需要特别检查它是否还在使用:
text
GET SSE Endpoint
Mcp-Session-Id
initialize Handshake
Server-Initiated JSON-RPC Request
Last-Event-ID
出现这些内容时,往往说明教程基于旧协议版本。
三十四、Streamable HTTP 的优点、限制和适用场景
优点
- 适合远程服务;
- 可以使用标准 HTTP Gateway 和 Load Balancer;
- 无状态设计便于横向扩容;
- 普通 Request 可以直接返回 JSON;
- 需要进度时可以使用 SSE;
- 可以沿用成熟的 HTTP Auth、Observability 和 Rate Limit 体系。
限制
- 需要处理 HTTP Security;
- Client 必须同时支持 JSON 和 SSE Response;
- Proxy Buffering 和 Timeout 可能影响 SSE;
- Header 与 Body 必须严格保持一致;
- 断线后没有 Event Resume;
- 非幂等 Request 重试需要业务保护。
常见场景
- 多用户共享 MCP Server;
- Cloud-hosted Tool Service;
- 需要统一 Gateway 和 Governance 的企业系统;
- 需要 Horizontal Scaling 的远程 Agent Infrastructure;
- 跨设备、跨网络访问的 MCP Service。
第三部分:对比、流程与选型
三十五、stdio 与 Streamable HTTP 完整对比
| 对比项 | stdio | Streamable HTTP |
|---|---|---|
| 典型位置 | 本地 | 远程或共享服务 |
| Server 启动 | Client 启动子进程 | Server 独立运行 |
| Client → Server | 写入 stdin |
HTTP POST |
| Server → Client | 从 stdout 读取 |
HTTP Response |
| Message Framing | 每行一个 JSON-RPC Message | 一个 POST Body;Response 为 JSON 或 SSE |
| Request Channel | 所有 Request 共用 stdout | 每个 Request 有独立 HTTP Response |
| 中间进度 | stdout Notification | Request-scoped SSE Notification |
| 长期订阅 | 共用 stdout,以 Subscription ID 关联 | subscriptions/listen SSE |
| Metadata | 全部位于 Body _meta |
Body _meta,部分镜像到 Header |
| 取消 | notifications/cancelled |
关闭 Request Response Stream |
| 关闭 | 关闭 stdin,等待 Process 退出 | 普通 HTTP Connection Lifecycle |
| 日志 | stderr |
Server Logging / Observability |
| 鉴权 | Process Environment、本机权限 | HTTP Authorization |
| 扩容 | 通常每个 Client 本地 Process | 适合 Load Balancer 与横向扩容 |
三十六、同一个 tools/call 在两种 Transport 中怎样流动
stdio
text
Host 启动 Server Process
↓
Client 向 stdin 写一行 tools/call JSON-RPC
↓
Server 执行 Tool
↓
Server 向 stdout 写 Progress Notification
↓
Server 向 stdout 写带相同 id 的 Response
Streamable HTTP:普通 JSON
text
Client POST /mcp,Body 为 tools/call
↓
Server 执行 Tool
↓
HTTP Response Content-Type: application/json
↓
Body 返回带相同 id 的 Response
Streamable HTTP:SSE
text
Client POST /mcp,Body 为 tools/call
↓
Server 返回 Content-Type: text/event-stream
↓
SSE Event:Progress Notification
↓
SSE Event:Progress Notification
↓
SSE Event:带相同 id 的最终 Response
↓
关闭当前 Response Stream
三条路径中的 MCP Method、Arguments、Request ID 和 Result 语义完全一致,变化的只是运输方式。
三十七、怎样选择 Transport
可以使用下面的简单判断:
text
Server 是否只需要在用户本机运行?
├─ 是
│ └─ Host 是否方便启动和管理子进程?
│ ├─ 是 → 优先 stdio
│ └─ 否 → 考虑本地 Streamable HTTP
│
└─ 否
└─ 是否需要远程、多用户或横向扩容?
├─ 是 → Streamable HTTP
└─ 否 → 根据部署边界选择
常见建议:
| 场景 | 推荐 |
|---|---|
| Desktop App 访问本地文件 | stdio |
| IDE 调用本地开发工具 | stdio |
| 个人本机数据库助手 | stdio |
| 企业统一 MCP Gateway | Streamable HTTP |
| Cloud SaaS MCP Server | Streamable HTTP |
| 多用户共享业务 Tool | Streamable HTTP |
| 需要 Load Balancer | Streamable HTTP |
不要仅因为 SSE 听起来"更实时"就选择 Streamable HTTP。Transport 首先由部署位置、Process Ownership、安全边界和扩容需求决定。
三十八、常见误区
误区 1:Transport 决定 Tool 的业务含义
Transport 只负责 Message Delivery。tools/call 的含义不会因为换成 stdio 或 HTTP 而改变。
误区 2:SSE 出现后就不再走 HTTP
SSE 是 Content-Type: text/event-stream 的 HTTP Response Body,全程仍然使用 HTTP。
误区 3:Streamable HTTP 的所有 Response 都必须是 SSE
Server 可以针对每个 Request 返回单个 JSON,也可以返回 Request-scoped SSE。Client 必须支持两种方式。
误区 4:每个 HTTP POST 都会建立永久 SSE
只有 Server 选择 SSE 时才产生 Stream。普通 Request 的 SSE 在最终 Response 后结束。只有 subscriptions/listen 等请求通常保持较长时间。
误区 5:stdio 的 stderr 只表示错误
stderr 是普通日志通道,可以承载 Debug、Info、Warning 和 Error。
误区 6:stdio Server 可以用 print() 调试
默认 print() 通常写入 stdout,会污染 JSON-RPC Message Channel。日志必须写入 stderr。
误区 7:stdio 每个 Request 都有独立 Stream
所有 Response 和 Notification 共用 stdout,依靠 Request ID、Progress Token 和 Subscription ID 关联。
误区 8:SSE 可以承载 Server 发起的 Sampling Request
2026-07-28 不允许 Server 发送独立 JSON-RPC Request。Sampling、Elicitation 和 Roots 使用 MRTR 的 input_required Result。
误区 9:HTTP Header 可以和 Body 不一致
Header 是为了让 Gateway 读取的镜像信息。它必须与 Body 匹配,否则会产生路由和授权漏洞。
误区 10:SSE 断开后可以用 Last-Event-ID 续传
当前版本不支持 SSE Resume。Request 或 Subscription 需要重新建立。
三十九、开发检查清单
stdio Server
- 从
stdin逐行读取 UTF-8 JSON-RPC; - 每条 Message 输出为单独一行;
-
stdout只输出合法 MCP Message; - 普通 Log 写入
stderr; - 不把 Pretty JSON 跨多行输出到
stdout; - 支持用 JSON-RPC
id关联并发 Request; - 正确处理
notifications/cancelled; - stdin EOF 后及时退出;
- Restart 后重新建立 Subscription。
stdio Client
- 正确启动和管理 Server Process;
- 写入
stdin的内容全部是 MCP Message; - 逐行解析
stdout; - 不把
stderr输出自动等同于 Server Error; - 关闭时先关闭 stdin,再等待 Process;
- Process 崩溃后处理 In-flight Request;
- 兼容旧版时正确探测
server/discover。
Streamable HTTP Client
- 每个 Client Message 使用独立 POST;
-
Accept同时支持application/json和text/event-stream; - 同时实现 JSON Response 与 SSE Response 解析;
- 每个 POST 携带 Protocol Version 和标准 Header;
-
Mcp-Method、Mcp-Name与 Body 保持一致; - 根据 Tool Schema 处理
x-mcp-header; - 通过关闭 Response Stream 取消 Request;
- SSE 断开后重新请求或重建 Subscription;
- 谨慎重试非幂等 Tool。
Streamable HTTP Server
- 只暴露统一 MCP POST Endpoint;
- Request 返回 JSON 或 Request-scoped SSE;
- SSE 只发送与原 Request 相关的 Notification;
- 最终 Response 后结束普通 Request Stream;
-
subscriptions/listen只发送订阅的 Change Notification; - 不通过 SSE 发送独立 JSON-RPC Request;
- 校验 Protocol Version、Method、Name 和自定义 Header;
- Header 与 Body 不匹配时返回
HeaderMismatch; - 验证
Origin; - 本地部署只绑定 Loopback;
- 远程部署实施认证和授权;
- 处理 Proxy Buffering、Keep-alive 和 Timeout。
四十、最终总结
MCP Transport 页面真正要告诉读者的是:
MCP 的 Message Meaning 在所有 Transport 上都相同,Transport 只定义 Message 怎样分隔、发送、接收、取消和结束。
stdio 的核心模型是:
text
Client 启动本地 Server 子进程
stdin 发送 MCP Message
stdout 接收 MCP Message
stderr 输出普通日志
一行一个 JSON-RPC Message
所有 Request 共用同一条输出通道
Streamable HTTP 的核心模型是:
text
Server 提供单一 MCP POST Endpoint
每个 Client Message 是独立 HTTP POST
简单结果返回 application/json
流式进度返回 text/event-stream
SSE 仍然位于 HTTP Response 内
每个普通 SSE 只服务于原 Request
长期变化通过 subscriptions/listen SSE 接收
如果只记住一个分层关系,可以记住:
text
MCP = 消息代表什么
JSON-RPC = 消息长什么样
stdio = 本地进程之间怎样送
HTTP = 网络之间怎样送
JSON = HTTP Body 一次性怎样装
SSE = HTTP Body 流式怎样装
理解这张图后,再看 MCP Client、Server、SSE、Request Metadata 和 Cancellation,就不会把不同层级的概念混在一起了。