Skip to content

Commit 4ab6503

Browse files
committed
AbstractClientMcpHandlerRegistry also discovers proxied beans
- Remove custom class resolution method and use AutoProxyUtils instead Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
1 parent fb9f4a0 commit 4ab6503

File tree

6 files changed

+116
-33
lines changed

6 files changed

+116
-33
lines changed

auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/autoconfigure/StreamableMcpAnnotationsWithLLMIT.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
import org.springframework.ai.mcp.client.common.autoconfigure.McpToolCallbackAutoConfiguration;
5050
import org.springframework.ai.mcp.client.common.autoconfigure.annotations.McpClientAnnotationScannerAutoConfiguration;
5151
import org.springframework.ai.mcp.client.webflux.autoconfigure.StreamableHttpWebFluxTransportAutoConfiguration;
52+
import org.springframework.ai.mcp.server.autoconfigure.capabilities.McpHandlerConfiguration;
5253
import org.springframework.ai.mcp.server.autoconfigure.capabilities.McpHandlerService;
5354
import org.springframework.ai.mcp.server.common.autoconfigure.McpServerAutoConfiguration;
5455
import org.springframework.ai.mcp.server.common.autoconfigure.McpServerObjectMapperAutoConfiguration;
@@ -240,7 +241,8 @@ public String weather(McpSyncRequestContext ctx, @McpToolParam String cityName)
240241
ctx.ping(); // call client ping
241242

242243
// call elicitation
243-
var elicitationResult = ctx.elicit(e -> e.message("Test message"), McpHandlerService.ElicitInput.class);
244+
var elicitationResult = ctx.elicit(e -> e.message("Test message"),
245+
McpHandlerConfiguration.ElicitInput.class);
244246

245247
ctx.progress(p -> p.progress(0.50).total(1.0).message("elicitation completed"));
246248

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
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.server.autoconfigure.capabilities;
18+
19+
import io.modelcontextprotocol.spec.McpSchema;
20+
import org.slf4j.Logger;
21+
import org.slf4j.LoggerFactory;
22+
import org.springaicommunity.mcp.annotation.McpElicitation;
23+
import org.springaicommunity.mcp.context.StructuredElicitResult;
24+
25+
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
26+
import org.springframework.context.annotation.Bean;
27+
import org.springframework.context.annotation.Configuration;
28+
import org.springframework.context.annotation.Scope;
29+
import org.springframework.web.context.annotation.RequestScope;
30+
31+
@Configuration
32+
public class McpHandlerConfiguration {
33+
34+
private static final Logger logger = LoggerFactory.getLogger(McpHandlerConfiguration.class);
35+
36+
@Bean
37+
ElicitationHandler elicitationHandler() {
38+
return new ElicitationHandler();
39+
}
40+
41+
// Ensure that we don't blow up on non-singleton beans
42+
@Bean
43+
@Scope(scopeName = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
44+
Foo foo() {
45+
return new Foo();
46+
}
47+
48+
// Ensure that we don't blow up on non-singleton beans
49+
@Bean
50+
@RequestScope
51+
Bar bar(Foo foo) {
52+
return new Bar();
53+
}
54+
55+
record ElicitationHandler() {
56+
57+
@McpElicitation(clients = "server1")
58+
public StructuredElicitResult<ElicitInput> elicitationHandler(McpSchema.ElicitRequest request) {
59+
logger.info("MCP ELICITATION: {}", request);
60+
ElicitInput elicitData = new ElicitInput(request.message());
61+
return StructuredElicitResult.builder().structuredContent(elicitData).build();
62+
}
63+
64+
}
65+
66+
public record ElicitInput(String message) {
67+
}
68+
69+
public static class Foo {
70+
71+
}
72+
73+
public static class Bar {
74+
75+
}
76+
77+
}

auto-configurations/mcp/spring-ai-autoconfigure-mcp-server-webflux/src/test/java/org/springframework/ai/mcp/server/autoconfigure/capabilities/McpHandlerService.java

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,7 @@
1919
import io.modelcontextprotocol.spec.McpSchema;
2020
import org.slf4j.Logger;
2121
import org.slf4j.LoggerFactory;
22-
import org.springaicommunity.mcp.annotation.McpElicitation;
2322
import org.springaicommunity.mcp.annotation.McpSampling;
24-
import org.springaicommunity.mcp.context.StructuredElicitResult;
2523

2624
import org.springframework.ai.chat.client.ChatClient;
2725
import org.springframework.stereotype.Service;
@@ -51,14 +49,4 @@ public McpSchema.CreateMessageResult samplingHandler(McpSchema.CreateMessageRequ
5149
.build();
5250
}
5351

54-
@McpElicitation(clients = "server1")
55-
public StructuredElicitResult<ElicitInput> elicitationHandler(McpSchema.ElicitRequest request) {
56-
logger.info("MCP ELICITATION: {}", request);
57-
ElicitInput elicitData = new ElicitInput(request.message());
58-
return StructuredElicitResult.builder().structuredContent(elicitData).build();
59-
}
60-
61-
public record ElicitInput(String message) {
62-
}
63-
6452
}

