diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/pom.xml b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/pom.xml similarity index 82% rename from auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/pom.xml rename to auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/pom.xml index b6c1ba1b816..cd2cb7c5914 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/pom.xml +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/pom.xml @@ -9,10 +9,10 @@ 1.1.0-SNAPSHOT ../../../pom.xml - spring-ai-autoconfigure-mcp-client + spring-ai-autoconfigure-mcp-client-common jar - Spring AI MCP Client Auto Configuration - Spring AI MCP Client Auto Configuration + Spring AI MCP Client Common Auto Configuration + Spring AI MCP Client Common Auto Configuration https://github.com/spring-projects/spring-ai @@ -35,11 +35,11 @@ true - + org.springframework.boot @@ -53,15 +53,6 @@ true - - - org.springframework.ai diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/java/org/springframework/ai/mcp/client/autoconfigure/McpClientAutoConfiguration.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/McpClientAutoConfiguration.java similarity index 91% rename from auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/java/org/springframework/ai/mcp/client/autoconfigure/McpClientAutoConfiguration.java rename to auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/McpClientAutoConfiguration.java index 4de3b5d1f1f..28c628e5019 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/java/org/springframework/ai/mcp/client/autoconfigure/McpClientAutoConfiguration.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/McpClientAutoConfiguration.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.ai.mcp.client.autoconfigure; +package org.springframework.ai.mcp.client.common.autoconfigure; import java.util.ArrayList; import java.util.List; @@ -24,9 +24,9 @@ import io.modelcontextprotocol.client.McpSyncClient; import io.modelcontextprotocol.spec.McpSchema; -import org.springframework.ai.mcp.client.autoconfigure.configurer.McpAsyncClientConfigurer; -import org.springframework.ai.mcp.client.autoconfigure.configurer.McpSyncClientConfigurer; -import org.springframework.ai.mcp.client.autoconfigure.properties.McpClientCommonProperties; +import org.springframework.ai.mcp.client.common.autoconfigure.configurer.McpAsyncClientConfigurer; +import org.springframework.ai.mcp.client.common.autoconfigure.configurer.McpSyncClientConfigurer; +import org.springframework.ai.mcp.client.common.autoconfigure.properties.McpClientCommonProperties; import org.springframework.ai.mcp.customizer.McpAsyncClientCustomizer; import org.springframework.ai.mcp.customizer.McpSyncClientCustomizer; import org.springframework.beans.factory.ObjectProvider; @@ -98,8 +98,14 @@ * @see SseHttpClientTransportAutoConfiguration * @see SseWebFluxTransportAutoConfiguration */ -@AutoConfiguration(after = { StdioTransportAutoConfiguration.class, SseHttpClientTransportAutoConfiguration.class, - SseWebFluxTransportAutoConfiguration.class }) +@AutoConfiguration(afterName = { + "org.springframework.ai.mcp.client.common.autoconfigure.StdioTransportAutoConfiguration", + "org.springframework.ai.mcp.client.httpclient.autoconfigure.SseHttpClientTransportAutoConfiguration", + "org.springframework.ai.mcp.client.httpclient.autoconfigure.StreamableHttpHttpClientTransportAutoConfiguration", + "org.springframework.ai.mcp.client.webflux.autoconfigure.SseWebFluxTransportAutoConfiguration", + "org.springframework.ai.mcp.client.webflux.autoconfigure.StreamableHttpWebFluxTransportAutoConfiguration" }) + +// @AutoConfiguration @ConditionalOnClass({ McpSchema.class }) @EnableConfigurationProperties(McpClientCommonProperties.class) @ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/java/org/springframework/ai/mcp/client/autoconfigure/McpToolCallbackAutoConfiguration.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/McpToolCallbackAutoConfiguration.java similarity index 95% rename from auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/java/org/springframework/ai/mcp/client/autoconfigure/McpToolCallbackAutoConfiguration.java rename to auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/McpToolCallbackAutoConfiguration.java index 53083e7620d..a477af8a47a 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/java/org/springframework/ai/mcp/client/autoconfigure/McpToolCallbackAutoConfiguration.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/McpToolCallbackAutoConfiguration.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.ai.mcp.client.autoconfigure; +package org.springframework.ai.mcp.client.common.autoconfigure; import java.util.List; @@ -23,7 +23,7 @@ import org.springframework.ai.mcp.AsyncMcpToolCallbackProvider; import org.springframework.ai.mcp.SyncMcpToolCallbackProvider; -import org.springframework.ai.mcp.client.autoconfigure.properties.McpClientCommonProperties; +import org.springframework.ai.mcp.client.common.autoconfigure.properties.McpClientCommonProperties; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.AllNestedConditions; diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/java/org/springframework/ai/mcp/client/autoconfigure/NamedClientMcpTransport.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/NamedClientMcpTransport.java similarity index 94% rename from auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/java/org/springframework/ai/mcp/client/autoconfigure/NamedClientMcpTransport.java rename to auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/NamedClientMcpTransport.java index 238e67ee566..55e840c0045 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/java/org/springframework/ai/mcp/client/autoconfigure/NamedClientMcpTransport.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/NamedClientMcpTransport.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.ai.mcp.client.autoconfigure; +package org.springframework.ai.mcp.client.common.autoconfigure; import io.modelcontextprotocol.spec.McpClientTransport; diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/java/org/springframework/ai/mcp/client/autoconfigure/StdioTransportAutoConfiguration.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/StdioTransportAutoConfiguration.java similarity index 92% rename from auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/java/org/springframework/ai/mcp/client/autoconfigure/StdioTransportAutoConfiguration.java rename to auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/StdioTransportAutoConfiguration.java index 57b96e1ad89..bb3aefbb66b 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/java/org/springframework/ai/mcp/client/autoconfigure/StdioTransportAutoConfiguration.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/StdioTransportAutoConfiguration.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.ai.mcp.client.autoconfigure; +package org.springframework.ai.mcp.client.common.autoconfigure; import java.util.ArrayList; import java.util.List; @@ -24,8 +24,8 @@ import io.modelcontextprotocol.client.transport.StdioClientTransport; import io.modelcontextprotocol.spec.McpSchema; -import org.springframework.ai.mcp.client.autoconfigure.properties.McpClientCommonProperties; -import org.springframework.ai.mcp.client.autoconfigure.properties.McpStdioClientProperties; +import org.springframework.ai.mcp.client.common.autoconfigure.properties.McpClientCommonProperties; +import org.springframework.ai.mcp.client.common.autoconfigure.properties.McpStdioClientProperties; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/aot/McpClientAutoConfigurationRuntimeHints.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/aot/McpClientAutoConfigurationRuntimeHints.java new file mode 100644 index 00000000000..c0f21e5a7c9 --- /dev/null +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/aot/McpClientAutoConfigurationRuntimeHints.java @@ -0,0 +1,42 @@ +/* + * 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.client.common.autoconfigure.aot; + +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; + +import static org.springframework.ai.aot.AiRuntimeHints.findJsonAnnotatedClassesInPackage; + +/** + * @author Josh Long + * @author Soby Chacko + * @author Christian Tzolov + */ +public class McpClientAutoConfigurationRuntimeHints implements RuntimeHintsRegistrar { + + @Override + public void registerHints(RuntimeHints hints, ClassLoader classLoader) { + hints.resources().registerPattern("**.json"); + + var mcs = MemberCategory.values(); + for (var tr : findJsonAnnotatedClassesInPackage("org.springframework.ai.mcp.client.common.autoconfigure")) { + hints.reflection().registerType(tr, mcs); + } + } + +} diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/java/org/springframework/ai/mcp/client/autoconfigure/configurer/McpAsyncClientConfigurer.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/configurer/McpAsyncClientConfigurer.java similarity index 94% rename from auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/java/org/springframework/ai/mcp/client/autoconfigure/configurer/McpAsyncClientConfigurer.java rename to auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/configurer/McpAsyncClientConfigurer.java index 5f57c90237d..7ba21e9a8b8 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/java/org/springframework/ai/mcp/client/autoconfigure/configurer/McpAsyncClientConfigurer.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/configurer/McpAsyncClientConfigurer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.ai.mcp.client.autoconfigure.configurer; +package org.springframework.ai.mcp.client.common.autoconfigure.configurer; import java.util.List; diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/java/org/springframework/ai/mcp/client/autoconfigure/configurer/McpSyncClientConfigurer.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/configurer/McpSyncClientConfigurer.java similarity index 96% rename from auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/java/org/springframework/ai/mcp/client/autoconfigure/configurer/McpSyncClientConfigurer.java rename to auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/configurer/McpSyncClientConfigurer.java index 681b5fb0001..87520419cc1 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/java/org/springframework/ai/mcp/client/autoconfigure/configurer/McpSyncClientConfigurer.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/configurer/McpSyncClientConfigurer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.ai.mcp.client.autoconfigure.configurer; +package org.springframework.ai.mcp.client.common.autoconfigure.configurer; import java.util.List; diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/java/org/springframework/ai/mcp/client/autoconfigure/properties/McpClientCommonProperties.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/properties/McpClientCommonProperties.java similarity index 98% rename from auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/java/org/springframework/ai/mcp/client/autoconfigure/properties/McpClientCommonProperties.java rename to auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/properties/McpClientCommonProperties.java index c53720bbe00..fcc534080aa 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/java/org/springframework/ai/mcp/client/autoconfigure/properties/McpClientCommonProperties.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/properties/McpClientCommonProperties.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.ai.mcp.client.autoconfigure.properties; +package org.springframework.ai.mcp.client.common.autoconfigure.properties; import java.time.Duration; diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/java/org/springframework/ai/mcp/client/autoconfigure/properties/McpSseClientProperties.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/properties/McpSseClientProperties.java similarity index 96% rename from auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/java/org/springframework/ai/mcp/client/autoconfigure/properties/McpSseClientProperties.java rename to auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/properties/McpSseClientProperties.java index 54a1963d0d6..f23029ddd96 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/java/org/springframework/ai/mcp/client/autoconfigure/properties/McpSseClientProperties.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/properties/McpSseClientProperties.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.ai.mcp.client.autoconfigure.properties; +package org.springframework.ai.mcp.client.common.autoconfigure.properties; import java.util.HashMap; import java.util.Map; diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/java/org/springframework/ai/mcp/client/autoconfigure/properties/McpStdioClientProperties.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/properties/McpStdioClientProperties.java similarity index 98% rename from auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/java/org/springframework/ai/mcp/client/autoconfigure/properties/McpStdioClientProperties.java rename to auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/properties/McpStdioClientProperties.java index 2b18ce3cda0..7517f45e858 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/java/org/springframework/ai/mcp/client/autoconfigure/properties/McpStdioClientProperties.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/properties/McpStdioClientProperties.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.ai.mcp.client.autoconfigure.properties; +package org.springframework.ai.mcp.client.common.autoconfigure.properties; import java.util.HashMap; import java.util.List; diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/properties/McpStreamableHttpClientProperties.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/properties/McpStreamableHttpClientProperties.java new file mode 100644 index 00000000000..afa74ded003 --- /dev/null +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/properties/McpStreamableHttpClientProperties.java @@ -0,0 +1,74 @@ +/* + * 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.client.common.autoconfigure.properties; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Configuration properties for Streamable Http client connections. + * + *

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

