Bug description
McpClientSession.sendRequest and McpServerSession.sendRequest put an entry into pendingResponses when a request is sent, but only remove it when a response arrives or the send itself fails. If the downstream .timeout(...) fires — or the caller cancels the Mono — no cleanup hook runs, so the entry stays in pendingResponses forever (request IDs are unique per request, so it is never overwritten).
The streamable variant already cleans up after its timeout (McpStreamableServerSession.McpStreamableServerSessionStream.sendRequest calls this.pendingResponses.remove(requestId) in a doOnError after .timeout(requestTimeout)), which suggests the legacy paths missing this is an oversight.
Amplifier
KeepAliveScheduler pings every session periodically via sendRequest(PING, ...). A dead or hung session therefore accumulates one leaked entry per ping interval on both the client and the server side; long-lived sessions grow unboundedly until close().
Suggested fix
Mirror the streamable pattern in both sendRequest methods:
.timeout(this.requestTimeout)
.doOnError(e -> this.pendingResponses.remove(requestId))
.doOnCancel(() -> this.pendingResponses.remove(requestId))
.handle(...)
Removal is idempotent, and the late-response path already tolerates a missing key.
I have the fix plus a regression test (testRequestTimeoutRemovesPendingResponse in McpClientSessionTests, asserting the map is empty after a timeout) ready and will open a PR referencing this issue.
Bug description
McpClientSession.sendRequestandMcpServerSession.sendRequestput an entry intopendingResponseswhen a request is sent, but only remove it when a response arrives or the send itself fails. If the downstream.timeout(...)fires — or the caller cancels the Mono — no cleanup hook runs, so the entry stays inpendingResponsesforever (request IDs are unique per request, so it is never overwritten).The streamable variant already cleans up after its timeout (
McpStreamableServerSession.McpStreamableServerSessionStream.sendRequestcallsthis.pendingResponses.remove(requestId)in adoOnErrorafter.timeout(requestTimeout)), which suggests the legacy paths missing this is an oversight.Amplifier
KeepAliveSchedulerpings every session periodically viasendRequest(PING, ...). A dead or hung session therefore accumulates one leaked entry per ping interval on both the client and the server side; long-lived sessions grow unboundedly untilclose().Suggested fix
Mirror the streamable pattern in both
sendRequestmethods:Removal is idempotent, and the late-response path already tolerates a missing key.
I have the fix plus a regression test (
testRequestTimeoutRemovesPendingResponseinMcpClientSessionTests, asserting the map is empty after a timeout) ready and will open a PR referencing this issue.