using System.Diagnostics; using ModelContextProtocol.Tests.Utils; namespace ModelContextProtocol.ConformanceTests; /// /// Shared fixture that starts a single ConformanceServer exposing both the legacy stateful MCP /// lifecycle (at , "/") and the SEP-2575 stateless lifecycle (at /// , "/stateless") on one port. A single long-lived server avoids /// the TCP TIME_WAIT conflicts that per-test restarts on a fixed port cause on Windows, and /// centralizes the port-binding logic that the stateful and stateless conformance tests previously /// duplicated. Shared by and /// via . /// public sealed class ConformanceServerFixture : IAsyncLifetime { // Use different ports for each target framework to allow parallel execution across the // multi-targeted test processes. net10.0 -> 3001, net9.0 -> 3002, net8.0 -> 3003. private static int GetPortForTargetFramework() { var testBinaryDir = AppContext.BaseDirectory; var targetFramework = Path.GetFileName(testBinaryDir.TrimEnd(Path.DirectorySeparatorChar)); return targetFramework switch { "net10.0" => 3001, "net9.0" => 3002, "net8.0" => 3003, _ => 3001 // Default fallback }; } private Task? _serverTask; private CancellationTokenSource? _serverCts; /// Base URL of the stateful MCP endpoint (mapped at "/"). public string ServerUrl { get; } = $"http://localhost:{GetPortForTargetFramework()}"; /// /// URL of the stateless MCP endpoint (mapped at "/stateless"), used by the 2026-07-28 /// scenarios (caching, MRTR, SEP-2243) that negotiate the stateless lifecycle. /// public string StatelessServerUrl => $"{ServerUrl}/stateless"; public async ValueTask InitializeAsync() { _serverCts = new CancellationTokenSource(); _serverTask = Task.Run(() => ConformanceServer.Program.MainAsync( ["--urls", ServerUrl], cancellationToken: _serverCts.Token)); // 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.HttpClientPollingTimeout }; while (stopwatch.Elapsed < timeout) { try { await httpClient.GetAsync($"{ServerUrl}/health"); return; } catch (HttpRequestException) { // Connection refused means server not ready yet } catch (TaskCanceledException) { // Timeout means server might be processing, give it more time } await Task.Delay(500); } throw new InvalidOperationException("ConformanceServer failed to start within the timeout period"); } public async ValueTask DisposeAsync() { if (_serverCts != null) { _serverCts.Cancel(); if (_serverTask != null) { try { await _serverTask.WaitAsync(TestConstants.DefaultTimeout); } catch { // Ignore exceptions during shutdown } } _serverCts.Dispose(); } } } /// /// xUnit collection that shares one across the conformance /// test classes so they run against a single server instance (and a single bound port). /// [CollectionDefinition(nameof(ConformanceServerCollection))] public sealed class ConformanceServerCollection : ICollectionFixture;