-
Notifications
You must be signed in to change notification settings - Fork 258
Description
Describe the bug
The streamable HTTP transport implementation in mcp/streamable.go leaks a goroutine on the server for each client that disconnects ungracefully. Over time, a server can accumulate many "zombie" sessions, consuming resources.
Details
When a new MCP session is established via the connect function in transport.go:141, the jsonrpc2 connection layer spawns a long-running goroutine to read incoming client messages. This goroutine, created in internal/jsonrpc2/conn.go:279, eventually blocks on the streamableServerConn.Read(...) method.
The Read method waits on one of three conditions:
- The session's context is canceled.
- A message arrives on the incoming channel.
- The session's done channel is closed.
When a client disconnects abruptly (e.g., due to network failure), no more HTTP requests can be made, so no new messages will ever arrive on the incoming channel. The session's context is intentionally detached from the underlying HTTP requests, so it does not get canceled. The session is only terminated, and its done channel closed, when a keep-alive fails or the client sends an explicit DELETE request.
In the case of an ungraceful disconnect, the server is never notified, leaving the reading goroutine blocked indefinitely in the Read(...) method, thus leaking the goroutine.
To Reproduce
Checkout the code from #496 (which adds an opt-in leak detector for unit tests) and run the test with the leak detector enabled:
❯ git checkout fgrosse:fgrosse/goleaks
❯ go test -run TestClientReplay/default -leak ./mcp
PASS
goleak: Errors on successful test run: found unexpected goroutines:
[Goroutine 66 in state select, with github.com/modelcontextprotocol/go-sdk/mcp.(*streamableServerConn).Read on top of the stack:
github.com/modelcontextprotocol/go-sdk/mcp.(*streamableServerConn).Read(0x140003ae0c0, {0x10553b1f8, 0x1400038a090})
/Users/fgroe/src/panw/mcp/official-go-sdk/mcp/streamable.go:869 +0x7c
github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2.(*Connection).readIncoming(0x1400038c410, {0x10553b1f8, 0x1400038a090}, {0x10c77c280, 0x140003ae0c0}, {0x105533c80, 0x140003a2010})
/Users/fgroe/src/panw/mcp/official-go-sdk/internal/jsonrpc2/conn.go:536 +0x54
created by github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2.NewConnection.(*Connection).start.func1 in goroutine 46
/Users/fgroe/src/panw/mcp/official-go-sdk/internal/jsonrpc2/conn.go:279 +0xd8
]
exit status 1
FAIL github.com/modelcontextprotocol/go-sdk/mcp 3.386sExpected behavior
The server should never leak any goroutines.
Additional context
I found this issue while investigating flaky unit tests via #489