Skip to content

Commit 9571501

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 58ed5ea commit 9571501

File tree

7 files changed

+73
-25
lines changed

7 files changed

+73
-25
lines changed

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

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,18 @@
110110
matchIfMissing = true)
111111
public class McpClientAutoConfiguration {
112112

113+
/**
114+
* Create a dynamic client name based on the client name and the name of the server
115+
* connection.
116+
* @param clientName the client name as defined by the configuration
117+
* @param serverConnectionName the name of the server connection being used by the
118+
* client
119+
* @return the connected client name
120+
*/
121+
private String connectedClientName(String clientName, String serverConnectionName) {
122+
return clientName + " - " + serverConnectionName;
123+
}
124+
113125
/**
114126
* Creates a list of {@link McpSyncClient} instances based on the available
115127
* transports.
@@ -144,7 +156,8 @@ public List<McpSyncClient> mcpSyncClients(McpSyncClientConfigurer mcpSyncClientC
144156
if (!CollectionUtils.isEmpty(namedTransports)) {
145157
for (NamedClientMcpTransport namedTransport : namedTransports) {
146158

147-
McpSchema.Implementation clientInfo = new McpSchema.Implementation(commonProperties.getName(),
159+
McpSchema.Implementation clientInfo = new McpSchema.Implementation(
160+
this.connectedClientName(commonProperties.getName(), namedTransport.name()),
148161
commonProperties.getVersion());
149162

150163
McpClient.SyncSpec syncSpec = McpClient.sync(namedTransport.transport())
@@ -256,7 +269,8 @@ public List<McpAsyncClient> mcpAsyncClients(McpAsyncClientConfigurer mcpSyncClie
256269
if (!CollectionUtils.isEmpty(namedTransports)) {
257270
for (NamedClientMcpTransport namedTransport : namedTransports) {
258271

259-
McpSchema.Implementation clientInfo = new McpSchema.Implementation(commonProperties.getName(),
272+
McpSchema.Implementation clientInfo = new McpSchema.Implementation(
273+
this.connectedClientName(commonProperties.getName(), namedTransport.name()),
260274
commonProperties.getVersion());
261275

262276
McpClient.AsyncSpec syncSpec = McpClient.async(namedTransport.transport())

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;
@@ -85,7 +86,7 @@ public AsyncMcpToolCallback(McpAsyncClient mcpClient, Tool tool) {
8586
@Override
8687
public ToolDefinition getToolDefinition() {
8788
return ToolDefinition.builder()
88-
.name(this.tool.name())
89+
.name(this.asyncMcpClient.getClientInfo().name() + "-" + this.tool.name())
8990
.description(this.tool.description())
9091
.inputSchema(ModelOptionsUtils.toJsonString(this.tool.inputSchema()))
9192
.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;
@@ -70,6 +71,7 @@ public class SyncMcpToolCallback implements ToolCallback {
7071
public SyncMcpToolCallback(McpSyncClient mcpClient, Tool tool) {
7172
this.mcpClient = mcpClient;
7273
this.tool = tool;
74+
7375
}
7476

7577
/**
@@ -86,7 +88,7 @@ public SyncMcpToolCallback(McpSyncClient mcpClient, Tool tool) {
8688
@Override
8789
public ToolDefinition getToolDefinition() {
8890
return ToolDefinition.builder()
89-
.name(this.tool.name())
91+
.name(mcpClient.getClientInfo().name() + "-" + this.tool.name())
9092
.description(this.tool.description())
9193
.inputSchema(ModelOptionsUtils.toJsonString(this.tool.inputSchema()))
9294
.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: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import io.modelcontextprotocol.client.McpSyncClient;
2222
import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
2323
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
24+
import io.modelcontextprotocol.spec.McpSchema.Implementation;
2425
import io.modelcontextprotocol.spec.McpSchema.Tool;
2526
import org.junit.jupiter.api.Test;
2627
import org.junit.jupiter.api.extension.ExtendWith;
@@ -45,30 +46,31 @@ class SyncMcpToolCallbackTests {
4546

4647
@Test
4748
void getToolDefinitionShouldReturnCorrectDefinition() {
48-
// Arrange
49+
50+
var clientInfo = new Implementation("testClient", "1.0.0");
51+
when(mcpClient.getClientInfo()).thenReturn(clientInfo);
4952
when(tool.name()).thenReturn("testTool");
5053
when(tool.description()).thenReturn("Test tool description");
5154

5255
SyncMcpToolCallback callback = new SyncMcpToolCallback(mcpClient, tool);
5356

54-
// Act
5557
var toolDefinition = callback.getToolDefinition();
5658

57-
// Assert
58-
assertThat(toolDefinition.name()).isEqualTo("testTool");
59+
assertThat(toolDefinition.name()).isEqualTo(clientInfo.name() + "-testTool");
5960
assertThat(toolDefinition.description()).isEqualTo("Test tool description");
6061
}
6162

6263
@Test
6364
void callShouldHandleJsonInputAndOutput() {
64-
// Arrange
65+
66+
when(mcpClient.getClientInfo()).thenReturn(new Implementation("testClient", "1.0.0"));
67+
6568
when(tool.name()).thenReturn("testTool");
6669
CallToolResult callResult = mock(CallToolResult.class);
6770
when(mcpClient.callTool(any(CallToolRequest.class))).thenReturn(callResult);
6871

6972
SyncMcpToolCallback callback = new SyncMcpToolCallback(mcpClient, tool);
7073

71-
// Act
7274
String response = callback.call("{\"param\":\"value\"}");
7375

7476
// Assert
@@ -77,17 +79,16 @@ void callShouldHandleJsonInputAndOutput() {
7779

7880
@Test
7981
void callShoulIngroeToolContext() {
80-
// Arrange
82+
when(mcpClient.getClientInfo()).thenReturn(new Implementation("testClient", "1.0.0"));
83+
8184
when(tool.name()).thenReturn("testTool");
8285
CallToolResult callResult = mock(CallToolResult.class);
8386
when(mcpClient.callTool(any(CallToolRequest.class))).thenReturn(callResult);
8487

8588
SyncMcpToolCallback callback = new SyncMcpToolCallback(mcpClient, tool);
8689

87-
// Act
8890
String response = callback.call("{\"param\":\"value\"}", new ToolContext(Map.of("foo", "bar")));
8991

90-
// Assert
9192
assertThat(response).isNotNull();
9293
}
9394

0 commit comments

Comments
 (0)