Skip to content

Commit 9e5b2a5

Browse files
committed
feat(mcp): Prefix tool names with client name to avoid naming conflicts
Enhances MCP tool naming by prefixing tool names with their client + transport name to prevent conflicts when multiple MCP clients expose tools with the same name. - Modifies client info in McpClientAutoConfiguration to include transport name - Updates SyncMcpToolCallback and AsyncMcpToolCallback to prefix tool names with client + transport name - Adds test coverage for tools with identical names but different client info Resolves #2393 Signed-off-by: Christian Tzolov <[email protected]>
1 parent 28bceb3 commit 9e5b2a5

File tree

7 files changed

+59
-24
lines changed

7 files changed

+59
-24
lines changed

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,8 @@ public List<McpSyncClient> mcpSyncClients(McpSyncClientConfigurer mcpSyncClientC
144144
if (!CollectionUtils.isEmpty(namedTransports)) {
145145
for (NamedClientMcpTransport namedTransport : namedTransports) {
146146

147-
McpSchema.Implementation clientInfo = new McpSchema.Implementation(commonProperties.getName(),
148-
commonProperties.getVersion());
147+
McpSchema.Implementation clientInfo = new McpSchema.Implementation(
148+
commonProperties.getName() + " - " + namedTransport.name(), commonProperties.getVersion());
149149

150150
McpClient.SyncSpec syncSpec = McpClient.sync(namedTransport.transport())
151151
.clientInfo(clientInfo)
@@ -256,8 +256,8 @@ public List<McpAsyncClient> mcpAsyncClients(McpAsyncClientConfigurer mcpSyncClie
256256
if (!CollectionUtils.isEmpty(namedTransports)) {
257257
for (NamedClientMcpTransport namedTransport : namedTransports) {
258258

259-
McpSchema.Implementation clientInfo = new McpSchema.Implementation(commonProperties.getName(),
260-
commonProperties.getVersion());
259+
McpSchema.Implementation clientInfo = new McpSchema.Implementation(
260+
commonProperties.getName() + " - " + namedTransport.name(), commonProperties.getVersion());
261261

262262
McpClient.AsyncSpec syncSpec = McpClient.async(namedTransport.transport())
263263
.clientInfo(clientInfo)

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@
1919

2020
/**
2121
* A named MCP client transport. Usually created by the transport auto-configurations, but
22-
* you can also create them manually. Expose the list castom NamedClientMcpTransport
23-
* as @Bean.
22+
* you can also create them manually.
2423
*
2524
* @param name the name of the transport. Usually the name of the server connection.
2625
* @param transport the MCP client transport.

auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/properties/McpStdioClientProperties.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

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

19-
import java.time.Duration;
2019
import java.util.HashMap;
2120
import java.util.List;
2221
import java.util.Map;
@@ -30,7 +29,6 @@
3029

3130
import org.springframework.boot.context.properties.ConfigurationProperties;
3231
import org.springframework.core.io.Resource;
33-
import org.springframework.util.Assert;
3432

3533
/**
3634
* Configuration properties for the Model Context Protocol (MCP) stdio client.

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.ai.mcp;
1818

1919
import java.util.Map;
20+
import java.util.UUID;
2021

2122
import io.modelcontextprotocol.client.McpAsyncClient;
2223
import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
@@ -84,7 +85,7 @@ public AsyncMcpToolCallback(McpAsyncClient mcpClient, Tool tool) {
8485
@Override
8586
public ToolDefinition getToolDefinition() {
8687
return ToolDefinition.builder()
87-
.name(this.tool.name())
88+
.name(this.asyncMcpClient.getClientInfo().name() + "-" + this.tool.name())
8889
.description(this.tool.description())
8990
.inputSchema(ModelOptionsUtils.toJsonString(this.tool.inputSchema()))
9091
.build();

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.ai.mcp;
1818

1919
import java.util.Map;
20+
import java.util.UUID;
2021

2122
import io.modelcontextprotocol.client.McpSyncClient;
2223
import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
@@ -69,6 +70,7 @@ public class SyncMcpToolCallback implements ToolCallback {
6970
public SyncMcpToolCallback(McpSyncClient mcpClient, Tool tool) {
7071
this.mcpClient = mcpClient;
7172
this.tool = tool;
73+
7274
}
7375

7476
/**
@@ -85,7 +87,7 @@ public SyncMcpToolCallback(McpSyncClient mcpClient, Tool tool) {
8587
@Override
8688
public ToolDefinition getToolDefinition() {
8789
return ToolDefinition.builder()
88-
.name(this.tool.name())
90+
.name(mcpClient.getClientInfo().name() + "-" + this.tool.name())
8991
.description(this.tool.description())
9092
.inputSchema(ModelOptionsUtils.toJsonString(this.tool.inputSchema()))
9193
.build();

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

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.mockito.junit.jupiter.MockitoExtension;
3030

3131
import io.modelcontextprotocol.client.McpSyncClient;
32+
import io.modelcontextprotocol.spec.McpSchema.Implementation;
3233
import io.modelcontextprotocol.spec.McpSchema.ListToolsResult;
3334
import io.modelcontextprotocol.spec.McpSchema.Tool;
3435

@@ -40,23 +41,24 @@ class SyncMcpToolCallbackProviderTests {
4041

4142
@Test
4243
void getToolCallbacksShouldReturnEmptyArrayWhenNoTools() {
43-
// Arrange
44+
4445
ListToolsResult listToolsResult = mock(ListToolsResult.class);
4546
when(listToolsResult.tools()).thenReturn(List.of());
4647
when(mcpClient.listTools()).thenReturn(listToolsResult);
4748

4849
SyncMcpToolCallbackProvider provider = new SyncMcpToolCallbackProvider(mcpClient);
4950

50-
// Act
5151
var callbacks = provider.getToolCallbacks();
5252

53-
// Assert
5453
assertThat(callbacks).isEmpty();
5554
}
5655

5756
@Test
5857
void getToolCallbacksShouldReturnCallbacksForEachTool() {
59-
// Arrange
58+
59+
var clientInfo = new Implementation("testClient", "1.0.0");
60+
when(mcpClient.getClientInfo()).thenReturn(clientInfo);
61+
6062
Tool tool1 = mock(Tool.class);
6163
when(tool1.name()).thenReturn("tool1");
6264

@@ -69,16 +71,16 @@ void getToolCallbacksShouldReturnCallbacksForEachTool() {
6971

7072
SyncMcpToolCallbackProvider provider = new SyncMcpToolCallbackProvider(mcpClient);
7173

72-
// Act
7374
var callbacks = provider.getToolCallbacks();
7475

75-
// Assert
7676
assertThat(callbacks).hasSize(2);
7777
}
7878

7979
@Test
8080
void getToolCallbacksShouldThrowExceptionForDuplicateToolNames() {
81-
// Arrange
81+
var clientInfo = new Implementation("testClient", "1.0.0");
82+
when(mcpClient.getClientInfo()).thenReturn(clientInfo);
83+
8284
Tool tool1 = mock(Tool.class);
8385
when(tool1.name()).thenReturn("sameName");
8486

@@ -91,9 +93,40 @@ void getToolCallbacksShouldThrowExceptionForDuplicateToolNames() {
9193

9294
SyncMcpToolCallbackProvider provider = new SyncMcpToolCallbackProvider(mcpClient);
9395

94-
// Act & Assert
9596
assertThatThrownBy(() -> provider.getToolCallbacks()).isInstanceOf(IllegalStateException.class)
9697
.hasMessageContaining("Multiple tools with the same name");
9798
}
9899

100+
@Test
101+
void getSameNameToolsButDifferntClientInfoNamesShouldProduceDifferentToolCallbackNames() {
102+
103+
Tool tool1 = mock(Tool.class);
104+
when(tool1.name()).thenReturn("sameName");
105+
106+
Tool tool2 = mock(Tool.class);
107+
when(tool2.name()).thenReturn("sameName");
108+
109+
McpSyncClient mcpClient1 = mock(McpSyncClient.class);
110+
ListToolsResult listToolsResult1 = mock(ListToolsResult.class);
111+
when(listToolsResult1.tools()).thenReturn(List.of(tool1));
112+
when(mcpClient1.listTools()).thenReturn(listToolsResult1);
113+
114+
var clientInfo1 = new Implementation("testClient1", "1.0.0");
115+
when(mcpClient1.getClientInfo()).thenReturn(clientInfo1);
116+
117+
McpSyncClient mcpClient2 = mock(McpSyncClient.class);
118+
ListToolsResult listToolsResult2 = mock(ListToolsResult.class);
119+
when(listToolsResult2.tools()).thenReturn(List.of(tool2));
120+
when(mcpClient2.listTools()).thenReturn(listToolsResult2);
121+
122+
var clientInfo2 = new Implementation("testClient2", "1.0.0");
123+
when(mcpClient2.getClientInfo()).thenReturn(clientInfo2);
124+
125+
SyncMcpToolCallbackProvider provider = new SyncMcpToolCallbackProvider(mcpClient1, mcpClient2);
126+
127+
var callbacks = provider.getToolCallbacks();
128+
129+
assertThat(callbacks).hasSize(2);
130+
}
131+
99132
}

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

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import io.modelcontextprotocol.client.McpSyncClient;
2020
import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
2121
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
22+
import io.modelcontextprotocol.spec.McpSchema.Implementation;
2223
import io.modelcontextprotocol.spec.McpSchema.Tool;
2324
import org.junit.jupiter.api.Test;
2425
import org.junit.jupiter.api.extension.ExtendWith;
@@ -41,30 +42,31 @@ class SyncMcpToolCallbackTests {
4142

4243
@Test
4344
void getToolDefinitionShouldReturnCorrectDefinition() {
44-
// Arrange
45+
46+
var clientInfo = new Implementation("testClient", "1.0.0");
47+
when(mcpClient.getClientInfo()).thenReturn(clientInfo);
4548
when(tool.name()).thenReturn("testTool");
4649
when(tool.description()).thenReturn("Test tool description");
4750

4851
SyncMcpToolCallback callback = new SyncMcpToolCallback(mcpClient, tool);
4952

50-
// Act
5153
var toolDefinition = callback.getToolDefinition();
5254

53-
// Assert
54-
assertThat(toolDefinition.name()).isEqualTo("testTool");
55+
assertThat(toolDefinition.name()).isEqualTo(clientInfo.name() + "-testTool");
5556
assertThat(toolDefinition.description()).isEqualTo("Test tool description");
5657
}
5758

5859
@Test
5960
void callShouldHandleJsonInputAndOutput() {
60-
// Arrange
61+
62+
when(mcpClient.getClientInfo()).thenReturn(new Implementation("testClient", "1.0.0"));
63+
6164
when(tool.name()).thenReturn("testTool");
6265
CallToolResult callResult = mock(CallToolResult.class);
6366
when(mcpClient.callTool(any(CallToolRequest.class))).thenReturn(callResult);
6467

6568
SyncMcpToolCallback callback = new SyncMcpToolCallback(mcpClient, tool);
6669

67-
// Act
6870
String response = callback.call("{\"param\":\"value\"}");
6971

7072
// Assert

0 commit comments

Comments
 (0)