HTTP Streamable 凭什么让 Anthropic 果断抛弃 SSE?MCP 传输层演进全解析
2025 年 3 月 26 日,Anthropic 发布了 MCP(Model Context Protocol)的新版规范,把旧的 HTTP+SSE 传输方式整个换掉,换成了一个叫 Streamable HTTP 的新机制。
官方文档里只有一句轻描淡写的说明:“This replaces the HTTP+SSE transport from protocol version 2024-11-05.”
但背后的理由值得细说。
SSE 方案到底有什么问题
旧版本(2024-11-05)的网络传输设计其实相当典型——SSE 负责服务端推送,HTTP POST 负责客户端发消息,两条通道各司其职。
用时序图描述大概是这样:
客户端 → GET /sse 建立 SSE 长连接
服务端 → endpoint event 告知客户端 POST 地址
客户端 → POST /messages 发送 JSON-RPC 消息
服务端 → SSE message 推送响应乍一看没什么毛病。
SSE 是标准协议,浏览器原生支持,服务端实现也简单。
但用在 MCP 这个场景里,问题开始浮现。
第一,强制长连接是个负担。
每个客户端必须先建立一条 SSE 长连接,才能收到任何来自服务端的消息。
哪怕是一次简单的工具调用(call a tool),也得先挂着这条连接。
对于无状态的轻量请求来说,这条连接完全是开销。
第二,两个端点带来了不必要的复杂度。
服务端要维护两套路由:一个 SSE 端点、一个 POST 端点,而且 POST 的地址还要通过 SSE 的 endpoint 事件动态下发。
这意味着客户端不能在没有先建立 SSE 连接的情况下发消息——即便它只想做一个简单的 JSON 请求/响应交互。
第三,断线重连的语义模糊。
SSE 断开了算不算请求取消?原来的规范没有明确说明。
这给实现方造成了困扰:有的客户端断连后重发请求,有的服务端则认为任务已结束。
这三个问题加在一起,对"写个轻量 MCP Server"的开发者来说很不友好——你哪怕只想暴露一个简单工具,也得搭一套 SSE 基础设施。
Streamable HTTP 的核心改变
新方案做了一件很干净的事:用一个端点搞定一切。
服务端只需要暴露一个 /mcp 路径,同时支持 POST 和 GET:
- POST
/mcp:客户端发 JSON-RPC 消息。服务端根据请求内容决定响应方式:- 简单请求/响应 → 直接返回
application/json - 需要流式推送 → 返回
text/event-stream(也就是 SSE,但是按需开启)
- 简单请求/响应 → 直接返回
- GET
/mcp:客户端想监听服务端主动推送时才使用,不强制
用时序图对比新旧两种方案:
旧方案(HTTP+SSE):
客户端 → GET /sse 必须先建长连接
服务端 ← endpoint event 拿到 POST 地址
客户端 → POST /messages 发消息
服务端 ← SSE event 收响应(通过长连接)
新方案(Streamable HTTP):
客户端 → POST /mcp 直接发消息
服务端 ← application/json 简单情况直接返回 JSON
或者
服务端 ← text/event-stream 需要流式时按需开启 SSE关键差异一目了然:SSE 从必选变成了可选。
服务端可以根据实际需要决定是否开启流式传输,不需要流式的场景就走普通 HTTP 请求/响应,实现成本降到最低。
实际影响:拿 Go 写个 MCP Server 的对比
旧方案要实现一个最简单的 MCP Server,至少需要这些:
// 旧方案:必须维护两个端点 + SSE 连接管理
func main() {
// 端点 1:SSE 长连接
http.HandleFunc("/sse", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
// 生成本次连接的 POST 端点地址并下发
sessionID := uuid.New().String()
postURL := fmt.Sprintf("http://localhost:8080/messages?session=%s", sessionID)
fmt.Fprintf(w, "event: endpoint\ndata: %s\n\n", postURL)
// 挂起连接,等待服务端推送
// 需要自行维护 session → writer 的映射
sessions.Store(sessionID, w)
<-r.Context().Done()
sessions.Delete(sessionID)
})
// 端点 2:接收客户端 POST
http.HandleFunc("/messages", func(w http.ResponseWriter, r *http.Request) {
sessionID := r.URL.Query().Get("session")
// 拿到对应的 SSE writer,把响应推回去
// ... 复杂的跨 goroutine 通信
})
http.ListenAndServe(":8080", nil)
}新方案呢?一个端点,Handler 里判断一下需不需要流式:
// 新方案:单端点,按需决定响应格式
func mcpHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
var req JSONRPCRequest
json.NewDecoder(r.Body).Decode(&req)
result, needsStream := handleRequest(req)
if needsStream {
// 需要流式推送:开启 SSE
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
flusher := w.(http.Flusher)
for chunk := range result.Stream() {
fmt.Fprintf(w, "data: %s\n\n", chunk)
flusher.Flush()
}
} else {
// 简单请求:直接返回 JSON
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(result.Data)
}
case http.MethodGet:
// 服务端主动推送通道(可选实现)
w.Header().Set("Content-Type", "text/event-stream")
// ...
}
}
func main() {
http.HandleFunc("/mcp", mcpHandler)
http.ListenAndServe(":8080", nil)
}代码量少了,更重要的是逻辑清晰了:需不需要流式,在处理请求的那一刻才决定,而不是在连接建立时就绑定。
断线语义明确了
新规范专门说明了断线不等于取消:
“Disconnection SHOULD NOT be interpreted as the client cancelling its request.” “To cancel, the client SHOULD explicitly send an MCP
CancelledNotification.”
同时引入了**断点续传(Resumability)机制,服务端可以给 SSE 流打上 Last-Event-ID,客户端断线重连后可以从中断处恢复,而不用从头重来。这对长时间运行的工具调用(比如跑一个几分钟的分析任务)来说意义很大。
一个端点为什么更合理
说到底,旧的两端点设计是把「连接建立」和「消息传递」两件事强绑在一起了。
SSE 本质上是一个单向的服务端推送机制,把它作为 MCP 的基础通信层,意味着所有客户端都要先建立一条「我要听你说话」的连接,然后才能「我要跟你说话」。
Streamable HTTP 翻转了这个默认值:默认是普通的请求/响应,流式是可选的附加能力。
这更符合 HTTP 本来的设计哲学,也让 MCP Server 的实现门槛低了一个台阶。
我个人觉得,这个改动更像是一次「回归常识」的修正,而不是什么颠覆性创新。
旧的 SSE 方案在浏览器场景下当然没问题,但 MCP 面向的是服务端 AI 工具生态,强制长连接在这里确实是多余的包袱。
向后兼容怎么处理
新规范保留了向后兼容说明,建议服务端在过渡期内同时支持两种传输方式。
对于还跑在旧版 SDK 上的客户端,规范推荐通过路径区分(如 /sse 走旧方案,/mcp 走新方案)或协议协商来兼容。
常见问题
Streamable HTTP 和 SSE 是什么关系?
Streamable HTTP 并不是彻底抛弃 SSE,而是把 SSE 从「必选的基础设施」降级为「按需启用的流式选项」。
服务端在处理需要多步推送的请求时,响应头依然是 text/event-stream,底层 SSE 协议还在。
变化的是触发时机:不再是「先建 SSE 连接才能通信」,而是「需要流式的时候才开 SSE」。
旧版 HTTP+SSE 的 MCP Server 需要立刻迁移吗?
官方规范建议在过渡期内兼容两种方式。
Anthropic 官方的 SDK 会维护向后兼容,但新项目建议直接用 Streamable HTTP,省掉两端点带来的复杂度。
Go 实现 Streamable HTTP 有推荐的库吗?
目前标准库 net/http 已经完全够用,核心是正确处理 http.Flusher 接口来实现流式推送。
官方的 github.com/modelcontextprotocol/go-sdk 已经支持新传输层,可以直接用,不用自己从头实现。
断点续传的 Last-Event-ID 怎么用?
服务端在 SSE 流里给每个事件加上 id 字段,客户端断线重连时带上 Last-Event-ID 请求头,服务端从该 ID 之后的事件开始重发。对于重要的工具调用结果,这是防丢失的关键机制。
如果你正在写 MCP Server,或者在研究 AI 工具协议的设计,欢迎在评论区聊聊你碰到的实际问题,或者对 Streamable HTTP 的看法~
版权声明
未经授权,禁止转载本文章。
如需转载请保留原文链接并注明出处。即视为默认获得授权。
未保留原文链接未注明出处或删除链接将视为侵权,必追究法律责任!
本文原文链接: https://fiveyoboy.com/articles/http-streamable-vs-sse-mcp/
备用原文链接: https://blog.fiveyoboy.com/articles/http-streamable-vs-sse-mcp/