Bring up to 0.1.13 conformance (#1254)
This commit is contained in:
parent
9c1538fd98
commit
27b5eb8031
3 changed files with 1249 additions and 78 deletions
|
|
@ -24,17 +24,32 @@ public class ClientConformanceTests
|
|||
[Theory(Skip = "Node.js is not installed. Skipping client conformance tests.", SkipUnless = nameof(IsNodeInstalled))]
|
||||
[InlineData("initialize")]
|
||||
[InlineData("tools_call")]
|
||||
[InlineData("elicitation-sep1034-client-defaults")]
|
||||
[InlineData("sse-retry")]
|
||||
[InlineData("auth/metadata-default")]
|
||||
[InlineData("auth/metadata-var1")]
|
||||
[InlineData("auth/metadata-var2")]
|
||||
[InlineData("auth/metadata-var3")]
|
||||
[InlineData("auth/basic-cimd")]
|
||||
// [InlineData("auth/2025-03-26-oauth-metadata-backcompat")]
|
||||
// [InlineData("auth/2025-03-26-oauth-endpoint-fallback")]
|
||||
[InlineData("auth/scope-from-www-authenticate")]
|
||||
[InlineData("auth/scope-from-scopes-supported")]
|
||||
[InlineData("auth/scope-omitted-when-undefined")]
|
||||
[InlineData("auth/scope-step-up")]
|
||||
[InlineData("auth/scope-retry-limit")]
|
||||
[InlineData("auth/token-endpoint-auth-basic")]
|
||||
[InlineData("auth/token-endpoint-auth-post")]
|
||||
[InlineData("auth/token-endpoint-auth-none")]
|
||||
[InlineData("auth/resource-mismatch")]
|
||||
[InlineData("auth/pre-registration")]
|
||||
|
||||
// Backcompat: Legacy 2025-03-26 OAuth flows (no PRM, root-location metadata) we don't implement.
|
||||
// [InlineData("auth/2025-03-26-oauth-metadata-backcompat")]
|
||||
// [InlineData("auth/2025-03-26-oauth-endpoint-fallback")]
|
||||
|
||||
// Extensions: Require ES256 JWT signing (private_key_jwt) and client_credentials grant support.
|
||||
// [InlineData("auth/client-credentials-jwt")]
|
||||
// [InlineData("auth/client-credentials-basic")]
|
||||
|
||||
public async Task RunConformanceTest(string scenario)
|
||||
{
|
||||
// Run the conformance test suite
|
||||
|
|
@ -88,7 +103,20 @@ public class ClientConformanceTests
|
|||
process.BeginOutputReadLine();
|
||||
process.BeginErrorReadLine();
|
||||
|
||||
await process.WaitForExitAsync();
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(5));
|
||||
try
|
||||
{
|
||||
await process.WaitForExitAsync(cts.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
process.Kill(entireProcessTree: true);
|
||||
return (
|
||||
Success: false,
|
||||
Output: outputBuilder.ToString(),
|
||||
Error: errorBuilder.ToString() + "\nProcess timed out after 5 minutes and was killed."
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
Success: process.ExitCode == 0,
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@ using ModelContextProtocol.Tests.Utils;
|
|||
namespace ModelContextProtocol.ConformanceTests;
|
||||
|
||||
/// <summary>
|
||||
/// Runs the official MCP conformance tests against the ConformanceServer.
|
||||
/// This test starts the ConformanceServer, runs the Node.js-based conformance test suite,
|
||||
/// and reports the results.
|
||||
/// Shared fixture that starts a single ConformanceServer instance for all tests in
|
||||
/// <see cref="ServerConformanceTests"/>. This avoids TCP port TIME_WAIT conflicts
|
||||
/// that occur when each test starts and stops its own server on the same port.
|
||||
/// </summary>
|
||||
public class ServerConformanceTests : IAsyncLifetime
|
||||
public class ConformanceServerFixture : IAsyncLifetime
|
||||
{
|
||||
// Use different ports for each target framework to allow parallel execution
|
||||
// net10.0 -> 3001, net9.0 -> 3002, net8.0 -> 3003
|
||||
|
|
@ -27,22 +27,16 @@ public class ServerConformanceTests : IAsyncLifetime
|
|||
};
|
||||
}
|
||||
|
||||
private readonly int _serverPort = GetPortForTargetFramework();
|
||||
private readonly string _serverUrl;
|
||||
private readonly ITestOutputHelper _output;
|
||||
private Task? _serverTask;
|
||||
private CancellationTokenSource? _serverCts;
|
||||
|
||||
public ServerConformanceTests(ITestOutputHelper output)
|
||||
{
|
||||
_output = output;
|
||||
_serverUrl = $"http://localhost:{_serverPort}";
|
||||
}
|
||||
public string ServerUrl { get; } = $"http://localhost:{GetPortForTargetFramework()}";
|
||||
|
||||
public async ValueTask InitializeAsync()
|
||||
{
|
||||
// Start the ConformanceServer
|
||||
StartConformanceServer();
|
||||
_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);
|
||||
|
|
@ -53,9 +47,7 @@ public class ServerConformanceTests : IAsyncLifetime
|
|||
{
|
||||
try
|
||||
{
|
||||
// Try to connect to the health endpoint
|
||||
await httpClient.GetAsync($"{_serverUrl}/health");
|
||||
// Any response (even an error) means the server is up
|
||||
await httpClient.GetAsync($"{ServerUrl}/health");
|
||||
return;
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
|
|
@ -75,7 +67,6 @@ public class ServerConformanceTests : IAsyncLifetime
|
|||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
// Stop the server
|
||||
if (_serverCts != null)
|
||||
{
|
||||
_serverCts.Cancel();
|
||||
|
|
@ -93,30 +84,52 @@ public class ServerConformanceTests : IAsyncLifetime
|
|||
_serverCts.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the official MCP conformance tests against the ConformanceServer.
|
||||
/// Uses a shared <see cref="ConformanceServerFixture"/> so the server is started once
|
||||
/// and reused across all tests, avoiding TCP port conflicts on Windows.
|
||||
/// </summary>
|
||||
public class ServerConformanceTests(ConformanceServerFixture fixture, ITestOutputHelper output)
|
||||
: IClassFixture<ConformanceServerFixture>
|
||||
{
|
||||
[Fact]
|
||||
public async Task RunConformanceTests()
|
||||
{
|
||||
Assert.SkipWhen(!NodeHelpers.IsNodeInstalled(), "Node.js is not installed. Skipping conformance tests.");
|
||||
|
||||
// Run the conformance test suite
|
||||
var result = await RunConformanceTestsAsync();
|
||||
var result = await RunConformanceTestsAsync($"server --url {fixture.ServerUrl}");
|
||||
|
||||
// Report the results
|
||||
Assert.True(result.Success,
|
||||
$"Conformance tests failed.\n\nStdout:\n{result.Output}\n\nStderr:\n{result.Error}");
|
||||
}
|
||||
|
||||
private void StartConformanceServer()
|
||||
[Fact]
|
||||
public async Task RunPendingConformanceTest_JsonSchema202012()
|
||||
{
|
||||
// Start the server in a background task
|
||||
_serverCts = new CancellationTokenSource();
|
||||
_serverTask = Task.Run(() => ConformanceServer.Program.MainAsync(["--urls", _serverUrl], new XunitLoggerProvider(_output), cancellationToken: _serverCts.Token));
|
||||
Assert.SkipWhen(!NodeHelpers.IsNodeInstalled(), "Node.js is not installed. Skipping conformance tests.");
|
||||
|
||||
var result = await RunConformanceTestsAsync($"server --url {fixture.ServerUrl} --scenario json-schema-2020-12");
|
||||
|
||||
Assert.True(result.Success,
|
||||
$"Conformance test failed.\n\nStdout:\n{result.Output}\n\nStderr:\n{result.Error}");
|
||||
}
|
||||
|
||||
private async Task<(bool Success, string Output, string Error)> RunConformanceTestsAsync()
|
||||
[Fact]
|
||||
public async Task RunPendingConformanceTest_ServerSsePolling()
|
||||
{
|
||||
var startInfo = NodeHelpers.ConformanceTestStartInfo($"server --url {_serverUrl}");
|
||||
Assert.SkipWhen(!NodeHelpers.IsNodeInstalled(), "Node.js is not installed. Skipping conformance tests.");
|
||||
|
||||
var result = await RunConformanceTestsAsync($"server --url {fixture.ServerUrl} --scenario server-sse-polling");
|
||||
|
||||
Assert.True(result.Success,
|
||||
$"Conformance test failed.\n\nStdout:\n{result.Output}\n\nStderr:\n{result.Error}");
|
||||
}
|
||||
|
||||
private async Task<(bool Success, string Output, string Error)> RunConformanceTestsAsync(string arguments)
|
||||
{
|
||||
var startInfo = NodeHelpers.ConformanceTestStartInfo(arguments);
|
||||
|
||||
var outputBuilder = new StringBuilder();
|
||||
var errorBuilder = new StringBuilder();
|
||||
|
|
@ -127,7 +140,7 @@ public class ServerConformanceTests : IAsyncLifetime
|
|||
{
|
||||
if (e.Data != null)
|
||||
{
|
||||
_output.WriteLine(e.Data);
|
||||
output.WriteLine(e.Data);
|
||||
outputBuilder.AppendLine(e.Data);
|
||||
}
|
||||
};
|
||||
|
|
@ -136,7 +149,7 @@ public class ServerConformanceTests : IAsyncLifetime
|
|||
{
|
||||
if (e.Data != null)
|
||||
{
|
||||
_output.WriteLine(e.Data);
|
||||
output.WriteLine(e.Data);
|
||||
errorBuilder.AppendLine(e.Data);
|
||||
}
|
||||
};
|
||||
|
|
@ -145,7 +158,20 @@ public class ServerConformanceTests : IAsyncLifetime
|
|||
process.BeginOutputReadLine();
|
||||
process.BeginErrorReadLine();
|
||||
|
||||
await process.WaitForExitAsync();
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(5));
|
||||
try
|
||||
{
|
||||
await process.WaitForExitAsync(cts.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
process.Kill(entireProcessTree: true);
|
||||
return (
|
||||
Success: false,
|
||||
Output: outputBuilder.ToString(),
|
||||
Error: errorBuilder.ToString() + "\nProcess timed out after 5 minutes and was killed."
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
Success: process.ExitCode == 0,
|
||||
|
|
|
|||
|
|
@ -470,11 +470,23 @@ public class StreamableHttpServerConformanceTests(ITestOutputHelper outputHelper
|
|||
await CallInitializeAndValidateAsync();
|
||||
await CallEchoAndValidateAsync();
|
||||
|
||||
// Add 5 seconds to idle timeout to account for the interval of the PeriodicTimer.
|
||||
fakeTimeProvider.Advance(TimeSpan.FromHours(2) + TimeSpan.FromSeconds(5));
|
||||
// The background IdleTrackingBackgroundService prunes sessions asynchronously after
|
||||
// the PeriodicTimer (5s interval) tick fires. We advance past the 2-hour idle timeout
|
||||
// then poll until the session returns NotFound. Each HTTP POST also refreshes the
|
||||
// session's LastActivityTicks via AcquireReferenceAsync, so we must re-advance time
|
||||
// each iteration to ensure the session appears idle again for the next prune pass.
|
||||
var deadline = DateTime.UtcNow + TestConstants.DefaultTimeout;
|
||||
HttpStatusCode statusCode;
|
||||
do
|
||||
{
|
||||
fakeTimeProvider.Advance(TimeSpan.FromHours(2) + TimeSpan.FromSeconds(5));
|
||||
await Task.Delay(100, TestContext.Current.CancellationToken);
|
||||
using var response = await HttpClient.PostAsync("", JsonContent(EchoRequest), TestContext.Current.CancellationToken);
|
||||
statusCode = response.StatusCode;
|
||||
}
|
||||
while (statusCode != HttpStatusCode.NotFound && DateTime.UtcNow < deadline);
|
||||
|
||||
using var response = await HttpClient.PostAsync("", JsonContent(EchoRequest), TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
Assert.Equal(HttpStatusCode.NotFound, statusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
Loading…
Reference in a new issue