diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/main/java/org/springframework/ai/mcp/server/autoconfigure/McpBackwardCompatibility.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/main/java/org/springframework/ai/mcp/server/autoconfigure/McpBackwardCompatibility.java deleted file mode 100644 index 3d4e59be6d8..00000000000 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/main/java/org/springframework/ai/mcp/server/autoconfigure/McpBackwardCompatibility.java +++ /dev/null @@ -1,112 +0,0 @@ -/* -* Copyright 2025 - 2025 the original author or authors. -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* https://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -*/ -package org.springframework.ai.mcp.server.autoconfigure; - -import java.util.List; -import java.util.function.BiConsumer; -import java.util.function.Consumer; - -import io.modelcontextprotocol.server.McpServerFeatures; -import io.modelcontextprotocol.server.McpSyncServerExchange; -import io.modelcontextprotocol.spec.McpSchema; - -import org.springframework.beans.factory.ObjectProvider; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.util.CollectionUtils; - -/** - * @author Christian Tzolov - */ -@Deprecated -@Configuration -public class McpBackwardCompatibility { - - @Bean - public List>> syncRootsChangeConsumerToHandler( - List>> rootsChangeConsumers) { - - if (CollectionUtils.isEmpty(rootsChangeConsumers)) { - return List.of(); - } - - return rootsChangeConsumers.stream() - .map(c -> (BiConsumer>) ((exchange, roots) -> c.accept(roots))) - .toList(); - } - - @Bean - public List syncToolsRegistrationToSpecificaiton( - ObjectProvider> toolRegistrations) { - - return toolRegistrations.stream() - .flatMap(List::stream) - .map(McpServerFeatures.SyncToolRegistration::toSpecification) - .toList(); - } - - @Bean - public List syncResourceRegistrationToSpecificaiton( - ObjectProvider> resourceRegistrations) { - - return resourceRegistrations.stream() - .flatMap(List::stream) - .map(McpServerFeatures.SyncResourceRegistration::toSpecification) - .toList(); - } - - @Bean - public List syncPromptRegistrationToSpecificaiton( - ObjectProvider> promptRegistrations) { - - return promptRegistrations.stream() - .flatMap(List::stream) - .map(McpServerFeatures.SyncPromptRegistration::toSpecification) - .toList(); - } - - // Async - @Bean - public List asyncToolsRegistrationToSpecificaiton( - ObjectProvider> toolRegistrations) { - - return toolRegistrations.stream() - .flatMap(List::stream) - .map(McpServerFeatures.AsyncToolRegistration::toSpecification) - .toList(); - } - - @Bean - public List asyncResourceRegistrationToSpecificaiton( - ObjectProvider> resourceRegistrations) { - - return resourceRegistrations.stream() - .flatMap(List::stream) - .map(McpServerFeatures.AsyncResourceRegistration::toSpecification) - .toList(); - } - - @Bean - public List asyncPromptRegistrationToSpecificaiton( - ObjectProvider> promptRegistrations) { - - return promptRegistrations.stream() - .flatMap(List::stream) - .map(McpServerFeatures.AsyncPromptRegistration::toSpecification) - .toList(); - } - -} 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 7838a74408e..7c88552e618 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 @@ -53,7 +53,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Import; import org.springframework.core.log.LogAccessor; import org.springframework.util.CollectionUtils; import org.springframework.util.MimeType; @@ -110,7 +109,6 @@ @AutoConfiguration(after = { McpWebMvcServerAutoConfiguration.class, McpWebFluxServerAutoConfiguration.class }) @ConditionalOnClass({ McpSchema.class, McpSyncServer.class }) @EnableConfigurationProperties(McpServerProperties.class) -@Import(McpBackwardCompatibility.class) @ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) public class McpServerAutoConfiguration { diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/main/java/org/springframework/ai/mcp/server/autoconfigure/McpServerProperties.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/main/java/org/springframework/ai/mcp/server/autoconfigure/McpServerProperties.java index 938a74aec9a..92b26007e80 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/main/java/org/springframework/ai/mcp/server/autoconfigure/McpServerProperties.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/main/java/org/springframework/ai/mcp/server/autoconfigure/McpServerProperties.java @@ -98,6 +98,14 @@ public class McpServerProperties { */ private boolean promptChangeNotification = true; + /** + */ + private String baseUrl = ""; + + /** + */ + private String sseEndpoint = "/sse"; + /** * The endpoint path for Server-Sent Events (SSE) when using web transports. *

@@ -196,6 +204,24 @@ public void setPromptChangeNotification(boolean promptChangeNotification) { this.promptChangeNotification = promptChangeNotification; } + public String getBaseUrl() { + return this.baseUrl; + } + + public void setBaseUrl(String baseUrl) { + Assert.notNull(baseUrl, "Base URL must not be null"); + this.baseUrl = baseUrl; + } + + public String getSseEndpoint() { + return this.sseEndpoint; + } + + public void setSseEndpoint(String sseEndpoint) { + Assert.hasText(sseEndpoint, "SSE endpoint must not be empty"); + this.sseEndpoint = sseEndpoint; + } + public String getSseMessageEndpoint() { return this.sseMessageEndpoint; } diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/test/java/org/springframework/ai/mcp/server/autoconfigure/McpServerAutoConfigurationBackwardCompatibilityIT.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/test/java/org/springframework/ai/mcp/server/autoconfigure/McpServerAutoConfigurationBackwardCompatibilityIT.java deleted file mode 100644 index fa50f74d8e9..00000000000 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-server/src/test/java/org/springframework/ai/mcp/server/autoconfigure/McpServerAutoConfigurationBackwardCompatibilityIT.java +++ /dev/null @@ -1,319 +0,0 @@ -/* - * Copyright 2025-2025 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.ai.mcp.server.autoconfigure; - -import java.util.List; -import java.util.function.Consumer; -import java.util.function.Function; - -import com.fasterxml.jackson.core.type.TypeReference; -import io.modelcontextprotocol.client.McpSyncClient; -import io.modelcontextprotocol.server.McpAsyncServer; -import io.modelcontextprotocol.server.McpServerFeatures.AsyncToolSpecification; -import io.modelcontextprotocol.server.McpServerFeatures.SyncPromptRegistration; -import io.modelcontextprotocol.server.McpServerFeatures.SyncResourceRegistration; -import io.modelcontextprotocol.server.McpServerFeatures.SyncToolSpecification; -import io.modelcontextprotocol.server.McpSyncServer; -import io.modelcontextprotocol.server.transport.StdioServerTransportProvider; -import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.McpServerTransport; -import io.modelcontextprotocol.spec.McpServerTransportProvider; -import io.modelcontextprotocol.spec.ServerMcpTransport; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; -import reactor.core.publisher.Mono; - -import org.springframework.ai.mcp.SyncMcpToolCallback; -import org.springframework.ai.tool.ToolCallback; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.when; - -@Disabled -public class McpServerAutoConfigurationBackwardCompatibilityIT { - - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(McpServerAutoConfiguration.class)); - - @Test - void defaultConfiguration() { - this.contextRunner.run(context -> { - assertThat(context).hasSingleBean(McpSyncServer.class); - assertThat(context).hasSingleBean(McpServerTransportProvider.class); - assertThat(context.getBean(McpServerTransportProvider.class)) - .isInstanceOf(StdioServerTransportProvider.class); - - McpServerProperties properties = context.getBean(McpServerProperties.class); - assertThat(properties.getName()).isEqualTo("mcp-server"); - assertThat(properties.getVersion()).isEqualTo("1.0.0"); - assertThat(properties.getType()).isEqualTo(McpServerProperties.ServerType.SYNC); - assertThat(properties.isToolChangeNotification()).isTrue(); - assertThat(properties.isResourceChangeNotification()).isTrue(); - assertThat(properties.isPromptChangeNotification()).isTrue(); - }); - } - - @Test - void asyncConfiguration() { - this.contextRunner - .withPropertyValues("spring.ai.mcp.server.type=ASYNC", "spring.ai.mcp.server.name=test-server", - "spring.ai.mcp.server.version=2.0.0") - .run(context -> { - assertThat(context).hasSingleBean(McpAsyncServer.class); - assertThat(context).doesNotHaveBean(McpSyncServer.class); - - McpServerProperties properties = context.getBean(McpServerProperties.class); - assertThat(properties.getName()).isEqualTo("test-server"); - assertThat(properties.getVersion()).isEqualTo("2.0.0"); - assertThat(properties.getType()).isEqualTo(McpServerProperties.ServerType.ASYNC); - }); - } - - @Test - void transportConfiguration() { - this.contextRunner.withUserConfiguration(CustomTransportConfiguration.class).run(context -> { - assertThat(context).hasSingleBean(McpServerTransport.class); - assertThat(context.getBean(McpServerTransport.class)).isInstanceOf(CustomServerTransport.class); - }); - } - - @Test - void serverNotificationConfiguration() { - this.contextRunner - .withPropertyValues("spring.ai.mcp.server.tool-change-notification=false", - "spring.ai.mcp.server.resource-change-notification=false") - .run(context -> { - McpServerProperties properties = context.getBean(McpServerProperties.class); - assertThat(properties.isToolChangeNotification()).isFalse(); - assertThat(properties.isResourceChangeNotification()).isFalse(); - }); - } - - // @Test - void invalidConfigurationThrowsException() { - this.contextRunner.withPropertyValues("spring.ai.mcp.server.version=invalid-version").run(context -> { - assertThat(context).hasFailed(); - assertThat(context).getFailure() - .hasRootCauseInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("Invalid version format"); - }); - } - - @Test - void disabledConfiguration() { - this.contextRunner.withPropertyValues("spring.ai.mcp.server.enabled=false").run(context -> { - assertThat(context).doesNotHaveBean(McpSyncServer.class); - assertThat(context).doesNotHaveBean(McpAsyncServer.class); - assertThat(context).doesNotHaveBean(ServerMcpTransport.class); - }); - } - - @Test - void notificationConfiguration() { - this.contextRunner - .withPropertyValues("spring.ai.mcp.server.tool-change-notification=false", - "spring.ai.mcp.server.resource-change-notification=false", - "spring.ai.mcp.server.prompt-change-notification=false") - .run(context -> { - McpServerProperties properties = context.getBean(McpServerProperties.class); - assertThat(properties.isToolChangeNotification()).isFalse(); - assertThat(properties.isResourceChangeNotification()).isFalse(); - assertThat(properties.isPromptChangeNotification()).isFalse(); - }); - } - - @Test - void stdioConfiguration() { - this.contextRunner.withPropertyValues("spring.ai.mcp.server.stdio=true").run(context -> { - McpServerProperties properties = context.getBean(McpServerProperties.class); - assertThat(properties.isStdio()).isTrue(); - }); - } - - @Test - void serverCapabilitiesConfiguration() { - this.contextRunner.run(context -> { - assertThat(context).hasSingleBean(McpSchema.ServerCapabilities.Builder.class); - McpSchema.ServerCapabilities.Builder builder = context.getBean(McpSchema.ServerCapabilities.Builder.class); - assertThat(builder).isNotNull(); - }); - } - - @Test - void toolRegistrationConfiguration() { - this.contextRunner.withUserConfiguration(TestToolConfiguration.class).run(context -> { - List tools = context.getBean("syncTools", List.class); - assertThat(tools).hasSize(2); - }); - } - - @Test - void resourceRegistrationConfiguration() { - this.contextRunner.withUserConfiguration(TestResourceConfiguration.class).run(context -> { - McpSyncServer server = context.getBean(McpSyncServer.class); - assertThat(server).isNotNull(); - }); - } - - @Test - void promptRegistrationConfiguration() { - this.contextRunner.withUserConfiguration(TestPromptConfiguration.class).run(context -> { - McpSyncServer server = context.getBean(McpSyncServer.class); - assertThat(server).isNotNull(); - }); - } - - @Test - void asyncToolRegistrationConfiguration() { - this.contextRunner.withPropertyValues("spring.ai.mcp.server.type=ASYNC") - .withUserConfiguration(TestToolConfiguration.class) - .run(context -> { - List tools = context.getBean("asyncTools", List.class); - assertThat(tools).hasSize(2); - }); - } - - @Test - void customCapabilitiesBuilder() { - this.contextRunner.withUserConfiguration(CustomCapabilitiesConfiguration.class).run(context -> { - assertThat(context).hasSingleBean(McpSchema.ServerCapabilities.Builder.class); - assertThat(context.getBean(McpSchema.ServerCapabilities.Builder.class)) - .isInstanceOf(CustomCapabilitiesBuilder.class); - }); - } - - @Test - void rootsChangeConsumerConfiguration() { - this.contextRunner.withUserConfiguration(TestRootsChangeConfiguration.class).run(context -> { - McpSyncServer server = context.getBean(McpSyncServer.class); - assertThat(server).isNotNull(); - }); - } - - @Configuration - static class TestResourceConfiguration { - - @Bean - List testResources() { - return List.of(); - } - - } - - @Configuration - static class TestPromptConfiguration { - - @Bean - List testPrompts() { - return List.of(); - } - - } - - @Configuration - static class CustomCapabilitiesConfiguration { - - @Bean - McpSchema.ServerCapabilities.Builder customCapabilitiesBuilder() { - return new CustomCapabilitiesBuilder(); - } - - } - - static class CustomCapabilitiesBuilder extends McpSchema.ServerCapabilities.Builder { - - // Custom implementation for testing - - } - - @Configuration - static class TestToolConfiguration { - - @Bean - List testTool() { - McpSyncClient mockClient = Mockito.mock(McpSyncClient.class); - McpSchema.Tool mockTool = Mockito.mock(McpSchema.Tool.class); - McpSchema.CallToolResult mockResult = Mockito.mock(McpSchema.CallToolResult.class); - - Mockito.when(mockTool.name()).thenReturn("test-tool"); - Mockito.when(mockTool.description()).thenReturn("Test Tool"); - Mockito.when(mockClient.callTool(Mockito.any(McpSchema.CallToolRequest.class))).thenReturn(mockResult); - when(mockClient.getClientInfo()).thenReturn(new McpSchema.Implementation("testClient", "1.0.0")); - - return List.of(new SyncMcpToolCallback(mockClient, mockTool)); - } - - } - - @Configuration - static class TestRootsChangeConfiguration { - - @Bean - Consumer> rootsChangeConsumer() { - return roots -> { - // Test implementation - }; - } - - } - - static class CustomServerTransport implements McpServerTransport { - - @Override - public Mono connect( - Function, Mono> messageHandler) { - return Mono.empty(); // Test implementation - } - - @Override - public Mono sendMessage(McpSchema.JSONRPCMessage message) { - return Mono.empty(); // Test implementation - } - - @Override - public T unmarshalFrom(Object value, TypeReference type) { - return null; // Test implementation - } - - @Override - public void close() { - // Test implementation - } - - @Override - public Mono closeGracefully() { - return Mono.empty(); // Test implementation - } - - } - - @Configuration - static class CustomTransportConfiguration { - - @Bean - McpServerTransport customTransport() { - return new CustomServerTransport(); - } - - } - -} 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 88d9c358001..c5218f9a5cd 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 @@ -18,7 +18,6 @@ import java.util.List; import java.util.function.BiConsumer; -import java.util.function.Function; import com.fasterxml.jackson.core.type.TypeReference; import io.modelcontextprotocol.client.McpSyncClient; @@ -33,7 +32,6 @@ import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpServerTransport; import io.modelcontextprotocol.spec.McpServerTransportProvider; -import io.modelcontextprotocol.spec.ServerMcpTransport; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import reactor.core.publisher.Mono; @@ -122,7 +120,7 @@ void disabledConfiguration() { this.contextRunner.withPropertyValues("spring.ai.mcp.server.enabled=false").run(context -> { assertThat(context).doesNotHaveBean(McpSyncServer.class); assertThat(context).doesNotHaveBean(McpAsyncServer.class); - assertThat(context).doesNotHaveBean(ServerMcpTransport.class); + assertThat(context).doesNotHaveBean(McpServerTransport.class); }); } @@ -277,12 +275,6 @@ BiConsumer> rootsChangeHandler() { static class CustomServerTransport implements McpServerTransport { - @Override - public Mono connect( - Function, Mono> messageHandler) { - return Mono.empty(); // Test implementation - } - @Override public Mono sendMessage(McpSchema.JSONRPCMessage message) { return Mono.empty(); // Test implementation diff --git a/mcp/common/src/main/java/org/springframework/ai/mcp/AsyncMcpToolCallbackProvider.java b/mcp/common/src/main/java/org/springframework/ai/mcp/AsyncMcpToolCallbackProvider.java index 6b645952de8..73c4c20b155 100644 --- a/mcp/common/src/main/java/org/springframework/ai/mcp/AsyncMcpToolCallbackProvider.java +++ b/mcp/common/src/main/java/org/springframework/ai/mcp/AsyncMcpToolCallbackProvider.java @@ -17,8 +17,10 @@ import java.util.ArrayList; import java.util.List; +import java.util.function.BiPredicate; import io.modelcontextprotocol.client.McpAsyncClient; +import io.modelcontextprotocol.spec.McpSchema.Tool; import io.modelcontextprotocol.util.Assert; import reactor.core.publisher.Flux; @@ -73,6 +75,21 @@ public class AsyncMcpToolCallbackProvider implements ToolCallbackProvider { private final List mcpClients; + private final BiPredicate toolFilter; + + /** + * Creates a new {@code AsyncMcpToolCallbackProvider} instance with a list of MCP + * clients. + * @param mcpClients the list of MCP clients to use for discovering tools + * @param toolFilter a filter to apply to each discovered tool + */ + public AsyncMcpToolCallbackProvider(BiPredicate toolFilter, List mcpClients) { + Assert.notNull(mcpClients, "MCP clients must not be null"); + Assert.notNull(toolFilter, "Tool filter must not be null"); + this.mcpClients = mcpClients; + this.toolFilter = toolFilter; + } + /** * Creates a new {@code AsyncMcpToolCallbackProvider} instance with a list of MCP * clients. @@ -82,13 +99,26 @@ public class AsyncMcpToolCallbackProvider implements ToolCallbackProvider { * @throws IllegalArgumentException if mcpClients is null */ public AsyncMcpToolCallbackProvider(List mcpClients) { - Assert.notNull(mcpClients, "McpClients must not be null"); - this.mcpClients = mcpClients; + this((mcpClient, tool) -> true, mcpClients); } + /** + * Creates a new {@code AsyncMcpToolCallbackProvider} instance with one or more MCP + * clients. + * @param mcpClients the MCP clients to use for discovering tools + * @param toolFilter a filter to apply to each discovered tool + */ + public AsyncMcpToolCallbackProvider(BiPredicate toolFilter, McpAsyncClient... mcpClients) { + this(toolFilter, List.of(mcpClients)); + } + + /** + * Creates a new {@code AsyncMcpToolCallbackProvider} instance with one or more MCP + * clients. + * @param mcpClients the MCP clients to use for discovering tools + */ public AsyncMcpToolCallbackProvider(McpAsyncClient... mcpClients) { - Assert.notNull(mcpClients, "McpClients must not be null"); - this.mcpClients = List.of(mcpClients); + this(List.of(mcpClients)); } /** @@ -116,6 +146,7 @@ public ToolCallback[] getToolCallbacks() { ToolCallback[] toolCallbacks = mcpClient.listTools() .map(response -> response.tools() .stream() + .filter(tool -> toolFilter.test(mcpClient, tool)) .map(tool -> new AsyncMcpToolCallback(mcpClient, tool)) .toArray(ToolCallback[]::new)) .block(); diff --git a/mcp/common/src/main/java/org/springframework/ai/mcp/McpToolUtils.java b/mcp/common/src/main/java/org/springframework/ai/mcp/McpToolUtils.java index c3c58accb9b..93d007fa8eb 100644 --- a/mcp/common/src/main/java/org/springframework/ai/mcp/McpToolUtils.java +++ b/mcp/common/src/main/java/org/springframework/ai/mcp/McpToolUtils.java @@ -17,6 +17,7 @@ import java.util.List; import java.util.Map; +import java.util.Optional; import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; @@ -24,7 +25,6 @@ import io.modelcontextprotocol.client.McpAsyncClient; import io.modelcontextprotocol.client.McpSyncClient; import io.modelcontextprotocol.server.McpServerFeatures; -import io.modelcontextprotocol.server.McpServerFeatures.AsyncToolRegistration; import io.modelcontextprotocol.server.McpServerFeatures.AsyncToolSpecification; import io.modelcontextprotocol.server.McpSyncServerExchange; import io.modelcontextprotocol.spec.McpSchema; @@ -62,6 +62,11 @@ */ public final class McpToolUtils { + /** + * The name of tool context key used to store the MCP exchange object. + */ + public static final String TOOL_CONTEXT_MCP_EXCHANGE_KEY = "exchange"; + private McpToolUtils() { } @@ -87,23 +92,6 @@ public static String prefixedToolName(String prefix, String toolName) { return formatted; } - /** - * Converts a list of Spring AI tool callbacks to MCP synchronous tool registrations. - *

- * This method processes multiple tool callbacks in bulk, converting each one to its - * corresponding MCP tool registration while maintaining synchronous execution - * semantics. - * @param toolCallbacks the list of tool callbacks to convert - * @return a list of MCP synchronous tool registrations - * @see #toSyncToolRegistration(ToolCallback) - * @deprecated Use {@link #toSyncToolSpecification(List)} instead. - */ - @Deprecated - public static List toSyncToolRegistration( - List toolCallbacks) { - return toolCallbacks.stream().map(McpToolUtils::toSyncToolRegistration).toList(); - } - /** * Converts a list of Spring AI tool callbacks to MCP synchronous tool specificaiton. *

@@ -118,27 +106,6 @@ public static List toSyncToolSpecificat return toolCallbacks.stream().map(McpToolUtils::toSyncToolSpecification).toList(); } - /** - * Convenience method to convert a variable number of tool callbacks to MCP - * synchronous tool registrations. - *

- * This is a varargs wrapper around {@link #toSyncToolRegistration(List)} for easier - * usage when working with individual callbacks. - * @param toolCallbacks the tool callbacks to convert - * @return a list of MCP synchronous tool registrations - * @see #toSyncToolRegistration(List) - * @deprecated Use {@link #toSyncToolSpecification(ToolCallback...)} instead. - */ - @Deprecated - public static List toSyncToolRegistrations(ToolCallback... toolCallbacks) { - return toSyncToolRegistration(List.of(toolCallbacks)); - } - - @Deprecated - public static List toSyncToolRegistration(ToolCallback... toolCallbacks) { - return toSyncToolRegistration(List.of(toolCallbacks)); - } - /** * Convenience method to convert a variable number of tool callbacks to MCP * synchronous tool specification. @@ -153,33 +120,6 @@ public static List toSyncToolSpecificat return toSyncToolSpecification(List.of(toolCallbacks)); } - /** - * Converts a Spring AI ToolCallback to an MCP SyncToolRegistration. This enables - * Spring AI functions to be exposed as MCP tools that can be discovered and invoked - * by language models. - * - *

- * The conversion process: - *

    - *
  • Creates an MCP Tool with the function's name and input schema
  • - *
  • Wraps the function's execution in a SyncToolRegistration that handles the MCP - * protocol
  • - *
  • Provides error handling and result formatting according to MCP - * specifications
  • - *
- * - * You can use the ToolCallback builder to create a new instance of ToolCallback using - * either java.util.function.Function or Method reference. - * @param toolCallback the Spring AI function callback to convert - * @return an MCP SyncToolRegistration that wraps the function callback - * @throws RuntimeException if there's an error during the function execution - * @deprecated Use {@link #toSyncToolSpecification(ToolCallback)} instead. - */ - @Deprecated - public static McpServerFeatures.SyncToolRegistration toSyncToolRegistration(ToolCallback toolCallback) { - return toSyncToolRegistration(toolCallback, null); - } - /** * Converts a Spring AI ToolCallback to an MCP SyncToolSpecification. This enables * Spring AI functions to be exposed as MCP tools that can be discovered and invoked @@ -205,65 +145,6 @@ public static McpServerFeatures.SyncToolSpecification toSyncToolSpecification(To return toSyncToolSpecification(toolCallback, null); } - /** - * Converts a Spring AI ToolCallback to an MCP SyncToolRegistration. This enables - * Spring AI functions to be exposed as MCP tools that can be discovered and invoked - * by language models. - * - *

- * The conversion process: - *

    - *
  • Creates an MCP Tool with the function's name and input schema
  • - *
  • Wraps the function's execution in a SyncToolRegistration that handles the MCP - * protocol
  • - *
  • Provides error handling and result formatting according to MCP - * specifications
  • - *
- * - * You can use the ToolCallback builder to create a new instance of ToolCallback using - * either java.util.function.Function or Method reference. - * @param toolCallback the Spring AI function callback to convert - * @param mimeType the MIME type of the output content - * @return an MCP SyncToolRegistration that wraps the function callback - * @throws RuntimeException if there's an error during the function execution - * @deprecated Use {@link #toSyncToolSpecification(ToolCallback, MimeType)} instead. - */ - @Deprecated - public static McpServerFeatures.SyncToolRegistration toSyncToolRegistration(ToolCallback toolCallback, - MimeType mimeType) { - - var tool = new McpSchema.Tool(toolCallback.getToolDefinition().name(), - toolCallback.getToolDefinition().description(), toolCallback.getToolDefinition().inputSchema()); - - return new McpServerFeatures.SyncToolRegistration(tool, request -> { - try { - String callResult = toolCallback.call(ModelOptionsUtils.toJsonString(request)); - String imgData = callResult; - if (mimeType != null && "image".equals(mimeType.getType())) { - String imgType = mimeType.toString(); - if (callResult.startsWith("{") && callResult.endsWith("}")) { - // This is most likely a JSON structure: - // let's try to parse it as a base64 wrapper. - var b64Struct = JsonParser.fromJson(callResult, Base64Wrapper.class); - if (b64Struct.mimeType() != null && b64Struct.data() != null - && b64Struct.mimeType.getType().equals("image")) { - // Get the base64 encoded image as is. - imgType = b64Struct.mimeType().toString(); - imgData = b64Struct.data(); - } - } - return new McpSchema.CallToolResult( - List.of(new McpSchema.ImageContent(List.of(Role.ASSISTANT), null, imgData, imgType)), - false); - } - return new McpSchema.CallToolResult(List.of(new McpSchema.TextContent(callResult)), false); - } - catch (Exception e) { - return new McpSchema.CallToolResult(List.of(new McpSchema.TextContent(e.getMessage())), true); - } - }); - } - /** * Converts a Spring AI ToolCallback to an MCP SyncToolSpecification. This enables * Spring AI functions to be exposed as MCP tools that can be discovered and invoked @@ -292,7 +173,7 @@ public static McpServerFeatures.SyncToolSpecification toSyncToolSpecification(To return new McpServerFeatures.SyncToolSpecification(tool, (exchange, request) -> { try { String callResult = toolCallback.call(ModelOptionsUtils.toJsonString(request), - new ToolContext(Map.of("exchange", exchange))); + new ToolContext(Map.of(TOOL_CONTEXT_MCP_EXCHANGE_KEY, exchange))); if (mimeType != null && mimeType.toString().startsWith("image")) { return new McpSchema.CallToolResult(List .of(new McpSchema.ImageContent(List.of(Role.ASSISTANT), null, callResult, mimeType.toString())), @@ -307,21 +188,16 @@ public static McpServerFeatures.SyncToolSpecification toSyncToolSpecification(To } /** - * Converts a list of Spring AI tool callbacks to MCP asynchronous tool registrations. - *

- * This method processes multiple tool callbacks in bulk, converting each one to its - * corresponding MCP tool registration while adding asynchronous execution - * capabilities. The resulting registrations will execute their tools on a bounded - * elastic scheduler. - * @param toolCallbacks the list of tool callbacks to convert - * @return a list of MCP asynchronous tool registrations - * @see #toAsyncToolRegistration(ToolCallback) - * @deprecated Use {@link #toAsyncToolSpecification(List)} instead. + * Retrieves the MCP exchange object from the provided tool context if it exists. + * @param toolContext the tool context from which to retrieve the MCP exchange + * @return the MCP exchange object, or null if not present in the context */ - @Deprecated - public static List toAsyncToolRegistration( - List toolCallbacks) { - return toolCallbacks.stream().map(McpToolUtils::toAsyncToolRegistration).toList(); + public static Optional getMcpExchange(ToolContext toolContext) { + if (toolContext != null && toolContext.getContext().containsKey(TOOL_CONTEXT_MCP_EXCHANGE_KEY)) { + return Optional + .ofNullable((McpSyncServerExchange) toolContext.getContext().get(TOOL_CONTEXT_MCP_EXCHANGE_KEY)); + } + return Optional.empty(); } /** @@ -339,20 +215,6 @@ public static List toAsyncToolSpecific return toolCallbacks.stream().map(McpToolUtils::toAsyncToolSpecification).toList(); } - /** - * Convenience method to convert a variable number of tool callbacks to MCP - * asynchronous tool specifications. - *

- * This is a varargs wrapper around {@link #toAsyncToolRegistration(List)} for easier - * usage when working with individual callbacks. - * @param toolCallbacks the tool callbacks to convert - * @return a list of MCP asynchronous tool registrations - * @see #toAsyncToolRegistration(List) - */ - public static List toAsyncToolRegistration(ToolCallback... toolCallbacks) { - return toAsyncToolRegistration(List.of(toolCallbacks)); - } - /** * Convenience method to convert a variable number of tool callbacks to MCP * asynchronous tool specificaiton. @@ -368,36 +230,6 @@ public static List toAsyncToolSpecific return toAsyncToolSpecifications(List.of(toolCallbacks)); } - /** - * Converts a Spring AI tool callback to an MCP asynchronous tool registration. - *

- * This method enables Spring AI tools to be exposed as asynchronous MCP tools that - * can be discovered and invoked by language models. The conversion process: - *

    - *
  • First converts the callback to a synchronous registration
  • - *
  • Wraps the synchronous execution in a reactive Mono
  • - *
  • Configures execution on a bounded elastic scheduler for non-blocking - * operation
  • - *
- *

- * The resulting async registration will: - *

    - *
  • Execute the tool without blocking the calling thread
  • - *
  • Handle errors and results asynchronously
  • - *
  • Provide backpressure through Project Reactor
  • - *
- * @param toolCallback the Spring AI tool callback to convert - * @return an MCP asynchronous tool registration that wraps the tool callback - * @see McpServerFeatures.AsyncToolRegistration - * @see Mono - * @see Schedulers#boundedElastic() - * @deprecated Use {@link #toAsyncToolSpecification(ToolCallback)} instead. - */ - @Deprecated - public static McpServerFeatures.AsyncToolRegistration toAsyncToolRegistration(ToolCallback toolCallback) { - return toAsyncToolRegistration(toolCallback, null); - } - /** * Converts a Spring AI tool callback to an MCP asynchronous tool registration. *

@@ -426,43 +258,6 @@ public static McpServerFeatures.AsyncToolSpecification toAsyncToolSpecification( return toAsyncToolSpecification(toolCallback, null); } - /** - * Converts a Spring AI tool callback to an MCP asynchronous tool registration. - *

- * This method enables Spring AI tools to be exposed as asynchronous MCP tools that - * can be discovered and invoked by language models. The conversion process: - *

    - *
  • First converts the callback to a synchronous registration
  • - *
  • Wraps the synchronous execution in a reactive Mono
  • - *
  • Configures execution on a bounded elastic scheduler for non-blocking - * operation
  • - *
- *

- * The resulting async registration will: - *

    - *
  • Execute the tool without blocking the calling thread
  • - *
  • Handle errors and results asynchronously
  • - *
  • Provide backpressure through Project Reactor
  • - *
- * @param toolCallback the Spring AI tool callback to convert - * @param mimeType the MIME type of the output content - * @return an MCP asynchronous tool registration that wraps the tool callback - * @see McpServerFeatures.AsyncToolRegistration - * @see Mono - * @see Schedulers#boundedElastic() - * @deprecated Use {@link #toAsyncToolSpecification(ToolCallback, MimeType)} instead. - */ - @Deprecated - public static McpServerFeatures.AsyncToolRegistration toAsyncToolRegistration(ToolCallback toolCallback, - MimeType mimeType) { - - McpServerFeatures.SyncToolRegistration syncToolRegistration = toSyncToolRegistration(toolCallback, mimeType); - - return new AsyncToolRegistration(syncToolRegistration.tool(), - map -> Mono.fromCallable(() -> syncToolRegistration.call().apply(map)) - .subscribeOn(Schedulers.boundedElastic())); - } - /** * Converts a Spring AI tool callback to an MCP asynchronous tool specification. *

diff --git a/mcp/common/src/main/java/org/springframework/ai/mcp/SyncMcpToolCallbackProvider.java b/mcp/common/src/main/java/org/springframework/ai/mcp/SyncMcpToolCallbackProvider.java index 945dc4c1f9f..9e11b9dfc4f 100644 --- a/mcp/common/src/main/java/org/springframework/ai/mcp/SyncMcpToolCallbackProvider.java +++ b/mcp/common/src/main/java/org/springframework/ai/mcp/SyncMcpToolCallbackProvider.java @@ -17,12 +17,15 @@ import java.util.ArrayList; import java.util.List; +import java.util.function.BiPredicate; import io.modelcontextprotocol.client.McpSyncClient; +import io.modelcontextprotocol.spec.McpSchema.Tool; import org.springframework.ai.tool.ToolCallback; import org.springframework.ai.tool.ToolCallbackProvider; import org.springframework.ai.tool.util.ToolUtils; +import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; /** @@ -69,17 +72,47 @@ public class SyncMcpToolCallbackProvider implements ToolCallbackProvider { private final List mcpClients; + private final BiPredicate toolFilter; + /** * Creates a new {@code SyncMcpToolCallbackProvider} instance with a list of MCP * clients. * @param mcpClients the list of MCP clients to use for discovering tools + * @param toolFilter a filter to apply to each discovered tool */ - public SyncMcpToolCallbackProvider(List mcpClients) { + public SyncMcpToolCallbackProvider(BiPredicate toolFilter, List mcpClients) { + Assert.notNull(mcpClients, "MCP clients must not be null"); + Assert.notNull(toolFilter, "Tool filter must not be null"); this.mcpClients = mcpClients; + this.toolFilter = toolFilter; + } + + /** + * Creates a new {@code SyncMcpToolCallbackProvider} instance with a list of MCP + * clients. + * @param mcpClients the list of MCP clients to use for discovering tools + */ + public SyncMcpToolCallbackProvider(List mcpClients) { + this((mcpClient, tool) -> true, mcpClients); } + /** + * Creates a new {@code SyncMcpToolCallbackProvider} instance with one or more MCP + * clients. + * @param mcpClients the MCP clients to use for discovering tools + * @param toolFilter a filter to apply to each discovered tool + */ + public SyncMcpToolCallbackProvider(BiPredicate toolFilter, McpSyncClient... mcpClients) { + this(toolFilter, List.of(mcpClients)); + } + + /** + * Creates a new {@code SyncMcpToolCallbackProvider} instance with one or more MCP + * clients. + * @param mcpClients the MCP clients to use for discovering tools + */ public SyncMcpToolCallbackProvider(McpSyncClient... mcpClients) { - this.mcpClients = List.of(mcpClients); + this(List.of(mcpClients)); } /** @@ -99,10 +132,11 @@ public ToolCallback[] getToolCallbacks() { var toolCallbacks = new ArrayList<>(); - mcpClients.stream().forEach(mcpClient -> { + this.mcpClients.stream().forEach(mcpClient -> { toolCallbacks.addAll(mcpClient.listTools() .tools() .stream() + .filter(tool -> toolFilter.test(mcpClient, tool)) .map(tool -> new SyncMcpToolCallback(mcpClient, tool)) .toList()); }); diff --git a/mcp/common/src/test/java/org/springframework/ai/mcp/SyncMcpToolCallbackProviderTests.java b/mcp/common/src/test/java/org/springframework/ai/mcp/SyncMcpToolCallbackProviderTests.java index 4b654cf4efc..be5cc98590c 100644 --- a/mcp/common/src/test/java/org/springframework/ai/mcp/SyncMcpToolCallbackProviderTests.java +++ b/mcp/common/src/test/java/org/springframework/ai/mcp/SyncMcpToolCallbackProviderTests.java @@ -22,6 +22,7 @@ import static org.mockito.Mockito.when; import java.util.List; +import java.util.function.BiPredicate; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -129,4 +130,142 @@ void getSameNameToolsButDifferntClientInfoNamesShouldProduceDifferentToolCallbac assertThat(callbacks).hasSize(2); } + @Test + void toolFilterShouldAcceptAllToolsByDefault() { + var clientInfo = new Implementation("testClient", "1.0.0"); + when(mcpClient.getClientInfo()).thenReturn(clientInfo); + + Tool tool1 = mock(Tool.class); + when(tool1.name()).thenReturn("tool1"); + + Tool tool2 = mock(Tool.class); + when(tool2.name()).thenReturn("tool2"); + + ListToolsResult listToolsResult = mock(ListToolsResult.class); + when(listToolsResult.tools()).thenReturn(List.of(tool1, tool2)); + when(mcpClient.listTools()).thenReturn(listToolsResult); + + // Using the constructor without explicit filter (should use default filter that + // accepts all) + SyncMcpToolCallbackProvider provider = new SyncMcpToolCallbackProvider(mcpClient); + + var callbacks = provider.getToolCallbacks(); + + assertThat(callbacks).hasSize(2); + } + + @Test + void toolFilterShouldRejectAllToolsWhenConfigured() { + + Tool tool1 = mock(Tool.class); + Tool tool2 = mock(Tool.class); + + ListToolsResult listToolsResult = mock(ListToolsResult.class); + when(listToolsResult.tools()).thenReturn(List.of(tool1, tool2)); + when(mcpClient.listTools()).thenReturn(listToolsResult); + + // Create a filter that rejects all tools + BiPredicate rejectAllFilter = (client, tool) -> false; + + SyncMcpToolCallbackProvider provider = new SyncMcpToolCallbackProvider(rejectAllFilter, mcpClient); + + var callbacks = provider.getToolCallbacks(); + + assertThat(callbacks).isEmpty(); + } + + @Test + void toolFilterShouldFilterToolsByNameWhenConfigured() { + var clientInfo = new Implementation("testClient", "1.0.0"); + when(mcpClient.getClientInfo()).thenReturn(clientInfo); + + Tool tool1 = mock(Tool.class); + when(tool1.name()).thenReturn("tool1"); + + Tool tool2 = mock(Tool.class); + when(tool2.name()).thenReturn("tool2"); + + Tool tool3 = mock(Tool.class); + when(tool3.name()).thenReturn("tool3"); + + ListToolsResult listToolsResult = mock(ListToolsResult.class); + when(listToolsResult.tools()).thenReturn(List.of(tool1, tool2, tool3)); + when(mcpClient.listTools()).thenReturn(listToolsResult); + + // Create a filter that only accepts tools with names containing "2" or "3" + BiPredicate nameFilter = (client, tool) -> tool.name().contains("2") + || tool.name().contains("3"); + + SyncMcpToolCallbackProvider provider = new SyncMcpToolCallbackProvider(nameFilter, mcpClient); + + var callbacks = provider.getToolCallbacks(); + + assertThat(callbacks).hasSize(2); + assertThat(callbacks[0].getToolDefinition().name()).isEqualTo("testClient_tool2"); + assertThat(callbacks[1].getToolDefinition().name()).isEqualTo("testClient_tool3"); + } + + @Test + void toolFilterShouldFilterToolsByClientWhenConfigured() { + Tool tool1 = mock(Tool.class); + when(tool1.name()).thenReturn("tool1"); + + Tool tool2 = mock(Tool.class); + + McpSyncClient mcpClient1 = mock(McpSyncClient.class); + ListToolsResult listToolsResult1 = mock(ListToolsResult.class); + when(listToolsResult1.tools()).thenReturn(List.of(tool1)); + when(mcpClient1.listTools()).thenReturn(listToolsResult1); + + var clientInfo1 = new Implementation("testClient1", "1.0.0"); + when(mcpClient1.getClientInfo()).thenReturn(clientInfo1); + + McpSyncClient mcpClient2 = mock(McpSyncClient.class); + ListToolsResult listToolsResult2 = mock(ListToolsResult.class); + when(listToolsResult2.tools()).thenReturn(List.of(tool2)); + when(mcpClient2.listTools()).thenReturn(listToolsResult2); + + var clientInfo2 = new Implementation("testClient2", "1.0.0"); + when(mcpClient2.getClientInfo()).thenReturn(clientInfo2); + + // Create a filter that only accepts tools from client1 + BiPredicate clientFilter = (client, + tool) -> client.getClientInfo().name().equals("testClient1"); + + SyncMcpToolCallbackProvider provider = new SyncMcpToolCallbackProvider(clientFilter, mcpClient1, mcpClient2); + + var callbacks = provider.getToolCallbacks(); + + assertThat(callbacks).hasSize(1); + assertThat(callbacks[0].getToolDefinition().name()).isEqualTo("testClient1_tool1"); + } + + @Test + void toolFilterShouldCombineClientAndToolCriteriaWhenConfigured() { + Tool tool1 = mock(Tool.class); + when(tool1.name()).thenReturn("weather"); + + Tool tool2 = mock(Tool.class); + when(tool2.name()).thenReturn("calculator"); + + McpSyncClient weatherClient = mock(McpSyncClient.class); + ListToolsResult weatherResult = mock(ListToolsResult.class); + when(weatherResult.tools()).thenReturn(List.of(tool1, tool2)); + when(weatherClient.listTools()).thenReturn(weatherResult); + + var weatherClientInfo = new Implementation("weather-service", "1.0.0"); + when(weatherClient.getClientInfo()).thenReturn(weatherClientInfo); + + // Create a filter that only accepts weather tools from the weather service + BiPredicate complexFilter = (client, + tool) -> client.getClientInfo().name().equals("weather-service") && tool.name().equals("weather"); + + SyncMcpToolCallbackProvider provider = new SyncMcpToolCallbackProvider(complexFilter, weatherClient); + + var callbacks = provider.getToolCallbacks(); + + assertThat(callbacks).hasSize(1); + assertThat(callbacks[0].getToolDefinition().name()).isEqualTo("weather_service_weather"); + } + } diff --git a/mcp/common/src/test/java/org/springframework/ai/mcp/ToolUtilsDeprecatedTests.java b/mcp/common/src/test/java/org/springframework/ai/mcp/ToolUtilsDeprecatedTests.java deleted file mode 100644 index 2a7c8e0677e..00000000000 --- a/mcp/common/src/test/java/org/springframework/ai/mcp/ToolUtilsDeprecatedTests.java +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Copyright 2025-2025 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.ai.mcp; - -import java.lang.reflect.Constructor; -import java.lang.reflect.Modifier; -import java.util.List; -import java.util.Map; - -import io.modelcontextprotocol.server.McpServerFeatures.AsyncToolRegistration; -import io.modelcontextprotocol.server.McpServerFeatures.SyncToolRegistration; -import io.modelcontextprotocol.spec.McpSchema.CallToolResult; -import io.modelcontextprotocol.spec.McpSchema.TextContent; -import org.junit.jupiter.api.Test; -import reactor.test.StepVerifier; - -import org.springframework.ai.tool.ToolCallback; -import org.springframework.ai.tool.definition.ToolDefinition; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -/** - * @deprecated used to test backward compatbility. Replaced by the {@link ToolUtilsTests} - * instead - */ -@Deprecated -class ToolUtilsDeprecatedTests { - - @Test - void constructorShouldBePrivate() throws Exception { - Constructor constructor = McpToolUtils.class.getDeclaredConstructor(); - assertThat(Modifier.isPrivate(constructor.getModifiers())).isTrue(); - constructor.setAccessible(true); - constructor.newInstance(); - } - - @Test - void toSyncToolRegistrationShouldConvertSingleCallback() { - // Arrange - ToolCallback callback = createMockToolCallback("test", "success"); - - // Act - SyncToolRegistration registration = McpToolUtils.toSyncToolRegistration(callback); - - // Assert - assertThat(registration).isNotNull(); - assertThat(registration.tool().name()).isEqualTo("test"); - - CallToolResult result = registration.call().apply(Map.of()); - TextContent content = (TextContent) result.content().get(0); - assertThat(content.text()).isEqualTo("success"); - assertThat(result.isError()).isFalse(); - } - - @Test - void toSyncToolRegistrationShouldHandleError() { - // Arrange - ToolCallback callback = createMockToolCallback("test", new RuntimeException("error")); - - // Act - SyncToolRegistration registration = McpToolUtils.toSyncToolRegistration(callback); - - // Assert - assertThat(registration).isNotNull(); - CallToolResult result = registration.call().apply(Map.of()); - TextContent content = (TextContent) result.content().get(0); - assertThat(content.text()).isEqualTo("error"); - assertThat(result.isError()).isTrue(); - } - - @Test - void toSyncToolRegistrationShouldConvertMultipleCallbacks() { - // Arrange - ToolCallback callback1 = createMockToolCallback("test1", "success1"); - ToolCallback callback2 = createMockToolCallback("test2", "success2"); - - // Act - List registrations = McpToolUtils.toSyncToolRegistrations(callback1, callback2); - - // Assert - assertThat(registrations).hasSize(2); - assertThat(registrations.get(0).tool().name()).isEqualTo("test1"); - assertThat(registrations.get(1).tool().name()).isEqualTo("test2"); - } - - @Test - void toAsyncToolRegistrationShouldConvertSingleCallback() { - // Arrange - ToolCallback callback = createMockToolCallback("test", "success"); - - // Act - AsyncToolRegistration registration = McpToolUtils.toAsyncToolRegistration(callback); - - // Assert - assertThat(registration).isNotNull(); - assertThat(registration.tool().name()).isEqualTo("test"); - - StepVerifier.create(registration.call().apply(Map.of())).assertNext(result -> { - TextContent content = (TextContent) result.content().get(0); - assertThat(content.text()).isEqualTo("success"); - assertThat(result.isError()).isFalse(); - }).verifyComplete(); - } - - @Test - void toAsyncToolRegistrationShouldHandleError() { - // Arrange - ToolCallback callback = createMockToolCallback("test", new RuntimeException("error")); - - // Act - AsyncToolRegistration registration = McpToolUtils.toAsyncToolRegistration(callback); - - // Assert - assertThat(registration).isNotNull(); - StepVerifier.create(registration.call().apply(Map.of())).assertNext(result -> { - TextContent content = (TextContent) result.content().get(0); - assertThat(content.text()).isEqualTo("error"); - assertThat(result.isError()).isTrue(); - }).verifyComplete(); - } - - @Test - void toAsyncToolRegistrationShouldConvertMultipleCallbacks() { - // Arrange - ToolCallback callback1 = createMockToolCallback("test1", "success1"); - ToolCallback callback2 = createMockToolCallback("test2", "success2"); - - // Act - List registrations = McpToolUtils.toAsyncToolRegistration(callback1, callback2); - - // Assert - assertThat(registrations).hasSize(2); - assertThat(registrations.get(0).tool().name()).isEqualTo("test1"); - assertThat(registrations.get(1).tool().name()).isEqualTo("test2"); - } - - private ToolCallback createMockToolCallback(String name, String result) { - ToolCallback callback = mock(ToolCallback.class); - ToolDefinition definition = ToolDefinition.builder() - .name(name) - .description("Test tool") - .inputSchema("{}") - .build(); - when(callback.getToolDefinition()).thenReturn(definition); - when(callback.call(anyString())).thenReturn(result); - return callback; - } - - private ToolCallback createMockToolCallback(String name, RuntimeException error) { - ToolCallback callback = mock(ToolCallback.class); - ToolDefinition definition = ToolDefinition.builder() - .name(name) - .description("Test tool") - .inputSchema("{}") - .build(); - when(callback.getToolDefinition()).thenReturn(definition); - when(callback.call(anyString())).thenThrow(error); - return callback; - } - -} diff --git a/pom.xml b/pom.xml index 6a1a5e5de9e..b3e75ae8da1 100644 --- a/pom.xml +++ b/pom.xml @@ -306,7 +306,7 @@ 4.12.0 - 0.8.1 + 0.9.0 4.13.1 diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-server-boot-starter-docs.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-server-boot-starter-docs.adoc index 4bdeb7425b4..14f705ced1c 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-server-boot-starter-docs.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-server-boot-starter-docs.adoc @@ -96,7 +96,9 @@ All properties are prefixed with `spring.ai.mcp.server`: |`prompt-change-notification` |Enable prompt change notifications |`true` |`tool-change-notification` |Enable tool change notifications |`true` |`tool-response-mime-type` |(optional) response MIME type per tool name. For example `spring.ai.mcp.server.tool-response-mime-type.generateImage=image/png` will associate the `image/png` mime type with the `generateImage()` tool name |`-` -|`sse-message-endpoint` |SSE endpoint path for web transport |`/mcp/message` +|`sse-message-endpoint` | Custom SSE Message endpoint path for web transport to be used by the client to send messages|`/mcp/message` +|`sse-endpoint` |Custom SSE endpoint path for web transport |`/sse` +|`base-url` | Optional URL prefix. For example `base-url=/api/v1` means that the client should access the sse endpont at `/api/v1` + `sse-endpoint` and the message endpoint is `/api/v1` + `sse-message-endpoint` | - |=== == Sync/Async Server Types