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

  1. Before addTool, inspect the current tool list and skip if the name exists, or removeTool first.
  2. Generate unique tool names for dynamically added tools.
  3. Catch the CompletionException/McpError and treat duplicate-add as upsert by removeTool then addTool.
  4. 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

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


AI-assisted analysis of alibaba/arthas@21cf2e9ba5 (2026-08-14). Data as JSON: /api/errors/7d1d1d3e53be9790. Report an issue: GitHub.