Skip to content

Commit b0121f1

Browse files
committed
Add bulk tool mutation handling tests for synchronous and parameterized scenarios
1 parent a4b1bcb commit b0121f1

2 files changed

Lines changed: 116 additions & 59 deletions

File tree

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

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import java.util.concurrent.CountDownLatch;
1919
import java.util.concurrent.TimeUnit;
2020
import java.util.concurrent.atomic.AtomicBoolean;
21+
import java.util.concurrent.atomic.AtomicInteger;
2122
import java.util.concurrent.atomic.AtomicReference;
2223
import java.util.function.BiFunction;
2324
import java.util.function.Function;
@@ -1069,6 +1070,90 @@ void testToolListChangeHandlingSuccess(String clientType) {
10691070
}
10701071
}
10711072

1073+
@ParameterizedTest(name = "{0} : {displayName} ")
1074+
@MethodSource("clientsForTesting")
1075+
void testBulkToolListChangeHandlingSuccess(String clientType) {
1076+
1077+
var clientBuilder = clientBuilders.get(clientType);
1078+
1079+
var callResponse = McpSchema.CallToolResult.builder()
1080+
.addContent(new McpSchema.TextContent("CALL RESPONSE"))
1081+
.build();
1082+
1083+
McpServerFeatures.SyncToolSpecification tool1 = McpServerFeatures.SyncToolSpecification.builder()
1084+
.tool(Tool.builder().name("bulk-tool-1").description("bulk tool 1").inputSchema(EMPTY_JSON_SCHEMA).build())
1085+
.callHandler((exchange, request) -> callResponse)
1086+
.build();
1087+
McpServerFeatures.SyncToolSpecification tool2 = McpServerFeatures.SyncToolSpecification.builder()
1088+
.tool(Tool.builder().name("bulk-tool-2").description("bulk tool 2").inputSchema(EMPTY_JSON_SCHEMA).build())
1089+
.callHandler((exchange, request) -> callResponse)
1090+
.build();
1091+
McpServerFeatures.SyncToolSpecification replacementTool2 = McpServerFeatures.SyncToolSpecification.builder()
1092+
.tool(Tool.builder()
1093+
.name("bulk-tool-2")
1094+
.title("Replacement Bulk Tool 2")
1095+
.description("replacement bulk tool 2")
1096+
.inputSchema(EMPTY_JSON_SCHEMA)
1097+
.build())
1098+
.callHandler((exchange, request) -> callResponse)
1099+
.build();
1100+
McpServerFeatures.SyncToolSpecification tool3 = McpServerFeatures.SyncToolSpecification.builder()
1101+
.tool(Tool.builder().name("bulk-tool-3").description("bulk tool 3").inputSchema(EMPTY_JSON_SCHEMA).build())
1102+
.callHandler((exchange, request) -> callResponse)
1103+
.build();
1104+
1105+
AtomicInteger notificationCount = new AtomicInteger();
1106+
AtomicReference<List<Tool>> toolsRef = new AtomicReference<>();
1107+
1108+
var mcpServer = prepareSyncServerBuilder().capabilities(ServerCapabilities.builder().tools(true).build())
1109+
.build();
1110+
1111+
try (var mcpClient = clientBuilder.toolsChangeConsumer(toolsUpdate -> {
1112+
toolsRef.set(toolsUpdate);
1113+
notificationCount.incrementAndGet();
1114+
}).build()) {
1115+
InitializeResult initResult = mcpClient.initialize();
1116+
assertThat(initResult).isNotNull();
1117+
assertThat(toolsRef.get()).isNull();
1118+
assertThat(notificationCount.get()).isZero();
1119+
assertThat(mcpClient.listTools().tools()).isEmpty();
1120+
1121+
mcpServer.addTools(List.of(tool1, tool2));
1122+
1123+
await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
1124+
assertThat(notificationCount.get()).isEqualTo(1);
1125+
assertThat(toolsRef.get()).extracting(Tool::name).containsExactly("bulk-tool-1", "bulk-tool-2");
1126+
});
1127+
assertThat(mcpClient.listTools().tools()).extracting(Tool::name)
1128+
.containsExactly("bulk-tool-1", "bulk-tool-2");
1129+
1130+
mcpServer.addTools(List.of(replacementTool2, tool3));
1131+
1132+
await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
1133+
assertThat(notificationCount.get()).isEqualTo(2);
1134+
List<Tool> tools = toolsRef.get();
1135+
assertThat(tools).extracting(Tool::name).containsExactly("bulk-tool-1", "bulk-tool-2", "bulk-tool-3");
1136+
Tool replacedTool = tools.stream()
1137+
.filter(tool -> "bulk-tool-2".equals(tool.name()))
1138+
.findFirst()
1139+
.orElseThrow();
1140+
assertThat(replacedTool.title()).isEqualTo("Replacement Bulk Tool 2");
1141+
});
1142+
1143+
mcpServer.removeTools(List.of("bulk-tool-1", "missing-tool"));
1144+
1145+
await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
1146+
assertThat(notificationCount.get()).isEqualTo(3);
1147+
assertThat(toolsRef.get()).extracting(Tool::name).containsExactly("bulk-tool-2", "bulk-tool-3");
1148+
});
1149+
assertThat(mcpClient.listTools().tools()).extracting(Tool::name)
1150+
.containsExactly("bulk-tool-2", "bulk-tool-3");
1151+
}
1152+
finally {
1153+
mcpServer.closeGracefully();
1154+
}
1155+
}
1156+
10721157
@ParameterizedTest(name = "{0} : {displayName} ")
10731158
@MethodSource("clientsForTesting")
10741159
void testInitialize(String clientType) {

mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletStatelessIntegrationTests.java

Lines changed: 31 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@
4646
import org.springframework.mock.web.MockHttpServletRequest;
4747
import org.springframework.mock.web.MockHttpServletResponse;
4848
import org.springframework.web.client.RestClient;
49-
import reactor.core.publisher.Mono;
5049

5150
import static io.modelcontextprotocol.server.transport.HttpServletStatelessServerTransport.APPLICATION_JSON;
5251
import static io.modelcontextprotocol.server.transport.HttpServletStatelessServerTransport.TEXT_EVENT_STREAM;
@@ -110,64 +109,49 @@ public void after() {
110109
// ---------------------------------------
111110
// Tools Tests
112111
// ---------------------------------------
113-
@Test
114-
void testStatelessAsyncBulkToolMutations() {
115-
var mcpServer = McpServer.async(mcpStatelessServerTransport)
116-
.capabilities(ServerCapabilities.builder().tools(true).build())
117-
.build();
118-
119-
mcpServer
120-
.addTools(List.of(asyncToolSpecification("duplicate-tool", "First tool"),
121-
asyncToolSpecification("middle-tool"), asyncToolSpecification("duplicate-tool", "Last tool")))
122-
.block();
123-
124-
List<Tool> tools = mcpServer.listTools().collectList().block();
125-
assertThat(tools).extracting(McpSchema.Tool::name).containsExactly("middle-tool", "duplicate-tool");
126-
assertThat(tools.get(1).title()).isEqualTo("Last tool");
127-
128-
mcpServer
129-
.addTools(List.of(asyncToolSpecification("middle-tool", "Replacement tool"),
130-
asyncToolSpecification("new-tool")))
131-
.block();
132-
133-
tools = mcpServer.listTools().collectList().block();
134-
assertThat(tools).extracting(McpSchema.Tool::name).containsExactly("duplicate-tool", "middle-tool", "new-tool");
135-
assertThat(tools.get(1).title()).isEqualTo("Replacement tool");
136-
137-
mcpServer.removeTools(List.of("duplicate-tool", "missing-tool")).block();
138-
139-
tools = mcpServer.listTools().collectList().block();
140-
assertThat(tools).extracting(McpSchema.Tool::name).containsExactly("middle-tool", "new-tool");
112+
@ParameterizedTest(name = "{0} : {displayName} ")
113+
@ValueSource(strings = { "httpclient" })
114+
void testStatelessSyncBulkToolMutations(String clientType) {
141115

142-
mcpServer.closeGracefully().block();
143-
}
116+
var clientBuilder = clientBuilders.get(clientType);
144117

145-
@Test
146-
void testStatelessSyncBulkToolMutations() {
147118
var mcpServer = McpServer.sync(mcpStatelessServerTransport)
148119
.capabilities(ServerCapabilities.builder().tools(true).build())
149120
.build();
150121

151-
mcpServer.addTools(List.of(syncToolSpecification("duplicate-tool", "First tool"),
152-
syncToolSpecification("middle-tool"), syncToolSpecification("duplicate-tool", "Last tool")));
122+
try (var mcpClient = clientBuilder.build()) {
123+
InitializeResult initResult = mcpClient.initialize();
124+
assertThat(initResult).isNotNull();
125+
assertThat(mcpClient.listTools().tools()).isEmpty();
153126

154-
List<Tool> tools = mcpServer.listTools();
155-
assertThat(tools).extracting(McpSchema.Tool::name).containsExactly("middle-tool", "duplicate-tool");
156-
assertThat(tools.get(1).title()).isEqualTo("Last tool");
127+
mcpServer.addTools(List.of(syncToolSpecification("duplicate-tool", "First tool"),
128+
syncToolSpecification("middle-tool"), syncToolSpecification("duplicate-tool", "Last tool")));
157129

158-
mcpServer.addTools(
159-
List.of(syncToolSpecification("middle-tool", "Replacement tool"), syncToolSpecification("new-tool")));
130+
await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
131+
List<Tool> tools = mcpClient.listTools().tools();
132+
assertThat(tools).extracting(McpSchema.Tool::name).containsExactly("middle-tool", "duplicate-tool");
133+
assertThat(tools.get(1).title()).isEqualTo("Last tool");
134+
});
160135

161-
tools = mcpServer.listTools();
162-
assertThat(tools).extracting(McpSchema.Tool::name).containsExactly("duplicate-tool", "middle-tool", "new-tool");
163-
assertThat(tools.get(1).title()).isEqualTo("Replacement tool");
136+
mcpServer.addTools(List.of(syncToolSpecification("middle-tool", "Replacement tool"),
137+
syncToolSpecification("new-tool")));
164138

165-
mcpServer.removeTools(List.of("duplicate-tool", "missing-tool"));
139+
await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
140+
List<Tool> tools = mcpClient.listTools().tools();
141+
assertThat(tools).extracting(McpSchema.Tool::name)
142+
.containsExactly("duplicate-tool", "middle-tool", "new-tool");
143+
assertThat(tools.get(1).title()).isEqualTo("Replacement tool");
144+
});
166145

167-
tools = mcpServer.listTools();
168-
assertThat(tools).extracting(McpSchema.Tool::name).containsExactly("middle-tool", "new-tool");
146+
mcpServer.removeTools(List.of("duplicate-tool", "missing-tool"));
169147

170-
mcpServer.closeGracefully().block();
148+
await().atMost(Duration.ofSeconds(5))
149+
.untilAsserted(() -> assertThat(mcpClient.listTools().tools()).extracting(McpSchema.Tool::name)
150+
.containsExactly("middle-tool", "new-tool"));
151+
}
152+
finally {
153+
mcpServer.closeGracefully().block();
154+
}
171155
}
172156

173157
@ParameterizedTest(name = "{0} : {displayName} ")
@@ -712,18 +696,6 @@ private double evaluateExpression(String expression) {
712696
};
713697
}
714698

715-
private McpStatelessServerFeatures.AsyncToolSpecification asyncToolSpecification(String name) {
716-
return asyncToolSpecification(name, name);
717-
}
718-
719-
private McpStatelessServerFeatures.AsyncToolSpecification asyncToolSpecification(String name, String title) {
720-
return McpStatelessServerFeatures.AsyncToolSpecification.builder()
721-
.tool(McpSchema.Tool.builder().name(name).title(title).inputSchema(EMPTY_JSON_SCHEMA).build())
722-
.callHandler(
723-
(context, request) -> Mono.just(CallToolResult.builder().content(List.of()).isError(false).build()))
724-
.build();
725-
}
726-
727699
private McpStatelessServerFeatures.SyncToolSpecification syncToolSpecification(String name) {
728700
return syncToolSpecification(name, name);
729701
}

0 commit comments

Comments
 (0)