From 364fac2895160b794bc8fcd70bbb048f63b7f565 Mon Sep 17 00:00:00 2001 From: Nikita Kibitkin Date: Sun, 26 Jul 2026 20:34:56 +0200 Subject: [PATCH] Reject listRoots when client lacks roots capability Fail fast with an IllegalStateException instead of sending a roots/list request the client cannot handle, matching the existing guards in createMessage and createElicitation. Fixes #1067 --- .../server/McpAsyncServerExchange.java | 7 ++++ .../server/McpAsyncServerExchangeTests.java | 34 +++++++++++++++++++ .../server/McpSyncServerExchangeTests.java | 33 ++++++++++++++++++ ...stractMcpClientServerIntegrationTests.java | 10 +++--- 4 files changed, 78 insertions(+), 6 deletions(-) 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..d977ac96f 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 roots 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..d74434898 100644 --- a/mcp-core/src/test/java/io/modelcontextprotocol/server/McpAsyncServerExchangeTests.java +++ b/mcp-core/src/test/java/io/modelcontextprotocol/server/McpAsyncServerExchangeTests.java @@ -170,6 +170,40 @@ void testListRootsWithError() { }); } + @Test + void testListRootsWithNullCapabilities() { + // Given - Create exchange with null capabilities + McpAsyncServerExchange exchangeWithNullCapabilities = new McpAsyncServerExchange("testSessionId", mockSession, + null, clientInfo, McpTransportContext.EMPTY); + + StepVerifier.create(exchangeWithNullCapabilities.listRoots()).verifyErrorSatisfies(error -> { + assertThat(error).isInstanceOf(IllegalStateException.class) + .hasMessage("Client must be initialized. Call the initialize method first!"); + }); + + // Verify that sendRequest was never called due to null capabilities + verify(mockSession, never()).sendRequest(eq(McpSchema.METHOD_ROOTS_LIST), any(), any(TypeRef.class)); + } + + @Test + void testListRootsWithoutRootsCapabilities() { + // Given - Create exchange without roots capabilities + McpSchema.ClientCapabilities capabilitiesWithoutRoots = McpSchema.ClientCapabilities.builder() + .sampling() + .build(); + + McpAsyncServerExchange exchangeWithoutRoots = new McpAsyncServerExchange("testSessionId", mockSession, + capabilitiesWithoutRoots, clientInfo, McpTransportContext.EMPTY); + + StepVerifier.create(exchangeWithoutRoots.listRoots()).verifyErrorSatisfies(error -> { + assertThat(error).isInstanceOf(IllegalStateException.class) + .hasMessage("Client must be configured with roots capabilities"); + }); + + // Verify that sendRequest was never called due to missing roots capabilities + verify(mockSession, never()).sendRequest(eq(McpSchema.METHOD_ROOTS_LIST), any(), any(TypeRef.class)); + } + @Test void testListRootsUnmodifiabilityAfterAccumulation() { diff --git a/mcp-core/src/test/java/io/modelcontextprotocol/server/McpSyncServerExchangeTests.java b/mcp-core/src/test/java/io/modelcontextprotocol/server/McpSyncServerExchangeTests.java index 017bf04ec..a5e7a3ef3 100644 --- a/mcp-core/src/test/java/io/modelcontextprotocol/server/McpSyncServerExchangeTests.java +++ b/mcp-core/src/test/java/io/modelcontextprotocol/server/McpSyncServerExchangeTests.java @@ -169,6 +169,39 @@ void testListRootsWithError() { assertThatThrownBy(() -> exchange.listRoots()).isInstanceOf(RuntimeException.class).hasMessage("Network error"); } + @Test + void testListRootsWithNullCapabilities() { + // Given - Create exchange with null capabilities + McpAsyncServerExchange asyncExchangeWithNullCapabilities = new McpAsyncServerExchange("testSessionId", + mockSession, null, clientInfo, McpTransportContext.EMPTY); + McpSyncServerExchange exchangeWithNullCapabilities = new McpSyncServerExchange( + asyncExchangeWithNullCapabilities); + + assertThatThrownBy(() -> exchangeWithNullCapabilities.listRoots()).isInstanceOf(IllegalStateException.class) + .hasMessage("Client must be initialized. Call the initialize method first!"); + + // Verify that sendRequest was never called due to null capabilities + verify(mockSession, never()).sendRequest(eq(McpSchema.METHOD_ROOTS_LIST), any(), any(TypeRef.class)); + } + + @Test + void testListRootsWithoutRootsCapabilities() { + // Given - Create exchange without roots capabilities + McpSchema.ClientCapabilities capabilitiesWithoutRoots = McpSchema.ClientCapabilities.builder() + .sampling() + .build(); + + McpAsyncServerExchange asyncExchangeWithoutRoots = new McpAsyncServerExchange("testSessionId", mockSession, + capabilitiesWithoutRoots, clientInfo, McpTransportContext.EMPTY); + McpSyncServerExchange exchangeWithoutRoots = new McpSyncServerExchange(asyncExchangeWithoutRoots); + + assertThatThrownBy(() -> exchangeWithoutRoots.listRoots()).isInstanceOf(IllegalStateException.class) + .hasMessage("Client must be configured with roots capabilities"); + + // Verify that sendRequest was never called due to missing roots capabilities + verify(mockSession, never()).sendRequest(eq(McpSchema.METHOD_ROOTS_LIST), any(), any(TypeRef.class)); + } + @Test void testListRootsUnmodifiabilityAfterAccumulation() { diff --git a/mcp-test/src/main/java/io/modelcontextprotocol/AbstractMcpClientServerIntegrationTests.java b/mcp-test/src/main/java/io/modelcontextprotocol/AbstractMcpClientServerIntegrationTests.java index 80a711da1..119951149 100644 --- a/mcp-test/src/main/java/io/modelcontextprotocol/AbstractMcpClientServerIntegrationTests.java +++ b/mcp-test/src/main/java/io/modelcontextprotocol/AbstractMcpClientServerIntegrationTests.java @@ -1056,12 +1056,10 @@ void testRootsWithoutCapability() { 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"); - } + assertThatThrownBy( + () -> mcpClient.callTool(McpSchema.CallToolRequest.builder("tool1").arguments(Map.of()).build())) + .isInstanceOf(McpError.class) + .hasMessage("Client must be configured with roots capabilities"); } finally { mcpServer.closeGracefully();