+ * Example configuration:

+ * spring.ai.mcp.client.streamable-http:
+ *   connections-http:
+ *     server1:
+ *       url: http://localhost:8080/events
+ *     server2:
+ *       url: http://otherserver:8081/events
+ * 
+ * + * @author Christian Tzolov + * @see ConnectionParameters + */ +@ConfigurationProperties(McpStreamableHttpClientProperties.CONFIG_PREFIX) +public class McpStreamableHttpClientProperties { + + public static final String CONFIG_PREFIX = "spring.ai.mcp.client.streamable-http"; + + /** + * Map of named Streamable Http connection configurations. + *

+ * The key represents the connection name, and the value contains the Streamable Http + * parameters for that connection. + */ + private final Map connections = new HashMap<>(); + + /** + * Returns the map of configured Streamable Http connections. + * @return map of connection names to their Streamable Http parameters + */ + public Map getConnections() { + return this.connections; + } + + /** + * Parameters for configuring an Streamable Http connection to an MCP server. + * + * @param url the URL endpoint for Streamable Http communication with the MCP server + * @param endpoint the endpoint for the MCP server + */ + public record ConnectionParameters(String url, String endpoint) { + } + +} diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/resources/META-INF/spring/aot.factories b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/resources/META-INF/spring/aot.factories new file mode 100644 index 00000000000..810b2a3164e --- /dev/null +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/resources/META-INF/spring/aot.factories @@ -0,0 +1,2 @@ +org.springframework.aot.hint.RuntimeHintsRegistrar=\ + org.springframework.ai.mcp.client.common.autoconfigure.aot.McpClientAutoConfigurationRuntimeHints diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/test/java/org/springframework/ai/mcp/client/autoconfigure/McpClientAutoConfigurationIT.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/java/org/springframework/ai/mcp/client/common/autoconfigure/McpClientAutoConfigurationIT.java similarity index 96% rename from auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/test/java/org/springframework/ai/mcp/client/autoconfigure/McpClientAutoConfigurationIT.java rename to auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/java/org/springframework/ai/mcp/client/common/autoconfigure/McpClientAutoConfigurationIT.java index 0b988f80cba..728188672c5 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/test/java/org/springframework/ai/mcp/client/autoconfigure/McpClientAutoConfigurationIT.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/java/org/springframework/ai/mcp/client/common/autoconfigure/McpClientAutoConfigurationIT.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.ai.mcp.client.autoconfigure; +package org.springframework.ai.mcp.client.common.autoconfigure; import java.time.Duration; import java.util.List; @@ -30,8 +30,8 @@ import org.mockito.Mockito; import reactor.core.publisher.Mono; -import org.springframework.ai.mcp.client.autoconfigure.configurer.McpSyncClientConfigurer; -import org.springframework.ai.mcp.client.autoconfigure.properties.McpClientCommonProperties; +import org.springframework.ai.mcp.client.common.autoconfigure.configurer.McpSyncClientConfigurer; +import org.springframework.ai.mcp.client.common.autoconfigure.properties.McpClientCommonProperties; import org.springframework.ai.mcp.customizer.McpSyncClientCustomizer; import org.springframework.ai.tool.ToolCallback; import org.springframework.boot.autoconfigure.AutoConfigurations; diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/test/java/org/springframework/ai/mcp/client/autoconfigure/McpClientAutoConfigurationRuntimeHintsTests.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/java/org/springframework/ai/mcp/client/common/autoconfigure/McpClientAutoConfigurationRuntimeHintsTests.java similarity index 92% rename from auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/test/java/org/springframework/ai/mcp/client/autoconfigure/McpClientAutoConfigurationRuntimeHintsTests.java rename to auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/java/org/springframework/ai/mcp/client/common/autoconfigure/McpClientAutoConfigurationRuntimeHintsTests.java index b5acb04af41..a514705c6b9 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/test/java/org/springframework/ai/mcp/client/autoconfigure/McpClientAutoConfigurationRuntimeHintsTests.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/java/org/springframework/ai/mcp/client/common/autoconfigure/McpClientAutoConfigurationRuntimeHintsTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.ai.mcp.client.autoconfigure; +package org.springframework.ai.mcp.client.common.autoconfigure; import java.io.IOException; import java.util.HashSet; @@ -22,8 +22,8 @@ import org.junit.jupiter.api.Test; -import org.springframework.ai.mcp.client.autoconfigure.aot.McpClientAutoConfigurationRuntimeHints; -import org.springframework.ai.mcp.client.autoconfigure.properties.McpStdioClientProperties; +import org.springframework.ai.mcp.client.common.autoconfigure.aot.McpClientAutoConfigurationRuntimeHints; +import org.springframework.ai.mcp.client.common.autoconfigure.properties.McpStdioClientProperties; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.TypeReference; import org.springframework.core.io.Resource; diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/test/java/org/springframework/ai/mcp/client/autoconfigure/McpToolCallbackAutoConfigurationConditionTests.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/java/org/springframework/ai/mcp/client/common/autoconfigure/McpToolCallbackAutoConfigurationConditionTests.java similarity index 93% rename from auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/test/java/org/springframework/ai/mcp/client/autoconfigure/McpToolCallbackAutoConfigurationConditionTests.java rename to auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/java/org/springframework/ai/mcp/client/common/autoconfigure/McpToolCallbackAutoConfigurationConditionTests.java index 0fb5174ef6c..3708e0fa036 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/test/java/org/springframework/ai/mcp/client/autoconfigure/McpToolCallbackAutoConfigurationConditionTests.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/java/org/springframework/ai/mcp/client/common/autoconfigure/McpToolCallbackAutoConfigurationConditionTests.java @@ -14,11 +14,11 @@ * limitations under the License. */ -package org.springframework.ai.mcp.client.autoconfigure; +package org.springframework.ai.mcp.client.common.autoconfigure; import org.junit.jupiter.api.Test; -import org.springframework.ai.mcp.client.autoconfigure.McpToolCallbackAutoConfiguration.McpToolCallbackAutoConfigurationCondition; +import org.springframework.ai.mcp.client.common.autoconfigure.McpToolCallbackAutoConfiguration.McpToolCallbackAutoConfigurationCondition; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/test/java/org/springframework/ai/mcp/client/autoconfigure/McpToolCallbackAutoConfigurationTests.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/java/org/springframework/ai/mcp/client/common/autoconfigure/McpToolCallbackAutoConfigurationTests.java similarity index 97% rename from auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/test/java/org/springframework/ai/mcp/client/autoconfigure/McpToolCallbackAutoConfigurationTests.java rename to auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/java/org/springframework/ai/mcp/client/common/autoconfigure/McpToolCallbackAutoConfigurationTests.java index 8838857971d..9a55167fa31 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/test/java/org/springframework/ai/mcp/client/autoconfigure/McpToolCallbackAutoConfigurationTests.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/java/org/springframework/ai/mcp/client/common/autoconfigure/McpToolCallbackAutoConfigurationTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.ai.mcp.client.autoconfigure; +package org.springframework.ai.mcp.client.common.autoconfigure; import org.junit.jupiter.api.Test; diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/test/java/org/springframework/ai/mcp/client/autoconfigure/properties/McpClientCommonPropertiesTests.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/java/org/springframework/ai/mcp/client/common/autoconfigure/properties/McpClientCommonPropertiesTests.java similarity index 99% rename from auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/test/java/org/springframework/ai/mcp/client/autoconfigure/properties/McpClientCommonPropertiesTests.java rename to auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/java/org/springframework/ai/mcp/client/common/autoconfigure/properties/McpClientCommonPropertiesTests.java index 39c25c26327..18eb85e2c3f 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/test/java/org/springframework/ai/mcp/client/autoconfigure/properties/McpClientCommonPropertiesTests.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/java/org/springframework/ai/mcp/client/common/autoconfigure/properties/McpClientCommonPropertiesTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.ai.mcp.client.autoconfigure.properties; +package org.springframework.ai.mcp.client.common.autoconfigure.properties; import java.time.Duration; diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/test/java/org/springframework/ai/mcp/client/autoconfigure/properties/McpSseClientPropertiesTests.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/java/org/springframework/ai/mcp/client/common/autoconfigure/properties/McpSseClientPropertiesTests.java similarity index 99% rename from auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/test/java/org/springframework/ai/mcp/client/autoconfigure/properties/McpSseClientPropertiesTests.java rename to auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/java/org/springframework/ai/mcp/client/common/autoconfigure/properties/McpSseClientPropertiesTests.java index 79f532c8f32..b3c72aa08b3 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/test/java/org/springframework/ai/mcp/client/autoconfigure/properties/McpSseClientPropertiesTests.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/java/org/springframework/ai/mcp/client/common/autoconfigure/properties/McpSseClientPropertiesTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.ai.mcp.client.autoconfigure.properties; +package org.springframework.ai.mcp.client.common.autoconfigure.properties; import java.util.Map; diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/test/resources/application-test.properties b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/resources/application-test.properties similarity index 100% rename from auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/test/resources/application-test.properties rename to auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/resources/application-test.properties diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/test/resources/nested/nested-config.json b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/resources/nested/nested-config.json similarity index 100% rename from auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/test/resources/nested/nested-config.json rename to auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/resources/nested/nested-config.json diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/test/resources/test-config.json b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/resources/test-config.json similarity index 100% rename from auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/test/resources/test-config.json rename to auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/resources/test-config.json diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/pom.xml b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/pom.xml new file mode 100644 index 00000000000..caef1b36103 --- /dev/null +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/pom.xml @@ -0,0 +1,84 @@ + + + 4.0.0 + + org.springframework.ai + spring-ai-parent + 1.1.0-SNAPSHOT + ../../../pom.xml + + spring-ai-autoconfigure-mcp-client-httpclient + jar + Spring AI MCP Client (HttpClient) Auto Configuration + Spring AI MCP Client (HttpClient) 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 + + + + org.springframework.ai + spring-ai-autoconfigure-mcp-client-common + ${project.parent.version} + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + org.springframework.boot + spring-boot-autoconfigure-processor + true + + + + + org.springframework.ai + spring-ai-test + ${project.parent.version} + test + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.mockito + mockito-core + test + + + + org.testcontainers + junit-jupiter + ${testcontainers.version} + test + + + + diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/java/org/springframework/ai/mcp/client/autoconfigure/SseHttpClientTransportAutoConfiguration.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/main/java/org/springframework/ai/mcp/client/httpclient/autoconfigure/SseHttpClientTransportAutoConfiguration.java similarity index 85% rename from auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/java/org/springframework/ai/mcp/client/autoconfigure/SseHttpClientTransportAutoConfiguration.java rename to auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/main/java/org/springframework/ai/mcp/client/httpclient/autoconfigure/SseHttpClientTransportAutoConfiguration.java index 79aabc0d2cc..1f713e75e88 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/java/org/springframework/ai/mcp/client/autoconfigure/SseHttpClientTransportAutoConfiguration.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/main/java/org/springframework/ai/mcp/client/httpclient/autoconfigure/SseHttpClientTransportAutoConfiguration.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.ai.mcp.client.autoconfigure; +package org.springframework.ai.mcp.client.httpclient.autoconfigure; import java.net.http.HttpClient; import java.util.ArrayList; @@ -26,13 +26,13 @@ import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport; import io.modelcontextprotocol.spec.McpSchema; -import org.springframework.ai.mcp.client.autoconfigure.properties.McpClientCommonProperties; -import org.springframework.ai.mcp.client.autoconfigure.properties.McpSseClientProperties; -import org.springframework.ai.mcp.client.autoconfigure.properties.McpSseClientProperties.SseParameters; +import org.springframework.ai.mcp.client.common.autoconfigure.NamedClientMcpTransport; +import org.springframework.ai.mcp.client.common.autoconfigure.properties.McpClientCommonProperties; +import org.springframework.ai.mcp.client.common.autoconfigure.properties.McpSseClientProperties; +import org.springframework.ai.mcp.client.common.autoconfigure.properties.McpSseClientProperties.SseParameters; 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.ConditionalOnMissingClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; @@ -61,9 +61,8 @@ * @see HttpClientSseClientTransport * @see McpSseClientProperties */ -@AutoConfiguration(after = SseWebFluxTransportAutoConfiguration.class) +@AutoConfiguration @ConditionalOnClass({ McpSchema.class, McpSyncClient.class }) -@ConditionalOnMissingClass("io.modelcontextprotocol.client.transport.WebFluxSseClientTransport") @EnableConfigurationProperties({ McpSseClientProperties.class, McpClientCommonProperties.class }) @ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true) @@ -85,7 +84,7 @@ public class SseHttpClientTransportAutoConfiguration { * @return list of named MCP transports */ @Bean - public List mcpHttpClientTransports(McpSseClientProperties sseProperties, + public List sseHttpClientTransports(McpSseClientProperties sseProperties, ObjectProvider objectMapperProvider) { ObjectMapper objectMapper = objectMapperProvider.getIfAvailable(ObjectMapper::new); diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/main/java/org/springframework/ai/mcp/client/httpclient/autoconfigure/StreamableHttpHttpClientTransportAutoConfiguration.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/main/java/org/springframework/ai/mcp/client/httpclient/autoconfigure/StreamableHttpHttpClientTransportAutoConfiguration.java new file mode 100644 index 00000000000..f18066553f8 --- /dev/null +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/main/java/org/springframework/ai/mcp/client/httpclient/autoconfigure/StreamableHttpHttpClientTransportAutoConfiguration.java @@ -0,0 +1,118 @@ +/* + * 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.client.httpclient.autoconfigure; + +import java.net.http.HttpClient; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.springframework.ai.mcp.client.common.autoconfigure.NamedClientMcpTransport; +import org.springframework.ai.mcp.client.common.autoconfigure.properties.McpClientCommonProperties; +import org.springframework.ai.mcp.client.common.autoconfigure.properties.McpStreamableHttpClientProperties; +import org.springframework.ai.mcp.client.common.autoconfigure.properties.McpStreamableHttpClientProperties.ConnectionParameters; +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.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.modelcontextprotocol.client.McpSyncClient; +import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport; +import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport; +import io.modelcontextprotocol.spec.McpSchema; + +/** + * Auto-configuration for Streamable HTTP client transport in the Model Context Protocol + * (MCP). + * + *

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

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

+ * Key features: + *

    + *
  • Creates HTTP client-based Streamable HTTP transports for configured MCP server + * connections + *
  • Configures ObjectMapper for JSON serialization/deserialization + *
  • Supports multiple named server connections with different URLs + *
+ * + * @see HttpClientStreamableHttpTransport + * @see McpStreamableHttpClientProperties + */ +@AutoConfiguration +@ConditionalOnClass({ McpSchema.class, McpSyncClient.class }) +@EnableConfigurationProperties({ McpStreamableHttpClientProperties.class, McpClientCommonProperties.class }) +@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", + matchIfMissing = true) +public class StreamableHttpHttpClientTransportAutoConfiguration { + + /** + * Creates a list of HTTP client-based Streamable HTTP transports for MCP + * communication. + * + *

+ * Each transport is configured with: + *

    + *
  • A new HttpClient instance + *
  • Server URL from properties + *
  • ObjectMapper for JSON processing + *
+ * @param streamableProperties the Streamable HTTP client properties containing server + * configurations + * @param objectMapperProvider the provider for ObjectMapper or a new instance if not + * available + * @return list of named MCP transports + */ + @Bean + public List streamableHttpHttpClientTransports( + McpStreamableHttpClientProperties streamableProperties, ObjectProvider objectMapperProvider) { + + ObjectMapper objectMapper = objectMapperProvider.getIfAvailable(ObjectMapper::new); + + List streamableHttpTransports = new ArrayList<>(); + + for (Map.Entry serverParameters : streamableProperties.getConnections() + .entrySet()) { + + String baseUrl = serverParameters.getValue().url(); + String streamableHttpEndpoint = serverParameters.getValue().endpoint() != null + ? serverParameters.getValue().endpoint() : "/mcp"; + + HttpClientStreamableHttpTransport transport = HttpClientStreamableHttpTransport.builder(baseUrl) + .endpoint(streamableHttpEndpoint) + .clientBuilder(HttpClient.newBuilder()) + .objectMapper(objectMapper) + .build(); + + streamableHttpTransports.add(new NamedClientMcpTransport(serverParameters.getKey(), transport)); + } + + return streamableHttpTransports; + } + +} diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/main/java/org/springframework/ai/mcp/client/httpclient/autoconfigure/aot/McpClientAutoConfigurationRuntimeHints.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/main/java/org/springframework/ai/mcp/client/httpclient/autoconfigure/aot/McpClientAutoConfigurationRuntimeHints.java new file mode 100644 index 00000000000..c32a4d3ffc1 --- /dev/null +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/main/java/org/springframework/ai/mcp/client/httpclient/autoconfigure/aot/McpClientAutoConfigurationRuntimeHints.java @@ -0,0 +1,42 @@ +/* + * 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.client.httpclient.autoconfigure.aot; + +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; + +import static org.springframework.ai.aot.AiRuntimeHints.findJsonAnnotatedClassesInPackage; + +/** + * @author Josh Long + * @author Soby Chacko + * @author Christian Tzolov + */ +public class McpClientAutoConfigurationRuntimeHints implements RuntimeHintsRegistrar { + + @Override + public void registerHints(RuntimeHints hints, ClassLoader classLoader) { + hints.resources().registerPattern("**.json"); + + var mcs = MemberCategory.values(); + for (var tr : findJsonAnnotatedClassesInPackage("org.springframework.ai.mcp.client.httpclient.autoconfigure")) { + hints.reflection().registerType(tr, mcs); + } + } + +} diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/main/resources/META-INF/spring/aot.factories b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/main/resources/META-INF/spring/aot.factories new file mode 100644 index 00000000000..63d01bc0352 --- /dev/null +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/main/resources/META-INF/spring/aot.factories @@ -0,0 +1,2 @@ +org.springframework.aot.hint.RuntimeHintsRegistrar=\ + org.springframework.ai.mcp.client.httpclient.autoconfigure.aot.McpClientAutoConfigurationRuntimeHints diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000000..8011f6035ca --- /dev/null +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/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.mcp.client.common.autoconfigure.StdioTransportAutoConfiguration +org.springframework.ai.mcp.client.common.autoconfigure.McpClientAutoConfiguration +org.springframework.ai.mcp.client.common.autoconfigure.McpToolCallbackAutoConfiguration +org.springframework.ai.mcp.client.httpclient.autoconfigure.SseHttpClientTransportAutoConfiguration +org.springframework.ai.mcp.client.httpclient.autoconfigure.StreamableHttpHttpClientTransportAutoConfiguration diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/test/java/org/springframework/ai/mcp/client/autoconfigure/SseHttpClientTransportAutoConfigurationIT.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/test/java/org/springframework/ai/mcp/client/autoconfigure/SseHttpClientTransportAutoConfigurationIT.java new file mode 100644 index 00000000000..70b9b2563a4 --- /dev/null +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/test/java/org/springframework/ai/mcp/client/autoconfigure/SseHttpClientTransportAutoConfigurationIT.java @@ -0,0 +1,86 @@ +/* + * Copyright 2024-2024 the original author or authors. + */ + +package org.springframework.ai.mcp.client.autoconfigure; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ai.mcp.client.common.autoconfigure.McpClientAutoConfiguration; +import org.springframework.ai.mcp.client.httpclient.autoconfigure.SseHttpClientTransportAutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; + +import io.modelcontextprotocol.client.McpSyncClient; +import io.modelcontextprotocol.spec.McpSchema.ListToolsResult; + +@Timeout(15) +public class SseHttpClientTransportAutoConfigurationIT { + + private static final Logger logger = LoggerFactory.getLogger(SseHttpClientTransportAutoConfigurationIT.class); + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withPropertyValues("spring.ai.mcp.client.initialized=false", + "spring.ai.mcp.client.sse.connections.server1.url=" + host) + .withConfiguration( + AutoConfigurations.of(McpClientAutoConfiguration.class, SseHttpClientTransportAutoConfiguration.class)); + + static String host = "http://localhost:3001"; + + // Uses the https://github.com/tzolov/mcp-everything-server-docker-image + @SuppressWarnings("resource") + static GenericContainer container = new GenericContainer<>("docker.io/tzolov/mcp-everything-server:v2") + .withCommand("node dist/index.js sse") + .withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String())) + .withExposedPorts(3001) + .waitingFor(Wait.forHttp("/").forStatusCode(404)); + + @BeforeAll + static void setUp() { + container.start(); + int port = container.getMappedPort(3001); + host = "http://" + container.getHost() + ":" + port; + logger.info("Container started at host: {}", host); + } + + @AfterAll + static void tearDown() { + container.stop(); + } + + @Test + void streamableHttpTest() { + this.contextRunner.run(context -> { + List mcpClients = (List) context.getBean("mcpSyncClients"); + + assertThat(mcpClients).isNotNull(); + assertThat(mcpClients).hasSize(1); + + McpSyncClient mcpClient = mcpClients.get(0); + + mcpClient.ping(); + + System.out.println("mcpClient = " + mcpClient.getServerInfo()); + + ListToolsResult toolsResult = mcpClient.listTools(); + + assertThat(toolsResult).isNotNull(); + assertThat(toolsResult.tools()).isNotEmpty(); + assertThat(toolsResult.tools()).hasSize(8); + + logger.info("tools = {}", toolsResult); + + }); + } + +} diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/test/java/org/springframework/ai/mcp/client/autoconfigure/SseHttpClientTransportAutoConfigurationTests.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/test/java/org/springframework/ai/mcp/client/autoconfigure/SseHttpClientTransportAutoConfigurationTests.java similarity index 73% rename from auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/test/java/org/springframework/ai/mcp/client/autoconfigure/SseHttpClientTransportAutoConfigurationTests.java rename to auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/test/java/org/springframework/ai/mcp/client/autoconfigure/SseHttpClientTransportAutoConfigurationTests.java index fadf71cec75..1e57a9551ea 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/test/java/org/springframework/ai/mcp/client/autoconfigure/SseHttpClientTransportAutoConfigurationTests.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/test/java/org/springframework/ai/mcp/client/autoconfigure/SseHttpClientTransportAutoConfigurationTests.java @@ -16,21 +16,23 @@ package org.springframework.ai.mcp.client.autoconfigure; +import static org.assertj.core.api.Assertions.assertThat; + import java.lang.reflect.Field; import java.util.List; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport; import org.junit.jupiter.api.Test; - +import org.springframework.ai.mcp.client.common.autoconfigure.NamedClientMcpTransport; +import org.springframework.ai.mcp.client.httpclient.autoconfigure.SseHttpClientTransportAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.util.ReflectionUtils; -import static org.assertj.core.api.Assertions.assertThat; +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport; /** * Tests for {@link SseHttpClientTransportAutoConfiguration}. @@ -42,47 +44,26 @@ public class SseHttpClientTransportAutoConfigurationTests { private final ApplicationContextRunner applicationContext = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(SseHttpClientTransportAutoConfiguration.class)); - @Test - void mcpHttpClientTransportsNotPresentIfMissingWebFluxSseClientTransportPresent() { - this.applicationContext.run(context -> assertThat(context.containsBean("mcpHttpClientTransports")).isFalse()); - } - - @Test - void mcpHttpClientTransportsPresentIfMissingWebFluxSseClientTransportNotPresent() { - this.applicationContext - .withClassLoader( - new FilteredClassLoader("io.modelcontextprotocol.client.transport.WebFluxSseClientTransport")) - .run(context -> assertThat(context.containsBean("mcpHttpClientTransports")).isTrue()); - } - @Test void mcpHttpClientTransportsNotPresentIfMcpClientDisabled() { - this.applicationContext - .withClassLoader( - new FilteredClassLoader("io.modelcontextprotocol.client.transport.WebFluxSseClientTransport")) - .withPropertyValues("spring.ai.mcp.client.enabled", "false") - .run(context -> assertThat(context.containsBean("mcpHttpClientTransports")).isFalse()); + this.applicationContext.withPropertyValues("spring.ai.mcp.client.enabled", "false") + .run(context -> assertThat(context.containsBean("sseHttpClientTransports")).isFalse()); } @Test void noTransportsCreatedWithEmptyConnections() { - this.applicationContext - .withClassLoader( - new FilteredClassLoader("io.modelcontextprotocol.client.transport.WebFluxSseClientTransport")) - .run(context -> { - List transports = context.getBean("mcpHttpClientTransports", List.class); - assertThat(transports).isEmpty(); - }); + this.applicationContext.run(context -> { + List transports = context.getBean("sseHttpClientTransports", List.class); + assertThat(transports).isEmpty(); + }); } @Test void singleConnectionCreatesOneTransport() { this.applicationContext - .withClassLoader( - new FilteredClassLoader("io.modelcontextprotocol.client.transport.WebFluxSseClientTransport")) .withPropertyValues("spring.ai.mcp.client.sse.connections.server1.url=http://localhost:8080") .run(context -> { - List transports = context.getBean("mcpHttpClientTransports", List.class); + List transports = context.getBean("sseHttpClientTransports", List.class); assertThat(transports).hasSize(1); assertThat(transports.get(0).name()).isEqualTo("server1"); assertThat(transports.get(0).transport()).isInstanceOf(HttpClientSseClientTransport.class); @@ -92,12 +73,10 @@ void singleConnectionCreatesOneTransport() { @Test void multipleConnectionsCreateMultipleTransports() { this.applicationContext - .withClassLoader( - new FilteredClassLoader("io.modelcontextprotocol.client.transport.WebFluxSseClientTransport")) .withPropertyValues("spring.ai.mcp.client.sse.connections.server1.url=http://localhost:8080", "spring.ai.mcp.client.sse.connections.server2.url=http://otherserver:8081") .run(context -> { - List transports = context.getBean("mcpHttpClientTransports", List.class); + List transports = context.getBean("sseHttpClientTransports", List.class); assertThat(transports).hasSize(2); assertThat(transports).extracting("name").containsExactlyInAnyOrder("server1", "server2"); assertThat(transports).extracting("transport") @@ -112,12 +91,10 @@ void multipleConnectionsCreateMultipleTransports() { @Test void customSseEndpointIsRespected() { this.applicationContext - .withClassLoader( - new FilteredClassLoader("io.modelcontextprotocol.client.transport.WebFluxSseClientTransport")) .withPropertyValues("spring.ai.mcp.client.sse.connections.server1.url=http://localhost:8080", "spring.ai.mcp.client.sse.connections.server1.sse-endpoint=/custom-sse") .run(context -> { - List transports = context.getBean("mcpHttpClientTransports", List.class); + List transports = context.getBean("sseHttpClientTransports", List.class); assertThat(transports).hasSize(1); assertThat(transports.get(0).name()).isEqualTo("server1"); assertThat(transports.get(0).transport()).isInstanceOf(HttpClientSseClientTransport.class); @@ -129,14 +106,11 @@ void customSseEndpointIsRespected() { @Test void customObjectMapperIsUsed() { - this.applicationContext - .withClassLoader( - new FilteredClassLoader("io.modelcontextprotocol.client.transport.WebFluxSseClientTransport")) - .withUserConfiguration(CustomObjectMapperConfiguration.class) + this.applicationContext.withUserConfiguration(CustomObjectMapperConfiguration.class) .withPropertyValues("spring.ai.mcp.client.sse.connections.server1.url=http://localhost:8080") .run(context -> { assertThat(context.getBean(ObjectMapper.class)).isNotNull(); - List transports = context.getBean("mcpHttpClientTransports", List.class); + List transports = context.getBean("sseHttpClientTransports", List.class); assertThat(transports).hasSize(1); }); } @@ -144,11 +118,9 @@ void customObjectMapperIsUsed() { @Test void defaultSseEndpointIsUsedWhenNotSpecified() { this.applicationContext - .withClassLoader( - new FilteredClassLoader("io.modelcontextprotocol.client.transport.WebFluxSseClientTransport")) .withPropertyValues("spring.ai.mcp.client.sse.connections.server1.url=http://localhost:8080") .run(context -> { - List transports = context.getBean("mcpHttpClientTransports", List.class); + List transports = context.getBean("sseHttpClientTransports", List.class); assertThat(transports).hasSize(1); assertThat(transports.get(0).name()).isEqualTo("server1"); assertThat(transports.get(0).transport()).isInstanceOf(HttpClientSseClientTransport.class); @@ -159,13 +131,11 @@ void defaultSseEndpointIsUsedWhenNotSpecified() { @Test void mixedConnectionsWithAndWithoutCustomSseEndpoint() { this.applicationContext - .withClassLoader( - new FilteredClassLoader("io.modelcontextprotocol.client.transport.WebFluxSseClientTransport")) .withPropertyValues("spring.ai.mcp.client.sse.connections.server1.url=http://localhost:8080", "spring.ai.mcp.client.sse.connections.server1.sse-endpoint=/custom-sse", "spring.ai.mcp.client.sse.connections.server2.url=http://otherserver:8081") .run(context -> { - List transports = context.getBean("mcpHttpClientTransports", List.class); + List transports = context.getBean("sseHttpClientTransports", List.class); assertThat(transports).hasSize(2); assertThat(transports).extracting("name").containsExactlyInAnyOrder("server1", "server2"); assertThat(transports).extracting("transport") diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/test/java/org/springframework/ai/mcp/client/autoconfigure/StreamableHttpHttpClientTransportAutoConfigurationIT.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/test/java/org/springframework/ai/mcp/client/autoconfigure/StreamableHttpHttpClientTransportAutoConfigurationIT.java new file mode 100644 index 00000000000..452373a711f --- /dev/null +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/test/java/org/springframework/ai/mcp/client/autoconfigure/StreamableHttpHttpClientTransportAutoConfigurationIT.java @@ -0,0 +1,87 @@ +/* + * Copyright 2024-2024 the original author or authors. + */ + +package org.springframework.ai.mcp.client.autoconfigure; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ai.mcp.client.common.autoconfigure.McpClientAutoConfiguration; +import org.springframework.ai.mcp.client.httpclient.autoconfigure.StreamableHttpHttpClientTransportAutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; + +import io.modelcontextprotocol.client.McpSyncClient; +import io.modelcontextprotocol.spec.McpSchema.ListToolsResult; + +@Timeout(15) +public class StreamableHttpHttpClientTransportAutoConfigurationIT { + + private static final Logger logger = LoggerFactory + .getLogger(StreamableHttpHttpClientTransportAutoConfigurationIT.class); + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withPropertyValues("spring.ai.mcp.client.initialized=false", + "spring.ai.mcp.client.streamable-http.connections.server1.url=" + host) + .withConfiguration(AutoConfigurations.of(McpClientAutoConfiguration.class, + StreamableHttpHttpClientTransportAutoConfiguration.class)); + + static String host = "http://localhost:3001"; + + // Uses the https://github.com/tzolov/mcp-everything-server-docker-image + @SuppressWarnings("resource") + static GenericContainer container = new GenericContainer<>("docker.io/tzolov/mcp-everything-server:v2") + .withCommand("node dist/index.js streamableHttp") + .withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String())) + .withExposedPorts(3001) + .waitingFor(Wait.forHttp("/").forStatusCode(404)); + + @BeforeAll + static void setUp() { + container.start(); + int port = container.getMappedPort(3001); + host = "http://" + container.getHost() + ":" + port; + logger.info("Container started at host: {}", host); + } + + @AfterAll + static void tearDown() { + container.stop(); + } + + @Test + void streamableHttpTest() { + this.contextRunner.run(context -> { + List mcpClients = (List) context.getBean("mcpSyncClients"); + + assertThat(mcpClients).isNotNull(); + assertThat(mcpClients).hasSize(1); + + McpSyncClient mcpClient = mcpClients.get(0); + + mcpClient.ping(); + + System.out.println("mcpClient = " + mcpClient.getServerInfo()); + + ListToolsResult toolsResult = mcpClient.listTools(); + + assertThat(toolsResult).isNotNull(); + assertThat(toolsResult.tools()).isNotEmpty(); + assertThat(toolsResult.tools()).hasSize(8); + + logger.info("tools = {}", toolsResult); + + }); + } + +} diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/test/resources/application-test.properties b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/test/resources/application-test.properties new file mode 100644 index 00000000000..9107b9e407a --- /dev/null +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/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/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/test/resources/nested/nested-config.json b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/test/resources/nested/nested-config.json new file mode 100644 index 00000000000..7cd51d6d490 --- /dev/null +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/test/resources/nested/nested-config.json @@ -0,0 +1,8 @@ +{ + "name": "nested-config", + "description": "Test JSON file in nested subfolder of test resources", + "version": "1.0.0", + "nestedProperties": { + "nestedProperty1": "nestedValue1" + } +} diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/test/resources/test-config.json b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/test/resources/test-config.json new file mode 100644 index 00000000000..57e2a46f20e --- /dev/null +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/test/resources/test-config.json @@ -0,0 +1,8 @@ +{ + "name": "test-config", + "description": "Test JSON file in root test resources folder", + "version": "1.0.0", + "properties": { + "testProperty1": "value1" + } +} diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/pom.xml b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/pom.xml new file mode 100644 index 00000000000..0f6eb5758f8 --- /dev/null +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/pom.xml @@ -0,0 +1,90 @@ + + + 4.0.0 + + org.springframework.ai + spring-ai-parent + 1.1.0-SNAPSHOT + ../../../pom.xml + + spring-ai-autoconfigure-mcp-client-webflux + jar + Spring AI MCP WebFlux Client Auto Configuration + Spring AI MCP WebFlux 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 + + + + org.springframework.ai + spring-ai-autoconfigure-mcp-client-common + ${project.parent.version} + + + + io.modelcontextprotocol.sdk + mcp-spring-webflux + true + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + org.springframework.boot + spring-boot-autoconfigure-processor + true + + + + + org.springframework.ai + spring-ai-test + ${project.parent.version} + test + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.mockito + mockito-core + test + + + + org.testcontainers + junit-jupiter + ${testcontainers.version} + test + + + + diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/java/org/springframework/ai/mcp/client/autoconfigure/SseWebFluxTransportAutoConfiguration.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/src/main/java/org/springframework/ai/mcp/client/webflux/autoconfigure/SseWebFluxTransportAutoConfiguration.java similarity index 87% rename from auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/java/org/springframework/ai/mcp/client/autoconfigure/SseWebFluxTransportAutoConfiguration.java rename to auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/src/main/java/org/springframework/ai/mcp/client/webflux/autoconfigure/SseWebFluxTransportAutoConfiguration.java index 4c064835e3b..595cd97dfa6 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/java/org/springframework/ai/mcp/client/autoconfigure/SseWebFluxTransportAutoConfiguration.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/src/main/java/org/springframework/ai/mcp/client/webflux/autoconfigure/SseWebFluxTransportAutoConfiguration.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.ai.mcp.client.autoconfigure; +package org.springframework.ai.mcp.client.webflux.autoconfigure; import java.util.ArrayList; import java.util.List; @@ -23,9 +23,10 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.client.transport.WebFluxSseClientTransport; -import org.springframework.ai.mcp.client.autoconfigure.properties.McpClientCommonProperties; -import org.springframework.ai.mcp.client.autoconfigure.properties.McpSseClientProperties; -import org.springframework.ai.mcp.client.autoconfigure.properties.McpSseClientProperties.SseParameters; +import org.springframework.ai.mcp.client.common.autoconfigure.NamedClientMcpTransport; +import org.springframework.ai.mcp.client.common.autoconfigure.properties.McpClientCommonProperties; +import org.springframework.ai.mcp.client.common.autoconfigure.properties.McpSseClientProperties; +import org.springframework.ai.mcp.client.common.autoconfigure.properties.McpSseClientProperties.SseParameters; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -79,7 +80,7 @@ public class SseWebFluxTransportAutoConfiguration { * @return list of named MCP transports */ @Bean - public List webFluxClientTransports(McpSseClientProperties sseProperties, + public List sseWebFluxClientTransports(McpSseClientProperties sseProperties, ObjectProvider webClientBuilderProvider, ObjectProvider objectMapperProvider) { diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/src/main/java/org/springframework/ai/mcp/client/webflux/autoconfigure/StreamableHttpWebFluxTransportAutoConfiguration.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/src/main/java/org/springframework/ai/mcp/client/webflux/autoconfigure/StreamableHttpWebFluxTransportAutoConfiguration.java new file mode 100644 index 00000000000..81d588e928b --- /dev/null +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/src/main/java/org/springframework/ai/mcp/client/webflux/autoconfigure/StreamableHttpWebFluxTransportAutoConfiguration.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.client.webflux.autoconfigure; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.springframework.ai.mcp.client.common.autoconfigure.NamedClientMcpTransport; +import org.springframework.ai.mcp.client.common.autoconfigure.properties.McpClientCommonProperties; +import org.springframework.ai.mcp.client.common.autoconfigure.properties.McpStreamableHttpClientProperties; +import org.springframework.ai.mcp.client.common.autoconfigure.properties.McpStreamableHttpClientProperties.ConnectionParameters; +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.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.web.reactive.function.client.WebClient; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.modelcontextprotocol.client.transport.WebClientStreamableHttpTransport; + +/** + * Auto-configuration for WebFlux-based Streamable HTTP client transport in the Model + * Context Protocol (MCP). + * + *

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

+ * Key features: + *

    + *
  • Creates WebFlux-based Streamable HTTP 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 WebClientStreamableHttpTransport + * @see McpStreamableHttpClientProperties + */ +@AutoConfiguration +@ConditionalOnClass({ WebClientStreamableHttpTransport.class, WebClient.class }) +@EnableConfigurationProperties({ McpStreamableHttpClientProperties.class, McpClientCommonProperties.class }) +@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true", + matchIfMissing = true) +public class StreamableHttpWebFluxTransportAutoConfiguration { + + /** + * Creates a list of WebFlux-based Streamable HTTP 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 streamableProperties the Streamable HTTP client properties containing server + * configurations + * @param webClientBuilderProvider the provider for WebClient.Builder + * @param objectMapperProvider the provider for ObjectMapper or a new instance if not + * available + * @return list of named MCP transports + */ + @Bean + public List streamableHttpwebFluxClientTransports( + McpStreamableHttpClientProperties streamableProperties, + ObjectProvider webClientBuilderProvider, + ObjectProvider objectMapperProvider) { + + List streamableHttpTransports = new ArrayList<>(); + + var webClientBuilderTemplate = webClientBuilderProvider.getIfAvailable(WebClient::builder); + var objectMapper = objectMapperProvider.getIfAvailable(ObjectMapper::new); + + for (Map.Entry serverParameters : streamableProperties.getConnections() + .entrySet()) { + var webClientBuilder = webClientBuilderTemplate.clone().baseUrl(serverParameters.getValue().url()); + String streamableHttpEndpoint = serverParameters.getValue().endpoint() != null + ? serverParameters.getValue().endpoint() : "/mcp"; + + var transport = WebClientStreamableHttpTransport.builder(webClientBuilder) + .endpoint(streamableHttpEndpoint) + .objectMapper(objectMapper) + .build(); + + streamableHttpTransports.add(new NamedClientMcpTransport(serverParameters.getKey(), transport)); + } + + return streamableHttpTransports; + } + +} diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/java/org/springframework/ai/mcp/client/autoconfigure/aot/McpClientAutoConfigurationRuntimeHints.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/src/main/java/org/springframework/ai/mcp/client/webflux/autoconfigure/aot/McpClientAutoConfigurationRuntimeHints.java similarity index 91% rename from auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/java/org/springframework/ai/mcp/client/autoconfigure/aot/McpClientAutoConfigurationRuntimeHints.java rename to auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/src/main/java/org/springframework/ai/mcp/client/webflux/autoconfigure/aot/McpClientAutoConfigurationRuntimeHints.java index e551f57b837..a29c572087d 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/java/org/springframework/ai/mcp/client/autoconfigure/aot/McpClientAutoConfigurationRuntimeHints.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/src/main/java/org/springframework/ai/mcp/client/webflux/autoconfigure/aot/McpClientAutoConfigurationRuntimeHints.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.ai.mcp.client.autoconfigure.aot; +package org.springframework.ai.mcp.client.webflux.autoconfigure.aot; import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.RuntimeHints; @@ -33,7 +33,7 @@ public void registerHints(RuntimeHints hints, ClassLoader classLoader) { hints.resources().registerPattern("**.json"); var mcs = MemberCategory.values(); - for (var tr : findJsonAnnotatedClassesInPackage("org.springframework.ai.mcp.client.autoconfigure")) { + for (var tr : findJsonAnnotatedClassesInPackage("org.springframework.ai.mcp.client.webflux.autoconfigure")) { hints.reflection().registerType(tr, mcs); } } diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/src/main/resources/META-INF/spring/aot.factories b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/src/main/resources/META-INF/spring/aot.factories new file mode 100644 index 00000000000..2e4c3886554 --- /dev/null +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/src/main/resources/META-INF/spring/aot.factories @@ -0,0 +1,2 @@ +org.springframework.aot.hint.RuntimeHintsRegistrar=\ + org.springframework.ai.mcp.client.webflux.autoconfigure.aot.McpClientAutoConfigurationRuntimeHints diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports similarity index 57% rename from auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports rename to auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index e740e0f7de3..abc775e076b 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -13,10 +13,9 @@ # See the License for the specific language governing permissions and # limitations under the License. # -org.springframework.ai.mcp.client.autoconfigure.StdioTransportAutoConfiguration -org.springframework.ai.mcp.client.autoconfigure.SseWebFluxTransportAutoConfiguration -org.springframework.ai.mcp.client.autoconfigure.SseHttpClientTransportAutoConfiguration -org.springframework.ai.mcp.client.autoconfigure.McpClientAutoConfiguration -org.springframework.ai.mcp.client.autoconfigure.McpToolCallbackAutoConfiguration - +org.springframework.ai.mcp.client.common.autoconfigure.StdioTransportAutoConfiguration +org.springframework.ai.mcp.client.common.autoconfigure.McpClientAutoConfiguration +org.springframework.ai.mcp.client.common.autoconfigure.McpToolCallbackAutoConfiguration +org.springframework.ai.mcp.client.webflux.autoconfigure.SseWebFluxTransportAutoConfiguration +org.springframework.ai.mcp.client.webflux.autoconfigure.StreamableHttpWebFluxTransportAutoConfiguration diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/src/test/java/org/springframework/ai/mcp/client/webflux/autoconfigure/SseWebFluxTransportAutoConfigurationIT.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/src/test/java/org/springframework/ai/mcp/client/webflux/autoconfigure/SseWebFluxTransportAutoConfigurationIT.java new file mode 100644 index 00000000000..096218786f1 --- /dev/null +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/src/test/java/org/springframework/ai/mcp/client/webflux/autoconfigure/SseWebFluxTransportAutoConfigurationIT.java @@ -0,0 +1,85 @@ +/* + * Copyright 2024-2024 the original author or authors. + */ + +package org.springframework.ai.mcp.client.webflux.autoconfigure; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ai.mcp.client.common.autoconfigure.McpClientAutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; + +import io.modelcontextprotocol.client.McpSyncClient; +import io.modelcontextprotocol.spec.McpSchema.ListToolsResult; + +@Timeout(15) +public class SseWebFluxTransportAutoConfigurationIT { + + private static final Logger logger = LoggerFactory.getLogger(SseWebFluxTransportAutoConfigurationIT.class); + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withPropertyValues("spring.ai.mcp.client.initialized=false", + "spring.ai.mcp.client.sse.connections.server1.url=" + host) + .withConfiguration( + AutoConfigurations.of(McpClientAutoConfiguration.class, SseWebFluxTransportAutoConfiguration.class)); + + static String host = "http://localhost:3001"; + + // Uses the https://github.com/tzolov/mcp-everything-server-docker-image + @SuppressWarnings("resource") + static GenericContainer container = new GenericContainer<>("docker.io/tzolov/mcp-everything-server:v2") + .withCommand("node dist/index.js sse") + .withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String())) + .withExposedPorts(3001) + .waitingFor(Wait.forHttp("/").forStatusCode(404)); + + @BeforeAll + static void setUp() { + container.start(); + int port = container.getMappedPort(3001); + host = "http://" + container.getHost() + ":" + port; + logger.info("Container started at host: {}", host); + } + + @AfterAll + static void tearDown() { + container.stop(); + } + + @Test + void streamableHttpTest() { + this.contextRunner.run(context -> { + List mcpClients = (List) context.getBean("mcpSyncClients"); + + assertThat(mcpClients).isNotNull(); + assertThat(mcpClients).hasSize(1); + + McpSyncClient mcpClient = mcpClients.get(0); + + mcpClient.ping(); + + System.out.println("mcpClient = " + mcpClient.getServerInfo()); + + ListToolsResult toolsResult = mcpClient.listTools(); + + assertThat(toolsResult).isNotNull(); + assertThat(toolsResult.tools()).isNotEmpty(); + assertThat(toolsResult.tools()).hasSize(8); + + logger.info("tools = {}", toolsResult); + + }); + } + +} diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/test/java/org/springframework/ai/mcp/client/autoconfigure/SseWebFluxTransportAutoConfigurationTests.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/src/test/java/org/springframework/ai/mcp/client/webflux/autoconfigure/SseWebFluxTransportAutoConfigurationTests.java similarity index 90% rename from auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/test/java/org/springframework/ai/mcp/client/autoconfigure/SseWebFluxTransportAutoConfigurationTests.java rename to auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/src/test/java/org/springframework/ai/mcp/client/webflux/autoconfigure/SseWebFluxTransportAutoConfigurationTests.java index e1faef952b0..fbac5a2a15c 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/test/java/org/springframework/ai/mcp/client/autoconfigure/SseWebFluxTransportAutoConfigurationTests.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/src/test/java/org/springframework/ai/mcp/client/webflux/autoconfigure/SseWebFluxTransportAutoConfigurationTests.java @@ -14,15 +14,15 @@ * limitations under the License. */ -package org.springframework.ai.mcp.client.autoconfigure; +package org.springframework.ai.mcp.client.webflux.autoconfigure; + +import static org.assertj.core.api.Assertions.assertThat; import java.lang.reflect.Field; import java.util.List; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.modelcontextprotocol.client.transport.WebFluxSseClientTransport; import org.junit.jupiter.api.Test; - +import org.springframework.ai.mcp.client.common.autoconfigure.NamedClientMcpTransport; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; @@ -31,7 +31,9 @@ import org.springframework.util.ReflectionUtils; import org.springframework.web.reactive.function.client.WebClient; -import static org.assertj.core.api.Assertions.assertThat; +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.modelcontextprotocol.client.transport.WebFluxSseClientTransport; /** * Tests for {@link SseWebFluxTransportAutoConfiguration}. @@ -45,7 +47,7 @@ public class SseWebFluxTransportAutoConfigurationTests { @Test void webFluxClientTransportsPresentIfWebFluxSseClientTransportPresent() { - this.applicationContext.run(context -> assertThat(context.containsBean("webFluxClientTransports")).isTrue()); + this.applicationContext.run(context -> assertThat(context.containsBean("sseWebFluxClientTransports")).isTrue()); } @Test @@ -53,19 +55,19 @@ void webFluxClientTransportsNotPresentIfMissingWebFluxSseClientTransportNotPrese this.applicationContext .withClassLoader( new FilteredClassLoader("io.modelcontextprotocol.client.transport.WebFluxSseClientTransport")) - .run(context -> assertThat(context.containsBean("webFluxClientTransports")).isFalse()); + .run(context -> assertThat(context.containsBean("sseWebFluxClientTransports")).isFalse()); } @Test void webFluxClientTransportsNotPresentIfMcpClientDisabled() { this.applicationContext.withPropertyValues("spring.ai.mcp.client.enabled", "false") - .run(context -> assertThat(context.containsBean("webFluxClientTransports")).isFalse()); + .run(context -> assertThat(context.containsBean("sseWebFluxClientTransports")).isFalse()); } @Test void noTransportsCreatedWithEmptyConnections() { this.applicationContext.run(context -> { - List transports = context.getBean("webFluxClientTransports", List.class); + List transports = context.getBean("sseWebFluxClientTransports", List.class); assertThat(transports).isEmpty(); }); } @@ -75,7 +77,7 @@ void singleConnectionCreatesOneTransport() { this.applicationContext .withPropertyValues("spring.ai.mcp.client.sse.connections.server1.url=http://localhost:8080") .run(context -> { - List transports = context.getBean("webFluxClientTransports", List.class); + List transports = context.getBean("sseWebFluxClientTransports", List.class); assertThat(transports).hasSize(1); assertThat(transports.get(0).name()).isEqualTo("server1"); assertThat(transports.get(0).transport()).isInstanceOf(WebFluxSseClientTransport.class); @@ -88,7 +90,7 @@ void multipleConnectionsCreateMultipleTransports() { .withPropertyValues("spring.ai.mcp.client.sse.connections.server1.url=http://localhost:8080", "spring.ai.mcp.client.sse.connections.server2.url=http://otherserver:8081") .run(context -> { - List transports = context.getBean("webFluxClientTransports", List.class); + List transports = context.getBean("sseWebFluxClientTransports", List.class); assertThat(transports).hasSize(2); assertThat(transports).extracting("name").containsExactlyInAnyOrder("server1", "server2"); assertThat(transports).extracting("transport") @@ -106,7 +108,7 @@ void customSseEndpointIsRespected() { .withPropertyValues("spring.ai.mcp.client.sse.connections.server1.url=http://localhost:8080", "spring.ai.mcp.client.sse.connections.server1.sse-endpoint=/custom-sse") .run(context -> { - List transports = context.getBean("webFluxClientTransports", List.class); + List transports = context.getBean("sseWebFluxClientTransports", List.class); assertThat(transports).hasSize(1); assertThat(transports.get(0).name()).isEqualTo("server1"); assertThat(transports.get(0).transport()).isInstanceOf(WebFluxSseClientTransport.class); @@ -122,7 +124,7 @@ void customWebClientBuilderIsUsed() { .withPropertyValues("spring.ai.mcp.client.sse.connections.server1.url=http://localhost:8080") .run(context -> { assertThat(context.getBean(WebClient.Builder.class)).isNotNull(); - List transports = context.getBean("webFluxClientTransports", List.class); + List transports = context.getBean("sseWebFluxClientTransports", List.class); assertThat(transports).hasSize(1); }); } @@ -133,7 +135,7 @@ void customObjectMapperIsUsed() { .withPropertyValues("spring.ai.mcp.client.sse.connections.server1.url=http://localhost:8080") .run(context -> { assertThat(context.getBean(ObjectMapper.class)).isNotNull(); - List transports = context.getBean("webFluxClientTransports", List.class); + List transports = context.getBean("sseWebFluxClientTransports", List.class); assertThat(transports).hasSize(1); }); } @@ -143,7 +145,7 @@ void defaultSseEndpointIsUsedWhenNotSpecified() { this.applicationContext .withPropertyValues("spring.ai.mcp.client.sse.connections.server1.url=http://localhost:8080") .run(context -> { - List transports = context.getBean("webFluxClientTransports", List.class); + List transports = context.getBean("sseWebFluxClientTransports", List.class); assertThat(transports).hasSize(1); assertThat(transports.get(0).name()).isEqualTo("server1"); assertThat(transports.get(0).transport()).isInstanceOf(WebFluxSseClientTransport.class); @@ -158,7 +160,7 @@ void mixedConnectionsWithAndWithoutCustomSseEndpoint() { "spring.ai.mcp.client.sse.connections.server1.sse-endpoint=/custom-sse", "spring.ai.mcp.client.sse.connections.server2.url=http://otherserver:8081") .run(context -> { - List transports = context.getBean("webFluxClientTransports", List.class); + List transports = context.getBean("sseWebFluxClientTransports", List.class); assertThat(transports).hasSize(2); assertThat(transports).extracting("name").containsExactlyInAnyOrder("server1", "server2"); assertThat(transports).extracting("transport") diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/src/test/java/org/springframework/ai/mcp/client/webflux/autoconfigure/StreamableHttpHttpClientTransportAutoConfigurationIT.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/src/test/java/org/springframework/ai/mcp/client/webflux/autoconfigure/StreamableHttpHttpClientTransportAutoConfigurationIT.java new file mode 100644 index 00000000000..b118bcd9dc7 --- /dev/null +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/src/test/java/org/springframework/ai/mcp/client/webflux/autoconfigure/StreamableHttpHttpClientTransportAutoConfigurationIT.java @@ -0,0 +1,86 @@ +/* + * Copyright 2024-2024 the original author or authors. + */ + +package org.springframework.ai.mcp.client.webflux.autoconfigure; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ai.mcp.client.common.autoconfigure.McpClientAutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; + +import io.modelcontextprotocol.client.McpSyncClient; +import io.modelcontextprotocol.spec.McpSchema.ListToolsResult; + +@Timeout(15) +public class StreamableHttpHttpClientTransportAutoConfigurationIT { + + private static final Logger logger = LoggerFactory + .getLogger(StreamableHttpHttpClientTransportAutoConfigurationIT.class); + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withPropertyValues("spring.ai.mcp.client.initialized=false", + "spring.ai.mcp.client.streamable-http.connections.server1.url=" + host) + .withConfiguration(AutoConfigurations.of(McpClientAutoConfiguration.class, + StreamableHttpWebFluxTransportAutoConfiguration.class)); + + static String host = "http://localhost:3001"; + + // Uses the https://github.com/tzolov/mcp-everything-server-docker-image + @SuppressWarnings("resource") + static GenericContainer container = new GenericContainer<>("docker.io/tzolov/mcp-everything-server:v2") + .withCommand("node dist/index.js streamableHttp") + .withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String())) + .withExposedPorts(3001) + .waitingFor(Wait.forHttp("/").forStatusCode(404)); + + @BeforeAll + static void setUp() { + container.start(); + int port = container.getMappedPort(3001); + host = "http://" + container.getHost() + ":" + port; + logger.info("Container started at host: {}", host); + } + + @AfterAll + static void tearDown() { + container.stop(); + } + + @Test + void streamableHttpTest() { + this.contextRunner.run(context -> { + List mcpClients = (List) context.getBean("mcpSyncClients"); + + assertThat(mcpClients).isNotNull(); + assertThat(mcpClients).hasSize(1); + + McpSyncClient mcpClient = mcpClients.get(0); + + mcpClient.ping(); + + System.out.println("mcpClient = " + mcpClient.getServerInfo()); + + ListToolsResult toolsResult = mcpClient.listTools(); + + assertThat(toolsResult).isNotNull(); + assertThat(toolsResult.tools()).isNotEmpty(); + assertThat(toolsResult.tools()).hasSize(8); + + logger.info("tools = {}", toolsResult); + + }); + } + +} diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/src/test/java/org/springframework/ai/mcp/client/webflux/autoconfigure/StreamableHttpWebFluxTransportAutoConfigurationTests.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/src/test/java/org/springframework/ai/mcp/client/webflux/autoconfigure/StreamableHttpWebFluxTransportAutoConfigurationTests.java new file mode 100644 index 00000000000..170db687cd4 --- /dev/null +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/src/test/java/org/springframework/ai/mcp/client/webflux/autoconfigure/StreamableHttpWebFluxTransportAutoConfigurationTests.java @@ -0,0 +1,219 @@ +/* + * 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.client.webflux.autoconfigure; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.lang.reflect.Field; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.ai.mcp.client.common.autoconfigure.NamedClientMcpTransport; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.ReflectionUtils; +import org.springframework.web.reactive.function.client.WebClient; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.modelcontextprotocol.client.transport.WebClientStreamableHttpTransport; + +/** + * Tests for {@link StreamableHttpWebFluxTransportAutoConfiguration}. + * + * @author Christian Tzolov + */ +public class StreamableHttpWebFluxTransportAutoConfigurationTests { + + private final ApplicationContextRunner applicationContext = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(StreamableHttpWebFluxTransportAutoConfiguration.class)); + + @Test + void webFluxClientTransportsPresentIfWebClientStreamableHttpTransportPresent() { + this.applicationContext + .run(context -> assertThat(context.containsBean("streamableHttpwebFluxClientTransports")).isTrue()); + } + + @Test + void webFluxClientTransportsNotPresentIfMissingWebClientStreamableHttpTransportNotPresent() { + this.applicationContext + .withClassLoader(new FilteredClassLoader( + "io.modelcontextprotocol.client.transport.WebClientStreamableHttpTransport")) + .run(context -> assertThat(context.containsBean("streamableHttpwebFluxClientTransports")).isFalse()); + } + + @Test + void webFluxClientTransportsNotPresentIfMcpClientDisabled() { + this.applicationContext.withPropertyValues("spring.ai.mcp.client.enabled", "false") + .run(context -> assertThat(context.containsBean("streamableHttpwebFluxClientTransports")).isFalse()); + } + + @Test + void noTransportsCreatedWithEmptyConnections() { + this.applicationContext.run(context -> { + List transports = context.getBean("streamableHttpwebFluxClientTransports", + List.class); + assertThat(transports).isEmpty(); + }); + } + + @Test + void singleConnectionCreatesOneTransport() { + this.applicationContext + .withPropertyValues("spring.ai.mcp.client.streamable-http.connections.server1.url=http://localhost:8080") + .run(context -> { + List transports = context.getBean("streamableHttpwebFluxClientTransports", + List.class); + assertThat(transports).hasSize(1); + assertThat(transports.get(0).name()).isEqualTo("server1"); + assertThat(transports.get(0).transport()).isInstanceOf(WebClientStreamableHttpTransport.class); + }); + } + + @Test + void multipleConnectionsCreateMultipleTransports() { + this.applicationContext + .withPropertyValues("spring.ai.mcp.client.streamable-http.connections.server1.url=http://localhost:8080", + "spring.ai.mcp.client.streamable-http.connections.server2.url=http://otherserver:8081") + .run(context -> { + List transports = context.getBean("streamableHttpwebFluxClientTransports", + List.class); + assertThat(transports).hasSize(2); + assertThat(transports).extracting("name").containsExactlyInAnyOrder("server1", "server2"); + assertThat(transports).extracting("transport") + .allMatch(transport -> transport instanceof WebClientStreamableHttpTransport); + for (NamedClientMcpTransport transport : transports) { + assertThat(transport.transport()).isInstanceOf(WebClientStreamableHttpTransport.class); + assertThat(getStreamableHttpEndpoint((WebClientStreamableHttpTransport) transport.transport())) + .isEqualTo("/mcp"); + } + }); + } + + @Test + void customStreamableHttpEndpointIsRespected() { + this.applicationContext + .withPropertyValues("spring.ai.mcp.client.streamable-http.connections.server1.url=http://localhost:8080", + "spring.ai.mcp.client.streamable-http.connections.server1.endpoint=/custom-mcp") + .run(context -> { + List transports = context.getBean("streamableHttpwebFluxClientTransports", + List.class); + assertThat(transports).hasSize(1); + assertThat(transports.get(0).name()).isEqualTo("server1"); + assertThat(transports.get(0).transport()).isInstanceOf(WebClientStreamableHttpTransport.class); + + assertThat(getStreamableHttpEndpoint((WebClientStreamableHttpTransport) transports.get(0).transport())) + .isEqualTo("/custom-mcp"); + }); + } + + @Test + void customWebClientBuilderIsUsed() { + this.applicationContext.withUserConfiguration(CustomWebClientConfiguration.class) + .withPropertyValues("spring.ai.mcp.client.streamable-http.connections.server1.url=http://localhost:8080") + .run(context -> { + assertThat(context.getBean(WebClient.Builder.class)).isNotNull(); + List transports = context.getBean("streamableHttpwebFluxClientTransports", + List.class); + assertThat(transports).hasSize(1); + }); + } + + @Test + void customObjectMapperIsUsed() { + this.applicationContext.withUserConfiguration(CustomObjectMapperConfiguration.class) + .withPropertyValues("spring.ai.mcp.client.streamable-http.connections.server1.url=http://localhost:8080") + .run(context -> { + assertThat(context.getBean(ObjectMapper.class)).isNotNull(); + List transports = context.getBean("streamableHttpwebFluxClientTransports", + List.class); + assertThat(transports).hasSize(1); + }); + } + + @Test + void defaultStreamableHttpEndpointIsUsedWhenNotSpecified() { + this.applicationContext + .withPropertyValues("spring.ai.mcp.client.streamable-http.connections.server1.url=http://localhost:8080") + .run(context -> { + List transports = context.getBean("streamableHttpwebFluxClientTransports", + List.class); + assertThat(transports).hasSize(1); + assertThat(transports.get(0).name()).isEqualTo("server1"); + assertThat(transports.get(0).transport()).isInstanceOf(WebClientStreamableHttpTransport.class); + // Default streamable HTTP endpoint is "/mcp" as specified in the + // configuration class + }); + } + + @Test + void mixedConnectionsWithAndWithoutCustomStreamableHttpEndpoint() { + this.applicationContext + .withPropertyValues("spring.ai.mcp.client.streamable-http.connections.server1.url=http://localhost:8080", + "spring.ai.mcp.client.streamable-http.connections.server1.endpoint=/custom-mcp", + "spring.ai.mcp.client.streamable-http.connections.server2.url=http://otherserver:8081") + .run(context -> { + List transports = context.getBean("streamableHttpwebFluxClientTransports", + List.class); + assertThat(transports).hasSize(2); + assertThat(transports).extracting("name").containsExactlyInAnyOrder("server1", "server2"); + assertThat(transports).extracting("transport") + .allMatch(transport -> transport instanceof WebClientStreamableHttpTransport); + for (NamedClientMcpTransport transport : transports) { + assertThat(transport.transport()).isInstanceOf(WebClientStreamableHttpTransport.class); + if (transport.name().equals("server1")) { + assertThat(getStreamableHttpEndpoint((WebClientStreamableHttpTransport) transport.transport())) + .isEqualTo("/custom-mcp"); + } + else { + assertThat(getStreamableHttpEndpoint((WebClientStreamableHttpTransport) transport.transport())) + .isEqualTo("/mcp"); + } + } + }); + } + + private String getStreamableHttpEndpoint(WebClientStreamableHttpTransport transport) { + Field privateField = ReflectionUtils.findField(WebClientStreamableHttpTransport.class, "endpoint"); + ReflectionUtils.makeAccessible(privateField); + return (String) ReflectionUtils.getField(privateField, transport); + } + + @Configuration + static class CustomWebClientConfiguration { + + @Bean + WebClient.Builder webClientBuilder() { + return WebClient.builder().baseUrl("http://custom-base-url"); + } + + } + + @Configuration + static class CustomObjectMapperConfiguration { + + @Bean + ObjectMapper objectMapper() { + return new ObjectMapper(); + } + + } + +} diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/src/test/resources/application-test.properties b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/src/test/resources/application-test.properties new file mode 100644 index 00000000000..9107b9e407a --- /dev/null +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/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/mcp/spring-ai-autoconfigure-mcp-client-webflux/src/test/resources/nested/nested-config.json b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/src/test/resources/nested/nested-config.json new file mode 100644 index 00000000000..7cd51d6d490 --- /dev/null +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/src/test/resources/nested/nested-config.json @@ -0,0 +1,8 @@ +{ + "name": "nested-config", + "description": "Test JSON file in nested subfolder of test resources", + "version": "1.0.0", + "nestedProperties": { + "nestedProperty1": "nestedValue1" + } +} diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/src/test/resources/test-config.json b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/src/test/resources/test-config.json new file mode 100644 index 00000000000..57e2a46f20e --- /dev/null +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/src/test/resources/test-config.json @@ -0,0 +1,8 @@ +{ + "name": "test-config", + "description": "Test JSON file in root test resources folder", + "version": "1.0.0", + "properties": { + "testProperty1": "value1" + } +} diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/resources/META-INF/spring/aot.factories b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/resources/META-INF/spring/aot.factories deleted file mode 100644 index 306551e0d4e..00000000000 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client/src/main/resources/META-INF/spring/aot.factories +++ /dev/null @@ -1,2 +0,0 @@ -org.springframework.aot.hint.RuntimeHintsRegistrar=\ - org.springframework.ai.mcp.client.autoconfigure.aot.McpClientAutoConfigurationRuntimeHints diff --git a/pom.xml b/pom.xml index 931409a7584..713d2f3335b 100644 --- a/pom.xml +++ b/pom.xml @@ -114,7 +114,10 @@ auto-configurations/models/spring-ai-autoconfigure-model-zhipuai auto-configurations/models/spring-ai-autoconfigure-model-deepseek - auto-configurations/mcp/spring-ai-autoconfigure-mcp-client + auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common + auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient + auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux + auto-configurations/mcp/spring-ai-autoconfigure-mcp-server auto-configurations/vector-stores/spring-ai-autoconfigure-vector-store-azure @@ -289,6 +292,7 @@ 24.09 2.5.8 2.3.0 + 1.20.4 4.0.1 4.29.3 diff --git a/spring-ai-bom/pom.xml b/spring-ai-bom/pom.xml index 6d597e38415..b213d60005e 100644 --- a/spring-ai-bom/pom.xml +++ b/spring-ai-bom/pom.xml @@ -531,7 +531,19 @@ org.springframework.ai - spring-ai-autoconfigure-mcp-client + spring-ai-autoconfigure-mcp-client-common + ${project.version} + + + + org.springframework.ai + spring-ai-autoconfigure-mcp-client-httpclient + ${project.version} + + + + org.springframework.ai + spring-ai-autoconfigure-mcp-client-webflux ${project.version} diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-client-boot-starter-docs.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-client-boot-starter-docs.adoc index 63ab785fcd3..fd6974ad77c 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-client-boot-starter-docs.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/mcp/mcp-client-boot-starter-docs.adoc @@ -1,24 +1,19 @@ = 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 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: * Management of multiple client instances * Automatic client initialization (if enabled) -* Support for multiple named transports +* Support for multiple named transports (STDIO, Http/SSE and Streamable HTTP) * Integration with Spring AI's tool execution framework * Proper lifecycle management with automatic cleanup of resources when the application context is closed * Customizable client creation through customizers == Starters -[NOTE] -==== -There has been a significant change in the Spring AI auto-configuration, starter modules' artifact names. -Please refer to the https://docs.spring.io/spring-ai/reference/upgrade-notes.html[upgrade notes] for more information. -==== - === Standard MCP Client [source,xml] @@ -29,15 +24,15 @@ Please refer to the https://docs.spring.io/spring-ai/reference/upgrade-notes.htm
---- -The standard starter connects 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. +The standard starter connects simultaneously to one or more MCP servers over `STDIO` (in-process), `SSE` and `Streamable Http` transports. +The SSE and Streamable-Http transports use the JDK HttpClient-based transport implementation. Each connection to an MCP server creates a new MCP client instance. You can choose either `SYNC` or `ASYNC` MCP clients (note: you cannot mix sync and async clients). -For production deployment, we recommend using the WebFlux-based SSE connection with the `spring-ai-starter-mcp-client-webflux`. +For production deployment, we recommend using the WebFlux-based SSE & StreamableHttp connection with the `spring-ai-starter-mcp-client-webflux`. === WebFlux Client -The WebFlux starter provides similar functionality to the standard starter but uses a WebFlux-based SSE transport implementation. +The WebFlux starter provides similar functionality to the standard starter but uses a WebFlux-based SSE and Streamable-Http transport implementation. [source,xml] ---- @@ -209,6 +204,44 @@ spring: sse-endpoint: /custom-sse ---- +=== Streamable Http Transport Properties + +Properties for Streamable Http transport are prefixed with `spring.ai.mcp.client.streamable-http`: + +[cols="3,4,3"] +|=== +|Property |Description | Default Value + +|`connections` +|Map of named Streamable Http connection configurations +|- + +|`connections.[name].url` +|Base URL endpoint for Streamable-Http communication with the MCP server +|- + +|`connections.[name].endpoint` +|the streamable-http endpoint (as url suffix) to use for the connection +|`/mcp` +|=== + +Example configuration: +[source,yaml] +---- +spring: + ai: + mcp: + client: + streamable-http: + connections: + server1: + url: http://localhost:8080 + server2: + url: http://otherserver:8081 + endpoint: /custom-sse +---- + + == Features === Sync/Async Client Types @@ -227,15 +260,16 @@ The auto-configuration provides extensive client spec customization capabilities The following customization options are available: * *Request Configuration* - Set custom request timeouts -* link:https://spec.modelcontextprotocol.io/specification/2024-11-05/client/sampling/[*Custom Sampling Handlers*] - standardized way for servers to request LLM sampling (`completions` or `generations`) from LLMs via clients. This flow allows clients to maintain control over model access, selection, and permissions while enabling servers to leverage AI capabilities — with no server API keys necessary. -* link:https://spec.modelcontextprotocol.io/specification/2024-11-05/client/roots/[*File system (Roots) Access*] - standardized way for clients to expose filesystem `roots` to servers. +* link:https://modelcontextprotocol.io/specification/2025-06-18/client/sampling[*Custom Sampling Handlers*] - standardized way for servers to request LLM sampling (`completions` or `generations`) from LLMs via clients. This flow allows clients to maintain control over model access, selection, and permissions while enabling servers to leverage AI capabilities — with no server API keys necessary. +* link:https://modelcontextprotocol.io/specification/2025-06-18/client/roots[*File system (Roots) Access*] - standardized way for clients to expose filesystem `roots` to servers. Roots define the boundaries of where servers can operate within the filesystem, allowing them to understand which directories and files they have access to. Servers can request the list of roots from supporting clients and receive notifications when that list changes. +* link:https://modelcontextprotocol.io/specification/2025-06-18/client/elicitation[*Elicitation Handlers*] - standardized way for servers to request additional information from users through the client during interactions. * *Event Handlers* - client's handler to be notified when a certain server event occurs: - Tools change notifications - when the list of available server tools changes - Resources change notifications - when the list of available server resources changes. - Prompts change notifications - when the list of available server prompts changes. -* link:https://spec.modelcontextprotocol.io/specification/2024-11-05/server/utilities/logging/[*Logging Handlers*] - standardized way for servers to send structured log messages to clients. +* link:https://modelcontextprotocol.io/specification/2025-06-18/server/utilities/logging[*Logging Handlers*] - standardized way for servers to send structured log messages to clients. Clients can control logging verbosity by setting minimum log levels @@ -313,13 +347,14 @@ The MCP client auto-configuration automatically detects and applies any customiz The auto-configuration supports multiple transport types: -* Standard I/O (Stdio) (activated by the `spring-ai-starter-mcp-client`) -* SSE HTTP (activated by the `spring-ai-starter-mcp-client`) -* SSE WebFlux (activated by the `spring-ai-starter-mcp-client-webflux`) +* Standard I/O (Stdio) (activated by the `spring-ai-starter-mcp-client` and `spring-ai-starter-mcp-client-webflux`) +* (HttpClient) HTTP/SSE and StreamableHTTP (activated by the `spring-ai-starter-mcp-client`) +* (WebFlux) HTTP/SSE and StreamableHTTP (activated by the `spring-ai-starter-mcp-client-webflux`) === Integration with Spring AI -The starter can configure tool callbacks that integrate with Spring AI's tool execution framework, allowing MCP tools to be used as part of AI interactions. This integration is enabled by default and can be disabled by setting the `spring.ai.mcp.client.toolcallback.enabled=false` property. +The starter can configure tool callbacks that integrate with Spring AI's tool execution framework, allowing MCP tools to be used as part of AI interactions. +This integration is enabled by default and can be disabled by setting the `spring.ai.mcp.client.toolcallback.enabled=false` property. == Usage Example @@ -341,7 +376,12 @@ spring: server1: url: http://localhost:8080 server2: - url: http://otherserver:8081 + url: http://otherserver:8081 + streamable-http: + connections: + server3: + url: http://localhost:8083 + endpoint: /mcp stdio: root-change-notification: false connections: diff --git a/spring-ai-spring-boot-starters/spring-ai-starter-mcp-client-webflux/pom.xml b/spring-ai-spring-boot-starters/spring-ai-starter-mcp-client-webflux/pom.xml index 1fea2cb06fc..0e55acfd195 100644 --- a/spring-ai-spring-boot-starters/spring-ai-starter-mcp-client-webflux/pom.xml +++ b/spring-ai-spring-boot-starters/spring-ai-starter-mcp-client-webflux/pom.xml @@ -46,7 +46,7 @@ org.springframework.ai - spring-ai-autoconfigure-mcp-client + spring-ai-autoconfigure-mcp-client-webflux ${project.parent.version} diff --git a/spring-ai-spring-boot-starters/spring-ai-starter-mcp-client/pom.xml b/spring-ai-spring-boot-starters/spring-ai-starter-mcp-client/pom.xml index 5ed20a88c2e..bf91cafc43c 100644 --- a/spring-ai-spring-boot-starters/spring-ai-starter-mcp-client/pom.xml +++ b/spring-ai-spring-boot-starters/spring-ai-starter-mcp-client/pom.xml @@ -44,7 +44,7 @@ org.springframework.ai - spring-ai-autoconfigure-mcp-client + spring-ai-autoconfigure-mcp-client-httpclient ${project.parent.version}