Skip to content

Commit 6a97f6a

Browse files
Reject listRoots when client lacks roots capability, without sending a request
1 parent 305e4cb commit 6a97f6a

5 files changed

Lines changed: 279 additions & 0 deletions

File tree

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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+

‎PR_DESCRIPTION.md‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
## Reject listRoots when client lacks roots capability, without sending a request
2+
3+
Fixes #1067
4+
5+
### What changed
6+
7+
`McpAsyncServerExchange#createMessage` and `#createElicitation` both fail fast
8+
with an `IllegalStateException` when the client hasn't declared the relevant
9+
capability, avoiding an unnecessary round trip to the client.
10+
11+
`#listRoots(String)` didn't follow the same pattern: it would send a
12+
`roots/list` request to the client even when the client never advertised
13+
`roots` support, only to fail later (or behave unexpectedly) depending on the
14+
client's own handling of an unsupported method.
15+
16+
This PR aligns `#listRoots(String)` with the existing fail-fast convention
17+
already used by `createMessage`/`createElicitation`:
18+
19+
- If `clientCapabilities` is `null` (client not yet initialized), fail with
20+
`"Client must be initialized. Call the initialize method first!"`
21+
- If `clientCapabilities.roots()` is `null` (client didn't declare roots
22+
support), fail with `"Client must be configured with roots capabilities"`
23+
- Otherwise, proceed with the request as before.
24+
25+
`listRoots()` (no-arg, paginated variant) and `McpSyncServerExchange#listRoots`
26+
both delegate to `listRoots(String)`, so they're covered automatically.
27+
28+
### Testing
29+
30+
Added three unit tests to `McpAsyncServerExchangeTests` mirroring the existing
31+
capability-check tests for `createMessage`:
32+
33+
- `testListRootsWithNullCapabilities`
34+
- `testListRootsWithoutRootsCapabilities`
35+
- `testListRootsWithSpecificCursorAndNullCapabilities`
36+
37+
Each verifies the correct `IllegalStateException` is raised and that
38+
`session.sendRequest(...)` is never invoked in these cases.
39+
40+
I audited all existing callers of `listRoots` in the repo (integration tests
41+
in `AbstractMcpClientServerIntegrationTests`, and the roots-changed handler in
42+
`McpAsyncServer`) — all already configure/assume roots capability, so this
43+
change shouldn't affect existing behavior anywhere else in the codebase.

‎listroots-fix.diff‎

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java
2+
index e27d612..d977ac9 100644
3+
--- a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java
4+
+++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java
5+
@@ -225,6 +225,13 @@ public class McpAsyncServerExchange {
6+
* @return A Mono that emits the list of roots result containing
7+
*/
8+
public Mono<McpSchema.ListRootsResult> listRoots(String cursor) {
9+
+ if (this.clientCapabilities == null) {
10+
+ return Mono
11+
+ .error(new IllegalStateException("Client must be initialized. Call the initialize method first!"));
12+
+ }
13+
+ if (this.clientCapabilities.roots() == null) {
14+
+ return Mono.error(new IllegalStateException("Client must be configured with roots capabilities"));
15+
+ }
16+
return this.session.sendRequest(McpSchema.METHOD_ROOTS_LIST, new McpSchema.PaginatedRequest(cursor),
17+
LIST_ROOTS_RESULT_TYPE_REF);
18+
}
19+
diff --git a/mcp-core/src/test/java/io/modelcontextprotocol/server/McpAsyncServerExchangeTests.java b/mcp-core/src/test/java/io/modelcontextprotocol/server/McpAsyncServerExchangeTests.java
20+
index f4f76b1..b556a8a 100644
21+
--- a/mcp-core/src/test/java/io/modelcontextprotocol/server/McpAsyncServerExchangeTests.java
22+
+++ b/mcp-core/src/test/java/io/modelcontextprotocol/server/McpAsyncServerExchangeTests.java
23+
@@ -157,6 +157,54 @@ class McpAsyncServerExchangeTests {
24+
}).verifyComplete();
25+
}
26+
27+
+ @Test
28+
+ void testListRootsWithNullCapabilities() {
29+
+
30+
+ McpAsyncServerExchange exchangeWithNullCapabilities = new McpAsyncServerExchange("testSessionId",
31+
+ mockSession, null, clientInfo, McpTransportContext.EMPTY);
32+
+
33+
+ StepVerifier.create(exchangeWithNullCapabilities.listRoots()).verifyErrorSatisfies(error -> {
34+
+ assertThat(error).isInstanceOf(IllegalStateException.class)
35+
+ .hasMessage("Client must be initialized. Call the initialize method first!");
36+
+ });
37+
+
38+
+ // Verify that sendRequest was never called due to null capabilities
39+
+ verify(mockSession, never()).sendRequest(eq(McpSchema.METHOD_ROOTS_LIST), any(), any(TypeRef.class));
40+
+ }
41+
+
42+
+ @Test
43+
+ void testListRootsWithoutRootsCapabilities() {
44+
+
45+
+ McpSchema.ClientCapabilities capabilitiesWithoutRoots = McpSchema.ClientCapabilities.builder()
46+
+ .sampling()
47+
+ .build();
48+
+
49+
+ McpAsyncServerExchange exchangeWithoutRoots = new McpAsyncServerExchange("testSessionId", mockSession,
50+
+ capabilitiesWithoutRoots, clientInfo, McpTransportContext.EMPTY);
51+
+
52+
+ StepVerifier.create(exchangeWithoutRoots.listRoots()).verifyErrorSatisfies(error -> {
53+
+ assertThat(error).isInstanceOf(IllegalStateException.class)
54+
+ .hasMessage("Client must be configured with roots capabilities");
55+
+ });
56+
+
57+
+ // Verify that sendRequest was never called due to missing roots capabilities
58+
+ verify(mockSession, never()).sendRequest(eq(McpSchema.METHOD_ROOTS_LIST), any(), any(TypeRef.class));
59+
+ }
60+
+
61+
+ @Test
62+
+ void testListRootsWithSpecificCursorAndNullCapabilities() {
63+
+
64+
+ McpAsyncServerExchange exchangeWithNullCapabilities = new McpAsyncServerExchange("testSessionId",
65+
+ mockSession, null, clientInfo, McpTransportContext.EMPTY);
66+
+
67+
+ StepVerifier.create(exchangeWithNullCapabilities.listRoots("someCursor")).verifyErrorSatisfies(error -> {
68+
+ assertThat(error).isInstanceOf(IllegalStateException.class)
69+
+ .hasMessage("Client must be initialized. Call the initialize method first!");
70+
+ });
71+
+
72+
+ verify(mockSession, never()).sendRequest(eq(McpSchema.METHOD_ROOTS_LIST), any(), any(TypeRef.class));
73+
+ }
74+
+
75+
@Test
76+
void testListRootsWithError() {
77+

‎mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServerExchange.java‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,13 @@ public Mono<McpSchema.ListRootsResult> listRoots() {
225225
* @return A Mono that emits the list of roots result containing
226226
*/
227227
public Mono<McpSchema.ListRootsResult> listRoots(String cursor) {
228+
if (this.clientCapabilities == null) {
229+
return Mono
230+
.error(new IllegalStateException("Client must be initialized. Call the initialize method first!"));
231+
}
232+
if (this.clientCapabilities.roots() == null) {
233+
return Mono.error(new IllegalStateException("Client must be configured with roots capabilities"));
234+
}
228235
return this.session.sendRequest(McpSchema.METHOD_ROOTS_LIST, new McpSchema.PaginatedRequest(cursor),
229236
LIST_ROOTS_RESULT_TYPE_REF);
230237
}

‎mcp-core/src/test/java/io/modelcontextprotocol/server/McpAsyncServerExchangeTests.java‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,54 @@ void testListRootsWithSpecificCursor() {
157157
}).verifyComplete();
158158
}
159159

