From d1fe01645d8d2c227c9d0ad1ef21525b387ddf6e Mon Sep 17 00:00:00 2001 From: zqj <915186434@qq.com> Date: Tue, 18 Aug 2026 11:19:53 +0800 Subject: [PATCH] Fix stateful roots list change handling(#1061) --- .../server/McpAsyncServer.java | 12 +- ...HttpServletStreamableIntegrationTests.java | 116 ++++++++++++++++++ 2 files changed, 124 insertions(+), 4 deletions(-) diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java index ac78c4ff0..4d887c5d6 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java @@ -197,12 +197,16 @@ private Map prepareNotificationHandlers(McpServe .rootsChangeConsumers(); if (Utils.isEmpty(rootsChangeConsumers)) { - rootsChangeConsumers = List.of((exchange, roots) -> Mono.fromRunnable(() -> logger - .warn("Roots list changed notification, but no consumers provided. Roots list changed: {}", roots))); + notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_ROOTS_LIST_CHANGED, (exchange, params) -> { + logger.debug("Received {}", McpSchema.METHOD_NOTIFICATION_ROOTS_LIST_CHANGED); + return Mono.empty(); + }); + } + else { + notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_ROOTS_LIST_CHANGED, + asyncRootsListChangedNotificationHandler(rootsChangeConsumers)); } - notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_ROOTS_LIST_CHANGED, - asyncRootsListChangedNotificationHandler(rootsChangeConsumers)); return notificationHandlers; } diff --git a/mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletStreamableIntegrationTests.java b/mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletStreamableIntegrationTests.java index 2c9d14030..e17b489c9 100644 --- a/mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletStreamableIntegrationTests.java +++ b/mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletStreamableIntegrationTests.java @@ -5,8 +5,11 @@ package io.modelcontextprotocol.server; import java.time.Duration; +import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Stream; @@ -14,10 +17,12 @@ import io.modelcontextprotocol.client.McpClient; import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport; import io.modelcontextprotocol.common.McpTransportContext; +import io.modelcontextprotocol.json.TypeRef; import io.modelcontextprotocol.server.McpServer.AsyncSpecification; import io.modelcontextprotocol.server.McpServer.SyncSpecification; import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider; import io.modelcontextprotocol.server.transport.TomcatTestUtil; +import io.modelcontextprotocol.spec.McpClientTransport; import io.modelcontextprotocol.spec.McpSchema; import jakarta.servlet.http.HttpServletRequest; import org.apache.catalina.LifecycleException; @@ -144,6 +149,117 @@ void testMissingHandlerReturnsMethodNotFoundError() { } + @Test + void testRootsListChangedWithoutConsumerDoesNotRequestRoots() { + var mcpServer = prepareSyncServerBuilder().build(); + var clientTransport = createRootsCountingClientTransport(); + + try (var mcpClient = McpClient.sync(clientTransport) + .capabilities(McpSchema.ClientCapabilities.builder().roots(true).build()) + .roots(McpSchema.Root.builder("file:///test/root").name("test-root").build()) + .build()) { + + assertThat(mcpClient.initialize()).isNotNull(); + + mcpClient.rootsListChangedNotification(); + + assertThat(clientTransport.rootsListRequestCount()).isZero(); + } + finally { + mcpServer.closeGracefully(); + } + } + + @Test + void testRootsListChangedWithConsumerRequestsRoots() { + List roots = List.of(McpSchema.Root.builder("file:///test/root").name("test-root").build()); + AtomicInteger consumerInvocationCount = new AtomicInteger(); + AtomicReference> receivedRoots = new AtomicReference<>(); + var mcpServer = prepareSyncServerBuilder().rootsChangeHandler((exchange, rootsUpdate) -> { + consumerInvocationCount.incrementAndGet(); + receivedRoots.set(rootsUpdate); + }).build(); + var clientTransport = createRootsCountingClientTransport(); + + try (var mcpClient = McpClient.sync(clientTransport) + .capabilities(McpSchema.ClientCapabilities.builder().roots(true).build()) + .roots(roots) + .build()) { + + assertThat(mcpClient.initialize()).isNotNull(); + + mcpClient.rootsListChangedNotification(); + + assertThat(clientTransport.rootsListRequestCount()).isOne(); + assertThat(consumerInvocationCount).hasValue(1); + assertThat(receivedRoots).hasValue(roots); + } + finally { + mcpServer.closeGracefully(); + } + } + + private RootsCountingClientTransport createRootsCountingClientTransport() { + return new RootsCountingClientTransport(HttpClientStreamableHttpTransport.builder("http://localhost:" + PORT) + .endpoint(MESSAGE_ENDPOINT) + .build()); + } + + private static final class RootsCountingClientTransport implements McpClientTransport { + + private final McpClientTransport delegate; + + private final AtomicInteger rootsListRequestCount = new AtomicInteger(); + + private RootsCountingClientTransport(McpClientTransport delegate) { + this.delegate = delegate; + } + + @Override + public Mono connect( + Function, Mono> messageHandler) { + return this.delegate + .connect(message -> messageHandler.apply(message.doOnNext(this::recordRootsListRequest))); + } + + private void recordRootsListRequest(McpSchema.JSONRPCMessage message) { + if (message instanceof McpSchema.JSONRPCRequest request + && McpSchema.METHOD_ROOTS_LIST.equals(request.method())) { + this.rootsListRequestCount.incrementAndGet(); + } + } + + private int rootsListRequestCount() { + return this.rootsListRequestCount.get(); + } + + @Override + public Mono sendMessage(McpSchema.JSONRPCMessage message) { + return this.delegate.sendMessage(message); + } + + @Override + public T unmarshalFrom(Object data, TypeRef typeRef) { + return this.delegate.unmarshalFrom(data, typeRef); + } + + @Override + public List protocolVersions() { + return this.delegate.protocolVersions(); + } + + @Override + public void setExceptionHandler(Consumer handler) { + this.delegate.setExceptionHandler(handler); + } + + @Override + public Mono closeGracefully() { + return this.delegate.closeGracefully(); + } + + } + static McpTransportContextExtractor TEST_CONTEXT_EXTRACTOR = (r) -> McpTransportContext .create(Map.of("important", "value"));