目录

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.”

但背后的理由值得细说。

/img/http-streamable-vs-sse-mcp/0101.png
StreamableHTTP

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/