mcp/mcp-annotations-spring/src/main/java/org/springframework/ai/mcp/annotation/spring/AbstractClientMcpHandlerRegistry.java

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@
3535
import org.springaicommunity.mcp.annotation.McpSampling;
3636
import org.springaicommunity.mcp.annotation.McpToolListChanged;
3737

38+
import org.springframework.aop.framework.autoproxy.AutoProxyUtils;
3839
import org.springframework.beans.BeansException;
39-
import org.springframework.beans.factory.config.BeanDefinition;
4040
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
4141
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
4242
import org.springframework.core.annotation.AnnotationUtils;
@@ -70,8 +70,11 @@ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
7070
Map<String, List<String>> elicitationClientToAnnotatedBeans = new HashMap<>();
7171
Map<String, List<String>> samplingClientToAnnotatedBeans = new HashMap<>();
7272
for (var beanName : beanFactory.getBeanDefinitionNames()) {
73-
var definition = beanFactory.getBeanDefinition(beanName);
74-
var foundAnnotations = scan(getBeanClass(definition, beanFactory.getBeanClassLoader()));
73+
if (!beanFactory.getBeanDefinition(beanName).isSingleton()) {
74+
// Only process singleton beans, not scoped beans
75+
continue;
76+
}
77+
var foundAnnotations = scan(AutoProxyUtils.determineTargetClass(beanFactory, beanName));
7578
if (!foundAnnotations.isEmpty()) {
7679
this.allAnnotatedBeans.add(beanName);
7780
}
@@ -118,23 +121,6 @@ else if (foundAnnotation instanceof McpElicitation elicitation) {
118121
.collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().build()));
119122
}
120123

121-
private static Class<?> getBeanClass(BeanDefinition definition, ClassLoader beanClassLoader) {
122-
if (definition.getResolvableType().resolve() != null) {
123-
return definition.getResolvableType().resolve();
124-
}
125-
// @Component beans registered by component scanning do not have a resolvable type
126-
// We try to resolve them using the beanClassName (which might be null)
127-
if (beanClassLoader != null && definition.getBeanClassName() != null) {
128-
try {
129-
return Class.forName(definition.getBeanClassName(), false, beanClassLoader);
130-
}
131-
catch (ClassNotFoundException ignored) {
132-
133-
}
134-
}
135-
return null;
136-
}
137-
138124
protected List<Annotation> scan(Class<?> beanClass) {
139125
List<Annotation> foundAnnotations = new ArrayList<>();
140126

mcp/mcp-annotations-spring/src/test/java/org/springframework/ai/mcp/annotation/spring/ClientMcpAsyncHandlersRegistryTests.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.springaicommunity.mcp.annotation.McpToolListChanged;
3434
import reactor.core.publisher.Mono;
3535

36+
import org.springframework.aop.framework.autoproxy.AutoProxyUtils;
3637
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
3738
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
3839

@@ -280,6 +281,20 @@ void supportsNonResolvableTypes() {
280281
assertThat(registry.getCapabilities("client-1").elicitation()).isNotNull();
281282
}
282283

284+
@Test
285+
void supportsProxiedClass() {
286+
var registry = new ClientMcpSyncHandlersRegistry();
287+
var beanFactory = new DefaultListableBeanFactory();
288+
var beanDefinition = BeanDefinitionBuilder.genericBeanDefinition(Object.class).getBeanDefinition();
289+
beanDefinition.setAttribute(AutoProxyUtils.ORIGINAL_TARGET_CLASS_ATTRIBUTE,
290+
ClientMcpSyncHandlersRegistryTests.ClientCapabilitiesConfiguration.class);
291+
beanFactory.registerBeanDefinition("myConfig", beanDefinition);
292+
293+
registry.postProcessBeanFactory(beanFactory);
294+
295+
assertThat(registry.getCapabilities("client-1").elicitation()).isNotNull();
296+
}
297+
283298
@Test
284299
@Disabled
285300
void missingHandler() {

mcp/mcp-annotations-spring/src/test/java/org/springframework/ai/mcp/annotation/spring/ClientMcpSyncHandlersRegistryTests.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.springaicommunity.mcp.annotation.McpSampling;
3333
import org.springaicommunity.mcp.annotation.McpToolListChanged;
3434

35+
import org.springframework.aop.framework.autoproxy.AutoProxyUtils;
3536
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
3637
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
3738

@@ -276,6 +277,20 @@ void supportsNonResolvableTypes() {
276277
assertThat(registry.getCapabilities("client-1").elicitation()).isNotNull();
277278
}
278279

280+
@Test
281+
void supportsProxiedClass() {
282+
var registry = new ClientMcpSyncHandlersRegistry();
283+
var beanFactory = new DefaultListableBeanFactory();
284+
var beanDefinition = BeanDefinitionBuilder.genericBeanDefinition(Object.class).getBeanDefinition();
285+
beanDefinition.setAttribute(AutoProxyUtils.ORIGINAL_TARGET_CLASS_ATTRIBUTE,
286+
ClientCapabilitiesConfiguration.class);
287+
beanFactory.registerBeanDefinition("myConfig", beanDefinition);
288+
289+
registry.postProcessBeanFactory(beanFactory);
290+
291+
assertThat(registry.getCapabilities("client-1").elicitation()).isNotNull();
292+
}
293+
279294
@Test
280295
@Disabled
281296
void missingHandler() {

0 commit comments

Comments
 (0)