alibaba/arthas · error · McpError
Tool with name '${toolName}' already exists
Error message
Tool with name '${toolName}' already exists What it means
McpNettyServer.addTool throws McpError (wrapped in CompletionException) when a tool with the same name is already registered. Tools are indexed both in a list and a toolsByName map keyed by Tool.getName(), so duplicate names would corrupt that index; the check refuses the second registration.
Source
Thrown at arthas-mcp-server/src/main/java/com/taobao/arthas/mcp/server/protocol/server/McpNettyServer.java:269
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.supplyAsync(() -> {
// Check for duplicate tool names
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);
this.toolsByName.put(toolSpecification.getTool().getName(), toolSpecification);
logger.debug("Added tool handler: {}", toolSpecification.getTool().getName());
return null;
}).thenCompose(ignored -> {
if (this.serverCapabilities.getTools().getListChanged()) {
return notifyToolsListChanged();
}
return CompletableFuture.completedFuture(null);
}).exceptionally(ex -> {
Throwable cause = ex instanceof CompletionException ? ex.getCause() : ex;
logger.error("Error while adding tool", cause);
throw new CompletionException(cause);
});
}
View on GitHub (pinned to 21cf2e9ba5)
Solutions
- Call removeTool(name) before addTool when refreshing a tool.
- Check getTools()/the existing list for the name before adding.
- Namespace tool names by module to avoid collisions.
Example fix
// before
server.addTool(spec); // name 'arthas.dump' already registered
// after
server.removeTool("arthas.dump").thenRun(() -> server.addTool(spec)); Defensive patterns
Strategy: validation
Validate before calling
boolean exists = server.getTools().stream()
.anyMatch(t -> t.getTool().getName().equals(name));
if (exists) server.removeTool(name).join();
server.addTool(spec).join(); Try / catch
try { server.addTool(spec).join(); }
catch (CompletionException e) {
if (e.getCause() instanceof io.modelcontextprotocol.spec.McpError && e.getCause().getMessage().contains("already exists")) { server.removeTool(name).thenRun(() -> server.addTool(spec)).join(); }
else throw e;
} Prevention
- Check the tool list before addTool.
- removeTool before re-adding on reload.
- Namespace tool names by module.
When it happens
Trigger: Calling addTool twice with tools whose Tool.name is equal (case-sensitive, exact); dynamically registering a tool whose name collides with one registered at server startup; re-registering after a hot reload without removing first.
Common situations: Plugin/tool auto-discovery registers the same tool twice; a reload path forgets to removeTool before re-adding; two modules contribute a tool with the same name.
Related errors
- Resource with URI '${resourceUri}' already exists
- Tool with name '${toolName}' not found
- Prompt with name '${promptName}' already exists
- Duplicate tool name: ${toolName}
- Tool with name '{}' already exists
AI-assisted analysis of alibaba/arthas@21cf2e9ba5 (2026-08-14).
Data as JSON: /api/errors/577bd3b04e43abca.
Report an issue: GitHub.