版权声明 本站原创文章 由 萌叔 发表
转载请注明 萌叔 | 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
内容含义
6cchunk 长度 = 0x6C = 108
\r\nchunk size 结束符
后面内容实际 chunk 数据
结尾 \r\nchunk 数据结束

不仅如此,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 恢复会话

核心结论

  1. SSE 模式下,GET /sse 是一条持久长连接,长时间无数据传输时极易被中间网关(Nginx、CDN、负载均衡器等)判定为空闲连接并主动断开。解决方法是开启 Ping RPC 心跳(Server 端或 Client 端均可发起),定期在连接上产生流量,防止被误杀。

  2. Streamable 模式将请求和响应合并在同一个连接中,大多数情况下不存在空闲连接的问题。但当 Server 选择以 text/event-stream 方式响应时(例如长时间任务的流式输出),同样需要关注心跳保活,避免连接被中间层中断。

  3. 从部署角度看,如果环境中有网关、反向代理等中间层,优先推荐使用 Streamable 模式,它能显著减少因长连接空闲导致的连接中断问题。如果必须使用 SSE 模式,务必配置好 Ping 心跳间隔。


微信公众号