diff --git a/mcp/mcp-annotations-spring/src/main/java/org/springframework/ai/mcp/annotation/spring/AbstractClientMcpHandlerRegistry.java b/mcp/mcp-annotations-spring/src/main/java/org/springframework/ai/mcp/annotation/spring/AbstractClientMcpHandlerRegistry.java index c1d5c84017e..dd56db4aa95 100644 --- a/mcp/mcp-annotations-spring/src/main/java/org/springframework/ai/mcp/annotation/spring/AbstractClientMcpHandlerRegistry.java +++ b/mcp/mcp-annotations-spring/src/main/java/org/springframework/ai/mcp/annotation/spring/AbstractClientMcpHandlerRegistry.java @@ -74,7 +74,14 @@ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) // Only process singleton beans, not scoped beans continue; } - var foundAnnotations = this.scan(AutoProxyUtils.determineTargetClass(beanFactory, beanName)); + Class beanClass = AutoProxyUtils.determineTargetClass(beanFactory, beanName); + if (beanClass == null) { + // If we cannot determine the bean class, we cannot scan it before + // it is really resolved. This is very likely an infrastructure-level + // bean, not a "service" type, skip it entirely. + continue; + } + var foundAnnotations = this.scan(beanClass); if (!foundAnnotations.isEmpty()) { this.allAnnotatedBeans.add(beanName); } diff --git a/mcp/mcp-annotations-spring/src/test/java/org/springframework/ai/mcp/annotation/spring/ClientMcpAsyncHandlersRegistryTests.java b/mcp/mcp-annotations-spring/src/test/java/org/springframework/ai/mcp/annotation/spring/ClientMcpAsyncHandlersRegistryTests.java index 6e7300bdd6f..6a04e8ac645 100644 --- a/mcp/mcp-annotations-spring/src/test/java/org/springframework/ai/mcp/annotation/spring/ClientMcpAsyncHandlersRegistryTests.java +++ b/mcp/mcp-annotations-spring/src/test/java/org/springframework/ai/mcp/annotation/spring/ClientMcpAsyncHandlersRegistryTests.java @@ -38,6 +38,7 @@ import org.springframework.beans.factory.support.DefaultListableBeanFactory; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.InstanceOfAssertFactories.type; @@ -340,6 +341,16 @@ void supportsProxiedClass() { assertThat(registry.getCapabilities("client-1").elicitation()).isNotNull(); } + @Test + void skipsUnknownBeanClass() { + var registry = new ClientMcpAsyncHandlersRegistry(); + var beanFactory = new DefaultListableBeanFactory(); + beanFactory.registerBeanDefinition("myConfig", + BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition()); + + assertThatNoException().isThrownBy(() -> registry.postProcessBeanFactory(beanFactory)); + } + static class ClientCapabilitiesConfiguration { @McpElicitation(clients = { "client-1", "client-2" }) diff --git a/mcp/mcp-annotations-spring/src/test/java/org/springframework/ai/mcp/annotation/spring/ClientMcpSyncHandlersRegistryTests.java b/mcp/mcp-annotations-spring/src/test/java/org/springframework/ai/mcp/annotation/spring/ClientMcpSyncHandlersRegistryTests.java index 9b75acf8aa6..8829c226e91 100644 --- a/mcp/mcp-annotations-spring/src/test/java/org/springframework/ai/mcp/annotation/spring/ClientMcpSyncHandlersRegistryTests.java +++ b/mcp/mcp-annotations-spring/src/test/java/org/springframework/ai/mcp/annotation/spring/ClientMcpSyncHandlersRegistryTests.java @@ -37,6 +37,7 @@ import org.springframework.beans.factory.support.DefaultListableBeanFactory; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.InstanceOfAssertFactories.type; @@ -332,6 +333,16 @@ void supportsProxiedClass() { assertThat(registry.getCapabilities("client-1").elicitation()).isNotNull(); } + @Test + void skipsUnknownBeanClass() { + var registry = new ClientMcpAsyncHandlersRegistry(); + var beanFactory = new DefaultListableBeanFactory(); + beanFactory.registerBeanDefinition("myConfig", + BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition()); + + assertThatNoException().isThrownBy(() -> registry.postProcessBeanFactory(beanFactory)); + } + static class ClientCapabilitiesConfiguration { @McpElicitation(clients = { "client-1", "client-2" })