Use package-lock.json to pin conformance test dependencies (#1247)
This commit is contained in:
parent
cfa89a3907
commit
e55ae7b7bd
9 changed files with 1416 additions and 54 deletions
13
.github/workflows/ci-build-test.yml
vendored
13
.github/workflows/ci-build-test.yml
vendored
|
|
@ -16,6 +16,8 @@ on:
|
||||||
- "*.props"
|
- "*.props"
|
||||||
- "Makefile"
|
- "Makefile"
|
||||||
- "global.json"
|
- "global.json"
|
||||||
|
- "package.json"
|
||||||
|
- "package-lock.json"
|
||||||
- "src/**"
|
- "src/**"
|
||||||
- "tests/**"
|
- "tests/**"
|
||||||
- "samples/**"
|
- "samples/**"
|
||||||
|
|
@ -52,15 +54,8 @@ jobs:
|
||||||
with:
|
with:
|
||||||
node-version: '20'
|
node-version: '20'
|
||||||
|
|
||||||
- name: 📦 Install dependencies for tests
|
- name: 📦 Install pinned npm dependencies for tests
|
||||||
run: npm install @modelcontextprotocol/server-everything@2025.12.18
|
run: npm ci
|
||||||
|
|
||||||
- name: 📦 Install dependencies for tests
|
|
||||||
run: npm install @modelcontextprotocol/server-memory
|
|
||||||
|
|
||||||
# Keep version in sync with McpConformanceVersion in Directory.Packages.props
|
|
||||||
- name: 📦 Install conformance test runner
|
|
||||||
run: npm install @modelcontextprotocol/conformance@0.1.10
|
|
||||||
|
|
||||||
- name: 🏗️ Build
|
- name: 🏗️ Build
|
||||||
run: make build CONFIGURATION=${{ matrix.configuration }}
|
run: make build CONFIGURATION=${{ matrix.configuration }}
|
||||||
|
|
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -34,6 +34,9 @@ _ReSharper*/
|
||||||
**/packages/*
|
**/packages/*
|
||||||
!**/packages/build/
|
!**/packages/build/
|
||||||
|
|
||||||
|
# npm
|
||||||
|
node_modules/
|
||||||
|
|
||||||
# MSTest test Results
|
# MSTest test Results
|
||||||
[Tt]est[Rr]esult*/
|
[Tt]est[Rr]esult*/
|
||||||
[Bb]uild[Ll]og.*
|
[Bb]uild[Ll]og.*
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,6 @@
|
||||||
<System9Version>9.0.11</System9Version>
|
<System9Version>9.0.11</System9Version>
|
||||||
<System10Version>10.0.2</System10Version>
|
<System10Version>10.0.2</System10Version>
|
||||||
<MicrosoftExtensionsVersion>10.2.0</MicrosoftExtensionsVersion>
|
<MicrosoftExtensionsVersion>10.2.0</MicrosoftExtensionsVersion>
|
||||||
<!-- Pin the conformance tester Node package version for CI stability.
|
|
||||||
Keep in sync npm install step at ci-build-test.yml -->
|
|
||||||
<McpConformanceVersion>0.1.10</McpConformanceVersion>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<!-- Product dependencies shared -->
|
<!-- Product dependencies shared -->
|
||||||
|
|
|
||||||
1274
package-lock.json
generated
Normal file
1274
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
10
package.json
Normal file
10
package.json
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"private": true,
|
||||||
|
"description": "Pinned npm dependencies for MCP C# SDK integration and conformance tests",
|
||||||
|
"dependencies": {
|
||||||
|
"@modelcontextprotocol/conformance": "0.1.10",
|
||||||
|
"@modelcontextprotocol/sdk": "1.25.3",
|
||||||
|
"@modelcontextprotocol/server-everything": "2025.12.18",
|
||||||
|
"@modelcontextprotocol/server-memory": "2026.1.26"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,20 +8,88 @@ namespace ModelContextProtocol.Tests.Utils;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class NodeHelpers
|
public static class NodeHelpers
|
||||||
{
|
{
|
||||||
|
private static readonly object _npmInstallLock = new();
|
||||||
|
private static bool _npmInstallCompleted;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a ProcessStartInfo configured to run npx with the specified arguments.
|
/// Finds the repository root by searching for package-lock.json in ancestor directories.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="arguments">The arguments to pass to npx.</param>
|
private static string FindRepoRoot()
|
||||||
/// <returns>A configured ProcessStartInfo for running npx.</returns>
|
|
||||||
public static ProcessStartInfo NpxStartInfo(string arguments)
|
|
||||||
{
|
{
|
||||||
|
var dir = AppContext.BaseDirectory;
|
||||||
|
while (dir is not null)
|
||||||
|
{
|
||||||
|
if (File.Exists(Path.Combine(dir, "package-lock.json")))
|
||||||
|
{
|
||||||
|
return dir;
|
||||||
|
}
|
||||||
|
dir = Path.GetDirectoryName(dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException("Could not find repository root (no package-lock.json found in ancestor directories).");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ensures npm dependencies are installed by running 'npm ci' from the repo root.
|
||||||
|
/// This is safe to call multiple times; it only runs once per test process.
|
||||||
|
/// </summary>
|
||||||
|
public static void EnsureNpmDependenciesInstalled()
|
||||||
|
{
|
||||||
|
if (_npmInstallCompleted)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_npmInstallLock)
|
||||||
|
{
|
||||||
|
if (_npmInstallCompleted)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var repoRoot = FindRepoRoot();
|
||||||
|
var nodeModulesPath = Path.Combine(repoRoot, "node_modules");
|
||||||
|
|
||||||
|
// Use 'npm ci' if node_modules doesn't exist, otherwise assume it's up to date.
|
||||||
|
if (!Directory.Exists(nodeModulesPath))
|
||||||
|
{
|
||||||
|
var startInfo = NpmStartInfo("ci", repoRoot);
|
||||||
|
using var process = Process.Start(startInfo)
|
||||||
|
?? throw new InvalidOperationException("Failed to start 'npm ci'.");
|
||||||
|
process.WaitForExit(120_000);
|
||||||
|
|
||||||
|
if (process.ExitCode != 0)
|
||||||
|
{
|
||||||
|
var error = process.StandardError.ReadToEnd();
|
||||||
|
throw new InvalidOperationException($"'npm ci' failed with exit code {process.ExitCode}: {error}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_npmInstallCompleted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a ProcessStartInfo configured to run a binary from node_modules/.bin/conformance.
|
||||||
|
/// Calls <see cref="EnsureNpmDependenciesInstalled"/> first.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="binaryName">The name of the binary in node_modules/.bin (e.g. "conformance").</param>
|
||||||
|
/// <param name="arguments">The arguments to pass to the binary.</param>
|
||||||
|
/// <returns>A configured ProcessStartInfo for running the binary.</returns>
|
||||||
|
public static ProcessStartInfo ConformanceTestStartInfo(string arguments)
|
||||||
|
{
|
||||||
|
EnsureNpmDependenciesInstalled();
|
||||||
|
|
||||||
|
var repoRoot = FindRepoRoot();
|
||||||
|
var binPath = Path.Combine(repoRoot, "node_modules", ".bin", "conformance");
|
||||||
|
|
||||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
{
|
{
|
||||||
// On Windows, npx is a PowerShell script, so we need to use cmd.exe to invoke it
|
// On Windows, node_modules/.bin contains .cmd shims that can be executed directly
|
||||||
return new ProcessStartInfo
|
return new ProcessStartInfo
|
||||||
{
|
{
|
||||||
FileName = "cmd.exe",
|
FileName = $"{binPath}.cmd",
|
||||||
Arguments = $"/c npx {arguments}",
|
Arguments = arguments,
|
||||||
RedirectStandardOutput = true,
|
RedirectStandardOutput = true,
|
||||||
RedirectStandardError = true,
|
RedirectStandardError = true,
|
||||||
UseShellExecute = false,
|
UseShellExecute = false,
|
||||||
|
|
@ -30,10 +98,9 @@ public static class NodeHelpers
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// On Unix-like systems, npx is typically a shell script that can be executed directly
|
|
||||||
return new ProcessStartInfo
|
return new ProcessStartInfo
|
||||||
{
|
{
|
||||||
FileName = "npx",
|
FileName = binPath,
|
||||||
Arguments = arguments,
|
Arguments = arguments,
|
||||||
RedirectStandardOutput = true,
|
RedirectStandardOutput = true,
|
||||||
RedirectStandardError = true,
|
RedirectStandardError = true,
|
||||||
|
|
@ -44,16 +111,22 @@ public static class NodeHelpers
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Checks if Node.js and npx are installed and available on the system.
|
/// Checks if Node.js is installed and available on the system.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>True if npx is available, false otherwise.</returns>
|
public static bool IsNodeInstalled()
|
||||||
public static bool IsNpxInstalled()
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var startInfo = NpxStartInfo("--version");
|
using var process = Process.Start(new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = "node",
|
||||||
|
Arguments = "--version",
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true
|
||||||
|
});
|
||||||
|
|
||||||
using var process = Process.Start(startInfo);
|
|
||||||
if (process == null)
|
if (process == null)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -67,4 +140,34 @@ public static class NodeHelpers
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static ProcessStartInfo NpmStartInfo(string arguments, string workingDirectory)
|
||||||
|
{
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
{
|
||||||
|
return new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = "cmd.exe",
|
||||||
|
Arguments = $"/c npm {arguments}",
|
||||||
|
WorkingDirectory = workingDirectory,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = "npm",
|
||||||
|
Arguments = arguments,
|
||||||
|
WorkingDirectory = workingDirectory,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,19 +9,19 @@ namespace ModelContextProtocol.ConformanceTests;
|
||||||
/// This test runs the Node.js-based conformance test suite for the client
|
/// This test runs the Node.js-based conformance test suite for the client
|
||||||
/// and reports the results.
|
/// and reports the results.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ClientConformanceTests //: IAsyncLifetime
|
public class ClientConformanceTests
|
||||||
{
|
{
|
||||||
private readonly ITestOutputHelper _output;
|
private readonly ITestOutputHelper _output;
|
||||||
|
|
||||||
// Public static property required for SkipUnless attribute
|
// Public static property required for SkipUnless attribute
|
||||||
public static bool IsNpxInstalled => NodeHelpers.IsNpxInstalled();
|
public static bool IsNodeInstalled => NodeHelpers.IsNodeInstalled();
|
||||||
|
|
||||||
public ClientConformanceTests(ITestOutputHelper output)
|
public ClientConformanceTests(ITestOutputHelper output)
|
||||||
{
|
{
|
||||||
_output = output;
|
_output = output;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory(Skip = "npx is not installed. Skipping client conformance tests.", SkipUnless = nameof(IsNpxInstalled))]
|
[Theory(Skip = "Node.js is not installed. Skipping client conformance tests.", SkipUnless = nameof(IsNodeInstalled))]
|
||||||
[InlineData("initialize")]
|
[InlineData("initialize")]
|
||||||
[InlineData("tools_call")]
|
[InlineData("tools_call")]
|
||||||
[InlineData("auth/metadata-default")]
|
[InlineData("auth/metadata-default")]
|
||||||
|
|
@ -59,7 +59,7 @@ public class ClientConformanceTests //: IAsyncLifetime
|
||||||
$"ConformanceClient executable not found at: {conformanceClientPath}");
|
$"ConformanceClient executable not found at: {conformanceClientPath}");
|
||||||
}
|
}
|
||||||
|
|
||||||
var startInfo = NodeHelpers.NpxStartInfo($"-y @modelcontextprotocol/conformance client --scenario {scenario} --command \"{conformanceClientPath} {scenario}\"");
|
var startInfo = NodeHelpers.ConformanceTestStartInfo($"client --scenario {scenario} --command \"{conformanceClientPath} {scenario}\"");
|
||||||
|
|
||||||
var outputBuilder = new StringBuilder();
|
var outputBuilder = new StringBuilder();
|
||||||
var errorBuilder = new StringBuilder();
|
var errorBuilder = new StringBuilder();
|
||||||
|
|
|
||||||
|
|
@ -52,13 +52,6 @@
|
||||||
<PackageReference Include="System.Linq.AsyncEnumerable" />
|
<PackageReference Include="System.Linq.AsyncEnumerable" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<AssemblyAttribute Include="System.Reflection.AssemblyMetadataAttribute">
|
|
||||||
<_Parameter1>McpConformanceVersion</_Parameter1>
|
|
||||||
<_Parameter2>$(McpConformanceVersion)</_Parameter2>
|
|
||||||
</AssemblyAttribute>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\samples\TestServerWithHosting\TestServerWithHosting.csproj" />
|
<ProjectReference Include="..\..\samples\TestServerWithHosting\TestServerWithHosting.csproj" />
|
||||||
<ProjectReference Include="..\..\src\ModelContextProtocol.Core\ModelContextProtocol.Core.csproj" />
|
<ProjectReference Include="..\..\src\ModelContextProtocol.Core\ModelContextProtocol.Core.csproj" />
|
||||||
|
|
|
||||||
|
|
@ -97,11 +97,10 @@ public class ServerConformanceTests : IAsyncLifetime
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task RunConformanceTests()
|
public async Task RunConformanceTests()
|
||||||
{
|
{
|
||||||
// Check if Node.js is installed
|
Assert.SkipWhen(!NodeHelpers.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
|
// Run the conformance test suite
|
||||||
var result = await RunNpxConformanceTests();
|
var result = await RunConformanceTestsAsync();
|
||||||
|
|
||||||
// Report the results
|
// Report the results
|
||||||
Assert.True(result.Success,
|
Assert.True(result.Success,
|
||||||
|
|
@ -115,21 +114,9 @@ public class ServerConformanceTests : IAsyncLifetime
|
||||||
_serverTask = Task.Run(() => ConformanceServer.Program.MainAsync(["--urls", _serverUrl], new XunitLoggerProvider(_output), cancellationToken: _serverCts.Token));
|
_serverTask = Task.Run(() => ConformanceServer.Program.MainAsync(["--urls", _serverUrl], new XunitLoggerProvider(_output), cancellationToken: _serverCts.Token));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetConformanceVersion()
|
private async Task<(bool Success, string Output, string Error)> RunConformanceTestsAsync()
|
||||||
{
|
{
|
||||||
var assembly = typeof(ServerConformanceTests).Assembly;
|
var startInfo = NodeHelpers.ConformanceTestStartInfo($"server --url {_serverUrl}");
|
||||||
var attribute = assembly.GetCustomAttributes(typeof(System.Reflection.AssemblyMetadataAttribute), false)
|
|
||||||
.Cast<System.Reflection.AssemblyMetadataAttribute>()
|
|
||||||
.FirstOrDefault(a => a.Key == "McpConformanceVersion");
|
|
||||||
return attribute?.Value ?? throw new InvalidOperationException("McpConformanceVersion not found in assembly metadata");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<(bool Success, string Output, string Error)> RunNpxConformanceTests()
|
|
||||||
{
|
|
||||||
// Version is configured in Directory.Packages.props for central management
|
|
||||||
var version = GetConformanceVersion();
|
|
||||||
|
|
||||||
var startInfo = NodeHelpers.NpxStartInfo($"-y @modelcontextprotocol/conformance@{version} server --url {_serverUrl}");
|
|
||||||
|
|
||||||
var outputBuilder = new StringBuilder();
|
var outputBuilder = new StringBuilder();
|
||||||
var errorBuilder = new StringBuilder();
|
var errorBuilder = new StringBuilder();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue