Skip to content

Commit 3a1fe1f

Browse files
committed
feat(mcp): Add customizable tool name prefix generation
- Introduce McpToolNamePrefixGenerator interface for flexible tool naming - Add default and no-prefix generator implementations - Update SyncMcpToolCallback and AsyncMcpToolCallback to accept prefixed names - Enhance tool callback providers to use the prefix generator - Add comprehensive tests for prefix generation functionality - Update documentation with tool name prefix generation examples - Rename McpMetadata to McpConnectionInfo with builder pattern This change allows users to customize how MCP tool names are prefixed, helping to avoid naming conflicts when integrating tools from multiple MCP servers. By default, the MCP client name is used as a prefix, but users can provide custom implementations or disable prefixing entirely. The McpConnectionInfo record now provides a cleaner API with a builder pattern and better encapsulates the connection metadata including client capabilities, client info, and initialization results. BREAKING CHANGE: McpMetadata renamed to McpConnectionInfo. Constructors for SyncMcpToolCallback and AsyncMcpToolCallback that don't accept prefixedToolName parameter are deprecated. Signed-off-by: Christian Tzolov <[email protected]>
1 parent 3fee79a commit 3a1fe1f

File tree

16 files changed

+503
-88
lines changed

16 files changed

+503
-88
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: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,13 @@
2323

2424
import org.springframework.ai.mcp.AsyncMcpToolCallbackProvider;
2525
import org.springframework.ai.mcp.McpToolFilter;
26+
import org.springframework.ai.mcp.McpToolNamePrefixGenerator;
2627
import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
2728
import org.springframework.ai.mcp.client.common.autoconfigure.properties.McpClientCommonProperties;
2829
import org.springframework.beans.factory.ObjectProvider;
2930
import org.springframework.boot.autoconfigure.AutoConfiguration;
3031
import org.springframework.boot.autoconfigure.condition.AllNestedConditions;
32+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
3133
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
3234
import org.springframework.boot.context.properties.EnableConfigurationProperties;
3335
import org.springframework.context.annotation.Bean;
@@ -40,6 +42,23 @@
4042
@Conditional(McpToolCallbackAutoConfiguration.McpToolCallbackAutoConfigurationCondition.class)
4143
public class McpToolCallbackAutoConfiguration {
4244

45+
/**
46+
* Provides a default {@link McpToolNamePrefixGenerator} bean if none is already
47+
* defined.
48+
* <p>
49+
* This generator is used to create uniquely prefixed tool names based on the MCP
50+
* connection information, helping to avoid name collisions when integrating tools
51+
* from multiple MCP servers.
52+
*
53+
* Register the McpToolNamePrefixGenerator.noPrefix() bean to disable the prefixing.
54+
* @return the default McpToolNamePrefixGenerator
55+
*/
56+
@Bean
57+
@ConditionalOnMissingBean
58+
public McpToolNamePrefixGenerator mcpToolNamePrefixGenerator() {
59+
return McpToolNamePrefixGenerator.defaultGenerator();
60+
}
61+
4362
/**
4463
* Creates tool callbacks for all configured MCP clients.
4564
*
@@ -49,25 +68,28 @@ public class McpToolCallbackAutoConfiguration {
4968
* @param syncClientsToolFilter list of {@link McpToolFilter}s for the sync client to
5069
* filter the discovered tools
5170
* @param syncMcpClients provider of MCP sync clients
71+
* @param mcpToolNamePrefixGenerator the tool name prefix generator
5272
* @return list of tool callbacks for MCP integration
5373
*/
5474
@Bean
5575
@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "SYNC",
5676
matchIfMissing = true)
5777
public SyncMcpToolCallbackProvider mcpToolCallbacks(ObjectProvider<McpToolFilter> syncClientsToolFilter,
58-
ObjectProvider<List<McpSyncClient>> syncMcpClients) {
78+
ObjectProvider<List<McpSyncClient>> syncMcpClients, McpToolNamePrefixGenerator mcpToolNamePrefixGenerator) {
5979
List<McpSyncClient> mcpClients = syncMcpClients.stream().flatMap(List::stream).toList();
6080
return new SyncMcpToolCallbackProvider(syncClientsToolFilter.getIfUnique((() -> (McpSyncClient, tool) -> true)),
61-
mcpClients);
81+
mcpToolNamePrefixGenerator, mcpClients);
6282
}
6383

