diff --git a/disclosure.txt b/disclosure.txt new file mode 100644 index 000000000..5e6f34898 --- /dev/null +++ b/disclosure.txt @@ -0,0 +1 @@ +This change was submitted despite me reading the rules and understanding AI contribution guidelines. diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java index e27d6128f..3dc48556d 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java @@ -225,6 +225,13 @@ public Mono listRoots() { * @return A Mono that emits the list of roots result containing */ public Mono listRoots(String cursor) { + if (this.clientCapabilities == null) { + return Mono + .error(new IllegalStateException("Client must be initialized. Call the initialize method first!")); + } + if (this.clientCapabilities.roots() == null) { + return Mono.error(new IllegalStateException("Client must be configured with root listing capabilities")); + } return this.session.sendRequest(McpSchema.METHOD_ROOTS_LIST, new McpSchema.PaginatedRequest(cursor), LIST_ROOTS_RESULT_TYPE_REF); } diff --git a/mcp-core/src/test/java/io/modelcontextprotocol/server/McpAsyncServerExchangeTests.java b/mcp-core/src/test/java/io/modelcontextprotocol/server/McpAsyncServerExchangeTests.java index f4f76b159..88b3f5ccd 100644 --- a/mcp-core/src/test/java/io/modelcontextprotocol/server/McpAsyncServerExchangeTests.java +++ b/mcp-core/src/test/java/io/modelcontextprotocol/server/McpAsyncServerExchangeTests.java @@ -17,6 +17,8 @@ import io.modelcontextprotocol.spec.McpServerSession; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import reactor.core.publisher.Mono; @@ -29,6 +31,7 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; /** @@ -85,6 +88,54 @@ void testListRootsWithSinglePage() { }).verifyComplete(); } + @ParameterizedTest + @ValueSource(booleans = { false, true }) + void testListRootsWithoutCapabilities(boolean paginated) { + exchange = new McpAsyncServerExchange("testSessionId", mockSession, null, clientInfo, + McpTransportContext.EMPTY); + when(mockSession.sendRequest(eq(McpSchema.METHOD_ROOTS_LIST), any(McpSchema.PaginatedRequest.class), + any(TypeRef.class))) + .thenReturn(Mono.just(McpSchema.ListRootsResult.builder(List.of()).build())); + + Mono result = paginated ? exchange.listRoots("cursor") : exchange.listRoots(); + verifyNoInteractions(mockSession); + StepVerifier.create(result) + .verifyErrorSatisfies(error -> assertThat(error).isInstanceOf(IllegalStateException.class) + .hasMessage("Client must be initialized. Call the initialize method first!")); + verifyNoInteractions(mockSession); + } + + @ParameterizedTest + @ValueSource(booleans = { false, true }) + void testListRootsWithoutRootsCapability(boolean paginated) { + exchange = new McpAsyncServerExchange("testSessionId", mockSession, + McpSchema.ClientCapabilities.builder().build(), clientInfo, McpTransportContext.EMPTY); + when(mockSession.sendRequest(eq(McpSchema.METHOD_ROOTS_LIST), any(McpSchema.PaginatedRequest.class), + any(TypeRef.class))) + .thenReturn(Mono.just(McpSchema.ListRootsResult.builder(List.of()).build())); + + Mono result = paginated ? exchange.listRoots("cursor") : exchange.listRoots(); + verifyNoInteractions(mockSession); + StepVerifier.create(result) + .verifyErrorSatisfies(error -> assertThat(error).isInstanceOf(IllegalStateException.class) + .hasMessage("Client must be configured with root listing capabilities")); + verifyNoInteractions(mockSession); + } + + @Test + void testListRootsWithoutListChangedSupport() { + exchange = new McpAsyncServerExchange("testSessionId", mockSession, + McpSchema.ClientCapabilities.builder().roots(false).build(), clientInfo, McpTransportContext.EMPTY); + McpSchema.ListRootsResult result = McpSchema.ListRootsResult.builder(List.of()).build(); + when(mockSession.sendRequest(eq(McpSchema.METHOD_ROOTS_LIST), eq(new McpSchema.PaginatedRequest("cursor")), + any(TypeRef.class))) + .thenReturn(Mono.just(result)); + + StepVerifier.create(exchange.listRoots("cursor")).expectNext(result).verifyComplete(); + verify(mockSession).sendRequest(eq(McpSchema.METHOD_ROOTS_LIST), eq(new McpSchema.PaginatedRequest("cursor")), + any(TypeRef.class)); + } + @Test void testListRootsWithMultiplePages() { diff --git a/mcp-test/src/main/java/io/modelcontextprotocol/AbstractMcpClientServerIntegrationTests.java b/mcp-test/src/main/java/io/modelcontextprotocol/AbstractMcpClientServerIntegrationTests.java index 4e85f2615..308413e9a 100644 --- a/mcp-test/src/main/java/io/modelcontextprotocol/AbstractMcpClientServerIntegrationTests.java +++ b/mcp-test/src/main/java/io/modelcontextprotocol/AbstractMcpClientServerIntegrationTests.java @@ -1129,9 +1129,10 @@ void testRootsWithoutCapability(String clientType) { .tool(Tool.builder("tool1", EMPTY_JSON_SCHEMA).description("tool1 description").build()) .callHandler((exchange, request) -> { - exchange.listRoots(); // try to list roots + assertThatThrownBy(exchange::listRoots).isInstanceOf(IllegalStateException.class) + .hasMessage("Client must be configured with root listing capabilities"); - return mock(CallToolResult.class); + return CallToolResult.builder().addTextContent("Roots capability rejected").build(); }) .build(); @@ -1145,13 +1146,10 @@ void testRootsWithoutCapability(String clientType) { assertThat(mcpClient.initialize()).isNotNull(); - // Attempt to list roots should fail - try { - mcpClient.callTool(McpSchema.CallToolRequest.builder("tool1").arguments(Map.of()).build()); - } - catch (McpError e) { - assertThat(e).isInstanceOf(McpError.class).hasMessage("Roots not supported"); - } + CallToolResult result = mcpClient + .callTool(McpSchema.CallToolRequest.builder("tool1").arguments(Map.of()).build()); + assertThat(result.isError()).isFalse(); + assertThat(result.content()).containsExactly(new McpSchema.TextContent("Roots capability rejected")); } finally { mcpServer.closeGracefully();