Conversation
The streamable HTTP server registers a session on `initialize` and, on mcp-go v0.43.2, never releases it: `handleDelete` clears the per-session stores but never calls `UnregisterSession`, so every session a client ever opened is retained. A POST-only client leaks roughly 8 KB per initialize, which in production shows up as linear heap growth until the container is OOMKilled. Bump mcp-go to v1.1.0, which unregisters the session on DELETE, and enable its idle sweeper so sessions abandoned without a DELETE are reclaimed too. The sweeper is opt-in even on v1.1.0, so the bump alone is not enough. The new `--session-idle-ttl` flag defaults to 10m and 0 disables it. Shutdown now stops the sweeper and closes sessions that are still registered. Measured over repeated bursts of 500 initialize requests, 15s apart: mcp-go v0.43.2 3.44 -> 7.74 -> 12.96 -> 17.96 -> 21.65 MB live heap this change 3.93 -> 9.55 -> 9.63 -> 9.55 -> 9.55 MB live heap Closes kagent-dev#84 Signed-off-by: younsl <cysl@kakao.com>
`make test` covered ./pkg/... and ./internal/... only, so the tests in cmd/ never ran in CI. That includes the session lifecycle regression tests this branch adds, which is the code path that leaked. Signed-off-by: younsl <cysl@kakao.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this changes
The streamable HTTP server registers an MCP session on
initializeand never released it, so heap grew with the number of sessions ever created until the pod was OOMKilled.github.com/mark3labs/mcp-gofromv0.43.2tov1.1.0, which unregisters the session inhandleDelete(mark3labs/mcp-go#724, released inv0.44.1).server.WithSessionIdleTTL, so sessions abandoned without aDELETEare reclaimed as well. It is opt-in even onv1.1.0(the default is zero, which disables it), so the dependency bump alone does not fix POST-only clients.--session-idle-ttl, default10m,0disables the sweeper.cmdpackage tests inmake test. They were excluded, so nothing incmd/was covered by CI, including the regression tests added here.Why the dependency bump alone is not enough
On
v0.43.2the session is stored inactiveSessionsand inserver.sessionsoninitialize, buthandleDeleteonly clears the per-session tool, resource, log-level and request-ID stores.UnregisterSessionis reached on theGETSSE path only, so a POST-only client, which is the normal request/response mode, retains every session it ever opened.v1.1.0fixes theDELETEpath, but a client that crashes, is restarted, or has its connection dropped by a load balancer never sends one. The sweeper is what bounds memory in that case.Verification
Eight bursts of 500
initializerequests, 15s apart, against both binaries. Live objects arego_memstats_heap_objectstaken as the minimum of 25 samples, which approximates the post-GC live set; the GC counter confirms collections ran between samples (6 at start, 15 and 34 at the end of the two runs).v0.43.2live objectsv0.43.2heapOn
v0.43.2the live set climbs with cumulative sessions and never returns. With this change it oscillates around its starting value and comes back to it: 21.7k objects and 4.09 MB after 4,000 sessions, against 20.2k and 3.90 MB before the first request.Tests added in
cmd/streamable_http_test.goassert the behavior directly rather than through heap numbers, usingserver.Hooksto observe session registration:DELETEis releasedDELETEis released by the sweeperAlso ran locally:
make build,make test-only(all packages pass),make helm-test(23 tests pass),gofmt -l .,go vet,go fix.make lintfails on this machine, but it fails identically onmainwith the same linter binary:golangci-lint v1.63.4reportsundefined: Byand similar typecheck errors for the ginkgo dot-imports intest/e2eunder a newer local Go toolchain. E2E was not run; it needs a Kind cluster.Operator impact
Default behavior changes: session state idle for more than 10 minutes is now reclaimed. A session reclaimed while its client is still alive is re-established on the next
initialize. Deployments that want a different value can set it throughtools.argsin the Helm chart, and--session-idle-ttl=0restores the previous, unbounded behavior.pprofon the metrics port, item 3 of the proposed fix in the issue, is left out to keep this change focused. Happy to open a separate PR for it.Closes #84