6484
@Bean
6585
@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC")
6686
public AsyncMcpToolCallbackProvider mcpAsyncToolCallbacks(ObjectProvider<McpToolFilter> asyncClientsToolFilter,
67-
ObjectProvider<List<McpAsyncClient>> mcpClientsProvider) {
87+
ObjectProvider<List<McpAsyncClient>> mcpClientsProvider,
88+
McpToolNamePrefixGenerator toolNamePrefixGenerator) {
6889
List<McpAsyncClient> mcpClients = mcpClientsProvider.stream().flatMap(List::stream).toList();
6990
return new AsyncMcpToolCallbackProvider(
70-
asyncClientsToolFilter.getIfUnique(() -> (McpAsyncClient, tool) -> true), mcpClients);
91+
asyncClientsToolFilter.getIfUnique(() -> (McpAsyncClient, tool) -> true), toolNamePrefixGenerator,
92+
mcpClients);
7193
}
7294

7395
public static class McpToolCallbackAutoConfigurationCondition extends AllNestedConditions {

auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/java/org/springframework/ai/mcp/client/common/autoconfigure/McpToolCallbackAutoConfigurationConditionTests.java

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
import reactor.core.publisher.Mono;
2727

2828
import org.springframework.ai.mcp.AsyncMcpToolCallbackProvider;
29-
import org.springframework.ai.mcp.McpMetadata;
29+
import org.springframework.ai.mcp.McpConnectionInfo;
3030
import org.springframework.ai.mcp.McpToolFilter;
3131
import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
3232
import org.springframework.ai.mcp.client.common.autoconfigure.McpToolCallbackAutoConfiguration.McpToolCallbackAutoConfigurationCondition;
@@ -107,8 +107,10 @@ void verifySyncToolCallbackFilterConfiguration() {
107107
McpSchema.ListToolsResult listToolsResult1 = mock(McpSchema.ListToolsResult.class);
108108
when(listToolsResult1.tools()).thenReturn(List.of(tool1, tool2));
109109
when(syncClient1.listTools()).thenReturn(listToolsResult1);
110-
assertThat(toolFilter.test(new McpMetadata(null, syncClient1.getClientInfo(), null), tool1)).isFalse();
111-
assertThat(toolFilter.test(new McpMetadata(null, syncClient1.getClientInfo(), null), tool2)).isTrue();
110+
assertThat(toolFilter.test(new McpConnectionInfo(null, syncClient1.getClientInfo(), null), tool1))
111+
.isFalse();
112+
assertThat(toolFilter.test(new McpConnectionInfo(null, syncClient1.getClientInfo(), null), tool2))
113+
.isTrue();
112114
});
113115
}
114116

@@ -133,8 +135,10 @@ void verifyASyncToolCallbackFilterConfiguration() {
133135
McpSchema.ListToolsResult listToolsResult1 = mock(McpSchema.ListToolsResult.class);
134136
when(listToolsResult1.tools()).thenReturn(List.of(tool1, tool2));
135137
when(asyncClient1.listTools()).thenReturn(Mono.just(listToolsResult1));
136-
assertThat(toolFilter.test(new McpMetadata(null, asyncClient1.getClientInfo(), null), tool1)).isFalse();
137-
assertThat(toolFilter.test(new McpMetadata(null, asyncClient1.getClientInfo(), null), tool2)).isTrue();
138+
assertThat(toolFilter.test(new McpConnectionInfo(null, asyncClient1.getClientInfo(), null), tool1))
139+
.isFalse();
140+
assertThat(toolFilter.test(new McpConnectionInfo(null, asyncClient1.getClientInfo(), null), tool2))
141+
.isTrue();
138142
});
139143
}
140144

