diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/annotations/McpClientAnnotationScannerAutoConfiguration.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/annotations/McpClientAnnotationScannerAutoConfiguration.java index f2b8632a013..8ce05bcbe07 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/annotations/McpClientAnnotationScannerAutoConfiguration.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/annotations/McpClientAnnotationScannerAutoConfiguration.java @@ -22,7 +22,10 @@ import org.springaicommunity.mcp.annotation.McpElicitation; import org.springaicommunity.mcp.annotation.McpLogging; import org.springaicommunity.mcp.annotation.McpProgress; +import org.springaicommunity.mcp.annotation.McpPromptListChanged; +import org.springaicommunity.mcp.annotation.McpResourceListChanged; import org.springaicommunity.mcp.annotation.McpSampling; +import org.springaicommunity.mcp.annotation.McpToolListChanged; import org.springframework.ai.mcp.annotation.spring.scan.AbstractAnnotatedMethodBeanFactoryInitializationAotProcessor; import org.springframework.ai.mcp.annotation.spring.scan.AbstractAnnotatedMethodBeanPostProcessor; @@ -41,6 +44,7 @@ /** * @author Christian Tzolov * @author Josh Long + * @author Fu Jian */ @AutoConfiguration @ConditionalOnClass(McpLogging.class) @@ -51,7 +55,8 @@ public class McpClientAnnotationScannerAutoConfiguration { private static final Set> CLIENT_MCP_ANNOTATIONS = Set.of(McpLogging.class, - McpSampling.class, McpElicitation.class, McpProgress.class); + McpSampling.class, McpElicitation.class, McpProgress.class, McpToolListChanged.class, + McpResourceListChanged.class, McpPromptListChanged.class); @Bean @ConditionalOnMissingBean diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/annotations/McpClientSpecificationFactoryAutoConfiguration.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/annotations/McpClientSpecificationFactoryAutoConfiguration.java index b28eac7d677..620028f0e63 100644 --- a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/annotations/McpClientSpecificationFactoryAutoConfiguration.java +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/main/java/org/springframework/ai/mcp/client/common/autoconfigure/annotations/McpClientSpecificationFactoryAutoConfiguration.java @@ -21,7 +21,16 @@ import org.springaicommunity.mcp.annotation.McpElicitation; import org.springaicommunity.mcp.annotation.McpLogging; import org.springaicommunity.mcp.annotation.McpProgress; +import org.springaicommunity.mcp.annotation.McpPromptListChanged; +import org.springaicommunity.mcp.annotation.McpResourceListChanged; import org.springaicommunity.mcp.annotation.McpSampling; +import org.springaicommunity.mcp.annotation.McpToolListChanged; +import org.springaicommunity.mcp.method.changed.prompt.AsyncPromptListChangedSpecification; +import org.springaicommunity.mcp.method.changed.prompt.SyncPromptListChangedSpecification; +import org.springaicommunity.mcp.method.changed.resource.AsyncResourceListChangedSpecification; +import org.springaicommunity.mcp.method.changed.resource.SyncResourceListChangedSpecification; +import org.springaicommunity.mcp.method.changed.tool.AsyncToolListChangedSpecification; +import org.springaicommunity.mcp.method.changed.tool.SyncToolListChangedSpecification; import org.springaicommunity.mcp.method.elicitation.AsyncElicitationSpecification; import org.springaicommunity.mcp.method.elicitation.SyncElicitationSpecification; import org.springaicommunity.mcp.method.logging.AsyncLoggingSpecification; @@ -43,6 +52,7 @@ /** * @author Christian Tzolov + * @author Fu Jian */ @AutoConfiguration(after = McpClientAnnotationScannerAutoConfiguration.class) @ConditionalOnClass(McpLogging.class) @@ -79,6 +89,27 @@ List progressSpecs(ClientMcpAnnotatedBeans beansWithM .progressSpecifications(beansWithMcpMethodAnnotations.getBeansByAnnotation(McpProgress.class)); } + @Bean + List syncToolListChangedSpecs( + ClientMcpAnnotatedBeans beansWithMcpMethodAnnotations) { + return SyncMcpAnnotationProviders.toolListChangedSpecifications( + beansWithMcpMethodAnnotations.getBeansByAnnotation(McpToolListChanged.class)); + } + + @Bean + List syncResourceListChangedSpecs( + ClientMcpAnnotatedBeans beansWithMcpMethodAnnotations) { + return SyncMcpAnnotationProviders.resourceListChangedSpecifications( + beansWithMcpMethodAnnotations.getBeansByAnnotation(McpResourceListChanged.class)); + } + + @Bean + List syncPromptListChangedSpecs( + ClientMcpAnnotatedBeans beansWithMcpMethodAnnotations) { + return SyncMcpAnnotationProviders.promptListChangedSpecifications( + beansWithMcpMethodAnnotations.getBeansByAnnotation(McpPromptListChanged.class)); + } + } @Configuration(proxyBeanMethods = false) @@ -105,6 +136,22 @@ List progressSpecs(ClientMcpAnnotatedBeans beanRegis return AsyncMcpAnnotationProviders.progressSpecifications(beanRegistry.getAllAnnotatedBeans()); } + @Bean + List asyncToolListChangedSpecs(ClientMcpAnnotatedBeans beanRegistry) { + return AsyncMcpAnnotationProviders.toolListChangedSpecifications(beanRegistry.getAllAnnotatedBeans()); + } + + @Bean + List asyncResourceListChangedSpecs( + ClientMcpAnnotatedBeans beanRegistry) { + return AsyncMcpAnnotationProviders.resourceListChangedSpecifications(beanRegistry.getAllAnnotatedBeans()); + } + + @Bean + List asyncPromptListChangedSpecs(ClientMcpAnnotatedBeans beanRegistry) { + return AsyncMcpAnnotationProviders.promptListChangedSpecifications(beanRegistry.getAllAnnotatedBeans()); + } + } } diff --git a/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/java/org/springframework/ai/mcp/client/common/autoconfigure/annotations/McpClientListChangedAnnotationsScanningIT.java b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/java/org/springframework/ai/mcp/client/common/autoconfigure/annotations/McpClientListChangedAnnotationsScanningIT.java new file mode 100644 index 00000000000..d00e3cc6b35 --- /dev/null +++ b/auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-common/src/test/java/org/springframework/ai/mcp/client/common/autoconfigure/annotations/McpClientListChangedAnnotationsScanningIT.java @@ -0,0 +1,119 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.ai.mcp.client.common.autoconfigure.annotations; + +import java.util.List; + +import io.modelcontextprotocol.spec.McpSchema; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springaicommunity.mcp.annotation.McpPromptListChanged; +import org.springaicommunity.mcp.annotation.McpResourceListChanged; +import org.springaicommunity.mcp.annotation.McpToolListChanged; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for MCP client list-changed annotations scanning. + * + *

