Skip to content

Commit f86468b

Browse files
committed
Autoconfigure MCP client with and async HTTP request customizer
Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
1 parent 1256eaf commit f86468b

File tree

4 files changed

+187
-13
lines changed

4 files changed

+187
-13
lines changed

auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/main/java/org/springframework/ai/mcp/client/httpclient/autoconfigure/SseHttpClientTransportAutoConfiguration.java

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@
2323

2424
import com.fasterxml.jackson.databind.ObjectMapper;
2525
import io.modelcontextprotocol.client.McpSyncClient;
26+
import io.modelcontextprotocol.client.transport.AsyncHttpRequestCustomizer;
2627
import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport;
28+
import io.modelcontextprotocol.client.transport.SyncHttpRequestCustomizer;
2729
import io.modelcontextprotocol.spec.McpSchema;
2830

2931
import org.springframework.ai.mcp.client.common.autoconfigure.NamedClientMcpTransport;
@@ -77,15 +79,22 @@ public class SseHttpClientTransportAutoConfiguration {
7779
* <li>A new HttpClient instance
7880
* <li>Server URL from properties
7981
* <li>ObjectMapper for JSON processing
82+
* <li>A sync or async HTTP request customizer. Sync takes precedence.
8083
* </ul>
8184
* @param sseProperties the SSE client properties containing server configurations
8285
* @param objectMapperProvider the provider for ObjectMapper or a new instance if not
8386
* available
87+
* @param syncHttpRequestCustomizer provider for {@link SyncHttpRequestCustomizer} if
88+
* available
89+
* @param asyncHttpRequestCustomizer provider fo {@link AsyncHttpRequestCustomizer} if
90+
* available
8491
* @return list of named MCP transports
8592
*/
8693
@Bean
8794
public List<NamedClientMcpTransport> sseHttpClientTransports(McpSseClientProperties sseProperties,
88-
ObjectProvider<ObjectMapper> objectMapperProvider) {
95+
ObjectProvider<ObjectMapper> objectMapperProvider,
96+
ObjectProvider<SyncHttpRequestCustomizer> syncHttpRequestCustomizer,
97+
ObjectProvider<AsyncHttpRequestCustomizer> asyncHttpRequestCustomizer) {
8998

9099
ObjectMapper objectMapper = objectMapperProvider.getIfAvailable(ObjectMapper::new);
91100

@@ -96,11 +105,15 @@ public List<NamedClientMcpTransport> sseHttpClientTransports(McpSseClientPropert
96105
String baseUrl = serverParameters.getValue().url();
97106
String sseEndpoint = serverParameters.getValue().sseEndpoint() != null
98107
? serverParameters.getValue().sseEndpoint() : "/sse";
99-
var transport = HttpClientSseClientTransport.builder(baseUrl)
108+
HttpClientSseClientTransport.Builder transportBuilder = HttpClientSseClientTransport.builder(baseUrl)
100109
.sseEndpoint(sseEndpoint)
101110
.clientBuilder(HttpClient.newBuilder())
102-
.objectMapper(objectMapper)
103-
.build();
111+
.objectMapper(objectMapper);
112+
113+
asyncHttpRequestCustomizer.ifUnique(transportBuilder::asyncHttpRequestCustomizer);
114+
syncHttpRequestCustomizer.ifUnique(transportBuilder::httpRequestCustomizer);
115+
116+
HttpClientSseClientTransport transport = transportBuilder.build();
104117
sseTransports.add(new NamedClientMcpTransport(serverParameters.getKey(), transport));
105118
}
106119

auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/main/java/org/springframework/ai/mcp/client/httpclient/autoconfigure/StreamableHttpHttpClientTransportAutoConfiguration.java

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,9 @@
3535
import com.fasterxml.jackson.databind.ObjectMapper;
3636

3737
import io.modelcontextprotocol.client.McpSyncClient;
38-
import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport;
38+
import io.modelcontextprotocol.client.transport.AsyncHttpRequestCustomizer;
3939
import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport;
40+
import io.modelcontextprotocol.client.transport.SyncHttpRequestCustomizer;
4041
import io.modelcontextprotocol.spec.McpSchema;
4142

4243
/**
@@ -59,6 +60,7 @@
5960
* connections
6061
* <li>Configures ObjectMapper for JSON serialization/deserialization
6162
* <li>Supports multiple named server connections with different URLs
63+
* <li>Adds a sync or async HTTP request customizer. Sync takes precedence.
6264
* </ul>
6365
*
6466
* @see HttpClientStreamableHttpTransport
@@ -86,11 +88,17 @@ public class StreamableHttpHttpClientTransportAutoConfiguration {
8688
* configurations
8789
* @param objectMapperProvider the provider for ObjectMapper or a new instance if not
8890
* available
91+
* @param syncHttpRequestCustomizer provider for {@link SyncHttpRequestCustomizer} if
92+
* available
93+
* @param asyncHttpRequestCustomizer provider fo {@link AsyncHttpRequestCustomizer} if
94+
* available
8995
* @return list of named MCP transports
9096
*/
9197
@Bean
9298
public List<NamedClientMcpTransport> streamableHttpHttpClientTransports(
93-
McpStreamableHttpClientProperties streamableProperties, ObjectProvider<ObjectMapper> objectMapperProvider) {
99+
McpStreamableHttpClientProperties streamableProperties, ObjectProvider<ObjectMapper> objectMapperProvider,
100+
ObjectProvider<SyncHttpRequestCustomizer> syncHttpRequestCustomizer,
101+
ObjectProvider<AsyncHttpRequestCustomizer> asyncHttpRequestCustomizer) {
94102

95103
ObjectMapper objectMapper = objectMapperProvider.getIfAvailable(ObjectMapper::new);
96104

@@ -103,11 +111,16 @@ public List<NamedClientMcpTransport> streamableHttpHttpClientTransports(
103111
String streamableHttpEndpoint = serverParameters.getValue().endpoint() != null
104112
? serverParameters.getValue().endpoint() : "/mcp";
105113

106-
HttpClientStreamableHttpTransport transport = HttpClientStreamableHttpTransport.builder(baseUrl)
114+
HttpClientStreamableHttpTransport.Builder transportBuilder = HttpClientStreamableHttpTransport
115+
.builder(baseUrl)
107116
.endpoint(streamableHttpEndpoint)
108117
.clientBuilder(HttpClient.newBuilder())
109-
.objectMapper(objectMapper)
110-
.build();
118+
.objectMapper(objectMapper);
119+
120+
asyncHttpRequestCustomizer.ifUnique(transportBuilder::asyncHttpRequestCustomizer);
121+
syncHttpRequestCustomizer.ifUnique(transportBuilder::httpRequestCustomizer);
122+
123+
HttpClientStreamableHttpTransport transport = transportBuilder.build();
111124

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

auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/test/java/org/springframework/ai/mcp/client/autoconfigure/SseHttpClientTransportAutoConfigurationIT.java

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
/*
2-
* Copyright 2024-2024 the original author or authors.
2+
* Copyright 2024-2025 the original author or authors.
33
*/
44

55
package org.springframework.ai.mcp.client.autoconfigure;
66

77
import static org.assertj.core.api.Assertions.assertThat;
8+
import static org.mockito.ArgumentMatchers.any;
9+
import static org.mockito.Mockito.atLeastOnce;
10+
import static org.mockito.Mockito.mock;
11+
import static org.mockito.Mockito.verify;
12+
import static org.mockito.Mockito.verifyNoInteractions;
13+
import static org.mockito.Mockito.when;
814

915
import java.util.List;
1016

@@ -17,12 +23,19 @@
1723
import org.springframework.ai.mcp.client.common.autoconfigure.McpClientAutoConfiguration;
1824
import org.springframework.ai.mcp.client.httpclient.autoconfigure.SseHttpClientTransportAutoConfiguration;
1925
import org.springframework.boot.autoconfigure.AutoConfigurations;
26+
import org.springframework.boot.context.annotation.UserConfigurations;
2027
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
28+
import org.springframework.context.annotation.Bean;
29+
import org.springframework.context.annotation.Configuration;
30+
2131
import org.testcontainers.containers.GenericContainer;
2232
import org.testcontainers.containers.wait.strategy.Wait;
2333

2434
import io.modelcontextprotocol.client.McpSyncClient;
35+
import io.modelcontextprotocol.client.transport.AsyncHttpRequestCustomizer;
36+
import io.modelcontextprotocol.client.transport.SyncHttpRequestCustomizer;
2537
import io.modelcontextprotocol.spec.McpSchema.ListToolsResult;
38+
import reactor.core.publisher.Mono;
2639

2740
@Timeout(15)
2841
public class SseHttpClientTransportAutoConfigurationIT {
@@ -79,8 +92,69 @@ void streamableHttpTest() {
7992
assertThat(toolsResult.tools()).hasSize(8);
8093

8194
logger.info("tools = {}", toolsResult);
82-
8395
});
8496
}
8597

98+
@Test
99+
void usesSyncRequestCustomizer() {
100+
this.contextRunner
101+
.withConfiguration(UserConfigurations.of(SyncRequestCustomizerConfiguration.class,
102+
AsyncRequestCustomizerConfiguration.class))
103+
.run(context -> {
104+
List<McpSyncClient> mcpClients = (List<McpSyncClient>) context.getBean("mcpSyncClients");
105+
106+
assertThat(mcpClients).isNotNull();
107+
assertThat(mcpClients).hasSize(1);
108+
109+
McpSyncClient mcpClient = mcpClients.get(0);
110+
111+
mcpClient.ping();
112+
113+
verify(context.getBean(SyncHttpRequestCustomizer.class), atLeastOnce()).customize(any(), any(), any(),
114+
any());
115+
verifyNoInteractions(context.getBean(AsyncHttpRequestCustomizer.class));
116+
});
117+
}
118+
119+
@Test
120+
void usesAsyncRequestCustomizer() {
121+
this.contextRunner.withConfiguration(UserConfigurations.of(AsyncRequestCustomizerConfiguration.class))
122+
.run(context -> {
123+
List<McpSyncClient> mcpClients = (List<McpSyncClient>) context.getBean("mcpSyncClients");
124+
125+
assertThat(mcpClients).isNotNull();
126+
assertThat(mcpClients).hasSize(1);
127+
128+
McpSyncClient mcpClient = mcpClients.get(0);
129+
130+
mcpClient.ping();
131+
132+
verify(context.getBean(AsyncHttpRequestCustomizer.class), atLeastOnce()).customize(any(), any(), any(),
133+
any());
134+
});
135+
}
136+
137+
@Configuration
138+
static class SyncRequestCustomizerConfiguration {
139+
140+
@Bean
141+
SyncHttpRequestCustomizer syncHttpRequestCustomizer() {
142+
return mock(SyncHttpRequestCustomizer.class);
143+
}
144+
145+
}
146+
147+
@Configuration
148+
static class AsyncRequestCustomizerConfiguration {
149+
150+
@Bean
151+
AsyncHttpRequestCustomizer asyncHttpRequestCustomizer() {
152+
AsyncHttpRequestCustomizer requestCustomizerMock = mock(AsyncHttpRequestCustomizer.class);
153+
when(requestCustomizerMock.customize(any(), any(), any(), any()))
154+
.thenAnswer(invocation -> Mono.just(invocation.getArguments()[0]));
155+
return requestCustomizerMock;
156+
}
157+
158+
}
159+
86160
}

auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-httpclient/src/test/java/org/springframework/ai/mcp/client/autoconfigure/StreamableHttpHttpClientTransportAutoConfigurationIT.java

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
/*
2-
* Copyright 2024-2024 the original author or authors.
2+
* Copyright 2024-2025 the original author or authors.
33
*/
44

55
package org.springframework.ai.mcp.client.autoconfigure;
66

77
import static org.assertj.core.api.Assertions.assertThat;
8+
import static org.mockito.ArgumentMatchers.any;
9+
import static org.mockito.Mockito.atLeastOnce;
10+
import static org.mockito.Mockito.mock;
11+
import static org.mockito.Mockito.verify;
12+
import static org.mockito.Mockito.verifyNoInteractions;
13+
import static org.mockito.Mockito.when;
814

915
import java.util.List;
1016

@@ -17,12 +23,19 @@
1723
import org.springframework.ai.mcp.client.common.autoconfigure.McpClientAutoConfiguration;
1824
import org.springframework.ai.mcp.client.httpclient.autoconfigure.StreamableHttpHttpClientTransportAutoConfiguration;
1925
import org.springframework.boot.autoconfigure.AutoConfigurations;
26+
import org.springframework.boot.context.annotation.UserConfigurations;
2027
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
28+
import org.springframework.context.annotation.Bean;
29+
import org.springframework.context.annotation.Configuration;
30+
2131
import org.testcontainers.containers.GenericContainer;
2232
import org.testcontainers.containers.wait.strategy.Wait;
2333

2434
import io.modelcontextprotocol.client.McpSyncClient;
35+
import io.modelcontextprotocol.client.transport.AsyncHttpRequestCustomizer;
36+
import io.modelcontextprotocol.client.transport.SyncHttpRequestCustomizer;
2537
import io.modelcontextprotocol.spec.McpSchema.ListToolsResult;
38+
import reactor.core.publisher.Mono;
2639

2740
@Timeout(15)
2841
public class StreamableHttpHttpClientTransportAutoConfigurationIT {
@@ -80,8 +93,69 @@ void streamableHttpTest() {
8093
assertThat(toolsResult.tools()).hasSize(8);
8194

8295
logger.info("tools = {}", toolsResult);
83-
8496
});
8597
}
8698

99+
@Test
100+
void usesSyncRequestCustomizer() {
101+
this.contextRunner
102+
.withConfiguration(UserConfigurations.of(SyncRequestCustomizerConfiguration.class,
103+
AsyncRequestCustomizerConfiguration.class))
104+
.run(context -> {
105+
List<McpSyncClient> mcpClients = (List<McpSyncClient>) context.getBean("mcpSyncClients");
106+
107+
assertThat(mcpClients).isNotNull();
108+
assertThat(mcpClients).hasSize(1);
109+
110+
McpSyncClient mcpClient = mcpClients.get(0);
111+
112+
mcpClient.ping();
113+
114+
verify(context.getBean(SyncHttpRequestCustomizer.class), atLeastOnce()).customize(any(), any(), any(),
115+
any());
116+
verifyNoInteractions(context.getBean(AsyncHttpRequestCustomizer.class));
117+
});
118+
}
119+
120+
@Test
121+
void usesAsyncRequestCustomizer() {
122+
this.contextRunner.withConfiguration(UserConfigurations.of(AsyncRequestCustomizerConfiguration.class))
123+
.run(context -> {
124+
List<McpSyncClient> mcpClients = (List<McpSyncClient>) context.getBean("mcpSyncClients");
125+
126+
assertThat(mcpClients).isNotNull();
127+
assertThat(mcpClients).hasSize(1);
128+
129+
McpSyncClient mcpClient = mcpClients.get(0);
130+
131+
mcpClient.ping();
132+
133+
verify(context.getBean(AsyncHttpRequestCustomizer.class), atLeastOnce()).customize(any(), any(), any(),
134+
any());
135+
});
136+
}
137+
138+
@Configuration
139+
static class SyncRequestCustomizerConfiguration {
140+
141+
@Bean
142+
SyncHttpRequestCustomizer syncHttpRequestCustomizer() {
143+
return mock(SyncHttpRequestCustomizer.class);
144+
}
145+
146+
}
147+
148+
@Configuration
149+
static class AsyncRequestCustomizerConfiguration {
150+
151+
@Bean
152+
AsyncHttpRequestCustomizer asyncHttpRequestCustomizer() {
153+
AsyncHttpRequestCustomizer requestCustomizerMock = mock(AsyncHttpRequestCustomizer.class);
154+
when(requestCustomizerMock.customize(any(), any(), any(), any()))
155+
.thenAnswer(invocation -> Mono.just(invocation.getArguments()[0]));
156+
return requestCustomizerMock;
157+
}
158+
159+
}
160+
87161
}

0 commit comments

Comments
 (0)