alibaba/arthas · error · McpError
Tool with name '{}' already exists
Error message
Tool with name '{}' already exists What it means
Thrown by McpStatelessNettyServer.addTool() when dynamically adding a ToolSpecification whose name already exists in the stateless server's tools list. The add runs async and fails the returned CompletableFuture with a McpError wrapped in CompletionException. Unlike the stateful builder, this is a runtime dynamic-registration check on a stateless server.
Source
Thrown at arthas-mcp-server/src/main/java/com/taobao/arthas/mcp/server/protocol/server/McpStatelessNettyServer.java:200
future.completeExceptionally(new McpError("Tool must not be null"));
return future;
}
if (toolSpecification.getCall() == null) {
CompletableFuture<Void> future = new CompletableFuture<>();
future.completeExceptionally(new McpError("Tool call handler must not be null"));
return future;
}
if (this.serverCapabilities.getTools() == null) {
CompletableFuture<Void> future = new CompletableFuture<>();
future.completeExceptionally(new McpError("Server must be configured with tool capabilities"));
return future;
}
return CompletableFuture
.runAsync(() -> {
if (this.tools.stream().anyMatch(th ->
th.getTool().getName().equals(toolSpecification.getTool().getName()))) {
throw new CompletionException(
new McpError("Tool with name '" + toolSpecification.getTool().getName() + "' already exists"));
}
this.tools.add(toolSpecification);
logger.debug("Added tool handler: {}", toolSpecification.getTool().getName());
})
.exceptionally(ex -> {
Throwable cause = ex instanceof CompletionException ? ex.getCause() : ex;
logger.error("Error while adding tool", cause);
throw new CompletionException(cause);
});
}
public CompletableFuture<Void> removeTool(String toolName) {
if (toolName == null) {
CompletableFuture<Void> future = new CompletableFuture<>();
future.completeExceptionally(new McpError("Tool name must not be null"));
return future;
}View on GitHub (pinned to 21cf2e9ba5)
Solutions
- Before addTool, inspect the current tool list and skip if the name exists, or removeTool first.
- Generate unique tool names for dynamically added tools.
- Catch the CompletionException/McpError and treat duplicate-add as upsert by removeTool then addTool.
- Coordinate tool registration across concurrent callers (addTool is not synchronized across names).
Example fix
// before statelessServer.addTool(spec).join(); // name exists // after String name = spec.getTool().getName(); statelessServer.removeTool(name).exceptionally(ex -> null).join(); statelessServer.addTool(spec).join();
Defensive patterns
Strategy: validation
Validate before calling
// Upsert: remove existing then add String name = spec.getTool().getName(); statelessServer.removeTool(name).exceptionally(ex -> null).join(); statelessServer.addTool(spec).join();
Try / catch
try {
statelessServer.addTool(spec).join();
} catch (CompletionException e) {
Throwable c = e.getCause();
if (c instanceof McpError me && me.getMessage().contains("already exists")) {
statelessServer.removeTool(spec.getTool().getName())
.exceptionally(x -> null).join();
statelessServer.addTool(spec).join();
} else throw e;
} Prevention
- Implement upsert: removeTool (ignore absent) then addTool.
- Generate unique names for dynamic tools.
- Serialize dynamic registrations per name to avoid races.
When it happens
Trigger: Calling addTool(spec) at runtime when a tool with the same getTool().getName() is already present; re-adding after a prior add succeeded.
Common situations: Live tool registration without checking current tools; plugin hot-load colliding with an existing tool; duplicate add during retries.
Related errors
- Tool with name '{}' not found
- Resource with URI '{}' already exists
- Prompt with name '{}' already exists
- Prompt with name '${promptName}' already exists
- Duplicate tool name: ${toolName}
AI-assisted analysis of alibaba/arthas@21cf2e9ba5 (2026-08-14).
Data as JSON: /api/errors/7d1d1d3e53be9790.
Report an issue: GitHub.