160+
@Test
161+
void testListRootsWithNullCapabilities() {
162+
163+
McpAsyncServerExchange exchangeWithNullCapabilities = new McpAsyncServerExchange("testSessionId",
164+
mockSession, null, clientInfo, McpTransportContext.EMPTY);
165+
166+
StepVerifier.create(exchangeWithNullCapabilities.listRoots()).verifyErrorSatisfies(error -> {
167+
assertThat(error).isInstanceOf(IllegalStateException.class)
168+
.hasMessage("Client must be initialized. Call the initialize method first!");
169+
});
170+
171+
// Verify that sendRequest was never called due to null capabilities
172+
verify(mockSession, never()).sendRequest(eq(McpSchema.METHOD_ROOTS_LIST), any(), any(TypeRef.class));
173+
}
174+
175+
@Test
176+
void testListRootsWithoutRootsCapabilities() {
177+
178+
McpSchema.ClientCapabilities capabilitiesWithoutRoots = McpSchema.ClientCapabilities.builder()
179+
.sampling()
180+
.build();
181+
182+
McpAsyncServerExchange exchangeWithoutRoots = new McpAsyncServerExchange("testSessionId", mockSession,
183+
capabilitiesWithoutRoots, clientInfo, McpTransportContext.EMPTY);
184+
185+
StepVerifier.create(exchangeWithoutRoots.listRoots()).verifyErrorSatisfies(error -> {
186+
assertThat(error).isInstanceOf(IllegalStateException.class)
187+
.hasMessage("Client must be configured with roots capabilities");
188+
});
189+
190+
// Verify that sendRequest was never called due to missing roots capabilities
191+
verify(mockSession, never()).sendRequest(eq(McpSchema.METHOD_ROOTS_LIST), any(), any(TypeRef.class));
192+
}
193+
194+
@Test
195+
void testListRootsWithSpecificCursorAndNullCapabilities() {
196+
197+
McpAsyncServerExchange exchangeWithNullCapabilities = new McpAsyncServerExchange("testSessionId",
198+
mockSession, null, clientInfo, McpTransportContext.EMPTY);
199+
200+
StepVerifier.create(exchangeWithNullCapabilities.listRoots("someCursor")).verifyErrorSatisfies(error -> {
201+
assertThat(error).isInstanceOf(IllegalStateException.class)
202+
.hasMessage("Client must be initialized. Call the initialize method first!");
203+
});
204+
205+
verify(mockSession, never()).sendRequest(eq(McpSchema.METHOD_ROOTS_LIST), any(), any(TypeRef.class));
206+
}
207+
160208
@Test
161209
void testListRootsWithError() {
162210

0 commit comments

Comments
 (0)