From 89ff9ebf5eac0646fa4a7226fd814e904571b851 Mon Sep 17 00:00:00 2001 From: Christian Tzolov Date: Wed, 26 Mar 2025 20:14:39 +0100 Subject: [PATCH] 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 --- .../McpServerAutoConfiguration.java | 50 ++++++++++++++----- .../McpServerAutoConfigurationIT.java | 4 +- 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/main/java/org/springframework/ai/mcp/server/autoconfigure/McpServerAutoConfiguration.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/main/java/org/springframework/ai/mcp/server/autoconfigure/McpServerAutoConfiguration.java index 64d80f420..7838a7440 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/main/java/org/springframework/ai/mcp/server/autoconfigure/McpServerAutoConfiguration.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/main/java/org/springframework/ai/mcp/server/autoconfigure/McpServerAutoConfiguration.java @@ -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 toSyncToolSpecifications(List 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 toAsyncToolSpecification(List 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 diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/test/java/org/springframework/ai/mcp/server/autoconfigure/McpServerAutoConfigurationIT.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/test/java/org/springframework/ai/mcp/server/autoconfigure/McpServerAutoConfigurationIT.java index 29eff9e5d..88d9c3580 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/test/java/org/springframework/ai/mcp/server/autoconfigure/McpServerAutoConfigurationIT.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/test/java/org/springframework/ai/mcp/server/autoconfigure/McpServerAutoConfigurationIT.java @@ -161,7 +161,7 @@ public class McpServerAutoConfigurationIT { void toolRegistrationConfiguration() { this.contextRunner.withUserConfiguration(TestToolConfiguration.class).run(context -> { List 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 tools = context.getBean("asyncTools", List.class); - assertThat(tools).hasSize(2); + assertThat(tools).hasSize(1); }); }