From 3563d1efc0fdc3fd91721247d9e6d1fbbe9165ab Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 30 Jan 2026 18:18:23 +0000 Subject: [PATCH 1/4] Initial plan From 0c2494568882c47d82dcf6f2aed06ac64ebff375 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 30 Jan 2026 18:24:23 +0000 Subject: [PATCH 2/4] Centralize HttpClient and process timeout constants in tests - Add HttpClientTimeout and HttpClientHealthCheckTimeout constants to TestConstants - Update all HttpClient.Timeout usages in tests to use centralized constants - Update all WaitForExitAsync calls to use TestConstants.DefaultTimeout - Update HttpClientTransportOptions.ConnectionTimeout default from 30s to 60s to match test timeout - Ensure consistent timeout behavior across tests to prevent CI timeouts Co-authored-by: ericstj <8918108+ericstj@users.noreply.github.com> --- .../Client/HttpClientTransportOptions.cs | 4 ++-- tests/Common/Utils/TestConstants.cs | 12 ++++++++++++ .../ServerConformanceTests.cs | 2 +- .../Utils/KestrelInMemoryTest.cs | 2 +- .../EverythingSseServerFixture.cs | 5 +++-- .../StdioServerIntegrationTests.cs | 2 +- 6 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/ModelContextProtocol.Core/Client/HttpClientTransportOptions.cs b/src/ModelContextProtocol.Core/Client/HttpClientTransportOptions.cs index 73eaae700..413e552d7 100644 --- a/src/ModelContextProtocol.Core/Client/HttpClientTransportOptions.cs +++ b/src/ModelContextProtocol.Core/Client/HttpClientTransportOptions.cs @@ -57,7 +57,7 @@ public required Uri Endpoint /// Gets or sets a timeout used to establish the initial connection to the SSE server. /// /// - /// The timeout used to establish the initial connection to the SSE server. The default is 30 seconds. + /// The timeout used to establish the initial connection to the SSE server. The default is 60 seconds. /// /// /// This timeout controls how long the client waits for: @@ -67,7 +67,7 @@ public required Uri Endpoint /// /// If the timeout expires before the connection is established, a is thrown. /// - public TimeSpan ConnectionTimeout { get; set; } = TimeSpan.FromSeconds(30); + public TimeSpan ConnectionTimeout { get; set; } = TimeSpan.FromSeconds(60); /// /// Gets or sets custom HTTP headers to include in requests to the SSE server. diff --git a/tests/Common/Utils/TestConstants.cs b/tests/Common/Utils/TestConstants.cs index eae9a54b1..4c3444ae7 100644 --- a/tests/Common/Utils/TestConstants.cs +++ b/tests/Common/Utils/TestConstants.cs @@ -10,4 +10,16 @@ public static class TestConstants /// Set to 60 seconds to provide sufficient buffer for slow CI environments. /// public static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(60); + + /// + /// Timeout for HttpClient operations in tests. + /// Set to 60 seconds to provide sufficient buffer for slow CI environments. + /// + public static readonly TimeSpan HttpClientTimeout = TimeSpan.FromSeconds(60); + + /// + /// Timeout for short-lived HTTP requests during server health checks. + /// Set to 2 seconds for quick failure detection while polling. + /// + public static readonly TimeSpan HttpClientHealthCheckTimeout = TimeSpan.FromSeconds(2); } diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/ServerConformanceTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/ServerConformanceTests.cs index 031a67a18..b1d37420a 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/ServerConformanceTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/ServerConformanceTests.cs @@ -47,7 +47,7 @@ public async ValueTask InitializeAsync() // Wait for server to be ready (retry for up to 30 seconds) var timeout = TimeSpan.FromSeconds(30); var stopwatch = Stopwatch.StartNew(); - using var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(2) }; + using var httpClient = new HttpClient { Timeout = TestConstants.HttpClientHealthCheckTimeout }; while (stopwatch.Elapsed < timeout) { diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/Utils/KestrelInMemoryTest.cs b/tests/ModelContextProtocol.AspNetCore.Tests/Utils/KestrelInMemoryTest.cs index c93a27650..0bbff49b4 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/Utils/KestrelInMemoryTest.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/Utils/KestrelInMemoryTest.cs @@ -42,7 +42,7 @@ public KestrelInMemoryTest(ITestOutputHelper testOutputHelper) protected static void ConfigureHttpClient(HttpClient httpClient) { httpClient.BaseAddress = new Uri("http://localhost:5000/"); - httpClient.Timeout = TimeSpan.FromSeconds(10); + httpClient.Timeout = TestConstants.HttpClientTimeout; } public override void Dispose() diff --git a/tests/ModelContextProtocol.Tests/EverythingSseServerFixture.cs b/tests/ModelContextProtocol.Tests/EverythingSseServerFixture.cs index f12aff5e5..71f82bf10 100644 --- a/tests/ModelContextProtocol.Tests/EverythingSseServerFixture.cs +++ b/tests/ModelContextProtocol.Tests/EverythingSseServerFixture.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using System.Net; +using ModelContextProtocol.Tests.Utils; namespace ModelContextProtocol.Tests; @@ -33,7 +34,7 @@ public async Task StartAsync() ?? throw new InvalidOperationException($"Could not start process for {processStartInfo.FileName} with '{processStartInfo.Arguments}'."); // Poll until the server is ready (up to 30 seconds) - using var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(2) }; + using var httpClient = new HttpClient { Timeout = TestConstants.HttpClientHealthCheckTimeout }; var endpoint = $"http://localhost:{_port}/sse"; var deadline = DateTime.UtcNow.AddSeconds(30); @@ -72,7 +73,7 @@ public async ValueTask DisposeAsync() using var stopProcess = Process.Start(stopInfo) ?? throw new InvalidOperationException($"Could not stop process for {stopInfo.FileName} with '{stopInfo.Arguments}'."); - await stopProcess.WaitForExitAsync(TimeSpan.FromSeconds(10)); + await stopProcess.WaitForExitAsync(TestConstants.DefaultTimeout); } catch (Exception ex) { diff --git a/tests/ModelContextProtocol.Tests/StdioServerIntegrationTests.cs b/tests/ModelContextProtocol.Tests/StdioServerIntegrationTests.cs index d14c376c1..88604f533 100644 --- a/tests/ModelContextProtocol.Tests/StdioServerIntegrationTests.cs +++ b/tests/ModelContextProtocol.Tests/StdioServerIntegrationTests.cs @@ -46,7 +46,7 @@ public async Task SigInt_DisposesTestServerWithHosting_Gracefully() // https://github.com/dotnet/runtime/issues/109432, https://github.com/dotnet/runtime/issues/44944 Assert.Equal(0, kill(process.Id, SIGINT)); - await process.WaitForExitAsync(TimeSpan.FromSeconds(10)); + await process.WaitForExitAsync(TestConstants.DefaultTimeout); Assert.True(process.HasExited); Assert.Equal(0, process.ExitCode); From 6d5b204fe180fdbd93731721194d32ad8ff0d3bc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 30 Jan 2026 18:38:52 +0000 Subject: [PATCH 3/4] Address PR feedback: revert product change and rename constant - Revert HttpClientTransportOptions.ConnectionTimeout from 60s back to 30s (no product changes) - Rename HttpClientHealthCheckTimeout to HttpClientPollingTimeout per feedback - Update all usages of the renamed constant - HttpClientTransportTests.cs does not need changes (uses test-specific ConnectionTimeout for mock handlers) Co-authored-by: ericstj <8918108+ericstj@users.noreply.github.com> --- .../Client/HttpClientTransportOptions.cs | 4 ++-- tests/Common/Utils/TestConstants.cs | 4 ++-- .../ServerConformanceTests.cs | 2 +- .../ModelContextProtocol.Tests/EverythingSseServerFixture.cs | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ModelContextProtocol.Core/Client/HttpClientTransportOptions.cs b/src/ModelContextProtocol.Core/Client/HttpClientTransportOptions.cs index 413e552d7..73eaae700 100644 --- a/src/ModelContextProtocol.Core/Client/HttpClientTransportOptions.cs +++ b/src/ModelContextProtocol.Core/Client/HttpClientTransportOptions.cs @@ -57,7 +57,7 @@ public required Uri Endpoint /// Gets or sets a timeout used to establish the initial connection to the SSE server. /// /// - /// The timeout used to establish the initial connection to the SSE server. The default is 60 seconds. + /// The timeout used to establish the initial connection to the SSE server. The default is 30 seconds. /// /// /// This timeout controls how long the client waits for: @@ -67,7 +67,7 @@ public required Uri Endpoint /// /// If the timeout expires before the connection is established, a is thrown. /// - public TimeSpan ConnectionTimeout { get; set; } = TimeSpan.FromSeconds(60); + public TimeSpan ConnectionTimeout { get; set; } = TimeSpan.FromSeconds(30); /// /// Gets or sets custom HTTP headers to include in requests to the SSE server. diff --git a/tests/Common/Utils/TestConstants.cs b/tests/Common/Utils/TestConstants.cs index 4c3444ae7..4a337df36 100644 --- a/tests/Common/Utils/TestConstants.cs +++ b/tests/Common/Utils/TestConstants.cs @@ -18,8 +18,8 @@ public static class TestConstants public static readonly TimeSpan HttpClientTimeout = TimeSpan.FromSeconds(60); /// - /// Timeout for short-lived HTTP requests during server health checks. + /// Timeout for short-lived HTTP requests during polling operations. /// Set to 2 seconds for quick failure detection while polling. /// - public static readonly TimeSpan HttpClientHealthCheckTimeout = TimeSpan.FromSeconds(2); + public static readonly TimeSpan HttpClientPollingTimeout = TimeSpan.FromSeconds(2); } diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/ServerConformanceTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/ServerConformanceTests.cs index b1d37420a..02a98b890 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/ServerConformanceTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/ServerConformanceTests.cs @@ -47,7 +47,7 @@ public async ValueTask InitializeAsync() // Wait for server to be ready (retry for up to 30 seconds) var timeout = TimeSpan.FromSeconds(30); var stopwatch = Stopwatch.StartNew(); - using var httpClient = new HttpClient { Timeout = TestConstants.HttpClientHealthCheckTimeout }; + using var httpClient = new HttpClient { Timeout = TestConstants.HttpClientPollingTimeout }; while (stopwatch.Elapsed < timeout) { diff --git a/tests/ModelContextProtocol.Tests/EverythingSseServerFixture.cs b/tests/ModelContextProtocol.Tests/EverythingSseServerFixture.cs index 71f82bf10..e579ff9f0 100644 --- a/tests/ModelContextProtocol.Tests/EverythingSseServerFixture.cs +++ b/tests/ModelContextProtocol.Tests/EverythingSseServerFixture.cs @@ -34,7 +34,7 @@ public async Task StartAsync() ?? throw new InvalidOperationException($"Could not start process for {processStartInfo.FileName} with '{processStartInfo.Arguments}'."); // Poll until the server is ready (up to 30 seconds) - using var httpClient = new HttpClient { Timeout = TestConstants.HttpClientHealthCheckTimeout }; + using var httpClient = new HttpClient { Timeout = TestConstants.HttpClientPollingTimeout }; var endpoint = $"http://localhost:{_port}/sse"; var deadline = DateTime.UtcNow.AddSeconds(30); From b72d0f7d3e29dc8bd042a376b4e4728abaa4f262 Mon Sep 17 00:00:00 2001 From: Eric StJohn Date: Fri, 30 Jan 2026 12:07:45 -0800 Subject: [PATCH 4/4] Add wait for TestOAuthServer to start --- .../OAuth/OAuthTestBase.cs | 5 +++++ .../Program.cs | 21 ++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/OAuthTestBase.cs b/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/OAuthTestBase.cs index 52b5616d0..5d66ee80e 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/OAuthTestBase.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/OAuth/OAuthTestBase.cs @@ -83,6 +83,11 @@ public async ValueTask DisposeAsync() protected async Task StartMcpServerAsync(string path = "", string? authScheme = null) { + // Wait for the OAuth server to be ready before starting the MCP server. + // This prevents race conditions in CI where the OAuth server may not be + // fully initialized when the first test request is made. + await TestOAuthServer.ServerStarted.WaitAsync(TestContext.Current.CancellationToken); + Builder.Services.Configure(JwtBearerDefaults.AuthenticationScheme, options => { options.TokenValidationParameters.ValidAudience = $"{McpServerUrl}{path}"; diff --git a/tests/ModelContextProtocol.TestOAuthServer/Program.cs b/tests/ModelContextProtocol.TestOAuthServer/Program.cs index e13c731de..364836311 100644 --- a/tests/ModelContextProtocol.TestOAuthServer/Program.cs +++ b/tests/ModelContextProtocol.TestOAuthServer/Program.cs @@ -33,6 +33,7 @@ public sealed class Program private readonly ILoggerProvider? _loggerProvider; private readonly IConnectionListenerFactory? _kestrelTransport; + private readonly TaskCompletionSource _serverStarted = new(TaskCreationOptions.RunContinuationsAsynchronously); /// /// Initializes a new instance of the class with logging and transport parameters. @@ -47,6 +48,11 @@ public Program(ILoggerProvider? loggerProvider = null, IConnectionListenerFactor _kestrelTransport = kestrelTransport; } + /// + /// Gets a task that completes when the server has started and is ready to accept connections. + /// + public Task ServerStarted => _serverStarted.Task; + // Track if we've already issued an already-expired token for the CanAuthenticate_WithTokenRefresh test which uses the test-refresh-client registration. public bool HasRefreshedToken { get; set; } @@ -541,7 +547,20 @@ IResult HandleMetadataRequest(HttpContext context, string? issuerPath = null) Console.WriteLine($"Demo Client ID: {clientId}"); Console.WriteLine($"Demo Client Secret: {clientSecret}"); - await app.RunAsync(cancellationToken); + await app.StartAsync(cancellationToken); + _serverStarted.TrySetResult(); + + // Wait until cancellation is requested + try + { + await Task.Delay(Timeout.Infinite, cancellationToken); + } + catch (OperationCanceledException) + { + // Expected when cancellation is requested + } + + await app.StopAsync(); } ///