From 470726732c230f0e37a518902267e1110598cb6e Mon Sep 17 00:00:00 2001 From: Mike Kistler Date: Sat, 22 Nov 2025 21:03:22 -0800 Subject: [PATCH 01/14] New dotnet console app for client compliance tests --- .../ModelContextProtocol.ConformanceClient.csproj | 10 ++++++++++ .../ModelContextProtocol.ConformanceClient/Program.cs | 2 ++ 2 files changed, 12 insertions(+) create mode 100644 tests/ModelContextProtocol.ConformanceClient/ModelContextProtocol.ConformanceClient.csproj create mode 100644 tests/ModelContextProtocol.ConformanceClient/Program.cs diff --git a/tests/ModelContextProtocol.ConformanceClient/ModelContextProtocol.ConformanceClient.csproj b/tests/ModelContextProtocol.ConformanceClient/ModelContextProtocol.ConformanceClient.csproj new file mode 100644 index 000000000..dfb40caaf --- /dev/null +++ b/tests/ModelContextProtocol.ConformanceClient/ModelContextProtocol.ConformanceClient.csproj @@ -0,0 +1,10 @@ + + + + Exe + net10.0 + enable + enable + + + diff --git a/tests/ModelContextProtocol.ConformanceClient/Program.cs b/tests/ModelContextProtocol.ConformanceClient/Program.cs new file mode 100644 index 000000000..39946ea18 --- /dev/null +++ b/tests/ModelContextProtocol.ConformanceClient/Program.cs @@ -0,0 +1,2 @@ +// See https://aka.ms/new-console-template for more information +Console.WriteLine("Hello, World!"); From 842fcf036873d19a2454fee48ce2cf178185aa70 Mon Sep 17 00:00:00 2001 From: Mike Kistler Date: Sat, 22 Nov 2025 21:45:49 -0800 Subject: [PATCH 02/14] First client tests passing --- ModelContextProtocol.slnx | 1 + ...elContextProtocol.ConformanceClient.csproj | 7 ++- .../Program.cs | 45 ++++++++++++++++++- 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/ModelContextProtocol.slnx b/ModelContextProtocol.slnx index 72ecc778d..74dd56ab2 100644 --- a/ModelContextProtocol.slnx +++ b/ModelContextProtocol.slnx @@ -70,6 +70,7 @@ + diff --git a/tests/ModelContextProtocol.ConformanceClient/ModelContextProtocol.ConformanceClient.csproj b/tests/ModelContextProtocol.ConformanceClient/ModelContextProtocol.ConformanceClient.csproj index dfb40caaf..20a5ef04f 100644 --- a/tests/ModelContextProtocol.ConformanceClient/ModelContextProtocol.ConformanceClient.csproj +++ b/tests/ModelContextProtocol.ConformanceClient/ModelContextProtocol.ConformanceClient.csproj @@ -1,10 +1,15 @@ - Exe net10.0 + Exe + enable enable + + + + diff --git a/tests/ModelContextProtocol.ConformanceClient/Program.cs b/tests/ModelContextProtocol.ConformanceClient/Program.cs index 39946ea18..25a1a8b3c 100644 --- a/tests/ModelContextProtocol.ConformanceClient/Program.cs +++ b/tests/ModelContextProtocol.ConformanceClient/Program.cs @@ -1,2 +1,43 @@ -// See https://aka.ms/new-console-template for more information -Console.WriteLine("Hello, World!"); +using System.Text.Json; +using ModelContextProtocol.Client; +using ModelContextProtocol.Protocol; + +// The endpoint will be passed as the first argument +var endpoint = args.Length > 0 ? args[0] : "http://localhost:3001"; + +var clientTransport = new HttpClientTransport(new() +{ + Endpoint = new Uri(endpoint), + TransportMode = HttpTransportMode.StreamableHttp, +}); + +McpClientOptions options = new() +{ + ClientInfo = new() + { + Name = "ElicitationClient", + Version = "1.0.0" + } +}; + +await using var mcpClient = await McpClient.CreateAsync(clientTransport, options); + +bool success = true; + +var tools = await mcpClient.ListToolsAsync(); +foreach (var tool in tools) +{ + Console.WriteLine($"Connected to server with tools: {tool.Name}"); +} + +if (tools.Count > 0) +{ + Console.WriteLine($"Calling tool: {tools.First().Name}"); + + var result = await mcpClient.CallToolAsync(toolName: tools.First().Name); + + success &= result.IsError != true; +} + +// Exit code 0 on success, 1 on failure +return success ? 0 : 1; \ No newline at end of file From 564840d6962863400c33ee4db348805a416e0f32 Mon Sep 17 00:00:00 2001 From: Mike Kistler Date: Sun, 23 Nov 2025 06:10:49 -0800 Subject: [PATCH 03/14] Client Conformance test tools_call working --- .../Program.cs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/tests/ModelContextProtocol.ConformanceClient/Program.cs b/tests/ModelContextProtocol.ConformanceClient/Program.cs index 25a1a8b3c..93adf6cd4 100644 --- a/tests/ModelContextProtocol.ConformanceClient/Program.cs +++ b/tests/ModelContextProtocol.ConformanceClient/Program.cs @@ -25,19 +25,18 @@ bool success = true; var tools = await mcpClient.ListToolsAsync(); -foreach (var tool in tools) -{ - Console.WriteLine($"Connected to server with tools: {tool.Name}"); -} +Console.WriteLine($"Available tools: {string.Join(", ", tools.Select(t => t.Name))}"); -if (tools.Count > 0) +// Call the "add_numbers" tool +var toolName = "add_numbers"; +Console.WriteLine($"Calling tool: {toolName}"); +var result = await mcpClient.CallToolAsync(toolName: toolName, arguments: new Dictionary { - Console.WriteLine($"Calling tool: {tools.First().Name}"); - - var result = await mcpClient.CallToolAsync(toolName: tools.First().Name); + { "a", 5 }, + { "b", 10 } +}); - success &= result.IsError != true; -} +success &= result.IsError != true; // Exit code 0 on success, 1 on failure return success ? 0 : 1; \ No newline at end of file From 7eb8943d1f06ac67dcc7e7f8bd0500ecae690d4c Mon Sep 17 00:00:00 2001 From: Mike Kistler Date: Sat, 29 Nov 2025 06:16:37 -0800 Subject: [PATCH 04/14] Add ClientConformanceTests test runner --- .../ClientConformanceTests.cs | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 tests/ModelContextProtocol.AspNetCore.Tests/ClientConformanceTests.cs diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/ClientConformanceTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/ClientConformanceTests.cs new file mode 100644 index 000000000..57b433ae9 --- /dev/null +++ b/tests/ModelContextProtocol.AspNetCore.Tests/ClientConformanceTests.cs @@ -0,0 +1,113 @@ +using System.Diagnostics; +using System.Text; +using ModelContextProtocol.Tests.Utils; + +namespace ModelContextProtocol.ConformanceTests; + +/// +/// Runs the official MCP conformance tests against the ConformanceClient. +/// This test runs the Node.js-based conformance test suite for the client +/// and reports the results. +/// +public class ClientConformanceTests //: IAsyncLifetime +{ + private readonly ITestOutputHelper _output; + + public ClientConformanceTests(ITestOutputHelper output) + { + _output = output; + } + + [Theory] + [InlineData("initialize")] + // [InlineData("tools_call")] + public async Task RunConformanceTest(string scenario) + { + // 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 RunClientConformanceScenario(scenario); + + // Report the results + 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)> RunClientConformanceScenario(string scenario) + { + var startInfo = new ProcessStartInfo + { + FileName = "npx", + Arguments = $"-y @modelcontextprotocol/conformance client --scenario {scenario} --command \"dotnet run --no-build --project ../ModelContextProtocol.ConformanceClient/ModelContextProtocol.ConformanceClient.csproj\"", + 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; + } + } +} From 3f9b8fab0a26556fb687475dabc5660a24f2427d Mon Sep 17 00:00:00 2001 From: Mike Kistler Date: Tue, 2 Dec 2025 15:44:38 -0800 Subject: [PATCH 05/14] Restructure client conformance tests. --- .../ClientConformanceTests.cs | 17 +- ...elContextProtocol.ConformanceClient.csproj | 10 +- .../Program.cs | 204 +++++++++++++++--- ...elContextProtocol.ConformanceServer.csproj | 1 - 4 files changed, 198 insertions(+), 34 deletions(-) diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/ClientConformanceTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/ClientConformanceTests.cs index 57b433ae9..fe1c3649d 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/ClientConformanceTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/ClientConformanceTests.cs @@ -20,7 +20,8 @@ public ClientConformanceTests(ITestOutputHelper output) [Theory] [InlineData("initialize")] - // [InlineData("tools_call")] + [InlineData("tools_call")] + [InlineData("auth/metadata-default")] public async Task RunConformanceTest(string scenario) { // Check if Node.js is installed @@ -36,10 +37,22 @@ public async Task RunConformanceTest(string scenario) private async Task<(bool Success, string Output, string Error)> RunClientConformanceScenario(string scenario) { + // Construct an absolute path to the conformance client executable + var exeSuffix = OperatingSystem.IsWindows() ? ".exe" : ""; + var conformanceClientPath = Path.GetFullPath($"./ModelContextProtocol.ConformanceClient{exeSuffix}"); + // Replace AspNetCore.Tests with ConformanceClient in the path + conformanceClientPath = conformanceClientPath.Replace("AspNetCore.Tests", "ConformanceClient"); + + if (!File.Exists(conformanceClientPath)) + { + throw new FileNotFoundException( + $"ConformanceClient executable not found at: {conformanceClientPath}"); + } + var startInfo = new ProcessStartInfo { FileName = "npx", - Arguments = $"-y @modelcontextprotocol/conformance client --scenario {scenario} --command \"dotnet run --no-build --project ../ModelContextProtocol.ConformanceClient/ModelContextProtocol.ConformanceClient.csproj\"", + Arguments = $"-y @modelcontextprotocol/conformance client --scenario {scenario} --command \"{conformanceClientPath} {scenario}\"", RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, diff --git a/tests/ModelContextProtocol.ConformanceClient/ModelContextProtocol.ConformanceClient.csproj b/tests/ModelContextProtocol.ConformanceClient/ModelContextProtocol.ConformanceClient.csproj index 20a5ef04f..b8f2af686 100644 --- a/tests/ModelContextProtocol.ConformanceClient/ModelContextProtocol.ConformanceClient.csproj +++ b/tests/ModelContextProtocol.ConformanceClient/ModelContextProtocol.ConformanceClient.csproj @@ -1,11 +1,15 @@ - net10.0 - Exe - + net10.0;net9.0;net8.0 enable enable + Exe + + + + + false diff --git a/tests/ModelContextProtocol.ConformanceClient/Program.cs b/tests/ModelContextProtocol.ConformanceClient/Program.cs index 93adf6cd4..4041b1c0b 100644 --- a/tests/ModelContextProtocol.ConformanceClient/Program.cs +++ b/tests/ModelContextProtocol.ConformanceClient/Program.cs @@ -1,42 +1,190 @@ -using System.Text.Json; +using System.Diagnostics; +using System.Net; +using System.Text; +using System.Web; + using ModelContextProtocol.Client; -using ModelContextProtocol.Protocol; -// The endpoint will be passed as the first argument -var endpoint = args.Length > 0 ? args[0] : "http://localhost:3001"; +// This program expects the following command-line arguments: +// 1. The client conformance test scenario to run (e.g., "tools_call") +// 2. The endpoint URL (e.g., "http://localhost:3001") -var clientTransport = new HttpClientTransport(new() +if (args.Length < 2) { - Endpoint = new Uri(endpoint), - TransportMode = HttpTransportMode.StreamableHttp, -}); + Console.WriteLine("Usage: dotnet run --project ModelContextProtocol.ConformanceClient.csproj [endpoint]"); + return 1; +} + +var scenario = args[0]; +var endpoint = args[1]; -McpClientOptions options = new() +switch (scenario) { - ClientInfo = new() + case "initialize": + return await ToolsCall(); + case "tools_call": + return await ToolsCall(); + case "auth/metadata-default": + return await AuthMetadata(); + default: + Console.WriteLine($"Unknown scenario: {scenario}"); + return 1; +} + +// Implements the "tools_call" conformance test scenario +// We can also use this for the "initialize" scenario +async Task ToolsCall() +{ + var clientTransport = new HttpClientTransport(new() { - Name = "ElicitationClient", - Version = "1.0.0" - } -}; + Endpoint = new Uri(endpoint), + TransportMode = HttpTransportMode.StreamableHttp + }); -await using var mcpClient = await McpClient.CreateAsync(clientTransport, options); + McpClientOptions options = new() + { + ClientInfo = new() + { + Name = "ConformanceClient", + Version = "1.0.0" + } + }; + + await using var mcpClient = await McpClient.CreateAsync(clientTransport, options); -bool success = true; + var tools = await mcpClient.ListToolsAsync(); + Console.WriteLine($"Available tools: {string.Join(", ", tools.Select(t => t.Name))}"); -var tools = await mcpClient.ListToolsAsync(); -Console.WriteLine($"Available tools: {string.Join(", ", tools.Select(t => t.Name))}"); + // Call the "add_numbers" tool + var toolName = "add_numbers"; + Console.WriteLine($"Calling tool: {toolName}"); + var result = await mcpClient.CallToolAsync(toolName: toolName, arguments: new Dictionary + { + { "a", 5 }, + { "b", 10 } + }); -// Call the "add_numbers" tool -var toolName = "add_numbers"; -Console.WriteLine($"Calling tool: {toolName}"); -var result = await mcpClient.CallToolAsync(toolName: toolName, arguments: new Dictionary + // Exit code 0 on success, 1 on failure + return result.IsError != true ? 0 : 1; +} + +async Task AuthMetadata() { + var clientTransport = new HttpClientTransport(new() + { + Endpoint = new Uri(endpoint), + TransportMode = HttpTransportMode.StreamableHttp, + OAuth = new() + { + RedirectUri = new Uri("http://localhost:1179/callback"), + AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync, + DynamicClientRegistration = new() + { + ClientName = "ProtectedMcpClient", + }, + } + }); + + McpClientOptions options = new() + { + ClientInfo = new() + { + Name = "ConformanceClient", + Version = "1.0.0" + } + }; + + try { + await using var mcpClient = await McpClient.CreateAsync(clientTransport, options); + await mcpClient.PingAsync(); + return 0; + } catch (Exception ex) { + Console.WriteLine($"Error during Ping: {ex.Message}"); + } + return 1; +} + +// Copied from ProtectedMcpClient sample +static async Task HandleAuthorizationUrlAsync(Uri authorizationUrl, Uri redirectUri, CancellationToken cancellationToken) { - { "a", 5 }, - { "b", 10 } -}); + Console.WriteLine("Starting OAuth authorization flow..."); + Console.WriteLine($"Opening browser to: {authorizationUrl}"); + + var listenerPrefix = redirectUri.GetLeftPart(UriPartial.Authority); + if (!listenerPrefix.EndsWith("/")) listenerPrefix += "/"; + + using var listener = new HttpListener(); + listener.Prefixes.Add(listenerPrefix); + + try + { + listener.Start(); + Console.WriteLine($"Listening for OAuth callback on: {listenerPrefix}"); -success &= result.IsError != true; + OpenBrowser(authorizationUrl); -// Exit code 0 on success, 1 on failure -return success ? 0 : 1; \ No newline at end of file + var context = await listener.GetContextAsync(); + var query = HttpUtility.ParseQueryString(context.Request.Url?.Query ?? string.Empty); + var code = query["code"]; + var error = query["error"]; + + string responseHtml = "

