diff --git a/mcp-test/src/test/java/io/modelcontextprotocol/client/ServerParameterUtils.java b/mcp-test/src/test/java/io/modelcontextprotocol/client/ServerParameterUtils.java index 547ccc5..1f44600 100644 --- a/mcp-test/src/test/java/io/modelcontextprotocol/client/ServerParameterUtils.java +++ b/mcp-test/src/test/java/io/modelcontextprotocol/client/ServerParameterUtils.java @@ -1,21 +1,188 @@ +/* + * Copyright 2024-2026 the original author or authors. + */ + package io.modelcontextprotocol.client; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; + import io.modelcontextprotocol.client.transport.ServerParameters; +/** + * Provides the {@link ServerParameters} used to launch the {@code server-everything} MCP + * server for the stdio client tests. + * + *
+ * Those tests spawn a fresh server process per test, so the launch cost is paid dozens of + * times per build. Going through {@code npx} costs roughly 3 seconds per spawn even with + * a warm cache, because npx re-resolves the package every time, and it inserts two extra + * processes ({@code npx} → {@code npm exec} → {@code node}) between the test + * and the server. The latter also means the process the transport owns is not the server, + * so closing the transport leaves the server behind. + * + *
+ * Instead, the package is installed once per JVM into a version-keyed directory under the
+ * temporary directory, and the server is launched directly with {@code node}, which
+ * brings the per-spawn cost down to roughly 0.4 seconds.
+ */
public final class ServerParameterUtils {
+ private static final String SERVER_EVERYTHING_VERSION = "2025.12.18";
+
+ private static final boolean IS_WINDOWS = System.getProperty("os.name").toLowerCase().contains("win");
+
+ private static final Path SERVER_SCRIPT = resolveServerScript();
+
+ private static final String NODE_EXECUTABLE = resolveNodeExecutable();
+
private ServerParameterUtils() {
}
public static ServerParameters createServerParameters() {
- if (System.getProperty("os.name").toLowerCase().contains("win")) {
- return ServerParameters.builder("cmd.exe")
- .args("/c", "npx.cmd", "-y", "@modelcontextprotocol/server-everything@2025.12.18", "stdio")
- .build();
+ return ServerParameters.builder(NODE_EXECUTABLE).args(SERVER_SCRIPT.toString(), "stdio").build();
+ }
+
+ private static Path resolveServerScript() {
+ Path tmpDir = Paths.get(System.getProperty("java.io.tmpdir"));
+ Path installDir = tmpDir.resolve("mcp-server-everything-" + SERVER_EVERYTHING_VERSION);
+ if (!Files.isRegularFile(serverScriptIn(installDir))) {
+ install(tmpDir, installDir);
+ }
+ Path script = serverScriptIn(installDir);
+ if (!Files.isRegularFile(script)) {
+ throw new IllegalStateException("server-everything was not installed at " + script);
+ }
+ return script;
+ }
+
+ private static Path serverScriptIn(Path installDir) {
+ return installDir
+ .resolve(Paths.get("node_modules", "@modelcontextprotocol", "server-everything", "dist", "index.js"));
+ }
+
+ /**
+ * Installs into a staging directory and moves it into place atomically, so that
+ * concurrent builds sharing the temporary directory can never observe a partially
+ * installed tree.
+ */
+ private static void install(Path tmpDir, Path installDir) {
+ Path staging;
+ try {
+ staging = Files.createTempDirectory(tmpDir, "mcp-server-everything-staging-");
+ }
+ catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ try {
+ run(npmCommand("install", "--prefix", staging.toString(), "--no-save", "--no-audit", "--no-fund",
+ "--loglevel=error", "@modelcontextprotocol/server-everything@" + SERVER_EVERYTHING_VERSION));
+ try {
+ Files.move(staging, installDir, StandardCopyOption.ATOMIC_MOVE);
+ }
+ catch (IOException e) {
+ // Another build won the race and installed it first, which is fine as
+ // long as the result is usable.
+ if (!Files.isRegularFile(serverScriptIn(installDir))) {
+ throw new UncheckedIOException("Failed to install server-everything to " + installDir, e);
+ }
+ }
+ }
+ finally {
+ deleteRecursively(staging);
+ }
+ }
+
+ private static void deleteRecursively(Path path) {
+ if (!Files.exists(path)) {
+ return;
+ }
+ try (Stream
- * These tests use npx to download and run the MCP "everything" server locally. The first
- * test execution will download the everything server scripts and cache them locally,
- * which can take more than 15 seconds. Subsequent test runs will use the cached version
- * and execute faster.
+ * These tests run the MCP "everything" server locally, spawning a fresh server process
+ * per test. The server package is installed once and launched directly with node, see
+ * {@link ServerParameterUtils}. The first test execution installs the package, which can
+ * take more than 15 seconds; subsequent runs reuse the installed copy.
*
* @author Christian Tzolov
* @author Dariusz Jędrzejczyk
*/
-@Timeout(25) // Giving extra time beyond the client timeout to account for initial server
- // download
+@Timeout(25) // Giving extra time beyond the client timeout to account for the one-time
+ // install of the server package
class StdioMcpAsyncClientTests extends AbstractMcpAsyncClientTests {
@Override
diff --git a/mcp-test/src/test/java/io/modelcontextprotocol/client/StdioMcpSyncClientTests.java b/mcp-test/src/test/java/io/modelcontextprotocol/client/StdioMcpSyncClientTests.java
index 08e5ea6..c147056 100644
--- a/mcp-test/src/test/java/io/modelcontextprotocol/client/StdioMcpSyncClientTests.java
+++ b/mcp-test/src/test/java/io/modelcontextprotocol/client/StdioMcpSyncClientTests.java
@@ -25,16 +25,16 @@ import static org.assertj.core.api.Assertions.assertThat;
* Tests for the {@link McpSyncClient} with {@link StdioClientTransport}.
*
*
- * These tests use npx to download and run the MCP "everything" server locally. The first
- * test execution will download the everything server scripts and cache them locally,
- * which can take more than 15 seconds. Subsequent test runs will use the cached version
- * and execute faster.
+ * These tests run the MCP "everything" server locally, spawning a fresh server process
+ * per test. The server package is installed once and launched directly with node, see
+ * {@link ServerParameterUtils}. The first test execution installs the package, which can
+ * take more than 15 seconds; subsequent runs reuse the installed copy.
*
* @author Christian Tzolov
* @author Dariusz Jędrzejczyk
*/
-@Timeout(25) // Giving extra time beyond the client timeout to account for initial server
- // download
+@Timeout(25) // Giving extra time beyond the client timeout to account for the one-time
+ // install of the server package
class StdioMcpSyncClientTests extends AbstractMcpSyncClientTests {
@Override