菜单

Streamable HTTP 与 SSE:把远程 MCP server 迁到当前的传输方式

2026-10-20MCP Checkup

如果你的 MCP server 有一个让客户端用 GET 打开的 /sse 地址,那它用的是 2024-11-05 版本里的 HTTP+SSE 传输。规范在 2025-03-26 用 Streamable HTTP 取代了它。之后,当前版本 2026-07-28 又把 Streamable HTTP 改了一遍:没有握手,没有会话,没有 GET 流。所以「迁到当前的传输方式」可能是两次迁移,而你可能希望老客户端在两次迁移中都能继续用。

这篇文章对比这三种形态,按规范里的向后兼容规则讲怎么继续服务老客户端,最后给出验证用的 curl 命令。把 your-server.example.com 换成你的主机名就行。

远程 MCP 的三种形态

HTTP+SSE(2024-11-05) Streamable HTTP(2025-03-26 到 2025-11-25) Streamable HTTP(2026-07-28)
端点 一条 GET 流,加一个由服务器在 endpoint 事件里告知的 POST 地址 一个 MCP 端点,支持 POST 和 GET 一个 MCP 端点,只用 POST
回复走哪里 作为 message 事件,走 GET 流 在这个 POST 的响应里:JSON 或 SSE 流 在这个 POST 的响应里:JSON 或只属于这个请求的 SSE 流
握手 initialize initialize 没有:每个请求都带 _meta,服务器必须实现 server/discover
会话 没有定义;回复依赖客户端那条打开着的 GET 流 可选的 Mcp-Session-Id,在 initialize 时下发 没有
GET 打开那条流 打开一条可选的额外流,或者回 405 不属于这个传输;回 405
服务器发给客户端的请求 走那条流 走 SSE 流 放在结果里返回;客户端带着答复重试
断流后续传 没有定义 Last-Event-ID 不支持;客户端重新发请求

最大的区别在回复走哪里。在 HTTP+SSE 下,一个 POST 的回复要走另一条长连接的 GET 流,所以收到 POST 的那个进程,得能找到握着这个客户端那条流的进程。Streamable HTTP 直接把回复放在这个 POST 自己的响应里。到了 2026-07-28,连会话都没有了,任何一个实例都能回答任何一个请求。

2026-07-28 改了什么

如果你在 2025 年已经迁到了 Streamable HTTP,下面这些是和传输有关的改动:

  • 没有 initialize,也没有 notifications/initialized。每个请求都在 _meta 里带上 io.modelcontextprotocol/protocolVersion 和 io.modelcontextprotocol/clientCapabilities,并且应该带上 io.modelcontextprotocol/clientInfo。
  • 没有会话。Mcp-Session-Id 头取消了。需要跨调用保存的状态,改用显式的句柄,由客户端作为工具参数传回来。
  • 没有 GET 流。变更通知改为走 subscriptions/listen 这个 POST 的响应流。
  • 没有续传。Last-Event-ID 和 SSE 事件 ID 都取消了。流断了,这个请求就丢了,客户端用新的 ID 重新发。
  • 关闭一个请求的响应流,就是取消这个请求。
  • 新增必需的请求头:每个 POST 都要带 MCP-Protocol-Version 和 Mcp-Method,tools/call、resources/read 和 prompts/get 还要带 Mcp-Name。缺了头,或者头和请求体对不上,会得到 400 和错误码 -32020(HeaderMismatch)。
  • 不支持的版本会得到 400 和 -32022(UnsupportedProtocolVersion),并列出服务器支持的版本。不存在的方法会得到 404 和 -32601。
  • HTTP+SSE 从 2025-03-26 起就已弃用,现在按规范的功能生命周期政策被正式列为 Deprecated:新实现不应该再采用,已有实现应该迁移;以后的版本可能会把它移除。

让老客户端继续能用

规范的向后兼容一节(2025-03-26 起就有,2026-07-28 也保留了)对服务器只说了一件事:在新的 MCP 端点之外,继续保留老传输的 SSE 端点和 POST 端点。你也可以把老的 POST 端点和新的 MCP 端点合成一个,但规范提醒,这可能带来不必要的复杂度。分开更简单:/sse 和它的 POST 地址原样保留,另外加一个 /mcp。

了解一下同时支持两种传输的客户端是怎么判断你用哪一种的,会有帮助,因为你的服务器得给它正确的信号:

  1. 它先向用户给的地址发一个 POST。
  2. 成功了,就用 Streamable HTTP。
  3. 如果这个 POST 失败,状态码是 400、404 或 405,而且响应体不是一个它认得的新版 JSON-RPC 错误,它就对同一个地址发 GET,等一个 endpoint 事件。2025-03-26 和 2025-06-18 的原文写的是任意 4xx;2025-11-25 明确为这三个状态码;2026-07-28 又加上了对响应体的判断。

由此有两点:

  • 保存了你老 /sse 地址的用户,他们的客户端会先对它发 POST。这个 POST 必须以 4xx 失败,比如 405,而不是 200、跳转或者一个 HTML 页面,这样客户端才会退回去发 GET。
  • 在新端点上,错误必须带一个真正的 JSON-RPC 错误响应体。在 2026-07-28 下,一个空的 400 或 404 看起来就像老服务器,同时支持两代协议的客户端就会转头去找 initialize,或者去找 SSE 流。

