diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/main/java/org/springframework/ai/mcp/client/httpclient/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
index 1f713e75e88..6d695a468d7 100644
--- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/main/java/org/springframework/ai/mcp/client/httpclient/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
@@ -23,7 +23,9 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.client.McpSyncClient;
+import io.modelcontextprotocol.client.transport.AsyncHttpRequestCustomizer;
import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport;
+import io.modelcontextprotocol.client.transport.SyncHttpRequestCustomizer;
import io.modelcontextprotocol.spec.McpSchema;
import org.springframework.ai.mcp.client.common.autoconfigure.NamedClientMcpTransport;
@@ -36,6 +38,7 @@
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
+import org.springframework.core.log.LogAccessor;
/**
* Auto-configuration for Server-Sent Events (SSE) HTTP client transport in the Model
@@ -68,6 +71,8 @@
matchIfMissing = true)
public class SseHttpClientTransportAutoConfiguration {
+ private static final LogAccessor logger = new LogAccessor(SseHttpClientTransportAutoConfiguration.class);
+
/**
* Creates a list of HTTP client-based SSE transports for MCP communication.
*
@@ -77,15 +82,22 @@ public class SseHttpClientTransportAutoConfiguration {
*
A new HttpClient instance
* Server URL from properties
* ObjectMapper for JSON processing
+ * A sync or async HTTP request customizer. Sync takes precedence.
*
* @param sseProperties the SSE client properties containing server configurations
* @param objectMapperProvider the provider for ObjectMapper or a new instance if not
* available
+ * @param syncHttpRequestCustomizer provider for {@link SyncHttpRequestCustomizer} if
+ * available
+ * @param asyncHttpRequestCustomizer provider fo {@link AsyncHttpRequestCustomizer} if
+ * available
* @return list of named MCP transports
*/
@Bean
public List sseHttpClientTransports(McpSseClientProperties sseProperties,
- ObjectProvider objectMapperProvider) {
+ ObjectProvider objectMapperProvider,
+ ObjectProvider syncHttpRequestCustomizer,
+ ObjectProvider asyncHttpRequestCustomizer) {
ObjectMapper objectMapper = objectMapperProvider.getIfAvailable(ObjectMapper::new);
@@ -96,11 +108,21 @@ public List sseHttpClientTransports(McpSseClientPropert
String baseUrl = serverParameters.getValue().url();
String sseEndpoint = serverParameters.getValue().sseEndpoint() != null
? serverParameters.getValue().sseEndpoint() : "/sse";
- var transport = HttpClientSseClientTransport.builder(baseUrl)
+ HttpClientSseClientTransport.Builder transportBuilder = HttpClientSseClientTransport.builder(baseUrl)
.sseEndpoint(sseEndpoint)
.clientBuilder(HttpClient.newBuilder())
- .objectMapper(objectMapper)
- .build();
+ .objectMapper(objectMapper);
+
+ asyncHttpRequestCustomizer.ifUnique(transportBuilder::asyncHttpRequestCustomizer);
+ syncHttpRequestCustomizer.ifUnique(transportBuilder::httpRequestCustomizer);
+ if (asyncHttpRequestCustomizer.getIfUnique() != null && syncHttpRequestCustomizer.getIfUnique() != null) {
+ logger.warn("Found beans of type %s and %s. Using %s.".formatted(
+ AsyncHttpRequestCustomizer.class.getSimpleName(),
+ SyncHttpRequestCustomizer.class.getSimpleName(),
+ SyncHttpRequestCustomizer.class.getSimpleName()));
+ }
+
+ HttpClientSseClientTransport transport = transportBuilder.build();
sseTransports.add(new NamedClientMcpTransport(serverParameters.getKey(), transport));
}
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
index f18066553f8..93f07b617fe 100644
--- 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
@@ -31,12 +31,14 @@
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
+import org.springframework.core.log.LogAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.client.McpSyncClient;
-import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport;
+import io.modelcontextprotocol.client.transport.AsyncHttpRequestCustomizer;
import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport;
+import io.modelcontextprotocol.client.transport.SyncHttpRequestCustomizer;
import io.modelcontextprotocol.spec.McpSchema;
/**
@@ -59,6 +61,7 @@
* connections
* Configures ObjectMapper for JSON serialization/deserialization
* Supports multiple named server connections with different URLs
+ * Adds a sync or async HTTP request customizer. Sync takes precedence.
*
*
* @see HttpClientStreamableHttpTransport
@@ -71,6 +74,8 @@
matchIfMissing = true)
public class StreamableHttpHttpClientTransportAutoConfiguration {
+ private static final LogAccessor logger = new LogAccessor(StreamableHttpHttpClientTransportAutoConfiguration.class);
+
/**
* Creates a list of HTTP client-based Streamable HTTP transports for MCP
* communication.
@@ -86,11 +91,17 @@ public class StreamableHttpHttpClientTransportAutoConfiguration {
* configurations
* @param objectMapperProvider the provider for ObjectMapper or a new instance if not
* available
+ * @param syncHttpRequestCustomizer provider for {@link SyncHttpRequestCustomizer} if
+ * available
+ * @param asyncHttpRequestCustomizer provider fo {@link AsyncHttpRequestCustomizer} if
+ * available
* @return list of named MCP transports
*/
@Bean
public List streamableHttpHttpClientTransports(
- McpStreamableHttpClientProperties streamableProperties, ObjectProvider objectMapperProvider) {
+ McpStreamableHttpClientProperties streamableProperties, ObjectProvider objectMapperProvider,
+ ObjectProvider syncHttpRequestCustomizer,
+ ObjectProvider asyncHttpRequestCustomizer) {
ObjectMapper objectMapper = objectMapperProvider.getIfAvailable(ObjectMapper::new);
@@ -103,11 +114,22 @@ public List streamableHttpHttpClientTransports(
String streamableHttpEndpoint = serverParameters.getValue().endpoint() != null
? serverParameters.getValue().endpoint() : "/mcp";
- HttpClientStreamableHttpTransport transport = HttpClientStreamableHttpTransport.builder(baseUrl)
+ HttpClientStreamableHttpTransport.Builder transportBuilder = HttpClientStreamableHttpTransport
+ .builder(baseUrl)
.endpoint(streamableHttpEndpoint)
.clientBuilder(HttpClient.newBuilder())
- .objectMapper(objectMapper)
- .build();
+ .objectMapper(objectMapper);
+
+ asyncHttpRequestCustomizer.ifUnique(transportBuilder::asyncHttpRequestCustomizer);
+ syncHttpRequestCustomizer.ifUnique(transportBuilder::httpRequestCustomizer);
+ if (asyncHttpRequestCustomizer.getIfUnique() != null && syncHttpRequestCustomizer.getIfUnique() != null) {
+ logger.warn("Found beans of type %s and %s. Using %s.".formatted(
+ AsyncHttpRequestCustomizer.class.getSimpleName(),
+ SyncHttpRequestCustomizer.class.getSimpleName(),
+ SyncHttpRequestCustomizer.class.getSimpleName()));
+ }
+
+ HttpClientStreamableHttpTransport transport = transportBuilder.build();
streamableHttpTransports.add(new NamedClientMcpTransport(serverParameters.getKey(), transport));
}
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
index 70b9b2563a4..a8872f5fe76 100644
--- 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
@@ -1,10 +1,16 @@
/*
- * Copyright 2024-2024 the original author or authors.
+ * Copyright 2024-2025 the original author or authors.
*/
package org.springframework.ai.mcp.client.autoconfigure;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
import java.util.List;
@@ -17,12 +23,19 @@
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.context.annotation.UserConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.wait.strategy.Wait;
import io.modelcontextprotocol.client.McpSyncClient;
+import io.modelcontextprotocol.client.transport.AsyncHttpRequestCustomizer;
+import io.modelcontextprotocol.client.transport.SyncHttpRequestCustomizer;
import io.modelcontextprotocol.spec.McpSchema.ListToolsResult;
+import reactor.core.publisher.Mono;
@Timeout(15)
public class SseHttpClientTransportAutoConfigurationIT {
@@ -79,8 +92,69 @@ void streamableHttpTest() {
assertThat(toolsResult.tools()).hasSize(8);
logger.info("tools = {}", toolsResult);
-
});
}
+ @Test
+ void usesSyncRequestCustomizer() {
+ this.contextRunner
+ .withConfiguration(UserConfigurations.of(SyncRequestCustomizerConfiguration.class,
+ AsyncRequestCustomizerConfiguration.class))
+ .run(context -> {
+ List mcpClients = (List) context.getBean("mcpSyncClients");
+
+ assertThat(mcpClients).isNotNull();
+ assertThat(mcpClients).hasSize(1);
+
+ McpSyncClient mcpClient = mcpClients.get(0);
+
+ mcpClient.ping();
+
+ verify(context.getBean(SyncHttpRequestCustomizer.class), atLeastOnce()).customize(any(), any(), any(),
+ any());
+ verifyNoInteractions(context.getBean(AsyncHttpRequestCustomizer.class));
+ });
+ }
+
+ @Test
+ void usesAsyncRequestCustomizer() {
+ this.contextRunner.withConfiguration(UserConfigurations.of(AsyncRequestCustomizerConfiguration.class))
+ .run(context -> {
+ List mcpClients = (List) context.getBean("mcpSyncClients");
+
+ assertThat(mcpClients).isNotNull();
+ assertThat(mcpClients).hasSize(1);
+
+ McpSyncClient mcpClient = mcpClients.get(0);
+
+ mcpClient.ping();
+
+ verify(context.getBean(AsyncHttpRequestCustomizer.class), atLeastOnce()).customize(any(), any(), any(),
+ any());
+ });
+ }
+
+ @Configuration
+ static class SyncRequestCustomizerConfiguration {
+
+ @Bean
+ SyncHttpRequestCustomizer syncHttpRequestCustomizer() {
+ return mock(SyncHttpRequestCustomizer.class);
+ }
+
+ }
+
+ @Configuration
+ static class AsyncRequestCustomizerConfiguration {
+
+ @Bean
+ AsyncHttpRequestCustomizer asyncHttpRequestCustomizer() {
+ AsyncHttpRequestCustomizer requestCustomizerMock = mock(AsyncHttpRequestCustomizer.class);
+ when(requestCustomizerMock.customize(any(), any(), any(), any()))
+ .thenAnswer(invocation -> Mono.just(invocation.getArguments()[0]));
+ return requestCustomizerMock;
+ }
+
+ }
+
}
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
index 452373a711f..36a420272f9 100644
--- 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
@@ -1,10 +1,16 @@
/*
- * Copyright 2024-2024 the original author or authors.
+ * Copyright 2024-2025 the original author or authors.
*/
package org.springframework.ai.mcp.client.autoconfigure;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
import java.util.List;
@@ -17,12 +23,19 @@
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.context.annotation.UserConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.wait.strategy.Wait;
import io.modelcontextprotocol.client.McpSyncClient;
+import io.modelcontextprotocol.client.transport.AsyncHttpRequestCustomizer;
+import io.modelcontextprotocol.client.transport.SyncHttpRequestCustomizer;
import io.modelcontextprotocol.spec.McpSchema.ListToolsResult;
+import reactor.core.publisher.Mono;
@Timeout(15)
public class StreamableHttpHttpClientTransportAutoConfigurationIT {
@@ -80,8 +93,69 @@ void streamableHttpTest() {
assertThat(toolsResult.tools()).hasSize(8);
logger.info("tools = {}", toolsResult);
-
});
}
+ @Test
+ void usesSyncRequestCustomizer() {
+ this.contextRunner
+ .withConfiguration(UserConfigurations.of(SyncRequestCustomizerConfiguration.class,
+ AsyncRequestCustomizerConfiguration.class))
+ .run(context -> {
+ List mcpClients = (List) context.getBean("mcpSyncClients");
+
+ assertThat(mcpClients).isNotNull();
+ assertThat(mcpClients).hasSize(1);
+
+ McpSyncClient mcpClient = mcpClients.get(0);
+
+ mcpClient.ping();
+
+ verify(context.getBean(SyncHttpRequestCustomizer.class), atLeastOnce()).customize(any(), any(), any(),
+ any());
+ verifyNoInteractions(context.getBean(AsyncHttpRequestCustomizer.class));
+ });
+ }
+
+ @Test
+ void usesAsyncRequestCustomizer() {
+ this.contextRunner.withConfiguration(UserConfigurations.of(AsyncRequestCustomizerConfiguration.class))
+ .run(context -> {
+ List mcpClients = (List) context.getBean("mcpSyncClients");
+
+ assertThat(mcpClients).isNotNull();
+ assertThat(mcpClients).hasSize(1);
+
+ McpSyncClient mcpClient = mcpClients.get(0);
+
+ mcpClient.ping();
+
+ verify(context.getBean(AsyncHttpRequestCustomizer.class), atLeastOnce()).customize(any(), any(), any(),
+ any());
+ });
+ }
+
+ @Configuration
+ static class SyncRequestCustomizerConfiguration {
+
+ @Bean
+ SyncHttpRequestCustomizer syncHttpRequestCustomizer() {
+ return mock(SyncHttpRequestCustomizer.class);
+ }
+
+ }
+
+ @Configuration
+ static class AsyncRequestCustomizerConfiguration {
+
+ @Bean
+ AsyncHttpRequestCustomizer asyncHttpRequestCustomizer() {
+ AsyncHttpRequestCustomizer requestCustomizerMock = mock(AsyncHttpRequestCustomizer.class);
+ when(requestCustomizerMock.customize(any(), any(), any(), any()))
+ .thenAnswer(invocation -> Mono.just(invocation.getArguments()[0]));
+ return requestCustomizerMock;
+ }
+
+ }
+
}