Skip to content

Commit 534c6af

Browse files
tzolovmarkpollack
authored andcommitted
feat: Add annotation-based configuration support for MCP clients and servers
- Introduce module for declarative MCP configuration - Create new mcp-annotations-spring module - Add annotation scanners to auto-discover MCP annotated beans and methods - Support MCP annotations: @mcptool, @McpResource, @McpPrompt, @McpComplete for servers - Support MCP annotations: @McpLogging, @McpSampling, @McpElicitation, @McpProgress for clients - Implement customizers to automatically register annotated specifications - Introduce McpAsyncAnnotationCustomizer and McpSyncAnnotationCustomizer for annotation-driven client configuration - Add Spring-aware annotation providers for both sync and async MCP operations - Include comprehensive integration tests for annotation-based MCP configuration - Update all MCP starter dependencies to include the new annotations module - Update BOM and parent POM to include mcp-annotations dependency version management - Include comprehensive integration test StreamableMcpAnnotationsIT demonstrating annotation usage - Move McpAsyncAnnotationCustomizer and McpSyncAnnotationCustomizer to annotations package - Add support for tool, resource, and prompt list changed notifications - New AsyncToolListChangedSpecification, AsyncResourceListChangedSpecification, AsyncPromptListChangedSpecification - New SyncToolListChangedSpecification, SyncResourceListChangedSpecification, SyncPromptListChangedSpecification - Wire list changed specifications in MCP client auto-configuration - Enhance annotation customizers with validation and logging - Prevent duplicate elicitation and sampling specs per client with proper error handling - Add comprehensive logging for all registered MCP client specifications - Track registered specifications per client using ConcurrentHashMap - Add unit test suite - Complete test coverage for McpSyncAnnotationCustomizer - Test duplicate validation, case-insensitive matching, and error scenarios - Update documentation with breaking changes - MCP client annotations now require mandatory clientId parameter - Update all examples to include clientId in annotation usage - Add configuration examples showing clientId mapping to connection names refactor: support multiple clients in MCP annotations and apply code style improvements - Change MCP annotation client specification from single clientId to clients array - Update @McpLogging, @McpSampling, @McpElicitation, @McpProgress annotations - Modify McpAsyncAnnotationCustomizer and McpSyncAnnotationCustomizer to iterate over client arrays - Update corresponding test cases to use new clients array format - Apply consistent code formatting and style improvements - Reorganize imports and add missing blank lines between import groups - Add 'this.' prefix for field access consistency - Move inner classes to bottom of files following Java conventions - Update method parameter access patterns - Clean up documentation - Remove redundant @param entries in Javadoc - Add missing newlines at end of files - Update .gitignore to exclude /contributing directory This change enables MCP annotations to target multiple clients simultaneously while maintaining backward compatibility through array support. Signed-off-by: Christian Tzolov <[email protected]>
1 parent dc82e5d commit 534c6af

File tree

49 files changed

+6057
-27
lines changed

Some content is hidden

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

49 files changed

