Skip to content

Commit 9373ec7

Browse files
committed
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 Signed-off-by: Christian Tzolov <[email protected]>
1 parent 6880753 commit 9373ec7

File tree

45 files changed

+6071
-22
lines changed

Some content is hidden

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

45 files changed

+6071
-22
lines changed

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: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,22 @@
2424
import io.modelcontextprotocol.client.McpSyncClient;
2525
import io.modelcontextprotocol.spec.McpSchema;
2626

27+
import org.springaicommunity.mcp.method.changed.prompt.AsyncPromptListChangedSpecification;
28+
import org.springaicommunity.mcp.method.changed.prompt.SyncPromptListChangedSpecification;
29+
import org.springaicommunity.mcp.method.changed.resource.AsyncResourceListChangedSpecification;
30+
import org.springaicommunity.mcp.method.changed.resource.SyncResourceListChangedSpecification;
31+
import org.springaicommunity.mcp.method.changed.tool.AsyncToolListChangedSpecification;
32+
import org.springaicommunity.mcp.method.changed.tool.SyncToolListChangedSpecification;
33+
import org.springaicommunity.mcp.method.elicitation.AsyncElicitationSpecification;
34+
import org.springaicommunity.mcp.method.elicitation.SyncElicitationSpecification;
35+
import org.springaicommunity.mcp.method.logging.AsyncLoggingSpecification;
36+
import org.springaicommunity.mcp.method.logging.SyncLoggingSpecification;
37+
import org.springaicommunity.mcp.method.progress.AsyncProgressSpecification;
38+
import org.springaicommunity.mcp.method.progress.SyncProgressSpecification;
39+
import org.springaicommunity.mcp.method.sampling.AsyncSamplingSpecification;
40+
import org.springaicommunity.mcp.method.sampling.SyncSamplingSpecification;
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;
@@ -208,6 +224,20 @@ McpSyncClientConfigurer mcpSyncClientConfigurer(ObjectProvider<McpSyncClientCust
208224
return new McpSyncClientConfigurer(customizerProvider.orderedStream().toList());
209225
}
210226

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

213243
@Bean
@@ -259,6 +289,18 @@ McpAsyncClientConfigurer mcpAsyncClientConfigurer(ObjectProvider<McpAsyncClientC
259289
return new McpAsyncClientConfigurer(customizerProvider.orderedStream().toList());
260290
}
261291