如果你还有 2025 年那一代的 Streamable HTTP 客户端:

  • 一个端点可以同时服务两代。带着新 _meta 字段的请求按无状态方式处理;一个 initialize 请求则选中它所协商版本的老行为,范围限于这个会话。
  • 没有 MCP-Protocol-Version 头的请求,来自比 2025-06-18 更老的客户端。如果你要支持这些客户端,可以把这类请求当作 2025-03-26 处理;如果不支持,就必须以 400 拒绝。
  • 如果你不再支持老的几代,规范也写明了只支持 2026-07-28 的服务器该怎么对待它们的流量:GET 和 DELETE 回 405;忽略任何 Mcp-Session-Id,也绝不生成或回显;忽略 Last-Event-ID;老客户端发来 initialize 时,在错误里写明你支持的版本。老客户端没法自己往新版本走,这条消息可能是它的用户能看到的唯一线索。

迁移容易出错的地方:GET

  • 在 2025-03-26 到 2025-11-25 下,对 MCP 端点的 GET 要么打开一条 SSE 流,要么回 405。回一个 200 加 HTML 页面不算,回 404 也不算。
  • 在 2026-07-28 下没有东西可以 GET。回 405,把变更通知挪到 subscriptions/listen。
  • 别让老的流出现在新路径上。如果对 /mcp 的 GET 打开了一条带 endpoint 事件的老式流,一个因为任何原因退回老传输的客户端,之后的所有流量都会一直走老传输。
  • 长连接的流会碰上代理的空闲超时。2026-07-28 的原文建议在长连接的流上定期发 SSE 注释行(以 : 开头的行),并带上 X-Accel-Buffering: no 头,让 nginx 这类代理不要缓冲事件。如果你保留了老的 GET 流,它经过的也是同样这些代理。

迁移容易出错的地方:会话

  • 如果你给 2025 那一代的客户端发 Mcp-Session-Id,负载均衡后面的每个实例都必须能找到这个会话。会话只存在某个进程的内存里,请求一落到别的实例上就变成「找不到」。
  • 你已经结束的、或者已经不认识的会话,必须回 404。这个状态码就是告诉客户端重新发一次 initialize 的信号。回 400 或 500 会让它卡住。
  • 在 2026-07-28 下,不要给新版请求生成会话,也不要要求会话。需要跨调用保存状态的工具,应该返回一个句柄,比如购物篮 ID,由模型作为参数传回来。每次调用都要按这个句柄核对调用方的授权。
  • 在 2026-07-28 下,tools/list 这类列表结果不再随连接而变。没有会话,也就没有可以随之变化的东西。
  • 请求头名称不区分大小写。2025-11-25 的原文写作 MCP-Session-Id,更早的写作 Mcp-Session-Id;两种都要能匹配。

用 curl 验证迁移

先把两个端点设好:

NEW=https://your-server.example.com/mcp
OLD=https://your-server.example.com/sse

1. 新版请求第一次就能成功,而且没有会话头。

curl -sS -D - "$NEW" \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H 'MCP-Protocol-Version: 2026-07-28' \
  -H 'Mcp-Method: server/discover' \
  -d '{"jsonrpc":"2.0","id":1,"method":"server/discover","params":{"_meta":{
        "io.modelcontextprotocol/protocolVersion":"2026-07-28",
        "io.modelcontextprotocol/clientInfo":{"name":"curl-test","version":"0.1"},
        "io.modelcontextprotocol/clientCapabilities":{}}}}' \
  | grep -iE '^HTTP/|^content-type|^mcp-session-id|supportedVersions'

应该看到 200,内容类型是 application/json 或 text/event-stream,supportedVersions 里有 2026-07-28,并且没有 mcp-session-id 这一行。

2. GET 和 DELETE 得到 405(如果你只支持 2026-07-28)。

curl -sS -o /dev/null -w 'GET %{http_code}\n' -H 'Accept: text/event-stream' "$NEW"
curl -sS -o /dev/null -w 'DELETE %{http_code}\n' -X DELETE "$NEW"

3. 不存在的方法得到 404,以及一个客户端认得的响应体(-32601)。

curl -sS -i "$NEW" \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H 'MCP-Protocol-Version: 2026-07-28' \
  -H 'Mcp-Method: no/such_method' \
  -d '{"jsonrpc":"2.0","id":2,"method":"no/such_method","params":{"_meta":{
        "io.modelcontextprotocol/protocolVersion":"2026-07-28",
        "io.modelcontextprotocol/clientCapabilities":{}}}}'

**4. 老客户端的 initialize 能得到有用的回答。**同时服务两代的服务器会返回 InitializeResult,可能还带一个 Mcp-Session-Id。只支持 2026-07-28 的服务器回 400,最好在里面写明它支持的版本。

curl -sS -i "$NEW" \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{
        "protocolVersion":"2025-06-18","capabilities":{},
        "clientInfo":{"name":"curl-test","version":"0.1"}}}'

**5. 如果保留了老传输,它的行为仍然像老传输。**老地址拒绝 POST,GET 打开一条流,第一个事件是 endpoint。

curl -sS -o /dev/null -w 'POST %{http_code}\n' "$OLD" -H 'Content-Type: application/json' -d '{}'
curl -sS -N --max-time 5 -H 'Accept: text/event-stream' "$OLD" | head -n 3

POST 应该得到一个 4xx;GET 应该先看到 event: endpoint,再看到一行带着 POST 地址的 data:。--max-time 会在五秒后关掉这条一直开着的流,所以 curl 最后报一条超时信息是正常的。

上线迁移的那次部署之后把这五步都跑一遍;之后每次改了代理或负载均衡,也再跑一遍,因为 GET 的处理和会话的路由也可能在那一层。

看看 agent 看到的是什么

迁移算不算完成,要看外部客户端能不能用,而不是你自己的客户端能不能用。在 mcpcheckup.com 粘贴你的地址,看看 agent 会看到什么。它会像 agent 一样从外部连接,每一项检查给出四种状态之一:已通过、未通过、观察到风险,或者未检查并写明原因。从不给一个总分。