22using ModelContextProtocol . Protocol ;
33using ModelContextProtocol . Tests . Utils ;
44using System . Net ;
5+ using System . Text ;
56
67namespace ModelContextProtocol . Tests . Transport ;
78
@@ -246,4 +247,83 @@ public async Task DisposeAsync_Should_Dispose_Resources()
246247 var transportBase = Assert . IsAssignableFrom < TransportBase > ( session ) ;
247248 Assert . False ( transportBase . IsConnected ) ;
248249 }
250+
251+ [ Fact ]
252+ public async Task StreamableHttp_InitialGetSseConnection_DoesNotCountAgainstMaxReconnectionAttempts ( )
253+ {
254+ // Arrange: The initial GET SSE connection (with no Last-Event-ID) is the initial connection,
255+ // not a reconnection. It should not count against MaxReconnectionAttempts.
256+ // With MaxReconnectionAttempts=2, we expect 1 initial + 2 reconnection = 3 total GET requests.
257+ const int MaxReconnectionAttempts = 2 ;
258+
259+ var getRequestCount = 0 ;
260+ var allGetRequestsDone = new TaskCompletionSource < bool > ( TaskCreationOptions . RunContinuationsAsynchronously ) ;
261+
262+ var options = new HttpClientTransportOptions
263+ {
264+ Endpoint = new Uri ( "http://localhost:8080" ) ,
265+ TransportMode = HttpTransportMode . StreamableHttp ,
266+ MaxReconnectionAttempts = MaxReconnectionAttempts ,
267+ DefaultReconnectionInterval = TimeSpan . FromMilliseconds ( 1 ) ,
268+ } ;
269+
270+ using var mockHttpHandler = new MockHttpHandler ( ) ;
271+ using var httpClient = new HttpClient ( mockHttpHandler ) ;
272+ await using var transport = new HttpClientTransport ( options , httpClient , LoggerFactory ) ;
273+
274+ mockHttpHandler . RequestHandler = ( request ) =>
275+ {
276+ if ( request . Method == HttpMethod . Post )
277+ {
278+ // Return a successful initialize response with a session-id header.
279+ // This triggers ReceiveUnsolicitedMessagesAsync which starts the GET SSE stream.
280+ var response = new HttpResponseMessage
281+ {
282+ StatusCode = HttpStatusCode . OK ,
283+ Content = new StringContent (
284+ """{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-03-26","capabilities":{},"serverInfo":{"name":"TestServer","version":"1.0.0"}}}""" ,
285+ Encoding . UTF8 ,
286+ "application/json" ) ,
287+ } ;
288+ response . Headers . Add ( "Mcp-Session-Id" , "test-session" ) ;
289+ return Task . FromResult ( response ) ;
290+ }
291+
292+ if ( request . Method == HttpMethod . Get )
293+ {
294+ // Return 500 for all GET SSE requests to force the retry loop to exhaust all attempts.
295+ var count = Interlocked . Increment ( ref getRequestCount ) ;
296+ if ( count == 1 + MaxReconnectionAttempts )
297+ {
298+ allGetRequestsDone . TrySetResult ( true ) ;
299+ }
300+ return Task . FromResult ( new HttpResponseMessage
301+ {
302+ StatusCode = HttpStatusCode . InternalServerError ,
303+ } ) ;
304+ }
305+
306+ if ( request . Method == HttpMethod . Delete )
307+ {
308+ return Task . FromResult ( new HttpResponseMessage
309+ {
310+ StatusCode = HttpStatusCode . OK ,
311+ } ) ;
312+ }
313+
314+ throw new InvalidOperationException ( $ "Unexpected request: { request . Method } ") ;
315+ } ;
316+
317+ // Act - Connect and send the initialize request, which starts the background GET SSE task.
318+ await using var session = await transport . ConnectAsync ( TestContext . Current . CancellationToken ) ;
319+ await session . SendMessageAsync (
320+ new JsonRpcRequest { Method = RequestMethods . Initialize , Id = new RequestId ( 1 ) } ,
321+ TestContext . Current . CancellationToken ) ;
322+
323+ // Wait for all expected GET requests to be made before disposing.
324+ await allGetRequestsDone . Task . WaitAsync ( TimeSpan . FromSeconds ( 10 ) , TestContext . Current . CancellationToken ) ;
325+
326+ // Assert - Total GET requests = 1 initial connection + MaxReconnectionAttempts reconnections.
327+ Assert . Equal ( 1 + MaxReconnectionAttempts , getRequestCount ) ;
328+ }
249329}
0 commit comments