@@ -156,7 +160,7 @@ static class McpClientFilterConfiguration {
156160
McpToolFilter mcpClientFilter() {
157161
return new McpToolFilter() {
158162
@Override
159-
public boolean test(McpMetadata metadata, McpSchema.Tool tool) {
163+
public boolean test(McpConnectionInfo metadata, McpSchema.Tool tool) {
160164
if (metadata.clientInfo().name().equals("client1") && tool.name().contains("tool1")) {
161165
return false;
162166
}

auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/java/org/springframework/ai/mcp/client/common/autoconfigure/McpToolCallbackAutoConfigurationTests.java

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,17 @@
1616

1717
package org.springframework.ai.mcp.client.common.autoconfigure;
1818

19+
import io.modelcontextprotocol.spec.McpSchema.Tool;
1920
import org.junit.jupiter.api.Test;
2021

22+
import org.springframework.ai.mcp.AsyncMcpToolCallbackProvider;
23+
import org.springframework.ai.mcp.McpConnectionInfo;
24+
import org.springframework.ai.mcp.McpToolNamePrefixGenerator;
25+
import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
2126
import org.springframework.boot.autoconfigure.AutoConfigurations;
2227
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
28+
import org.springframework.context.annotation.Bean;
29+
import org.springframework.context.annotation.Configuration;
2330

2431
import static org.assertj.core.api.Assertions.assertThat;
2532

@@ -85,4 +92,94 @@ void enabledMcpToolCallbackAutoConfiguration() {
8592
});
8693
}
8794

95+
@Test
96+
void defaultMcpToolNamePrefixGeneratorIsCreated() {
97+
// Test with SYNC mode (default)
98+
this.applicationContext.run(context -> {
99+
assertThat(context).hasBean("mcpToolNamePrefixGenerator");
100+
McpToolNamePrefixGenerator generator = context.getBean(McpToolNamePrefixGenerator.class);
101+
assertThat(generator).isNotNull();
102+
});
103+
104+
// Test with ASYNC mode
105+
this.applicationContext.withPropertyValues("spring.ai.mcp.client.type=ASYNC").run(context -> {
106+
assertThat(context).hasBean("mcpToolNamePrefixGenerator");
107+
McpToolNamePrefixGenerator generator = context.getBean(McpToolNamePrefixGenerator.class);
108+
assertThat(generator).isNotNull();
109+
});
110+
}
111+
112+
@Test
113+
void customMcpToolNamePrefixGeneratorOverridesDefault() {
114+
// Test with SYNC mode
115+
this.applicationContext.withUserConfiguration(CustomPrefixGeneratorConfig.class).run(context -> {
116+
assertThat(context).hasBean("mcpToolNamePrefixGenerator");
117+
McpToolNamePrefixGenerator generator = context.getBean(McpToolNamePrefixGenerator.class);
118+
assertThat(generator).isInstanceOf(CustomPrefixGenerator.class);
119+
assertThat(context).hasBean("mcpToolCallbacks");
120+
// Verify the custom generator is injected into the provider
121+
SyncMcpToolCallbackProvider provider = context.getBean(SyncMcpToolCallbackProvider.class);
122+
assertThat(provider).isNotNull();
123+
});
124+
125+
// Test with ASYNC mode
126+
this.applicationContext.withUserConfiguration(CustomPrefixGeneratorConfig.class)
127+
.withPropertyValues("spring.ai.mcp.client.type=ASYNC")
128+
.run(context -> {
129+
assertThat(context).hasBean("mcpToolNamePrefixGenerator");
130+
McpToolNamePrefixGenerator generator = context.getBean(McpToolNamePrefixGenerator.class);
131+
assertThat(generator).isInstanceOf(CustomPrefixGenerator.class);
132+
assertThat(context).hasBean("mcpAsyncToolCallbacks");
133+
// Verify the custom generator is injected into the provider
134+
AsyncMcpToolCallbackProvider provider = context.getBean(AsyncMcpToolCallbackProvider.class);
135+
assertThat(provider).isNotNull();
136+
});
137+
}
138+
139+
@Test
140+
void mcpToolNamePrefixGeneratorIsInjectedIntoProviders() {
141+
// Test SYNC provider receives the generator
142+
this.applicationContext.run(context -> {
143+
assertThat(context).hasBean("mcpToolNamePrefixGenerator");
144+
assertThat(context).hasBean("mcpToolCallbacks");
145+
146+
McpToolNamePrefixGenerator generator = context.getBean(McpToolNamePrefixGenerator.class);
147+
SyncMcpToolCallbackProvider provider = context.getBean(SyncMcpToolCallbackProvider.class);
148+
149+
assertThat(generator).isNotNull();
150+
assertThat(provider).isNotNull();
151+
});
152+
153+
// Test ASYNC provider receives the generator
154+
this.applicationContext.withPropertyValues("spring.ai.mcp.client.type=ASYNC").run(context -> {
155+
assertThat(context).hasBean("mcpToolNamePrefixGenerator");
156+
assertThat(context).hasBean("mcpAsyncToolCallbacks");
157+
158+
McpToolNamePrefixGenerator generator = context.getBean(McpToolNamePrefixGenerator.class);
159+
AsyncMcpToolCallbackProvider provider = context.getBean(AsyncMcpToolCallbackProvider.class);
160+
161+
assertThat(generator).isNotNull();
162+
assertThat(provider).isNotNull();
163+
});
164+
}
165+
166+
@Configuration
167+
static class CustomPrefixGeneratorConfig {
168+
169+
@Bean
170+
public McpToolNamePrefixGenerator mcpToolNamePrefixGenerator() {
171+
return new CustomPrefixGenerator();
172+
}
173+
174+
}
175+
176+
static class CustomPrefixGenerator implements McpToolNamePrefixGenerator {
177+
178+
@Override
179+
public String prefixedToolName(McpConnectionInfo mcpConnInfo, Tool tool) {
180+
return "custom_" + tool.name();
181+
}
182+
183+
}
184+
88185
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,7 @@ List<ToolCallback> testTool() {
395395
Mockito.when(mockClient.callTool(Mockito.any(McpSchema.CallToolRequest.class))).thenReturn(mockResult);
396396
when(mockClient.getClientInfo()).thenReturn(new McpSchema.Implementation("testClient", "1.0.0"));
397397

398-
return List.of(new SyncMcpToolCallback(mockClient, mockTool));
398+
return List.of(new SyncMcpToolCallback(mockClient, mockTool, mockTool.name()));
399399
}
400400

401401
}
@@ -413,7 +413,7 @@ ToolCallbackProvider testToolCallbackProvider() {
413413
Mockito.when(mockTool.description()).thenReturn("Provider Tool");
414414
when(mockClient.getClientInfo()).thenReturn(new McpSchema.Implementation("testClient", "1.0.0"));
415415

416-
return new ToolCallback[] { new SyncMcpToolCallback(mockClient, mockTool) };
416+
return new ToolCallback[] { new SyncMcpToolCallback(mockClient, mockTool, mockTool.name()) };
417417
};
418418
}
419419

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

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.springframework.ai.tool.definition.DefaultToolDefinition;
3232
import org.springframework.ai.tool.definition.ToolDefinition;
3333
import org.springframework.ai.tool.execution.ToolExecutionException;
34+
import org.springframework.util.Assert;
3435
import org.springframework.util.StringUtils;
3536

3637
/**
@@ -71,14 +72,33 @@ public class AsyncMcpToolCallback implements ToolCallback {
7172

7273
private final Tool tool;
7374

75+
private final String prefixedToolName;
76+
7477
/**
7578
* Creates a new {@code AsyncMcpToolCallback} instance.
7679
* @param mcpClient the MCP client to use for tool execution
7780
* @param tool the MCP tool definition to adapt
81+
* @deprecated use {@link #AsyncMcpToolCallback(McpAsyncClient, Tool, String)}
7882
*/
83+
@Deprecated
7984
public AsyncMcpToolCallback(McpAsyncClient mcpClient, Tool tool) {
85+
this(mcpClient, tool, McpToolUtils.prefixedToolName(mcpClient.getClientInfo().name(), tool.name()));
86+
}
87+
88+
/**
89+
* Creates a new {@code AsyncMcpToolCallback} instance.
90+
* @param mcpClient the MCP client to use for tool execution
91+
* @param tool the MCP tool definition to adapt
92+
* @param prefixedToolName the prefixed tool name to use in the tool definition.
93+
*/
94+
public AsyncMcpToolCallback(McpAsyncClient mcpClient, Tool tool, String prefixedToolName) {
95+
Assert.notNull(mcpClient, "MCP client must not be null");
96+
Assert.notNull(tool, "MCP tool must not be null");
97+
Assert.hasText(prefixedToolName, "Prefixed tool name must not be empty");
98+
8099
this.asyncMcpClient = mcpClient;
81100
this.tool = tool;
101+
this.prefixedToolName = prefixedToolName;
82102
}
83103

84104
/**
@@ -95,7 +115,7 @@ public AsyncMcpToolCallback(McpAsyncClient mcpClient, Tool tool) {
95115
@Override
96116
public ToolDefinition getToolDefinition() {
97117
return DefaultToolDefinition.builder()
98-
.name(McpToolUtils.prefixedToolName(this.asyncMcpClient.getClientInfo().name(), this.tool.name()))
118+
.name(this.prefixedToolName)
99119
.description(this.tool.description())
100120
.inputSchema(ModelOptionsUtils.toJsonString(this.tool.inputSchema()))
101121
.build();

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

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,17 +76,37 @@ public class AsyncMcpToolCallbackProvider implements ToolCallbackProvider {
7676

7777
private final List<McpAsyncClient> mcpClients;
7878

79+
private final McpToolNamePrefixGenerator toolNamePrefixGenerator;
80+
7981
/**
8082
* Creates a new {@code AsyncMcpToolCallbackProvider} instance with a list of MCP
8183
* clients.
8284
* @param toolFilter a filter to apply to each discovered tool
8385
* @param mcpClients the list of MCP clients to use for discovering tools
86+
* @deprecated use
87+
* {@link #AsyncMcpToolCallbackProvider(McpToolFilter, McpToolNamePrefixGenerator, List)}
8488
*/
89+
@Deprecated
8590
public AsyncMcpToolCallbackProvider(McpToolFilter toolFilter, List<McpAsyncClient> mcpClients) {
91+
this(toolFilter, McpToolNamePrefixGenerator.defaultGenerator(), mcpClients);
92+
}
93+
94+
/**
95+
* Creates a new {@code AsyncMcpToolCallbackProvider} instance with a list of MCP
96+
* clients.
97+
* @param toolFilter a filter to apply to each discovered tool
98+
* @param toolNamePrefixGenerator the tool name prefix generator to use when creating
99+
* tool callbacks.
100+
* @param mcpClients the list of MCP clients to use for discovering tools
101+
*/
102+
public AsyncMcpToolCallbackProvider(McpToolFilter toolFilter, McpToolNamePrefixGenerator toolNamePrefixGenerator,
103+
List<McpAsyncClient> mcpClients) {
86104
Assert.notNull(mcpClients, "MCP clients must not be null");
87105
Assert.notNull(toolFilter, "Tool filter must not be null");
106+
Assert.notNull(toolNamePrefixGenerator, "Tool name prefix generator must not be null");
88107
this.toolFilter = toolFilter;
89108
this.mcpClients = mcpClients;
109+
this.toolNamePrefixGenerator = toolNamePrefixGenerator;
90110
}
91111

92112
/**
@@ -145,9 +165,20 @@ public ToolCallback[] getToolCallbacks() {
145165
ToolCallback[] toolCallbacks = mcpClient.listTools()
146166
.map(response -> response.tools()
147167
.stream()
148-
.filter(tool -> this.toolFilter.test(new McpMetadata(mcpClient.getClientCapabilities(),
149-
mcpClient.getClientInfo(), mcpClient.getCurrentInitializationResult()), tool))
150-
.map(tool -> new AsyncMcpToolCallback(mcpClient, tool))
168+
.filter(tool -> this.toolFilter.test(McpConnectionInfo.builder()
169+
.clientCapabilities(mcpClient.getClientCapabilities())
170+
.clientInfo(mcpClient.getClientInfo())
171+
.initializeResult(mcpClient.getCurrentInitializationResult())
172+
.build(), tool))
173+
.map(tool -> {
174+
McpConnectionInfo connectionInfo = McpConnectionInfo.builder()
175+
.clientCapabilities(mcpClient.getClientCapabilities())
176+
.clientInfo(mcpClient.getClientInfo())
177+
.initializeResult(mcpClient.getCurrentInitializationResult())
178+
.build();
179+
return new AsyncMcpToolCallback(mcpClient, tool,
180+
this.toolNamePrefixGenerator.prefixedToolName(connectionInfo, tool));
181+
})
151182
.toArray(ToolCallback[]::new))
152183
.block();
153184

0 commit comments

Comments
 (0)