-
Notifications
You must be signed in to change notification settings - Fork 574
Add Conformance tests for the server #983
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
mikekistler
wants to merge
14
commits into
modelcontextprotocol:main
Choose a base branch
from
mikekistler:mdk/conformance-server
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+981
−2
Open
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
51a3ac6
Add ComplianceServer sample
mikekistler 03b678f
ComplianceServer now passing all tests (with one fix)
mikekistler 73cbe0f
Address PR review comments
mikekistler dc4757f
Move ConformanceServer to tests and fix naming
mikekistler 6689ec0
Add test project for conformance tests
mikekistler 7f75706
Fix conformance tests
mikekistler effa3ce
Skip the Conformance tests if Node is not installed
mikekistler ed0adcf
Restucture and add testing for .net8 and .net9
mikekistler 33059b5
A few more little fixes
mikekistler fbbc807
Fix Conformance tests in Release configuration
mikekistler f2ba02d
Attempt fix for server conformance tests in CI
mikekistler cd3d3fb
Another fix attempt for server conformance tests in CI
mikekistler 769fb69
Launch Conformance Server in-process for better debugging
mikekistler ef4198c
Move ConformanceTests into AspNetCore test project
mikekistler File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
209 changes: 209 additions & 0 deletions
209
tests/ModelContextProtocol.AspNetCore.Tests/ServerConformanceTests.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,209 @@ | ||
| using System.Diagnostics; | ||
| using System.Text; | ||
| 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. | ||
| /// </summary> | ||
| public class ServerConformanceTests : IAsyncLifetime | ||
| { | ||
| // Use different ports for each target framework to allow parallel execution | ||
| // 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 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 async ValueTask InitializeAsync() | ||
| { | ||
| // Start the ConformanceServer | ||
| StartConformanceServer(); | ||
|
|
||
| // 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) }; | ||
|
|
||
| while (stopwatch.Elapsed < timeout) | ||
| { | ||
| try | ||
| { | ||
| // Try to connect to the health endpoint | ||
| await httpClient.GetAsync($"{_serverUrl}/health"); | ||
| // Any response (even an error) means the server is up | ||
| 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() | ||
| { | ||
| // Stop the server | ||
| if (_serverCts != null) | ||
| { | ||
| _serverCts.Cancel(); | ||
| if (_serverTask != null) | ||
| { | ||
| try | ||
| { | ||
| await _serverTask.WaitAsync(TimeSpan.FromSeconds(5)); | ||
| } | ||
| catch | ||
| { | ||
| // Ignore exceptions during shutdown | ||
| } | ||
| } | ||
| _serverCts.Dispose(); | ||
| } | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task RunConformanceTests() | ||
| { | ||
| // Check if Node.js is installed | ||
| Assert.SkipWhen(!IsNodeInstalled(), "Node.js is not installed. Skipping conformance tests."); | ||
|
|
||
| // Run the conformance test suite | ||
| var result = await RunNpxConformanceTests(); | ||
|
|
||
| // Report the results | ||
| Assert.True(result.Success, | ||
| $"Conformance tests failed.\n\nStdout:\n{result.Output}\n\nStderr:\n{result.Error}"); | ||
| } | ||
|
|
||
| private void StartConformanceServer() | ||
| { | ||
| // The ConformanceServer binary is in a parallel directory to the test binary | ||
| // Test binary is in: artifacts/bin/ModelContextProtocol.ConformanceTests/Debug/{tfm}/ | ||
| // ConformanceServer binary is in: artifacts/bin/ModelContextProtocol.ConformanceServer/Debug/{tfm}/ | ||
| var testBinaryDir = AppContext.BaseDirectory; // e.g., .../net10.0/ | ||
| var configuration = Path.GetFileName(Path.GetDirectoryName(testBinaryDir.TrimEnd(Path.DirectorySeparatorChar))!); | ||
| var targetFramework = Path.GetFileName(testBinaryDir.TrimEnd(Path.DirectorySeparatorChar)); | ||
| var conformanceServerDir = Path.GetFullPath( | ||
| Path.Combine(testBinaryDir, "..", "..", "..", "ModelContextProtocol.ConformanceServer", configuration, targetFramework)); | ||
|
|
||
| if (!Directory.Exists(conformanceServerDir)) | ||
| { | ||
| throw new DirectoryNotFoundException( | ||
| $"ConformanceServer directory not found at: {conformanceServerDir}"); | ||
| } | ||
|
|
||
| // Start the server in a background task | ||
| _serverCts = new CancellationTokenSource(); | ||
| _serverTask = Task.Run(() => ConformanceServer.Program.MainAsync(["--urls", _serverUrl], new XunitLoggerProvider(_output), cancellationToken: _serverCts.Token)); | ||
| } | ||
|
|
||
| private async Task<(bool Success, string Output, string Error)> RunNpxConformanceTests() | ||
| { | ||
| var startInfo = new ProcessStartInfo | ||
| { | ||
| FileName = "npx", | ||
| Arguments = $"-y @modelcontextprotocol/conformance server --url {_serverUrl}", | ||
| RedirectStandardOutput = true, | ||
| RedirectStandardError = true, | ||
| UseShellExecute = false, | ||
| CreateNoWindow = true | ||
| }; | ||
|
|
||
| var outputBuilder = new StringBuilder(); | ||
| var errorBuilder = new StringBuilder(); | ||
|
|
||
| var process = new Process { StartInfo = startInfo }; | ||
|
|
||
| process.OutputDataReceived += (sender, e) => | ||
| { | ||
| if (e.Data != null) | ||
| { | ||
| _output.WriteLine(e.Data); | ||
| outputBuilder.AppendLine(e.Data); | ||
| } | ||
| }; | ||
|
|
||
| process.ErrorDataReceived += (sender, e) => | ||
| { | ||
| if (e.Data != null) | ||
| { | ||
| _output.WriteLine(e.Data); | ||
| errorBuilder.AppendLine(e.Data); | ||
| } | ||
| }; | ||
|
|
||
| process.Start(); | ||
| process.BeginOutputReadLine(); | ||
| process.BeginErrorReadLine(); | ||
|
|
||
| await process.WaitForExitAsync(); | ||
|
|
||
| return ( | ||
| Success: process.ExitCode == 0, | ||
| Output: outputBuilder.ToString(), | ||
| Error: errorBuilder.ToString() | ||
| ); | ||
| } | ||
|
|
||
| private static bool IsNodeInstalled() | ||
| { | ||
| try | ||
| { | ||
| var startInfo = new ProcessStartInfo | ||
| { | ||
| FileName = "npx", // Check specifically for npx because windows seems unable to find it | ||
| Arguments = "--version", | ||
| RedirectStandardOutput = true, | ||
| RedirectStandardError = true, | ||
| UseShellExecute = false, | ||
| CreateNoWindow = true | ||
| }; | ||
|
|
||
| using var process = Process.Start(startInfo); | ||
| if (process == null) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| process.WaitForExit(5000); | ||
| return process.ExitCode == 0; | ||
| } | ||
| catch | ||
| { | ||
| return false; | ||
| } | ||
| } | ||
| } | ||
20 changes: 20 additions & 0 deletions
20
tests/ModelContextProtocol.ConformanceServer/ModelContextProtocol.ConformanceServer.csproj
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk.Web"> | ||
mikekistler marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| <PropertyGroup> | ||
| <TargetFrameworks>net10.0;net9.0;net8.0</TargetFrameworks> | ||
| <Nullable>enable</Nullable> | ||
| <ImplicitUsings>enable</ImplicitUsings> | ||
| <OutputType>Exe</OutputType> | ||
| <AssemblyName>ConformanceServer</AssemblyName> | ||
| </PropertyGroup> | ||
|
|
||
| <PropertyGroup Condition="'$(TargetFramework)' == 'net9.0'"> | ||
| <!-- For better test coverage, only disable reflection in one of the targets --> | ||
| <JsonSerializerIsReflectionEnabledByDefault>false</JsonSerializerIsReflectionEnabledByDefault> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <ProjectReference Include="../../src/ModelContextProtocol.AspNetCore/ModelContextProtocol.AspNetCore.csproj" /> | ||
| </ItemGroup> | ||
|
|
||
| </Project> | ||
75 changes: 75 additions & 0 deletions
75
tests/ModelContextProtocol.ConformanceServer/ModelContextProtocol.ConformanceServer.http
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| @HostAddress = http://localhost:3001 | ||
|
|
||
| POST {{HostAddress}}/ | ||
| Accept: application/json, text/event-stream | ||
| Content-Type: application/json | ||
|
|
||
| { | ||
| "jsonrpc": "2.0", | ||
| "id": 1, | ||
| "method": "ping" | ||
| } | ||
|
|
||
| ### | ||
|
|
||
| POST {{HostAddress}}/ | ||
| Accept: application/json, text/event-stream | ||
| Content-Type: application/json | ||
|
|
||
| { | ||
| "jsonrpc": "2.0", | ||
| "id": 2, | ||
| "method": "initialize", | ||
| "params": { | ||
| "clientInfo": { | ||
| "name": "RestClient", | ||
| "version": "0.1.0" | ||
| }, | ||
| "capabilities": {}, | ||
| "protocolVersion": "2025-06-18" | ||
| } | ||
| } | ||
|
|
||
| ### | ||
|
|
||
| @SessionId = XxIXkrK210aKVnZxD8Iu_g | ||
|
|
||
| POST {{HostAddress}}/ | ||
| Accept: application/json, text/event-stream | ||
| Content-Type: application/json | ||
| MCP-Protocol-Version: 2025-06-18 | ||
| Mcp-Session-Id: {{SessionId}} | ||
|
|
||
| { | ||
| "jsonrpc": "2.0", | ||
| "id": 3, | ||
| "method": "tools/list" | ||
| } | ||
|
|
||
| ### | ||
|
|
||
| POST {{HostAddress}}/ | ||
| Accept: application/json, text/event-stream | ||
| Content-Type: application/json | ||
| MCP-Protocol-Version: 2025-06-18 | ||
| Mcp-Session-Id: {{SessionId}} | ||
|
|
||
| { | ||
| "jsonrpc": "2.0", | ||
| "id": 4, | ||
| "method": "resources/list" | ||
| } | ||
|
|
||
| ### | ||
|
|
||
| POST {{HostAddress}}/ | ||
| Accept: application/json, text/event-stream | ||
| Content-Type: application/json | ||
| MCP-Protocol-Version: 2025-06-18 | ||
| Mcp-Session-Id: {{SessionId}} | ||
|
|
||
| { | ||
| "jsonrpc": "2.0", | ||
| "id": 5, | ||
| "method": "prompts/list" | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.