MCP Transport 完整指南:stdio 与 Streamable HTTP 到底怎样传消息

MCP Transport 完整指南:stdio 与 Streamable HTTP 到底怎样传消息

本文根据 MCP 2026-07-28Transport OverviewstdioStreamable 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 子进程的 stdinstdout
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

可能出现三种结果:

  1. 返回 DiscoverResult:对方是新版 Server,从双方支持的版本中选择一个;
  2. 返回可识别的新版 Version Error:对方仍是新版,只是版本不匹配,应选择其支持版本,不能退回 initialize
  3. 返回其他错误或超时:对方可能是旧版,再退回 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-28 Core 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-NameMcp-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-262025-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/jsontext/event-stream
  • 同时实现 JSON Response 与 SSE Response 解析;
  • 每个 POST 携带 Protocol Version 和标准 Header;
  • Mcp-MethodMcp-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,就不会把不同层级的概念混在一起了。

参考资料

相关推荐
数据知道3 小时前
网络安全实战:子域名接管实战——从 CNAME 配置错误到完全控制
网络·安全·web安全·网络安全
lf13210274 小时前
用 JSON Schema 管装修节点记录:从照片台账到可校验工程数据
网络·数据库·人工智能·经验分享·物联网·json·智能家居
LayZhangStrive4 小时前
后端通识 - 远程服务调用RPC
网络·网络协议·rpc·sentinel·openfeign·远程服务调用
华清远见成都中心5 小时前
FreeRTOS事件组(Event Group)的工作机制分析
服务器·网络·网络协议
xiaoxiangsiyan6 小时前
RHCE2026云原生路线EX188和EX288完整备考指南
运维·网络·云原生·自动化
西安景驰电子6 小时前
《PTP精确时间协议系列》第二篇:工程部署、调试与性能优化
运维·服务器·网络·数据库·windows·性能优化
tiantianuser6 小时前
NVME-oF IP 设计15 : 适于高速网络存储系统的IP设计1
网络协议·rdma·高速传输·cmac·roce v2
虹科网络安全7 小时前
KnowBe4 SAT 是什么?安全意识培训平台功能与应用详解
网络
艺杯羹7 小时前
古典密码学攻防演进全景:从凯撒移位、频率分析到一次一密与恩尼格玛机破译
网络·人工智能·安全·网络安全·密码学·密码安全