Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Refactor conformance tests to use shared NodeHelpers and improve clie…
…nt conformance setup
  • Loading branch information
mikekistler committed Jan 8, 2026
commit f047c47d2e83f1292b2da591f8997a4b95b94667
70 changes: 70 additions & 0 deletions tests/Common/Utils/NodeHelpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using System.Diagnostics;
using System.Runtime.InteropServices;

namespace ModelContextProtocol.Tests.Utils;

/// <summary>
/// Helper utilities for Node.js and npm operations.
/// </summary>
public static class NodeHelpers
{
/// <summary>
/// Creates a ProcessStartInfo configured to run npx with the specified arguments.
/// </summary>
/// <param name="arguments">The arguments to pass to npx.</param>
/// <returns>A configured ProcessStartInfo for running npx.</returns>
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
};
}
}

/// <summary>
/// Checks if Node.js and npx are installed and available on the system.
/// </summary>
/// <returns>True if npx is available, false otherwise.</returns>
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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);

Expand All @@ -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();
Expand Down Expand Up @@ -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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
<ProjectReference Include="..\..\src\ModelContextProtocol.AspNetCore\ModelContextProtocol.AspNetCore.csproj" />
<ProjectReference Include="..\ModelContextProtocol.TestSseServer\ModelContextProtocol.TestSseServer.csproj" />
<ProjectReference Include="..\ModelContextProtocol.TestOAuthServer\ModelContextProtocol.TestOAuthServer.csproj" />
<ProjectReference Include="..\ModelContextProtocol.ConformanceClient\ModelContextProtocol.ConformanceClient.csproj" />
<ProjectReference Include="..\ModelContextProtocol.ConformanceServer\ModelContextProtocol.ConformanceServer.csproj" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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();
Expand Down Expand Up @@ -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;
}
}
}
19 changes: 11 additions & 8 deletions tests/ModelContextProtocol.ConformanceClient/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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",
Expand Down Expand Up @@ -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;
Expand Down