From fe377ee5e14207bdfeee2847ddc83a1795c38820 Mon Sep 17 00:00:00 2001 From: Christian Tzolov Date: Fri, 7 Feb 2025 14:57:18 +0100 Subject: [PATCH] Refactor: MCP Autoconfig Modularization Core Architecture Changes: - Split MCP into dedicated client/server modules - Created separate starters: spring-ai-starter-mcp-webmvc and spring-ai-starter-mcp-webflux - Removed property-based transport configuration in favor of auto-configuration - Added support for multiple transport types (STDIO, WebMVC, WebFlux) Client Improvements: - Added support for both synchronous and asynchronous MCP clients - Fixed client auto-configuration issues - Added root change notification property to common properties Configuration Enhancements: - Improved configuration properties organization and validation - Added ConditionalOnMissingBean for WebMvc/WebFlux configurations - Enhanced lifecycle management and customization support Testing and Documentation: - Added comprehensive integration tests for McpClientAutoConfiguration - Updated McpServerAutoConfigurationIT - Added extensive JavaDoc documentation - Improved MCP client/server starter documentation - Added documentation for common utilities - Updated navigation for new MCP documentation sections Signed-off-by: Christian Tzolov --- .../spring-ai-mcp-client/pom.xml | 74 ++++ .../client/McpClientAutoConfiguration.java | 293 +++++++++++++++ .../mcp/client/NamedClientMcpTransport.java | 32 ++ ...eHttpClientTransportAutoConfiguration.java | 116 ++++++ .../SseWebFluxTransportAutoConfiguration.java | 124 ++++++ .../StdioTransportAutoConfiguration.java | 87 +++++ .../configurer/McpAsyncClientConfigurer.java | 16 +- .../configurer/McpSyncClientConfigurer.java | 76 ++++ .../properties/McpClientCommonProperties.java | 160 ++++++++ .../properties/McpSseClientProperties.java | 73 ++++ .../properties}/McpStdioClientProperties.java | 91 +---- ...ot.autoconfigure.AutoConfiguration.imports | 21 ++ .../client/McpClientAutoConfigurationIT.java | 190 ++++++++++ .../resources/application-test.properties | 10 + .../spring-ai-mcp-server/pom.xml | 62 +++ .../mcp/server/McpServerProperties.java | 65 +--- .../server/MpcServerAutoConfiguration.java | 101 ++--- .../MpcWebFluxServerAutoConfiguration.java | 5 +- .../MpcWebMvcServerAutoConfiguration.java | 9 +- ...ot.autoconfigure.AutoConfiguration.imports | 20 + .../server/McpServerAutoConfigurationIT.java | 311 +++++++++++++++ mcp-client-boot-starter-docs.adoc | 340 +++++++++++++++++ mcp-client-boot-starter-docs.md | 284 ++++++++++++++ mcp-helpers.adoc | 168 +++++++++ mcp-helpers.md | 156 ++++++++ mcp-server-boot-starter-docs.adoc | 310 +++++++++++++++ mcp-server-boot-starter-docs.md | 294 +++++++++++++++ .../ai/mcp/AsyncMcpToolCallback.java | 113 ++++++ .../ai/mcp/AsyncMcpToolCallbackProvider.java | 132 +++++++ .../ai/mcp/McpSyncClientCustomizer.java | 28 -- .../springframework/ai/mcp/McpToolUtils.java | 64 +++- ...Callback.java => SyncMcpToolCallback.java} | 7 +- .../ai/mcp/SyncMcpToolCallbackProvider.java | 24 +- .../ai/mcp/{ => aot}/McpHints.java | 46 ++- .../customizer/McpAsyncClientCustomizer.java | 44 +++ .../customizer/McpSyncClientCustomizer.java | 44 +++ .../springframework/ai/mcp/package-info.java | 20 + .../resources/META-INF/spring/aot.factories | 2 +- ... => SyncMcpToolCallbackProviderTests.java} | 2 +- ...sts.java => SyncMcpToolCallbackTests.java} | 6 +- pom.xml | 21 +- spring-ai-bom/pom.xml | 39 +- .../mcp/java-mcp-client-architecture.jpg | Bin 0 -> 676694 bytes .../mcp/java-mcp-server-architecture.jpg | Bin 0 -> 859935 bytes .../images/mcp/java-mcp-uml-classdiagram.svg | 1 + .../modules/ROOT/images/mcp/mcp-stack.svg | 197 ++++++++++ .../src/main/antora/modules/ROOT/nav.adoc | 6 +- .../api/mcp/mcp-client-boot-starter-docs.adoc | 353 ++++++++++++++++++ .../ROOT/pages/api/mcp/mcp-helpers.adoc | 156 ++++++++ .../ROOT/pages/api/mcp/mcp-overview.adoc | 90 +++++ .../api/mcp/mcp-server-boot-starter-docs.adoc | 218 +++++++++++ .../pages/api/model-context-protocol.adoc | 117 ------ spring-ai-spring-boot-autoconfigure/pom.xml | 3 - .../mcp/client/stdio/McpStdioConnection.java | 61 --- .../MpcStdioClientAutoConfiguration.java | 137 ------- ...ot.autoconfigure.AutoConfiguration.imports | 5 - .../server/McpServerAutoConfigurationIT.java | 93 ----- .../pom.xml | 66 ++++ .../pom.xml | 8 +- .../pom.xml | 72 ++++ .../pom.xml | 71 ++++ .../spring-ai-starter-mcp-server/pom.xml | 58 +++ 62 files changed, 5133 insertions(+), 659 deletions(-) create mode 100644 auto-configurations/spring-ai-mcp-client/pom.xml create mode 100644 auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/McpClientAutoConfiguration.java create mode 100644 auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/NamedClientMcpTransport.java create mode 100644 auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/SseHttpClientTransportAutoConfiguration.java create mode 100644 auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/SseWebFluxTransportAutoConfiguration.java create mode 100644 auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/StdioTransportAutoConfiguration.java rename spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/mcp/client/stdio/McpSyncClientConfigurer.java => auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/configurer/McpAsyncClientConfigurer.java (62%) create mode 100644 auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/configurer/McpSyncClientConfigurer.java create mode 100644 auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/properties/McpClientCommonProperties.java create mode 100644 auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/properties/McpSseClientProperties.java rename {spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/mcp/client/stdio => auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/properties}/McpStdioClientProperties.java (66%) create mode 100644 auto-configurations/spring-ai-mcp-client/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 auto-configurations/spring-ai-mcp-client/src/test/java/org/springframework/ai/autoconfigure/mcp/client/McpClientAutoConfigurationIT.java create mode 100644 auto-configurations/spring-ai-mcp-client/src/test/resources/application-test.properties create mode 100644 auto-configurations/spring-ai-mcp-server/pom.xml rename {spring-ai-spring-boot-autoconfigure => auto-configurations/spring-ai-mcp-server}/src/main/java/org/springframework/ai/autoconfigure/mcp/server/McpServerProperties.java (82%) rename {spring-ai-spring-boot-autoconfigure => auto-configurations/spring-ai-mcp-server}/src/main/java/org/springframework/ai/autoconfigure/mcp/server/MpcServerAutoConfiguration.java (72%) rename {spring-ai-spring-boot-autoconfigure => auto-configurations/spring-ai-mcp-server}/src/main/java/org/springframework/ai/autoconfigure/mcp/server/MpcWebFluxServerAutoConfiguration.java (94%) rename {spring-ai-spring-boot-autoconfigure => auto-configurations/spring-ai-mcp-server}/src/main/java/org/springframework/ai/autoconfigure/mcp/server/MpcWebMvcServerAutoConfiguration.java (94%) create mode 100644 auto-configurations/spring-ai-mcp-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 auto-configurations/spring-ai-mcp-server/src/test/java/org/springframework/ai/autoconfigure/mcp/server/McpServerAutoConfigurationIT.java create mode 100644 mcp-client-boot-starter-docs.adoc create mode 100644 mcp-client-boot-starter-docs.md create mode 100644 mcp-helpers.adoc create mode 100644 mcp-helpers.md create mode 100644 mcp-server-boot-starter-docs.adoc create mode 100644 mcp-server-boot-starter-docs.md create mode 100644 mcp/common/src/main/java/org/springframework/ai/mcp/AsyncMcpToolCallback.java create mode 100644 mcp/common/src/main/java/org/springframework/ai/mcp/AsyncMcpToolCallbackProvider.java delete mode 100644 mcp/common/src/main/java/org/springframework/ai/mcp/McpSyncClientCustomizer.java rename mcp/common/src/main/java/org/springframework/ai/mcp/{McpToolCallback.java => SyncMcpToolCallback.java} (95%) rename mcp/common/src/main/java/org/springframework/ai/mcp/{ => aot}/McpHints.java (54%) create mode 100644 mcp/common/src/main/java/org/springframework/ai/mcp/customizer/McpAsyncClientCustomizer.java create mode 100644 mcp/common/src/main/java/org/springframework/ai/mcp/customizer/McpSyncClientCustomizer.java rename mcp/common/src/test/java/org/springframework/ai/mcp/{McpToolCallbackProviderTests.java => SyncMcpToolCallbackProviderTests.java} (98%) rename mcp/common/src/test/java/org/springframework/ai/mcp/{McpToolCallbackTests.java => SyncMcpToolCallbackTests.java} (92%) create mode 100644 spring-ai-docs/src/main/antora/modules/ROOT/images/mcp/java-mcp-client-architecture.jpg create mode 100644 spring-ai-docs/src/main/antora/modules/ROOT/images/mcp/java-mcp-server-architecture.jpg create mode 100644 spring-ai-docs/src/main/antora/modules/ROOT/images/mcp/java-mcp-uml-classdiagram.svg create mode 100644 spring-ai-docs/src/main/antora/modules/ROOT/images/mcp/mcp-stack.svg create mode 100644 spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-client-boot-starter-docs.adoc create mode 100644 spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-helpers.adoc create mode 100644 spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-overview.adoc create mode 100644 spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-server-boot-starter-docs.adoc delete mode 100644 spring-ai-docs/src/main/antora/modules/ROOT/pages/api/model-context-protocol.adoc delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/mcp/client/stdio/McpStdioConnection.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/mcp/client/stdio/MpcStdioClientAutoConfiguration.java delete mode 100644 spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/mcp/server/McpServerAutoConfigurationIT.java create mode 100644 spring-ai-spring-boot-starters/spring-ai-starter-mcp-client-webflux/pom.xml rename spring-ai-spring-boot-starters/{spring-ai-starter-mcp => spring-ai-starter-mcp-client}/pom.xml (88%) create mode 100644 spring-ai-spring-boot-starters/spring-ai-starter-mcp-server-webflux/pom.xml create mode 100644 spring-ai-spring-boot-starters/spring-ai-starter-mcp-server-webmvc/pom.xml create mode 100644 spring-ai-spring-boot-starters/spring-ai-starter-mcp-server/pom.xml diff --git a/auto-configurations/spring-ai-mcp-client/pom.xml b/auto-configurations/spring-ai-mcp-client/pom.xml new file mode 100644 index 000000000..da1b08b67 --- /dev/null +++ b/auto-configurations/spring-ai-mcp-client/pom.xml @@ -0,0 +1,74 @@ + + + 4.0.0 + + org.springframework.ai + spring-ai + 1.0.0-SNAPSHOT + ../../pom.xml + + spring-ai-mcp-client-spring-boot-autoconfigure + jar + Spring AI MCP Client Auto Configuration + Spring AI MCP Client Auto Configuration + https://github.com/spring-projects/spring-ai + + + https://github.com/spring-projects/spring-ai + git://github.com/spring-projects/spring-ai.git + git@github.com:spring-projects/spring-ai.git + + + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.ai + spring-ai-mcp + ${project.parent.version} + true + + + + io.modelcontextprotocol.sdk + mcp-spring-webflux + true + + + + + + + + org.springframework.ai + spring-ai-test + ${project.parent.version} + test + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.mockito + mockito-core + test + + + + diff --git a/auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/McpClientAutoConfiguration.java b/auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/McpClientAutoConfiguration.java new file mode 100644 index 000000000..23a3d1013 --- /dev/null +++ b/auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/McpClientAutoConfiguration.java @@ -0,0 +1,293 @@ +/* + * 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.autoconfigure.mcp.client; + +import java.util.ArrayList; +import java.util.List; + +import io.modelcontextprotocol.client.McpAsyncClient; +import io.modelcontextprotocol.client.McpClient; +import io.modelcontextprotocol.client.McpSyncClient; +import io.modelcontextprotocol.spec.McpSchema; + +import org.springframework.ai.autoconfigure.mcp.client.configurer.McpAsyncClientConfigurer; +import org.springframework.ai.autoconfigure.mcp.client.configurer.McpSyncClientConfigurer; +import org.springframework.ai.autoconfigure.mcp.client.properties.McpClientCommonProperties; +import org.springframework.ai.mcp.McpToolUtils; +import org.springframework.ai.mcp.customizer.McpAsyncClientCustomizer; +import org.springframework.ai.mcp.customizer.McpSyncClientCustomizer; +import org.springframework.ai.tool.ToolCallback; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.util.CollectionUtils; + +/** + * Auto-configuration for Model Context Protocol (MCP) client support. + * + *

+ * This configuration class sets up the necessary beans for MCP client functionality, + * including both synchronous and asynchronous clients along with their respective tool + * callbacks. It is automatically enabled when the required classes are present on the + * classpath and can be explicitly disabled through properties. + * + *

+ * Configuration Properties: + *

+ * + *

+ * The configuration is activated after the transport-specific auto-configurations (Stdio, + * SSE HTTP, and SSE WebFlux) to ensure proper initialization order. At least one + * transport must be available for the clients to be created. + * + *

+ * Key features: + *

+ * + * @see McpSyncClient + * @see McpAsyncClient + * @see McpClientCommonProperties + * @see McpSyncClientCustomizer + * @see McpAsyncClientCustomizer + * @see StdioTransportAutoConfiguration + * @see SseHttpClientTransportAutoConfiguration + * @see SseWebFluxTransportAutoConfiguration + */ +@AutoConfiguration(after = { StdioTransportAutoConfiguration.class, SseHttpClientTransportAutoConfiguration.class, + SseWebFluxTransportAutoConfiguration.class }) +@ConditionalOnClass({ McpSchema.class }) +@EnableConfigurationProperties(McpClientCommonProperties.class) +@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", + matchIfMissing = true) +public class McpClientAutoConfiguration { + + /** + * Creates a list of {@link McpSyncClient} instances based on the available + * transports. + * + *

+ * Each client is configured with: + *

+ * + *

+ * If initialization is enabled in properties, the clients are automatically + * initialized. + * @param mcpSyncClientConfigurer the configurer for customizing client creation + * @param commonProperties common MCP client properties + * @param transportsProvider provider of named MCP transports + * @return list of configured MCP sync clients + */ + @Bean + @ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "SYNC", + matchIfMissing = true) + public List mcpSyncClients(McpSyncClientConfigurer mcpSyncClientConfigurer, + McpClientCommonProperties commonProperties, + ObjectProvider> transportsProvider) { + + List mcpSyncClients = new ArrayList<>(); + + List namedTransports = transportsProvider.stream().flatMap(List::stream).toList(); + + if (!CollectionUtils.isEmpty(namedTransports)) { + for (NamedClientMcpTransport namedTransport : namedTransports) { + + McpSchema.Implementation clientInfo = new McpSchema.Implementation(commonProperties.getName(), + commonProperties.getVersion()); + + McpClient.SyncSpec syncSpec = McpClient.sync(namedTransport.transport()) + .clientInfo(clientInfo) + .requestTimeout(commonProperties.getRequestTimeout()); + + syncSpec = mcpSyncClientConfigurer.configure(namedTransport.name(), syncSpec); + + var syncClient = syncSpec.build(); + + if (commonProperties.isInitialized()) { + syncClient.initialize(); + } + + mcpSyncClients.add(syncClient); + } + } + + return mcpSyncClients; + } + + /** + * Creates tool callbacks for all configured MCP clients. + * + *

+ * These callbacks enable integration with Spring AI's tool execution framework, + * allowing MCP tools to be used as part of AI interactions. + * @param mcpClientsProvider provider of MCP sync clients + * @return list of tool callbacks for MCP integration + */ + @Bean + @ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "SYNC", + matchIfMissing = true) + public List toolCallbacks(ObjectProvider> mcpClientsProvider) { + List mcpClients = mcpClientsProvider.stream().flatMap(List::stream).toList(); + return McpToolUtils.getToolCallbacksFromSyncClients(mcpClients); + } + + /** + * Record class that implements {@link AutoCloseable} to ensure proper cleanup of MCP + * clients. + * + *

+ * This class is responsible for closing all MCP sync clients when the application + * context is closed, preventing resource leaks. + */ + public record ClosebleMcpSyncClients(List clients) implements AutoCloseable { + + @Override + public void close() { + this.clients.forEach(McpSyncClient::close); + } + } + + /** + * Creates a closeable wrapper for MCP sync clients to ensure proper resource cleanup. + * @param clients the list of MCP sync clients to manage + * @return a closeable wrapper for the clients + */ + @Bean + @ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "SYNC", + matchIfMissing = true) + public ClosebleMcpSyncClients makeSyncClientsClosable(List clients) { + return new ClosebleMcpSyncClients(clients); + } + + /** + * Creates the default {@link McpSyncClientConfigurer} if none is provided. + * + *

+ * This configurer aggregates all available {@link McpSyncClientCustomizer} instances + * to allow for customization of MCP sync client creation. + * @param customizerProvider provider of MCP sync client customizers + * @return the configured MCP sync client configurer + */ + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "SYNC", + matchIfMissing = true) + McpSyncClientConfigurer mcpSyncClientConfigurer(ObjectProvider customizerProvider) { + return new McpSyncClientConfigurer(customizerProvider.orderedStream().toList()); + } + + // Async client configuration + + @Bean + @ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC") + public List mcpAsyncClients(McpAsyncClientConfigurer mcpSyncClientConfigurer, + McpClientCommonProperties commonProperties, + ObjectProvider> transportsProvider) { + + List mcpSyncClients = new ArrayList<>(); + + List namedTransports = transportsProvider.stream().flatMap(List::stream).toList(); + + if (!CollectionUtils.isEmpty(namedTransports)) { + for (NamedClientMcpTransport namedTransport : namedTransports) { + + McpSchema.Implementation clientInfo = new McpSchema.Implementation(commonProperties.getName(), + commonProperties.getVersion()); + + McpClient.AsyncSpec syncSpec = McpClient.async(namedTransport.transport()) + .clientInfo(clientInfo) + .requestTimeout(commonProperties.getRequestTimeout()); + + syncSpec = mcpSyncClientConfigurer.configure(namedTransport.name(), syncSpec); + + var syncClient = syncSpec.build(); + + if (commonProperties.isInitialized()) { + syncClient.initialize(); + } + + mcpSyncClients.add(syncClient); + } + } + + return mcpSyncClients; + } + + @Bean + @ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC") + public List asyncToolCallbacks(ObjectProvider> mcpClientsProvider) { + List mcpClients = mcpClientsProvider.stream().flatMap(List::stream).toList(); + return McpToolUtils.getToolCallbacksFromAsyncClinents(mcpClients); + } + + public record ClosebleMcpAsyncClients(List clients) implements AutoCloseable { + @Override + public void close() { + this.clients.forEach(McpAsyncClient::close); + } + } + + @Bean + @ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC") + public ClosebleMcpAsyncClients makeAsynClientsClosable(List clients) { + return new ClosebleMcpAsyncClients(clients); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC") + McpAsyncClientConfigurer mcpAsyncClientConfigurer(ObjectProvider customizerProvider) { + return new McpAsyncClientConfigurer(customizerProvider.orderedStream().toList()); + } + +} diff --git a/auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/NamedClientMcpTransport.java b/auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/NamedClientMcpTransport.java new file mode 100644 index 000000000..de2bb7c60 --- /dev/null +++ b/auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/NamedClientMcpTransport.java @@ -0,0 +1,32 @@ +/* +* Copyright 2024 - 2024 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.autoconfigure.mcp.client; + +import io.modelcontextprotocol.spec.ClientMcpTransport; + +/** + * A named MCP client transport. Usually created by the transport auto-configurations, but + * you can also create them manually. Expose the list castom NamedClientMcpTransport + * as @Bean. + * + * @param name the name of the transport. Usually the name of the server connection. + * @param transport the MCP client transport. + * @author Christian Tzolov + * @since 1.0.0 + */ +public record NamedClientMcpTransport(String name, ClientMcpTransport transport) { + +} diff --git a/auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/SseHttpClientTransportAutoConfiguration.java b/auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/SseHttpClientTransportAutoConfiguration.java new file mode 100644 index 000000000..31aaa2244 --- /dev/null +++ b/auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/SseHttpClientTransportAutoConfiguration.java @@ -0,0 +1,116 @@ +/* + * 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.autoconfigure.mcp.client; + +import java.net.http.HttpClient; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.modelcontextprotocol.client.McpSyncClient; +import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport; +import io.modelcontextprotocol.spec.McpSchema; + +import org.springframework.ai.autoconfigure.mcp.client.properties.McpClientCommonProperties; +import org.springframework.ai.autoconfigure.mcp.client.properties.McpSseClientProperties; +import org.springframework.ai.autoconfigure.mcp.client.properties.McpSseClientProperties.SseParameters; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; + +/** + * Auto-configuration for Server-Sent Events (SSE) HTTP client transport in the Model + * Context Protocol (MCP). + * + *