292+
@Bean
293+
@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC")
294+
public McpAsyncClientCustomizer mcpAnnotationMcpAsyncClientCustomizer(List<AsyncLoggingSpecification> loggingSpecs,
295+
List<AsyncSamplingSpecification> samplingSpecs, List<AsyncElicitationSpecification> elicitationSpecs,
296+
List<AsyncProgressSpecification> progressSpecs,
297+
List<AsyncToolListChangedSpecification> toolListChangedSpecs,
298+
List<AsyncResourceListChangedSpecification> resourceListChangedSpecs,
299+
List<AsyncPromptListChangedSpecification> promptListChangedSpecs) {
300+
return new McpAsyncAnnotationCustomizer(samplingSpecs, loggingSpecs, elicitationSpecs, progressSpecs,
301+
toolListChangedSpecs, resourceListChangedSpecs, promptListChangedSpecs);
302+
}
303+
262304
/**
263305
* Record class that implements {@link AutoCloseable} to ensure proper cleanup of MCP
264306
* clients.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
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+
import org.springframework.ai.mcp.annotation.spring.scan.AbstractMcpAnnotatedBeans;
27+
import org.springframework.ai.mcp.annotation.spring.scan.AbstractAnnotatedMethodBeanPostProcessor;
28+
import org.springframework.boot.autoconfigure.AutoConfiguration;
29+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
30+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
31+
import org.springframework.boot.context.properties.EnableConfigurationProperties;
32+
import org.springframework.context.annotation.Bean;
33+
34+
/**
35+
* @author Christian Tzolov
36+
*/
37+
@AutoConfiguration
38+
@ConditionalOnProperty(prefix = ClientAnnotationScannerProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
39+
matchIfMissing = true)
40+
@EnableConfigurationProperties(ClientAnnotationScannerProperties.class)
41+
public class ClientAnnotationScannerAutoConfiguration {
42+
43+
private static final Set<Class<? extends Annotation>> CLIENT_MCP_ANNOTATIONS = Set.of(McpLogging.class,
44+
McpSampling.class, McpElicitation.class, McpProgress.class);
45+
46+
public static class ClientMcpAnnotatedBeans extends AbstractMcpAnnotatedBeans {
47+
48+
}
49+
50+
public static class ClientAnnotatedMethodBeanPostProcessor extends AbstractAnnotatedMethodBeanPostProcessor {
51+
52+
public ClientAnnotatedMethodBeanPostProcessor(ClientMcpAnnotatedBeans clientMcpAnnotatedBeans,
53+
Set<Class<? extends Annotation>> targetAnnotations) {
54+
super(clientMcpAnnotatedBeans, targetAnnotations);
55+
}
56+
57+
}
58+
59+
@Bean
60+
@ConditionalOnMissingBean
61+
public ClientMcpAnnotatedBeans clientAnnotatedBeans() {
62+
return new ClientMcpAnnotatedBeans();
63+
}
64+
65+
@Bean
66+
@ConditionalOnMissingBean
67+
public ClientAnnotatedMethodBeanPostProcessor clientAnnotatedMethodBeanPostProcessor(
68+
ClientMcpAnnotatedBeans clientMcpAnnotatedBeans, ClientAnnotationScannerProperties properties) {
69+
return new ClientAnnotatedMethodBeanPostProcessor(clientMcpAnnotatedBeans, CLIENT_MCP_ANNOTATIONS);
70+
}
71+
72+
}
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 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,107 @@
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+
import org.springframework.ai.mcp.annotation.spring.AsyncMcpAnnotationProviders;
34+
import org.springframework.ai.mcp.annotation.spring.SyncMcpAnnotationProviders;
35+
import org.springframework.ai.mcp.client.common.autoconfigure.annotations.ClientAnnotationScannerAutoConfiguration.ClientMcpAnnotatedBeans;
36+
import org.springframework.ai.mcp.client.common.autoconfigure.properties.McpClientCommonProperties;
37+
import org.springframework.boot.autoconfigure.AutoConfiguration;
38+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
39+
import org.springframework.context.annotation.Bean;
40+
import org.springframework.context.annotation.Configuration;
41+
42+
/**
43+
* @author Christian Tzolov
44+
*/
45+
@AutoConfiguration(after = ClientAnnotationScannerAutoConfiguration.class)
46+
@ConditionalOnProperty(prefix = ClientAnnotationScannerProperties.CONFIG_PREFIX, name = "enabled", havingValue = "true",
47+
matchIfMissing = true)
48+
public class ClientSpecificationFactoryAutoConfiguration {
49+
50+
@Configuration(proxyBeanMethods = false)
51+
@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "SYNC",
52+
matchIfMissing = true)
53+
static class SyncClientSpecificationConfiguration {
54+
55+
@Bean
56+
List<SyncLoggingSpecification> loggingSpecs(ClientMcpAnnotatedBeans beansWithMcpMethodAnnotations) {
57+
return SyncMcpAnnotationProviders
58+
.loggingSpecifications(beansWithMcpMethodAnnotations.getBeansByAnnotation(McpLogging.class));
59+
}
60+
61+
@Bean
62+
List<SyncSamplingSpecification> samplingSpecs(ClientMcpAnnotatedBeans beansWithMcpMethodAnnotations) {
63+
return SyncMcpAnnotationProviders
64+
.samplingSpecifications(beansWithMcpMethodAnnotations.getBeansByAnnotation(McpSampling.class));
65+
}
66+
67+
@Bean
68+
List<SyncElicitationSpecification> elicitationSpecs(ClientMcpAnnotatedBeans beansWithMcpMethodAnnotations) {
69+
return SyncMcpAnnotationProviders
70+
.elicitationSpecifications(beansWithMcpMethodAnnotations.getBeansByAnnotation(McpElicitation.class));
71+
}
72+
73+
@Bean
74+
List<SyncProgressSpecification> progressSpecs(ClientMcpAnnotatedBeans beansWithMcpMethodAnnotations) {
75+
return SyncMcpAnnotationProviders
76+
.progressSpecifications(beansWithMcpMethodAnnotations.getBeansByAnnotation(McpProgress.class));
77+
}
78+
79+
}
80+
81+
@Configuration(proxyBeanMethods = false)
82+
@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC")
83+
static class AsyncClientSpecificationConfiguration {
84+
85+
@Bean
86+
List<AsyncLoggingSpecification> loggingSpecs(ClientMcpAnnotatedBeans beanRegistry) {
87+
return AsyncMcpAnnotationProviders.loggingSpecifications(beanRegistry.getAllAnnotatedBeans());
88+
}
89+
90+
@Bean
91+
List<AsyncSamplingSpecification> samplingSpecs(ClientMcpAnnotatedBeans beanRegistry) {
92+
return AsyncMcpAnnotationProviders.samplingSpecifications(beanRegistry.getAllAnnotatedBeans());
93+
}
94+
95+
@Bean
96+
List<AsyncElicitationSpecification> elicitationSpecs(ClientMcpAnnotatedBeans beanRegistry) {
97+
return AsyncMcpAnnotationProviders.elicitationSpecifications(beanRegistry.getAllAnnotatedBeans());
98+
}
99+
100+
@Bean
101+
List<AsyncProgressSpecification> progressSpecs(ClientMcpAnnotatedBeans beanRegistry) {
102+
return AsyncMcpAnnotationProviders.progressSpecifications(beanRegistry.getAllAnnotatedBeans());
103+
}
104+
105+
}
106+
107+
}

0 commit comments

Comments
 (0)