From 78b62c1b6e9e44587bd50c4bf66536c871496685 Mon Sep 17 00:00:00 2001 From: Yerang-RB <312474169+Yerang-RB@users.noreply.github.com> Date: Sat, 19 Sep 2026 17:23:15 +0900 Subject: [PATCH] Reject listRoots when client lacks roots capability McpAsyncServerExchange#createMessage and #createElicitation both fail fast when the client is not initialized or has not declared the matching capability. listRoots sent the request regardless. Add the same two guards to listRoots(String cursor). The no-arg listRoots() delegates to it, so both overloads are covered by the single check. --- .../server/McpAsyncServerExchange.java | 7 ++++ .../server/McpAsyncServerExchangeTests.java | 34 +++++++++++++++++++ 2 files changed, 41 insertions(+) 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..6f3f7892d 100644 --- a/mcp-core/src/test/java/io/modelcontextprotocol/server/McpAsyncServerExchangeTests.java +++ b/mcp-core/src/test/java/io/modelcontextprotocol/server/McpAsyncServerExchangeTests.java @@ -207,6 +207,40 @@ void testListRootsUnmodifiabilityAfterAccumulation() { }).verifyComplete(); } + @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 testGetClientCapabilities() { assertThat(exchange.getClientCapabilities()).isEqualTo(clientCapabilities);