+ * This configuration class sets up the necessary beans for SSE-based HTTP client + * transport when WebFlux is not available. It provides HTTP client-based SSE transport + * implementation for MCP client communication. + * + *

+ * The configuration is activated after the WebFlux SSE transport auto-configuration to + * ensure proper fallback behavior when WebFlux is not available. + * + *

+ * Key features: + *

+ * + * @see HttpClientSseClientTransport + * @see McpSseClientProperties + */ +@AutoConfiguration(after = SseWebFluxTransportAutoConfiguration.class) +@ConditionalOnClass({ McpSchema.class, McpSyncClient.class }) +@ConditionalOnMissingClass("io.modelcontextprotocol.client.transport.public class WebFluxSseClientTransport") +@EnableConfigurationProperties({ McpSseClientProperties.class, McpClientCommonProperties.class }) +@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", + matchIfMissing = true) +public class SseHttpClientTransportAutoConfiguration { + + /** + * Creates a list of HTTP client-based SSE transports for MCP communication. + * + *

+ * Each transport is configured with: + *

+ * @param sseProperties the SSE client properties containing server configurations + * @param objectMapper the ObjectMapper for JSON serialization/deserialization + * @return list of named MCP transports + */ + @Bean + public List mcpHttpClientTransports(McpSseClientProperties sseProperties, + ObjectMapper objectMapper) { + + List sseTransports = new ArrayList<>(); + + for (Map.Entry serverParameters : sseProperties.getConnections().entrySet()) { + + var transport = new HttpClientSseClientTransport(HttpClient.newBuilder(), serverParameters.getValue().url(), + objectMapper); + sseTransports.add(new NamedClientMcpTransport(serverParameters.getKey(), transport)); + } + + return sseTransports; + } + + /** + * Creates the default ObjectMapper if none is provided. + * + *

+ * This ObjectMapper is used for JSON serialization and deserialization in the SSE + * transport implementation. + * @return the configured ObjectMapper instance + */ + @Bean + @ConditionalOnMissingBean + public ObjectMapper objectMapper() { + return new ObjectMapper(); + } + +} diff --git a/auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/SseWebFluxTransportAutoConfiguration.java b/auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/SseWebFluxTransportAutoConfiguration.java new file mode 100644 index 000000000..d92520e5a --- /dev/null +++ b/auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/SseWebFluxTransportAutoConfiguration.java @@ -0,0 +1,124 @@ +/* + * 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.autoconfigure.mcp.client; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.modelcontextprotocol.client.transport.WebFluxSseClientTransport; + +import org.springframework.ai.autoconfigure.mcp.client.properties.McpClientCommonProperties; +import org.springframework.ai.autoconfigure.mcp.client.properties.McpSseClientProperties; +import org.springframework.ai.autoconfigure.mcp.client.properties.McpSseClientProperties.SseParameters; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.web.reactive.function.client.WebClient; + +/** + * Auto-configuration for WebFlux-based Server-Sent Events (SSE) client transport in the + * Model Context Protocol (MCP). + * + *

+ * This configuration class sets up the necessary beans for SSE-based WebFlux transport, + * providing reactive transport implementation for MCP client communication when WebFlux + * is available on the classpath. + * + *

+ * Key features: + *

    + *
  • Creates WebFlux-based SSE transports for configured MCP server connections + *
  • Configures WebClient.Builder for HTTP client operations + *
  • Sets up ObjectMapper for JSON serialization/deserialization + *
  • Supports multiple named server connections with different base URLs + *
+ * + * @see WebFluxSseClientTransport + * @see McpSseClientProperties + */ +@AutoConfiguration +@ConditionalOnClass(WebFluxSseClientTransport.class) +@EnableConfigurationProperties({ McpSseClientProperties.class, McpClientCommonProperties.class }) +@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", + matchIfMissing = true) +public class SseWebFluxTransportAutoConfiguration { + + /** + * Creates a list of WebFlux-based SSE transports for MCP communication. + * + *

+ * Each transport is configured with: + *

    + *
  • A cloned WebClient.Builder with server-specific base URL + *
  • ObjectMapper for JSON processing + *
  • Server connection parameters from properties + *
+ * @param sseProperties the SSE client properties containing server configurations + * @param webClientBuilderTemplate the template WebClient.Builder to clone for each + * connection + * @param objectMapper the ObjectMapper for JSON serialization/deserialization + * @return list of named MCP transports + */ + @Bean + public List webFluxClientTransports(McpSseClientProperties sseProperties, + WebClient.Builder webClientBuilderTemplate, ObjectMapper objectMapper) { + + List sseTransports = new ArrayList<>(); + + for (Map.Entry serverParameters : sseProperties.getConnections().entrySet()) { + var webClientBuilder = webClientBuilderTemplate.clone().baseUrl(serverParameters.getValue().url()); + var transport = new WebFluxSseClientTransport(webClientBuilder, objectMapper); + sseTransports.add(new NamedClientMcpTransport(serverParameters.getKey(), transport)); + } + + return sseTransports; + } + + /** + * Creates the default WebClient.Builder if none is provided. + * + *

+ * This builder serves as a template for creating server-specific WebClient instances + * used in SSE transport implementation. + * @return the configured WebClient.Builder instance + */ + @Bean + @ConditionalOnMissingBean + public WebClient.Builder webClientBuilder() { + return WebClient.builder(); + } + + /** + * Creates the default ObjectMapper if none is provided. + * + *

+ * This ObjectMapper is used for JSON serialization and deserialization in the SSE + * transport implementation. + * @return the configured ObjectMapper instance + */ + @Bean + @ConditionalOnMissingBean + public ObjectMapper objectMapper() { + return new ObjectMapper(); + } + +} diff --git a/auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/StdioTransportAutoConfiguration.java b/auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/StdioTransportAutoConfiguration.java new file mode 100644 index 000000000..bc5e8d3c9 --- /dev/null +++ b/auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/StdioTransportAutoConfiguration.java @@ -0,0 +1,87 @@ +/* + * 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.autoconfigure.mcp.client; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import io.modelcontextprotocol.client.transport.ServerParameters; +import io.modelcontextprotocol.client.transport.StdioClientTransport; +import io.modelcontextprotocol.spec.McpSchema; + +import org.springframework.ai.autoconfigure.mcp.client.properties.McpClientCommonProperties; +import org.springframework.ai.autoconfigure.mcp.client.properties.McpStdioClientProperties; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; + +/** + * Auto-configuration for Standard Input/Output (stdio) transport in the Model Context + * Protocol (MCP). + * + *

+ * This configuration class sets up the necessary beans for stdio-based transport, + * enabling communication with MCP servers through standard input and output streams. + * + *

+ * Key features: + *

    + *
  • Creates stdio transports for configured MCP server connections + *
  • Supports multiple named server connections with different parameters + *
  • Configures transport with server-specific parameters + *
+ * + * @see StdioClientTransport + * @see McpStdioClientProperties + */ +@AutoConfiguration +@ConditionalOnClass({ McpSchema.class }) +@EnableConfigurationProperties({ McpStdioClientProperties.class, McpClientCommonProperties.class }) +@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", + matchIfMissing = true) +public class StdioTransportAutoConfiguration { + + /** + * Creates a list of stdio-based transports for MCP communication. + * + *

+ * Each transport is configured with: + *

    + *
  • Server-specific parameters from properties + *
  • Unique connection name for identification + *
