Skip to content

Commit 3c69651

Browse files
committed
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 ba36b1e commit 3c69651

File tree

21 files changed

+773
-735
lines changed

21 files changed

+773
-735
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/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/McpClientAutoConfiguration.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
import io.modelcontextprotocol.client.McpClient;
2424
import io.modelcontextprotocol.client.McpSyncClient;
2525
import io.modelcontextprotocol.spec.McpSchema;
26-
2726
import org.springaicommunity.mcp.method.changed.prompt.AsyncPromptListChangedSpecification;
2827
import org.springaicommunity.mcp.method.changed.prompt.SyncPromptListChangedSpecification;
2928
import org.springaicommunity.mcp.method.changed.resource.AsyncResourceListChangedSpecification;
@@ -38,6 +37,7 @@
3837
import org.springaicommunity.mcp.method.progress.SyncProgressSpecification;
3938
import org.springaicommunity.mcp.method.sampling.AsyncSamplingSpecification;
4039
import org.springaicommunity.mcp.method.sampling.SyncSamplingSpecification;
40+
4141
import org.springframework.ai.mcp.client.common.autoconfigure.annotations.McpAsyncAnnotationCustomizer;
4242
import org.springframework.ai.mcp.client.common.autoconfigure.annotations.McpSyncAnnotationCustomizer;
4343
import org.springframework.ai.mcp.client.common.autoconfigure.configurer.McpAsyncClientConfigurer;
@@ -111,8 +111,6 @@
111111
* @see McpSyncClientCustomizer
112112
* @see McpAsyncClientCustomizer
113113
* @see StdioTransportAutoConfiguration
114-
* @see SseHttpClientTransportAutoConfiguration
115-
* @see SseWebFluxTransportAutoConfiguration
116114
*/
117115
@AutoConfiguration(afterName = {
118116
"org.springframework.ai.mcp.client.common.autoconfigure.StdioTransportAutoConfiguration",

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

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@
2323
import org.springaicommunity.mcp.annotation.McpLogging;
2424
import org.springaicommunity.mcp.annotation.McpProgress;
2525
import org.springaicommunity.mcp.annotation.McpSampling;
26-
import org.springframework.ai.mcp.annotation.spring.scan.AbstractMcpAnnotatedBeans;
26+
2727
import org.springframework.ai.mcp.annotation.spring.scan.AbstractAnnotatedMethodBeanPostProcessor;
28+
import org.springframework.ai.mcp.annotation.spring.scan.AbstractMcpAnnotatedBeans;
2829
import org.springframework.boot.autoconfigure.AutoConfiguration;
2930
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
3031
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@@ -43,19 +44,6 @@ public class ClientAnnotationScannerAutoConfiguration {
4344
private static final Set<Class<? extends Annotation>> CLIENT_MCP_ANNOTATIONS = Set.of(McpLogging.class,
4445
McpSampling.class, McpElicitation.class, McpProgress.class);
4546

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-
5947
@Bean
6048
@ConditionalOnMissingBean
6149
public ClientMcpAnnotatedBeans clientAnnotatedBeans() {
@@ -69,4 +57,17 @@ public ClientAnnotatedMethodBeanPostProcessor clientAnnotatedMethodBeanPostProce
6957
return new ClientAnnotatedMethodBeanPostProcessor(clientMcpAnnotatedBeans, CLIENT_MCP_ANNOTATIONS);
7058
}
7159

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+
7273
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,11 @@ public class ClientAnnotationScannerProperties {
2929
private boolean enabled = true;
3030

3131
public boolean isEnabled() {
32-
return enabled;
32+
return this.enabled;
3333
}
3434

3535
public void setEnabled(boolean enabled) {
3636
this.enabled = enabled;
3737
}
3838

39-
}
39+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import org.springaicommunity.mcp.method.progress.SyncProgressSpecification;
3131
import org.springaicommunity.mcp.method.sampling.AsyncSamplingSpecification;
3232
import org.springaicommunity.mcp.method.sampling.SyncSamplingSpecification;
33+
3334
import org.springframework.ai.mcp.annotation.spring.AsyncMcpAnnotationProviders;
3435
import org.springframework.ai.mcp.annotation.spring.SyncMcpAnnotationProviders;
3536
import org.springframework.ai.mcp.client.common.autoconfigure.annotations.ClientAnnotationScannerAutoConfiguration.ClientMcpAnnotatedBeans;

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

Lines changed: 64 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919
import java.util.List;
2020
import java.util.Map;
2121
import java.util.concurrent.ConcurrentHashMap;
22+
import java.util.stream.Stream;
2223

24+
import io.modelcontextprotocol.client.McpClient.AsyncSpec;
2325
import org.slf4j.Logger;
2426
import org.slf4j.LoggerFactory;
2527
import org.springaicommunity.mcp.method.changed.prompt.AsyncPromptListChangedSpecification;
@@ -29,11 +31,10 @@
2931
import org.springaicommunity.mcp.method.logging.AsyncLoggingSpecification;
3032
import org.springaicommunity.mcp.method.progress.AsyncProgressSpecification;
3133
import org.springaicommunity.mcp.method.sampling.AsyncSamplingSpecification;
34+
3235
import org.springframework.ai.mcp.customizer.McpAsyncClientCustomizer;
3336
import org.springframework.util.CollectionUtils;
3437

35-
import io.modelcontextprotocol.client.McpClient.AsyncSpec;
36-
3738
/**
3839
* @author Christian Tzolov
3940
*/
@@ -80,85 +81,99 @@ public McpAsyncAnnotationCustomizer(List<AsyncSamplingSpecification> asyncSampli
8081
@Override
8182
public void customize(String name, AsyncSpec clientSpec) {
8283

83-
if (!CollectionUtils.isEmpty(asyncElicitationSpecifications)) {
84+
if (!CollectionUtils.isEmpty(this.asyncElicitationSpecifications)) {
8485
this.asyncElicitationSpecifications.forEach(elicitationSpec -> {
85-
if (elicitationSpec.clientId().equalsIgnoreCase(name)) {
86+
Stream.of(elicitationSpec.clients()).forEach(clientId -> {
87+
if (clientId.equalsIgnoreCase(name)) {
8688

87-
// Check if client already has an elicitation spec
88-
if (clientElicitationSpecs.containsKey(name)) {
89-
throw new IllegalArgumentException("Client '" + name
90-
+ "' already has an elicitationSpec registered. Only one elicitationSpec is allowed per client.");
91-
}
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+
}
9294

93-
clientElicitationSpecs.put(name, Boolean.TRUE);
94-
clientSpec.elicitation(elicitationSpec.elicitationHandler());
95+
this.clientElicitationSpecs.put(name, Boolean.TRUE);
96+
clientSpec.elicitation(elicitationSpec.elicitationHandler());
9597

96-
logger.info("Registered elicitationSpec for client '{}'.", name);
98+
logger.info("Registered elicitationSpec for client '{}'.", name);
9799

98-
}
100+
}
101+
});
99102
});
100103
}
101104

102-
if (!CollectionUtils.isEmpty(asyncSamplingSpecifications)) {
105+
if (!CollectionUtils.isEmpty(this.asyncSamplingSpecifications)) {
103106
this.asyncSamplingSpecifications.forEach(samplingSpec -> {
104-
if (samplingSpec.clientId().equalsIgnoreCase(name)) {
107+
Stream.of(samplingSpec.clients()).forEach(clientId -> {
108+
if (clientId.equalsIgnoreCase(name)) {
105109

106-
// Check if client already has a sampling spec
107-
if (clientSamplingSpecs.containsKey(name)) {
108-
throw new IllegalArgumentException("Client '" + name
109-
+ "' already has a samplingSpec registered. Only one samplingSpec is allowed per client.");
110-
}
111-
clientSamplingSpecs.put(name, Boolean.TRUE);
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);
112116

113-
clientSpec.sampling(samplingSpec.samplingHandler());
117+
clientSpec.sampling(samplingSpec.samplingHandler());
114118

115-
logger.info("Registered samplingSpec for client '{}'.", name);
116-
}
119+
logger.info("Registered samplingSpec for client '{}'.", name);
120+
}
121+
});
117122
});
118123
}
119124

120-
if (!CollectionUtils.isEmpty(asyncLoggingSpecifications)) {
125+
if (!CollectionUtils.isEmpty(this.asyncLoggingSpecifications)) {
121126
this.asyncLoggingSpecifications.forEach(loggingSpec -> {
122-
if (loggingSpec.clientId().equalsIgnoreCase(name)) {
123-
clientSpec.loggingConsumer(loggingSpec.loggingHandler());
124-
logger.info("Registered loggingSpec for client '{}'.", name);
125-
}
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+
});
126133
});
127134
}
128135

129-
if (!CollectionUtils.isEmpty(asyncProgressSpecifications)) {
136+
if (!CollectionUtils.isEmpty(this.asyncProgressSpecifications)) {
130137
this.asyncProgressSpecifications.forEach(progressSpec -> {
131-
if (progressSpec.clientId().equalsIgnoreCase(name)) {
132-
clientSpec.progressConsumer(progressSpec.progressHandler());
133-
logger.info("Registered progressSpec for client '{}'.", name);
134-
}
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+
});
135144
});
136145
}
137146

138-
if (!CollectionUtils.isEmpty(asyncToolListChangedSpecifications)) {
147+
if (!CollectionUtils.isEmpty(this.asyncToolListChangedSpecifications)) {
139148
this.asyncToolListChangedSpecifications.forEach(toolListChangedSpec -> {
140-
if (toolListChangedSpec.clientId().equalsIgnoreCase(name)) {
141-
clientSpec.toolsChangeConsumer(toolListChangedSpec.toolListChangeHandler());
142-
logger.info("Registered toolListChangedSpec for client '{}'.", name);
143-
}
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+
});
144155
});
145156
}
146157

147-
if (!CollectionUtils.isEmpty(asyncResourceListChangedSpecifications)) {
158+
if (!CollectionUtils.isEmpty(this.asyncResourceListChangedSpecifications)) {
148159
this.asyncResourceListChangedSpecifications.forEach(resourceListChangedSpec -> {
149-
if (resourceListChangedSpec.clientId().equalsIgnoreCase(name)) {
150-
clientSpec.resourcesChangeConsumer(resourceListChangedSpec.resourceListChangeHandler());
151-
logger.info("Registered resourceListChangedSpec for client '{}'.", name);
152-
}
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+
});
153166
});
154167
}
155168

156-
if (!CollectionUtils.isEmpty(asyncPromptListChangedSpecifications)) {
169+
if (!CollectionUtils.isEmpty(this.asyncPromptListChangedSpecifications)) {
157170
this.asyncPromptListChangedSpecifications.forEach(promptListChangedSpec -> {
158-
if (promptListChangedSpec.clientId().equalsIgnoreCase(name)) {
159-
clientSpec.promptsChangeConsumer(promptListChangedSpec.promptListChangeHandler());
160-
logger.info("Registered promptListChangedSpec for client '{}'.", name);
161-
}
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+
});
162177
});
163178
}
164179
}

0 commit comments

Comments
 (0)