You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
fix(server): expire idle Streamable HTTP sessions and cap their count - #1032
mcpStreamableHttp kept every session until DELETE, so an initialize flood could exhaust memory. Idle sessions now expire, and new ones are capped.
StreamableHttpClientTransport now reports a 404 on its GET stream through onError instead of dropping the stream quietly, so a client learns that its session has ended.
Idle sessions expire after 30 min without requests; an open GET stream does not count. An expired session answers 404, and the client has to start a new session: StreamableHttpClientTransport does not do it by itself.
GET without Mcp-Session-Id returns 400.
Types of changes
Bug fix (non-breaking change which fixes an issue)
New feature (non-breaking change which adds functionality)
Breaking change (fix or feature that would cause existing functionality to change)
This only rejects IDs that are still present in the routing map, but cleanUpRequests removes them after completion. MCP 2025-11-25 requires that a request ID MUST NOT have been previously used by the requestor within the same session, so a completed ID is accepted here. Keep a separate session-lifetime set of all observed IDs (atomically reserving an entire batch) and clear it only when the transport closes.
The reason will be displayed to describe this comment to others. Learn more.
Copilot review overview
🟡 Changes recommended
Late responses can be misrouted after request-ID reuse, inherited Ktor configuration can preserve disconnect leaks, and readiness probing accepts server errors.
The reason will be displayed to describe this comment to others. Learn more.
Blocking: this installs HttpRequestLifecycle { cancelCallOnClose = true } on the receiver route, and for the canonical entry points that receiver is the routing root — Application.mcp does routing { mcp(...) }, and the README's Route.mcp example is routing { mcp { MyServer() } }. So the whole application silently switches to "cancel the handler when the client disconnects", not just the MCP endpoints. mcpStreamableHttp gets this right by installing inside route(path); the legacy SSE path does not.
Failure scenario: an app that serves MCP next to its own routes, e.g. Application.mcp { MyServer() } plus post("/orders") { chargeCard(); saveOrder() }. Today a client that disconnects mid-POST still gets its order saved; after this upgrade the handler is cancelled at the next suspension point, so the card is charged and the order is never written. Ktor's own docs call this out: keep cancelCallOnClose = false "when you need to complete processing regardless of client connection status (e.g., important side effects, database transactions)". Verified against a real CIO server on b0070ae — an unrelated get("/slow") whose client drops the socket:
and with this one line removed, routing { mcp { ... } } gives cancelled=false completed=true, so this line is the cause. It is also not in the PR's Breaking Changes list.
Suggested fix: keep the install inside the routes MCP actually registers, the way mcpStreamableHttp keeps it inside route(path). In this overload that means scoping it to the SSE GET:
I confirmed that all five ClientDisconnectIntegrationTest cases (including an SSE session ends when its client disconnects) still pass with that change, while /slow is no longer cancelled. Worth pinning with a test that an unrelated route's handler still runs to completion after its client disconnects alongside Application.mcp.
The reason will be displayed to describe this comment to others. Learn more.
lgtm
This branch has not been deployed
No deployments
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
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.
mcpStreamableHttpkept every session untilDELETE, so aninitializeflood could exhaust memory. Idle sessions now expire, and new ones are capped.StreamableHttpClientTransportnow reports a404on itsGETstream throughonErrorinstead of dropping the stream quietly, so a client learns that its session has ended.closes #922
closes #715
How Has This Been Tested?
Unit tests,
testApplicationtests, and integration tests against a real CIO server with abrupt client disconnects and with the SDK's own client.Breaking Changes
mcpStreamableHttpJVM signature changed; recompile.GETstream does not count. An expired session answers404, and the client has to start a new session:StreamableHttpClientTransportdoes not do it by itself.GETwithoutMcp-Session-Idreturns400.Types of changes
Checklist