From 5057f92733b422c2e1c00bc04a4c6ee8723e9d40 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 17:53:13 +0000 Subject: [PATCH 1/4] Initial plan From 47ed90da2405d57cc87d6f7072f1d1883f38be03 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 18:23:38 +0000 Subject: [PATCH 2/4] Fix race condition in McpSessionHandler.SendRequestAsync where foreground transport send blocks indefinitely when response arrives via background GET SSE stream Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../McpSessionHandler.cs | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/ModelContextProtocol.Core/McpSessionHandler.cs b/src/ModelContextProtocol.Core/McpSessionHandler.cs index 4d9cb01ba..08914591c 100644 --- a/src/ModelContextProtocol.Core/McpSessionHandler.cs +++ b/src/ModelContextProtocol.Core/McpSessionHandler.cs @@ -528,7 +528,29 @@ public async Task SendRequestAsync(JsonRpcRequest request, Canc LogSendingRequest(EndpointName, request.Method); } - await SendToRelatedTransportAsync(request, cancellationToken).ConfigureAwait(false); + // Use a linked CTS so that we can cancel the transport send if the response TCS completes + // from a concurrent channel (e.g. the background GET SSE stream in Streamable HTTP). + // Without this, the foreground transport send could block indefinitely waiting for a response + // that was already delivered via a different stream. + using var sendCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + + Task sendTask = SendToRelatedTransportAsync(request, sendCts.Token); + if (!sendTask.IsCompleted) + { + _ = tcs.Task.ContinueWith(static (_, state) => ((CancellationTokenSource)state!).Cancel(), sendCts, sendCts.Token, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); + } + + try + { + await sendTask.ConfigureAwait(false); + } + catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) + { + } + finally + { + sendCts.Cancel(); + } // Now that the request has been sent, register for cancellation. If we registered before, // a cancellation request could arrive before the server knew about that request ID, in which From bd41d2b0e833fafc9cac3e8a1c2e4d73822b2f33 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 20:15:54 +0000 Subject: [PATCH 3/4] Tighten catch filter: only catch OCE when tcs.Task has already completed, preventing masking of transport errors like connection closure Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- src/ModelContextProtocol.Core/McpSessionHandler.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ModelContextProtocol.Core/McpSessionHandler.cs b/src/ModelContextProtocol.Core/McpSessionHandler.cs index 08914591c..b993c5333 100644 --- a/src/ModelContextProtocol.Core/McpSessionHandler.cs +++ b/src/ModelContextProtocol.Core/McpSessionHandler.cs @@ -544,8 +544,10 @@ public async Task SendRequestAsync(JsonRpcRequest request, Canc { await sendTask.ConfigureAwait(false); } - catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) + catch (OperationCanceledException) when (tcs.Task.IsCompleted && !cancellationToken.IsCancellationRequested) { + // The response arrived via a concurrent channel (e.g., background GET SSE stream), + // which cancelled the transport send. Proceed to retrieve the already-completed response. } finally { From 0619fb4e5abc7759fdff82e47515ef0f11e2e773 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 23:31:16 +0000 Subject: [PATCH 4/4] Replace ContinueWith+CTS with Task.WhenAny to avoid CTS disposal race and transport cancellation issues Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../McpSessionHandler.cs | 33 ++++++++----------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/src/ModelContextProtocol.Core/McpSessionHandler.cs b/src/ModelContextProtocol.Core/McpSessionHandler.cs index b993c5333..4f79c5e90 100644 --- a/src/ModelContextProtocol.Core/McpSessionHandler.cs +++ b/src/ModelContextProtocol.Core/McpSessionHandler.cs @@ -528,31 +528,26 @@ public async Task SendRequestAsync(JsonRpcRequest request, Canc LogSendingRequest(EndpointName, request.Method); } - // Use a linked CTS so that we can cancel the transport send if the response TCS completes - // from a concurrent channel (e.g. the background GET SSE stream in Streamable HTTP). - // Without this, the foreground transport send could block indefinitely waiting for a response + // Wait for either the transport send to complete or for the response to arrive via a + // concurrent channel (e.g. the background GET SSE stream in Streamable HTTP). Without + // this, the foreground transport send could block indefinitely waiting for a response // that was already delivered via a different stream. - using var sendCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - - Task sendTask = SendToRelatedTransportAsync(request, sendCts.Token); - if (!sendTask.IsCompleted) + Task sendTask = SendToRelatedTransportAsync(request, cancellationToken); + if (sendTask != await Task.WhenAny(sendTask, tcs.Task).ConfigureAwait(false)) { - _ = tcs.Task.ContinueWith(static (_, state) => ((CancellationTokenSource)state!).Cancel(), sendCts, sendCts.Token, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); + // The response arrived via a concurrent channel before the transport send completed. + // Observe any exception from the still-running send to prevent unobserved task exceptions. + _ = sendTask.ContinueWith( + static (t, _) => _ = t.Exception, + null, + CancellationToken.None, + TaskContinuationOptions.OnlyOnFaulted, + TaskScheduler.Default); } - - try + else { await sendTask.ConfigureAwait(false); } - catch (OperationCanceledException) when (tcs.Task.IsCompleted && !cancellationToken.IsCancellationRequested) - { - // The response arrived via a concurrent channel (e.g., background GET SSE stream), - // which cancelled the transport send. Proceed to retrieve the already-completed response. - } - finally - { - sendCts.Cancel(); - } // Now that the request has been sent, register for cancellation. If we registered before, // a cancellation request could arrive before the server knew about that request ID, in which