Skip to content

Proposal: Add StreamableHTTPOptions.SessionTimeout (Ungraceful client disconnects leads to zombie sessions) #499

@fgrosse

Description

@fgrosse

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:

  1. The session's context is canceled.
  2. A message arrives on the incoming channel.
  3. 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.386s

Expected behavior

The server should never leak any goroutines.

Additional context

I found this issue while investigating flaky unit tests via #489

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingproposalA proposal for an a new API or behavior. See CONTRIBUTING.md.proposal-acceptedProposals that have been accepted.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions