Skip to content

Commit 364fac2

Browse files
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
1 parent fd00498 commit 364fac2

4 files changed

Lines changed: 78 additions & 6 deletions

File tree

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: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,40 @@ void testListRootsWithError() {
170170
});
171171
}
172172

173+
@Test
174+
void testListRootsWithNullCapabilities() {
175+
// Given - Create exchange with null capabilities
176+
McpAsyncServerExchange exchangeWithNullCapabilities = new McpAsyncServerExchange("testSessionId", mockSession,
177+
null, clientInfo, McpTransportContext.EMPTY);
178+
179+
StepVerifier.create(exchangeWithNullCapabilities.listRoots()).verifyErrorSatisfies(error -> {
180+
assertThat(error).isInstanceOf(IllegalStateException.class)
181+
.hasMessage("Client must be initialized. Call the initialize method first!");
182+
});
183+
184+
// Verify that sendRequest was never called due to null capabilities
185+
verify(mockSession, never()).sendRequest(eq(McpSchema.METHOD_ROOTS_LIST), any(), any(TypeRef.class));
186+
}
187+
188+
@Test
189+
void testListRootsWithoutRootsCapabilities() {
190+
// Given - Create exchange without roots capabilities
191+
McpSchema.ClientCapabilities capabilitiesWithoutRoots = McpSchema.ClientCapabilities.builder()
192+
.sampling()
193+
.build();
194+
195+
McpAsyncServerExchange exchangeWithoutRoots = new McpAsyncServerExchange("testSessionId", mockSession,
196+
capabilitiesWithoutRoots, clientInfo, McpTransportContext.EMPTY);
197+
198+
StepVerifier.create(exchangeWithoutRoots.listRoots()).verifyErrorSatisfies(error -> {
199+
assertThat(error).isInstanceOf(IllegalStateException.class)
200+
.hasMessage("Client must be configured with roots capabilities");
201+
});
202+
203+
// Verify that sendRequest was never called due to missing roots capabilities
204+
verify(mockSession, never()).sendRequest(eq(McpSchema.METHOD_ROOTS_LIST), any(), any(TypeRef.class));
205+
}
206+
173207
@Test
174208
void testListRootsUnmodifiabilityAfterAccumulation() {
175209

mcp-core/src/test/java/io/modelcontextprotocol/server/McpSyncServerExchangeTests.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,39 @@ void testListRootsWithError() {
169169
assertThatThrownBy(() -> exchange.listRoots()).isInstanceOf(RuntimeException.class).hasMessage("Network error");
170170
}
171171

172+
@Test
173+
void testListRootsWithNullCapabilities() {
174+
// Given - Create exchange with null capabilities
175+
McpAsyncServerExchange asyncExchangeWithNullCapabilities = new McpAsyncServerExchange("testSessionId",
176+
mockSession, null, clientInfo, McpTransportContext.EMPTY);
177+
McpSyncServerExchange exchangeWithNullCapabilities = new McpSyncServerExchange(
178+
asyncExchangeWithNullCapabilities);
179+
180+
assertThatThrownBy(() -> exchangeWithNullCapabilities.listRoots()).isInstanceOf(IllegalStateException.class)
181+
.hasMessage("Client must be initialized. Call the initialize method first!");
182+
183+
// Verify that sendRequest was never called due to null capabilities
184+
verify(mockSession, never()).sendRequest(eq(McpSchema.METHOD_ROOTS_LIST), any(), any(TypeRef.class));
185+
}
186+
187+
@Test
188+
void testListRootsWithoutRootsCapabilities() {
189+
// Given - Create exchange without roots capabilities
190+
McpSchema.ClientCapabilities capabilitiesWithoutRoots = McpSchema.ClientCapabilities.builder()
191+
.sampling()
192+
.build();
193+
194+
McpAsyncServerExchange asyncExchangeWithoutRoots = new McpAsyncServerExchange("testSessionId", mockSession,
195+
capabilitiesWithoutRoots, clientInfo, McpTransportContext.EMPTY);
196+
McpSyncServerExchange exchangeWithoutRoots = new McpSyncServerExchange(asyncExchangeWithoutRoots);
197+
198+
assertThatThrownBy(() -> exchangeWithoutRoots.listRoots()).isInstanceOf(IllegalStateException.class)
199+
.hasMessage("Client must be configured with roots capabilities");
200+
201+
// Verify that sendRequest was never called due to missing roots capabilities
202+
verify(mockSession, never()).sendRequest(eq(McpSchema.METHOD_ROOTS_LIST), any(), any(TypeRef.class));
203+
}
204+
172205
@Test
173206
void testListRootsUnmodifiabilityAfterAccumulation() {
174207

mcp-test/src/main/java/io/modelcontextprotocol/AbstractMcpClientServerIntegrationTests.java

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1056,12 +1056,10 @@ void testRootsWithoutCapability() {
10561056
assertThat(mcpClient.initialize()).isNotNull();
10571057

10581058
// Attempt to list roots should fail
1059-
try {
1060-
mcpClient.callTool(McpSchema.CallToolRequest.builder("tool1").arguments(Map.of()).build());
1061-
}
1062-
catch (McpError e) {
1063-
assertThat(e).isInstanceOf(McpError.class).hasMessage("Roots not supported");
1064-
}
1059+
assertThatThrownBy(
1060+
() -> mcpClient.callTool(McpSchema.CallToolRequest.builder("tool1").arguments(Map.of()).build()))
1061+
.isInstanceOf(McpError.class)
1062+
.hasMessage("Client must be configured with roots capabilities");
10651063
}
10661064
finally {
10671065
mcpServer.closeGracefully();

0 commit comments

Comments
 (0)