@@ -99,15 +99,33 @@ private async Task InitializeAsync(JsonRpcMessage message, CancellationToken can
9999 // behavior. Capture the underlying error (status + body) before falling back so that,
100100 // if SSE also fails, we can surface the real Streamable HTTP diagnostic to the caller
101101 // instead of dropping it on the floor (see https://github.com/modelcontextprotocol/csharp-sdk/issues/1526).
102- LogStreamableHttpFailed ( _name , response . StatusCode ) ;
103-
104102 // This reads the response body a second time for the application/json case, where
105103 // TryReadJsonRpcErrorAsync above already read it. HttpContent buffers after the first
106104 // read, so this returns the same buffered content and is safe (not a second stream
107105 // consumption). For the common non-JSON error responses (415, 405, plain text)
108106 // TryReadJsonRpcErrorAsync returns early on the content type, so there is no double read.
109107 var streamableHttpError = await HttpResponseMessageExtensions . CreateHttpRequestExceptionWithBodyAsync ( response , cancellationToken ) . ConfigureAwait ( false ) ;
110108
109+ if ( IsDiscoverProbeRejection ( message , response . StatusCode ) )
110+ {
111+ // The server/discover probe is protocol negotiation, not transport detection. A server
112+ // predating SEP-2575 rejects the session-less POST with 400 (can't parse the request) or
113+ // 404 (requires Mcp-Session-Id on every non-initialize POST) whether it speaks Streamable
114+ // HTTP or SSE, so neither status is evidence about which transport to use. McpClientImpl
115+ // .ConnectAsync treats exactly these two statuses as "initialize-handshake server" and
116+ // immediately retries with initialize on this same transport — and that attempt still
117+ // falls back to SSE, so an SSE-only server is reached one POST later rather than not at
118+ // all. Attempting SSE here instead spends a GET whose result is discarded on every
119+ // connect to a Streamable-HTTP-only server that predates SEP-2575, and logs a "falling
120+ // back to SSE transport" line that misreports settled protocol negotiation as a failure.
121+ LogSkippingSseFallbackForDiscoverProbe ( _name , response . StatusCode ) ;
122+
123+ await streamableHttpTransport . DisposeAsync ( ) . ConfigureAwait ( false ) ;
124+ throw streamableHttpError ;
125+ }
126+
127+ LogStreamableHttpFailed ( _name , response . StatusCode ) ;
128+
111129 await streamableHttpTransport . DisposeAsync ( ) . ConfigureAwait ( false ) ;
112130 await InitializeSseTransportAsync ( message , streamableHttpError , cancellationToken ) . ConfigureAwait ( false ) ;
113131 }
@@ -122,6 +140,15 @@ private async Task InitializeAsync(JsonRpcMessage message, CancellationToken can
122140 }
123141 }
124142
143+ /// <summary>
144+ /// Returns <see langword="true"/> when the failed request was the SEP-2575 <c>server/discover</c> probe and the
145+ /// status is one of the two <see cref="McpClientImpl"/> already reads as "this server requires the initialize
146+ /// handshake", meaning the SSE fallback cannot contribute anything the initialize retry won't.
147+ /// </summary>
148+ private static bool IsDiscoverProbeRejection ( JsonRpcMessage message , HttpStatusCode statusCode ) =>
149+ statusCode is HttpStatusCode . BadRequest or HttpStatusCode . NotFound &&
150+ message is JsonRpcRequest { Method : RequestMethods . ServerDiscover } ;
151+
125152 private async Task InitializeSseTransportAsync ( JsonRpcMessage message , HttpRequestException ? streamableHttpError , CancellationToken cancellationToken )
126153 {
127154 if ( _options . KnownSessionId is not null )
@@ -184,6 +211,9 @@ public async ValueTask DisposeAsync()
184211 [ LoggerMessage ( Level = LogLevel . Information , Message = "{EndpointName} streamable HTTP transport failed with status code {StatusCode}, falling back to SSE transport." ) ]
185212 private partial void LogStreamableHttpFailed ( string endpointName , HttpStatusCode statusCode ) ;
186213
214+ [ LoggerMessage ( Level = LogLevel . Debug , Message = "{EndpointName} server/discover probe rejected with status code {StatusCode}; skipping the SSE fallback so the initialize handshake is attempted instead." ) ]
215+ private partial void LogSkippingSseFallbackForDiscoverProbe ( string endpointName , HttpStatusCode statusCode ) ;
216+
187217 [ LoggerMessage ( Level = LogLevel . Information , Message = "{EndpointName} using Streamable HTTP transport." ) ]
188218 private partial void LogUsingStreamableHttp ( string endpointName ) ;
189219
0 commit comments