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",
};