Skip to content

[Bug]: MCP Streamable HTTP client never DELETEs retired sessions — 287 sessions / 146 sockets accumulate against one server #162

Description

@inercia

What happened?

Auggie's Streamable HTTP MCP client never sends DELETE to terminate an MCP session it has stopped using. Each new session mints a fresh Mcp-Session-Id via initialize, and the previous one is abandoned rather than terminated: its TCP connection and standalone SSE GET keepalive stream stay open indefinitely, so the server keeps the session (and everything it pins) alive forever.

Over a normal working day a single auggie process accumulates well over a hundred simultaneously-open MCP sessions against one server.

Measured against a local MCP server (Go, modelcontextprotocol/go-sdk v1.4.0, Streamable HTTP on 127.0.0.1:5757/mcp), with server-side request logging:

Metric (single server, ~29-minute log window) Value
Distinct Mcp-Session-Ids seen on SSE GET 287
Distinct Mcp-Session-Ids that ever sent a POST (i.e. actually in use) 14
DELETE requests received from any client, ever 0
Established TCP connections from the busiest single auggie pid 146

So ~95% of the live sessions are pure keepalive ghosts: they were initialized, used briefly or not at all, then abandoned while still holding an open SSE stream.

lsof -nP -iTCP:5757 -sTCP:ESTABLISHED, grouped by client pid:

146 node 50364
 60 node 61552
 34 node 90807
 19 node 54851
 11 node 67619

Each ghost session costs the server a goroutine set, a file descriptor and an open SSE stream. On our side ~89% of the server process's goroutines were attributable to these abandoned sessions. It is also self-harming for Auggie: 146 sockets to one server saturates the client's own connection pool, and we correlated this with Auggie's own MCP initialization timed out after Ns gate firing (budgets escalating 11s → 634s) while the server's handler was answering in under 1ms throughout.

The MCP spec (2025-03-26, §Session Management) is explicit here:

Clients that no longer need a particular session ... SHOULD send an HTTP DELETE to the MCP endpoint with the Mcp-Session-Id header, to explicitly terminate the session.

What did you expect to happen?

When Auggie retires an MCP transport — the ACP/agent session it belonged to ended, the client reconnected, the server list was reloaded, or the process is shutting down — it should send DELETE <endpoint> with the Mcp-Session-Id header (and close the SSE GET) before dropping the transport, so the server can release the session immediately.

Steady state should be roughly one live MCP session per active agent session, not an ever-growing pile.

Steps to reproduce

  1. Register any Streamable HTTP MCP server globally (~/.augment/settings.json), e.g. http://127.0.0.1:5757/mcp.
  2. Enable request logging on that server (method, path, Mcp-Session-Id).
  3. Run Auggie normally for an hour, starting/ending several agent sessions (or driving it via ACP so sessions are created and retired repeatedly).
  4. Observe on the server:
    • the number of distinct Mcp-Session-Ids climbs monotonically (~8 new sessions/hour/process in our case);
    • the great majority of them only ever issue the standalone GET keepalive, never a POST;
    • not a single DELETE is ever received;
    • lsof -nP -iTCP:<port> -sTCP:ESTABLISHED shows the connection count from the auggie pid growing without bound.

Server-side termination is well tolerated, which confirms the sessions really are abandoned. We deleted two sessions by hand with curl -X DELETE -H 'Mcp-Session-Id: <id>' <endpoint> (both returned 204):

  • an idle ghost (SSE only, zero POSTs ever): the client retried the GET twice, got 404 both times, then tore the transport down cleanly — no error storm, sibling sessions unaffected;
  • a live session that was actively carrying tools/call traffic: subsequent tool calls continued with zero user-visible disruption, transparently failing over to another pooled session the same process already held.

Auggie version

0.34.0 (commit 81042879), installed via npm/Homebrew (@augmentcode/auggie)

Request ID

n/a — this is a client-side transport-lifecycle issue observed from the MCP server side, not a model request failure.

Environment details

Environment
  • OS: macOS (Apple Silicon)
  • Shell: zsh
  • Tool/CLI version: auggie 0.34.0 (commit 81042879)
  • MCP server under test: Go, github.com/modelcontextprotocol/go-sdk v1.4.0, Streamable HTTP transport, 127.0.0.1:5757/mcp
  • Transport: Streamable HTTP (MCP spec 2025-03-26), JSONResponse: true

Anything else we need to know?

Relationship to #149. These are two halves of the same lifecycle gap and a fix for one without the other is incomplete:

Concretely: a server operator's only defence against the pile-up is a SessionTimeout, and turning that on is exactly what exposes #149. Fixing both — DELETE on retire here, and 404 → re-initialize in #149 — closes the loop.

Workaround for server authors (what we did): set an idle-session timeout on the Streamable HTTP handler. With the Go SDK, mcp.StreamableHTTPOptions{SessionTimeout: 30 * time.Minute}. Note the SDK's idle timer keys off POST activity only — the periodic SSE GET recycle does not refresh it — which is what makes this effective against exactly the ghost population described above. Server-side reaping is a mitigation, not a fix: it cannot reclaim a session any earlier than the timeout, and it depends on clients handling the resulting 404 correctly (#149).

Happy to supply the raw server logs or re-run the measurement against a build with a fix.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions