Skip to content

Commit 901175e

Browse files
committed
refactor: deprecate tool spec's arguments-only handler in favor of CallToolRequest (#375)
Tool handlers now receive CallToolRequest instead of raw arguments to enable access to additional metadata parameters like progressToken - Deprecate handlers that take only Map<String, Object> arguments - Introduce new handlers that receive the full CallToolRequest object - Maintain backward compatibility with deprecated call handlers - Enhance API to provide access to complete tool request context beyond just arguments - Add builder pattern for AsyncToolSpecification and SyncToolSpecification - Add duplicate tool name validation during server building and runtime registration - Update all test files to use new callTool handlers and builder pattern - Add test coverage for new builder functionality and CallToolRequest handling Signed-off-by: Christian Tzolov <[email protected]>
1 parent 95df67e commit 901175e

File tree

14 files changed

+1687
-692
lines changed

14 files changed

+1687
-692
lines changed

mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/WebFluxSseIntegrationTests.java

Lines changed: 206 additions & 165 deletions
Large diffs are not rendered by default.

mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseIntegrationTests.java

Lines changed: 198 additions & 176 deletions
Large diffs are not rendered by default.

mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerTests.java

Lines changed: 97 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@
3434
*
3535
* @author Christian Tzolov
3636
*/
37-
// KEEP IN SYNC with the class in mcp-test module
3837
public abstract class AbstractMcpAsyncServerTests {
3938

4039
private static final String TEST_TOOL_NAME = "test-tool";
@@ -102,6 +101,7 @@ void testImmediateClose() {
102101
""";
103102

104103
@Test
104+
@Deprecated
105105
void testAddTool() {
106106
Tool newTool = new McpSchema.Tool("new-tool", "New test tool", emptyJsonSchema);
107107
var mcpAsyncServer = McpServer.async(createMcpTransportProvider())
@@ -117,6 +117,23 @@ void testAddTool() {
117117
}
118118

119119
@Test
120+
void testAddToolCall() {
121+
Tool newTool = new McpSchema.Tool("new-tool", "New test tool", emptyJsonSchema);
122+
var mcpAsyncServer = McpServer.async(createMcpTransportProvider())
123+
.serverInfo("test-server", "1.0.0")
124+
.capabilities(ServerCapabilities.builder().tools(true).build())
125+
.build();
126+
127+
StepVerifier.create(mcpAsyncServer.addTool(McpServerFeatures.AsyncToolSpecification.builder()
128+
.tool(newTool)
129+
.callHandler((exchange, request) -> Mono.just(new CallToolResult(List.of(), false)))
130+
.build())).verifyComplete();
131+
132+
assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException();
133+
}
134+
135+
@Test
136+
@Deprecated
120137
void testAddDuplicateTool() {
121138
Tool duplicateTool = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema);
122139

@@ -137,14 +154,91 @@ void testAddDuplicateTool() {
137154
assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException();
138155
}
139156

157+
@Test
158+
void testAddDuplicateToolCall() {
159+
Tool duplicateTool = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema);
160+
161+
var mcpAsyncServer = McpServer.async(createMcpTransportProvider())
162+
.serverInfo("test-server", "1.0.0")
163+
.capabilities(ServerCapabilities.builder().tools(true).build())
164+
.toolCall(duplicateTool, (exchange, request) -> Mono.just(new CallToolResult(List.of(), false)))
165+
.build();
166+
167+
StepVerifier.create(mcpAsyncServer.addTool(McpServerFeatures.AsyncToolSpecification.builder()
168+
.tool(duplicateTool)
169+
.callHandler((exchange, request) -> Mono.just(new CallToolResult(List.of(), false)))
170+
.build())).verifyErrorSatisfies(error -> {
171+
assertThat(error).isInstanceOf(McpError.class)
172+
.hasMessage("Tool with name '" + TEST_TOOL_NAME + "' already exists");
173+
});
174+
175+
assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException();
176+
}
177+
178+
@Test
179+
void testDuplicateToolCallDuringBuilding() {
180+
Tool duplicateTool = new Tool("duplicate-build-toolcall", "Duplicate toolcall during building",
181+
emptyJsonSchema);
182+
183+
assertThatThrownBy(() -> McpServer.async(createMcpTransportProvider())
184+
.serverInfo("test-server", "1.0.0")
185+
.capabilities(ServerCapabilities.builder().tools(true).build())
186+
.toolCall(duplicateTool, (exchange, request) -> Mono.just(new CallToolResult(List.of(), false)))
187+
.toolCall(duplicateTool, (exchange, request) -> Mono.just(new CallToolResult(List.of(), false))) // Duplicate!
188+
.build()).isInstanceOf(IllegalArgumentException.class)
189+
.hasMessage("Tool with name 'duplicate-build-toolcall' is already registered.");
190+
}
191+
192+
@Test
193+
void testDuplicateToolsInBatchListRegistration() {
194+
Tool duplicateTool = new Tool("batch-list-tool", "Duplicate tool in batch list", emptyJsonSchema);
195+
List<McpServerFeatures.AsyncToolSpecification> specs = List.of(
196+
McpServerFeatures.AsyncToolSpecification.builder()
197+
.tool(duplicateTool)
198+
.callHandler((exchange, request) -> Mono.just(new CallToolResult(List.of(), false)))
199+
.build(),
200+
McpServerFeatures.AsyncToolSpecification.builder()
201+
.tool(duplicateTool)
202+
.callHandler((exchange, request) -> Mono.just(new CallToolResult(List.of(), false)))
203+
.build() // Duplicate!
204+
);
205+
206+
assertThatThrownBy(() -> McpServer.async(createMcpTransportProvider())
207+
.serverInfo("test-server", "1.0.0")
208+
.capabilities(ServerCapabilities.builder().tools(true).build())
209+
.tools(specs)
210+
.build()).isInstanceOf(IllegalArgumentException.class)
211+
.hasMessage("Tool with name 'batch-list-tool' is already registered.");
212+
}
213+
214+
@Test
215+
void testDuplicateToolsInBatchVarargsRegistration() {
216+
Tool duplicateTool = new Tool("batch-varargs-tool", "Duplicate tool in batch varargs", emptyJsonSchema);
217+
218+
assertThatThrownBy(() -> McpServer.async(createMcpTransportProvider())
219+
.serverInfo("test-server", "1.0.0")
220+
.capabilities(ServerCapabilities.builder().tools(true).build())
221+
.tools(McpServerFeatures.AsyncToolSpecification.builder()
222+
.tool(duplicateTool)
223+
.callHandler((exchange, request) -> Mono.just(new CallToolResult(List.of(), false)))
224+
.build(),
225+
McpServerFeatures.AsyncToolSpecification.builder()
226+
.tool(duplicateTool)
227+
.callHandler((exchange, request) -> Mono.just(new CallToolResult(List.of(), false)))
228+
.build() // Duplicate!
229+
)
230+
.build()).isInstanceOf(IllegalArgumentException.class)
231+
.hasMessage("Tool with name 'batch-varargs-tool' is already registered.");
232+
}
233+
140234
@Test
141235
void testRemoveTool() {
142236
Tool too = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema);
143237

144238
var mcpAsyncServer = McpServer.async(createMcpTransportProvider())
145239
.serverInfo("test-server", "1.0.0")
146240
.capabilities(ServerCapabilities.builder().tools(true).build())
147-
.tool(too, (exchange, args) -> Mono.just(new CallToolResult(List.of(), false)))
241+
.toolCall(too, (exchange, request) -> Mono.just(new CallToolResult(List.of(), false)))
148242
.build();
149243

150244
StepVerifier.create(mcpAsyncServer.removeTool(TEST_TOOL_NAME)).verifyComplete();
@@ -173,7 +267,7 @@ void testNotifyToolsListChanged() {
173267
var mcpAsyncServer = McpServer.async(createMcpTransportProvider())
174268
.serverInfo("test-server", "1.0.0")
175269
.capabilities(ServerCapabilities.builder().tools(true).build())
176-
.tool(too, (exchange, args) -> Mono.just(new CallToolResult(List.of(), false)))
270+
.toolCall(too, (exchange, args) -> Mono.just(new CallToolResult(List.of(), false)))
177271
.build();
178272

179273
StepVerifier.create(mcpAsyncServer.notifyToolsListChanged()).verifyComplete();

mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpSyncServerTests.java

Lines changed: 103 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,16 @@
44

55
package io.modelcontextprotocol.server;
66

7+
import static org.assertj.core.api.Assertions.assertThat;
8+
import static org.assertj.core.api.Assertions.assertThatCode;
9+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
10+
711
import java.util.List;
812

13+
import org.junit.jupiter.api.AfterEach;
14+
import org.junit.jupiter.api.BeforeEach;
15+
import org.junit.jupiter.api.Test;
16+
917
import io.modelcontextprotocol.spec.McpError;
1018
import io.modelcontextprotocol.spec.McpSchema;
1119
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
@@ -17,21 +25,13 @@
1725
import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities;
1826
import io.modelcontextprotocol.spec.McpSchema.Tool;
1927
import io.modelcontextprotocol.spec.McpServerTransportProvider;
20-
import org.junit.jupiter.api.AfterEach;
21-
import org.junit.jupiter.api.BeforeEach;
22-
import org.junit.jupiter.api.Test;
23-
24-
import static org.assertj.core.api.Assertions.assertThat;
25-
import static org.assertj.core.api.Assertions.assertThatCode;
26-
import static org.assertj.core.api.Assertions.assertThatThrownBy;
2728

2829
/**
2930
* Test suite for the {@link McpSyncServer} that can be used with different
30-
* {@link io.modelcontextprotocol.spec.McpServerTransportProvider} implementations.
31+
* {@link McpTransportProvider} implementations.
3132
*
3233
* @author Christian Tzolov
3334
*/
34-
// KEEP IN SYNC with the class in mcp-test module
3535
public abstract class AbstractMcpSyncServerTests {
3636

3737
private static final String TEST_TOOL_NAME = "test-tool";
@@ -109,6 +109,7 @@ void testGetAsyncServer() {
109109
""";
110110

111111
@Test
112+
@Deprecated
112113
void testAddTool() {
113114
var mcpSyncServer = McpServer.sync(createMcpTransportProvider())
114115
.serverInfo("test-server", "1.0.0")
@@ -124,6 +125,23 @@ void testAddTool() {
124125
}
125126

126127
@Test
128+
void testAddToolCall() {
129+
var mcpSyncServer = McpServer.sync(createMcpTransportProvider())
130+
.serverInfo("test-server", "1.0.0")
131+
.capabilities(ServerCapabilities.builder().tools(true).build())
132+
.build();
133+
134+
Tool newTool = new McpSchema.Tool("new-tool", "New test tool", emptyJsonSchema);
135+
assertThatCode(() -> mcpSyncServer.addTool(McpServerFeatures.SyncToolSpecification.builder()
136+
.tool(newTool)
137+
.callHandler((exchange, request) -> new CallToolResult(List.of(), false))
138+
.build())).doesNotThrowAnyException();
139+
140+
assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException();
141+
}
142+
143+
@Test
144+
@Deprecated
127145
void testAddDuplicateTool() {
128146
Tool duplicateTool = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema);
129147

@@ -141,14 +159,89 @@ void testAddDuplicateTool() {
141159
assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException();
142160
}
143161

162+
@Test
163+
void testAddDuplicateToolCall() {
164+
Tool duplicateTool = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema);
165+
166+
var mcpSyncServer = McpServer.sync(createMcpTransportProvider())
167+
.serverInfo("test-server", "1.0.0")
168+
.capabilities(ServerCapabilities.builder().tools(true).build())
169+
.toolCall(duplicateTool, (exchange, request) -> new CallToolResult(List.of(), false))
170+
.build();
171+
172+
assertThatThrownBy(() -> mcpSyncServer.addTool(McpServerFeatures.SyncToolSpecification.builder()
173+
.tool(duplicateTool)
174+
.callHandler((exchange, request) -> new CallToolResult(List.of(), false))
175+
.build())).isInstanceOf(McpError.class)
176+
.hasMessage("Tool with name '" + TEST_TOOL_NAME + "' already exists");
177+
178+
assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException();
179+
}
180+
181+
@Test
182+
void testDuplicateToolCallDuringBuilding() {
183+
Tool duplicateTool = new Tool("duplicate-build-toolcall", "Duplicate toolcall during building",
184+
emptyJsonSchema);
185+
186+
assertThatThrownBy(() -> McpServer.sync(createMcpTransportProvider())
187+
.serverInfo("test-server", "1.0.0")
188+
.capabilities(ServerCapabilities.builder().tools(true).build())
189+
.toolCall(duplicateTool, (exchange, request) -> new CallToolResult(List.of(), false))
190+
.toolCall(duplicateTool, (exchange, request) -> new CallToolResult(List.of(), false)) // Duplicate!
191+
.build()).isInstanceOf(IllegalArgumentException.class)
192+
.hasMessage("Tool with name 'duplicate-build-toolcall' is already registered.");
193+
}
194+
195+
@Test
196+
void testDuplicateToolsInBatchListRegistration() {
197+
Tool duplicateTool = new Tool("batch-list-tool", "Duplicate tool in batch list", emptyJsonSchema);
198+
List<McpServerFeatures.SyncToolSpecification> specs = List.of(
199+
McpServerFeatures.SyncToolSpecification.builder()
200+
.tool(duplicateTool)
201+
.callHandler((exchange, request) -> new CallToolResult(List.of(), false))
202+
.build(),
203+
McpServerFeatures.SyncToolSpecification.builder()
204+
.tool(duplicateTool)
205+
.callHandler((exchange, request) -> new CallToolResult(List.of(), false))
206+
.build() // Duplicate!
207+
);
208+
209+
assertThatThrownBy(() -> McpServer.sync(createMcpTransportProvider())
210+
.serverInfo("test-server", "1.0.0")
211+
.capabilities(ServerCapabilities.builder().tools(true).build())
212+
.tools(specs)
213+
.build()).isInstanceOf(IllegalArgumentException.class)
214+
.hasMessage("Tool with name 'batch-list-tool' is already registered.");
215+
}
216+
217+
@Test
218+
void testDuplicateToolsInBatchVarargsRegistration() {
219+
Tool duplicateTool = new Tool("batch-varargs-tool", "Duplicate tool in batch varargs", emptyJsonSchema);
220+
221+
assertThatThrownBy(() -> McpServer.sync(createMcpTransportProvider())
222+
.serverInfo("test-server", "1.0.0")
223+
.capabilities(ServerCapabilities.builder().tools(true).build())
224+
.tools(McpServerFeatures.SyncToolSpecification.builder()
225+
.tool(duplicateTool)
226+
.callHandler((exchange, request) -> new CallToolResult(List.of(), false))
227+
.build(),
228+
McpServerFeatures.SyncToolSpecification.builder()
229+
.tool(duplicateTool)
230+
.callHandler((exchange, request) -> new CallToolResult(List.of(), false))
231+
.build() // Duplicate!
232+
)
233+
.build()).isInstanceOf(IllegalArgumentException.class)
234+
.hasMessage("Tool with name 'batch-varargs-tool' is already registered.");
235+
}
236+
144237
@Test
145238
void testRemoveTool() {
146239
Tool tool = new McpSchema.Tool(TEST_TOOL_NAME, "Test tool", emptyJsonSchema);
147240

148241
var mcpSyncServer = McpServer.sync(createMcpTransportProvider())
149242
.serverInfo("test-server", "1.0.0")
150243
.capabilities(ServerCapabilities.builder().tools(true).build())
151-
.tool(tool, (exchange, args) -> new CallToolResult(List.of(), false))
244+
.toolCall(tool, (exchange, args) -> new CallToolResult(List.of(), false))
152245
.build();
153246

154247
assertThatCode(() -> mcpSyncServer.removeTool(TEST_TOOL_NAME)).doesNotThrowAnyException();

mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ private McpServerSession.NotificationHandler asyncRootsListChangedNotificationHa
268268
// ---------------------------------------
269269

270270
/**
271-
* Add a new tool specification at runtime.
271+
* Add a new tool call specification at runtime.
272272
* @param toolSpecification The tool specification to add
273273
* @return Mono that completes when clients have been notified of the change
274274
*/
@@ -279,7 +279,7 @@ public Mono<Void> addTool(McpServerFeatures.AsyncToolSpecification toolSpecifica
279279
if (toolSpecification.tool() == null) {
280280
return Mono.error(new McpError("Tool must not be null"));
281281
}
282-
if (toolSpecification.call() == null) {
282+
if (toolSpecification.call() == null && toolSpecification.callHandler() == null) {
283283
return Mono.error(new McpError("Tool call handler must not be null"));
284284
}
285285
if (this.serverCapabilities.tools() == null) {
@@ -360,7 +360,7 @@ private McpServerSession.RequestHandler<CallToolResult> toolsCallRequestHandler(
360360
return Mono.error(new McpError("Tool not found: " + callToolRequest.name()));
361361
}
362362

363-
return toolSpecification.map(tool -> tool.call().apply(exchange, callToolRequest.arguments()))
363+
return toolSpecification.map(tool -> tool.callHandler().apply(exchange, callToolRequest))
364364
.orElse(Mono.error(new McpError("Tool not found: " + callToolRequest.name())));
365365
};
366366
}

0 commit comments

Comments
 (0)