Skip to content

Commit 1555952

Browse files
committed
Reject roots listing before sending unsupported client requests
1 parent 1cf7903 commit 1555952

4 files changed

Lines changed: 66 additions & 9 deletions

File tree

‎disclosure.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
This change was submitted despite me reading the rules and understanding AI contribution guidelines.

‎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 root listing 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: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
import io.modelcontextprotocol.spec.McpServerSession;
1818
import org.junit.jupiter.api.BeforeEach;
1919
import org.junit.jupiter.api.Test;
20+
import org.junit.jupiter.params.ParameterizedTest;
21+
import org.junit.jupiter.params.provider.ValueSource;
2022
import org.mockito.Mock;
2123
import org.mockito.MockitoAnnotations;
2224
import reactor.core.publisher.Mono;
@@ -29,6 +31,7 @@
2931
import static org.mockito.Mockito.never;
3032
import static org.mockito.Mockito.times;
3133
import static org.mockito.Mockito.verify;
34+
import static org.mockito.Mockito.verifyNoInteractions;
3235
import static org.mockito.Mockito.when;
3336

3437
/**
@@ -85,6 +88,54 @@ void testListRootsWithSinglePage() {
8588
}).verifyComplete();
8689
}
8790

91+
@ParameterizedTest
92+
@ValueSource(booleans = { false, true })
93+
void testListRootsWithoutCapabilities(boolean paginated) {
94+
exchange = new McpAsyncServerExchange("testSessionId", mockSession, null, clientInfo,
95+
McpTransportContext.EMPTY);
96+
when(mockSession.sendRequest(eq(McpSchema.METHOD_ROOTS_LIST), any(McpSchema.PaginatedRequest.class),
97+
any(TypeRef.class)))
98+
.thenReturn(Mono.just(McpSchema.ListRootsResult.builder(List.of()).build()));
99+
100+
Mono<McpSchema.ListRootsResult> result = paginated ? exchange.listRoots("cursor") : exchange.listRoots();
101+
verifyNoInteractions(mockSession);
102+
StepVerifier.create(result)
103+
.verifyErrorSatisfies(error -> assertThat(error).isInstanceOf(IllegalStateException.class)
104+
.hasMessage("Client must be initialized. Call the initialize method first!"));
105+
verifyNoInteractions(mockSession);
106+
}
107+
108+
@ParameterizedTest
109+
@ValueSource(booleans = { false, true })
110+
void testListRootsWithoutRootsCapability(boolean paginated) {
111+
exchange = new McpAsyncServerExchange("testSessionId", mockSession,
112+
McpSchema.ClientCapabilities.builder().build(), clientInfo, McpTransportContext.EMPTY);
113+
when(mockSession.sendRequest(eq(McpSchema.METHOD_ROOTS_LIST), any(McpSchema.PaginatedRequest.class),
114+
any(TypeRef.class)))
115+
.thenReturn(Mono.just(McpSchema.ListRootsResult.builder(List.of()).build()));
116+
117+
Mono<McpSchema.ListRootsResult> result = paginated ? exchange.listRoots("cursor") : exchange.listRoots();
118+
verifyNoInteractions(mockSession);
119+
StepVerifier.create(result)
120+
.verifyErrorSatisfies(error -> assertThat(error).isInstanceOf(IllegalStateException.class)
121+
.hasMessage("Client must be configured with root listing capabilities"));
122+
verifyNoInteractions(mockSession);
123+
}
124+
125+
@Test
126+
void testListRootsWithoutListChangedSupport() {
127+
exchange = new McpAsyncServerExchange("testSessionId", mockSession,
128+
McpSchema.ClientCapabilities.builder().roots(false).build(), clientInfo, McpTransportContext.EMPTY);
129+
McpSchema.ListRootsResult result = McpSchema.ListRootsResult.builder(List.of()).build();
130+
when(mockSession.sendRequest(eq(McpSchema.METHOD_ROOTS_LIST), eq(new McpSchema.PaginatedRequest("cursor")),
131+
any(TypeRef.class)))
132+
.thenReturn(Mono.just(result));
133+
134+
StepVerifier.create(exchange.listRoots("cursor")).expectNext(result).verifyComplete();
135+
verify(mockSession).sendRequest(eq(McpSchema.METHOD_ROOTS_LIST), eq(new McpSchema.PaginatedRequest("cursor")),
136+
any(TypeRef.class));
137+
}
138+
88139
@Test
89140
void testListRootsWithMultiplePages() {
90141

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

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1129,9 +1129,10 @@ void testRootsWithoutCapability(String clientType) {
11291129
.tool(Tool.builder("tool1", EMPTY_JSON_SCHEMA).description("tool1 description").build())
11301130
.callHandler((exchange, request) -> {
11311131

1132-
exchange.listRoots(); // try to list roots
1132+
assertThatThrownBy(exchange::listRoots).isInstanceOf(IllegalStateException.class)
1133+
.hasMessage("Client must be configured with root listing capabilities");
11331134

1134-
return mock(CallToolResult.class);
1135+
return CallToolResult.builder().addTextContent("Roots capability rejected").build();
11351136
})
11361137
.build();
11371138

@@ -1145,13 +1146,10 @@ void testRootsWithoutCapability(String clientType) {
11451146

11461147
assertThat(mcpClient.initialize()).isNotNull();
11471148

1148-
// Attempt to list roots should fail
1149-
try {
1150-
mcpClient.callTool(McpSchema.CallToolRequest.builder("tool1").arguments(Map.of()).build());
1151-
}
1152-
catch (McpError e) {
1153-
assertThat(e).isInstanceOf(McpError.class).hasMessage("Roots not supported");
1154-
}
1149+
CallToolResult result = mcpClient
1150+
.callTool(McpSchema.CallToolRequest.builder("tool1").arguments(Map.of()).build());
1151+
assertThat(result.isError()).isFalse();
1152+
assertThat(result.content()).containsExactly(new McpSchema.TextContent("Roots capability rejected"));
11551153
}
11561154
finally {
11571155
mcpServer.closeGracefully();

0 commit comments

Comments
 (0)