+ * This test validates that the annotation scanner correctly identifies and processes + * {@code @McpToolListChanged}, {@code @McpResourceListChanged}, and + * {@code @McpPromptListChanged} annotations. + * + * @author Fu Jian + */ +public class McpClientListChangedAnnotationsScanningIT { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(McpClientAnnotationScannerAutoConfiguration.class, + McpClientSpecificationFactoryAutoConfiguration.class)); + + @ParameterizedTest + @ValueSource(strings = { "SYNC", "ASYNC" }) + void shouldScanAllThreeListChangedAnnotations(String clientType) { + String prefix = clientType.toLowerCase(); + + this.contextRunner.withUserConfiguration(AllListChangedConfiguration.class) + .withPropertyValues("spring.ai.mcp.client.type=" + clientType) + .run(context -> { + // Verify all three annotations were scanned + McpClientAnnotationScannerAutoConfiguration.ClientMcpAnnotatedBeans annotatedBeans = context + .getBean(McpClientAnnotationScannerAutoConfiguration.ClientMcpAnnotatedBeans.class); + assertThat(annotatedBeans.getBeansByAnnotation(McpToolListChanged.class)).hasSize(1); + assertThat(annotatedBeans.getBeansByAnnotation(McpResourceListChanged.class)).hasSize(1); + assertThat(annotatedBeans.getBeansByAnnotation(McpPromptListChanged.class)).hasSize(1); + + // Verify all three specification beans were created + assertThat(context).hasBean(prefix + "ToolListChangedSpecs"); + assertThat(context).hasBean(prefix + "ResourceListChangedSpecs"); + assertThat(context).hasBean(prefix + "PromptListChangedSpecs"); + }); + } + + @ParameterizedTest + @ValueSource(strings = { "SYNC", "ASYNC" }) + void shouldNotScanAnnotationsWhenScannerDisabled(String clientType) { + String prefix = clientType.toLowerCase(); + + this.contextRunner.withUserConfiguration(AllListChangedConfiguration.class) + .withPropertyValues("spring.ai.mcp.client.type=" + clientType, + "spring.ai.mcp.client.annotation-scanner.enabled=false") + .run(context -> { + // Verify scanner beans were not created + assertThat(context).doesNotHaveBean(McpClientAnnotationScannerAutoConfiguration.class); + assertThat(context).doesNotHaveBean(prefix + "ToolListChangedSpecs"); + assertThat(context).doesNotHaveBean(prefix + "ResourceListChangedSpecs"); + assertThat(context).doesNotHaveBean(prefix + "PromptListChangedSpecs"); + }); + } + + @Configuration + static class AllListChangedConfiguration { + + @Bean + TestListChangedHandlers testHandlers() { + return new TestListChangedHandlers(); + } + + } + + static class TestListChangedHandlers { + + @McpToolListChanged(clients = "test-client") + public void onToolListChanged(List updatedTools) { + // Test handler for tool list changes + } + + @McpResourceListChanged(clients = "test-client") + public void onResourceListChanged(List updatedResources) { + // Test handler for resource list changes + } + + @McpPromptListChanged(clients = "test-client") + public void onPromptListChanged(List updatedPrompts) { + // Test handler for prompt list changes + } + + } + +}