spring-projects/spring-ai · error · IllegalStateException

Multiple tools with the same name (%s)

Error message

Multiple tools with the same name (%s)

What it means

AsyncMcpToolCallbackProvider.getToolCallbacks() collects ToolCallbacks from multiple MCP clients and rejects the whole set if two or more callbacks resolve to the same tool name, since the framework cannot disambiguate which callback to invoke. Duplicate names arise when multiple MCP servers expose identically named tools or when connectionNamePrefix is absent or insufficient to namespace them.

Source

Thrown at mcp/common/src/main/java/org/springframework/ai/mcp/AsyncMcpToolCallbackProvider.java:203

	}

	private static McpConnectionInfo connectionInfo(McpAsyncClient mcpClient) {
		return McpConnectionInfo.builder()
			.clientCapabilities(mcpClient.getClientCapabilities())
			.clientInfo(mcpClient.getClientInfo())
			.initializeResult(mcpClient.getCurrentInitializationResult())
			.build();
	}

	/**
	 * Validates tool name uniqueness.
	 * @param toolCallbacks callbacks to validate
	 * @throws IllegalStateException if duplicate names found
	 */
	private void validateToolCallbacks(List<ToolCallback> toolCallbacks) {
		List<String> duplicateToolNames = ToolUtils.getDuplicateToolNames(toolCallbacks);
		if (!duplicateToolNames.isEmpty()) {
			throw new IllegalStateException(
					"Multiple tools with the same name (%s)".formatted(String.join(", ", duplicateToolNames)));
		}
	}

	/**
	 * Creates a reactive stream of tool callbacks from multiple MCP clients.
	 * <p>
	 * Provides fully reactive tool discovery suitable for non-blocking applications.
	 * Combines tools from all clients into a single stream with name conflict validation.
	 * @param mcpClients MCP clients for tool discovery
	 * @return Flux of tool callbacks from all clients
	 */
	public static Flux<ToolCallback> asyncToolCallbacks(List<McpAsyncClient> mcpClients) {
		if (CollectionUtils.isEmpty(mcpClients)) {
			return Flux.empty();
		}

		return Flux.fromArray(new AsyncMcpToolCallbackProvider(mcpClients).getToolCallbacks());

View on GitHub (pinned to 98a7beda4f)

Solutions

  1. Set a unique connection name prefix per MCP client so tool names are namespaced as <prefix>_<toolName>
  2. Rename the duplicate tool on one of the MCP servers
  3. Inspect the comma-separated list in the message to identify the colliding tool names and which servers expose them
  4. If only one server is needed, remove the redundant McpAsyncClient from the provider

Example fix

// before — two clients both expose tool "search", no prefixes
new AsyncMcpToolCallbackProvider(List.of(asyncClientA, asyncClientB));
// after — prefixes namespace names: serverA_search, serverB_search
new AsyncMcpToolCallbackProvider(
    Map.of("serverA", asyncClientA, "serverB", asyncClientB));
Defensive patterns

Strategy: validation

Validate before calling

List<String> dups = ToolUtils.getDuplicateToolNames(provider.getToolCallbacks());
if (!dups.isEmpty()) throw new ConfigurationException("Duplicate MCP tool names: " + dups); // run at startup, before publishing callbacks

Type guard

Set<String> seen = new HashSet<>();
List<ToolCallback> unique = callbacks.stream()
    .filter(c -> seen.add(c.getToolDefinition().name()))
    .toList();
if (unique.size() != callbacks.size()) throw new IllegalStateException("Duplicate MCP tool names detected");

Prevention

When it happens

Trigger: Calling getToolCallbacks() on an AsyncMcpToolCallbackProvider built from two or more McpAsyncClient instances where ToolUtils.prefixedToolName(...) yields identical names for tools from different clients (no connectionNamePrefix set, or the same prefix used twice).

Common situations: Configuring multiple MCP servers that both expose a tool named e.g. 'search' without a per-connection connectionNamePrefix; two server entries in configuration sharing the same name/prefix; upgrading spring-ai where prefixing behavior changed so previously-distinct names now collide.

Related errors


AI-assisted analysis of spring-projects/spring-ai@98a7beda4f (2026-09-11). Data as JSON: /api/errors/8057badf1ff5b73b. Report an issue: GitHub.