Skip to content

Commit 13f4428

Browse files
committed
refactor: Split MCP auto-configurations into dedicated client/server modules
- Separate MCP client and server auto-configurations into dedicated modules - Add support for both sync and async MCP clients - Add support for STDIO, WebMVC and WebFlux transports - Improve configuration properties organization and validation - Add proper lifecycle management and customization support - Add comprehensive JavaDoc documentation Signed-off-by: Christian Tzolov <[email protected]>
1 parent 8b65a08 commit 13f4428

File tree

42 files changed

+1686
-354
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1686
-354
lines changed
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xmlns="http://maven.apache.org/POM/4.0.0"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
<parent>
7+
<groupId>org.springframework.ai</groupId>
8+
<artifactId>spring-ai</artifactId>
9+
<version>1.0.0-SNAPSHOT</version>
10+
<relativePath>../../pom.xml</relativePath>
11+
</parent>
12+
<artifactId>spring-ai-mcp-client-spring-boot-autoconfigure</artifactId>
13+
<packaging>jar</packaging>
14+
<name>Spring AI MCP Client Auto Configuration</name>
15+
<description>Spring AI MCP Client Auto Configuration</description>
16+
<url>https://github.com/spring-projects/spring-ai</url>
17+
18+
<scm>
19+
<url>https://github.com/spring-projects/spring-ai</url>
20+
<connection>git://github.com/spring-projects/spring-ai.git</connection>
21+
<developerConnection>[email protected]:spring-projects/spring-ai.git</developerConnection>
22+
</scm>
23+
24+
25+
<dependencies>
26+
<dependency>
27+
<groupId>org.springframework.boot</groupId>
28+
<artifactId>spring-boot-starter</artifactId>
29+
</dependency>
30+
31+
<dependency>
32+
<groupId>org.springframework.ai</groupId>
33+
<artifactId>spring-ai-mcp</artifactId>
34+
<version>${project.parent.version}</version>
35+
<optional>true</optional>
36+
</dependency>
37+
38+
<dependency>
39+
<groupId>io.modelcontextprotocol.sdk</groupId>
40+
<artifactId>mcp-spring-webflux</artifactId>
41+
<optional>true</optional>
42+
</dependency>
43+
44+
<!-- NOTE: Currently the webmvc doesn't implement client transport.
45+
We will add it in the future based on ResrtClient.
46+
-->
47+
<!-- <dependency>
48+
<groupId>io.modelcontextprotocol.sdk</groupId>
49+
<artifactId>mcp-spring-webmvc</artifactId>
50+
<optional>true</optional>
51+
</dependency> -->
52+
53+
</dependencies>
54+
55+
</project>
Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,30 +14,30 @@
1414
* limitations under the License.
1515
*/
1616

17-
package org.springframework.ai.autoconfigure.mcp.client.stdio;
17+
package org.springframework.ai.autoconfigure.mcp.client;
1818

1919
import java.util.List;
2020

2121
import io.modelcontextprotocol.client.McpClient;
2222

23-
import org.springframework.ai.mcp.McpSyncClientCustomizer;
23+
import org.springframework.ai.mcp.customizer.McpAsyncClientCustomizer;
2424

