Skip to content

Commit f5e8349

Browse files
tzolovmarkpollack
authored andcommitted
feat(mcp): enhance tool naming with server title and prefix shortening
- Add title parameter to McpSchema.Implementation constructor to distinguish between client name and server name - Update prefixedToolName() to include optional title parameter and shorten long prefixes - Implement shorten() method to abbreviate prefixes by taking first letter of each word - Ensure generated tool names stay within 64-character limit - Update all related tests to use new constructor signature and verify shortened names This change improves tool name uniqueness when multiple MCP servers provide tools with the same name, while keeping names concise through intelligent prefix shortening. Signed-off-by: Christian Tzolov <[email protected]>
1 parent 677730f commit f5e8349

File tree

14 files changed

+128
-82
lines changed

14 files changed

+128
-82
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ public List<McpSyncClient> mcpSyncClients(McpSyncClientConfigurer mcpSyncClientC
172172

173173
McpSchema.Implementation clientInfo = new McpSchema.Implementation(
174174
this.connectedClientName(commonProperties.getName(), namedTransport.name()),
175-
commonProperties.getVersion());
175+
namedTransport.name(), commonProperties.getVersion());
176176

177177
McpClient.SyncSpec spec = McpClient.sync(namedTransport.transport())
178178
.clientInfo(clientInfo)

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ List<ToolCallback> testDuplicateToolCallbacks() {
261261
Mockito.when(mockTool1.name()).thenReturn("duplicate-tool");
262262
Mockito.when(mockTool1.description()).thenReturn("First Tool");
263263
Mockito.when(mockClient1.callTool(Mockito.any(McpSchema.CallToolRequest.class))).thenReturn(mockResult1);
264-
when(mockClient1.getClientInfo()).thenReturn(new McpSchema.Implementation("testClient1", "1.0.0"));
264+
when(mockClient1.getClientInfo()).thenReturn(new McpSchema.Implementation("frist_client", "1.0.0"));
265265

266266
McpSyncClient mockClient2 = Mockito.mock(McpSyncClient.class);
267267
McpSchema.Tool mockTool2 = Mockito.mock(McpSchema.Tool.class);
@@ -270,7 +270,7 @@ List<ToolCallback> testDuplicateToolCallbacks() {
270270
Mockito.when(mockTool2.name()).thenReturn("duplicate-tool");
271271
Mockito.when(mockTool2.description()).thenReturn("Second Tool");
272272
Mockito.when(mockClient2.callTool(Mockito.any(McpSchema.CallToolRequest.class))).thenReturn(mockResult2);
273-
when(mockClient2.getClientInfo()).thenReturn(new McpSchema.Implementation("testClient2", "1.0.0"));
273+
when(mockClient2.getClientInfo()).thenReturn(new McpSchema.Implementation("second_client", "1.0.0"));
274274

275275
return List.of(SyncMcpToolCallback.builder().mcpClient(mockClient1).tool(mockTool1).build(),
276276
SyncMcpToolCallback.builder().mcpClient(mockClient2).tool(mockTool2).build());

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ List<ToolCallback> testDuplicateToolCallbacks() {
261261
Mockito.when(mockTool1.name()).thenReturn("duplicate-tool");
262262
Mockito.when(mockTool1.description()).thenReturn("First Tool");
263263
Mockito.when(mockClient1.callTool(Mockito.any(McpSchema.CallToolRequest.class))).thenReturn(mockResult1);
264-
when(mockClient1.getClientInfo()).thenReturn(new McpSchema.Implementation("testClient1", "1.0.0"));
264+
when(mockClient1.getClientInfo()).thenReturn(new McpSchema.Implementation("client", "server1", "1.0.0"));
265265

266266
McpSyncClient mockClient2 = Mockito.mock(McpSyncClient.class);
267267
McpSchema.Tool mockTool2 = Mockito.mock(McpSchema.Tool.class);
@@ -270,7 +270,7 @@ List<ToolCallback> testDuplicateToolCallbacks() {
270270
Mockito.when(mockTool2.name()).thenReturn("duplicate-tool");
271271
Mockito.when(mockTool2.description()).thenReturn("Second Tool");
272272
Mockito.when(mockClient2.callTool(Mockito.any(McpSchema.CallToolRequest.class))).thenReturn(mockResult2);
273-
when(mockClient2.getClientInfo()).thenReturn(new McpSchema.Implementation("testClient2", "1.0.0"));
273+
when(mockClient2.getClientInfo()).thenReturn(new McpSchema.Implementation("client", "server2", "1.0.0"));
274274

275275
return List.of(SyncMcpToolCallback.builder().mcpClient(mockClient1).tool(mockTool1).build(),
276276
SyncMcpToolCallback.builder().mcpClient(mockClient2).tool(mockTool2).build());

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@ public class AsyncMcpToolCallback implements ToolCallback {
6666
*/
6767
@Deprecated
6868
public AsyncMcpToolCallback(McpAsyncClient mcpClient, Tool tool) {
69-
this(mcpClient, tool, McpToolUtils.prefixedToolName(mcpClient.getClientInfo().name(), tool.name()),
70-
ToolContextToMcpMetaConverter.defaultConverter());
69+
this(mcpClient, tool, McpToolUtils.prefixedToolName(mcpClient.getClientInfo().name(),
70+
mcpClient.getClientInfo().title(), tool.name()), ToolContextToMcpMetaConverter.defaultConverter());
7171
}
7272

7373
/**

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public interface McpToolNamePrefixGenerator {
4747
*/
4848
static McpToolNamePrefixGenerator defaultGenerator() {
4949
return (mcpConnectionIfo, tool) -> McpToolUtils.prefixedToolName(mcpConnectionIfo.clientInfo().name(),
50-
tool.name());
50+
mcpConnectionIfo.clientInfo().title(), tool.name());
5151
}
5252

5353
/**

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

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.util.Map;
2121
import java.util.Optional;
2222
import java.util.function.BiFunction;
23+
import java.util.stream.Stream;
2324

2425
import com.fasterxml.jackson.annotation.JsonAlias;
2526
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@@ -73,28 +74,62 @@ public final class McpToolUtils {
7374
private McpToolUtils() {
7475
}
7576

76-
public static String prefixedToolName(String prefix, String toolName) {
77+
/**
78+
* @param prefix Client name, combination of client info name and the 'server'
79+
* connection name.
80+
* @param title Server connection name
81+
* @param toolName original MCP server tool name.
82+
* @return the prefix to use for the tool to avoid name collisions.
83+
*/
84+
public static String prefixedToolName(String prefix, String title, String toolName) {
7785

7886
if (StringUtils.isEmpty(prefix) || StringUtils.isEmpty(toolName)) {
7987
throw new IllegalArgumentException("Prefix or toolName cannot be null or empty");
8088
}
8189

82-
String input = prefix + "_" + toolName;
90+
String input = shorten(format(prefix));
91+
if (!StringUtils.isEmpty(title)) {
92+
input = input + "_" + format(title); // Do not shorten the title.
93+
}
94+
95+
input = input + "_" + format(toolName);
8396

97+
// If the string is longer than 64 characters, keep the last 64 characters
98+
if (input.length() > 64) {
99+
input = input.substring(input.length() - 64);
100+
}
101+
102+
return input;
103+
}
104+
105+
public static String prefixedToolName(String prefix, String toolName) {
106+
return prefixedToolName(prefix, null, toolName);
107+
}
108+
109+
private static String format(String input) {
84110
// Replace any character that isn't alphanumeric, underscore, or hyphen with
85111
// concatenation. Support Han script + CJK blocks for complete Chinese character
86112
// coverage
87113
String formatted = input
88114
.replaceAll("[^\\p{IsHan}\\p{InCJK_Unified_Ideographs}\\p{InCJK_Compatibility_Ideographs}a-zA-Z0-9_-]", "");
89115

90-
formatted = formatted.replaceAll("-", "_");
116+
return formatted.replaceAll("-", "_");
117+
}
91118

92-
// If the string is longer than 64 characters, keep the last 64 characters
93-
if (formatted.length() > 64) {
94-
formatted = formatted.substring(formatted.length() - 64);
119+
/**
120+
* Shortens a string by taking the first letter of each word separated by underscores
121+
* @param input String in format "Word1_Word2_Word3_server"
122+
* @return Shortened string with first letters in lowercase "w_w_w_s"
123+
*/
124+
private static String shorten(String input) {
125+
if (input == null || input.isEmpty()) {
126+
return "";
95127
}
96128

97-
return formatted;
129+
return Stream.of(input.toLowerCase().split("_"))
130+
.filter(word -> !word.isEmpty())
131+
.map(word -> String.valueOf(word.charAt(0)))
132+
.collect(java.util.stream.Collectors.joining("_"));
98133
}
99134

100135
/**

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,8 @@ public class SyncMcpToolCallback implements ToolCallback {
6363
*/
6464
@Deprecated
6565
public SyncMcpToolCallback(McpSyncClient mcpClient, Tool tool) {
66-
this(mcpClient, tool, McpToolUtils.prefixedToolName(mcpClient.getClientInfo().name(), tool.name()),
67-
ToolContextToMcpMetaConverter.defaultConverter());
66+
this(mcpClient, tool, McpToolUtils.prefixedToolName(mcpClient.getClientInfo().name(),
67+
mcpClient.getClientInfo().title(), tool.name()), ToolContextToMcpMetaConverter.defaultConverter());
6868
}
6969

7070
/**

mcp/common/src/test/java/org/springframework/ai/mcp/AsyncMcpToolCallbackProviderTests.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -225,8 +225,8 @@ void toolFilterShouldFilterToolsByNameWhenConfigured() {
225225
var callbacks = provider.getToolCallbacks();
226226

227227
assertThat(callbacks).hasSize(2);
228-
assertThat(callbacks[0].getToolDefinition().name()).isEqualTo("testClient_tool2");
229-
assertThat(callbacks[1].getToolDefinition().name()).isEqualTo("testClient_tool3");
228+
assertThat(callbacks[0].getToolDefinition().name()).isEqualTo("t_tool2");
229+
assertThat(callbacks[1].getToolDefinition().name()).isEqualTo("t_tool3");
230230
}
231231

232232
@Test
@@ -266,7 +266,7 @@ void toolFilterShouldFilterToolsByClientWhenConfigured() {
266266
var callbacks = provider.getToolCallbacks();
267267

268268
assertThat(callbacks).hasSize(1);
269-
assertThat(callbacks[0].getToolDefinition().name()).isEqualTo("testClient1_tool1");
269+
assertThat(callbacks[0].getToolDefinition().name()).isEqualTo("t_tool1");
270270
}
271271

272272
@Test
@@ -299,7 +299,7 @@ void toolFilterShouldCombineClientAndToolCriteriaWhenConfigured() {
299299
var callbacks = provider.getToolCallbacks();
300300

301301
assertThat(callbacks).hasSize(1);
302-
assertThat(callbacks[0].getToolDefinition().name()).isEqualTo("weather_service_weather");
302+
assertThat(callbacks[0].getToolDefinition().name()).isEqualTo("w_s_weather");
303303
}
304304

305305
@Test

mcp/common/src/test/java/org/springframework/ai/mcp/SyncMcpToolCallbackBuilderTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ void builderShouldCreateInstanceWithRequiredFields() {
4949
assertThat(callback).isNotNull();
5050
assertThat(callback.getOriginalToolName()).isEqualTo("test-tool");
5151
assertThat(callback.getToolDefinition()).isNotNull();
52-
assertThat(callback.getToolDefinition().name()).isEqualTo("test_client_test_tool");
52+
assertThat(callback.getToolDefinition().name()).isEqualTo("t_c_test_tool");
5353
assertThat(callback.getToolDefinition().description()).isEqualTo("Test tool description");
5454
}
5555

mcp/common/src/test/java/org/springframework/ai/mcp/SyncMcpToolCallbackProviderBuilderTest.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ void builderShouldCreateInstanceWithSingleClient() {
4747
assertThat(provider).isNotNull();
4848
ToolCallback[] callbacks = provider.getToolCallbacks();
4949
assertThat(callbacks).hasSize(1);
50-
assertThat(callbacks[0].getToolDefinition().name()).isEqualTo("test_client_test_tool");
50+
assertThat(callbacks[0].getToolDefinition().name()).isEqualTo("t_c_test_tool");
5151
}
5252

5353
@Test
@@ -64,8 +64,8 @@ void builderShouldCreateInstanceWithMultipleClients() {
6464
assertThat(provider).isNotNull();
6565
ToolCallback[] callbacks = provider.getToolCallbacks();
6666
assertThat(callbacks).hasSize(2);
67-
assertThat(callbacks[0].getToolDefinition().name()).isEqualTo("client1_tool1");
68-
assertThat(callbacks[1].getToolDefinition().name()).isEqualTo("client2_tool2");
67+
assertThat(callbacks[0].getToolDefinition().name()).isEqualTo("c_tool1");
68+
assertThat(callbacks[1].getToolDefinition().name()).isEqualTo("c_tool2");
6969
}
7070

7171
@Test
@@ -111,7 +111,7 @@ void builderShouldCreateInstanceWithCustomToolFilter() {
111111
assertThat(provider).isNotNull();
112112
ToolCallback[] callbacks = provider.getToolCallbacks();
113113
assertThat(callbacks).hasSize(1);
114-
assertThat(callbacks[0].getToolDefinition().name()).isEqualTo("client_filtered_tool");
114+
assertThat(callbacks[0].getToolDefinition().name()).isEqualTo("c_filtered_tool");
115115
}
116116

117117
@Test
@@ -230,8 +230,8 @@ void builderShouldReplaceClientsWhenSettingNewList() {
230230
assertThat(provider).isNotNull();
231231
ToolCallback[] callbacks = provider.getToolCallbacks();
232232
assertThat(callbacks).hasSize(2);
233-
assertThat(callbacks[0].getToolDefinition().name()).isEqualTo("client2_tool2");
234-
assertThat(callbacks[1].getToolDefinition().name()).isEqualTo("client3_tool3");
233+
assertThat(callbacks[0].getToolDefinition().name()).isEqualTo("c_tool2");
234+
assertThat(callbacks[1].getToolDefinition().name()).isEqualTo("c_tool3");
235235
}
236236

237237
private McpSyncClient createMockClient(String clientName, String toolName) {

0 commit comments

Comments
 (0)