Skip to content

Commit 31db86d

Browse files
tzolovilayaperumalg
authored andcommitted
refactor: Improve MCP tool name prefix generation with automatic duplicate handling
- Introduce DefaultMcpToolNamePrefixGenerator to ensure unique tool names across MCP connections - Replace default prefix generation strategy from client/title-based to duplicate detection - Automatically add counter prefixes (dp_1_, dp_2_) when duplicate tool names are detected - Track existing connections and tool names to maintain idempotency - Update tests to reflect new duplicate handling behavior (keep existing tool on duplicate) - Update documentation to describe the new default generator behavior - Change default from McpToolNamePrefixGenerator.defaultGenerator() to new DefaultMcpToolNamePrefixGenerator BREAKING CHANGE: The default tool name generation strategy has changed. Tools now use their original names by default, with automatic prefixing only when duplicates are detected across different connections. Signed-off-by: Christian Tzolov <[email protected]>
1 parent 0c1a089 commit 31db86d

File tree

18 files changed

+187
-86
lines changed

18 files changed

+187
-86
lines changed

auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/McpToolCallbackAutoConfiguration.java

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import io.modelcontextprotocol.client.McpSyncClient;
2323

2424
import org.springframework.ai.mcp.AsyncMcpToolCallbackProvider;
25+
import org.springframework.ai.mcp.DefaultMcpToolNamePrefixGenerator;
2526
import org.springframework.ai.mcp.McpToolFilter;
2627
import org.springframework.ai.mcp.McpToolNamePrefixGenerator;
2728
import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
@@ -30,18 +31,28 @@
3031
import org.springframework.beans.factory.ObjectProvider;
3132
import org.springframework.boot.autoconfigure.AutoConfiguration;
3233
import org.springframework.boot.autoconfigure.condition.AllNestedConditions;
34+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
3335
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
3436
import org.springframework.boot.context.properties.EnableConfigurationProperties;
3537
import org.springframework.context.annotation.Bean;
3638
import org.springframework.context.annotation.Conditional;
3739

