Skip to content

Commit 0dea2cc

Browse files
Copilotstephentoub
andcommitted
Move session ID and background GET setup before SSE processing to prevent cancellation race
The McpSessionHandler.SendRequestAsync races the transport send against the response TCS using Task.WhenAny. When the TCS completes during SSE stream processing, the send task is cancelled. But SendHttpRequestAsync previously set SessionId, _negotiatedProtocolVersion, and started _getReceiveTask AFTER SSE processing (lines 136-148), so these critical side effects were lost. Fix: extract SessionId from HTTP response headers and start _getReceiveTask immediately after receiving a successful HTTP response for initialize requests, before SSE content processing begins. The protocol version (from the response body) is still set after SSE processing when possible. Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
1 parent 37809ca commit 0dea2cc

1 file changed

Lines changed: 15 additions & 8 deletions

File tree

src/ModelContextProtocol.Core/Client/StreamableHttpClientSessionTransport.cs

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,21 @@ internal async Task<HttpResponseMessage> SendHttpRequestAsync(JsonRpcMessage mes
100100
}
101101

102102
var rpcRequest = message as JsonRpcRequest;
103+
104+
// For initialize requests, capture session metadata from response headers immediately,
105+
// before processing the response body. McpSessionHandler.SendRequestAsync races the
106+
// transport send against the response TCS and may cancel this task once the response
107+
// arrives during SSE stream processing, so session state must be set before that.
108+
if (rpcRequest?.Method == RequestMethods.Initialize)
109+
{
110+
if (response.Headers.TryGetValues("Mcp-Session-Id", out var sessionIdValues))
111+
{
112+
SessionId = sessionIdValues.FirstOrDefault();
113+
}
114+
115+
_getReceiveTask ??= ReceiveUnsolicitedMessagesAsync();
116+
}
117+
103118
JsonRpcMessageWithId? rpcResponseOrError = null;
104119

105120
if (response.Content.Headers.ContentType?.MediaType == "application/json")
@@ -135,16 +150,8 @@ internal async Task<HttpResponseMessage> SendHttpRequestAsync(JsonRpcMessage mes
135150

136151
if (rpcRequest.Method == RequestMethods.Initialize && rpcResponseOrError is JsonRpcResponse initResponse)
137152
{
138-
// We've successfully initialized! Copy session-id and protocol version, then start GET request if any.
139-
if (response.Headers.TryGetValues("Mcp-Session-Id", out var sessionIdValues))
140-
{
141-
SessionId = sessionIdValues.FirstOrDefault();
142-
}
143-
144153
var initializeResult = JsonSerializer.Deserialize(initResponse.Result, McpJsonUtilities.JsonContext.Default.InitializeResult);
145154
_negotiatedProtocolVersion = initializeResult?.ProtocolVersion;
146-
147-
_getReceiveTask ??= ReceiveUnsolicitedMessagesAsync();
148155
}
149156

150157
return response;

0 commit comments

Comments
 (0)