From d1dfd7f9f4bf84689087ba90aa90e5b701d06e83 Mon Sep 17 00:00:00 2001 From: jeffhandley <1031940+jeffhandley@users.noreply.github.com> Date: Sat, 31 Oct 2026 00:39:01 +0000 Subject: [PATCH] Fix parallel integration test server startup Launch pinned npm test servers directly with node instead of npx to avoid shared-cache races. Add concurrent startup coverage and document installation prerequisites. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 31750ec3-6eef-4c89-bf64-2bd317a692d9 --- CONTRIBUTING.md | 3 +++ tests/Common/Utils/NodeHelpers.cs | 14 ++++++++++++++ .../ClientIntegrationTestFixture.cs | 10 +++++----- .../ClientIntegrationTests.cs | 16 ++++++++++++++-- 4 files changed, 36 insertions(+), 7 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7f9411025..fa1473e1f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,6 +26,9 @@ Before you begin, ensure you have the following installed: - **.NET SDK 10.0 or later** - Required to build and test the project - Download from [dotnet.microsoft.com/download](https://dotnet.microsoft.com/download) - Verify installation: `dotnet --version` +- **Node.js 22 or later and npm** - Required for integration and conformance tests + - Run `npm ci` from the repository root before running tests to install the pinned test dependencies. + - Integration tests launch the installed servers directly with `node`, avoiding npm installation and shared-cache access during parallel tests. The dev container configuration in this repository includes all the necessary tools and SDKs to get started quickly. diff --git a/tests/Common/Utils/NodeHelpers.cs b/tests/Common/Utils/NodeHelpers.cs index b38bb0ffe..2d557db1d 100644 --- a/tests/Common/Utils/NodeHelpers.cs +++ b/tests/Common/Utils/NodeHelpers.cs @@ -31,6 +31,20 @@ private static string FindRepoRoot() throw new InvalidOperationException("Could not find repository root (no package-lock.json found in ancestor directories)."); } + /// + /// Gets the entry point of a test server installed by 'npm ci', without invoking npm during tests. + /// + public static string GetTestServerScriptPath(string serverName) + { + var scriptPath = Path.Combine(FindRepoRoot(), "node_modules", "@modelcontextprotocol", $"server-{serverName}", "dist", "index.js"); + if (!File.Exists(scriptPath)) + { + throw new FileNotFoundException("Test server is not installed. Run 'npm ci' from the repository root before running tests.", scriptPath); + } + + return scriptPath; + } + /// /// 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. diff --git a/tests/ModelContextProtocol.Tests/ClientIntegrationTestFixture.cs b/tests/ModelContextProtocol.Tests/ClientIntegrationTestFixture.cs index 7d40fa6b0..8aca46621 100644 --- a/tests/ModelContextProtocol.Tests/ClientIntegrationTestFixture.cs +++ b/tests/ModelContextProtocol.Tests/ClientIntegrationTestFixture.cs @@ -16,15 +16,16 @@ public class ClientIntegrationTestFixture public ClientIntegrationTestFixture() { - const string ServerEverythingVersion = "2026.1.26"; string testServerExecutable = Path.Combine(AppContext.BaseDirectory, "TestServer.exe"); string testServerDll = Path.Combine(AppContext.BaseDirectory, "TestServer.dll"); EverythingServerTransportOptions = new() { - Command = "npx", - // Change to Arguments = ["mcp-server-everything"] if you want to run the server locally after creating a symlink - Arguments = ["-y", "--verbose", $"@modelcontextprotocol/server-everything@{ServerEverythingVersion}"], + Command = "node", + // To use a local Everything server, build it, then run `npm link ` + // from this repository root after `npm ci`. The linked package must contain dist/index.js; + // no argument changes are needed because this path resolves through the package link. + Arguments = [NodeHelpers.GetTestServerScriptPath("everything")], Name = "Everything", }; @@ -36,7 +37,6 @@ public ClientIntegrationTestFixture() if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - // Change to Arguments to "mcp-server-everything" if you want to run the server locally after creating a symlink TestServerTransportOptions.Arguments = [PlatformDetection.IsMonoRuntime ? testServerExecutable : testServerDll]; } } diff --git a/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs b/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs index 4f0d478b9..373598dc1 100644 --- a/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs +++ b/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs @@ -61,6 +61,18 @@ public async Task Connect_ShouldProvideServerFields(string clientId) Assert.Null(client.SessionId); } + [Theory] + [MemberData(nameof(GetClients))] + public async Task ConnectConcurrently_Stdio(string clientId) + { + await Task.WhenAll(Enumerable.Range(0, 4).Select(async _ => + { + await using var client = await _fixture.CreateClientAsync(clientId); + var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken); + Assert.NotEmpty(tools); + })); + } + [Theory] [MemberData(nameof(GetClients))] public async Task ListTools_Stdio(string clientId) @@ -464,8 +476,8 @@ public async Task CallTool_Stdio_MemoryServer() // arrange StdioClientTransportOptions stdioOptions = new() { - Command = "npx", - Arguments = ["-y", "@modelcontextprotocol/server-memory"], + Command = "node", + Arguments = [NodeHelpers.GetTestServerScriptPath("memory")], Name = "memory", };