diff --git a/src/ModelContextProtocol.Core/McpSessionHandler.cs b/src/ModelContextProtocol.Core/McpSessionHandler.cs index b2f94fb28..451837a6e 100644 --- a/src/ModelContextProtocol.Core/McpSessionHandler.cs +++ b/src/ModelContextProtocol.Core/McpSessionHandler.cs @@ -536,7 +536,38 @@ public async Task SendRequestAsync(JsonRpcRequest request, Canc LogSendingRequest(EndpointName, request.Method); } - await SendToRelatedTransportAsync(request, cancellationToken).ConfigureAwait(false); + // For most requests, 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. + // + // For the initialize request, always await the send to completion. The transport's + // SendMessageAsync has side effects (setting SessionId, starting background tasks) + // that must complete before subsequent messages are sent. + if (!tcs.Task.IsCompleted) + { + Task sendTask = SendToRelatedTransportAsync(request, cancellationToken); + if (method == RequestMethods.Initialize || + sendTask == await Task.WhenAny(sendTask, tcs.Task).ConfigureAwait(false)) + { + await sendTask.ConfigureAwait(false); + } + else + { + // The response arrived via a concurrent channel before the transport send completed. + // Let the send finish naturally but observe any faults at debug level. + _ = sendTask.ContinueWith( + static (t, s) => + { + var handler = (McpSessionHandler)s!; + handler.LogTransportSendFaulted(handler.EndpointName, t.Exception!); + }, + this, + CancellationToken.None, + TaskContinuationOptions.OnlyOnFaulted, + TaskScheduler.Default); + } + } // 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 @@ -1078,4 +1109,7 @@ private static McpProtocolException CreateRemoteProtocolException(JsonRpcError e [LoggerMessage(Level = LogLevel.Trace, Message = "{EndpointName} session {SessionId} disposed with transport {TransportKind}")] private partial void LogSessionDisposed(string endpointName, string sessionId, string transportKind); + + [LoggerMessage(Level = LogLevel.Debug, Message = "{EndpointName} transport send faulted after response was already received.")] + private partial void LogTransportSendFaulted(string endpointName, Exception exception); }