fix(mcp): Implement tool de-duplication by name in MCP server

Add de-duplication logic for tools in the MCP server configuration, ensuring that tools
with the same name are not registered multiple times. The implementation keeps the
first occurrence of each tool name and discards duplicates.

- Modified toSyncToolSpecifications and toAsyncToolSpecification methods to de-duplicate tools
- Updated tests to verify that duplicate tools are properly filtered out

Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
This commit is contained in:
Christian Tzolov
2025-03-26 20:14:39 +01:00
parent 3b543cfb68
commit 89ff9ebf5e
2 changed files with 40 additions and 14 deletions

View File

@@ -20,6 +20,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.stream.Collectors;
import io.modelcontextprotocol.server.McpAsyncServer;
import io.modelcontextprotocol.server.McpAsyncServerExchange;
@@ -145,12 +146,25 @@ public class McpServerAutoConfiguration {
private List<McpServerFeatures.SyncToolSpecification> toSyncToolSpecifications(List<ToolCallback> tools,
McpServerProperties serverProperties) {
return tools.stream().map(tool -> {
String toolName = tool.getToolDefinition().name();
MimeType mimeType = (serverProperties.getToolResponseMimeType().containsKey(toolName))
? MimeType.valueOf(serverProperties.getToolResponseMimeType().get(toolName)) : null;
return McpToolUtils.toSyncToolSpecification(tool, mimeType);
}).toList();
// De-duplicate tools by their name, keeping the first occurrence of each tool
// name
return tools.stream()
.collect(Collectors.toMap(tool -> tool.getToolDefinition().name(), // Key:
// tool
// name
tool -> tool, // Value: the tool itself
(existing, replacement) -> existing)) // On duplicate key, keep the
// existing tool
.values()
.stream()
.map(tool -> {
String toolName = tool.getToolDefinition().name();
MimeType mimeType = (serverProperties.getToolResponseMimeType().containsKey(toolName))
? MimeType.valueOf(serverProperties.getToolResponseMimeType().get(toolName)) : null;
return McpToolUtils.toSyncToolSpecification(tool, mimeType);
})
.toList();
}
@Bean
@@ -231,12 +245,24 @@ public class McpServerAutoConfiguration {
private List<McpServerFeatures.AsyncToolSpecification> toAsyncToolSpecification(List<ToolCallback> tools,
McpServerProperties serverProperties) {
return tools.stream().map(tool -> {
String toolName = tool.getToolDefinition().name();
MimeType mimeType = (serverProperties.getToolResponseMimeType().containsKey(toolName))
? MimeType.valueOf(serverProperties.getToolResponseMimeType().get(toolName)) : null;
return McpToolUtils.toAsyncToolSpecification(tool, mimeType);
}).toList();
// De-duplicate tools by their name, keeping the first occurrence of each tool
// name
return tools.stream()
.collect(Collectors.toMap(tool -> tool.getToolDefinition().name(), // Key:
// tool
// name
tool -> tool, // Value: the tool itself
(existing, replacement) -> existing)) // On duplicate key, keep the
// existing tool
.values()
.stream()
.map(tool -> {
String toolName = tool.getToolDefinition().name();
MimeType mimeType = (serverProperties.getToolResponseMimeType().containsKey(toolName))
? MimeType.valueOf(serverProperties.getToolResponseMimeType().get(toolName)) : null;
return McpToolUtils.toAsyncToolSpecification(tool, mimeType);
})
.toList();
}
@Bean

View File

@@ -161,7 +161,7 @@ public class McpServerAutoConfigurationIT {
void toolRegistrationConfiguration() {
this.contextRunner.withUserConfiguration(TestToolConfiguration.class).run(context -> {
List<SyncToolSpecification> tools = context.getBean("syncTools", List.class);
assertThat(tools).hasSize(2);
assertThat(tools).hasSize(1);
});
}
@@ -187,7 +187,7 @@ public class McpServerAutoConfigurationIT {
.withUserConfiguration(TestToolConfiguration.class)
.run(context -> {
List<AsyncToolSpecification> tools = context.getBean("asyncTools", List.class);
assertThat(tools).hasSize(2);
assertThat(tools).hasSize(1);
});
}