+6057
-27
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,5 @@ CLAUDE.md
5050
qodana.yaml
5151
__pycache__/
5252
*.pyc
53+
54+
/contributing

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,73 @@
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.ConditionalOnMissingBean;
31+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
32+
import org.springframework.boot.context.properties.EnableConfigurationProperties;
33+
import org.springframework.context.annotation.Bean;
34+
35+
/**
36+
* @author Christian Tzolov
37+
*/
38+
@AutoConfiguration
39+
@ConditionalOnProperty(prefix = ClientAnnotationScannerProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
40+
matchIfMissing = true)
41+
@EnableConfigurationProperties(ClientAnnotationScannerProperties.class)
42+
public class ClientAnnotationScannerAutoConfiguration {
43+
44+
private static final Set<Class<? extends Annotation>> CLIENT_MCP_ANNOTATIONS = Set.of(McpLogging.class,
45+
McpSampling.class, McpElicitation.class, McpProgress.class);
46+
47+
@Bean
48+
@ConditionalOnMissingBean
49+
public ClientMcpAnnotatedBeans clientAnnotatedBeans() {
50+
return new ClientMcpAnnotatedBeans();
51+
}
52+
53+
@Bean
54+
@ConditionalOnMissingBean
55+
public ClientAnnotatedMethodBeanPostProcessor clientAnnotatedMethodBeanPostProcessor(
56+
ClientMcpAnnotatedBeans clientMcpAnnotatedBeans, ClientAnnotationScannerProperties properties) {
57+
return new ClientAnnotatedMethodBeanPostProcessor(clientMcpAnnotatedBeans, CLIENT_MCP_ANNOTATIONS);
58+
}
59+
60+
public static class ClientMcpAnnotatedBeans extends AbstractMcpAnnotatedBeans {
61+
62+
}
63+
64+
public static class ClientAnnotatedMethodBeanPostProcessor extends AbstractAnnotatedMethodBeanPostProcessor {
65+
66+
public ClientAnnotatedMethodBeanPostProcessor(ClientMcpAnnotatedBeans clientMcpAnnotatedBeans,
67+
Set<Class<? extends Annotation>> targetAnnotations) {
68+
super(clientMcpAnnotatedBeans, targetAnnotations);
69+
}
70+
71+
}
72+
73+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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 org.springframework.boot.context.properties.ConfigurationProperties;
20+
21+
/**
22+
* @author Christian Tzolov
23+
*/
24+
@ConfigurationProperties(prefix = ClientAnnotationScannerProperties.CONFIG_PREFIX)
25+
public class ClientAnnotationScannerProperties {
26+
27+
public static final String CONFIG_PREFIX = "spring.ai.mcp.client.annotation-scanner";
28+
29+
private boolean enabled = true;
30+
31+
public boolean isEnabled() {
32+
return this.enabled;
33+
}
34+
35+
public void setEnabled(boolean enabled) {
36+
this.enabled = enabled;
37+
}
38+
39+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
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+
21+
import org.springaicommunity.mcp.annotation.McpElicitation;
22+
import org.springaicommunity.mcp.annotation.McpLogging;
23+
import org.springaicommunity.mcp.annotation.McpProgress;
24+
import org.springaicommunity.mcp.annotation.McpSampling;
25+
import org.springaicommunity.mcp.method.elicitation.AsyncElicitationSpecification;
26+
import org.springaicommunity.mcp.method.elicitation.SyncElicitationSpecification;
27+
import org.springaicommunity.mcp.method.logging.AsyncLoggingSpecification;
28+
import org.springaicommunity.mcp.method.logging.SyncLoggingSpecification;
29+
import org.springaicommunity.mcp.method.progress.AsyncProgressSpecification;
30+
import org.springaicommunity.mcp.method.progress.SyncProgressSpecification;
31+
import org.springaicommunity.mcp.method.sampling.AsyncSamplingSpecification;
32+
import org.springaicommunity.mcp.method.sampling.SyncSamplingSpecification;
33+
34+
import org.springframework.ai.mcp.annotation.spring.AsyncMcpAnnotationProviders;
35+
import org.springframework.ai.mcp.annotation.spring.SyncMcpAnnotationProviders;
36+
import org.springframework.ai.mcp.client.common.autoconfigure.annotations.ClientAnnotationScannerAutoConfiguration.ClientMcpAnnotatedBeans;
37+
import org.springframework.ai.mcp.client.common.autoconfigure.properties.McpClientCommonProperties;
38+
import org.springframework.boot.autoconfigure.AutoConfiguration;
39+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
40+
import org.springframework.context.annotation.Bean;
41+
import org.springframework.context.annotation.Configuration;
42+
43+
/**
44+
* @author Christian Tzolov
45+
*/
46+
@AutoConfiguration(after = ClientAnnotationScannerAutoConfiguration.class)
47+
@ConditionalOnProperty(prefix = ClientAnnotationScannerProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
48+
matchIfMissing = true)
49+
public class ClientSpecificationFactoryAutoConfiguration {
50+
51+
@Configuration(proxyBeanMethods = false)
52+
@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "SYNC",
53+
matchIfMissing = true)
54+
static class SyncClientSpecificationConfiguration {
55+
56+
@Bean
57+
List<SyncLoggingSpecification> loggingSpecs(ClientMcpAnnotatedBeans beansWithMcpMethodAnnotations) {
58+
return SyncMcpAnnotationProviders
59+
.loggingSpecifications(beansWithMcpMethodAnnotations.getBeansByAnnotation(McpLogging.class));
60+
}
61+
62+
@Bean
63+
List<SyncSamplingSpecification> samplingSpecs(ClientMcpAnnotatedBeans beansWithMcpMethodAnnotations) {
64+
return SyncMcpAnnotationProviders
65+
.samplingSpecifications(beansWithMcpMethodAnnotations.getBeansByAnnotation(McpSampling.class));
66+
}
67+
68+
@Bean
69+
List<SyncElicitationSpecification> elicitationSpecs(ClientMcpAnnotatedBeans beansWithMcpMethodAnnotations) {
70+
return SyncMcpAnnotationProviders
71+
.elicitationSpecifications(beansWithMcpMethodAnnotations.getBeansByAnnotation(McpElicitation.class));
72+
}
73+
74+
@Bean
75+
List<SyncProgressSpecification> progressSpecs(ClientMcpAnnotatedBeans beansWithMcpMethodAnnotations) {
76+
return SyncMcpAnnotationProviders
77+
.progressSpecifications(beansWithMcpMethodAnnotations.getBeansByAnnotation(McpProgress.class));
78+
}
79+
80+
}
81+
82+
@Configuration(proxyBeanMethods = false)
83+
@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC")
84+
static class AsyncClientSpecificationConfiguration {
85+
86+
@Bean
87+
List<AsyncLoggingSpecification> loggingSpecs(ClientMcpAnnotatedBeans beanRegistry) {
88+
return AsyncMcpAnnotationProviders.loggingSpecifications(beanRegistry.getAllAnnotatedBeans());
89+
}
90+
91+
@Bean
92+
List<AsyncSamplingSpecification> samplingSpecs(ClientMcpAnnotatedBeans beanRegistry) {
93+
return AsyncMcpAnnotationProviders.samplingSpecifications(beanRegistry.getAllAnnotatedBeans());
94+
}
95+
96+
@Bean
97+
List<AsyncElicitationSpecification> elicitationSpecs(ClientMcpAnnotatedBeans beanRegistry) {
98+
return AsyncMcpAnnotationProviders.elicitationSpecifications(beanRegistry.getAllAnnotatedBeans());
99+
}
100+
101+
@Bean
102+
List<AsyncProgressSpecification> progressSpecs(ClientMcpAnnotatedBeans beanRegistry) {
103+
return AsyncMcpAnnotationProviders.progressSpecifications(beanRegistry.getAllAnnotatedBeans());
104+
}
105+
106+
}
107+
108+
}

0 commit comments

Comments
 (0)