Authentication complete

You can close this window now.

"; + byte[] buffer = Encoding.UTF8.GetBytes(responseHtml); + context.Response.ContentLength64 = buffer.Length; + context.Response.ContentType = "text/html"; + context.Response.OutputStream.Write(buffer, 0, buffer.Length); + context.Response.Close(); + + if (!string.IsNullOrEmpty(error)) + { + Console.WriteLine($"Auth error: {error}"); + return null; + } + + if (string.IsNullOrEmpty(code)) + { + Console.WriteLine("No authorization code received"); + return null; + } + + Console.WriteLine("Authorization code received successfully."); + return code; + } + catch (Exception ex) + { + Console.WriteLine($"Error getting auth code: {ex.Message}"); + return null; + } + finally + { + if (listener.IsListening) listener.Stop(); + } +} + +/// +/// Opens the specified URL in the default browser. +/// +/// The URL to open. +static void OpenBrowser(Uri url) +{ + // Validate the URI scheme - only allow safe protocols + if (url.Scheme != Uri.UriSchemeHttp && url.Scheme != Uri.UriSchemeHttps) + { + Console.WriteLine($"Error: Only HTTP and HTTPS URLs are allowed."); + return; + } + + try + { + var psi = new ProcessStartInfo + { + FileName = url.ToString(), + UseShellExecute = true + }; + Process.Start(psi); + } + catch (Exception ex) + { + Console.WriteLine($"Error opening browser: {ex.Message}"); + Console.WriteLine($"Please manually open this URL: {url}"); + } +} diff --git a/tests/ModelContextProtocol.ConformanceServer/ModelContextProtocol.ConformanceServer.csproj b/tests/ModelContextProtocol.ConformanceServer/ModelContextProtocol.ConformanceServer.csproj index 73f4f89bc..15b2c87f2 100644 --- a/tests/ModelContextProtocol.ConformanceServer/ModelContextProtocol.ConformanceServer.csproj +++ b/tests/ModelContextProtocol.ConformanceServer/ModelContextProtocol.ConformanceServer.csproj @@ -5,7 +5,6 @@ enable enable Exe - ConformanceServer From 8349cd077a7f2554dc8b6a738331368ab2b62769 Mon Sep 17 00:00:00 2001 From: Mike Kistler Date: Wed, 10 Dec 2025 10:48:20 -0600 Subject: [PATCH 06/14] Refactor client to single codepath for all tests --- .../ClientConformanceTests.cs | 10 ++ .../Program.cs | 110 ++++++------------ 2 files changed, 46 insertions(+), 74 deletions(-) diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/ClientConformanceTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/ClientConformanceTests.cs index fe1c3649d..86d2f15bb 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/ClientConformanceTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/ClientConformanceTests.cs @@ -22,6 +22,16 @@ public ClientConformanceTests(ITestOutputHelper output) [InlineData("initialize")] [InlineData("tools_call")] [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")] public async Task RunConformanceTest(string scenario) { // Check if Node.js is installed diff --git a/tests/ModelContextProtocol.ConformanceClient/Program.cs b/tests/ModelContextProtocol.ConformanceClient/Program.cs index 4041b1c0b..d011e9bd8 100644 --- a/tests/ModelContextProtocol.ConformanceClient/Program.cs +++ b/tests/ModelContextProtocol.ConformanceClient/Program.cs @@ -18,90 +18,52 @@ var scenario = args[0]; var endpoint = args[1]; -switch (scenario) +McpClientOptions options = new() { - case "initialize": - return await ToolsCall(); - case "tools_call": - return await ToolsCall(); - case "auth/metadata-default": - return await AuthMetadata(); - default: - Console.WriteLine($"Unknown scenario: {scenario}"); - return 1; -} - -// Implements the "tools_call" conformance test scenario -// We can also use this for the "initialize" scenario -async Task ToolsCall() -{ - var clientTransport = new HttpClientTransport(new() + ClientInfo = new() { - Endpoint = new Uri(endpoint), - TransportMode = HttpTransportMode.StreamableHttp - }); + Name = "ConformanceClient", + Version = "1.0.0" + } +}; - McpClientOptions options = new() +var clientTransport = new HttpClientTransport(new() +{ + Endpoint = new Uri(endpoint), + TransportMode = HttpTransportMode.StreamableHttp, + OAuth = new() { - ClientInfo = new() + RedirectUri = new Uri("http://localhost:1179/callback"), + AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync, + DynamicClientRegistration = new() { - Name = "ConformanceClient", - Version = "1.0.0" - } - }; - - await using var mcpClient = await McpClient.CreateAsync(clientTransport, options); + ClientName = "ProtectedMcpClient", + }, + } +}); - var tools = await mcpClient.ListToolsAsync(); - Console.WriteLine($"Available tools: {string.Join(", ", tools.Select(t => t.Name))}"); +await using var mcpClient = await McpClient.CreateAsync(clientTransport, options); - // Call the "add_numbers" tool - var toolName = "add_numbers"; - Console.WriteLine($"Calling tool: {toolName}"); - var result = await mcpClient.CallToolAsync(toolName: toolName, arguments: new Dictionary - { - { "a", 5 }, - { "b", 10 } - }); - - // Exit code 0 on success, 1 on failure - return result.IsError != true ? 0 : 1; +try { + await mcpClient.PingAsync(); +} catch (Exception ex) { + Console.WriteLine($"Error during Ping: {ex.Message}"); } -async Task AuthMetadata() { - var clientTransport = new HttpClientTransport(new() - { - Endpoint = new Uri(endpoint), - TransportMode = HttpTransportMode.StreamableHttp, - OAuth = new() - { - RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync, - DynamicClientRegistration = new() - { - ClientName = "ProtectedMcpClient", - }, - } - }); +var tools = await mcpClient.ListToolsAsync(); +Console.WriteLine($"Available tools: {string.Join(", ", tools.Select(t => t.Name))}"); - McpClientOptions options = new() - { - ClientInfo = new() - { - Name = "ConformanceClient", - Version = "1.0.0" - } - }; - - try { - await using var mcpClient = await McpClient.CreateAsync(clientTransport, options); - await mcpClient.PingAsync(); - return 0; - } catch (Exception ex) { - Console.WriteLine($"Error during Ping: {ex.Message}"); - } - return 1; -} +// Call the "add_numbers" tool +var toolName = "add_numbers"; +Console.WriteLine($"Calling tool: {toolName}"); +var result = await mcpClient.CallToolAsync(toolName: toolName, arguments: new Dictionary +{ + { "a", 5 }, + { "b", 10 } +}); + +// Exit code 0 on success, 1 on failure +return result.IsError != true ? 0 : 1; // Copied from ProtectedMcpClient sample static async Task HandleAuthorizationUrlAsync(Uri authorizationUrl, Uri redirectUri, CancellationToken cancellationToken) From edf74f5b8a200d76f81bfeef04c2c2ca2ff62979 Mon Sep 17 00:00:00 2001 From: Mike Kistler Date: Thu, 11 Dec 2025 07:00:58 -0600 Subject: [PATCH 07/14] Make tool call conditional on scenario --- .../Program.cs | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/tests/ModelContextProtocol.ConformanceClient/Program.cs b/tests/ModelContextProtocol.ConformanceClient/Program.cs index d011e9bd8..b4fbd5924 100644 --- a/tests/ModelContextProtocol.ConformanceClient/Program.cs +++ b/tests/ModelContextProtocol.ConformanceClient/Program.cs @@ -50,17 +50,20 @@ Console.WriteLine($"Error during Ping: {ex.Message}"); } -var tools = await mcpClient.ListToolsAsync(); -Console.WriteLine($"Available tools: {string.Join(", ", tools.Select(t => t.Name))}"); - -// Call the "add_numbers" tool -var toolName = "add_numbers"; -Console.WriteLine($"Calling tool: {toolName}"); -var result = await mcpClient.CallToolAsync(toolName: toolName, arguments: new Dictionary +if (scenario == "tools_call") { - { "a", 5 }, - { "b", 10 } -}); + var tools = await mcpClient.ListToolsAsync(); + Console.WriteLine($"Available tools: {string.Join(", ", tools.Select(t => t.Name))}"); + + // Call the "add_numbers" tool + var toolName = "add_numbers"; + Console.WriteLine($"Calling tool: {toolName}"); + var result = await mcpClient.CallToolAsync(toolName: toolName, arguments: new Dictionary + { + { "a", 5 }, + { "b", 10 } + }); +} // Exit code 0 on success, 1 on failure return result.IsError != true ? 0 : 1; From ff16ca6a5ca5383655b0dfbbe4be3267ee741b47 Mon Sep 17 00:00:00 2001 From: Mike Kistler Date: Thu, 11 Dec 2025 16:07:15 -0600 Subject: [PATCH 08/14] Fixes/improvements from debug session --- .../ClientConformanceTests.cs | 4 +- ...elContextProtocol.ConformanceClient.csproj | 4 + .../Program.cs | 81 ++++++++++++------- 3 files changed, 56 insertions(+), 33 deletions(-) diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/ClientConformanceTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/ClientConformanceTests.cs index 86d2f15bb..723729bff 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/ClientConformanceTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/ClientConformanceTests.cs @@ -26,8 +26,8 @@ public ClientConformanceTests(ITestOutputHelper output) [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/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")] diff --git a/tests/ModelContextProtocol.ConformanceClient/ModelContextProtocol.ConformanceClient.csproj b/tests/ModelContextProtocol.ConformanceClient/ModelContextProtocol.ConformanceClient.csproj index b8f2af686..e6cfad564 100644 --- a/tests/ModelContextProtocol.ConformanceClient/ModelContextProtocol.ConformanceClient.csproj +++ b/tests/ModelContextProtocol.ConformanceClient/ModelContextProtocol.ConformanceClient.csproj @@ -16,4 +16,8 @@
+ + + +
diff --git a/tests/ModelContextProtocol.ConformanceClient/Program.cs b/tests/ModelContextProtocol.ConformanceClient/Program.cs index b4fbd5924..eaaaf3692 100644 --- a/tests/ModelContextProtocol.ConformanceClient/Program.cs +++ b/tests/ModelContextProtocol.ConformanceClient/Program.cs @@ -2,8 +2,9 @@ using System.Net; using System.Text; using System.Web; - +using Microsoft.Extensions.Logging; using ModelContextProtocol.Client; +using ModelContextProtocol.Protocol; // This program expects the following command-line arguments: // 1. The client conformance test scenario to run (e.g., "tools_call") @@ -27,6 +28,11 @@ } }; +var consoleLoggerFactory = LoggerFactory.Create(builder => +{ + builder.AddConsole(); +}); + var clientTransport = new HttpClientTransport(new() { Endpoint = new Uri(endpoint), @@ -40,33 +46,53 @@ ClientName = "ProtectedMcpClient", }, } -}); +}, loggerFactory: consoleLoggerFactory); -await using var mcpClient = await McpClient.CreateAsync(clientTransport, options); +await using var mcpClient = await McpClient.CreateAsync(clientTransport, options, loggerFactory: consoleLoggerFactory); -try { - await mcpClient.PingAsync(); -} catch (Exception ex) { - Console.WriteLine($"Error during Ping: {ex.Message}"); -} +bool success = true; -if (scenario == "tools_call") +switch (scenario) { - var tools = await mcpClient.ListToolsAsync(); - Console.WriteLine($"Available tools: {string.Join(", ", tools.Select(t => t.Name))}"); + case "tools_call": + { + var tools = await mcpClient.ListToolsAsync(); + Console.WriteLine($"Available tools: {string.Join(", ", tools.Select(t => t.Name))}"); - // Call the "add_numbers" tool - var toolName = "add_numbers"; - Console.WriteLine($"Calling tool: {toolName}"); - var result = await mcpClient.CallToolAsync(toolName: toolName, arguments: new Dictionary + // Call the "add_numbers" tool + var toolName = "add_numbers"; + Console.WriteLine($"Calling tool: {toolName}"); + var result = await mcpClient.CallToolAsync(toolName: toolName, arguments: new Dictionary + { + { "a", 5 }, + { "b", 10 } + }); + success &= !(result.IsError == true); + break; + } + case "auth/scope-step-up": { - { "a", 5 }, - { "b", 10 } - }); + // Just testing that we can authenticate and list tools + var tools = await mcpClient.ListToolsAsync(); + Console.WriteLine($"Available tools: {string.Join(", ", tools.Select(t => t.Name))}"); + + // Call the "test_tool" tool + var toolName = tools.FirstOrDefault()?.Name ?? "test-tool"; + Console.WriteLine($"Calling tool: {toolName}"); + var result = await mcpClient.CallToolAsync(toolName: toolName, arguments: new Dictionary + { + { "foo", "bar" }, + }); + success &= !(result.IsError == true); + break; + } + default: + // No extra processing for other scenarios + break; } // Exit code 0 on success, 1 on failure -return result.IsError != true ? 0 : 1; +return success ? 0 : 1; // Copied from ProtectedMcpClient sample static async Task HandleAuthorizationUrlAsync(Uri authorizationUrl, Uri redirectUri, CancellationToken cancellationToken) @@ -85,7 +111,7 @@ listener.Start(); Console.WriteLine($"Listening for OAuth callback on: {listenerPrefix}"); - OpenBrowser(authorizationUrl); + _ = OpenBrowserAsync(authorizationUrl); var context = await listener.GetContextAsync(); var query = HttpUtility.ParseQueryString(context.Request.Url?.Query ?? string.Empty); @@ -125,11 +151,8 @@ } } -/// -/// Opens the specified URL in the default browser. -/// -/// The URL to open. -static void OpenBrowser(Uri url) +// Simulate a user opening the browser and logging in +static async Task OpenBrowserAsync(Uri url) { // Validate the URI scheme - only allow safe protocols if (url.Scheme != Uri.UriSchemeHttp && url.Scheme != Uri.UriSchemeHttps) @@ -140,12 +163,8 @@ static void OpenBrowser(Uri url) try { - var psi = new ProcessStartInfo - { - FileName = url.ToString(), - UseShellExecute = true - }; - Process.Start(psi); + using var httpClient = new HttpClient(); + using var authResponse = await httpClient.GetAsync(url); } catch (Exception ex) { From 9817b62a1a2f08be7f3b6700e628e0e8ddd609f7 Mon Sep 17 00:00:00 2001 From: Mike Kistler Date: Mon, 15 Dec 2025 19:52:48 -0600 Subject: [PATCH 09/14] Remove unnecessary usings --- tests/ModelContextProtocol.ConformanceClient/Program.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/ModelContextProtocol.ConformanceClient/Program.cs b/tests/ModelContextProtocol.ConformanceClient/Program.cs index eaaaf3692..679453fd0 100644 --- a/tests/ModelContextProtocol.ConformanceClient/Program.cs +++ b/tests/ModelContextProtocol.ConformanceClient/Program.cs @@ -1,10 +1,8 @@ -using System.Diagnostics; using System.Net; using System.Text; using System.Web; using Microsoft.Extensions.Logging; using ModelContextProtocol.Client; -using ModelContextProtocol.Protocol; // This program expects the following command-line arguments: // 1. The client conformance test scenario to run (e.g., "tools_call") From 19b753c5fb1b72c4ee78876561cc305191d1a900 Mon Sep 17 00:00:00 2001 From: Mike Kistler Date: Tue, 16 Dec 2025 07:11:49 -0600 Subject: [PATCH 10/14] Copilot fix for hanging tests --- .../Program.cs | 45 +++++++++++++------ 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/tests/ModelContextProtocol.ConformanceClient/Program.cs b/tests/ModelContextProtocol.ConformanceClient/Program.cs index 679453fd0..fecf6e2e6 100644 --- a/tests/ModelContextProtocol.ConformanceClient/Program.cs +++ b/tests/ModelContextProtocol.ConformanceClient/Program.cs @@ -1,4 +1,5 @@ using System.Net; +using System.Net.Sockets; using System.Text; using System.Web; using Microsoft.Extensions.Logging; @@ -31,14 +32,37 @@ builder.AddConsole(); }); +// Configure OAuth callback port via environment or pick an ephemeral port. +var callbackPortEnv = Environment.GetEnvironmentVariable("OAUTH_CALLBACK_PORT"); +int callbackPort = 0; +if (!string.IsNullOrEmpty(callbackPortEnv) && int.TryParse(callbackPortEnv, out var parsedPort)) +{ + callbackPort = parsedPort; +} + +if (callbackPort == 0) +{ + var tcp = new TcpListener(IPAddress.Loopback, 0); + tcp.Start(); + callbackPort = ((IPEndPoint)tcp.LocalEndpoint).Port; + tcp.Stop(); +} + +var listenerPrefix = $"http://localhost:{callbackPort}/"; +var preStartedListener = new HttpListener(); +preStartedListener.Prefixes.Add(listenerPrefix); +preStartedListener.Start(); + +var clientRedirectUri = new Uri($"http://localhost:{callbackPort}/callback"); + var clientTransport = new HttpClientTransport(new() { Endpoint = new Uri(endpoint), TransportMode = HttpTransportMode.StreamableHttp, OAuth = new() { - RedirectUri = new Uri("http://localhost:1179/callback"), - AuthorizationRedirectDelegate = HandleAuthorizationUrlAsync, + RedirectUri = clientRedirectUri, + AuthorizationRedirectDelegate = (authUrl, redirectUri, ct) => HandleAuthorizationUrlWithListenerAsync(authUrl, redirectUri, preStartedListener, ct), DynamicClientRegistration = new() { ClientName = "ProtectedMcpClient", @@ -93,25 +117,18 @@ return success ? 0 : 1; // Copied from ProtectedMcpClient sample -static async Task HandleAuthorizationUrlAsync(Uri authorizationUrl, Uri redirectUri, CancellationToken cancellationToken) +static async Task HandleAuthorizationUrlWithListenerAsync(Uri authorizationUrl, Uri redirectUri, HttpListener listener, CancellationToken cancellationToken) { Console.WriteLine("Starting OAuth authorization flow..."); Console.WriteLine($"Opening browser to: {authorizationUrl}"); - var listenerPrefix = redirectUri.GetLeftPart(UriPartial.Authority); - if (!listenerPrefix.EndsWith("/")) listenerPrefix += "/"; - - using var listener = new HttpListener(); - listener.Prefixes.Add(listenerPrefix); - try { - listener.Start(); - Console.WriteLine($"Listening for OAuth callback on: {listenerPrefix}"); - _ = OpenBrowserAsync(authorizationUrl); - var context = await listener.GetContextAsync(); + Console.WriteLine($"Listening for OAuth callback on: {listener.Prefixes.Cast().FirstOrDefault()}"); + var contextTask = listener.GetContextAsync(); + var context = await contextTask.WaitAsync(cancellationToken); var query = HttpUtility.ParseQueryString(context.Request.Url?.Query ?? string.Empty); var code = query["code"]; var error = query["error"]; @@ -145,7 +162,7 @@ } finally { - if (listener.IsListening) listener.Stop(); + try { if (listener.IsListening) listener.Stop(); } catch { } } } From 1c186284af95b19afca65346b1f5529441c68a73 Mon Sep 17 00:00:00 2001 From: Mike Kistler Date: Wed, 7 Jan 2026 19:26:20 +0000 Subject: [PATCH 11/14] Fix auth/scope-step-up conformance test The OAuth HTTP listener was being stopped after each authorization callback, preventing it from handling multiple OAuth flows such as scope step-up. Remove the finally block that stops the listener so it remains active for the lifetime of the client, allowing it to handle multiple authorization flows including scope step-up and re-authentication scenarios. --- tests/ModelContextProtocol.ConformanceClient/Program.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/ModelContextProtocol.ConformanceClient/Program.cs b/tests/ModelContextProtocol.ConformanceClient/Program.cs index fecf6e2e6..9e766e145 100644 --- a/tests/ModelContextProtocol.ConformanceClient/Program.cs +++ b/tests/ModelContextProtocol.ConformanceClient/Program.cs @@ -160,10 +160,7 @@ Console.WriteLine($"Error getting auth code: {ex.Message}"); return null; } - finally - { - try { if (listener.IsListening) listener.Stop(); } catch { } - } + // Note: Don't stop the listener here - it may be reused for scope step-up or re-authentication } // Simulate a user opening the browser and logging in From 014be506d789dc9bc18235a4812284122e2d6994 Mon Sep 17 00:00:00 2001 From: Mike Kistler Date: Wed, 7 Jan 2026 20:53:25 +0000 Subject: [PATCH 12/14] Use conformance test's expected client metadata document URI --- tests/ModelContextProtocol.ConformanceClient/Program.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/ModelContextProtocol.ConformanceClient/Program.cs b/tests/ModelContextProtocol.ConformanceClient/Program.cs index 9e766e145..b2d56950e 100644 --- a/tests/ModelContextProtocol.ConformanceClient/Program.cs +++ b/tests/ModelContextProtocol.ConformanceClient/Program.cs @@ -62,6 +62,8 @@ OAuth = new() { RedirectUri = clientRedirectUri, + // Configure the metadata document URI for CIMD. + ClientMetadataDocumentUri = new Uri("https://conformance-test.local/client-metadata.json"), AuthorizationRedirectDelegate = (authUrl, redirectUri, ct) => HandleAuthorizationUrlWithListenerAsync(authUrl, redirectUri, preStartedListener, ct), DynamicClientRegistration = new() { From 0bb4c5ae30fd043d154cc8b43cc108ca35f6f82f Mon Sep 17 00:00:00 2001 From: Mike Kistler Date: Wed, 7 Jan 2026 21:05:25 -0600 Subject: [PATCH 13/14] Apply suggestions from code review Co-authored-by: Stephen Halter --- .../ClientConformanceTests.cs | 2 +- .../Program.cs | 72 ++++--------------- 2 files changed, 15 insertions(+), 59 deletions(-) diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/ClientConformanceTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/ClientConformanceTests.cs index 723729bff..ac4f4d8f9 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/ClientConformanceTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/ClientConformanceTests.cs @@ -18,7 +18,7 @@ public ClientConformanceTests(ITestOutputHelper output) _output = output; } - [Theory] + [Theory(Skip = "npx is not installed. Skipping client conformance tests.", SkipUnless = nameof(IsNpxInstalled))] [InlineData("initialize")] [InlineData("tools_call")] [InlineData("auth/metadata-default")] diff --git a/tests/ModelContextProtocol.ConformanceClient/Program.cs b/tests/ModelContextProtocol.ConformanceClient/Program.cs index b2d56950e..461102c2e 100644 --- a/tests/ModelContextProtocol.ConformanceClient/Program.cs +++ b/tests/ModelContextProtocol.ConformanceClient/Program.cs @@ -119,70 +119,26 @@ return success ? 0 : 1; // Copied from ProtectedMcpClient sample -static async Task HandleAuthorizationUrlWithListenerAsync(Uri authorizationUrl, Uri redirectUri, HttpListener listener, CancellationToken cancellationToken) +// Simulate a user opening the browser and logging in +// Copied from OAuthTestBase +static async Task HandleAuthorizationUrlAsync(Uri authorizationUrl, Uri redirectUri, CancellationToken cancellationToken) { Console.WriteLine("Starting OAuth authorization flow..."); - Console.WriteLine($"Opening browser to: {authorizationUrl}"); + Console.WriteLine($"Simulating opening browser to: {authorizationUrl}"); - try + using var handler = new HttpClientHandler() { - _ = OpenBrowserAsync(authorizationUrl); - - Console.WriteLine($"Listening for OAuth callback on: {listener.Prefixes.Cast().FirstOrDefault()}"); - var contextTask = listener.GetContextAsync(); - var context = await contextTask.WaitAsync(cancellationToken); - var query = HttpUtility.ParseQueryString(context.Request.Url?.Query ?? string.Empty); - var code = query["code"]; - var error = query["error"]; - - string responseHtml = "

Authentication complete

You can close this window now.

"; - byte[] buffer = Encoding.UTF8.GetBytes(responseHtml); - context.Response.ContentLength64 = buffer.Length; - context.Response.ContentType = "text/html"; - context.Response.OutputStream.Write(buffer, 0, buffer.Length); - context.Response.Close(); - - if (!string.IsNullOrEmpty(error)) - { - Console.WriteLine($"Auth error: {error}"); - return null; - } - - if (string.IsNullOrEmpty(code)) - { - Console.WriteLine("No authorization code received"); - return null; - } + AllowAutoRedirect = false, + }; + using var httpClient = new HttpClient(handler); + using var redirectResponse = await httpClient.GetAsync(authorizationUrl, cancellationToken); + var location = redirectResponse.Headers.Location; - Console.WriteLine("Authorization code received successfully."); - return code; - } - catch (Exception ex) + if (location is not null && !string.IsNullOrEmpty(location.Query)) { - Console.WriteLine($"Error getting auth code: {ex.Message}"); - return null; + var queryParams = QueryHelpers.ParseQuery(location.Query); + return queryParams["code"]; } - // Note: Don't stop the listener here - it may be reused for scope step-up or re-authentication -} -// Simulate a user opening the browser and logging in -static async Task OpenBrowserAsync(Uri url) -{ - // Validate the URI scheme - only allow safe protocols - if (url.Scheme != Uri.UriSchemeHttp && url.Scheme != Uri.UriSchemeHttps) - { - Console.WriteLine($"Error: Only HTTP and HTTPS URLs are allowed."); - return; - } - - try - { - using var httpClient = new HttpClient(); - using var authResponse = await httpClient.GetAsync(url); - } - catch (Exception ex) - { - Console.WriteLine($"Error opening browser: {ex.Message}"); - Console.WriteLine($"Please manually open this URL: {url}"); - } + return null; } From f047c47d2e83f1292b2da591f8997a4b95b94667 Mon Sep 17 00:00:00 2001 From: Mike Kistler Date: Thu, 8 Jan 2026 09:02:59 -0600 Subject: [PATCH 14/14] Refactor conformance tests to use shared NodeHelpers and improve client conformance setup --- tests/Common/Utils/NodeHelpers.cs | 70 +++++++++++++++++++ .../ClientConformanceTests.cs | 45 ++---------- ...delContextProtocol.AspNetCore.Tests.csproj | 1 + .../ServerConformanceTests.cs | 41 +---------- .../Program.cs | 19 ++--- 5 files changed, 88 insertions(+), 88 deletions(-) create mode 100644 tests/Common/Utils/NodeHelpers.cs diff --git a/tests/Common/Utils/NodeHelpers.cs b/tests/Common/Utils/NodeHelpers.cs new file mode 100644 index 000000000..70e02d6c2 --- /dev/null +++ b/tests/Common/Utils/NodeHelpers.cs @@ -0,0 +1,70 @@ +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace ModelContextProtocol.Tests.Utils; + +/// +/// Helper utilities for Node.js and npm operations. +/// +public static class NodeHelpers +{ + /// + /// Creates a ProcessStartInfo configured to run npx with the specified arguments. + /// + /// The arguments to pass to npx. + /// A configured ProcessStartInfo for running npx. + public static ProcessStartInfo NpxStartInfo(string arguments) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // On Windows, npx is a PowerShell script, so we need to use cmd.exe to invoke it + return new ProcessStartInfo + { + FileName = "cmd.exe", + Arguments = $"/c npx {arguments}", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + } + else + { + // On Unix-like systems, npx is typically a shell script that can be executed directly + return new ProcessStartInfo + { + FileName = "npx", + Arguments = arguments, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + } + } + + /// + /// Checks if Node.js and npx are installed and available on the system. + /// + /// True if npx is available, false otherwise. + public static bool IsNpxInstalled() + { + try + { + var startInfo = NpxStartInfo("--version"); + + using var process = Process.Start(startInfo); + if (process == null) + { + return false; + } + + process.WaitForExit(5000); + return process.ExitCode == 0; + } + catch + { + return false; + } + } +} diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/ClientConformanceTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/ClientConformanceTests.cs index ac4f4d8f9..7a92cd67c 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/ClientConformanceTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/ClientConformanceTests.cs @@ -13,6 +13,9 @@ public class ClientConformanceTests //: IAsyncLifetime { private readonly ITestOutputHelper _output; + // Public static property required for SkipUnless attribute + public static bool IsNpxInstalled => NodeHelpers.IsNpxInstalled(); + public ClientConformanceTests(ITestOutputHelper output) { _output = output; @@ -34,9 +37,6 @@ public ClientConformanceTests(ITestOutputHelper output) [InlineData("auth/scope-step-up")] public async Task RunConformanceTest(string scenario) { - // 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 RunClientConformanceScenario(scenario); @@ -59,15 +59,7 @@ public async Task RunConformanceTest(string scenario) $"ConformanceClient executable not found at: {conformanceClientPath}"); } - var startInfo = new ProcessStartInfo - { - FileName = "npx", - Arguments = $"-y @modelcontextprotocol/conformance client --scenario {scenario} --command \"{conformanceClientPath} {scenario}\"", - RedirectStandardOutput = true, - RedirectStandardError = true, - UseShellExecute = false, - CreateNoWindow = true - }; + var startInfo = NodeHelpers.NpxStartInfo($"-y @modelcontextprotocol/conformance client --scenario {scenario} --command \"{conformanceClientPath} {scenario}\""); var outputBuilder = new StringBuilder(); var errorBuilder = new StringBuilder(); @@ -104,33 +96,4 @@ public async Task RunConformanceTest(string scenario) 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; - } - } } diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/ModelContextProtocol.AspNetCore.Tests.csproj b/tests/ModelContextProtocol.AspNetCore.Tests/ModelContextProtocol.AspNetCore.Tests.csproj index 128b5158c..095560a22 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/ModelContextProtocol.AspNetCore.Tests.csproj +++ b/tests/ModelContextProtocol.AspNetCore.Tests/ModelContextProtocol.AspNetCore.Tests.csproj @@ -57,6 +57,7 @@ + diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/ServerConformanceTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/ServerConformanceTests.cs index 8c0055fe8..312f32457 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/ServerConformanceTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/ServerConformanceTests.cs @@ -98,7 +98,7 @@ public async ValueTask DisposeAsync() public async Task RunConformanceTests() { // Check if Node.js is installed - Assert.SkipWhen(!IsNodeInstalled(), "Node.js is not installed. Skipping conformance tests."); + Assert.SkipWhen(!NodeHelpers.IsNpxInstalled(), "Node.js is not installed. Skipping conformance tests."); // Run the conformance test suite var result = await RunNpxConformanceTests(); @@ -117,15 +117,7 @@ private void StartConformanceServer() 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 startInfo = NodeHelpers.NpxStartInfo($"-y @modelcontextprotocol/conformance server --url {_serverUrl}"); var outputBuilder = new StringBuilder(); var errorBuilder = new StringBuilder(); @@ -162,33 +154,4 @@ private void StartConformanceServer() 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; - } - } } diff --git a/tests/ModelContextProtocol.ConformanceClient/Program.cs b/tests/ModelContextProtocol.ConformanceClient/Program.cs index 461102c2e..e2f09e88f 100644 --- a/tests/ModelContextProtocol.ConformanceClient/Program.cs +++ b/tests/ModelContextProtocol.ConformanceClient/Program.cs @@ -48,11 +48,6 @@ tcp.Stop(); } -var listenerPrefix = $"http://localhost:{callbackPort}/"; -var preStartedListener = new HttpListener(); -preStartedListener.Prefixes.Add(listenerPrefix); -preStartedListener.Start(); - var clientRedirectUri = new Uri($"http://localhost:{callbackPort}/callback"); var clientTransport = new HttpClientTransport(new() @@ -64,7 +59,7 @@ RedirectUri = clientRedirectUri, // Configure the metadata document URI for CIMD. ClientMetadataDocumentUri = new Uri("https://conformance-test.local/client-metadata.json"), - AuthorizationRedirectDelegate = (authUrl, redirectUri, ct) => HandleAuthorizationUrlWithListenerAsync(authUrl, redirectUri, preStartedListener, ct), + AuthorizationRedirectDelegate = (authUrl, redirectUri, ct) => HandleAuthorizationUrlAsync(authUrl, redirectUri, ct), DynamicClientRegistration = new() { ClientName = "ProtectedMcpClient", @@ -136,8 +131,16 @@ if (location is not null && !string.IsNullOrEmpty(location.Query)) { - var queryParams = QueryHelpers.ParseQuery(location.Query); - return queryParams["code"]; + // Parse query string to extract "code" parameter + var query = location.Query.TrimStart('?'); + foreach (var pair in query.Split('&')) + { + var parts = pair.Split('=', 2); + if (parts.Length == 2 && parts[0] == "code") + { + return HttpUtility.UrlDecode(parts[1]); + } + } } return null;