Skip to content

Commit cd25a07

Browse files
Merge branch 'spring-projects:main' into patch-6
2 parents 8064889 + e0137c2 commit cd25a07

File tree

285 files changed

+9426
-2437
lines changed

Some content is hidden

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

285 files changed

+9426
-2437
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,4 @@ CLAUDE.md
5050
qodana.yaml
5151
__pycache__/
5252
*.pyc
53+
tmp

CONTRIBUTING.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ When issuing pull requests, please ensure that your commit history is linear.
110110
From the command line you can check this using:
111111

112112
----
113-
log --graph --pretty=oneline
113+
git log --graph --pretty=oneline
114114
----
115115

116116
As this may cause lots of typing, we recommend creating a global alias, e.g. `git logg` for this:

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ Please refer to the [Getting Started Guide](https://docs.spring.io/spring-ai/ref
4848
<!-- * [Discussions](https://github.com/spring-projects/spring-ai/discussions) - Go here if you have a question, suggestion, or feedback! -->
4949
* [Awesome Spring AI](https://github.com/spring-ai-community/awesome-spring-ai) - A curated list of awesome resources, tools, tutorials, and projects for building generative AI applications using Spring AI
5050
* [Spring AI Examples](https://github.com/spring-projects/spring-ai-examples) contains example projects that explain specific features in more detail.
51+
* [Spring AI Community](https://github.com/spring-ai-community) - A community-driven organization for building Spring-based integrations with AI models, agents, vector databases, and more.
5152

5253
## Breaking changes
5354

@@ -149,3 +150,7 @@ The wiki pages
149150
[Code Style](https://github.com/spring-projects/spring-framework/wiki/Code-Style) and
150151
[IntelliJ IDEA Editor Settings](https://github.com/spring-projects/spring-framework/wiki/IntelliJ-IDEA-Editor-Settings)
151152
define the source file coding standards we use along with some IDEA editor settings we customize.
153+
154+
## Contributing
155+
156+
Your contributions are always welcome! Please read the [contribution guidelines](CONTRIBUTING.adoc) first.

advisors/spring-ai-advisors-vector-store/src/test/java/org/springframework/ai/chat/client/advisor/vectorstore/VectorStoreChatMemoryAdvisorTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@
1818

1919
import org.junit.jupiter.api.Test;
2020
import org.mockito.Mockito;
21+
import reactor.core.scheduler.Scheduler;
2122

2223
import org.springframework.ai.chat.prompt.PromptTemplate;
2324
import org.springframework.ai.vectorstore.VectorStore;
24-
import reactor.core.scheduler.Scheduler;
2525

2626
import static org.assertj.core.api.Assertions.assertThat;
2727
import static org.assertj.core.api.Assertions.assertThatThrownBy;

auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/pom.xml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,12 @@
3535
<optional>true</optional>
3636
</dependency>
3737

38-
<!-- <dependency>
39-
<groupId>io.modelcontextprotocol.sdk</groupId>
40-
<artifactId>mcp-spring-webflux</artifactId>
38+
<dependency>
39+
<groupId>org.springframework.ai</groupId>
40+
<artifactId>spring-ai-mcp-annotations</artifactId>
41+
<version>${project.parent.version}</version>
4142
<optional>true</optional>
42-
</dependency> -->
43+
</dependency>
4344

4445
<dependency>
4546
<groupId>org.springframework.boot</groupId>

auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/McpClientAutoConfiguration.java

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,23 @@
2323
import io.modelcontextprotocol.client.McpClient;
2424
import io.modelcontextprotocol.client.McpSyncClient;
2525
import io.modelcontextprotocol.spec.McpSchema;
26-
26+
import org.springaicommunity.mcp.method.changed.prompt.AsyncPromptListChangedSpecification;
27+
import org.springaicommunity.mcp.method.changed.prompt.SyncPromptListChangedSpecification;
28+
import org.springaicommunity.mcp.method.changed.resource.AsyncResourceListChangedSpecification;
29+
import org.springaicommunity.mcp.method.changed.resource.SyncResourceListChangedSpecification;
30+
import org.springaicommunity.mcp.method.changed.tool.AsyncToolListChangedSpecification;
31+
import org.springaicommunity.mcp.method.changed.tool.SyncToolListChangedSpecification;
32+
import org.springaicommunity.mcp.method.elicitation.AsyncElicitationSpecification;
33+
import org.springaicommunity.mcp.method.elicitation.SyncElicitationSpecification;
34+
import org.springaicommunity.mcp.method.logging.AsyncLoggingSpecification;
35+
import org.springaicommunity.mcp.method.logging.SyncLoggingSpecification;
36+
import org.springaicommunity.mcp.method.progress.AsyncProgressSpecification;
37+
import org.springaicommunity.mcp.method.progress.SyncProgressSpecification;
38+
import org.springaicommunity.mcp.method.sampling.AsyncSamplingSpecification;
39+
import org.springaicommunity.mcp.method.sampling.SyncSamplingSpecification;
40+
41+
import org.springframework.ai.mcp.client.common.autoconfigure.annotations.McpAsyncAnnotationCustomizer;
42+
import org.springframework.ai.mcp.client.common.autoconfigure.annotations.McpSyncAnnotationCustomizer;
2743
import org.springframework.ai.mcp.client.common.autoconfigure.configurer.McpAsyncClientConfigurer;
2844
import org.springframework.ai.mcp.client.common.autoconfigure.configurer.McpSyncClientConfigurer;
2945
import org.springframework.ai.mcp.client.common.autoconfigure.properties.McpClientCommonProperties;
@@ -95,8 +111,6 @@
95111
* @see McpSyncClientCustomizer
96112
* @see McpAsyncClientCustomizer
97113
* @see StdioTransportAutoConfiguration
98-
* @see SseHttpClientTransportAutoConfiguration
99-
* @see SseWebFluxTransportAutoConfiguration
100114
*/
101115
@AutoConfiguration(afterName = {
102116
"org.springframework.ai.mcp.client.common.autoconfigure.StdioTransportAutoConfiguration",
@@ -208,6 +222,20 @@ McpSyncClientConfigurer mcpSyncClientConfigurer(ObjectProvider<McpSyncClientCust
208222
return new McpSyncClientConfigurer(customizerProvider.orderedStream().toList());
209223
}
210224

225+
@Bean
226+
@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "SYNC",
227+
matchIfMissing = true)
228+
public McpSyncClientCustomizer mcpAnnotationMcpSyncClientCustomizer(List<SyncLoggingSpecification> loggingSpecs,
229+
List<SyncSamplingSpecification> samplingSpecs, List<SyncElicitationSpecification> elicitationSpecs,
230+
List<SyncProgressSpecification> progressSpecs,
231+
List<SyncToolListChangedSpecification> syncToolListChangedSpecifications,
232+
List<SyncResourceListChangedSpecification> syncResourceListChangedSpecifications,
233+
List<SyncPromptListChangedSpecification> syncPromptListChangedSpecifications) {
234+
return new McpSyncAnnotationCustomizer(samplingSpecs, loggingSpecs, elicitationSpecs, progressSpecs,
235+
syncToolListChangedSpecifications, syncResourceListChangedSpecifications,
236+
syncPromptListChangedSpecifications);
237+
}
238+
211239
// Async client configuration
212240

213241
@Bean
@@ -259,6 +287,18 @@ McpAsyncClientConfigurer mcpAsyncClientConfigurer(ObjectProvider<McpAsyncClientC
259287
return new McpAsyncClientConfigurer(customizerProvider.orderedStream().toList());
260288
}
261289

290+
@Bean
291+
@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC")
292+
public McpAsyncClientCustomizer mcpAnnotationMcpAsyncClientCustomizer(List<AsyncLoggingSpecification> loggingSpecs,
293+
List<AsyncSamplingSpecification> samplingSpecs, List<AsyncElicitationSpecification> elicitationSpecs,
294+
List<AsyncProgressSpecification> progressSpecs,
295+
List<AsyncToolListChangedSpecification> toolListChangedSpecs,
296+
List<AsyncResourceListChangedSpecification> resourceListChangedSpecs,
297+
List<AsyncPromptListChangedSpecification> promptListChangedSpecs) {
298+
return new McpAsyncAnnotationCustomizer(samplingSpecs, loggingSpecs, elicitationSpecs, progressSpecs,
299+
toolListChangedSpecs, resourceListChangedSpecs, promptListChangedSpecs);
300+
}
301+
262302
/**
263303
* Record class that implements {@link AutoCloseable} to ensure proper cleanup of MCP
264304
* clients.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
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.mcp.client.common.autoconfigure.annotations;
18+
19+
import java.util.List;
20+
import java.util.Map;
21+
import java.util.concurrent.ConcurrentHashMap;
22+
import java.util.stream.Stream;
23+
24+
import io.modelcontextprotocol.client.McpClient.AsyncSpec;
25+
import org.slf4j.Logger;
26+
import org.slf4j.LoggerFactory;
27+
import org.springaicommunity.mcp.method.changed.prompt.AsyncPromptListChangedSpecification;
28+
import org.springaicommunity.mcp.method.changed.resource.AsyncResourceListChangedSpecification;
29+
import org.springaicommunity.mcp.method.changed.tool.AsyncToolListChangedSpecification;
30+
import org.springaicommunity.mcp.method.elicitation.AsyncElicitationSpecification;
31+
import org.springaicommunity.mcp.method.logging.AsyncLoggingSpecification;
32+
import org.springaicommunity.mcp.method.progress.AsyncProgressSpecification;
33+
import org.springaicommunity.mcp.method.sampling.AsyncSamplingSpecification;
34+
35+
import org.springframework.ai.mcp.customizer.McpAsyncClientCustomizer;
36+
import org.springframework.util.CollectionUtils;
37+
38+
/**
39+
* @author Christian Tzolov
40+
*/
41+
public class McpAsyncAnnotationCustomizer implements McpAsyncClientCustomizer {
42+
43+
private static final Logger logger = LoggerFactory.getLogger(McpAsyncAnnotationCustomizer.class);
44+
45+
private final List<AsyncSamplingSpecification> asyncSamplingSpecifications;
46+
47+
private final List<AsyncLoggingSpecification> asyncLoggingSpecifications;
48+
49+
private final List<AsyncElicitationSpecification> asyncElicitationSpecifications;
50+
51+
private final List<AsyncProgressSpecification> asyncProgressSpecifications;
52+
53+
private final List<AsyncToolListChangedSpecification> asyncToolListChangedSpecifications;
54+
55+
private final List<AsyncResourceListChangedSpecification> asyncResourceListChangedSpecifications;
56+
57+
private final List<AsyncPromptListChangedSpecification> asyncPromptListChangedSpecifications;
58+
59+
// Tracking registered specifications per client
60+
private final Map<String, Boolean> clientElicitationSpecs = new ConcurrentHashMap<>();
61+
62+
private final Map<String, Boolean> clientSamplingSpecs = new ConcurrentHashMap<>();
63+
64+
public McpAsyncAnnotationCustomizer(List<AsyncSamplingSpecification> asyncSamplingSpecifications,
65+
List<AsyncLoggingSpecification> asyncLoggingSpecifications,
66+
List<AsyncElicitationSpecification> asyncElicitationSpecifications,
67+
List<AsyncProgressSpecification> asyncProgressSpecifications,
68+
List<AsyncToolListChangedSpecification> asyncToolListChangedSpecifications,
69+
List<AsyncResourceListChangedSpecification> asyncResourceListChangedSpecifications,
70+
List<AsyncPromptListChangedSpecification> asyncPromptListChangedSpecifications) {
71+
72+
this.asyncSamplingSpecifications = asyncSamplingSpecifications;
73+
this.asyncLoggingSpecifications = asyncLoggingSpecifications;
74+
this.asyncElicitationSpecifications = asyncElicitationSpecifications;
75+
this.asyncProgressSpecifications = asyncProgressSpecifications;
76+
this.asyncToolListChangedSpecifications = asyncToolListChangedSpecifications;
77+
this.asyncResourceListChangedSpecifications = asyncResourceListChangedSpecifications;
78+
this.asyncPromptListChangedSpecifications = asyncPromptListChangedSpecifications;
79+
}
80+
81+
@Override
82+
public void customize(String name, AsyncSpec clientSpec) {
83+
84+
if (!CollectionUtils.isEmpty(this.asyncElicitationSpecifications)) {
85+
this.asyncElicitationSpecifications.forEach(elicitationSpec -> {
86+
Stream.of(elicitationSpec.clients()).forEach(clientId -> {
87+
if (clientId.equalsIgnoreCase(name)) {
88+
89+
// Check if client already has an elicitation spec
90+
if (this.clientElicitationSpecs.containsKey(name)) {
91+
throw new IllegalArgumentException("Client '" + name
92+
+ "' already has an elicitationSpec registered. Only one elicitationSpec is allowed per client.");
93+
}
94+
95+
this.clientElicitationSpecs.put(name, Boolean.TRUE);
96+
clientSpec.elicitation(elicitationSpec.elicitationHandler());
97+
98+
logger.info("Registered elicitationSpec for client '{}'.", name);
99+
100+
}
101+
});
102+
});
103+
}
104+
105+
if (!CollectionUtils.isEmpty(this.asyncSamplingSpecifications)) {
106+
this.asyncSamplingSpecifications.forEach(samplingSpec -> {
107+
Stream.of(samplingSpec.clients()).forEach(clientId -> {
108+
if (clientId.equalsIgnoreCase(name)) {
109+
110+
// Check if client already has a sampling spec
111+
if (this.clientSamplingSpecs.containsKey(name)) {
112+
throw new IllegalArgumentException("Client '" + name
113+
+ "' already has a samplingSpec registered. Only one samplingSpec is allowed per client.");
114+
}
115+
this.clientSamplingSpecs.put(name, Boolean.TRUE);
116+
117+
clientSpec.sampling(samplingSpec.samplingHandler());
118+
119+
logger.info("Registered samplingSpec for client '{}'.", name);
120+
}
121+
});
122+
});
123+
}
124+
125+
if (!CollectionUtils.isEmpty(this.asyncLoggingSpecifications)) {
126+
this.asyncLoggingSpecifications.forEach(loggingSpec -> {
127+
Stream.of(loggingSpec.clients()).forEach(clientId -> {
128+
if (clientId.equalsIgnoreCase(name)) {
129+
clientSpec.loggingConsumer(loggingSpec.loggingHandler());
130+
logger.info("Registered loggingSpec for client '{}'.", name);
131+
}
132+
});
133+
});
134+
}
135+
136+
if (!CollectionUtils.isEmpty(this.asyncProgressSpecifications)) {
137+
this.asyncProgressSpecifications.forEach(progressSpec -> {
138+
Stream.of(progressSpec.clients()).forEach(clientId -> {
139+
if (clientId.equalsIgnoreCase(name)) {
140+
clientSpec.progressConsumer(progressSpec.progressHandler());
141+
logger.info("Registered progressSpec for client '{}'.", name);
142+
}
143+
});
144+
});
145+
}
146+
147+
if (!CollectionUtils.isEmpty(this.asyncToolListChangedSpecifications)) {
148+
this.asyncToolListChangedSpecifications.forEach(toolListChangedSpec -> {
149+
Stream.of(toolListChangedSpec.clients()).forEach(clientId -> {
150+
if (clientId.equalsIgnoreCase(name)) {
151+
clientSpec.toolsChangeConsumer(toolListChangedSpec.toolListChangeHandler());
152+
logger.info("Registered toolListChangedSpec for client '{}'.", name);
153+
}
154+
});
155+
});
156+
}
157+
158+
if (!CollectionUtils.isEmpty(this.asyncResourceListChangedSpecifications)) {
159+
this.asyncResourceListChangedSpecifications.forEach(resourceListChangedSpec -> {
160+
Stream.of(resourceListChangedSpec.clients()).forEach(clientId -> {
161+
if (clientId.equalsIgnoreCase(name)) {
162+
clientSpec.resourcesChangeConsumer(resourceListChangedSpec.resourceListChangeHandler());
163+
logger.info("Registered resourceListChangedSpec for client '{}'.", name);
164+
}
165+
});
166+
});
167+
}
168+
169+
if (!CollectionUtils.isEmpty(this.asyncPromptListChangedSpecifications)) {
170+
this.asyncPromptListChangedSpecifications.forEach(promptListChangedSpec -> {
171+
Stream.of(promptListChangedSpec.clients()).forEach(clientId -> {
172+
if (clientId.equalsIgnoreCase(name)) {
173+
clientSpec.promptsChangeConsumer(promptListChangedSpec.promptListChangeHandler());
174+
logger.info("Registered promptListChangedSpec for client '{}'.", name);
175+
}
176+
});
177+
});
178+
}
179+
}
180+
181+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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.mcp.client.common.autoconfigure.annotations;
18+
19+
import java.lang.annotation.Annotation;
20+
import java.util.Set;
21+
22+
import org.springaicommunity.mcp.annotation.McpElicitation;
23+
import org.springaicommunity.mcp.annotation.McpLogging;
24+
import org.springaicommunity.mcp.annotation.McpProgress;
25+
import org.springaicommunity.mcp.annotation.McpSampling;
26+
27+
import org.springframework.ai.mcp.annotation.spring.scan.AbstractAnnotatedMethodBeanPostProcessor;
28+
import org.springframework.ai.mcp.annotation.spring.scan.AbstractMcpAnnotatedBeans;
29+
import org.springframework.boot.autoconfigure.AutoConfiguration;
30+
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
31+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
32+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
33+
import org.springframework.boot.context.properties.EnableConfigurationProperties;
34+
import org.springframework.context.annotation.Bean;
35+
36+
/**
37+
* @author Christian Tzolov
38+
*/
39+
@AutoConfiguration
40+
@ConditionalOnClass(McpLogging.class)
41+
@ConditionalOnProperty(prefix = McpClientAnnotationScannerProperties.CONFIG_PREFIX, name = "enabled",
42+
havingValue = "true", matchIfMissing = true)
43+
@EnableConfigurationProperties(McpClientAnnotationScannerProperties.class)
44+
public class McpClientAnnotationScannerAutoConfiguration {
45+
46+
private static final Set<Class<? extends Annotation>> CLIENT_MCP_ANNOTATIONS = Set.of(McpLogging.class,
47+
McpSampling.class, McpElicitation.class, McpProgress.class);
48+
49+
@Bean
50+
@ConditionalOnMissingBean
51+
public ClientMcpAnnotatedBeans clientAnnotatedBeans() {
52+
return new ClientMcpAnnotatedBeans();
53+
}
54+
55+
@Bean
56+
@ConditionalOnMissingBean
57+
public ClientAnnotatedMethodBeanPostProcessor clientAnnotatedMethodBeanPostProcessor(
58+
ClientMcpAnnotatedBeans clientMcpAnnotatedBeans, McpClientAnnotationScannerProperties properties) {
59+
return new ClientAnnotatedMethodBeanPostProcessor(clientMcpAnnotatedBeans, CLIENT_MCP_ANNOTATIONS);
60+
}
61+
62+
public static class ClientMcpAnnotatedBeans extends AbstractMcpAnnotatedBeans {
63+
64+
}
65+
66+
public static class ClientAnnotatedMethodBeanPostProcessor extends AbstractAnnotatedMethodBeanPostProcessor {
67+
68+
public ClientAnnotatedMethodBeanPostProcessor(ClientMcpAnnotatedBeans clientMcpAnnotatedBeans,
69+
Set<Class<? extends Annotation>> targetAnnotations) {
70+
super(clientMcpAnnotatedBeans, targetAnnotations);
71+
}
72+
73+
}
74+
75+
}

0 commit comments

Comments
 (0)