Skip to content

Commit 952276c

Browse files
authored
Merge branch 'spring-projects:main' into main
2 parents bc3c73c + fd9f388 commit 952276c

File tree

251 files changed

+10010
-3269
lines changed

Some content is hidden

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

251 files changed

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

0 commit comments

Comments
 (0)