From 2132001a86bcb3948de02d942d78ce43bcb4f064 Mon Sep 17 00:00:00 2001 From: Mike Kistler Date: Sat, 22 Nov 2025 21:03:22 -0800 Subject: [PATCH 01/10] 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 7818913eb039db23787d55026765a7c64bab8c2e Mon Sep 17 00:00:00 2001 From: Mike Kistler Date: Sat, 22 Nov 2025 21:45:49 -0800 Subject: [PATCH 02/10] 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 fe3bfd918a55ea406cd91a2ffb1bfe14c3b18d36 Mon Sep 17 00:00:00 2001 From: Mike Kistler Date: Sun, 23 Nov 2025 06:10:49 -0800 Subject: [PATCH 03/10] 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 87a91eede7305152422ee15447dc246937ae22ae Mon Sep 17 00:00:00 2001 From: Mike Kistler Date: Sat, 29 Nov 2025 06:16:37 -0800 Subject: [PATCH 04/10] 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 fb5497b92588ac26f6c5347db589a175b9d93106 Mon Sep 17 00:00:00 2001 From: Mike Kistler Date: Tue, 2 Dec 2025 15:44:38 -0800 Subject: [PATCH 05/10] 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 b31f62c871bd72335e6cfd6078070b64b1fe3563 Mon Sep 17 00:00:00 2001 From: Mike Kistler Date: Wed, 10 Dec 2025 10:48:20 -0600 Subject: [PATCH 06/10] 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 b4b536c94480f3d8a6c71d2c9b3cff2a3abff9a7 Mon Sep 17 00:00:00 2001 From: Mike Kistler Date: Thu, 11 Dec 2025 07:00:58 -0600 Subject: [PATCH 07/10] 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 36bcf48149d4f813895d617894f75c100a359104 Mon Sep 17 00:00:00 2001 From: Mike Kistler Date: Thu, 11 Dec 2025 16:07:15 -0600 Subject: [PATCH 08/10] 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 d84d3aeeeb05b9413a214c7c256f1705454fedf5 Mon Sep 17 00:00:00 2001 From: Mike Kistler Date: Mon, 15 Dec 2025 19:52:48 -0600 Subject: [PATCH 09/10] 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 da7ca688ba3d4bc4dfc9ce7ce0f36d9247d173b0 Mon Sep 17 00:00:00 2001 From: Mike Kistler Date: Tue, 16 Dec 2025 07:11:49 -0600 Subject: [PATCH 10/10] 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 { } } }