25-
public class McpSyncClientConfigurer {
25+
public class McpAsyncClientConfigurer {
2626

27-
private List<McpSyncClientCustomizer> customizers;
27+
private List<McpAsyncClientCustomizer> customizers;
2828

29-
void setCustomizers(List<McpSyncClientCustomizer> customizers) {
29+
void setCustomizers(List<McpAsyncClientCustomizer> customizers) {
3030
this.customizers = customizers;
3131
}
3232

33-
public McpClient.SyncSpec configure(String name, McpClient.SyncSpec spec) {
33+
public McpClient.AsyncSpec configure(String name, McpClient.AsyncSpec spec) {
3434
applyCustomizers(name, spec);
3535
return spec;
3636
}
3737

38-
private void applyCustomizers(String name, McpClient.SyncSpec spec) {
38+
private void applyCustomizers(String name, McpClient.AsyncSpec spec) {
3939
if (this.customizers != null) {
40-
for (McpSyncClientCustomizer customizer : this.customizers) {
40+
for (McpAsyncClientCustomizer customizer : this.customizers) {
4141
customizer.customize(name, spec);
4242
}
4343
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
/*
2+
* Copyright 2025-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.ai.autoconfigure.mcp.client;
18+
19+
import java.util.ArrayList;
20+
import java.util.List;
21+
22+
import io.modelcontextprotocol.client.McpAsyncClient;
23+
import io.modelcontextprotocol.client.McpClient;
24+
import io.modelcontextprotocol.client.McpSyncClient;
25+
import io.modelcontextprotocol.spec.McpSchema;
26+
27+
import org.springframework.ai.mcp.McpToolUtils;
28+
import org.springframework.ai.mcp.customizer.McpAsyncClientCustomizer;
29+
import org.springframework.ai.mcp.customizer.McpSyncClientCustomizer;
30+
import org.springframework.ai.tool.ToolCallback;
31+
import org.springframework.beans.factory.ObjectProvider;
32+
import org.springframework.boot.autoconfigure.AutoConfiguration;
33+
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
34+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
35+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
36+
import org.springframework.boot.context.properties.EnableConfigurationProperties;
37+
import org.springframework.context.annotation.Bean;
38+
import org.springframework.util.CollectionUtils;
39+
40+
/**
41+
* Auto-configuration for Model Context Protocol (MCP) client support.
42+
*
43+
* <p>
44+
* This configuration class sets up the necessary beans for MCP client functionality,
45+
* including both synchronous and asynchronous clients along with their respective tool
46+
* callbacks. It is automatically enabled when the required classes are present on the
47+
* classpath and can be explicitly disabled through properties.
48+
*
49+
* <p>
50+
* Configuration Properties:
51+
* <ul>
52+
* <li>{@code spring.ai.mcp.client.enabled} - Enable/disable MCP client support (default:
53+
* true)
54+
* <li>{@code spring.ai.mcp.client.type} - Client type: SYNC or ASYNC (default: SYNC)
55+
* <li>{@code spring.ai.mcp.client.name} - Client implementation name
56+
* <li>{@code spring.ai.mcp.client.version} - Client implementation version
57+
* <li>{@code spring.ai.mcp.client.request-timeout} - Request timeout duration
58+
* <li>{@code spring.ai.mcp.client.initialized} - Whether to initialize clients on
59+
* creation
60+
* </ul>
61+
*
62+
* <p>
63+
* The configuration is activated after the transport-specific auto-configurations (Stdio,
64+
* SSE HTTP, and SSE WebFlux) to ensure proper initialization order. At least one
65+
* transport must be available for the clients to be created.
66+
*
67+
* <p>
68+
* Key features:
69+
* <ul>
70+
* <li>Synchronous and Asynchronous Client Support:
71+
* <ul>
72+
* <li>Creates and configures MCP clients based on available transports
73+
* <li>Supports both blocking (sync) and non-blocking (async) operations
74+
* <li>Automatic client initialization if enabled
75+
* </ul>
76+
* <li>Integration Support:
77+
* <ul>
78+
* <li>Sets up tool callbacks for Spring AI integration
79+
* <li>Supports multiple named transports
80+
* <li>Proper lifecycle management with automatic cleanup
81+
* </ul>
82+
* <li>Customization Options:
83+
* <ul>
84+
* <li>Extensible through {@link McpSyncClientCustomizer} and
85+
* {@link McpAsyncClientCustomizer}
86+
* <li>Configurable timeouts and client information
87+
* <li>Support for custom transport implementations
88+
* </ul>
89+
* </ul>
90+
*
91+
* @see McpSyncClient
92+
* @see McpAsyncClient
93+
* @see McpClientCommonProperties
94+
* @see McpSyncClientCustomizer
95+
* @see McpAsyncClientCustomizer
96+
* @see StdioTransportAutoConfiguration
97+
* @see SseHttpClientTransportAutoConfiguration
98+
* @see SseWebFluxTransportAutoConfiguration
99+
*/
100+
@AutoConfiguration(after = { StdioTransportAutoConfiguration.class, SseHttpClientTransportAutoConfiguration.class,
101+
SseWebFluxTransportAutoConfiguration.class })
102+
@ConditionalOnClass({ McpSchema.class })
103+
@EnableConfigurationProperties(McpClientCommonProperties.class)
104+
@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
105+
matchIfMissing = true)
106+
public class McpClientAutoConfiguration {
107+
108+
/**
109+
* Creates a list of {@link McpSyncClient} instances based on the available
110+
* transports.
111+
*
112+
* <p>
113+
* Each client is configured with:
114+
* <ul>
115+
* <li>Client information (name and version) from common properties
116+
* <li>Request timeout settings
117+
* <li>Custom configurations through {@link McpSyncClientConfigurer}
118+
* </ul>
119+
*
120+
* <p>
121+
* If initialization is enabled in properties, the clients are automatically
122+
* initialized.
123+
* @param mcpSyncClientConfigurer the configurer for customizing client creation
124+
* @param commonProperties common MCP client properties
125+
* @param transportsProvider provider of named MCP transports
126+
* @return list of configured MCP sync clients
127+
*/
128+
@Bean
129+
@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "SYNC",
130+
matchIfMissing = true)
131+
public List<McpSyncClient> mcpSyncClients(McpSyncClientConfigurer mcpSyncClientConfigurer,
132+
McpClientCommonProperties commonProperties,
133+
ObjectProvider<List<NamedClientMcpTransport>> transportsProvider) {
134+
135+
List<McpSyncClient> mcpSyncClients = new ArrayList<>();
136+
137+
List<NamedClientMcpTransport> namedTransports = transportsProvider.stream().flatMap(List::stream).toList();
138+
139+
if (!CollectionUtils.isEmpty(namedTransports)) {
140+
for (NamedClientMcpTransport namedTransport : namedTransports) {
141+
142+
McpSchema.Implementation clientInfo = new McpSchema.Implementation(commonProperties.getName(),
143+
commonProperties.getVersion());
144+
145+
McpClient.SyncSpec syncSpec = McpClient.sync(namedTransport.transport())
146+
.clientInfo(clientInfo)
147+
.requestTimeout(commonProperties.getRequestTimeout());
148+
149+
syncSpec = mcpSyncClientConfigurer.configure(namedTransport.name(), syncSpec);
150+
151+
var syncClient = syncSpec.build();
152+
153+
if (commonProperties.isInitialized()) {
154+
syncClient.initialize();
155+
}
156+
157+
mcpSyncClients.add(syncClient);
158+
}
159+
}
160+
161+
return mcpSyncClients;
162+
}
163+
164+
/**
165+
* Creates tool callbacks for all configured MCP clients.
166+
*
167+
* <p>
168+
* These callbacks enable integration with Spring AI's tool execution framework,
169+
* allowing MCP tools to be used as part of AI interactions.
170+
* @param mcpClientsProvider provider of MCP sync clients
171+
* @return list of tool callbacks for MCP integration
172+
*/
173+
@Bean
174+
@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "SYNC",
175+
matchIfMissing = true)
176+
public List<ToolCallback> toolCallbacks(ObjectProvider<List<McpSyncClient>> mcpClientsProvider) {
177+
List<McpSyncClient> mcpClients = mcpClientsProvider.stream().flatMap(List::stream).toList();
178+
return McpToolUtils.getToolCallbacksFromSyncClients(mcpClients);
179+
}
180+
181+
/**
182+
* Record class that implements {@link AutoCloseable} to ensure proper cleanup of MCP
183+
* clients.
184+
*
185+
* <p>
186+
* This class is responsible for closing all MCP sync clients when the application
187+
* context is closed, preventing resource leaks.
188+
*/
189+
public record ClosebleMcpSyncClients(List<McpSyncClient> clients) implements AutoCloseable {
190+
191+
@Override
192+
public void close() {
193+
this.clients.forEach(McpSyncClient::close);
194+
}
195+
}
196+
197+
/**
198+
* Creates a closeable wrapper for MCP sync clients to ensure proper resource cleanup.
199+
* @param clients the list of MCP sync clients to manage
200+
* @return a closeable wrapper for the clients
201+
*/
202+
@Bean
203+
@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "SYNC",
204+
matchIfMissing = true)
205+
public ClosebleMcpSyncClients makeSyncClientsClosable(List<McpSyncClient> clients) {
206+
return new ClosebleMcpSyncClients(clients);
207+
}
208+
209+
/**
210+
* Creates the default {@link McpSyncClientConfigurer} if none is provided.
211+
*
212+
* <p>
213+
* This configurer aggregates all available {@link McpSyncClientCustomizer} instances
214+
* to allow for customization of MCP sync client creation.
215+
* @param customizerProvider provider of MCP sync client customizers
216+
* @return the configured MCP sync client configurer
217+
*/
218+
@Bean
219+
@ConditionalOnMissingBean
220+
@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "SYNC",
221+
matchIfMissing = true)
222+
McpSyncClientConfigurer mcpSyncClientConfigurer(ObjectProvider<McpSyncClientCustomizer> customizerProvider) {
223+
McpSyncClientConfigurer configurer = new McpSyncClientConfigurer();
224+
configurer.setCustomizers(customizerProvider.orderedStream().toList());
225+
return configurer;
226+
}
227+
228+
// Async client configuration
229+
230+
@Bean
231+
@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC")
232+
public List<McpAsyncClient> mcpAsyncClients(McpAsyncClientConfigurer mcpSyncClientConfigurer,
233+
McpClientCommonProperties commonProperties,
234+
ObjectProvider<List<NamedClientMcpTransport>> transportsProvider) {
235+
236+
List<McpAsyncClient> mcpSyncClients = new ArrayList<>();
237+
238+
List<NamedClientMcpTransport> namedTransports = transportsProvider.stream().flatMap(List::stream).toList();
239+
240+
if (!CollectionUtils.isEmpty(namedTransports)) {
241+
for (NamedClientMcpTransport namedTransport : namedTransports) {
242+
243+
McpSchema.Implementation clientInfo = new McpSchema.Implementation(commonProperties.getName(),
244+
commonProperties.getVersion());
245+
246+
McpClient.AsyncSpec syncSpec = McpClient.async(namedTransport.transport())
247+
.clientInfo(clientInfo)
248+
.requestTimeout(commonProperties.getRequestTimeout());
249+
250+
syncSpec = mcpSyncClientConfigurer.configure(namedTransport.name(), syncSpec);
251+
252+
var syncClient = syncSpec.build();
253+
254+
if (commonProperties.isInitialized()) {
255+
syncClient.initialize();
256+
}
257+
258+
mcpSyncClients.add(syncClient);
259+
}
260+
}
261+
262+
return mcpSyncClients;
263+
}
264+
265+
@Bean
266+
@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC")
267+
public List<ToolCallback> asyncToolCallbacks(ObjectProvider<List<McpAsyncClient>> mcpClientsProvider) {
268+
List<McpAsyncClient> mcpClients = mcpClientsProvider.stream().flatMap(List::stream).toList();
269+
return McpToolUtils.getToolCallbacksFromAsyncClinents(mcpClients);
270+
}
271+
272+
public record ClosebleMcpAsyncClients(List<McpAsyncClient> clients) implements AutoCloseable {
273+
@Override
274+
public void close() {
275+
this.clients.forEach(McpAsyncClient::close);
276+
}
277+
}
278+
279+
@Bean
280+
@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC")
281+
public ClosebleMcpAsyncClients makeAsynClientsClosable(List<McpAsyncClient> clients) {
282+
return new ClosebleMcpAsyncClients(clients);
283+
}
284+
285+
@Bean
286+
@ConditionalOnMissingBean
287+
@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC")
288+
McpAsyncClientConfigurer mcpAsyncClientConfigurer(ObjectProvider<McpAsyncClientCustomizer> customizerProvider) {
289+
McpAsyncClientConfigurer configurer = new McpAsyncClientConfigurer();
290+
configurer.setCustomizers(customizerProvider.orderedStream().toList());
291+
return configurer;
292+
}
293+
294+
}

0 commit comments

Comments
 (0)