Skip to content

Commit 2a2f155

Browse files
Kehrlanntzolov
authored andcommitted
Refactor MCP client annotation handling with unified handler registries (#4802)
Replace specification factory pattern with centralized handler registries that scan and register MCP client handlers (sampling, elicitation, logging, progress, and list-changed notifications). This simplifies the auto-configuration by: - Introducing ClientMcpSyncHandlersRegistry and ClientMcpAsyncHandlersRegistry that scan beans once for annotations and expose handlers by client name - Removing intermediate specification factory beans and customizers - Directly configuring MCP client specs from registries during client creation - Eliminating need for separate specification classes per handler type - Simplifying ToolCallingAutoConfiguration by removing BeanDefinitionRegistryPostProcessor complexity - In febf86c, we broke a dependency cycle ChatClient -> McpClient - With the introduction of ClientMcpSyncHandlersRegistry and the async variant, there is no dependency McpClient -> MCP handlers anymore, breaking the cycle in a simpler way. - Here, we revert most of the changes of febf86c, but keep the tests. - Remove unused MCP annotated beans auto-configuration - Introduce AbstractClientMcpHandlerRegistry - Find MCP Client annotations on @component beans - AbstractClientMcpHandlerRegistry also discovers proxied beans - Remove custom class resolution method and use AutoProxyUtils instead - Add logging to MCP handlers registry - Throw MCP Error on missing sampling and elicitation handlers in a client - Fix missing auto-configurations McpClientAutoConfigurationIT Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
1 parent c631340 commit 2a2f155

File tree

29 files changed

+2068
-1112
lines changed

29 files changed

+2068
-1112
lines changed

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

Lines changed: 39 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -23,23 +23,9 @@
2323
import io.modelcontextprotocol.client.McpClient;
2424
import io.modelcontextprotocol.client.McpSyncClient;
2525
import io.modelcontextprotocol.spec.McpSchema;
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;
26+
27+
import org.springframework.ai.mcp.annotation.spring.ClientMcpAsyncHandlersRegistry;
28+
import org.springframework.ai.mcp.annotation.spring.ClientMcpSyncHandlersRegistry;
4329
import org.springframework.ai.mcp.client.common.autoconfigure.configurer.McpAsyncClientConfigurer;
4430
import org.springframework.ai.mcp.client.common.autoconfigure.configurer.McpSyncClientConfigurer;
4531
import org.springframework.ai.mcp.client.common.autoconfigure.properties.McpClientCommonProperties;
@@ -161,7 +147,8 @@ private String connectedClientName(String clientName, String serverConnectionNam
161147
matchIfMissing = true)
162148
public List<McpSyncClient> mcpSyncClients(McpSyncClientConfigurer mcpSyncClientConfigurer,
163149
McpClientCommonProperties commonProperties,
164-
ObjectProvider<List<NamedClientMcpTransport>> transportsProvider) {
150+
ObjectProvider<List<NamedClientMcpTransport>> transportsProvider,
151+
ClientMcpSyncHandlersRegistry clientMcpSyncHandlersRegistry) {
165152

166153
List<McpSyncClient> mcpSyncClients = new ArrayList<>();
167154

@@ -176,7 +163,22 @@ public List<McpSyncClient> mcpSyncClients(McpSyncClientConfigurer mcpSyncClientC
176163

177164
McpClient.SyncSpec spec = McpClient.sync(namedTransport.transport())
178165
.clientInfo(clientInfo)
179-
.requestTimeout(commonProperties.getRequestTimeout());
166+
.requestTimeout(commonProperties.getRequestTimeout())
167+
.sampling(samplingRequest -> clientMcpSyncHandlersRegistry.handleSampling(namedTransport.name(),
168+
samplingRequest))
169+
.elicitation(elicitationRequest -> clientMcpSyncHandlersRegistry
170+
.handleElicitation(namedTransport.name(), elicitationRequest))
171+
.loggingConsumer(loggingMessageNotification -> clientMcpSyncHandlersRegistry
172+
.handleLogging(namedTransport.name(), loggingMessageNotification))
173+
.progressConsumer(progressNotification -> clientMcpSyncHandlersRegistry
174+
.handleProgress(namedTransport.name(), progressNotification))
175+
.toolsChangeConsumer(newTools -> clientMcpSyncHandlersRegistry
176+
.handleToolListChanged(namedTransport.name(), newTools))
177+
.promptsChangeConsumer(newPrompts -> clientMcpSyncHandlersRegistry
178+
.handlePromptListChanged(namedTransport.name(), newPrompts))
179+
.resourcesChangeConsumer(newResources -> clientMcpSyncHandlersRegistry
180+
.handleResourceListChanged(namedTransport.name(), newResources))
181+
.capabilities(clientMcpSyncHandlersRegistry.getCapabilities(namedTransport.name()));
180182

181183
spec = mcpSyncClientConfigurer.configure(namedTransport.name(), spec);
182184

@@ -222,27 +224,14 @@ McpSyncClientConfigurer mcpSyncClientConfigurer(ObjectProvider<McpSyncClientCust
222224
return new McpSyncClientConfigurer(customizerProvider.orderedStream().toList());
223225
}
224226

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-
239227
// Async client configuration
240228

241229
@Bean
242230
@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC")
243231
public List<McpAsyncClient> mcpAsyncClients(McpAsyncClientConfigurer mcpAsyncClientConfigurer,
244232
McpClientCommonProperties commonProperties,
245-
ObjectProvider<List<NamedClientMcpTransport>> transportsProvider) {
233+
ObjectProvider<List<NamedClientMcpTransport>> transportsProvider,
234+
ClientMcpAsyncHandlersRegistry clientMcpAsyncHandlersRegistry) {
246235

247236
List<McpAsyncClient> mcpAsyncClients = new ArrayList<>();
248237

@@ -257,7 +246,22 @@ public List<McpAsyncClient> mcpAsyncClients(McpAsyncClientConfigurer mcpAsyncCli
257246

258247
McpClient.AsyncSpec spec = McpClient.async(namedTransport.transport())
259248
.clientInfo(clientInfo)
260-
.requestTimeout(commonProperties.getRequestTimeout());
249+
.requestTimeout(commonProperties.getRequestTimeout())
250+
.sampling(samplingRequest -> clientMcpAsyncHandlersRegistry.handleSampling(namedTransport.name(),
251+
samplingRequest))
252+
.elicitation(elicitationRequest -> clientMcpAsyncHandlersRegistry
253+
.handleElicitation(namedTransport.name(), elicitationRequest))
254+
.loggingConsumer(loggingMessageNotification -> clientMcpAsyncHandlersRegistry
255+
.handleLogging(namedTransport.name(), loggingMessageNotification))
256+
.progressConsumer(progressNotification -> clientMcpAsyncHandlersRegistry
257+
.handleProgress(namedTransport.name(), progressNotification))
258+
.toolsChangeConsumer(newTools -> clientMcpAsyncHandlersRegistry
259+
.handleToolListChanged(namedTransport.name(), newTools))
260+
.promptsChangeConsumer(newPrompts -> clientMcpAsyncHandlersRegistry
261+
.handlePromptListChanged(namedTransport.name(), newPrompts))
262+
.resourcesChangeConsumer(newResources -> clientMcpAsyncHandlersRegistry
263+
.handleResourceListChanged(namedTransport.name(), newResources))
264+
.capabilities(clientMcpAsyncHandlersRegistry.getCapabilities(namedTransport.name()));
261265

262266
spec = mcpAsyncClientConfigurer.configure(namedTransport.name(), spec);
263267

@@ -287,18 +291,6 @@ McpAsyncClientConfigurer mcpAsyncClientConfigurer(ObjectProvider<McpAsyncClientC
287291
return new McpAsyncClientConfigurer(customizerProvider.orderedStream().toList());
288292
}
289293

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-
302294
/**
303295
* Record class that implements {@link AutoCloseable} to ensure proper cleanup of MCP
304296
* clients.

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

Lines changed: 0 additions & 181 deletions
This file was deleted.

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

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,11 @@
2727
import org.springaicommunity.mcp.annotation.McpSampling;
2828
import org.springaicommunity.mcp.annotation.McpToolListChanged;
2929

30+
import org.springframework.ai.mcp.annotation.spring.ClientMcpAsyncHandlersRegistry;
31+
import org.springframework.ai.mcp.annotation.spring.ClientMcpSyncHandlersRegistry;
3032
import org.springframework.ai.mcp.annotation.spring.scan.AbstractAnnotatedMethodBeanFactoryInitializationAotProcessor;
31-
import org.springframework.ai.mcp.annotation.spring.scan.AbstractAnnotatedMethodBeanPostProcessor;
3233
import org.springframework.ai.mcp.annotation.spring.scan.AbstractMcpAnnotatedBeans;
34+
import org.springframework.ai.mcp.client.common.autoconfigure.properties.McpClientCommonProperties;
3335
import org.springframework.aot.hint.MemberCategory;
3436
import org.springframework.aot.hint.RuntimeHints;
3537
import org.springframework.aot.hint.RuntimeHintsRegistrar;
@@ -60,15 +62,17 @@ public class McpClientAnnotationScannerAutoConfiguration {
6062

6163
@Bean
6264
@ConditionalOnMissingBean
63-
public ClientMcpAnnotatedBeans clientAnnotatedBeans() {
64-
return new ClientMcpAnnotatedBeans();
65+
@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "SYNC",
66+
matchIfMissing = true)
67+
public ClientMcpSyncHandlersRegistry clientMcpSyncHandlersRegistry() {
68+
return new ClientMcpSyncHandlersRegistry();
6569
}
6670

6771
@Bean
6872
@ConditionalOnMissingBean
69-
public static ClientAnnotatedMethodBeanPostProcessor clientAnnotatedMethodBeanPostProcessor(
70-
ClientMcpAnnotatedBeans clientMcpAnnotatedBeans, McpClientAnnotationScannerProperties properties) {
71-
return new ClientAnnotatedMethodBeanPostProcessor(clientMcpAnnotatedBeans, CLIENT_MCP_ANNOTATIONS);
73+
@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC")
74+
public ClientMcpAsyncHandlersRegistry clientMcpAsyncHandlersRegistry() {
75+
return new ClientMcpAsyncHandlersRegistry();
7276
}
7377

7478
@Bean
@@ -90,15 +94,6 @@ public ClientAnnotatedBeanFactoryInitializationAotProcessor(
9094

9195
}
9296

93-
public static class ClientAnnotatedMethodBeanPostProcessor extends AbstractAnnotatedMethodBeanPostProcessor {
94-
95-
public ClientAnnotatedMethodBeanPostProcessor(ClientMcpAnnotatedBeans clientMcpAnnotatedBeans,
96-
Set<Class<? extends Annotation>> targetAnnotations) {
97-
super(clientMcpAnnotatedBeans, targetAnnotations);
98-
}
99-
100-
}
101-
10297
static class AnnotationHints implements RuntimeHintsRegistrar {
10398

10499
@Override

0 commit comments

Comments
 (0)