3840
/**
41+
* Responsible to convert MCP (sync and async) clients into Spring AI
42+
* ToolCallbacksProviders. These providers are used by Spring AI to discover and execute
43+
* tools.
3944
*/
4045
@AutoConfiguration(after = { McpClientAutoConfiguration.class })
4146
@EnableConfigurationProperties(McpClientCommonProperties.class)
4247
@Conditional(McpToolCallbackAutoConfiguration.McpToolCallbackAutoConfigurationCondition.class)
4348
public class McpToolCallbackAutoConfiguration {
4449

50+
@Bean
51+
@ConditionalOnMissingBean
52+
public McpToolNamePrefixGenerator defaultMcpToolNamePrefixGenerator() {
53+
return new DefaultMcpToolNamePrefixGenerator();
54+
}
55+
4556
/**
4657
* Creates tool callbacks for all configured MCP clients.
4758
*
@@ -61,12 +72,14 @@ public SyncMcpToolCallbackProvider mcpToolCallbacks(ObjectProvider<McpToolFilter
6172
ObjectProvider<List<McpSyncClient>> syncMcpClients,
6273
ObjectProvider<McpToolNamePrefixGenerator> mcpToolNamePrefixGenerator,
6374
ObjectProvider<ToolContextToMcpMetaConverter> toolContextToMcpMetaConverter) {
75+
6476
List<McpSyncClient> mcpClients = syncMcpClients.stream().flatMap(List::stream).toList();
77+
6578
return SyncMcpToolCallbackProvider.builder()
6679
.mcpClients(mcpClients)
6780
.toolFilter(syncClientsToolFilter.getIfUnique((() -> (McpSyncClient, tool) -> true)))
6881
.toolNamePrefixGenerator(
69-
mcpToolNamePrefixGenerator.getIfUnique(() -> McpToolNamePrefixGenerator.defaultGenerator()))
82+
mcpToolNamePrefixGenerator.getIfUnique(() -> McpToolNamePrefixGenerator.noPrefix()))
7083
.toolContextToMcpMetaConverter(
7184
toolContextToMcpMetaConverter.getIfUnique(() -> ToolContextToMcpMetaConverter.defaultConverter()))
7285
.build();
@@ -81,8 +94,7 @@ public AsyncMcpToolCallbackProvider mcpAsyncToolCallbacks(ObjectProvider<McpTool
8194
List<McpAsyncClient> mcpClients = mcpClientsProvider.stream().flatMap(List::stream).toList();
8295
return AsyncMcpToolCallbackProvider.builder()
8396
.toolFilter(asyncClientsToolFilter.getIfUnique(() -> (McpAsyncClient, tool) -> true))
84-
.toolNamePrefixGenerator(
85-
toolNamePrefixGenerator.getIfUnique(() -> McpToolNamePrefixGenerator.defaultGenerator()))
97+
.toolNamePrefixGenerator(toolNamePrefixGenerator.getIfUnique(() -> McpToolNamePrefixGenerator.noPrefix()))
8698
.toolContextToMcpMetaConverter(
8799
toolContextToMcpMetaConverter.getIfUnique(() -> ToolContextToMcpMetaConverter.defaultConverter()))
88100
.mcpClients(mcpClients)

auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/test/java/org/springframework/ai/mcp/server/common/autoconfigure/StatelessToolCallbackConverterAutoConfigurationIT.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,9 @@ void duplicateToolNamesDeduplication() {
127127

128128
@SuppressWarnings("unchecked")
129129
List<SyncToolSpecification> syncTools = (List<SyncToolSpecification>) context.getBean("syncTools");
130-
// Tools have different client prefixes, so both should be present
131-
assertThat(syncTools).hasSize(2);
130+
131+
// On duplicate key, keep the existing tool
132+
assertThat(syncTools).hasSize(1);
132133
});
133134
}
134135

auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-common/src/test/java/org/springframework/ai/mcp/server/common/autoconfigure/ToolCallbackConverterAutoConfigurationIT.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,9 @@ void duplicateToolNamesDeduplication() {
127127

128128
@SuppressWarnings("unchecked")
129129
List<SyncToolSpecification> syncTools = (List<SyncToolSpecification>) context.getBean("syncTools");
130-
// Tools have different client prefixes, so both should be present
131-
assertThat(syncTools).hasSize(2);
130+
131+
// On duplicate key, keep the existing tool
132+
assertThat(syncTools).hasSize(1);
132133
});
133134
}
134135

mcp/common/src/main/java/org/springframework/ai/mcp/AsyncMcpToolCallback.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -227,11 +227,7 @@ public AsyncMcpToolCallback build() {
227227

228228
// Apply defaults if not specified
229229
if (this.prefixedToolName == null) {
230-
this.prefixedToolName = McpToolNamePrefixGenerator.defaultGenerator()
231-
.prefixedToolName(McpConnectionInfo.builder()
232-
.clientCapabilities(this.mcpClient.getClientCapabilities())
233-
.clientInfo(this.mcpClient.getClientInfo())
234-
.build(), this.tool);
230+
this.prefixedToolName = McpToolUtils.format(this.tool.name());
235231
}
236232

237233
return new AsyncMcpToolCallback(this.mcpClient, this.tool, this.prefixedToolName,

mcp/common/src/main/java/org/springframework/ai/mcp/AsyncMcpToolCallbackProvider.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ public class AsyncMcpToolCallbackProvider implements ToolCallbackProvider {
5757
*/
5858
@Deprecated
5959
public AsyncMcpToolCallbackProvider(McpToolFilter toolFilter, List<McpAsyncClient> mcpClients) {
60-
this(toolFilter, McpToolNamePrefixGenerator.defaultGenerator(),
61-
ToolContextToMcpMetaConverter.defaultConverter(), mcpClients);
60+
this(toolFilter, McpToolNamePrefixGenerator.noPrefix(), ToolContextToMcpMetaConverter.defaultConverter(),
61+
mcpClients);
6262
}
6363

6464
/**
@@ -203,7 +203,7 @@ public final static class Builder {
203203

204204
private List<McpAsyncClient> mcpClients;
205205

206-
private McpToolNamePrefixGenerator toolNamePrefixGenerator = McpToolNamePrefixGenerator.defaultGenerator();
206+
private McpToolNamePrefixGenerator toolNamePrefixGenerator = new DefaultMcpToolNamePrefixGenerator();
207207

208208
private ToolContextToMcpMetaConverter toolContextToMcpMetaConverter = ToolContextToMcpMetaConverter
209209
.defaultConverter();
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Copyright 2025-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.ai.mcp;
18+
19+
import java.util.Set;
20+
import java.util.concurrent.ConcurrentHashMap;
21+
import java.util.concurrent.atomic.AtomicInteger;
22+
23+
import io.modelcontextprotocol.spec.McpSchema;
24+
import io.modelcontextprotocol.spec.McpSchema.Implementation;
25+
import io.modelcontextprotocol.spec.McpSchema.Tool;
26+
import org.slf4j.Logger;
27+
import org.slf4j.LoggerFactory;
28+
29+
/**
30+
* Default implementation of {@link McpToolNamePrefixGenerator} that ensures unique tool
31+
* names for all client/server connections.
32+
*
33+
* <p>
34+
* This implementation ensures that tool names are unique across different MCP clients and
35+
* servers by tracking existing connections and appending a counter to duplicate tool
36+
* names.
37+
*
38+
* <p>
39+
* For each unique combination of (client, server, tool), e.g. each connection, the tool
40+
* name is generated only once. If a tool name has already been used, a prefix with a
41+
* counter is added to make it unique (e.g., "alt_1_toolName", "alt_2_toolName", etc.).
42+
*
43+
* <p>
44+
* This implementation is thread-safe.
45+
*
46+
* @author Christian Tzolov
47+
*/
48+
public class DefaultMcpToolNamePrefixGenerator implements McpToolNamePrefixGenerator {
49+
50+
private static final Logger logger = LoggerFactory.getLogger(DefaultMcpToolNamePrefixGenerator.class);
51+
52+
// Idempotency tracking. For a given combination of (client, server, tool) we will
53+
// generate a unique tool name only once.
54+
private Set<ConnectionId> existingConnections = ConcurrentHashMap.newKeySet();
55+
56+
private Set<String> allUsedToolNames = ConcurrentHashMap.newKeySet();
57+
58+
private AtomicInteger counter = new AtomicInteger(1);
59+
60+
@Override
61+
public String prefixedToolName(McpConnectionInfo mcpConnectionInfo, McpSchema.Tool tool) {
62+
63+
String uniqueToolName = McpToolUtils.format(tool.name());
64+
65+
if (this.existingConnections
66+
.add(new ConnectionId(mcpConnectionInfo.clientInfo(), (mcpConnectionInfo.initializeResult() != null)
67+
? mcpConnectionInfo.initializeResult().serverInfo() : null, tool))) {
68+
if (!this.allUsedToolNames.add(uniqueToolName)) {
69+
uniqueToolName = "alt_" + this.counter.getAndIncrement() + "_" + uniqueToolName;
70+
this.allUsedToolNames.add(uniqueToolName);
71+
logger.warn("Tool name '{}' already exists. Using unique tool name '{}'", tool.name(), uniqueToolName);
72+
}
73+
}
74+
75+
return uniqueToolName;
76+
}
77+
78+
private record ConnectionId(Implementation clientInfo, Implementation serverInfo, Tool tool) {
79+
}
80+
81+
}

mcp/common/src/main/java/org/springframework/ai/mcp/McpToolNamePrefixGenerator.java

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -40,16 +40,6 @@ public interface McpToolNamePrefixGenerator {
4040

4141
String prefixedToolName(McpConnectionInfo mcpConnectionInfo, Tool tool);
4242

43-
/**
44-
* Default implementation that uses the MCP client name to generate the prefixed tool
45-
* name.
46-
* @return the default prefix generator
47-
*/
48-
static McpToolNamePrefixGenerator defaultGenerator() {
49-
return (mcpConnectionIfo, tool) -> McpToolUtils.prefixedToolName(mcpConnectionIfo.clientInfo().name(),
50-
mcpConnectionIfo.clientInfo().title(), tool.name());
51-
}
52-
5343
/**
5444
* Static factory method to create a no-op prefix generator that returns the tool name
5545
* @return a prefix generator that returns the tool name as-is

mcp/common/src/main/java/org/springframework/ai/mcp/McpToolUtils.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ public static String prefixedToolName(String prefix, String toolName) {
106106
return prefixedToolName(prefix, null, toolName);
107107
}
108108

109-
private static String format(String input) {
109+
public static String format(String input) {
110110
// Replace any character that isn't alphanumeric, underscore, or hyphen with
111111
// concatenation. Support Han script + CJK blocks for complete Chinese character
112112
// coverage

mcp/common/src/main/java/org/springframework/ai/mcp/SyncMcpToolCallback.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -224,11 +224,7 @@ public SyncMcpToolCallback build() {
224224

225225
// Apply defaults if not specified
226226
if (this.prefixedToolName == null) {
227-
this.prefixedToolName = McpToolNamePrefixGenerator.defaultGenerator()
228-
.prefixedToolName(McpConnectionInfo.builder()
229-
.clientCapabilities(this.mcpClient.getClientCapabilities())
230-
.clientInfo(this.mcpClient.getClientInfo())
231-
.build(), this.tool);
227+
this.prefixedToolName = McpToolUtils.format(this.tool.name());
232228
}
233229

234230
return new SyncMcpToolCallback(this.mcpClient, this.tool, this.prefixedToolName,

mcp/common/src/main/java/org/springframework/ai/mcp/SyncMcpToolCallbackProvider.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ public class SyncMcpToolCallbackProvider implements ToolCallbackProvider {
5555
*/
5656
@Deprecated
5757
public SyncMcpToolCallbackProvider(McpToolFilter toolFilter, List<McpSyncClient> mcpClients) {
58-
this(toolFilter, McpToolNamePrefixGenerator.defaultGenerator(), mcpClients,
58+
this(toolFilter, McpToolNamePrefixGenerator.noPrefix(), mcpClients,
5959
ToolContextToMcpMetaConverter.defaultConverter());
6060
}
6161

@@ -114,6 +114,7 @@ public SyncMcpToolCallbackProvider(McpSyncClient... mcpClients) {
114114

115115
@Override
116116
public ToolCallback[] getToolCallbacks() {
117+
117118
var array = this.mcpClients.stream()
118119
.flatMap(mcpClient -> mcpClient.listTools()
119120
.tools()
@@ -184,7 +185,7 @@ public static class Builder {
184185

185186
private McpToolFilter toolFilter = (mcpClient, tool) -> true;
186187

187-
private McpToolNamePrefixGenerator toolNamePrefixGenerator = McpToolNamePrefixGenerator.defaultGenerator();
188+
private McpToolNamePrefixGenerator toolNamePrefixGenerator = new DefaultMcpToolNamePrefixGenerator();
188189

189190
private ToolContextToMcpMetaConverter toolContextToMcpMetaConverter = ToolContextToMcpMetaConverter
190191
.defaultConverter();
@@ -234,8 +235,7 @@ public Builder toolFilter(McpToolFilter toolFilter) {
234235
}
235236

236237
/**
237-
* Sets tool name prefix generator. Defaults to
238-
* {@link McpToolNamePrefixGenerator#defaultGenerator()}.
238+
* Sets tool name prefix generator.
239239
* @param toolNamePrefixGenerator generates prefixes for tool names
240240
* @return this builder
241241
*/

0 commit comments

Comments
 (0)