Skip to content

Commit fe377ee

Browse files
tzolovmarkpollack
authored andcommitted
Refactor: MCP Autoconfig Modularization
Core Architecture Changes: - Split MCP into dedicated client/server modules - Created separate starters: spring-ai-starter-mcp-webmvc and spring-ai-starter-mcp-webflux - Removed property-based transport configuration in favor of auto-configuration - Added support for multiple transport types (STDIO, WebMVC, WebFlux) Client Improvements: - Added support for both synchronous and asynchronous MCP clients - Fixed client auto-configuration issues - Added root change notification property to common properties Configuration Enhancements: - Improved configuration properties organization and validation - Added ConditionalOnMissingBean for WebMvc/WebFlux configurations - Enhanced lifecycle management and customization support Testing and Documentation: - Added comprehensive integration tests for McpClientAutoConfiguration - Updated McpServerAutoConfigurationIT - Added extensive JavaDoc documentation - Improved MCP client/server starter documentation - Added documentation for common utilities - Updated navigation for new MCP documentation sections Signed-off-by: Christian Tzolov <[email protected]>
1 parent 015662a commit fe377ee

File tree

62 files changed

+5129
-655
lines changed

Some content is hidden

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

62 files changed

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

0 commit comments

Comments
 (0)