Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webflux</artifactId>
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
Expand Down Expand Up @@ -73,6 +79,10 @@
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webflux</artifactId>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* 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;

import org.springframework.web.reactive.function.client.WebClient;

/**
* Factory interface for creating {@link WebClient.Builder} instances per connection name.
*
* <p>
* This factory allows customization of WebClient configuration on a per-connection basis,
* enabling fine-grained control over HTTP client settings such as timeouts, SSL
* configurations, and base URLs for each MCP server connection.
*
* <p>
* The default implementation returns a standard {@link WebClient.Builder}. Custom
* implementations can provide connection-specific configurations based on the connection
* name.
*
* @author limch02
* @since 1.0.0
*/
public interface WebClientFactory {

/**
* Creates a {@link WebClient.Builder} for the given connection name.
* <p>
* The default implementation returns a standard {@link WebClient.Builder}. Custom
* implementations can override this method to provide connection-specific
* configurations.
* @param connectionName the name of the MCP server connection
* @return a WebClient.Builder instance configured for the connection
*/
default WebClient.Builder create(String connectionName) {
return WebClient.builder();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* 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 org.springframework.ai.mcp.client.common.autoconfigure.WebClientFactory;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.web.reactive.function.client.WebClient;

/**
* Default configuration for {@link WebClientFactory}.
*
* <p>
* Provides a default implementation of {@link WebClientFactory} that returns a standard
* {@link WebClient.Builder} for all connections. This bean is only created if no custom
* {@link WebClientFactory} bean is provided.
*
* @author limch02
* @since 1.0.0
*/
@AutoConfiguration
@ConditionalOnClass(WebClient.class)
public class DefaultWebClientFactory {

/**
* Creates a default {@link WebClientFactory} implementation.
* <p>
* This factory returns a standard {@link WebClient.Builder} for all connection names.
* Custom implementations can be provided by defining a {@link WebClientFactory} bean.
* @return the default WebClientFactory instance
*/
@Bean
@ConditionalOnMissingBean(WebClientFactory.class)
public WebClientFactory webClientFactory() {
return new DefaultWebClientFactoryImpl();
}

private static class DefaultWebClientFactoryImpl implements WebClientFactory {

@Override
public WebClient.Builder create(String connectionName) {
return WebClient.builder();
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper;

import org.springframework.ai.mcp.client.common.autoconfigure.NamedClientMcpTransport;
import org.springframework.ai.mcp.client.common.autoconfigure.WebClientFactory;
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;
Expand Down Expand Up @@ -58,7 +59,7 @@
* @see WebClientStreamableHttpTransport
* @see McpStreamableHttpClientProperties
*/
@AutoConfiguration
@AutoConfiguration(after = DefaultWebClientFactory.class)
@ConditionalOnClass({ WebClientStreamableHttpTransport.class, WebClient.class })
@EnableConfigurationProperties({ McpStreamableHttpClientProperties.class, McpClientCommonProperties.class })
@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
Expand All @@ -71,31 +72,31 @@ public class StreamableHttpWebFluxTransportAutoConfiguration {
* <p>
* Each transport is configured with:
* <ul>
* <li>A cloned WebClient.Builder with server-specific base URL
* <li>A WebClient.Builder created per connection name via WebClientFactory
* <li>ObjectMapper for JSON processing
* <li>Server connection parameters from properties
* </ul>
* @param streamableProperties the Streamable HTTP client properties containing server
* configurations
* @param webClientBuilderProvider the provider for WebClient.Builder
* @param webClientFactory the factory for creating WebClient.Builder instances per
* connection name
* @param objectMapperProvider the provider for ObjectMapper or a new instance if not
* available
* @return list of named MCP transports
*/
@Bean
public List<NamedClientMcpTransport> streamableHttpWebFluxClientTransports(
McpStreamableHttpClientProperties streamableProperties,
ObjectProvider<WebClient.Builder> webClientBuilderProvider,
McpStreamableHttpClientProperties streamableProperties, WebClientFactory webClientFactory,
ObjectProvider<ObjectMapper> objectMapperProvider) {

List<NamedClientMcpTransport> streamableHttpTransports = new ArrayList<>();

var webClientBuilderTemplate = webClientBuilderProvider.getIfAvailable(WebClient::builder);
var objectMapper = objectMapperProvider.getIfAvailable(ObjectMapper::new);

for (Map.Entry<String, ConnectionParameters> serverParameters : streamableProperties.getConnections()
.entrySet()) {
var webClientBuilder = webClientBuilderTemplate.clone().baseUrl(serverParameters.getValue().url());
String connectionName = serverParameters.getKey();
var webClientBuilder = webClientFactory.create(connectionName).baseUrl(serverParameters.getValue().url());
String streamableHttpEndpoint = serverParameters.getValue().endpoint() != null
? serverParameters.getValue().endpoint() : "/mcp";

Expand All @@ -104,7 +105,7 @@ public List<NamedClientMcpTransport> streamableHttpWebFluxClientTransports(
.jsonMapper(new JacksonMcpJsonMapper(objectMapper))
.build();

streamableHttpTransports.add(new NamedClientMcpTransport(serverParameters.getKey(), transport));
streamableHttpTransports.add(new NamedClientMcpTransport(connectionName, transport));
}

return streamableHttpTransports;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
org.springframework.ai.mcp.client.webflux.autoconfigure.DefaultWebClientFactory
org.springframework.ai.mcp.client.webflux.autoconfigure.SseWebFluxTransportAutoConfiguration
org.springframework.ai.mcp.client.webflux.autoconfigure.StreamableHttpWebFluxTransportAutoConfiguration

Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.ResolvableType;

Expand All @@ -67,6 +68,8 @@ void mcpClientSupportsSampling() {
.withPropertyValues("spring.ai.mcp.client.streamable-http.connections.server1.url=http://localhost:0",
"spring.ai.mcp.client.initialized=false")
.withConfiguration(AutoConfigurations.of(
// WebClientFactory
DefaultWebClientFactory.class,
// Transport
StreamableHttpWebFluxTransportAutoConfiguration.class,
// MCP clients
Expand Down Expand Up @@ -180,16 +183,16 @@ CustomToolCallbackProvider customToolCallbackProvider() {

// Ignored by the resolver
@Bean
SyncMcpToolCallbackProvider mcpToolCallbackProvider() {
SyncMcpToolCallbackProvider mcpToolCallbackProvider(@Lazy ToolCallbackResolver resolver) {
var tcp = mock(SyncMcpToolCallbackProvider.class);
when(tcp.getToolCallbacks())
.thenThrow(new RuntimeException("mcpToolCallbackProvider#getToolCallbacks should not be called"));
return tcp;
}

// Ignored by the resolver
// This bean depends on the resolver, to ensure there are no cyclic dependencies
@Bean
CustomMcpToolCallbackProvider customMcpToolCallbackProvider() {
CustomMcpToolCallbackProvider customMcpToolCallbackProvider(@Lazy ToolCallbackResolver resolver) {
return new CustomMcpToolCallbackProvider();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public class SseWebFluxTransportAutoConfigurationIT {
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,
.withConfiguration(AutoConfigurations.of(DefaultWebClientFactory.class, McpClientAutoConfiguration.class,
McpClientAnnotationScannerAutoConfiguration.class, SseWebFluxTransportAutoConfiguration.class));

static String host = "http://localhost:3001";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public class StreamableHttpHttpClientTransportAutoConfigurationIT {
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,
.withConfiguration(AutoConfigurations.of(DefaultWebClientFactory.class, McpClientAutoConfiguration.class,
McpClientAnnotationScannerAutoConfiguration.class,
StreamableHttpWebFluxTransportAutoConfiguration.class));

Expand Down
Loading