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

  1. Call removeTool(name) before addTool when refreshing a tool.
  2. Check getTools()/the existing list for the name before adding.
  3. 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

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


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