|
| 1 | +From 9fd3a16c0306d5afbe35ff6d32cb2a84c34482ab Mon Sep 17 00:00:00 2001 |
| 2 | +From: Rohit Kushwaha <subhamkushwahagd@gmail.com> |
| 3 | +Date: Thu, 20 Aug 2026 19:35:32 +0000 |
| 4 | +Subject: [PATCH] Reject listRoots when client lacks roots capability, without |
| 5 | + sending a request |
| 6 | + |
| 7 | +McpAsyncServerExchange#createMessage and #createElicitation both |
| 8 | +fail fast with an IllegalStateException when the client hasn't |
| 9 | +declared the relevant capability, avoiding an unnecessary round |
| 10 | +trip to the client. #listRoots(String) did not follow the same |
| 11 | +pattern and would send a roots/list request even when the client |
| 12 | +never advertised roots support. |
| 13 | + |
| 14 | +This aligns #listRoots(String) with the existing fail-fast |
| 15 | +convention: it now checks clientCapabilities for null (client not |
| 16 | +yet initialized) and for a missing roots capability before sending |
| 17 | +McpSchema.METHOD_ROOTS_LIST. |
| 18 | + |
| 19 | +Fixes #1067 |
| 20 | +--- |
| 21 | + .../server/McpAsyncServerExchange.java | 7 +++ |
| 22 | + .../server/McpAsyncServerExchangeTests.java | 48 +++++++++++++++++++ |
| 23 | + 2 files changed, 55 insertions(+) |
| 24 | + |
| 25 | +diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java |
| 26 | +index e27d612..d977ac9 100644 |
| 27 | +--- a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java |
| 28 | ++++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java |
| 29 | +@@ -225,6 +225,13 @@ public class McpAsyncServerExchange { |
| 30 | + * @return A Mono that emits the list of roots result containing |
| 31 | + */ |
| 32 | + public Mono<McpSchema.ListRootsResult> listRoots(String cursor) { |
| 33 | ++ if (this.clientCapabilities == null) { |
| 34 | ++ return Mono |
| 35 | ++ .error(new IllegalStateException("Client must be initialized. Call the initialize method first!")); |
| 36 | ++ } |
| 37 | ++ if (this.clientCapabilities.roots() == null) { |
| 38 | ++ return Mono.error(new IllegalStateException("Client must be configured with roots capabilities")); |
| 39 | ++ } |
| 40 | + return this.session.sendRequest(McpSchema.METHOD_ROOTS_LIST, new McpSchema.PaginatedRequest(cursor), |
| 41 | + LIST_ROOTS_RESULT_TYPE_REF); |
| 42 | + } |
| 43 | +diff --git a/mcp-core/src/test/java/io/modelcontextprotocol/server/McpAsyncServerExchangeTests.java b/mcp-core/src/test/java/io/modelcontextprotocol/server/McpAsyncServerExchangeTests.java |
| 44 | +index f4f76b1..b556a8a 100644 |
| 45 | +--- a/mcp-core/src/test/java/io/modelcontextprotocol/server/McpAsyncServerExchangeTests.java |
| 46 | ++++ b/mcp-core/src/test/java/io/modelcontextprotocol/server/McpAsyncServerExchangeTests.java |
| 47 | +@@ -157,6 +157,54 @@ class McpAsyncServerExchangeTests { |
| 48 | + }).verifyComplete(); |
| 49 | + } |
| 50 | + |
| 51 | ++ @Test |
| 52 | ++ void testListRootsWithNullCapabilities() { |
| 53 | ++ |
| 54 | ++ McpAsyncServerExchange exchangeWithNullCapabilities = new McpAsyncServerExchange("testSessionId", |
| 55 | ++ mockSession, null, clientInfo, McpTransportContext.EMPTY); |
| 56 | ++ |
| 57 | ++ StepVerifier.create(exchangeWithNullCapabilities.listRoots()).verifyErrorSatisfies(error -> { |
| 58 | ++ assertThat(error).isInstanceOf(IllegalStateException.class) |
| 59 | ++ .hasMessage("Client must be initialized. Call the initialize method first!"); |
| 60 | ++ }); |
| 61 | ++ |
| 62 | ++ // Verify that sendRequest was never called due to null capabilities |
| 63 | ++ verify(mockSession, never()).sendRequest(eq(McpSchema.METHOD_ROOTS_LIST), any(), any(TypeRef.class)); |
| 64 | ++ } |
| 65 | ++ |
| 66 | ++ @Test |
| 67 | ++ void testListRootsWithoutRootsCapabilities() { |
| 68 | ++ |
| 69 | ++ McpSchema.ClientCapabilities capabilitiesWithoutRoots = McpSchema.ClientCapabilities.builder() |
| 70 | ++ .sampling() |
| 71 | ++ .build(); |
| 72 | ++ |
| 73 | ++ McpAsyncServerExchange exchangeWithoutRoots = new McpAsyncServerExchange("testSessionId", mockSession, |
| 74 | ++ capabilitiesWithoutRoots, clientInfo, McpTransportContext.EMPTY); |
| 75 | ++ |
| 76 | ++ StepVerifier.create(exchangeWithoutRoots.listRoots()).verifyErrorSatisfies(error -> { |
| 77 | ++ assertThat(error).isInstanceOf(IllegalStateException.class) |
| 78 | ++ .hasMessage("Client must be configured with roots capabilities"); |
| 79 | ++ }); |
| 80 | ++ |
| 81 | ++ // Verify that sendRequest was never called due to missing roots capabilities |
| 82 | ++ verify(mockSession, never()).sendRequest(eq(McpSchema.METHOD_ROOTS_LIST), any(), any(TypeRef.class)); |
| 83 | ++ } |
| 84 | ++ |
| 85 | ++ @Test |
| 86 | ++ void testListRootsWithSpecificCursorAndNullCapabilities() { |
| 87 | ++ |
| 88 | ++ McpAsyncServerExchange exchangeWithNullCapabilities = new McpAsyncServerExchange("testSessionId", |
| 89 | ++ mockSession, null, clientInfo, McpTransportContext.EMPTY); |
| 90 | ++ |
| 91 | ++ StepVerifier.create(exchangeWithNullCapabilities.listRoots("someCursor")).verifyErrorSatisfies(error -> { |
| 92 | ++ assertThat(error).isInstanceOf(IllegalStateException.class) |
| 93 | ++ .hasMessage("Client must be initialized. Call the initialize method first!"); |
| 94 | ++ }); |
| 95 | ++ |
| 96 | ++ verify(mockSession, never()).sendRequest(eq(McpSchema.METHOD_ROOTS_LIST), any(), any(TypeRef.class)); |
| 97 | ++ } |
| 98 | ++ |
| 99 | + @Test |
| 100 | + void testListRootsWithError() { |
| 101 | + |
| 102 | +-- |
| 103 | +2.43.0 |
| 104 | + |
0 commit comments