Skip to content

feat: refactor the tool specifications #351

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ void testImmediateClose() {
""";

@Test
@Deprecated
void testAddTool() {
Tool newTool = new McpSchema.Tool("new-tool", "New test tool", emptyJsonSchema);
var mcpAsyncServer = McpServer.async(createMcpTransportProvider())
Expand All @@ -116,6 +117,21 @@ void testAddTool() {
assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException();
}

@Test
void testAddToolCall() {
Tool newTool = new McpSchema.Tool("new-tool", "New test tool", emptyJsonSchema);
var mcpAsyncServer = McpServer.async(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().tools(true).build())
.build();

StepVerifier.create(mcpAsyncServer.addTool(new McpServerFeatures.AsyncToolCallSpecification(newTool,
(exchange, request) -> Mono.just(new CallToolResult(List.of(), false)))))
.verifyComplete();

assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException();
}

@Test
void testAddDuplicateTool() {
Tool duplicateTool = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ void testGetAsyncServer() {
""";

@Test
@Deprecated
void testAddTool() {
var mcpSyncServer = McpServer.sync(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
Expand All @@ -123,6 +124,21 @@ void testAddTool() {
assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException();
}

@Test
void testAddToolCall() {
var mcpSyncServer = McpServer.sync(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().tools(true).build())
.build();

Tool newTool = new McpSchema.Tool("new-tool", "New test tool", emptyJsonSchema);
assertThatCode(() -> mcpSyncServer.addTool(new McpServerFeatures.SyncToolCallSpecification(newTool,
(exchange, request) -> new CallToolResult(List.of(), false))))
.doesNotThrowAnyException();

assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException();
}

@Test
void testAddDuplicateTool() {
Tool duplicateTool = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public class McpAsyncServer {

private final String instructions;

private final CopyOnWriteArrayList<McpServerFeatures.AsyncToolSpecification> tools = new CopyOnWriteArrayList<>();
private final CopyOnWriteArrayList<McpServerFeatures.AsyncToolCallSpecification> tools = new CopyOnWriteArrayList<>();

private final CopyOnWriteArrayList<McpSchema.ResourceTemplate> resourceTemplates = new CopyOnWriteArrayList<>();

Expand Down Expand Up @@ -271,15 +271,27 @@ private McpServerSession.NotificationHandler asyncRootsListChangedNotificationHa
* Add a new tool specification at runtime.
* @param toolSpecification The tool specification to add
* @return Mono that completes when clients have been notified of the change
* @deprecated Use {@link #addTool(McpServerFeatures.AsyncToolCallSpecification)}
* instead.
*/
@Deprecated
public Mono<Void> addTool(McpServerFeatures.AsyncToolSpecification toolSpecification) {
if (toolSpecification == null) {
return addTool(toolSpecification.toToolCall());
}

/**
* Add a new tool call specification at runtime.
* @param toolCallSpecification The tool specification to add
* @return Mono that completes when clients have been notified of the change
*/
public Mono<Void> addTool(McpServerFeatures.AsyncToolCallSpecification toolCallSpecification) {
if (toolCallSpecification == null) {
return Mono.error(new McpError("Tool specification must not be null"));
}
if (toolSpecification.tool() == null) {
if (toolCallSpecification.tool() == null) {
return Mono.error(new McpError("Tool must not be null"));
}
if (toolSpecification.call() == null) {
if (toolCallSpecification.call() == null) {
return Mono.error(new McpError("Tool call handler must not be null"));
}
if (this.serverCapabilities.tools() == null) {
Expand All @@ -288,13 +300,13 @@ public Mono<Void> addTool(McpServerFeatures.AsyncToolSpecification toolSpecifica

return Mono.defer(() -> {
// Check for duplicate tool names
if (this.tools.stream().anyMatch(th -> th.tool().name().equals(toolSpecification.tool().name()))) {
if (this.tools.stream().anyMatch(th -> th.tool().name().equals(toolCallSpecification.tool().name()))) {
return Mono
.error(new McpError("Tool with name '" + toolSpecification.tool().name() + "' already exists"));
.error(new McpError("Tool with name '" + toolCallSpecification.tool().name() + "' already exists"));
}

this.tools.add(toolSpecification);
logger.debug("Added tool handler: {}", toolSpecification.tool().name());
this.tools.add(toolCallSpecification);
logger.debug("Added tool handler: {}", toolCallSpecification.tool().name());

if (this.serverCapabilities.tools().listChanged()) {
return notifyToolsListChanged();
Expand Down Expand Up @@ -340,7 +352,7 @@ public Mono<Void> notifyToolsListChanged() {

private McpServerSession.RequestHandler<McpSchema.ListToolsResult> toolsListRequestHandler() {
return (exchange, params) -> {
List<Tool> tools = this.tools.stream().map(McpServerFeatures.AsyncToolSpecification::tool).toList();
List<Tool> tools = this.tools.stream().map(McpServerFeatures.AsyncToolCallSpecification::tool).toList();

return Mono.just(new McpSchema.ListToolsResult(tools, null));
};
Expand All @@ -352,15 +364,15 @@ private McpServerSession.RequestHandler<CallToolResult> toolsCallRequestHandler(
new TypeReference<McpSchema.CallToolRequest>() {
});

Optional<McpServerFeatures.AsyncToolSpecification> toolSpecification = this.tools.stream()
Optional<McpServerFeatures.AsyncToolCallSpecification> toolSpecification = this.tools.stream()
.filter(tr -> callToolRequest.name().equals(tr.tool().name()))
.findAny();

if (toolSpecification.isEmpty()) {
return Mono.error(new McpError("Tool not found: " + callToolRequest.name()));
}

return toolSpecification.map(tool -> tool.call().apply(exchange, callToolRequest.arguments()))
return toolSpecification.map(tool -> tool.call().apply(exchange, callToolRequest))
.orElse(Mono.error(new McpError("Tool not found: " + callToolRequest.name())));
};
}
Expand Down
141 changes: 134 additions & 7 deletions mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ class AsyncSpecification {
* Each tool is uniquely identified by a name and includes metadata describing its
* schema.
*/
private final List<McpServerFeatures.AsyncToolSpecification> tools = new ArrayList<>();
private final List<McpServerFeatures.AsyncToolCallSpecification> tools = new ArrayList<>();

/**
* The Model Context Protocol (MCP) provides a standardized way for servers to
Expand Down Expand Up @@ -321,13 +321,40 @@ public AsyncSpecification capabilities(McpSchema.ServerCapabilities serverCapabi
* map of arguments passed to the tool.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if tool or handler is null
* @deprecated Use {@link #toolCall(McpSchema.Tool, BiFunction)} instead for tool
* calls that require a request object.
*/
@Deprecated
public AsyncSpecification tool(McpSchema.Tool tool,
BiFunction<McpAsyncServerExchange, Map<String, Object>, Mono<CallToolResult>> handler) {
Assert.notNull(tool, "Tool must not be null");
Assert.notNull(handler, "Handler must not be null");

this.tools.add(new McpServerFeatures.AsyncToolSpecification(tool, handler));
this.tools.add(new McpServerFeatures.AsyncToolSpecification(tool, handler).toToolCall());

return this;
}

/**
* Adds a single tool with its implementation handler to the server. This is a
* convenience method for registering individual tools without creating a
* {@link McpServerFeatures.AsyncToolCallSpecification} explicitly.
* @param tool The tool definition including name, description, and schema. Must
* not be null.
* @param handler The function that implements the tool's logic. Must not be null.
* The function's first argument is an {@link McpAsyncServerExchange} upon which
* the server can interact with the connected client. The second argument is the
* {@link McpSchema.CallToolRequest} object containing the tool call
* @return This builder instance for method chaining
* @throws IllegalArgumentException if tool or handler is null
*/
public AsyncSpecification toolCall(McpSchema.Tool tool,
BiFunction<McpAsyncServerExchange, McpSchema.CallToolRequest, Mono<CallToolResult>> handler) {

Assert.notNull(tool, "Tool must not be null");
Assert.notNull(handler, "Handler must not be null");

this.tools.add(new McpServerFeatures.AsyncToolCallSpecification(tool, handler));

return this;
}
Expand All @@ -341,10 +368,29 @@ public AsyncSpecification tool(McpSchema.Tool tool,
* @return This builder instance for method chaining
* @throws IllegalArgumentException if toolSpecifications is null
* @see #tools(McpServerFeatures.AsyncToolSpecification...)
* @deprecated Use {@link #toolCalls(List)} instead for adding multiple tool
* calls.
*/
@Deprecated
public AsyncSpecification tools(List<McpServerFeatures.AsyncToolSpecification> toolSpecifications) {
Assert.notNull(toolSpecifications, "Tool handlers list must not be null");
this.tools.addAll(toolSpecifications);
this.tools.addAll(toolSpecifications.stream().map(s -> s.toToolCall()).toList());
return this;
}

/**
* Adds multiple tools with their handlers to the server using a List. This method
* is useful when tools are dynamically generated or loaded from a configuration
* source.
* @param toolCallSpecifications The list of tool specifications to add. Must not
* be null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if toolSpecifications is null
* @see #tools(McpServerFeatures.AsyncToolCallSpecification...)
*/
public AsyncSpecification toolCalls(List<McpServerFeatures.AsyncToolCallSpecification> toolCallSpecifications) {
Assert.notNull(toolCallSpecifications, "Tool handlers list must not be null");
this.tools.addAll(toolCallSpecifications);
return this;
}

Expand All @@ -363,11 +409,28 @@ public AsyncSpecification tools(List<McpServerFeatures.AsyncToolSpecification> t
* @param toolSpecifications The tool specifications to add. Must not be null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if toolSpecifications is null
* @see #tools(List)
* @deprecated Use
* {@link #toolCalls(McpServerFeatures.AsyncToolCallSpecification...)} instead
*/
@Deprecated
public AsyncSpecification tools(McpServerFeatures.AsyncToolSpecification... toolSpecifications) {
Assert.notNull(toolSpecifications, "Tool handlers list must not be null");
for (McpServerFeatures.AsyncToolSpecification tool : toolSpecifications) {
this.tools.add(tool.toToolCall());
}
return this;
}

/**
* Adds multiple tools with their handlers to the server using varargs. This
* method provides a convenient way to register multiple tools inline.
* @param toolSpecifications The tool specifications to add. Must not be null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if toolSpecifications is null
*/
public AsyncSpecification toolCalls(McpServerFeatures.AsyncToolCallSpecification... toolCallSpecifications) {
Assert.notNull(toolCallSpecifications, "Tool handlers list must not be null");
for (McpServerFeatures.AsyncToolCallSpecification tool : toolCallSpecifications) {
this.tools.add(tool);
}
return this;
Expand Down Expand Up @@ -667,7 +730,7 @@ class SyncSpecification {
* Each tool is uniquely identified by a name and includes metadata describing its
* schema.
*/
private final List<McpServerFeatures.SyncToolSpecification> tools = new ArrayList<>();
private final List<McpServerFeatures.SyncToolCallSpecification> tools = new ArrayList<>();

/**
* The Model Context Protocol (MCP) provides a standardized way for servers to
Expand Down Expand Up @@ -812,13 +875,39 @@ public SyncSpecification capabilities(McpSchema.ServerCapabilities serverCapabil
* list of arguments passed to the tool.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if tool or handler is null
* @deprecated Use {@link #toolCall(McpSchema.Tool, BiFunction)} instead for tool
* calls that require a request object.
*/
@Deprecated
public SyncSpecification tool(McpSchema.Tool tool,
BiFunction<McpSyncServerExchange, Map<String, Object>, McpSchema.CallToolResult> handler) {
Assert.notNull(tool, "Tool must not be null");
Assert.notNull(handler, "Handler must not be null");

this.tools.add(new McpServerFeatures.SyncToolSpecification(tool, handler));
this.tools.add(new McpServerFeatures.SyncToolSpecification(tool, handler).toToolCall());

return this;
}

/**
* Adds a single tool with its implementation handler to the server. This is a
* convenience method for registering individual tools without creating a
* {@link McpServerFeatures.SyncToolSpecification} explicitly.
* @param tool The tool definition including name, description, and schema. Must
* not be null.
* @param handler The function that implements the tool's logic. Must not be null.
* The function's first argument is an {@link McpSyncServerExchange} upon which
* the server can interact with the connected client. The second argument is the
* list of arguments passed to the tool.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if tool or handler is null
*/
public SyncSpecification toolCall(McpSchema.Tool tool,
BiFunction<McpSyncServerExchange, McpSchema.CallToolRequest, McpSchema.CallToolResult> handler) {
Assert.notNull(tool, "Tool must not be null");
Assert.notNull(handler, "Handler must not be null");

this.tools.add(new McpServerFeatures.SyncToolCallSpecification(tool, handler));

return this;
}
Expand All @@ -832,10 +921,28 @@ public SyncSpecification tool(McpSchema.Tool tool,
* @return This builder instance for method chaining
* @throws IllegalArgumentException if toolSpecifications is null
* @see #tools(McpServerFeatures.SyncToolSpecification...)
* @deprecated Use {@link #toolCalls(List)} instead for adding multiple tool
* calls.
*/
@Deprecated
public SyncSpecification tools(List<McpServerFeatures.SyncToolSpecification> toolSpecifications) {
Assert.notNull(toolSpecifications, "Tool handlers list must not be null");
this.tools.addAll(toolSpecifications);
this.tools.addAll(toolSpecifications.stream().map(s -> s.toToolCall()).toList());
return this;
}

/**
* Adds multiple tools with their handlers to the server using a List. This method
* is useful when tools are dynamically generated or loaded from a configuration
* source.
* @param toolSpecifications The list of tool specifications to add. Must not be
* null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if toolSpecifications is null
*/
public SyncSpecification toolCalls(List<McpServerFeatures.SyncToolCallSpecification> toolCallSpecifications) {
Assert.notNull(toolCallSpecifications, "Tool handlers list must not be null");
this.tools.addAll(toolCallSpecifications);
return this;
}

Expand All @@ -855,10 +962,30 @@ public SyncSpecification tools(List<McpServerFeatures.SyncToolSpecification> too
* @return This builder instance for method chaining
* @throws IllegalArgumentException if toolSpecifications is null
* @see #tools(List)
* @deprecated Use
* {@link #toolCalls(McpServerFeatures.SyncToolCallSpecification...)} instead for
* tool calls that require a request object.
*/
@Deprecated
public SyncSpecification tools(McpServerFeatures.SyncToolSpecification... toolSpecifications) {
Assert.notNull(toolSpecifications, "Tool handlers list must not be null");
for (McpServerFeatures.SyncToolSpecification tool : toolSpecifications) {
this.tools.add(tool.toToolCall());
}
return this;
}

/**
* Adds multiple tools with their handlers to the server using varargs. This
* method provides a convenient way to register multiple tools inline.
* @param toolSpecifications The tool specifications to add. Must not be null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if toolSpecifications is null
* @see #tools(List)
*/
public SyncSpecification toolCalls(McpServerFeatures.SyncToolCallSpecification... toolCallSpecifications) {
Assert.notNull(toolCallSpecifications, "Tool handlers list must not be null");
for (McpServerFeatures.SyncToolCallSpecification tool : toolCallSpecifications) {
this.tools.add(tool);
}
return this;
Expand Down
Loading