+ * @param sdioProperties the stdio client properties containing server configurations + * @return list of named MCP transports + */ + @Bean + public List stdioTransports(McpStdioClientProperties sdioProperties) { + + List stdoiTransports = new ArrayList<>(); + + for (Map.Entry serverParameters : sdioProperties.toServerParameters().entrySet()) { + var transport = new StdioClientTransport(serverParameters.getValue()); + stdoiTransports.add(new NamedClientMcpTransport(serverParameters.getKey(), transport)); + + } + + return stdoiTransports; + } + +} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/mcp/client/stdio/McpSyncClientConfigurer.java b/auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/configurer/McpAsyncClientConfigurer.java similarity index 62% rename from spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/mcp/client/stdio/McpSyncClientConfigurer.java rename to auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/configurer/McpAsyncClientConfigurer.java index 159a38057..70b33d7a0 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/mcp/client/stdio/McpSyncClientConfigurer.java +++ b/auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/configurer/McpAsyncClientConfigurer.java @@ -14,30 +14,30 @@ * limitations under the License. */ -package org.springframework.ai.autoconfigure.mcp.client.stdio; +package org.springframework.ai.autoconfigure.mcp.client.configurer; import java.util.List; import io.modelcontextprotocol.client.McpClient; -import org.springframework.ai.mcp.McpSyncClientCustomizer; +import org.springframework.ai.mcp.customizer.McpAsyncClientCustomizer; -public class McpSyncClientConfigurer { +public class McpAsyncClientConfigurer { - private List customizers; + private List customizers; - void setCustomizers(List customizers) { + public McpAsyncClientConfigurer(List customizers) { this.customizers = customizers; } - public McpClient.SyncSpec configure(String name, McpClient.SyncSpec spec) { + public McpClient.AsyncSpec configure(String name, McpClient.AsyncSpec spec) { applyCustomizers(name, spec); return spec; } - private void applyCustomizers(String name, McpClient.SyncSpec spec) { + private void applyCustomizers(String name, McpClient.AsyncSpec spec) { if (this.customizers != null) { - for (McpSyncClientCustomizer customizer : this.customizers) { + for (McpAsyncClientCustomizer customizer : this.customizers) { customizer.customize(name, spec); } } diff --git a/auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/configurer/McpSyncClientConfigurer.java b/auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/configurer/McpSyncClientConfigurer.java new file mode 100644 index 000000000..cc8c331cd --- /dev/null +++ b/auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/configurer/McpSyncClientConfigurer.java @@ -0,0 +1,76 @@ +/* + * 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.autoconfigure.mcp.client.configurer; + +import java.util.List; + +import io.modelcontextprotocol.client.McpClient; + +import org.springframework.ai.mcp.customizer.McpSyncClientCustomizer; + +/** + * Configurer class for customizing MCP synchronous clients. + * + *

+ * This class manages a collection of {@link McpSyncClientCustomizer} instances that can + * be applied to customize the configuration of MCP synchronous clients during their + * creation. + * + *

+ * The configurer applies customizations in the order they are registered, allowing for + * sequential modifications to the client specifications. + * + * @see McpSyncClientCustomizer + * @see McpClient.SyncSpec + */ +public class McpSyncClientConfigurer { + + private List customizers; + + public McpSyncClientConfigurer(List customizers) { + this.customizers = customizers; + } + + /** + * Configures an MCP sync client specification by applying all registered customizers. + * @param name the name of the client being configured + * @param spec the specification to customize + * @return the customized specification + */ + public McpClient.SyncSpec configure(String name, McpClient.SyncSpec spec) { + applyCustomizers(name, spec); + return spec; + } + + /** + * Applies all registered customizers to the given specification. + * + *

+ * Customizers are applied in the order they were registered. If no customizers are + * registered, this method has no effect. + * @param name the name of the client being customized + * @param spec the specification to customize + */ + private void applyCustomizers(String name, McpClient.SyncSpec spec) { + if (this.customizers != null) { + for (McpSyncClientCustomizer customizer : this.customizers) { + customizer.customize(name, spec); + } + } + } + +} diff --git a/auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/properties/McpClientCommonProperties.java b/auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/properties/McpClientCommonProperties.java new file mode 100644 index 000000000..b7c73aae1 --- /dev/null +++ b/auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/properties/McpClientCommonProperties.java @@ -0,0 +1,160 @@ +/* + * 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.autoconfigure.mcp.client.properties; + +import java.time.Duration; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Common Configuration properties for the Model Context Protocol (MCP) clients shared for + * all transport types. + * + * @author Christian Tzolov + * @since 1.0.0 + */ +@ConfigurationProperties(McpClientCommonProperties.CONFIG_PREFIX) +public class McpClientCommonProperties { + + public static final String CONFIG_PREFIX = "spring.ai.mcp.client"; + + /** + * Enable/disable the MCP client. + *

+ * When set to false, the MCP client and all its components will not be initialized. + */ + private boolean enabled = true; + + /** + * The name of the MCP client instance. + *

+ * This name is reported to clients and used for compatibility checks. + */ + private String name = "spring-ai-mcp-client"; + + /** + * The version of the MCP client instance. + *

+ * This version is reported to clients and used for compatibility checks. + */ + private String version = "1.0.0"; + + /** + * Flag to indicate if the MCP client has to be initialized. + */ + private boolean initialized = true; + + /** + * The timeout duration for MCP client requests. + *

+ * Defaults to 20 seconds. + */ + private Duration requestTimeout = Duration.ofSeconds(20); + + /** + * The type of client to use for MCP client communication. + *

+ * Supported types are: + *

    + *
  • SYNC - Standard synchronous client (default)
  • + *
  • ASYNC - Asynchronous client
  • + *
+ */ + private ClientType type = ClientType.SYNC; + + /** + * Client types supported by the MCP client. + */ + public enum ClientType { + + /** + * Synchronous (McpSyncClient) client + */ + SYNC, + + /** + * Asynchronous (McpAsyncClient) client + */ + ASYNC + + } + + /** + * Flag to enable/disable root change notifications. + *

+ * When enabled, the client will be notified of changes to the root configuration. + * Defaults to true. + */ + private boolean rootChangeNotification = true; + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public String getVersion() { + return this.version; + } + + public void setVersion(String version) { + this.version = version; + } + + public boolean isInitialized() { + return this.initialized; + } + + public void setInitialized(boolean initialized) { + this.initialized = initialized; + } + + public Duration getRequestTimeout() { + return this.requestTimeout; + } + + public void setRequestTimeout(Duration requestTimeout) { + this.requestTimeout = requestTimeout; + } + + public ClientType getType() { + return this.type; + } + + public void setType(ClientType type) { + this.type = type; + } + + public boolean isRootChangeNotification() { + return this.rootChangeNotification; + } + + public void setRootChangeNotification(boolean rootChangeNotification) { + this.rootChangeNotification = rootChangeNotification; + } + +} diff --git a/auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/properties/McpSseClientProperties.java b/auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/properties/McpSseClientProperties.java new file mode 100644 index 000000000..ed5575a14 --- /dev/null +++ b/auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/properties/McpSseClientProperties.java @@ -0,0 +1,73 @@ +/* +* Copyright 2024 - 2024 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.autoconfigure.mcp.client.properties; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Configuration properties for Server-Sent Events (SSE) based MCP client connections. + * + *

+ * These properties allow configuration of multiple named SSE connections to MCP servers. + * Each connection is configured with a URL endpoint for SSE communication. + * + *

+ * Example configuration:

+ * spring.ai.mcp.client.sse:
+ *   connections:
+ *     server1:
+ *       url: http://localhost:8080/events
+ *     server2:
+ *       url: http://otherserver:8081/events
+ * 
+ * + * @author Christian Tzolov + * @since 1.0.0 + * @see SseParameters + */ +@ConfigurationProperties(McpSseClientProperties.CONFIG_PREFIX) +public class McpSseClientProperties { + + public static final String CONFIG_PREFIX = "spring.ai.mcp.client.sse"; + + /** + * Parameters for configuring an SSE connection to an MCP server. + * + * @param url the URL endpoint for SSE communication with the MCP server + */ + public record SseParameters(String url) { + } + + /** + * Map of named SSE connection configurations. + *

+ * The key represents the connection name, and the value contains the SSE parameters + * for that connection. + */ + private final Map connections = new HashMap<>(); + + /** + * Returns the map of configured SSE connections. + * @return map of connection names to their SSE parameters + */ + public Map getConnections() { + return this.connections; + } + +} diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/mcp/client/stdio/McpStdioClientProperties.java b/auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/properties/McpStdioClientProperties.java similarity index 66% rename from spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/mcp/client/stdio/McpStdioClientProperties.java rename to auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/properties/McpStdioClientProperties.java index 34a72f67a..1284b9d51 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/mcp/client/stdio/McpStdioClientProperties.java +++ b/auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/properties/McpStdioClientProperties.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.ai.autoconfigure.mcp.client.stdio; +package org.springframework.ai.autoconfigure.mcp.client.properties; import java.time.Duration; import java.util.HashMap; @@ -47,35 +47,6 @@ public class McpStdioClientProperties { public static final String CONFIG_PREFIX = "spring.ai.mcp.client.stdio"; - /** - * Enable/disable the MCP client. - *

- * When set to false, the MCP client and all its components will not be initialized. - */ - private boolean enabled = false; - - /** - * The version of the MCP client instance. - *

- * This version is reported to clients and used for compatibility checks. - */ - private String version = "1.0.0"; - - /** - * The timeout duration for MCP client requests. - *

- * Defaults to 20 seconds. - */ - private Duration requestTimeout = Duration.ofSeconds(20); - - /** - * Flag to enable/disable root change notifications. - *

- * When enabled, the client will be notified of changes to the root configuration. - * Defaults to true. - */ - private boolean rootChangeNotification = true; - /** * Resource containing the MCP servers configuration. *

@@ -90,12 +61,7 @@ public class McpStdioClientProperties { * Each entry represents a named connection with its specific configuration * parameters. */ - private final Map stdioConnections = new HashMap<>(); - - /** - * Flag to indicate if the MCP client has to be initialized. - */ - private boolean initialize = true; + private final Map connections = new HashMap<>(); public Resource getServersConfiguration() { return this.serversConfiguration; @@ -105,50 +71,8 @@ public class McpStdioClientProperties { this.serversConfiguration = stdioConnectionResources; } - public Map getStdioConnections() { - return this.stdioConnections; - } - - public boolean isRootChangeNotification() { - return this.rootChangeNotification; - } - - public void setRootChangeNotification(boolean rootChangeNotification) { - this.rootChangeNotification = rootChangeNotification; - } - - public Duration getRequestTimeout() { - return this.requestTimeout; - } - - public void setRequestTimeout(Duration requestTimeout) { - Assert.notNull(requestTimeout, "Request timeout must not be null"); - this.requestTimeout = requestTimeout; - } - - public boolean isEnabled() { - return this.enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - public String getVersion() { - return this.version; - } - - public void setVersion(String version) { - Assert.hasText(version, "Version must not be empty"); - this.version = version; - } - - public boolean isInitialize() { - return this.initialize; - } - - public void setInitialize(boolean initialize) { - this.initialize = initialize; + public Map getConnections() { + return this.connections; } /** @@ -170,6 +94,11 @@ public class McpStdioClientProperties { * Map of environment variables for the server process. */ @JsonProperty("env") Map env) { + + public ServerParameters toServerParameters() { + return ServerParameters.builder(this.command()).args(this.args()).env(this.env()).build(); + } + } private Map resourceToServerParameters() { @@ -200,7 +129,7 @@ public class McpStdioClientProperties { serverParameters.putAll(resourceToServerParameters()); } - for (Map.Entry entry : this.stdioConnections.entrySet()) { + for (Map.Entry entry : this.connections.entrySet()) { serverParameters.put(entry.getKey(), entry.getValue().toServerParameters()); } return serverParameters; diff --git a/auto-configurations/spring-ai-mcp-client/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/spring-ai-mcp-client/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000..f13b5f748 --- /dev/null +++ b/auto-configurations/spring-ai-mcp-client/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,21 @@ +# +# 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. +# +org.springframework.ai.autoconfigure.mcp.client.StdioTransportAutoConfiguration +org.springframework.ai.autoconfigure.mcp.client.SseWebFluxTransportAutoConfiguration +org.springframework.ai.autoconfigure.mcp.client.SseHttpClientTransportAutoConfiguration +org.springframework.ai.autoconfigure.mcp.client.McpClientAutoConfiguration + + diff --git a/auto-configurations/spring-ai-mcp-client/src/test/java/org/springframework/ai/autoconfigure/mcp/client/McpClientAutoConfigurationIT.java b/auto-configurations/spring-ai-mcp-client/src/test/java/org/springframework/ai/autoconfigure/mcp/client/McpClientAutoConfigurationIT.java new file mode 100644 index 000000000..780d105d9 --- /dev/null +++ b/auto-configurations/spring-ai-mcp-client/src/test/java/org/springframework/ai/autoconfigure/mcp/client/McpClientAutoConfigurationIT.java @@ -0,0 +1,190 @@ +/* + * 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.autoconfigure.mcp.client; + +import java.time.Duration; +import java.util.List; +import java.util.function.Function; + +import com.fasterxml.jackson.core.type.TypeReference; +import io.modelcontextprotocol.client.McpAsyncClient; +import io.modelcontextprotocol.client.McpSyncClient; +import io.modelcontextprotocol.spec.ClientMcpTransport; +import io.modelcontextprotocol.spec.McpSchema; +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.autoconfigure.mcp.client.configurer.McpSyncClientConfigurer; +import org.springframework.ai.autoconfigure.mcp.client.properties.McpClientCommonProperties; +import org.springframework.ai.mcp.customizer.McpSyncClientCustomizer; +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; + +@Disabled +public class McpClientAutoConfigurationIT { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(McpClientAutoConfiguration.class)); + + @Test + void defaultConfiguration() { + this.contextRunner.withUserConfiguration(TestTransportConfiguration.class).run(context -> { + List clients = context.getBean("mcpSyncClients", List.class); + assertThat(clients).hasSize(1); + + McpClientCommonProperties properties = context.getBean(McpClientCommonProperties.class); + assertThat(properties.getName()).isEqualTo("mcp-client"); + assertThat(properties.getVersion()).isEqualTo("1.0.0"); + assertThat(properties.getType()).isEqualTo(McpClientCommonProperties.ClientType.SYNC); + assertThat(properties.getRequestTimeout()).isEqualTo(Duration.ofSeconds(30)); + assertThat(properties.isInitialized()).isTrue(); + }); + } + + @Test + void asyncConfiguration() { + this.contextRunner + .withPropertyValues("spring.ai.mcp.client.type=ASYNC", "spring.ai.mcp.client.name=test-client", + "spring.ai.mcp.client.version=2.0.0", "spring.ai.mcp.client.request-timeout=60s", + "spring.ai.mcp.client.initialized=false") + .withUserConfiguration(TestTransportConfiguration.class) + .run(context -> { + List clients = context.getBean("mcpAsyncClients", List.class); + assertThat(clients).hasSize(1); + + McpClientCommonProperties properties = context.getBean(McpClientCommonProperties.class); + assertThat(properties.getName()).isEqualTo("test-client"); + assertThat(properties.getVersion()).isEqualTo("2.0.0"); + assertThat(properties.getType()).isEqualTo(McpClientCommonProperties.ClientType.ASYNC); + assertThat(properties.getRequestTimeout()).isEqualTo(Duration.ofSeconds(60)); + assertThat(properties.isInitialized()).isFalse(); + }); + } + + @Test + void disabledConfiguration() { + this.contextRunner.withPropertyValues("spring.ai.mcp.client.enabled=false").run(context -> { + assertThat(context).doesNotHaveBean(McpSyncClient.class); + assertThat(context).doesNotHaveBean(McpAsyncClient.class); + assertThat(context).doesNotHaveBean(ToolCallback.class); + }); + } + + @Test + void customTransportConfiguration() { + this.contextRunner.withUserConfiguration(CustomTransportConfiguration.class).run(context -> { + List transports = context.getBean("customTransports", List.class); + assertThat(transports).hasSize(1); + assertThat(transports.get(0).transport()).isInstanceOf(CustomClientTransport.class); + }); + } + + @Test + void clientCustomization() { + this.contextRunner.withUserConfiguration(TestTransportConfiguration.class, CustomizerConfiguration.class) + .run(context -> { + assertThat(context).hasSingleBean(McpSyncClientConfigurer.class); + List clients = context.getBean("mcpSyncClients", List.class); + assertThat(clients).hasSize(1); + }); + } + + @Test + void toolCallbacksCreation() { + this.contextRunner.withUserConfiguration(TestTransportConfiguration.class).run(context -> { + assertThat(context).hasSingleBean(List.class); + List callbacks = context.getBean("toolCallbacks", List.class); + assertThat(callbacks).isNotEmpty(); + }); + } + + @Test + void closeableWrappersCreation() { + this.contextRunner.withUserConfiguration(TestTransportConfiguration.class).run(context -> { + assertThat(context).hasSingleBean(McpClientAutoConfiguration.ClosebleMcpSyncClients.class); + }); + } + + @Configuration + static class TestTransportConfiguration { + + @Bean + List testTransports() { + return List.of(new NamedClientMcpTransport("test", Mockito.mock(ClientMcpTransport.class))); + } + + } + + @Configuration + static class CustomTransportConfiguration { + + @Bean + List customTransports() { + return List.of(new NamedClientMcpTransport("custom", new CustomClientTransport())); + } + + } + + @Configuration + static class CustomizerConfiguration { + + @Bean + McpSyncClientCustomizer testCustomizer() { + return (name, spec) -> { + /* no-op */ }; + } + + } + + static class CustomClientTransport implements ClientMcpTransport { + + @Override + public void close() { + // Test implementation + } + + @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 Mono closeGracefully() { + return Mono.empty(); // Test implementation + } + + } + +} diff --git a/auto-configurations/spring-ai-mcp-client/src/test/resources/application-test.properties b/auto-configurations/spring-ai-mcp-client/src/test/resources/application-test.properties new file mode 100644 index 000000000..9107b9e40 --- /dev/null +++ b/auto-configurations/spring-ai-mcp-client/src/test/resources/application-test.properties @@ -0,0 +1,10 @@ +# Test MCP STDIO client configuration +spring.ai.mcp.client.stdio.enabled=true +spring.ai.mcp.client.stdio.version=test-version +spring.ai.mcp.client.stdio.request-timeout=15s +spring.ai.mcp.client.stdio.root-change-notification=false + +# Test server configuration +spring.ai.mcp.client.stdio.stdio-connections.test-server.command=echo +spring.ai.mcp.client.stdio.stdio-connections.test-server.args[0]=test +spring.ai.mcp.client.stdio.stdio-connections.test-server.env.TEST_ENV=test-value diff --git a/auto-configurations/spring-ai-mcp-server/pom.xml b/auto-configurations/spring-ai-mcp-server/pom.xml new file mode 100644 index 000000000..751884f8a --- /dev/null +++ b/auto-configurations/spring-ai-mcp-server/pom.xml @@ -0,0 +1,62 @@ + + + 4.0.0 + + org.springframework.ai + spring-ai + 1.0.0-SNAPSHOT + ../../pom.xml + + spring-ai-mcp-server-spring-boot-autoconfigure + jar + Spring AI MCP Server Auto Configuration + Spring AI MCP Server Auto Configuration + https://github.com/spring-projects/spring-ai + + + https://github.com/spring-projects/spring-ai + git://github.com/spring-projects/spring-ai.git + git@github.com:spring-projects/spring-ai.git + + + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.ai + spring-ai-mcp + ${project.parent.version} + true + + + + io.modelcontextprotocol.sdk + mcp-spring-webflux + true + + + + io.modelcontextprotocol.sdk + mcp-spring-webmvc + true + + + + + + org.springframework.ai + spring-ai-test + ${project.parent.version} + test + + + + + + diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/mcp/server/McpServerProperties.java b/auto-configurations/spring-ai-mcp-server/src/main/java/org/springframework/ai/autoconfigure/mcp/server/McpServerProperties.java similarity index 82% rename from spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/mcp/server/McpServerProperties.java rename to auto-configurations/spring-ai-mcp-server/src/main/java/org/springframework/ai/autoconfigure/mcp/server/McpServerProperties.java index 5be80b131..00867c089 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/mcp/server/McpServerProperties.java +++ b/auto-configurations/spring-ai-mcp-server/src/main/java/org/springframework/ai/autoconfigure/mcp/server/McpServerProperties.java @@ -25,7 +25,6 @@ import org.springframework.util.Assert; * These properties control the behavior and configuration of the MCP server, including: *

    *
  • Server identification (name and version)
  • - *
  • Transport type (STDIO, WEBMVC, or WEBFLUX)
  • *
  • Change notification settings for tools, resources, and prompts
  • *
  • Web transport endpoint configuration
  • *
@@ -46,7 +45,15 @@ public class McpServerProperties { *

* When set to false, the MCP server and all its components will not be initialized. */ - private boolean enabled = false; + private boolean enabled = true; + + /** + * Enable/disable the standard input/output (stdio) transport. + *

+ * When enabled, the server will listen for incoming messages on the standard input + * and write responses to the standard output. + */ + private boolean stdio = false; /** * The name of the MCP server instance. @@ -88,18 +95,6 @@ public class McpServerProperties { */ private boolean promptChangeNotification = true; - /** - * The transport type to use for MCP server communication. - *

- * Supported types are: - *

    - *
  • STDIO - Standard input/output transport (default)
  • - *
  • WEBMVC - Spring MVC Server-Sent Events transport
  • - *
  • WEBFLUX - Spring WebFlux Server-Sent Events transport
  • - *
- */ - private Transport transport = Transport.STDIO; - /** * The endpoint path for Server-Sent Events (SSE) when using web transports. *

@@ -118,31 +113,6 @@ public class McpServerProperties { */ private ServerType type = ServerType.SYNC; - /** - * Transport types supported by the MCP server. - */ - public enum Transport { - - /** - * Standard input/output transport, suitable for command-line tools and local - * development. - */ - STDIO, - - /** - * Spring MVC Server-Sent Events transport, requires spring-boot-starter-web and - * mcp-spring-webmvc. - */ - WEBMVC, - - /** - * Spring WebFlux Server-Sent Events transport, requires - * spring-boot-starter-webflux and mcp-spring-webflux. - */ - WEBFLUX - - } - /** * Server types supported by the MCP server. */ @@ -160,6 +130,14 @@ public class McpServerProperties { } + public boolean isStdio() { + return this.stdio; + } + + public void setStdio(boolean stdio) { + this.stdio = stdio; + } + public boolean isEnabled() { return this.enabled; } @@ -210,15 +188,6 @@ public class McpServerProperties { this.promptChangeNotification = promptChangeNotification; } - public Transport getTransport() { - return this.transport; - } - - public void setTransport(Transport transport) { - Assert.notNull(transport, "Transport must not be null"); - this.transport = transport; - } - public String getSseMessageEndpoint() { return this.sseMessageEndpoint; } diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/mcp/server/MpcServerAutoConfiguration.java b/auto-configurations/spring-ai-mcp-server/src/main/java/org/springframework/ai/autoconfigure/mcp/server/MpcServerAutoConfiguration.java similarity index 72% rename from spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/mcp/server/MpcServerAutoConfiguration.java rename to auto-configurations/spring-ai-mcp-server/src/main/java/org/springframework/ai/autoconfigure/mcp/server/MpcServerAutoConfiguration.java index 1293f3d97..dfb8f9760 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/mcp/server/MpcServerAutoConfiguration.java +++ b/auto-configurations/spring-ai-mcp-server/src/main/java/org/springframework/ai/autoconfigure/mcp/server/MpcServerAutoConfiguration.java @@ -20,19 +20,23 @@ import java.util.List; import java.util.function.Consumer; import java.util.function.Function; -import io.modelcontextprotocol.server.McpServer; -import reactor.core.publisher.Mono; -import io.modelcontextprotocol.server.McpServer.SyncSpec; -import io.modelcontextprotocol.server.McpServer.AsyncSpec; -import io.modelcontextprotocol.server.McpServerFeatures; -import io.modelcontextprotocol.server.McpServerFeatures.SyncToolRegistration; -import io.modelcontextprotocol.server.McpServerFeatures.AsyncToolRegistration; -import io.modelcontextprotocol.server.McpSyncServer; import io.modelcontextprotocol.server.McpAsyncServer; +import io.modelcontextprotocol.server.McpServer; +import io.modelcontextprotocol.server.McpServer.AsyncSpec; +import io.modelcontextprotocol.server.McpServer.SyncSpec; +import io.modelcontextprotocol.server.McpServerFeatures; +import io.modelcontextprotocol.server.McpServerFeatures.AsyncPromptRegistration; +import io.modelcontextprotocol.server.McpServerFeatures.AsyncResourceRegistration; +import io.modelcontextprotocol.server.McpServerFeatures.AsyncToolRegistration; +import io.modelcontextprotocol.server.McpServerFeatures.SyncPromptRegistration; +import io.modelcontextprotocol.server.McpServerFeatures.SyncResourceRegistration; +import io.modelcontextprotocol.server.McpServerFeatures.SyncToolRegistration; +import io.modelcontextprotocol.server.McpSyncServer; import io.modelcontextprotocol.server.transport.StdioServerTransport; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSchema.Implementation; import io.modelcontextprotocol.spec.ServerMcpTransport; +import reactor.core.publisher.Mono; import org.springframework.ai.mcp.McpToolUtils; import org.springframework.ai.tool.ToolCallback; @@ -45,6 +49,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.core.log.LogAccessor; +import org.springframework.util.CollectionUtils; /** * {@link EnableAutoConfiguration Auto-configuration} for the Model Context Protocol (MCP) @@ -94,18 +99,17 @@ import org.springframework.core.log.LogAccessor; * @see MpcWebMvcServerAutoConfiguration * @see org.springframework.ai.mcp.ToolCallback */ -@AutoConfiguration +@AutoConfiguration(after = { MpcWebMvcServerAutoConfiguration.class, MpcWebFluxServerAutoConfiguration.class }) @ConditionalOnClass({ McpSchema.class, McpSyncServer.class }) @EnableConfigurationProperties(McpServerProperties.class) -@ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true") +@ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", + matchIfMissing = true) public class MpcServerAutoConfiguration { private static final LogAccessor logger = new LogAccessor(MpcServerAutoConfiguration.class); @Bean @ConditionalOnMissingBean - @ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "transport", havingValue = "STDIO", - matchIfMissing = true) public ServerMcpTransport stdioServerTransport() { return new StdioServerTransport(); } @@ -119,8 +123,9 @@ public class MpcServerAutoConfiguration { @Bean @ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "type", havingValue = "SYNC", matchIfMissing = true) - public List syncTools(List toolCalls) { - return McpToolUtils.toSyncToolRegistration(toolCalls); + public List syncTools(ObjectProvider> toolCalls) { + var tools = toolCalls.stream().flatMap(List::stream).toList(); + return McpToolUtils.toSyncToolRegistration(tools); } @Bean @@ -128,9 +133,8 @@ public class MpcServerAutoConfiguration { matchIfMissing = true) public McpSyncServer mcpSyncServer(ServerMcpTransport transport, McpSchema.ServerCapabilities.Builder capabilitiesBuilder, McpServerProperties serverProperties, - ObjectProvider> tools, - ObjectProvider> resources, - ObjectProvider> prompts, + ObjectProvider> tools, ObjectProvider> resources, + ObjectProvider> prompts, ObjectProvider>> rootsChangeConsumers) { McpSchema.Implementation serverInfo = new Implementation(serverProperties.getName(), @@ -139,26 +143,29 @@ public class MpcServerAutoConfiguration { // Create the server with both tool and resource capabilities SyncSpec serverBuilder = McpServer.sync(transport).serverInfo(serverInfo); - tools.ifAvailable(toolsList -> { - serverBuilder.tools(toolsList); + List toolResgistrations = tools.stream().flatMap(List::stream).toList(); + if (!CollectionUtils.isEmpty(toolResgistrations)) { + serverBuilder.tools(toolResgistrations); capabilitiesBuilder.tools(serverProperties.isToolChangeNotification()); - logger.info("Registered tools" + toolsList.size() + " notification: " + logger.info("Registered tools" + toolResgistrations.size() + " notification: " + serverProperties.isToolChangeNotification()); - }); + } - resources.ifAvailable(resourceList -> { - serverBuilder.resources(resourceList); + List resourceResgistrations = resources.stream().flatMap(List::stream).toList(); + if (!CollectionUtils.isEmpty(resourceResgistrations)) { + serverBuilder.resources(resourceResgistrations); capabilitiesBuilder.resources(false, serverProperties.isResourceChangeNotification()); - logger.info("Registered resources" + resourceList.size() + " notification: " + logger.info("Registered resources" + resourceResgistrations.size() + " notification: " + serverProperties.isResourceChangeNotification()); - }); + } - prompts.ifAvailable(promptList -> { - serverBuilder.prompts(promptList); + List promptResgistrations = prompts.stream().flatMap(List::stream).toList(); + if (!CollectionUtils.isEmpty(promptResgistrations)) { + serverBuilder.prompts(promptResgistrations); capabilitiesBuilder.prompts(serverProperties.isPromptChangeNotification()); - logger.info("Registered prompts" + promptList.size() + " notification: " + logger.info("Registered prompts" + promptResgistrations.size() + " notification: " + serverProperties.isPromptChangeNotification()); - }); + } rootsChangeConsumers.ifAvailable(consumer -> { serverBuilder.rootsChangeConsumer(consumer); @@ -172,8 +179,9 @@ public class MpcServerAutoConfiguration { @Bean @ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC") - public List asyncTools(List toolCalls) { - return McpToolUtils.toAsyncToolRegistration(toolCalls); + public List asyncTools(ObjectProvider> toolCalls) { + var tools = toolCalls.stream().flatMap(List::stream).toList(); + return McpToolUtils.toAsyncToolRegistration(tools); } @Bean @@ -181,8 +189,8 @@ public class MpcServerAutoConfiguration { public McpAsyncServer mcpAsyncServer(ServerMcpTransport transport, McpSchema.ServerCapabilities.Builder capabilitiesBuilder, McpServerProperties serverProperties, ObjectProvider> tools, - ObjectProvider> resources, - ObjectProvider> prompts, + ObjectProvider> resources, + ObjectProvider> prompts, ObjectProvider>> rootsChangeConsumer) { McpSchema.Implementation serverInfo = new Implementation(serverProperties.getName(), @@ -191,26 +199,29 @@ public class MpcServerAutoConfiguration { // Create the server with both tool and resource capabilities AsyncSpec serverBilder = McpServer.async(transport).serverInfo(serverInfo); - tools.ifAvailable(toolsList -> { - serverBilder.tools(toolsList); + List toolResgistrations = tools.stream().flatMap(List::stream).toList(); + if (!CollectionUtils.isEmpty(toolResgistrations)) { + serverBilder.tools(toolResgistrations); capabilitiesBuilder.tools(serverProperties.isToolChangeNotification()); - logger.info("Registered tools" + toolsList.size() + " notification: " + logger.info("Registered tools" + toolResgistrations.size() + " notification: " + serverProperties.isToolChangeNotification()); - }); + } - resources.ifAvailable(resourceList -> { - serverBilder.resources(resourceList); + List resourceResgistrations = resources.stream().flatMap(List::stream).toList(); + if (!CollectionUtils.isEmpty(resourceResgistrations)) { + serverBilder.resources(resourceResgistrations); capabilitiesBuilder.resources(false, serverProperties.isResourceChangeNotification()); - logger.info("Registered resources" + resourceList.size() + " notification: " + logger.info("Registered resources" + resourceResgistrations.size() + " notification: " + serverProperties.isResourceChangeNotification()); - }); + } - prompts.ifAvailable(promptList -> { - serverBilder.prompts(promptList); + List promptResgistrations = prompts.stream().flatMap(List::stream).toList(); + if (!CollectionUtils.isEmpty(promptResgistrations)) { + serverBilder.prompts(promptResgistrations); capabilitiesBuilder.prompts(serverProperties.isPromptChangeNotification()); - logger.info("Registered prompts" + promptList.size() + " notification: " + logger.info("Registered prompts" + promptResgistrations.size() + " notification: " + serverProperties.isPromptChangeNotification()); - }); + } rootsChangeConsumer.ifAvailable(consumer -> { Function, Mono> asyncConsumer = roots -> { diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/mcp/server/MpcWebFluxServerAutoConfiguration.java b/auto-configurations/spring-ai-mcp-server/src/main/java/org/springframework/ai/autoconfigure/mcp/server/MpcWebFluxServerAutoConfiguration.java similarity index 94% rename from spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/mcp/server/MpcWebFluxServerAutoConfiguration.java rename to auto-configurations/spring-ai-mcp-server/src/main/java/org/springframework/ai/autoconfigure/mcp/server/MpcWebFluxServerAutoConfiguration.java index a1ce31bca..703ee4843 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/mcp/server/MpcWebFluxServerAutoConfiguration.java +++ b/auto-configurations/spring-ai-mcp-server/src/main/java/org/springframework/ai/autoconfigure/mcp/server/MpcWebFluxServerAutoConfiguration.java @@ -18,6 +18,7 @@ package org.springframework.ai.autoconfigure.mcp.server; import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.server.transport.WebFluxSseServerTransport; +import io.modelcontextprotocol.spec.ServerMcpTransport; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -64,7 +65,9 @@ import org.springframework.web.reactive.function.server.RouterFunction; */ @AutoConfiguration @ConditionalOnClass({ WebFluxSseServerTransport.class }) -@ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "transport", havingValue = "WEBFLUX") +@ConditionalOnMissingBean(ServerMcpTransport.class) +@ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "stdio", havingValue = "false", + matchIfMissing = true) public class MpcWebFluxServerAutoConfiguration { @Bean diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/mcp/server/MpcWebMvcServerAutoConfiguration.java b/auto-configurations/spring-ai-mcp-server/src/main/java/org/springframework/ai/autoconfigure/mcp/server/MpcWebMvcServerAutoConfiguration.java similarity index 94% rename from spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/mcp/server/MpcWebMvcServerAutoConfiguration.java rename to auto-configurations/spring-ai-mcp-server/src/main/java/org/springframework/ai/autoconfigure/mcp/server/MpcWebMvcServerAutoConfiguration.java index 5759cc4f6..55d19df49 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/mcp/server/MpcWebMvcServerAutoConfiguration.java +++ b/auto-configurations/spring-ai-mcp-server/src/main/java/org/springframework/ai/autoconfigure/mcp/server/MpcWebMvcServerAutoConfiguration.java @@ -18,6 +18,7 @@ package org.springframework.ai.autoconfigure.mcp.server; import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.server.transport.WebMvcSseServerTransport; +import io.modelcontextprotocol.spec.ServerMcpTransport; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -48,10 +49,6 @@ import org.springframework.web.servlet.function.ServerResponse; *

* Required dependencies:

{@code
  * 
- *     io.modelcontextprotocol.sdk
- *     mcp-spring-webmvc
- * 
- * 
  *     org.springframework.boot
  *     spring-boot-starter-web
  * 
@@ -64,7 +61,9 @@ import org.springframework.web.servlet.function.ServerResponse;
  */
 @AutoConfiguration
 @ConditionalOnClass({ WebMvcSseServerTransport.class })
-@ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "transport", havingValue = "WEBMVC")
+@ConditionalOnMissingBean(ServerMcpTransport.class)
+@ConditionalOnProperty(prefix = McpServerProperties.CONFIG_PREFIX, name = "stdio", havingValue = "false",
+		matchIfMissing = true)
 public class MpcWebMvcServerAutoConfiguration {
 
 	@Bean
diff --git a/auto-configurations/spring-ai-mcp-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/spring-ai-mcp-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
new file mode 100644
index 000000000..eee7e32e6
--- /dev/null
+++ b/auto-configurations/spring-ai-mcp-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -0,0 +1,20 @@
+#
+# 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.
+#
+
+org.springframework.ai.autoconfigure.mcp.server.MpcServerAutoConfiguration
+org.springframework.ai.autoconfigure.mcp.server.MpcWebMvcServerAutoConfiguration
+org.springframework.ai.autoconfigure.mcp.server.MpcWebFluxServerAutoConfiguration
+
diff --git a/auto-configurations/spring-ai-mcp-server/src/test/java/org/springframework/ai/autoconfigure/mcp/server/McpServerAutoConfigurationIT.java b/auto-configurations/spring-ai-mcp-server/src/test/java/org/springframework/ai/autoconfigure/mcp/server/McpServerAutoConfigurationIT.java
new file mode 100644
index 000000000..34490ea53
--- /dev/null
+++ b/auto-configurations/spring-ai-mcp-server/src/test/java/org/springframework/ai/autoconfigure/mcp/server/McpServerAutoConfigurationIT.java
@@ -0,0 +1,311 @@
+/*
+ * 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.autoconfigure.mcp.server;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import io.modelcontextprotocol.client.McpSyncClient;
+import io.modelcontextprotocol.server.McpAsyncServer;
+import io.modelcontextprotocol.server.McpServerFeatures.AsyncToolRegistration;
+import io.modelcontextprotocol.server.McpServerFeatures.SyncToolRegistration;
+import io.modelcontextprotocol.server.McpServerFeatures.SyncResourceRegistration;
+import io.modelcontextprotocol.server.McpServerFeatures.SyncPromptRegistration;
+import io.modelcontextprotocol.server.McpSyncServer;
+import io.modelcontextprotocol.server.transport.StdioServerTransport;
+import io.modelcontextprotocol.spec.McpSchema;
+import io.modelcontextprotocol.spec.ServerMcpTransport;
+import org.mockito.Mockito;
+import org.junit.jupiter.api.Test;
+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 reactor.core.publisher.Mono;
+
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class McpServerAutoConfigurationIT {
+
+	private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+		.withConfiguration(AutoConfigurations.of(MpcServerAutoConfiguration.class));
+
+	@Test
+	void defaultConfiguration() {
+		this.contextRunner.run(context -> {
+			assertThat(context).hasSingleBean(McpSyncServer.class);
+			assertThat(context).hasSingleBean(ServerMcpTransport.class);
+			assertThat(context.getBean(ServerMcpTransport.class)).isInstanceOf(StdioServerTransport.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(ServerMcpTransport.class);
+			assertThat(context.getBean(ServerMcpTransport.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(1);
+		});
+	}
+
+	@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(1);
+			});
+	}
+
+	@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);
+
+			return List.of(new SyncMcpToolCallback(mockClient, mockTool));
+		}
+
+	}
+
+	@Configuration
+	static class TestRootsChangeConfiguration {
+
+		@Bean
+		Consumer> rootsChangeConsumer() {
+			return roots -> {
+				// Test implementation
+			};
+		}
+
+	}
+
+	static class CustomServerTransport implements ServerMcpTransport {
+
+		@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
+		ServerMcpTransport customTransport() {
+			return new CustomServerTransport();
+		}
+
+	}
+
+}
diff --git a/mcp-client-boot-starter-docs.adoc b/mcp-client-boot-starter-docs.adoc
new file mode 100644
index 000000000..a43ec00a6
--- /dev/null
+++ b/mcp-client-boot-starter-docs.adoc
@@ -0,0 +1,340 @@
+= Spring AI MCP Client Boot Starter
+
+The Spring AI MCP (Model Context Protocol) Client Boot Starter provides auto-configuration for MCP client functionality in Spring Boot applications. It supports both synchronous and asynchronous client implementations with various transport options.
+
+The MCP Client Boot Starter provides:
+
+* Automatic client initialization (if enabled)
+* Support for multiple named transports
+* Integration with Spring AI's tool execution framework
+* Proper lifecycle management with automatic cleanup
+* Customizable client creation through customizers
+
+
+== Dependencies
+
+=== Core Starter
+
+[source,xml]
+----
+
+    org.springframework.ai
+    spring-ai-mcp-client-spring-boot-starter
+    ${spring-ai.version}
+
+----
+
+It will connect, simultaneously, to one or more MCP Servers over `STDIO` (in-process) and/or `SSE` (remote) transports.
+The SSE connection uses the HttpClient-based transport implementation.
+Every connection to an MCP Server creates a new MCP Client instance.
+You can opt for either `SYNC` or `ASYNC` MCP Clients (Note: you cannot mix sync and async clients).
+For more enterprise-ready deployment, it is recommended to use the WebFlux-based SSE connection using the `spring-ai-mcp-client-webflux-spring-boot-starter` starter.
+
+=== WebFlux Starter
+
+Similar to the core starter, it allows configuring one or more STDIO and SSE connections, but uses WebFlux-based SSE transport implementation.
+
+[source,xml]
+----
+
+    org.springframework.ai
+    spring-ai-mcp-client-webflux-spring-boot-starter
+    ${spring-ai.version}
+
+----
+
+== Configuration Properties
+
+=== Common Properties
+
+All common configuration properties are prefixed with `spring.ai.mcp.client`:
+
+[cols="3,4,3"]
+|===
+|Property |Description |Default Value
+
+|`enabled`
+|Enable/disable the MCP client
+|`true`
+
+|`name`
+|Name of the MCP client instance (used for compatibility checks)
+|`spring-ai-mcp-client`
+
+|`version`
+|Version of the MCP client instance
+|`1.0.0`
+
+|`initialized`
+|Whether to initialize clients on creation
+|`true`
+
+|`request-timeout`
+|Timeout duration for MCP client requests
+|`20s`
+
+|`type`
+|Client type (SYNC or ASYNC). You can not mix client type. All clients can be either sync or async
+|`SYNC`
+
+|`root-change-notification`
+|Enable/disable root change notifications for all clients
+|`true`
+|===
+
+=== SSE Transport Properties
+
+Properties for Server-Sent Events (SSE) transport are prefixed with `spring.ai.mcp.client.sse`:
+
+[cols="2,4"]
+|===
+|Property |Description
+
+|`connections`
+|Map of named SSE connection configurations
+
+|`connections.[name].url`
+|URL endpoint for SSE communication with the MCP server
+|===
+
+Example configuration:
+[source,yaml]
+----
+spring:
+  ai:
+    mcp:
+      client:
+        sse:
+          connections:
+            server1:
+              url: http://localhost:8080
+            server2:
+              url: http://otherserver:8081
+----
+
+=== Stdio Transport Properties
+
+Properties for Standard I/O transport are prefixed with `spring.ai.mcp.client.stdio`:
+
+[cols="3,4,3"]
+|===
+|Property |Description |Default Value
+
+|`servers-configuration`
+|Resource containing the MCP servers configuration in JSON format
+|-
+
+|`connections`
+|Map of named stdio connection configurations
+|-
+
+|`connections.[name].command`
+|The command to execute for the MCP server
+|-
+
+|`connections.[name].args`
+|List of command arguments
+|-
+
+|`connections.[name].env`
+|Map of environment variables for the server process
+|-
+|===
+
+Example configuration:
+[source,yaml]
+----
+spring:
+  ai:
+    mcp:
+      client:
+        stdio:
+          root-change-notification: true
+          connections:
+            server1:
+              command: /path/to/server
+              args:
+                - --port=8080
+                - --mode=production
+              env:
+                API_KEY: your-api-key
+                DEBUG: "true"
+----
+
+Alternatively, you can configure stdio connections using an external JSON file using the link:https://modelcontextprotocol.io/quickstart/user[Claude Desctop format]:
+
+[source,yaml]
+----
+spring:
+  ai:
+    mcp:
+      client:
+        stdio:
+          servers-configuration: classpath:mcp-servers.json
+----
+
+The Claude Destop format looks like this:
+
+[source,json]
+----
+{
+  "mcpServers": {
+    "filesystem": {
+      "command": "npx",
+      "args": [
+        "-y",
+        "@modelcontextprotocol/server-filesystem",
+        "/Users/username/Desktop",
+        "/Users/username/Downloads"
+      ]
+    }
+  }
+}
+----
+Currently the Claude Destop supports only STDIO connection types.
+
+== Features
+
+=== Client Types
+
+The starter supports two types of clients:
+
+1. *Synchronous Client (SYNC)*
+   * Default client type
+   * Blocking operations
+   * Suitable for traditional request-response patterns
+
+2. *Asynchronous Client (ASYNC)*
+   * Non-blocking operations
+   * Suitable for reactive applications
+   * Must be explicitly configured using `spring.ai.mcp.client.type=ASYNC`
+
+=== Client Customization
+
+The auto-configuration supports customization through:
+
+* `McpSyncClientCustomizer` for synchronous clients
+* `McpAsyncClientCustomizer` for asynchronous clients
+
+== Usage Example
+
+1. Add the appropriate starter dependency to your project.
+
+2. Configure the client in `application.properties` or `application.yml`:
+
+[source,yaml]
+----
+spring:
+  ai:
+    mcp:
+      client:
+        enabled: true
+        name: my-mcp-client
+        version: 1.0.0
+        request-timeout: 30s
+        type: SYNC  # or ASYNC for reactive applications
+        sse:
+          connections:
+            server1:
+              url: http://localhost:8080
+            server2:
+              url: http://otherserver:8081        
+        stdio:
+          root-change-notification: false
+          connections:
+            server1:
+              command: /path/to/server
+              args:
+                - --port=8080
+                - --mode=production
+              env:
+                API_KEY: your-api-key
+                DEBUG: "true"
+----
+
+3. The MCP client beans will be automatically configured and available for injection:
+
+[source,java]
+----
+@Autowired
+private List mcpSyncClients;  // For sync client
+
+// OR
+
+@Autowired
+private List mcpAsyncClients;  // For async client
+----
+
+== Transport Support
+
+The auto-configuration supports multiple transport types:
+
+* Standard I/O (Stdio)
+* SSE HTTP
+* SSE WebFlux (requires `spring-ai-starter-mcp-client-webflux`)
+
+At least one transport must be available for the clients to be created.
+
+== Integration with Spring AI
+
+The starter automatically configures tool callbacks that integrate with Spring AI's tool execution framework, allowing MCP tools to be used as part of AI interactions.
+
+== Lifecycle Management
+
+The auto-configuration includes proper lifecycle management:
+
+* Automatic initialization of clients (if enabled)
+* Proper cleanup of resources when the application context is closed
+* Management of multiple client instances
+
+== Best Practices
+
+1. Choose the appropriate client type based on your application's needs:
+   * Use SYNC client for traditional applications
+   * Use ASYNC client for reactive applications
+
+2. Configure appropriate timeout values based on your use case:
+[source,yaml]
+----
+spring:
+  ai:
+    mcp:
+      client:
+        request-timeout: 30s
+----
+
+3. Use customizers for advanced client configuration:
+[source,java]
+----
+@Component
+public class MyMcpClientCustomizer implements McpSyncClientCustomizer {
+    @Override
+    public void customize(String name, McpClient.SyncSpec clientSpec) {
+        // Custom configuration
+    }
+}
+----
+
+== Troubleshooting
+
+Common issues and solutions:
+
+1. *Client Not Created*
+   * Verify that at least one transport is available
+   * Check if the client is enabled in configuration
+   * Ensure required dependencies are present
+
+2. *Timeout Issues*
+   * Adjust the `request-timeout` property
+   * Check network connectivity
+   * Verify server response times
+
+3. *Integration Issues*
+   * Ensure proper transport configuration
+   * Check client initialization status
+
+== Additional Resources
+
+* link:https://docs.spring.io/spring-ai/reference/[Spring AI Documentation]
+* link:https://modelcontextprotocol.github.io/specification/[Model Context Protocol Specification]
+* link:https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.developing-auto-configuration[Spring Boot Auto-configuration]
diff --git a/mcp-client-boot-starter-docs.md b/mcp-client-boot-starter-docs.md
new file mode 100644
index 000000000..ea7cc8600
--- /dev/null
+++ b/mcp-client-boot-starter-docs.md
@@ -0,0 +1,284 @@
+# Spring AI MCP Client Boot Starter
+
+The Spring AI MCP (Model Context Protocol) Client Boot Starter provides auto-configuration for MCP client functionality in Spring Boot applications. It supports both synchronous and asynchronous client implementations with various transport options.
+
+The MCP Client Boot Starter provides:
+
+- Automatic client initialization (if enabled)
+- Support for multiple named transports
+- Integration with Spring AI's tool execution framework
+- Proper lifecycle management with automatic cleanup
+- Customizable client creation through customizers
+
+
+## Dependencies
+
+### Core Starter
+
+```xml
+
+    org.springframework.ai
+    spring-ai-mcp-client-spring-boot-starter
+    ${spring-ai.version}
+
+```
+
+It will connect, simultaneously, to one or more MCP Servers over `STDIO` (in-process) and/or `SSE` (remote) transports.
+The SSE connection uses the HttpClient-based transport implementation.
+Every connection to an MCP Server creates a new MCP Client instance.
+You can opt for either `SYNC` or `ASYNC` MCP Clients (Note: you cannot mix sync and async clients).
+For more enterprise-ready deployment, it is recommended to use the WebFlux-based SSE connection using the `spring-ai-mcp-client-webflux-spring-boot-starter` starter.
+
+### WebFlux Starter
+
+Similar to the core starter, it allows configuring one or more STDIO and SSE connections, but uses WebFlux-based SSE transport implementation.
+
+```xml
+
+    org.springframework.ai
+    spring-ai-mcp-client-webflux-spring-boot-starter
+    ${spring-ai.version}
+
+```
+
+## Configuration Properties
+
+### Common Properties
+
+All common configuration properties are prefixed with `spring.ai.mcp.client`:
+
+| Property | Description | Default Value |
+|----------|-------------|---------------|
+| `enabled` | Enable/disable the MCP client | `true` |
+| `name` | Name of the MCP client instance (used for compatibility checks) | `spring-ai-mcp-client` |
+| `version` | Version of the MCP client instance | `1.0.0` |
+| `initialized` | Whether to initialize clients on creation | `true` |
+| `request-timeout` | Timeout duration for MCP client requests | `20s` |
+| `type` | Client type (SYNC or ASYNC). You can not mix client type. All clients can be either sync or async | `SYNC` |
+| `root-change-notification` | Enable/disable root change notifications for all clients| `true` |
+
+### SSE Transport Properties
+
+Properties for Server-Sent Events (SSE) transport are prefixed with `spring.ai.mcp.client.sse`:
+
+| Property | Description |
+|----------|-------------|
+| `connections` | Map of named SSE connection configurations |
+| `connections.[name].url` | URL endpoint for SSE communication with the MCP server |
+
+Example configuration:
+```yaml
+spring:
+  ai:
+    mcp:
+      client:
+        sse:
+          connections:
+            server1:
+              url: http://localhost:8080
+            server2:
+              url: http://otherserver:8081
+```
+
+### Stdio Transport Properties
+
+Properties for Standard I/O transport are prefixed with `spring.ai.mcp.client.stdio`:
+
+| Property | Description | Default Value |
+|----------|-------------|---------------|
+| `servers-configuration` | Resource containing the MCP servers configuration in JSON format | - |
+| `connections` | Map of named stdio connection configurations | - |
+| `connections.[name].command` | The command to execute for the MCP server | - |
+| `connections.[name].args` | List of command arguments | - |
+| `connections.[name].env` | Map of environment variables for the server process | - |
+
+Example configuration:
+```yaml
+spring:
+  ai:
+    mcp:
+      client:
+        stdio:
+          root-change-notification: true
+          connections:
+            server1:
+              command: /path/to/server
+              args:
+                - --port=8080
+                - --mode=production
+              env:
+                API_KEY: your-api-key
+                DEBUG: "true"
+```
+
+Alternatively, you can configure stdio connections using an external JSON file using the [Claude Desctop format](https://modelcontextprotocol.io/quickstart/user):
+
+```yaml
+spring:
+  ai:
+    mcp:
+      client:
+        stdio:
+          servers-configuration: classpath:mcp-servers.json
+```
+
+The Claude Destop format looks like this:
+
+```json
+{
+  "mcpServers": {
+    "filesystem": {
+      "command": "npx",
+      "args": [
+        "-y",
+        "@modelcontextprotocol/server-filesystem",
+        "/Users/username/Desktop",
+        "/Users/username/Downloads"
+      ]
+    }
+  }
+}
+```
+Currently the Claude Destop supports only STDIO connection types.
+
+## Features
+
+### Client Types
+
+The starter supports two types of clients:
+
+1. **Synchronous Client (SYNC)**
+   - Default client type
+   - Blocking operations
+   - Suitable for traditional request-response patterns
+
+2. **Asynchronous Client (ASYNC)**
+   - Non-blocking operations
+   - Suitable for reactive applications
+   - Must be explicitly configured using `spring.ai.mcp.client.type=ASYNC`
+
+### Client Customization
+
+The auto-configuration supports customization through:
+
+- `McpSyncClientCustomizer` for synchronous clients
+- `McpAsyncClientCustomizer` for asynchronous clients
+
+## Usage Example
+
+1. Add the appropriate starter dependency to your project.
+
+2. Configure the client in `application.properties` or `application.yml`:
+
+```yaml
+spring:
+  ai:
+    mcp:
+      client:
+        enabled: true
+        name: my-mcp-client
+        version: 1.0.0
+        request-timeout: 30s
+        type: SYNC  # or ASYNC for reactive applications
+        sse:
+          connections:
+            server1:
+              url: http://localhost:8080
+            server2:
+              url: http://otherserver:8081        
+        stdio:
+          root-change-notification: false
+          connections:
+            server1:
+              command: /path/to/server
+              args:
+                - --port=8080
+                - --mode=production
+              env:
+                API_KEY: your-api-key
+                DEBUG: "true"
+```
+
+3. The MCP client beans will be automatically configured and available for injection:
+
+```java
+@Autowired
+private List mcpSyncClients;  // For sync client
+
+// OR
+
+@Autowired
+private List mcpAsyncClients;  // For async client
+```
+
+## Transport Support
+
+The auto-configuration supports multiple transport types:
+
+- Standard I/O (Stdio)
+- SSE HTTP
+- SSE WebFlux (requires `spring-ai-starter-mcp-client-webflux`)
+
+At least one transport must be available for the clients to be created.
+
+## Integration with Spring AI
+
+The starter automatically configures tool callbacks that integrate with Spring AI's tool execution framework, allowing MCP tools to be used as part of AI interactions.
+
+## Lifecycle Management
+
+The auto-configuration includes proper lifecycle management:
+
+- Automatic initialization of clients (if enabled)
+- Proper cleanup of resources when the application context is closed
+- Management of multiple client instances
+
+## Best Practices
+
+1. Choose the appropriate client type based on your application's needs:
+   - Use SYNC client for traditional applications
+   - Use ASYNC client for reactive applications
+
+2. Configure appropriate timeout values based on your use case:
+   ```yaml
+   spring:
+     ai:
+       mcp:
+         client:
+           request-timeout: 30s
+   ```
+
+3. Use customizers for advanced client configuration:
+   ```java
+   @Component
+   public class MyMcpClientCustomizer implements McpSyncClientCustomizer {
+       @Override
+       public void customize(String name, McpClient.SyncSpec clientSpec) {
+           // Custom configuration
+       }
+   }
+   ```
+
+## Troubleshooting
+
+Common issues and solutions:
+
+1. **Client Not Created**
+   - Verify that at least one transport is available
+   - Check if the client is enabled in configuration
+   - Ensure required dependencies are present
+
+2. **Timeout Issues**
+   - Adjust the `request-timeout` property
+   - Check network connectivity
+   - Verify server response times
+
+3. **Integration Issues**
+   - Ensure proper transport configuration
+   - Check client initialization status
+
+## Additional Resources
+
+- [Spring AI Documentation](https://docs.spring.io/spring-ai/reference/)
+- [Model Context Protocol Specification](https://modelcontextprotocol.github.io/specification/)
+- [Spring Boot Auto-configuration](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.developing-auto-configuration)
diff --git a/mcp-helpers.adoc b/mcp-helpers.adoc
new file mode 100644
index 000000000..532816eb1
--- /dev/null
+++ b/mcp-helpers.adoc
@@ -0,0 +1,168 @@
+= Spring AI MCP Common Utilities
+:page-title: Spring AI MCP Common Utilities
+
+This document provides reference documentation for the common utilities in Spring AI's Model Context Protocol (MCP) integration.
+
+== Overview
+
+The MCP common utilities provide foundational support for integrating Model Context Protocol with Spring AI applications. These utilities enable seamless communication between Spring AI's tool system and MCP servers, supporting both synchronous and asynchronous operations.
+
+== Core Components
+
+=== Tool Callbacks
+
+==== AsyncMcpToolCallback
+
+Adapts MCP tools to Spring AI's tool interface with asynchronous execution support.
+
+[source,java]
+----
+McpAsyncClient mcpClient = // obtain MCP client
+Tool mcpTool = // obtain MCP tool definition
+ToolCallback callback = new AsyncMcpToolCallback(mcpClient, mcpTool);
+
+// Use the tool through Spring AI's interfaces
+ToolDefinition definition = callback.getToolDefinition();
+String result = callback.call("{\"param\": \"value\"}");
+----
+
+==== SyncMcpToolCallback
+
+Similar to AsyncMcpToolCallback but provides synchronous execution semantics.
+
+[source,java]
+----
+McpSyncClient mcpClient = // obtain MCP client
+Tool mcpTool = // obtain MCP tool definition
+ToolCallback callback = new SyncMcpToolCallback(mcpClient, mcpTool);
+
+// Use the tool through Spring AI's interfaces
+ToolDefinition definition = callback.getToolDefinition();
+String result = callback.call("{\"param\": \"value\"}");
+----
+
+=== Tool Callback Providers
+
+==== AsyncMcpToolCallbackProvider
+
+Discovers and provides MCP tools asynchronously.
+
+[source,java]
+----
+McpAsyncClient mcpClient = // obtain MCP client
+ToolCallbackProvider provider = new AsyncMcpToolCallbackProvider(mcpClient);
+
+// Get all available tools
+ToolCallback[] tools = provider.getToolCallbacks();
+----
+
+The provider also offers a utility method for working with multiple clients:
+
+[source,java]
+----
+List clients = // obtain list of clients
+Flux callbacks = AsyncMcpToolCallbackProvider.asyncToolCallbacks(clients);
+----
+
+==== SyncMcpToolCallbackProvider
+
+Similar to AsyncMcpToolCallbackProvider but works with synchronous clients.
+
+[source,java]
+----
+McpSyncClient mcpClient = // obtain MCP client
+ToolCallbackProvider provider = new SyncMcpToolCallbackProvider(mcpClient);
+
+// Get all available tools
+ToolCallback[] tools = provider.getToolCallbacks();
+----
+
+For multiple clients:
+
+[source,java]
+----
+List clients = // obtain list of clients
+List callbacks = SyncMcpToolCallbackProvider.syncToolCallbacks(clients);
+----
+
+=== Client Customization
+
+==== McpAsyncClientCustomizer
+
+Allows customization of asynchronous MCP client configurations.
+
+[source,java]
+----
+@Component
+public class CustomMcpAsyncClientCustomizer implements McpAsyncClientCustomizer {
+    @Override
+    public void customize(String name, McpClient.AsyncSpec spec) {
+        // Customize the async client configuration
+        spec.requestTimeout(Duration.ofSeconds(30));
+    }
+}
+----
+
+==== McpSyncClientCustomizer
+
+Similar to McpAsyncClientCustomizer but for synchronous clients.
+
+[source,java]
+----
+@Component
+public class CustomMcpSyncClientCustomizer implements McpSyncClientCustomizer {
+    @Override
+    public void customize(String name, McpClient.SyncSpec spec) {
+        // Customize the sync client configuration
+        spec.requestTimeout(Duration.ofSeconds(30));
+    }
+}
+----
+
+=== Utility Classes
+
+==== McpToolUtils
+
+Provides helper methods for working with MCP tools in a Spring AI environment.
+
+Converting Spring AI tool callbacks to MCP tool registrations:
+
+[source,java]
+----
+// For synchronous tools
+List toolCallbacks = // obtain tool callbacks
+List syncRegs = McpToolUtils.toSyncToolRegistration(toolCallbacks);
+
+// For asynchronous tools
+List asyncRegs = McpToolUtils.toAsyncToolRegistration(toolCallbacks);
+----
+
+Getting tool callbacks from MCP clients:
+
+[source,java]
+----
+// From sync clients
+List syncClients = // obtain sync clients
+List syncCallbacks = McpToolUtils.getToolCallbacksFromSyncClients(syncClients);
+
+// From async clients
+List asyncClients = // obtain async clients
+List asyncCallbacks = McpToolUtils.getToolCallbacksFromAsyncClients(asyncClients);
+----
+
+=== Native Image Support
+
+==== McpHints
+
+Provides GraalVM native image hints for MCP schema classes.
+
+[source,java]
+----
+@Configuration
+@ImportRuntimeHints(McpHints.class)
+public class MyConfiguration {
+    // Configuration code
+}
+----
+
+This class automatically registers all necessary reflection hints for MCP schema classes when building native images.
diff --git a/mcp-helpers.md b/mcp-helpers.md
new file mode 100644
index 000000000..90b643e2d
--- /dev/null
+++ b/mcp-helpers.md
@@ -0,0 +1,156 @@
+# Spring AI MCP Common Utilities
+
+This document provides reference documentation for the common utilities in Spring AI's Model Context Protocol (MCP) integration.
+
+## Overview
+
+The MCP common utilities provide foundational support for integrating Model Context Protocol with Spring AI applications. These utilities enable seamless communication between Spring AI's tool system and MCP servers, supporting both synchronous and asynchronous operations.
+
+## Core Components
+
+### Tool Callbacks
+
+#### AsyncMcpToolCallback
+
+Adapts MCP tools to Spring AI's tool interface with asynchronous execution support.
+
+```java
+McpAsyncClient mcpClient = // obtain MCP client
+Tool mcpTool = // obtain MCP tool definition
+ToolCallback callback = new AsyncMcpToolCallback(mcpClient, mcpTool);
+
+// Use the tool through Spring AI's interfaces
+ToolDefinition definition = callback.getToolDefinition();
+String result = callback.call("{\"param\": \"value\"}");
+```
+
+#### SyncMcpToolCallback
+
+Similar to AsyncMcpToolCallback but provides synchronous execution semantics.
+
+```java
+McpSyncClient mcpClient = // obtain MCP client
+Tool mcpTool = // obtain MCP tool definition
+ToolCallback callback = new SyncMcpToolCallback(mcpClient, mcpTool);
+
+// Use the tool through Spring AI's interfaces
+ToolDefinition definition = callback.getToolDefinition();
+String result = callback.call("{\"param\": \"value\"}");
+```
+
+### Tool Callback Providers
+
+#### AsyncMcpToolCallbackProvider
+
+Discovers and provides MCP tools asynchronously.
+
+```java
+McpAsyncClient mcpClient = // obtain MCP client
+ToolCallbackProvider provider = new AsyncMcpToolCallbackProvider(mcpClient);
+
+// Get all available tools
+ToolCallback[] tools = provider.getToolCallbacks();
+```
+
+The provider also offers a utility method for working with multiple clients:
+
+```java
+List clients = // obtain list of clients
+Flux callbacks = AsyncMcpToolCallbackProvider.asyncToolCallbacks(clients);
+```
+
+#### SyncMcpToolCallbackProvider
+
+Similar to AsyncMcpToolCallbackProvider but works with synchronous clients.
+
+```java
+McpSyncClient mcpClient = // obtain MCP client
+ToolCallbackProvider provider = new SyncMcpToolCallbackProvider(mcpClient);
+
+// Get all available tools
+ToolCallback[] tools = provider.getToolCallbacks();
+```
+
+For multiple clients:
+
+```java
+List clients = // obtain list of clients
+List callbacks = SyncMcpToolCallbackProvider.syncToolCallbacks(clients);
+```
+
+### Client Customization
+
+#### McpAsyncClientCustomizer
+
+Allows customization of asynchronous MCP client configurations.
+
+```java
+@Component
+public class CustomMcpAsyncClientCustomizer implements McpAsyncClientCustomizer {
+    @Override
+    public void customize(String name, McpClient.AsyncSpec spec) {
+        // Customize the async client configuration
+        spec.requestTimeout(Duration.ofSeconds(30));
+    }
+}
+```
+
+#### McpSyncClientCustomizer
+
+Similar to McpAsyncClientCustomizer but for synchronous clients.
+
+```java
+@Component
+public class CustomMcpSyncClientCustomizer implements McpSyncClientCustomizer {
+    @Override
+    public void customize(String name, McpClient.SyncSpec spec) {
+        // Customize the sync client configuration
+        spec.requestTimeout(Duration.ofSeconds(30));
+    }
+}
+```
+
+### Utility Classes
+
+#### McpToolUtils
+
+Provides helper methods for working with MCP tools in a Spring AI environment.
+
+Converting Spring AI tool callbacks to MCP tool registrations:
+
+```java
+// For synchronous tools
+List toolCallbacks = // obtain tool callbacks
+List syncRegs = McpToolUtils.toSyncToolRegistration(toolCallbacks);
+
+// For asynchronous tools
+List asyncRegs = McpToolUtils.toAsyncToolRegistration(toolCallbacks);
+```
+
+Getting tool callbacks from MCP clients:
+
+```java
+// From sync clients
+List syncClients = // obtain sync clients
+List syncCallbacks = McpToolUtils.getToolCallbacksFromSyncClients(syncClients);
+
+// From async clients
+List asyncClients = // obtain async clients
+List asyncCallbacks = McpToolUtils.getToolCallbacksFromAsyncClients(asyncClients);
+```
+
+### Native Image Support
+
+#### McpHints
+
+Provides GraalVM native image hints for MCP schema classes.
+
+```java
+@Configuration
+@ImportRuntimeHints(McpHints.class)
+public class MyConfiguration {
+    // Configuration code
+}
+```
+
+This class automatically registers all necessary reflection hints for MCP schema classes when building native images.
\ No newline at end of file
diff --git a/mcp-server-boot-starter-docs.adoc b/mcp-server-boot-starter-docs.adoc
new file mode 100644
index 000000000..cf9784e9d
--- /dev/null
+++ b/mcp-server-boot-starter-docs.adoc
@@ -0,0 +1,310 @@
+= Spring AI MCP Server Boot Starter
+
+The Spring AI MCP (Model Context Protocol) Server Boot Starter provides auto-configuration for setting up an MCP server in Spring Boot applications. It enables seamless integration of MCP server capabilities with Spring Boot's auto-configuration system.
+
+== Overview
+
+The MCP Server Boot Starter offers:
+
+* Automatic configuration of MCP server components
+* Support for both synchronous and asynchronous operation modes
+* Multiple transport layer options
+* Flexible tool, resource, and prompt registration
+* Change notification capabilities
+
+== Starter Dependencies
+
+Choose one of the following starters based on your transport requirements:
+
+=== 1. Standard MCP Server
+
+Full MCP Server features support with `STDIO` server transport.
+
+[source,xml]
+----
+
+    org.springframework.ai
+    spring-ai-mcp-server-spring-boot-starter
+    ${spring-ai.version}
+
+----
+
+=== 2. WebMVC Server
+
+Full MCP Server features support with `SSE` (Server-Sent Events) server transport based on Spring MVC and an optional `STDIO` transport.
+
+[source,xml]
+----
+
+    org.springframework.ai
+    spring-ai-mcp-server-webmvc-spring-boot-starter
+    ${spring-ai.version}
+
+----
+
+This starter includes:
+
+* spring-boot-starter-web
+* mcp-spring-webmvc
+(optionally allows `stdio` transport deployment)
+
+=== 3. WebFlux Server
+
+Full MCP Server features support with `SSE` (Server-Sent Events) server transport based on Spring WebFlux and an optional `STDIO` transport.
+
+[source,xml]
+----
+
+    org.springframework.ai
+    spring-ai-mcp-server-webflux-spring-boot-starter
+    ${spring-ai.version}
+
+----
+
+This starter includes:
+
+* spring-boot-starter-webflux
+* mcp-spring-webflux
+(optionally allows `stdio` transport deployment)
+
+== Configuration Properties
+
+All properties are prefixed with `spring.ai.mcp.server`:
+
+[options="header"]
+|===
+|Property |Description |Default
+|`enabled` |Enable/disable the MCP server |`true`
+|`stdio` |Enable/disable stdio transport |`false`
+|`name` |Server name for identification |`mcp-server`
+|`version` |Server version |`1.0.0`
+|`type` |Server type (SYNC/ASYNC) |`SYNC`
+|`resource-change-notification` |Enable resource change notifications |`true`
+|`tool-change-notification` |Enable tool change notifications |`true`
+|`prompt-change-notification` |Enable prompt change notifications |`true`
+|`sse-message-endpoint` |SSE endpoint path for web transport |`/mcp/message`
+|===
+
+== Server Types
+
+=== Synchronous Server
+* Default server type
+* Uses `McpSyncServer`
+* Suitable for straightforward request-response patterns
+* Configure with `spring.ai.mcp.server.type=SYNC`
+* Automatically configures synchronous tool registrations
+
+=== Asynchronous Server
+* Uses `McpAsyncServer`
+* Suitable for non-blocking operations
+* Configure with `spring.ai.mcp.server.type=ASYNC`
+* Automatically configures asynchronous tool registrations with Project Reactor support
+
+== Transport Options
+
+The MCP Server supports three transport mechanisms, each with its dedicated starter:
+
+=== 1. Standard Input/Output (STDIO)
+* Use `spring-ai-mcp-server-spring-boot-starter`
+* Default transport when using the standard starter
+* Suitable for command-line tools and testing
+* No additional web dependencies required
+
+=== 2. Spring MVC (Server-Sent Events)
+* Use `spring-ai-mcp-server-webmvc-spring-boot-starter`
+* Provides HTTP-based transport using Spring MVC
+* Uses `WebMvcSseServerTransport`
+* Automatically configures SSE endpoints
+* Ideal for traditional web applications
+* Optionally you can deploy `STDIO` transport by setting the `spring.ai.mcp.server.stdio=true` property.
+
+=== 3. Spring WebFlux (Reactive SSE)
+* Use `spring-ai-mcp-server-webflux-spring-boot-starter`
+* Provides reactive transport using Spring WebFlux
+* Uses `WebFluxSseServerTransport`
+* Automatically configures reactive SSE endpoints
+* Ideal for reactive applications with non-blocking requirements
+* Optionally you can deploy `STDIO` transport by setting the `spring.ai.mcp.server.stdio=true` property.
+
+== Features and Capabilities
+
+=== 1. Tools Registration
+* Support for both sync and async tool execution
+* Automatic tool registration through Spring beans
+* Change notification support
+* Tools are automatically converted to sync/async registrations based on server type
+
+=== 2. Resource Management
+* Static and dynamic resource registration
+* Optional change notifications
+* Support for resource templates
+* Automatic conversion between sync/async resource registrations
+
+=== 3. Prompt Templates
+* Configurable prompt registration
+* Change notification support
+* Template versioning
+* Automatic conversion between sync/async prompt registrations
+
+=== 4. Root Change Consumers
+* Support for monitoring root changes
+* Automatic conversion to async consumers for reactive applications
+* Optional registration through Spring beans
+
+== Usage Examples
+
+=== 1. Standard STDIO Server Configuration
+[source,yaml]
+----
+# Using spring-ai-mcp-server-spring-boot-starter
+spring:
+  ai:
+    mcp:
+      server:
+        name: stdio-mcp-server
+        version: 1.0.0
+        type: SYNC
+        stdio: true
+----
+
+=== 2. WebMVC Server Configuration
+[source,yaml]
+----
+# Using spring-ai-mcp-server-webmvc-spring-boot-starter
+spring:
+  ai:
+    mcp:
+      server:
+        name: webmvc-mcp-server
+        version: 1.0.0
+        type: SYNC
+        stdio: false
+        sse-message-endpoint: /mcp/messages
+----
+
+=== 3. WebFlux Server Configuration
+[source,yaml]
+----
+# Using spring-ai-mcp-server-webflux-spring-boot-starter
+spring:
+  ai:
+    mcp:
+      server:
+        name: webflux-mcp-server
+        version: 1.0.0
+        type: ASYNC  # Recommended for reactive applications
+        stdio: false
+        sse-message-endpoint: /mcp/messages
+----
+
+=== Tool Registration Examples
+
+==== 1. Synchronous Tool (for SYNC server type)
+[source,java]
+----
+@Configuration
+public class SyncToolConfig {
+    
+    @Bean
+    public ToolCallback syncTool() {
+        return new ToolCallback() {
+            @Override
+            public String getName() {
+                return "syncTool";
+            }
+            
+            @Override
+            public Object execute(Map params) {
+                // Synchronous implementation
+                return result;
+            }
+        };
+    }
+}
+----
+
+==== 2. Asynchronous Tool (for ASYNC server type)
+[source,java]
+----
+@Configuration
+public class AsyncToolConfig {
+    
+    @Bean
+    public ToolCallback asyncTool() {
+        return new ToolCallback() {
+            @Override
+            public String getName() {
+                return "asyncTool";
+            }
+            
+            @Override
+            public Object execute(Map params) {
+                // Asynchronous implementation using Project Reactor
+                return Mono.just("result")
+                    .map(r -> processResult(r))
+                    .subscribeOn(Schedulers.boundedElastic());
+            }
+        };
+    }
+}
+----
+
+== Auto-configuration Classes
+
+The starter provides several auto-configuration classes:
+
+1. `MpcServerAutoConfiguration`: Core server configuration
+* Configures basic server components
+* Handles tool, resource, and prompt registrations
+* Manages server capabilities and change notifications
+* Provides both sync and async server implementations
+
+2. `MpcWebMvcServerAutoConfiguration`: Spring MVC transport
+* Configures SSE endpoints for web transport
+* Integrates with Spring MVC infrastructure
+
+3. `MpcWebFluxServerAutoConfiguration`: Spring WebFlux transport
+* Configures reactive SSE endpoints
+* Integrates with Spring WebFlux infrastructure
+
+These classes are conditionally enabled based on the classpath and configuration properties.
+
+== Conditional Configuration
+
+The auto-configuration is activated when:
+
+* Required MCP classes are on the classpath
+* `spring.ai.mcp.server.enabled=true` (default)
+* Appropriate transport dependencies are available
+
+== Best Practices
+
+1. Choose the appropriate server type based on your use case:
+* Use SYNC for simple request-response patterns
+* Use ASYNC for non-blocking operations and reactive applications
+
+2. Select the transport mechanism based on your application type:
+* Use STDIO for command-line tools and testing
+* Use WebMvc for traditional web applications
+* Use WebFlux for reactive applications
+
+3. Configure change notifications based on your needs:
+* Enable only the notifications you need
+* Consider performance implications of notifications
+* Use appropriate consumers for root changes
+
+4. Properly version your server and tools:
+* Use semantic versioning
+* Document version changes
+* Handle version compatibility
+
+5. Tool Implementation:
+* Implement tools as Spring beans for automatic registration
+* Return Mono/Flux for async operations in ASYNC mode
+* Use appropriate error handling strategies
+
+== Additional Resources
+
+* link:https://docs.spring.io/spring-ai/reference/[Spring AI Documentation]
+* link:https://modelcontextprotocol.github.io/specification/[Model Context Protocol Specification]
+* link:https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.developing-auto-configuration[Spring Boot Auto-configuration]
diff --git a/mcp-server-boot-starter-docs.md b/mcp-server-boot-starter-docs.md
new file mode 100644
index 000000000..393c6935e
--- /dev/null
+++ b/mcp-server-boot-starter-docs.md
@@ -0,0 +1,294 @@
+# Spring AI MCP Server Boot Starter
+
+The Spring AI MCP (Model Context Protocol) Server Boot Starter provides auto-configuration for setting up an MCP server in Spring Boot applications. It enables seamless integration of MCP server capabilities with Spring Boot's auto-configuration system.
+
+## Overview
+
+The MCP Server Boot Starter offers:
+- Automatic configuration of MCP server components
+- Support for both synchronous and asynchronous operation modes
+- Multiple transport layer options
+- Flexible tool, resource, and prompt registration
+- Change notification capabilities
+
+## Starter Dependencies
+
+Choose one of the following starters based on your transport requirements:
+
+### 1. Standard MCP Server
+
+Full MCP Server features support with `STDIO` server transport.
+
+```xml
+
+    org.springframework.ai
+    spring-ai-mcp-server-spring-boot-starter
+    ${spring-ai.version}
+
+```
+
+### 2. WebMVC Server
+
+Full MCP Server features support with `SSE` (Server-Sent Events) server transport based on Spring MVC and an optional `STDIO` transport.
+
+```xml
+
+    org.springframework.ai
+    spring-ai-mcp-server-webmvc-spring-boot-starter
+    ${spring-ai.version}
+
+```
+This starter includes:
+- spring-boot-starter-web
+- mcp-spring-webmvc
+(optionally allows `stdio` transport deployment)
+
+### 3. WebFlux Server 
+
+Full MCP Server features support with `SSE` (Server-Sent Events) server transport based on Spring WebFlux and an optional `STDIO` transport.
+
+```xml
+
+    org.springframework.ai
+    spring-ai-mcp-server-webflux-spring-boot-starter
+    ${spring-ai.version}
+
+```
+This starter includes:
+- spring-boot-starter-webflux
+- mcp-spring-webflux
+(optionally allows `stdio` transport deployment)
+
+## Configuration Properties
+
+All properties are prefixed with `spring.ai.mcp.server`:
+
+| Property | Description | Default |
+|----------|-------------|---------|
+| `enabled` | Enable/disable the MCP server | `true` |
+| `stdio` | Enable/disable stdio transport | `false` |
+| `name` | Server name for identification | `mcp-server` |
+| `version` | Server version | `1.0.0` |
+| `type` | Server type (SYNC/ASYNC) | `SYNC` |
+| `resource-change-notification` | Enable resource change notifications | `true` |
+| `tool-change-notification` | Enable tool change notifications | `true` |
+| `prompt-change-notification` | Enable prompt change notifications | `true` |
+| `sse-message-endpoint` | SSE endpoint path for web transport | `/mcp/message` |
+
+## Server Types
+
+### Synchronous Server
+- Default server type
+- Uses `McpSyncServer`
+- Suitable for straightforward request-response patterns
+- Configure with `spring.ai.mcp.server.type=SYNC`
+- Automatically configures synchronous tool registrations
+
+### Asynchronous Server
+- Uses `McpAsyncServer`
+- Suitable for non-blocking operations
+- Configure with `spring.ai.mcp.server.type=ASYNC`
+- Automatically configures asynchronous tool registrations with Project Reactor support
+
+## Transport Options
+
+The MCP Server supports three transport mechanisms, each with its dedicated starter:
+
+### 1. Standard Input/Output (STDIO)
+- Use `spring-ai-mcp-server-spring-boot-starter`
+- Default transport when using the standard starter
+- Suitable for command-line tools and testing
+- No additional web dependencies required
+
+### 2. Spring MVC (Server-Sent Events)
+- Use `spring-ai-mcp-server-webmvc-spring-boot-starter`
+- Provides HTTP-based transport using Spring MVC
+- Uses `WebMvcSseServerTransport`
+- Automatically configures SSE endpoints
+- Ideal for traditional web applications
+- Optionally you can deploy `STDIO` transport by setting the `spring.ai.mcp.server.stdio=true` property.
+
+### 3. Spring WebFlux (Reactive SSE)
+- Use `spring-ai-mcp-server-webflux-spring-boot-starter`
+- Provides reactive transport using Spring WebFlux
+- Uses `WebFluxSseServerTransport`
+- Automatically configures reactive SSE endpoints
+- Ideal for reactive applications with non-blocking requirements
+- Optionally you can deploy `STDIO` transport by setting the `spring.ai.mcp.server.stdio=true` property.
+
+## Features and Capabilities
+
+### 1. Tools Registration
+- Support for both sync and async tool execution
+- Automatic tool registration through Spring beans
+- Change notification support
+- Tools are automatically converted to sync/async registrations based on server type
+
+### 2. Resource Management
+- Static and dynamic resource registration
+- Optional change notifications
+- Support for resource templates
+- Automatic conversion between sync/async resource registrations
+
+### 3. Prompt Templates
+- Configurable prompt registration
+- Change notification support
+- Template versioning
+- Automatic conversion between sync/async prompt registrations
+
+### 4. Root Change Consumers
+- Support for monitoring root changes
+- Automatic conversion to async consumers for reactive applications
+- Optional registration through Spring beans
+
+## Usage Examples
+
+### 1. Standard STDIO Server Configuration
+```yaml
+# Using spring-ai-mcp-server-spring-boot-starter
+spring:
+  ai:
+    mcp:
+      server:
+        name: stdio-mcp-server
+        version: 1.0.0
+        type: SYNC
+        stdio: true
+```
+
+### 2. WebMVC Server Configuration
+```yaml
+# Using spring-ai-mcp-server-webmvc-spring-boot-starter
+spring:
+  ai:
+    mcp:
+      server:
+        name: webmvc-mcp-server
+        version: 1.0.0
+        type: SYNC
+        stdio: false
+        sse-message-endpoint: /mcp/messages
+```
+
+### 3. WebFlux Server Configuration
+```yaml
+# Using spring-ai-mcp-server-webflux-spring-boot-starter
+spring:
+  ai:
+    mcp:
+      server:
+        name: webflux-mcp-server
+        version: 1.0.0
+        type: ASYNC  # Recommended for reactive applications
+        stdio: false
+        sse-message-endpoint: /mcp/messages
+```
+
+### Tool Registration Examples
+
+#### 1. Synchronous Tool (for SYNC server type)
+```java
+@Configuration
+public class SyncToolConfig {
+    
+    @Bean
+    public ToolCallback syncTool() {
+        return new ToolCallback() {
+            @Override
+            public String getName() {
+                return "syncTool";
+            }
+            
+            @Override
+            public Object execute(Map params) {
+                // Synchronous implementation
+                return result;
+            }
+        };
+    }
+}
+```
+
+#### 2. Asynchronous Tool (for ASYNC server type)
+```java
+@Configuration
+public class AsyncToolConfig {
+    
+    @Bean
+    public ToolCallback asyncTool() {
+        return new ToolCallback() {
+            @Override
+            public String getName() {
+                return "asyncTool";
+            }
+            
+            @Override
+            public Object execute(Map params) {
+                // Asynchronous implementation using Project Reactor
+                return Mono.just("result")
+                    .map(r -> processResult(r))
+                    .subscribeOn(Schedulers.boundedElastic());
+            }
+        };
+    }
+}
+```
+
+## Auto-configuration Classes
+
+The starter provides several auto-configuration classes:
+
+1. `MpcServerAutoConfiguration`: Core server configuration
+   - Configures basic server components
+   - Handles tool, resource, and prompt registrations
+   - Manages server capabilities and change notifications
+   - Provides both sync and async server implementations
+
+2. `MpcWebMvcServerAutoConfiguration`: Spring MVC transport
+   - Configures SSE endpoints for web transport
+   - Integrates with Spring MVC infrastructure
+
+3. `MpcWebFluxServerAutoConfiguration`: Spring WebFlux transport
+   - Configures reactive SSE endpoints
+   - Integrates with Spring WebFlux infrastructure
+
+These classes are conditionally enabled based on the classpath and configuration properties.
+
+## Conditional Configuration
+
+The auto-configuration is activated when:
+- Required MCP classes are on the classpath
+- `spring.ai.mcp.server.enabled=true` (default)
+- Appropriate transport dependencies are available
+
+## Best Practices
+
+1. Choose the appropriate server type based on your use case:
+   - Use SYNC for simple request-response patterns
+   - Use ASYNC for non-blocking operations and reactive applications
+
+2. Select the transport mechanism based on your application type:
+   - Use STDIO for command-line tools and testing
+   - Use WebMvc for traditional web applications
+   - Use WebFlux for reactive applications
+
+3. Configure change notifications based on your needs:
+   - Enable only the notifications you need
+   - Consider performance implications of notifications
+   - Use appropriate consumers for root changes
+
+4. Properly version your server and tools:
+   - Use semantic versioning
+   - Document version changes
+   - Handle version compatibility
+
+5. Tool Implementation:
+   - Implement tools as Spring beans for automatic registration
+   - Return Mono/Flux for async operations in ASYNC mode
+   - Use appropriate error handling strategies
+
+## Additional Resources
+
+- [Spring AI Documentation](https://docs.spring.io/spring-ai/reference/)
+- [Model Context Protocol Specification](https://modelcontextprotocol.github.io/specification/)
+- [Spring Boot Auto-configuration](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.developing-auto-configuration)
diff --git a/mcp/common/src/main/java/org/springframework/ai/mcp/AsyncMcpToolCallback.java b/mcp/common/src/main/java/org/springframework/ai/mcp/AsyncMcpToolCallback.java
new file mode 100644
index 000000000..121e1d9b8
--- /dev/null
+++ b/mcp/common/src/main/java/org/springframework/ai/mcp/AsyncMcpToolCallback.java
@@ -0,0 +1,113 @@
+/*
+ * 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.util.Map;
+
+import io.modelcontextprotocol.client.McpAsyncClient;
+import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
+import io.modelcontextprotocol.spec.McpSchema.Tool;
+
+import org.springframework.ai.model.ModelOptionsUtils;
+import org.springframework.ai.tool.ToolCallback;
+import org.springframework.ai.tool.definition.ToolDefinition;
+
+/**
+ * Implementation of {@link ToolCallback} that adapts MCP tools to Spring AI's tool
+ * interface with asynchronous execution support.
+ * 

+ * This class acts as a bridge between the Model Context Protocol (MCP) and Spring AI's + * tool system, allowing MCP tools to be used seamlessly within Spring AI applications. + * It: + *

    + *
  • Converts MCP tool definitions to Spring AI tool definitions
  • + *
  • Handles the asynchronous execution of tool calls through the MCP client
  • + *
  • Manages JSON serialization/deserialization of tool inputs and outputs
  • + *
+ *

+ * Example usage:

{@code
+ * McpAsyncClient mcpClient = // obtain MCP client
+ * Tool mcpTool = // obtain MCP tool definition
+ * ToolCallback callback = new AsyncMcpToolCallback(mcpClient, mcpTool);
+ *
+ * // Use the tool through Spring AI's interfaces
+ * ToolDefinition definition = callback.getToolDefinition();
+ * String result = callback.call("{\"param\": \"value\"}");
+ * }
+ * + * @author Christian Tzolov + * @see ToolCallback + * @see McpAsyncClient + * @see Tool + */ +public class AsyncMcpToolCallback implements ToolCallback { + + private final McpAsyncClient asyncMcpClient; + + private final Tool tool; + + /** + * Creates a new {@code AsyncMcpToolCallback} instance. + * @param mcpClient the MCP client to use for tool execution + * @param tool the MCP tool definition to adapt + */ + public AsyncMcpToolCallback(McpAsyncClient mcpClient, Tool tool) { + this.asyncMcpClient = mcpClient; + this.tool = tool; + } + + /** + * Returns a Spring AI tool definition adapted from the MCP tool. + *

+ * The tool definition includes: + *

    + *
  • The tool's name from the MCP definition
  • + *
  • The tool's description from the MCP definition
  • + *
  • The input schema converted to JSON format
  • + *
+ * @return the Spring AI tool definition + */ + @Override + public ToolDefinition getToolDefinition() { + return ToolDefinition.builder() + .name(this.tool.name()) + .description(this.tool.description()) + .inputSchema(ModelOptionsUtils.toJsonString(this.tool.inputSchema())) + .build(); + } + + /** + * Executes the tool with the provided input asynchronously. + *

+ * This method: + *

    + *
  1. Converts the JSON input string to a map of arguments
  2. + *
  3. Calls the tool through the MCP client asynchronously
  4. + *
  5. Converts the tool's response content to a JSON string
  6. + *
+ * @param functionInput the tool input as a JSON string + * @return the tool's response as a JSON string + */ + @Override + public String call(String functionInput) { + Map arguments = ModelOptionsUtils.jsonToMap(functionInput); + return this.asyncMcpClient.callTool(new CallToolRequest(this.getToolDefinition().name(), arguments)) + .map(response -> ModelOptionsUtils.toJsonString(response.content())) + .block(); + } + +} 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 new file mode 100644 index 000000000..f8f1bb1eb --- /dev/null +++ b/mcp/common/src/main/java/org/springframework/ai/mcp/AsyncMcpToolCallbackProvider.java @@ -0,0 +1,132 @@ +/* + * 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.util.List; + +import io.modelcontextprotocol.client.McpAsyncClient; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.ai.tool.ToolCallback; +import org.springframework.ai.tool.ToolCallbackProvider; +import org.springframework.ai.tool.util.ToolUtils; +import org.springframework.util.CollectionUtils; + +/** + * Implementation of {@link ToolCallbackProvider} that discovers and provides MCP tools + * asynchronously. + *

+ * This class acts as a tool provider for Spring AI, automatically discovering tools from + * an MCP server and making them available as Spring AI tools. It: + *

    + *
  • Connects to an MCP server through an async client
  • + *
  • Lists and retrieves available tools from the server
  • + *
  • Creates {@link AsyncMcpToolCallback} instances for each discovered tool
  • + *
  • Validates tool names to prevent duplicates
  • + *
+ *

+ * Example usage:

{@code
+ * McpAsyncClient mcpClient = // obtain MCP client
+ * ToolCallbackProvider provider = new AsyncMcpToolCallbackProvider(mcpClient);
+ *
+ * // Get all available tools
+ * ToolCallback[] tools = provider.getToolCallbacks();
+ * }
+ * + * @author Christian Tzolov + * @since 1.0.0 + * @see ToolCallbackProvider + * @see AsyncMcpToolCallback + * @see McpAsyncClient + */ +public class AsyncMcpToolCallbackProvider implements ToolCallbackProvider { + + private final McpAsyncClient mcpClient; + + /** + * Creates a new {@code AsyncMcpToolCallbackProvider} instance. + * @param mcpClient the MCP client to use for discovering tools + */ + public AsyncMcpToolCallbackProvider(McpAsyncClient mcpClient) { + this.mcpClient = mcpClient; + } + + /** + * Discovers and returns all available tools from the MCP server asynchronously. + *

+ * This method: + *

    + *
  1. Retrieves the list of tools from the MCP server
  2. + *
  3. Creates a {@link AsyncMcpToolCallback} for each tool
  4. + *
  5. Validates that there are no duplicate tool names
  6. + *
+ * @return an array of tool callbacks, one for each discovered tool + * @throws IllegalStateException if duplicate tool names are found + */ + @Override + public ToolCallback[] getToolCallbacks() { + var toolCallbacks = this.mcpClient.listTools() + .map(response -> response.tools() + .stream() + .map(tool -> new AsyncMcpToolCallback(this.mcpClient, tool)) + .toArray(ToolCallback[]::new)) + .block(); + + validateToolCallbacks(toolCallbacks); + + return toolCallbacks; + } + + /** + * Validates that there are no duplicate tool names in the provided callbacks. + *

+ * This method ensures that each tool has a unique name, which is required for proper + * tool resolution and execution. + * @param toolCallbacks the tool callbacks to validate + * @throws IllegalStateException if duplicate tool names are found + */ + private void validateToolCallbacks(ToolCallback[] toolCallbacks) { + List duplicateToolNames = ToolUtils.getDuplicateToolNames(toolCallbacks); + if (!duplicateToolNames.isEmpty()) { + throw new IllegalStateException( + "Multiple tools with the same name (%s)".formatted(String.join(", ", duplicateToolNames))); + } + } + + /** + * Creates a reactive stream of tool callbacks from multiple MCP clients. + *

+ * This utility method: + *

    + *
  1. Takes a list of MCP clients
  2. + *
  3. Creates a provider for each client
  4. + *
  5. Retrieves and flattens all tool callbacks into a single stream
  6. + *
+ * @param mcpClients the list of MCP clients to create callbacks from + * @return a Flux of tool callbacks from all provided clients + */ + public static Flux asyncToolCallbacks(List mcpClients) { + if (CollectionUtils.isEmpty(mcpClients)) { + return Flux.empty(); + } + + return Flux.fromIterable(mcpClients) + .flatMap(mcpClient -> Mono.just(new AsyncMcpToolCallbackProvider(mcpClient).getToolCallbacks())) + .flatMap(callbacks -> Flux.fromArray(callbacks)); + } + +} diff --git a/mcp/common/src/main/java/org/springframework/ai/mcp/McpSyncClientCustomizer.java b/mcp/common/src/main/java/org/springframework/ai/mcp/McpSyncClientCustomizer.java deleted file mode 100644 index 0e1a7ae24..000000000 --- a/mcp/common/src/main/java/org/springframework/ai/mcp/McpSyncClientCustomizer.java +++ /dev/null @@ -1,28 +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 io.modelcontextprotocol.client.McpClient; - -/** - * @author Christian Tzolov - * @since 1.0.0 - */ -public interface McpSyncClientCustomizer { - - void customize(String name, McpClient.SyncSpec sync); - -} 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 821ff153e..5a69b92b8 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 @@ package org.springframework.ai.mcp; import java.util.List; +import io.modelcontextprotocol.client.McpAsyncClient; import io.modelcontextprotocol.client.McpSyncClient; import io.modelcontextprotocol.server.McpServerFeatures; import io.modelcontextprotocol.server.McpServerFeatures.AsyncToolRegistration; @@ -180,11 +181,32 @@ public final class McpToolUtils { .subscribeOn(Schedulers.boundedElastic())); } - public static List getToolCallbacks(McpSyncClient... mcpClients) { - return getToolCallbacks(List.of(mcpClients)); + /** + * Convenience method to get tool callbacks from multiple synchronous MCP clients. + *

+ * This is a varargs wrapper around {@link #getToolCallbacksFromSyncClients(List)} for + * easier usage when working with individual clients. + * @param mcpClients the synchronous MCP clients to get callbacks from + * @return a list of tool callbacks from all provided clients + * @see #getToolCallbacksFromSyncClients(List) + */ + public static List getToolCallbacksFromSyncClients(McpSyncClient... mcpClients) { + return getToolCallbacksFromSyncClients(List.of(mcpClients)); } - public static List getToolCallbacks(List mcpClients) { + /** + * Gets tool callbacks from a list of synchronous MCP clients. + *

+ * This method: + *

    + *
  1. Takes a list of synchronous MCP clients
  2. + *
  3. Creates a provider for each client
  4. + *
  5. Retrieves and combines all tool callbacks into a single list
  6. + *
+ * @param mcpClients the list of synchronous MCP clients to get callbacks from + * @return a list of tool callbacks from all provided clients + */ + public static List getToolCallbacksFromSyncClients(List mcpClients) { if (CollectionUtils.isEmpty(mcpClients)) { return List.of(); @@ -195,4 +217,40 @@ public final class McpToolUtils { .toList(); } + /** + * Convenience method to get tool callbacks from multiple asynchronous MCP clients. + *

+ * This is a varargs wrapper around {@link #getToolCallbacksFromAsyncClinents(List)} + * for easier usage when working with individual clients. + * @param asynMcpClients the asynchronous MCP clients to get callbacks from + * @return a list of tool callbacks from all provided clients + * @see #getToolCallbacksFromAsyncClinents(List) + */ + public static List getToolCallbacksFromAsyncClients(McpAsyncClient... asynMcpClients) { + return getToolCallbacksFromAsyncClinents(List.of(asynMcpClients)); + } + + /** + * Gets tool callbacks from a list of asynchronous MCP clients. + *

+ * This method: + *

    + *
  1. Takes a list of asynchronous MCP clients
  2. + *
  3. Creates a provider for each client
  4. + *
  5. Retrieves and combines all tool callbacks into a single list
  6. + *
+ * @param asynMcpClients the list of asynchronous MCP clients to get callbacks from + * @return a list of tool callbacks from all provided clients + */ + public static List getToolCallbacksFromAsyncClinents(List asynMcpClients) { + + if (CollectionUtils.isEmpty(asynMcpClients)) { + return List.of(); + } + return asynMcpClients.stream() + .map(mcpClient -> List.of((new AsyncMcpToolCallbackProvider(mcpClient).getToolCallbacks()))) + .flatMap(List::stream) + .toList(); + } + } diff --git a/mcp/common/src/main/java/org/springframework/ai/mcp/McpToolCallback.java b/mcp/common/src/main/java/org/springframework/ai/mcp/SyncMcpToolCallback.java similarity index 95% rename from mcp/common/src/main/java/org/springframework/ai/mcp/McpToolCallback.java rename to mcp/common/src/main/java/org/springframework/ai/mcp/SyncMcpToolCallback.java index b4e3ada13..ffbe09303 100644 --- a/mcp/common/src/main/java/org/springframework/ai/mcp/McpToolCallback.java +++ b/mcp/common/src/main/java/org/springframework/ai/mcp/SyncMcpToolCallback.java @@ -55,19 +55,18 @@ import org.springframework.ai.tool.definition.ToolDefinition; * @see McpSyncClient * @see Tool */ -public class McpToolCallback implements ToolCallback { +public class SyncMcpToolCallback implements ToolCallback { private final McpSyncClient mcpClient; private final Tool tool; /** - * Creates a new {@code McpToolCallback} instance. + * Creates a new {@code SyncMcpToolCallback} instance. * @param mcpClient the MCP client to use for tool execution * @param tool the MCP tool definition to adapt */ - - public McpToolCallback(McpSyncClient mcpClient, Tool tool) { + public SyncMcpToolCallback(McpSyncClient mcpClient, Tool tool) { this.mcpClient = mcpClient; this.tool = tool; } 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 3051ad7f0..ea18baf42 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 @@ -32,13 +32,13 @@ import org.springframework.util.CollectionUtils; *
    *
  • Connects to an MCP server through a sync client
  • *
  • Lists and retrieves available tools from the server
  • - *
  • Creates {@link McpToolCallback} instances for each discovered tool
  • + *
  • Creates {@link SyncMcpToolCallback} instances for each discovered tool
  • *
  • Validates tool names to prevent duplicates
  • *
*

* Example usage:

{@code
  * McpSyncClient mcpClient = // obtain MCP client
- * ToolCallbackProvider provider = new McpToolCallbackProvider(mcpClient);
+ * ToolCallbackProvider provider = new SyncMcpToolCallbackProvider(mcpClient);
  *
  * // Get all available tools
  * ToolCallback[] tools = provider.getToolCallbacks();
@@ -47,7 +47,7 @@ import org.springframework.util.CollectionUtils;
  * @author Christian Tzolov
  * @since 1.0.0
  * @see ToolCallbackProvider
- * @see McpToolCallback
+ * @see SyncMcpToolCallback
  * @see McpSyncClient
  */
 
@@ -56,7 +56,7 @@ public class SyncMcpToolCallbackProvider implements ToolCallbackProvider {
 	private final McpSyncClient mcpClient;
 
 	/**
-	 * Creates a new {@code McpToolCallbackProvider} instance.
+	 * Creates a new {@code SyncMcpToolCallbackProvider} instance.
 	 * @param mcpClient the MCP client to use for discovering tools
 	 */
 	public SyncMcpToolCallbackProvider(McpSyncClient mcpClient) {
@@ -69,7 +69,7 @@ public class SyncMcpToolCallbackProvider implements ToolCallbackProvider {
 	 * This method:
 	 * 
    *
  1. Retrieves the list of tools from the MCP server
  2. - *
  3. Creates a {@link McpToolCallback} for each tool
  4. + *
  5. Creates a {@link SyncMcpToolCallback} for each tool
  6. *
  7. Validates that there are no duplicate tool names
  8. *
* @return an array of tool callbacks, one for each discovered tool @@ -81,7 +81,7 @@ public class SyncMcpToolCallbackProvider implements ToolCallbackProvider { var toolCallbacks = this.mcpClient.listTools() .tools() .stream() - .map(tool -> new McpToolCallback(this.mcpClient, tool)) + .map(tool -> new SyncMcpToolCallback(this.mcpClient, tool)) .toArray(ToolCallback[]::new); validateToolCallbacks(toolCallbacks); @@ -106,6 +106,18 @@ public class SyncMcpToolCallbackProvider implements ToolCallbackProvider { } } + /** + * Creates a list of tool callbacks from multiple MCP clients. + *

+ * This utility method: + *

    + *
  1. Takes a list of MCP clients
  2. + *
  3. Creates a provider for each client
  4. + *
  5. Retrieves and combines all tool callbacks into a single list
  6. + *
+ * @param mcpClients the list of MCP clients to create callbacks from + * @return a list of tool callbacks from all provided clients + */ public static List syncToolCallbacks(List mcpClients) { if (CollectionUtils.isEmpty(mcpClients)) { diff --git a/mcp/common/src/main/java/org/springframework/ai/mcp/McpHints.java b/mcp/common/src/main/java/org/springframework/ai/mcp/aot/McpHints.java similarity index 54% rename from mcp/common/src/main/java/org/springframework/ai/mcp/McpHints.java rename to mcp/common/src/main/java/org/springframework/ai/mcp/aot/McpHints.java index cc6b617b3..fcf8c81d2 100644 --- a/mcp/common/src/main/java/org/springframework/ai/mcp/McpHints.java +++ b/mcp/common/src/main/java/org/springframework/ai/mcp/aot/McpHints.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.ai.mcp; +package org.springframework.ai.mcp.aot; import java.util.ArrayList; import java.util.Arrays; @@ -30,12 +30,36 @@ import org.springframework.aot.hint.TypeReference; import org.springframework.lang.Nullable; /** + * Runtime hints registrar for Model Context Protocol (MCP) schema classes. + *

+ * This class provides GraalVM native image hints for MCP schema classes to ensure proper + * reflection access in native images. It: + *

    + *
  • Registers all nested classes of {@link McpSchema} for reflection
  • + *
  • Enables all member categories (fields, methods, etc.) for registered types
  • + *
  • Ensures proper serialization/deserialization in native images
  • + *
+ * * @author Josh Long * @since 1.0.0 + * @see RuntimeHintsRegistrar + * @see McpSchema */ @SuppressWarnings("unused") public class McpHints implements RuntimeHintsRegistrar { + /** + * Registers runtime hints for MCP schema classes. + *

+ * This method: + *

    + *
  1. Discovers all nested classes within {@link McpSchema}
  2. + *
  3. Registers each discovered class for reflection access
  4. + *
  5. Enables all member categories for complete reflection support
  6. + *
+ * @param hints the hints instance to register hints with + * @param classLoader the classloader to use (may be null) + */ @Override public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { var mcs = MemberCategory.values(); @@ -45,12 +69,32 @@ public class McpHints implements RuntimeHintsRegistrar { } } + /** + * Discovers all inner classes of a given class. + *

+ * This method recursively finds all nested classes (both declared and inherited) of + * the provided class and converts them to type references. + * @param clazz the class to find inner classes for + * @return a set of type references for all discovered inner classes + */ private Set innerClasses(Class clazz) { var indent = new HashSet(); this.findNestedClasses(clazz, indent); return indent.stream().map(TypeReference::of).collect(Collectors.toSet()); } + /** + * Recursively finds all nested classes of a given class. + *

+ * This method: + *

    + *
  1. Collects both declared and inherited nested classes
  2. + *
  3. Recursively processes each nested class
  4. + *
  5. Adds the class names to the provided set
  6. + *
+ * @param clazz the class to find nested classes for + * @param indent the set to collect class names in + */ private void findNestedClasses(Class clazz, Set indent) { var classes = new ArrayList>(); classes.addAll(Arrays.asList(clazz.getDeclaredClasses())); diff --git a/mcp/common/src/main/java/org/springframework/ai/mcp/customizer/McpAsyncClientCustomizer.java b/mcp/common/src/main/java/org/springframework/ai/mcp/customizer/McpAsyncClientCustomizer.java new file mode 100644 index 000000000..b059bcbe3 --- /dev/null +++ b/mcp/common/src/main/java/org/springframework/ai/mcp/customizer/McpAsyncClientCustomizer.java @@ -0,0 +1,44 @@ +/* + * 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.customizer; + +import io.modelcontextprotocol.client.McpClient; + +/** + * Interface for customizing asynchronous MCP client configurations. + *

+ * This interface allows for customization of MCP client behavior through Spring's + * customizer pattern. Implementations can modify the client's configuration before it is + * used in the application. + *

+ * + * @author Christian Tzolov + * @since 1.0.0 + * @see io.modelcontextprotocol.client.McpClient.AsyncSpec + */ +public interface McpAsyncClientCustomizer { + + /** + * Customizes an asynchronous MCP client configuration. + *

+ * This method is called for each async MCP client being created, allowing for + * client-specific customizations based on the client's name and specification. + * @param name the name of the MCP client being customized + * @param spec the async specification to customize + */ + void customize(String name, McpClient.AsyncSpec spec); + +} diff --git a/mcp/common/src/main/java/org/springframework/ai/mcp/customizer/McpSyncClientCustomizer.java b/mcp/common/src/main/java/org/springframework/ai/mcp/customizer/McpSyncClientCustomizer.java new file mode 100644 index 000000000..98c0a1cdc --- /dev/null +++ b/mcp/common/src/main/java/org/springframework/ai/mcp/customizer/McpSyncClientCustomizer.java @@ -0,0 +1,44 @@ +/* + * 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.customizer; + +import io.modelcontextprotocol.client.McpClient; + +/** + * Interface for customizing synchronous MCP client configurations. + *

+ * This interface allows for customization of MCP client behavior through Spring's + * customizer pattern. Implementations can modify the client's configuration before it is + * used in the application. + *

+ * + * @author Christian Tzolov + * @since 1.0.0 + * @see io.modelcontextprotocol.client.McpClient.SyncSpec + */ +public interface McpSyncClientCustomizer { + + /** + * Customizes a synchronous MCP client configuration. + *

+ * This method is called for each sync MCP client being created, allowing for + * client-specific customizations based on the client's name and specification. + * @param name the name of the MCP client being customized + * @param spec the sync specification to customize + */ + void customize(String name, McpClient.SyncSpec spec); + +} diff --git a/mcp/common/src/main/java/org/springframework/ai/mcp/package-info.java b/mcp/common/src/main/java/org/springframework/ai/mcp/package-info.java index fdd7a9d99..df28217af 100644 --- a/mcp/common/src/main/java/org/springframework/ai/mcp/package-info.java +++ b/mcp/common/src/main/java/org/springframework/ai/mcp/package-info.java @@ -14,6 +14,26 @@ * limitations under the License. */ +/** + * Core support for Model Context Protocol (MCP) integration in Spring AI. + *

+ * This package provides the foundational classes and utilities for integrating MCP with + * Spring AI's tool system. It includes: + *

    + *
  • Tool callback implementations for both synchronous and asynchronous MCP + * operations
  • + *
  • Tool callback providers that discover and expose MCP tools
  • + *
  • Utility classes for converting between Spring AI and MCP tool representations
  • + *
  • Support for customizing MCP client behavior
  • + *
+ *

+ * The classes in this package enable seamless integration between Spring AI applications + * and MCP servers, allowing language models to discover and invoke tools through a + * standardized protocol. + * + * @author Christian Tzolov + * @since 1.0.0 + */ @NonNullApi @NonNullFields package org.springframework.ai.mcp; diff --git a/mcp/common/src/main/resources/META-INF/spring/aot.factories b/mcp/common/src/main/resources/META-INF/spring/aot.factories index dbf904981..b961ce5bc 100644 --- a/mcp/common/src/main/resources/META-INF/spring/aot.factories +++ b/mcp/common/src/main/resources/META-INF/spring/aot.factories @@ -1,2 +1,2 @@ org.springframework.aot.hint.RuntimeHintsRegistrar=\ - org.springframework.ai.mcp.McpHints \ No newline at end of file + org.springframework.ai.mcp.aot.McpHints \ No newline at end of file diff --git a/mcp/common/src/test/java/org/springframework/ai/mcp/McpToolCallbackProviderTests.java b/mcp/common/src/test/java/org/springframework/ai/mcp/SyncMcpToolCallbackProviderTests.java similarity index 98% rename from mcp/common/src/test/java/org/springframework/ai/mcp/McpToolCallbackProviderTests.java rename to mcp/common/src/test/java/org/springframework/ai/mcp/SyncMcpToolCallbackProviderTests.java index 0fc86888d..608ae9f20 100644 --- a/mcp/common/src/test/java/org/springframework/ai/mcp/McpToolCallbackProviderTests.java +++ b/mcp/common/src/test/java/org/springframework/ai/mcp/SyncMcpToolCallbackProviderTests.java @@ -33,7 +33,7 @@ import io.modelcontextprotocol.spec.McpSchema.ListToolsResult; import io.modelcontextprotocol.spec.McpSchema.Tool; @ExtendWith(MockitoExtension.class) -class McpToolCallbackProviderTests { +class SyncMcpToolCallbackProviderTests { @Mock private McpSyncClient mcpClient; diff --git a/mcp/common/src/test/java/org/springframework/ai/mcp/McpToolCallbackTests.java b/mcp/common/src/test/java/org/springframework/ai/mcp/SyncMcpToolCallbackTests.java similarity index 92% rename from mcp/common/src/test/java/org/springframework/ai/mcp/McpToolCallbackTests.java rename to mcp/common/src/test/java/org/springframework/ai/mcp/SyncMcpToolCallbackTests.java index d0eefaf75..713138525 100644 --- a/mcp/common/src/test/java/org/springframework/ai/mcp/McpToolCallbackTests.java +++ b/mcp/common/src/test/java/org/springframework/ai/mcp/SyncMcpToolCallbackTests.java @@ -31,7 +31,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) -class McpToolCallbackTests { +class SyncMcpToolCallbackTests { @Mock private McpSyncClient mcpClient; @@ -45,7 +45,7 @@ class McpToolCallbackTests { when(tool.name()).thenReturn("testTool"); when(tool.description()).thenReturn("Test tool description"); - McpToolCallback callback = new McpToolCallback(mcpClient, tool); + SyncMcpToolCallback callback = new SyncMcpToolCallback(mcpClient, tool); // Act var toolDefinition = callback.getToolDefinition(); @@ -62,7 +62,7 @@ class McpToolCallbackTests { CallToolResult callResult = mock(CallToolResult.class); when(mcpClient.callTool(any(CallToolRequest.class))).thenReturn(callResult); - McpToolCallback callback = new McpToolCallback(mcpClient, tool); + SyncMcpToolCallback callback = new SyncMcpToolCallback(mcpClient, tool); // Act String response = callback.call("{\"param\":\"value\"}"); diff --git a/pom.xml b/pom.xml index 2136b768b..f47ff2f57 100644 --- a/pom.xml +++ b/pom.xml @@ -34,7 +34,12 @@ spring-ai-bom spring-ai-core spring-ai-test + spring-ai-spring-boot-autoconfigure + + auto-configurations/spring-ai-mcp-client + auto-configurations/spring-ai-mcp-server + spring-ai-retry spring-ai-spring-boot-docker-compose spring-ai-spring-boot-testcontainers @@ -127,7 +132,11 @@ spring-ai-spring-boot-starters/spring-ai-starter-zhipuai spring-ai-spring-boot-starters/spring-ai-starter-moonshot - spring-ai-spring-boot-starters/spring-ai-starter-mcp + spring-ai-spring-boot-starters/spring-ai-starter-mcp-client + spring-ai-spring-boot-starters/spring-ai-starter-mcp-server + spring-ai-spring-boot-starters/spring-ai-starter-mcp-client-webflux + spring-ai-spring-boot-starters/spring-ai-starter-mcp-server-webflux + spring-ai-spring-boot-starters/spring-ai-starter-mcp-server-webmvc spring-ai-integration-tests @@ -890,6 +899,16 @@ lead + + tzolov + Christian Tzolov + christian tzolov at broadcom.com + Broadcom + http://www.spring.io + + lead + + diff --git a/spring-ai-bom/pom.xml b/spring-ai-bom/pom.xml index 5979f1508..093caee4d 100644 --- a/spring-ai-bom/pom.xml +++ b/spring-ai-bom/pom.xml @@ -310,6 +310,18 @@ ${project.version} + + org.springframework.ai + spring-ai-mcp-client-spring-boot-autoconfigure + ${project.version} + + + + org.springframework.ai + spring-ai-mcp-server-spring-boot-autoconfigure + ${project.version} + + org.springframework.ai @@ -589,10 +601,35 @@ org.springframework.ai - spring-ai-mcp-spring-boot-starter + spring-ai-mcp-client-spring-boot-starter ${project.version} + + org.springframework.ai + spring-ai-mcp-server-spring-boot-starter + ${project.version} + + + + org.springframework.ai + spring-ai-mcp-client-webflux-spring-boot-starter + ${project.version} + + + + org.springframework.ai + spring-ai-mcp-server-webflux-spring-boot-starter + ${project.version} + + + + org.springframework.ai + spring-ai-mcp-server-webmvc-spring-boot-starter + ${project.version} + + + diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/images/mcp/java-mcp-client-architecture.jpg b/spring-ai-docs/src/main/antora/modules/ROOT/images/mcp/java-mcp-client-architecture.jpg new file mode 100644 index 0000000000000000000000000000000000000000..688a2b4ad0f92090bf0033ab5eb3b20fce1a8484 GIT binary patch literal 676694 zcmeFZ2~=9=-Zx5`G-*a_osu}6YO0Be8iMoKHVzp;OAv9k(Kstca5ie&G;xYpn;5}~ zMk5F~)F>h%v5E6Y6mbGHjyT~wkC(jXeQ*8NUFUr3oVC8*bMMN-0v`4={r3Oad;e$l z`}W@hKEGvg(*khl5CCvU^aAXU9O}Dy{krRScdRXLg3bTl&;$^bBUb@{0Aw)gj^($1 zv$J>j+p+h5AMv5>t~>hvhwpz-MDwooe3%^oV5|RwKL70S@p~R1$Nr?r{ky2U|25qa(dQ?go&dlS5db)C4**EK0{~7q{#Tpm z;~!xAo2W@ngey?=@&fn+JOFT zLmx&SKJuAp;O8fgpA;Sa7t8)10P)Yng2Y&d4=DjY5kGWT{Lp?KK>owEcKFbT@;|5h z+0kRiKRt5zlMl@ypNpbo3Jmz#-9(PmdlG|Llt|C4k>NJ1!}8R!J8M zx4j!2`l`H>EpzUgwjRPRDCEbdWnJCU$~yYr-a$V~{Wa|c6=dUv`swHV(z*+l&vV~s zUAKU_Kl~~z?`^G$nHOPXY~iB$y_drto}y*tL})(wfa-rRiEe~XKRJB#(6P@%t>1}@ zK#7`85$9C-ALFTa5XrAkOjNnZnL-+|pl{rIBvO{bEUO>Wolg7UY& zK7I9eM8^&N)1u6Ud+cLhT{nBE;=VrsICWUGp7>#LfGJ=a`{$Y;Yac1_kpdqn@R0%^ zDe#d3|9_*v;RRMlZPx-y+t^ZN&_v@a^864J6#+x?f!c5z)=x@G9^0i>SGM5uno(OR z^~E|4xf)J{gi&nVQv-{VldLcQu&&ta-eXgjaw5)r5=~npu4R{`83Y?luD^MGVF4k7 znvYISAp3^+p0ia|V)Pb}y$J==b@ftpq3E1JCvHtgR3}Yn|8O+IRIjGt?zIQfO4Y_8 z&Nzan>J6fmn1iv@U@?}gH7tlaF#ogVru!1Y8meiz@Ii^#ff*uLkvhASs}BNI1>!5s z5y%zjvd4CyE_E(qOz-&G&;WiQ^fl}N=4w;@d5a;cr&#jX&al9ElDk&;X0aNdrSGETorutx_iI=c?u$M0Q<-IC5 z);HE?VAJ}*Gz&X__K(Y2&kGzVJJzribD#ayIHS|DZI_lgux_*@cVs(IXe~5!y>wt2 z7sUhpmvdGEwr#)P7_{yktz9r z#f@N=pbzZHfqhOJ-u!bz#SUy0;dX0SZVK+>*8cYu@^NcF-nIYzJp6dqez+7qK9D~? zkpIrIKk{0UF!YhveiWnrTQ2@6Mtzi!|E{ooRDM1xKOgnl|JU_e1&f8r)Xv_+-ZK$> z53aqX#wuDhGknMOBa9}CGvQ!xO#gdqTsidDUK^N1SX{eh8;&WL(=#%V+3FjZeXYpP z3wcW3>yu2XT~?N1XDH8!pyZiT9*T}9o953FG?iPsB$S7ax=s|5y5(5Mt&W{TpEjG3 zY^(JkudkTH8UR1F{r}CP_3xgKTbBI_Wzc1PRWZrQ!ylGpk`~oExRKKukd;ri6|U|BPI~TKgih7|CeybVx8bNe{B4Rgt6DX-3m} zJ)OC0Inpb;z~94!ua{Ej&od`KT{F77C*8jBT^>8`Yt7P8v4x7jw~rL?_;9OzK)%VE zv?0gnj}y;Dj`Y8(C>+@TxoY*z?Kqz6PpEwW2o`|Zwl*@#PS^)DEv@|AwEiZ2lcE*^ zj8Jg-1*foaZ6BcRVO6;P&G7BLXVkmfA8?1h|Eo~{hp%%eG03Z{BqD04J4QVy?Vvqe zcbKs&w>a;PK&$J%J2@L(yQ7}D@WPUK<_A~z2E}3jp+_f-O-&ZEMYmY=?3FNb#1B&H zzx{P*Yjp4e;D64FCH4WS<5R~|=O1b=+uBUKEg3Ptyk@-M zvFF>8Z9n($ZTjvd-`aH%Y)RoCe;w@q=#>sjjrVB+?h~sPXo5M#pqS&W zJMEAPL!r^aQmE=a;Gncne~@RC@sB;y+`7Yw`+)iq_lLg@^?&qAM=#amZ!yQ`W1$<= z2HS%yW9aex3c8V*?A%s9bbv8r861jEIUD<^EJpI;%Fg!Nu-!}Rzn2b@CIB`Se<4v1 z?7nr#a3G6kGO{C)zL}BfA2tv%JT_~+XPN#|#q#y3ns$2_Dd`};@Tw9!;kuGf5&Hld zM*EN6p1%;>sRQ!jlcIy{qUQ9_q#z~gJOk(R`T$eV3qB}FZm$14zXhAW>&}F=0Z0LV z9q>a3c0Xt4?GU~)x_(1db6H!bZ@zrFwz&_#b3j^z$sA-BtY8JnnyK5iiXT7|k$)ZN z|Lm2{9uOAKIUZydF|swCEQK}guxsq-11#bGK{0Z9{`k-eaqw+pBoTY)&cI)Hs)rA3 zzH#rP8q@pRNefo46TShZLq3AIr=_Nw2PMV-MYZJQOd4H^?Rs8l`T?~S@2^AspS=?B zK;*==ii6BzM>jSv$9UFv&V#UZfF%qclpzI!5;arS%tx42>#LvSm;ZIAdhEdNN0l7V zF1hZn*(@qCIHpIKKgCE?QGtUSVj5{{cbX8Q`uw;6?00?Gj z>;vv?rT7s1vW39HOodlF`YvhY3v+-PK!kkAKsGbu{X2-)2&)gd@npJp?IvzghP!u#s z+y>?h1<8%LBUgm{PDpMxNXKgjQ zhBE9Mu1hGOrNyP1&%oP3K!n;PE@2!9G$Ph~(ZP@jXrQuz3gJox*}rao?HdUIn5eru zbgr#shaq}b{f$9K8xd9Gp02s=nbE$v*cpUVJ+Q-5NuL$trceigy$n$Fu6_N4r?~Cs zKQH9DvLhXieXgT0(^g_Nu+9Ct=bCUJlhgM{cj1^5fd@A0)#|iqgGs=FEM|v0CxCJ~ zGJXhdR)@YVzhW&rbSVFqKd;%t>5gRt)0^_TA$|s-GG7!sHr97vyJDZ$R()x@6Vj54 z73f5;F2>In701h6AC$9x-=HxyWKcaFnIExftJah$Ed+O(LphkDDj(u2)$U;ZoQYC< z+w(vKnc#zqe~;^pi^F`W(4&$>4fJ_DbBJ}}zm`7j*L6J%DIfPnzLyb7c%f%dk)4E` z&@pq!6`;w1js!1?69m+6vp(YZ`@AYa&>) z6>F=)=iPH@?OZ@Dsm(YulYPfSQ1JpAwgjlfm|79^iyP`jZXgFdPh~^G%g5u#eahkq zdyGNVzE6^?|D?k|sC`s}{{dnC{lkBu8hkZJYf(b+k&!wtYH7BUAKYd@{33DNZ36zF zpcLFl(pAgDQ#&4<(Hk2Z^StNW+RAjES)U<~bJI8-tt$Xf#!Z@Y8J{V!POdxye*VS#f-EBp(HD{b+onVR< zV}3K3;~UmRmp?^E2SyoL&mL32er0_js>}5HdT;Wg5!E)$D0TkEuGo-^ZrxI^W7a}Z z%FR59LGrgoax474y-h7vvSX-61M*BHayIrshF3!omzVju7YqgZ8hSSNZf z#Ez`ZG{_+0f1^+Wm9VQys+AmZh4VAdBJyUliu~d-pGJ+C!!7zd*o7WY`N`6BnW?0w zdqu4}Q~Q7!7uunX6!arG2X53L5^kj-?V+=rtK?ObfKuVlu};-X!GVem@vDlK9ShGU zDrNh8^?Z|ao|(brfxzjskc!r9JJb#RCcOOLF|1e%5W(G{HSzQ2c}XFAOoBx|uQ&DlU~XJICVaMuvBJBJ1?a+t~GOF7YPU=Sb( z4l~MCGP*W8q_~mlDy_$$>lVrQmWZip`1>pxZCZR)Fc%u7%IwH+Bc+VOVI@3I*WQRY z1lc6HvbWbrd=Y;~L1$uVa7}Mu6&@jHl5>8D3C1Uns0w>up>zoS1CbRE;8s?Gw@fk= zSuoz+Fvqc{_U}C#D1IGt-Y@^_%|C~j{D|if=AbcT)rU7R37xKPS`4e=6u`z$3xD3* za8qf5R$W>qr%i{y~8lPysrPrKMwBx7$ zba92Wev5c)DBlvWY0&p!vZL8k(QCSaDM9@k^@I_h!So^b(2aF@{1XT3me)1zY~t37 z;`t~_h7&Gy2)QR%VTn3^K61TcJ8a#Q5QSm=5f{SP8x>eq<%T z9M?Al6<@sXSN-jb7d{Nfui2qJDPHvNee|B+FD=+prv#fzHm;bNBruGe_2lFWKgFC5 zUWHk)kl818q$HftsFI#y;CwQ%U=}i3BPQJ!+Ld{f#g z6YLgtikxfMk6n~p#?4zgbPvR*h@r_YnqQl!#MFk@yRbT%rhT>>Wm~cc5wLU5>Ah~G z?M2;yx^U023!W9C$7cJNg3FJ!Naxy!p7P2orqSU; z60L9_ShK=P%Ddgw(FfM*A#~P>zLgB1@$^gOH!KK0W&SC6gG2Ll@_a8K%r&kz6CU_- z%QVu%_4*6$>RoF>tXa&b-ks^Gd~J0)%dAac zT*mXkj3OvTvz)-J4d2Af_Vyv=(S+Dimn$B#;8_ge%zXW{L&Cg;M?9HH(HAH1&t^`Z((WK3RiFNF=<8oPcdvz~ujstDrMhTXqPuBg z>JlfPZC%XlW`&UTLmMw^@y1O~RK2EPcZ~ZpQa)dAhQu4l;b}(-hEx8ab!AwMMZ54&Ewv5P-K4!>iz2<-awlHNg2C*?Ff0n8MU*3weES zR9w=cY4`T|rLW72Oap_geFK-_5sHN-0H#`!25vd-5rdg_36Mtf_1~9EE$*G>SySgk|Ywqs58Og z$C_u+w5+}fY6%AJA23sRUZcEXekmMJ=coX`nec_7<(1|hmX zz|nI>llb|24|JFGX^&|SyZ$W88H*%AYxWEX z1Q*{+1HpmA*jYaXnMLb}`wv4#=XTRb9XjxmZz9_tOqZtl;SGwM^Rhw;a$LIWmP96r zgvXcB=R5=BvMgXWW}5_0T+Ype;e=gpSR%u-NZ1$Ze>Y2Mr~P zprF)y>7YVOSVC7~xG(aB4_0rI-S}>4>PDz6N?X+;EK!==Q z%Y6Xi4vbT7btyP_#KKs=2UW2)(OC>jONva)CHv(@)FI5F9hnu=7%E7<6xc&3*eWoi zTx?pNR(tZQXuN4;7?a_Z#F9N%KeMK~R$6Fe^<`g)Z0wxBA0F;!*PdHaBQq0cQFUlH z<*e%?bc~#SSrv6BxtG40%uY}7tl#xARsN3Q+Xi-|IdjtxTI>$b$Wm>vrG8H1%|8dJ z&b6py<;GKxTliq=`|Xv2_YVvvX!!w5S>5}7xQd09U4!}vdwUtxG#w#SLK|)x`Q=ii zPX_VEK*a+tF;tkHntKmv%rCw>W#D-Q&3Kv?dNEh^4HzvXljhAuCZs5(ufANAY5rU1 zebhCrJsJO`j4UV!!nA}lbOPY58#aA;ogDCn0jJZX@Y{P+Qe6`0AVHW@dkZs62Kk;} zoUIHNv}D?DE+wTasiq-(Q^=w`%uUOCCWD8A;Uk*zo6{No_MH_%%2VWA3C^uZ&obvL$uKJIyzw^Vg;I+-gsS3z5=kPth7SR*wsG{-bA!%a?izB-O zNc*me$J$z~0O^(OWvS>r>J)wwdoRkdS!Jk_4wdbV8#t4fY?QYTC`}K%%Oi4f!qga2 z(Q`Jt8~(#c2#?wCJDA@oPUh%c!7m{^;ZzS?lLM{ES68!q4KL*>T}qgHX1RDd#a!X#DoJ2$pKe+OXs#YC}o6|JiG2gCj z^$b>Pv$!xDzuaQ0v=8`>=%NpEoM~36r>3H19`lSSN*v>76Mp*gRWp zVX{E2RZ>Dif`~<9qEmaujS6-&mHi&AEskVc#vn7-H$!{fTh=hHs(+qvkHxLGo`2( zy)yb$J-vWOOR8B@$oW;T!c_@r@*Cs-rT;Ta?N zZ@liXr|44FBwclmwXJF2^Q1xN;|5;~Pk-B>o?YVQ($K^-PXuWt@dx?Z2pcm;!_B+a-iussj)^iW8_-7~FU-u!vL_O;2Wox|CSt=X+ghe@yqHd= zuT4L{wVA&`lkrV5m=DR;<`ShT1|ePA1Ay+8f1+f5w5|RV;{1C}f1Pc0!qi$pWv?Tg z9TxEYSQtdt%YNA)o=_az0e2!dk&SOARy`MRUrl;R(dJ}M>7gri=fdvzbS_YszKMco z&NU{F+h4~{Igt}%2ZBnV-Jp=@>zCo(ul$-?+nFvF=F@^~(Nn~R1BVIQ+FD=pG+a5F zh-9+fr7a-KFj9H&M+`k5Xcj~fQ!ab_Ixt8AK4Ke!31@i8wDhIH<)7kGCBSXgk&zX- zW8m18^48qrw{ugz2tWDO<%D;YbbkG5dNO5NJ^*1uvlTge^kwK^s7<(9F3&O7os;Vr z%hK)K7SbzrP*R3@Ph8xwJO+eJz-|FoyzWI-3hY-ahH1}*b$((p`SYh=Cp(xlFEu@f5=q!A*t z(vHFKfr!VVzedQ7uJY(csxj7tPH)^UVv|b?!Iol28pS9BNy&+xZdD$=F|zXkrt#Zi z-wrvr+FtRq(|J{uiZB^Rxu>I)lRFH1FaY^w4@w?_3*UsWi5)le_ zer?MDLI!+NMmobNh@sc7cxQrs$fC)jPuEMlX*UAST9={8P^sewHG6pe0o!qTa!0sr zl7i(uqh))vQ#eu2TrZ74L zzO>L@58StlHR?^kTFY}s2F<`2iVD+}|%d*^G64LlT&Id!4}Skv&@x;^ycP=){N2XbF#1-pg(iQIYV`}SV)YzuqHEd2_Z6lVf z3Ip#`?y2bkNKM5TT21#IBfTFzUG>GZE%(dg}-%=krmtanwtw=%?-RX8|D->Pt) zx!nCwipZu!$L5LZauDn z_o6SY!xZ)bZqi4SX6mob8!rhSKL@=0Fh0G*Zd6%zh=kC(zsIlcC7R#?iN* z=8n0;5RVPZ!!&|~d;R^w*NDeudP&{(DT7!HuG7b6-Uo*&NiLXW6>gJONzB3t zL`+hMwaKWb@o9?Fj!1s;Chn$Xk44;fJuLH0>!=GY4#L}+q21TAr<;N8O3hXSU2?rP z;JrOGR*g8aK+r<}`HNX=fR?PI-%p0JH>hS%-in+dP05hoGfqZ2;TxSpZcj&*)^!|K z&pYYjX^X254=U`l+LoxzPO!vqP;n=`ERd-KJk(CU0U5F;zg%?@Pm$^Oj zv%9#e5v9v-x?gE+<~Wzh2+f-W>(wQuY>kqjzUhVjG@XzwUF`$`+b$nhy+iq^2K}MgOA1Jfwh{RX?KYI%CXg)JeDALwtJOgS8CAHMRh596p6*qP`UbJ4mgqQ?3h4#7JDL5c5``JPj8EWPa}AA z_LQc3GGu&Ky^C=K_bnC2`+DRBy$qnF^=XjAa*ES^U~#;tC7fp(JoB4k-{PYUgzj)H z&K}!)_0{|^f4sL8MS-JMeR5#?098oj|E=_zgKhD3-mrH$+>T+T5 zU3hHX!q6vU?6BZ_oToUT>E7)WEsmu1Ru z3eJ3t_uFmXeGtnwIXH5RVc%Z;^R|gkc8Fhtw@G&O)i=ZYfOF)Y6@OoUpKY3KyNSTu zh?J+MmJwJNy_tx#riDHbD{0yX3`H1vB`;FdnlkQ(d7kF^J)ekx>CSe-XW%TFIAp+m z^JYZ0y1~r=mtV?-5nM(XjIH4f%H)&VGtqpd^`Pl`}>L?taj+9#7NiZWbUX) za|HTSW`yOh_wipFH+qdq<5mC1o97qCz;>6UCXq6&U++ZHmH}7kJ7ijn5Yplg<0VM z`O5g1%Lxk=qfZ{_juh_>tI%a1-bKnKgOmyq1GyM@e{-(skD+|0sbWK~bUlIGW?kJ{ zVaM4B<)~PbD^om&fv|GMa!UqPT{U|)0)HBAPmanL-z!3>4tRI0D?gz|GB)gnIzb@f zyv1vzDMAb`2aWNJ2vL_lF+-)3mXViC+JnLiJBa{dI|6rkUyL{WLh2$~KoY zm>~3a`)} z?w@;PR5wxrwqX5qWa+iY467tqz4t#A58gMTBLxy`u{C?@yD9sCu(IX*^DUc-J4tv> z`ZAnSZt1{)vlR;2SLZ-SM$^v(1i3zvO>u{tgHrKvaJvsyr6&xu%oUdDk+54nBF|D zD)6=b^BG;4VY?MyvxaqI&6sb@>^kv%bznz#%`al9r0udn{7;KDhIjUS`Eift(6HUB z>&FJ~9*?*nr#+|7Kqu)*>qAq)lU2!3Ii|d^A9mH60#QOm7T@gZmAJ*#VsQIU@s5A4kJ(0(5nGgr>in#Chd zSzc37!%-tOu2;_83N9LP?YxbVedy58G*^YpMzM3EV5arOV?4S7$M3T8 z%kqfU(M0hA6c2IV=#PY%#^7bd&J*4~z>y-6+PYC393VV-hkt%Av!Xo8m}?4er9Mgy zWcDynmATk8hNrl$d>Z^ABUU0lStTLEz>u_lf4Qu|d8l5C-T3zP4yO2gM8faX7*(&) zrDh(^1ooC(Fz;rEOC%FwyBOMajr)M>x0vCMGTu{+lb(|dO;K!sO438GGkj;egc6b{ zNYkk1k#6qSfiH2_uC)xb+o|61wz9WEGtfjlP`)I|i*axIyoPhN zuW53OdfPzCOuZ&{yjQ8?8mFbg+=gZLwqq=?0E@Adm47*0#0{%NQv)6F-=NLjknv2N zi(@0)BBo?msLAr=ZW&1Ek~sI?r`rcHC73LrCvRJ;G3kUv-))ZBhN~H0NG8`h&}EfK z80=13-|;Qj>59YSGv8d?R9?S`IpJ+foSx?Ety&H~0T;(EKXQ6(^ec17jgqN(x34LI zVRmnN)3zoFyc%FfSzKyLsjJ~J$;A^rZaJ55lL^lr*i8A}u{>VnTvHFpbZgwao%zrz zH)WNm!nV~=AfFu(g1>(tMZubm!0hT0@%3a@1*KC-{*bbv91=$uY)`N^e zn_Ay1PqzIwbm1+(uV?M_6n=nQ-+hT-+Lbtq-kZwL-;zQgF;Am3D9ou66Xp4iqh^-t zOVFH~o`cM%TJqSLae;h_ykEYyqqixLXC~V++`)~8;B}G zSj)vyR3$Bxw3eGVowj#V9!;lHE%?Hw)m_aD<9$G0LQllqzTXv<9e>ye;k!!&dPibv z@J`Sqf}2ZQjScH!Qs`vHbWY5fru6bUj)4W+J&Dmuk7-@)7~ZpPzmh}TV)hhUr3N1s z{RctKiHRLOw5qDooC}0UVRwvAPt%>YsNm&v$e+F_lQM=w7#*&)1h=k&>uJb}g!%Cj zD@xep)TK%?V^tUUyBc+`BrTxvZDRHXhO0i}=fak?K`$q=ms!RFz*! zy}2!Mmw34{Db>PmP@!aR zUQv*UhE-XRxLF<8@BAi_d-|-uF{9kiwpTyqx#-4-PWQ*l zw(dF|m^{oP&Xh~=8-?v$lK1fGaLuo2zXT-D}4#NqJ?WVak>hnq2V@+zHis!J$g4!3xE-hVvmtRd7O`5$^@ z^8ZI#uKHJE-(-ffA3h=t53;FO49|trwIGxpO>WZjIViBjhl~?QnKj0IwVje#*Jkzm zt6N!iOC}oujAB<#Wk|u`fWf2YjGZFM4vSAiLRlUXK<8rgjspRUSv`I-VOL^<|F(dU zUKn7Qk(2JJ;@e%GkVO#(hqlAb%$#WA)4k?EVbwc>CybCy%&}V<);7dni^00Q563fLr~DPBkcZ&l}`T4vEGEIepz6cvqVb- zI23FhqhhF{@)hrHslMk-Aw=e4SLH~~w9%pwp7is1660rzUCuGT-xa!&-i>&@8~rhoSq;VrNOShf5@Mp@tE6&PXbA^nn>%Kpek5q*wcSOhc%yUms^25%_w ztx&>%M$+tVYd(^Be%7(03v^{-lw&+Mg{&m&Zp7&jh5A6_#O;y?|$CSH}KpTh%{N~q|Ds))3ZhUFXs&S(7|vHekBioDdk8V zVd&4$cp8glYHs7X_92mpbT>ZRzwNE~{}R6lx{qduxo?lLO|%30(=8cu`v9+u=*f*U z6JN`H5)L>MB+>_R&MPv(ff$b}1A)x1##6&Dw!0fPCL9ZSu8-V1NfNSyB16qVf&FU} z3*sCrGUjB%j_~#-B~(F%-fq28%TtW_u_XrQq`f7($aNq}H;PskQx6n-7HZ}~jw%Aa z#~?`b79+`ajl!>7FQ`7Qd4-X>8#pT~@6XRm>sYA^?U3IjByRKf5K3*^&|W-zZY*fm z0|zsQtstY%39@qr#q5)4HMA1toHF`qKUciWsFN|Ft-nlaP&NH#oJ5n}CJs$oex*1T zy}(|xuP@AA&UP<(SPqOTqGJUTxEQtEZq&k3t|nXv(fX>MGDo`F#;0O zDss?EZv`qNiiNbM$Q7~!v%=Lfvgc-i<>Q+H!JTj5&4VlJu+BF#U%!QNG|X5Mkw0v7 zjsMHmeiJHU%W)sDNRHj>E$V!)_?4qRsEhWqzTDqNk5Y*4BVpOOkwx^!%$7?ty{2zO zwseR#oGaNb30}gK7tI;p*Y#f%cpxgYtEx%a$u>h>tLHB!7T+BjZ^dg4LlO7jmLdZZ zgDu&H>P^g&AKH=aoqng=k${%=#Vu7hzYZB0XDhy?qSMu?MVt&Sw<~G3W>WfW;;eJm z_-;x)OQ@ukW6$=03dT2Y5rkF0_ZsBOnmV0na$xxGk~RAn=B;6u5+{ojb49WNCI|=| zN{Cw#?{vO-HN*f63aLdrBhg#|jSLAKqv`sB`T~?K8 zSqot*e(}1nF{~`#F=KP9nlb}p?WP+>-LKs8>*55r8DdT3m|SEC)jNjA+-H zOu23b-nbP1m^V4vsT>hRfj0I~XZ_nxdO?AxxfiJ3I};wJbq$Gmo#^>6E10=)4IIeH z$y95(%6ML8t9cHk%RcW{FWzh|*S7aUS~y(_HFMMR(&EIgV&YUh(H-wa0=x7*Mjzbv1mTGRs zE=}_+|Ml_p*8!Om+Mbmyi`w_iW-&LC@%U^HPo6{WCEt#x53ZFY8OiVrUB%7^`iA;% zz97KBXfmpB%fOq+EjH-c2W&N?G|0ZWq}(dc3{nnELXM!?9VUNtrD-yu5LPHy40}}N z%-iwbewtc1b+(5-RnR#d6wgdo6?YQ8wKJ{#0M9d<6;@h#l;n7DyCtO#VN zJ(}VwgU^XpI#pU=#{f!=bBXWRX_kv41>?Qr ztJ=!~aVZ$6ChOa-RcFLE-G#ne8hL3%!3aX;%Dqu%GKULqe4IhTW_Zo(<~(HMU%JF4 zSjh7^kH|2j(xnnpE2?vc%S#RzrX9U0Fd487sLV_cUf?oITQCrNFzBm@eG8L1KlGWr=7e&m)&XqJVw`G5`_4w7R_sN3mf1cGm2toV9*-^3^_Y}_= zL&Z~(*nNO)1`>to&xDE(L6s-$!wpIil0U~VxrKBCqW>^!{NjI1Z{a8QlcRh zDoZdQ`t6{ORvB0)^zAX&E||(czg2sVeh+QzJtv>&?i`31N5Oa)RL!;virv z?ddNJMq<{@q3Wr@s2Qgo_2n7!tU_*DKOwf0#v)mdJn|az9`@`km@oRa`gZpSzx1JR z(lXMG53ffMd=eRKpoH8gHxBoB%<$X~^dx4O1Brbj{{1jExr~;3GQ5L=x)}f7`5$YI z6R7`=E_T!`F3+bsMpR>y(*rRbieWb9x|OwS_oajG(EQ3d>xqlq*gvm!m5S% zFhftd|Kw$@5CbI~z_#L_s8S!Z=l%(C{@td(Dtqn_f%-a*DX*(ghOZ1MRqK^o(? zwbmFkM1w+CTPCWe#~~Y{9Qn4T`1n=XXE$m`7SG9$u=<8kn_LKonJ_b zbNUAFp#tA8#u(Utl+;fZkzDS`X4Eh_)-psJ0Zz6xcXspydI=pP7v@4Xcp z{KVg<2DOe(6yLj5I4?x78+`D=E8lNTM+tsX~o8)7ZF87cMo>9FY9@U z-SiM4aWb*0he9B&=w8Oaj2Kb;Us@Up>YNGlh(uy+an+2$OlvXUW!@b!mvtJH7i>5= zPFsB$FHT>||><<9HmpQl0{U%YboWFLPB~aR|A|0 zxD~|Z9JJha)b20+heMv2^R%Pyw8J1yr638mm>NMXSuNS_*lE4fZ)3zJJg4Oh26FC= z`Rb6$p1P*LLK&>UKtQ7Ff#c2^qm;!KJqaIEuGI#BOZ_l5fBKu&;xURc%J;|7#nP=VH z+#f^xr)Lb}ShPos&{-~lL%x)ocq!3`fE|~t3(Hl0BQ|-X&K;=7G!VHLr^@Vc?EGXc zm?io=K>3=Vi_!|}Jxf}9CMq*tR=3^xR*&}CVAo|!4N*=S2X4dWP(w4KRH}%0jcDfj zdTk7bCpWA>j}2N__u5P=7#o2<3SG}QhuG>-D^8rqDVm|e6cYIo9m;sG*a}Cv+p~`~ zNg-z_w#%OD(FQ)vgsQ94!G5p1v;k(GM!Qjg-iE~6%v_|i%rQ87erc|fj$Ij~x@xQ& z_$FXj#|l=Cm2 zMip%%ae}nm*6N`JGxGs^129d1^OSi0S-mgo<&w5A6MY*Mt40YKl5m|1e=6lhkXVem^PLwH9BNb ze0vDiI_masc+3Bby)S`k^4u1swbix`lmeoREmRN?6vH5MTN$h-x2CN{=R?z``h#Wc3oMP4a)9biQk6KGFA=7#AQx+HR`)2YLXYA(#ou8fr(AV&w_sa z7)f9g>lZ!mS*PaaN)XwxWKFXmJDy-({GeaWvtqN