版权声明 本站原创文章 由 萌叔 发表
转载请注明 萌叔 | http://vearne.cc
0. 起因:
萌叔开发的某个MCP服务,在本地调试正常,然后当它在线上部署时,发出tool调用后,经常没有反应,使用 modelcontextprotocol/inspector 进行调试 发现
Error from MCP server: SseError: SSE error: Premature close
MCP 服务器的 SSE 连接提前关闭错误。这个错误通常意味着客户端与服务器建立的 Server-Sent Events 长连接被意外中断了。
有可能MCP的Client和Server的连接被当成空闲连接给kill了。 这是否与协议的设计有某种联系,萌叔使用wireshark抓包发现了一些有趣的事情。
1. SSE 模式
HTTP+SSE 传输(需要 /sse和 /message两个端点) 并通过不同的 HTTP 方法来区分行为:
- POST 请求:客户端向该端点发送 JSON-RPC 消息(请求、通知或响应)。
- GET 请求:客户端向该端点发起请求以建立 SSE(Server-Sent Events)流,用于接收服务端的长连接推送或流式数据。
- DELETE 请求(可选):可用于终止会话。
1.1【连接1】
专门用于MCP-Server向MCP-Client推送数据
请求
GET /sse HTTP/1.1
accept: text/event-stream
accept-encoding: gzip, deflate, br
user-agent: node-fetch
Host: localhost:8080
Connection: keep-alive
响应
注意:【连接1】是长连接,且Content-Type: text/event-stream,连接不会被关闭。
text/event-stream
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Cache-Control: no-cache
Connection: keep-alive
Content-Type: text/event-stream
Date: Sat, 09 May 2026 07:24:50 GMT
Transfer-Encoding: chunked
51
event: endpoint
data: /message?sessionId=b14c4662-90d6-4b58-8da4-6fb8091c9700
1.2 【连接2】
MCP-Client发起一个 tools/call
请求
POST /message?sessionId=e8769169-6676-4e6b-b9c7-af68e4942b32 HTTP/1.1
host: localhost:8080
connection: keep-alive
mcp-protocol-version: 2025-06-18
Accept: text/event-stream
content-type: application/json
accept-language: *
sec-fetch-mode: cors
user-agent: node
accept-encoding: gzip, deflate
content-length: 134
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"_meta":{"progressToken":3},"name":"hello_world","arguments":{"name":"lily"}}}
响应
注意: server的Response并不包含 tools/call的结果
MCP-Server 只是告诉MCP-Client,任务已经收到。
这是 SSE 传输模式的特定行为:POST 请求返回 202 Accepted 是 SSE 传输层的实现细节,并非 MCP 协议本身的规范要求。
在 Streamable 模式下,tools/call 的结果会直接在 POST 响应体中返回(详见第 3.2 节)。
HTTP/1.1 202 Accepted
Date: Sat, 09 May 2026 07:04:15 GMT
Content-Length: 0
1.3 tools/call的结果仍然通过【连接1】的event stream进行推送
6c ← chunk size(十六进制)
\r\n
event: message
data: {"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"Hello, lily!"}]}}
\r\n
| 内容 | 含义 |
|---|---|
| 6c | chunk 长度 = 0x6C = 108 |
| \r\n | chunk size 结束符 |
| 后面内容 | 实际 chunk 数据 |
| 结尾 \r\n | chunk 数据结束 |
不仅如此,MCP-Server端发起的Ping,其它tools/call的结果也都通过【连接1】的的event stream进行推送
40
\r\n
event: message
data:{"jsonrpc":"2.0","id":129,"method":"ping"}
\r\n
MCP的SSE模式协议规定,【连接1】(GET /sse)如果连接断开,MCP-Client有义务重新发起连接
2. 解决方法
由于【连接1】(GET /sse) 是长连接,如果一段时间没有数据传输,很容易被网关判定为空闲连接,进而被kill。 为解决这个问题,MCP规范允许Server或Client发起Ping RPC。
2.1 MCP-Client 发起Ping RPC
1) 客户端发送 Ping 请求:
客户端通过 POST /message端点发送一个标准的 JSON-RPC 请求:
{ "jsonrpc": "2.0", "id": 1, "method": "ping" }
服务端通常立即返回 HTTP 202 Accepted,表示已接收。
2) 服务端返回 Ping 响应:
根据 MCP 规范,ping是一个请求/响应模式,接收方必须立即回复一个空结果。在 SSE 模式下,所有的 JSON-RPC 响应(Response)都必须通过最初建立的 /sse长连接(SSE 流)下发,而不能直接在 POST 的 HTTP 回复体中返回。 所以,服务端会往 /sse连接里推送:
{ "jsonrpc": "2.0", "id": 1, "result": {} }
2.2 MCP-Server 发起Ping RPC
Server端发送的Ping也在【连接1】(GET /sse) 中以event stream的形式推送
40
event: message
data:{"jsonrpc":"2.0","id":129,"method":"ping"}
以 mark3labs/mcp-go为例,可以以如下方式开启 类似心跳,周期性发送Ping RPC
func (worker *MCPWorker) StartSSEServer(s *server.MCPServer) {
addr := "0.0.0.0:" + getEnv("MCP_SSE_PORT", "8080")
log.Info("Starting SSE HTTP server", zap.String("addr", addr))
worker.SSEServer = server.NewSSEServer(s,
server.WithKeepAlive(true), // 这里
server.WithKeepAliveInterval(5*time.Second), // 这里
)
if err := worker.SSEServer.Start(addr); err != nil {
log.Fatalf("Server error: %s\n", err)
}
}
3. Streamable 模式
Streamable HTTP 将所有通信合并到了同一个endpoint(默认是/mcp)下, 请求和响应一一对应,一般而言处于同一个连接中。
注意: 响应Content-Type可以是 “application/json” 或者 “text/event-stream”,由MCP-Server自行决定。
3.1 建立会话
请求
POST /mcp HTTP/1.1
accept: application/json, text/event-stream
accept-encoding: gzip, deflate, br
content-length: 330
content-type: application/json
user-agent: node-fetch
Host: localhost:8081
Connection: keep-alive
{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{"sampling":{},"elicitation":{},"roots":{"listChanged":true},"tasks":{"list":{},"cancel":{},"requests":{"sampling":{"createMessage":{}},"elicitation":{"create":{}}}}},"clientInfo":{"name":"inspector-client","version":"0.21.2"}}}
响应
HTTP/1.1 200 OK
Content-Type: application/json
Mcp-Session-Id: mcp-session-4ef132a7-3d7e-4153-b62e-c73ee0cb5305
Date: Sat, 09 May 2026 09:47:50 GMT
Content-Length: 167
{"jsonrpc":"2.0","id":0,"result":{"protocolVersion":"2025-06-18","capabilities":{"tools":{"listChanged":true}},"serverInfo":{"name":"metrics-mcp","version":"0.0.4"}}}
3.2 tools/call
请求
POST /mcp HTTP/1.1
accept: application/json, text/event-stream
accept-encoding: gzip, deflate, br
content-length: 37
content-type: application/json
mcp-protocol-version: 2025-06-18
mcp-session-id: mcp-session-ef312962-8590-44c2-867d-33845bb1a585
user-agent: node-fetch
Host: localhost:8081
Connection: keep-alive
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"_meta":{"progressToken":3},"name":"hello_world","arguments":{"name":"lily"}}}
响应
HTTP/1.1 200 OK
Content-Type: application/json
Date: Sat, 09 May 2026 09:28:00 GMT
Content-Length: 86
{"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"Hello, lily!"}]}}
3.3 会话恢复(Session Resumption)
Streamable 模式的一个重要特性是会话恢复。在 initialize 阶段(3.1 节),Server 会在响应头中返回 Mcp-Session-Id,
客户端在后续请求中通过 mcp-session-id 头部携带该标识。
如果连接意外断开,客户端可以携带同一个 Mcp-Session-Id 重新发起请求,Server 端据此恢复之前的会话状态,而无需重新初始化。
POST /mcp HTTP/1.1
mcp-session-id: mcp-session-ef312962-8590-44c2-867d-33845bb1a585
...
这与 SSE 模式形成鲜明对比:
| 特性 | SSE 模式 | Streamable 模式 |
|---|---|---|
| 会话标识 | sessionId(query 参数) | Mcp-Session-Id(HTTP 头部) |
| 连接断开后 | Client 重新建立 /sse 长连接,但之前的会话状态可能丢失 | Client 携带 Mcp-Session-Id 重连,Server 可恢复会话状态 |
| 恢复能力 | 较弱 — 依赖 Client/Server 双方的重连实现 | 较强 — 协议层面支持会话恢复 |
简单来说,SSE 模式下连接断开是"从头再来",而 Streamable 模式下是"断点续传"。
3.4 开启Server端心跳
如果响应使用 “text/event-stream”,tools/call 处理逻辑复杂,长时间没有数据传输,
仍然有可能导致连接被误认为空闲而被kill。
仍旧以 mark3labs/mcp-go为例,
可以以如下方式开启心跳,周期性发送Ping RPC,心跳与响应使用同一个连接。
func (worker *MCPWorker) StartStreamableHttp(s *server.MCPServer) {
addr := "0.0.0.0:" + getEnv("MCP_STREAMABLE_PORT", "8081")
log.Info("Starting Streamable HTTP server", zap.String("addr", addr))
worker.StreamableServer = server.NewStreamableHTTPServer(s,
server.WithEndpointPath("/streamable"),
server.WithHeartbeatInterval(5*time.Second), // 这里
)
if err := worker.StreamableServer.Start(addr); err != nil {
log.Fatalf("Server error: %s\n", err)
}
}
4. 总结
MCP 协议提供了两种 HTTP 传输模式,各有特点:
| 特性 | SSE 模式 | Streamable 模式 |
|---|---|---|
| 端点 | /sse + /message 两个 | /mcp 单个 |
| 连接模型 | 长连接(GET /sse)+ 短请求(POST /message) | 每次请求复用同一连接 |
| 响应方式 | 所有响应通过 SSE 长连接推送 | 响应直接在 POST 回复中返回(也可能使用 SSE) |
| 空闲连接风险 | 高 — GET /sse 长连接容易被网关判定为空闲而 kill | 低 — 但如果响应使用 SSE 流式输出,仍需注意 |
| 会话恢复 | 不支持 — 连接断开后需重建,状态可能丢失 | 支持 — 通过 Mcp-Session-Id 恢复会话 |
核心结论:
SSE 模式下,
GET /sse是一条持久长连接,长时间无数据传输时极易被中间网关(Nginx、CDN、负载均衡器等)判定为空闲连接并主动断开。解决方法是开启 Ping RPC 心跳(Server 端或 Client 端均可发起),定期在连接上产生流量,防止被误杀。Streamable 模式将请求和响应合并在同一个连接中,大多数情况下不存在空闲连接的问题。但当 Server 选择以
text/event-stream方式响应时(例如长时间任务的流式输出),同样需要关注心跳保活,避免连接被中间层中断。从部署角度看,如果环境中有网关、反向代理等中间层,优先推荐使用 Streamable 模式,它能显著减少因长连接空闲导致的连接中断问题。如果必须使用 SSE 模式,务必配置好 Ping 心跳间隔。
