Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -41,6 +44,7 @@
/**
* @author Christian Tzolov
* @author Josh Long
* @author Fu Jian
*/
@AutoConfiguration
@ConditionalOnClass(McpLogging.class)
Expand All @@ -51,7 +55,8 @@
public class McpClientAnnotationScannerAutoConfiguration {

private static final Set<Class<? extends Annotation>> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -43,6 +52,7 @@

/**
* @author Christian Tzolov
* @author Fu Jian
*/
@AutoConfiguration(after = McpClientAnnotationScannerAutoConfiguration.class)
@ConditionalOnClass(McpLogging.class)
Expand Down Expand Up @@ -79,6 +89,27 @@ List<SyncProgressSpecification> progressSpecs(ClientMcpAnnotatedBeans beansWithM
.progressSpecifications(beansWithMcpMethodAnnotations.getBeansByAnnotation(McpProgress.class));
}

@Bean
List<SyncToolListChangedSpecification> syncToolListChangedSpecs(
ClientMcpAnnotatedBeans beansWithMcpMethodAnnotations) {
return SyncMcpAnnotationProviders.toolListChangedSpecifications(
beansWithMcpMethodAnnotations.getBeansByAnnotation(McpToolListChanged.class));
}

@Bean
List<SyncResourceListChangedSpecification> syncResourceListChangedSpecs(
ClientMcpAnnotatedBeans beansWithMcpMethodAnnotations) {
return SyncMcpAnnotationProviders.resourceListChangedSpecifications(
beansWithMcpMethodAnnotations.getBeansByAnnotation(McpResourceListChanged.class));
}

@Bean
List<SyncPromptListChangedSpecification> syncPromptListChangedSpecs(
ClientMcpAnnotatedBeans beansWithMcpMethodAnnotations) {
return SyncMcpAnnotationProviders.promptListChangedSpecifications(
beansWithMcpMethodAnnotations.getBeansByAnnotation(McpPromptListChanged.class));
}

}

@Configuration(proxyBeanMethods = false)
Expand All @@ -105,6 +136,22 @@ List<AsyncProgressSpecification> progressSpecs(ClientMcpAnnotatedBeans beanRegis
return AsyncMcpAnnotationProviders.progressSpecifications(beanRegistry.getAllAnnotatedBeans());
}

@Bean
List<AsyncToolListChangedSpecification> asyncToolListChangedSpecs(ClientMcpAnnotatedBeans beanRegistry) {
return AsyncMcpAnnotationProviders.toolListChangedSpecifications(beanRegistry.getAllAnnotatedBeans());
}

@Bean
List<AsyncResourceListChangedSpecification> asyncResourceListChangedSpecs(
ClientMcpAnnotatedBeans beanRegistry) {
return AsyncMcpAnnotationProviders.resourceListChangedSpecifications(beanRegistry.getAllAnnotatedBeans());
}

@Bean
List<AsyncPromptListChangedSpecification> asyncPromptListChangedSpecs(ClientMcpAnnotatedBeans beanRegistry) {
return AsyncMcpAnnotationProviders.promptListChangedSpecifications(beanRegistry.getAllAnnotatedBeans());
}

}

}
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>
* 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<McpSchema.Tool> updatedTools) {
// Test handler for tool list changes
}

@McpResourceListChanged(clients = "test-client")
public void onResourceListChanged(List<McpSchema.Resource> updatedResources) {
// Test handler for resource list changes
}

@McpPromptListChanged(clients = "test-client")
public void onPromptListChanged(List<McpSchema.Prompt> updatedPrompts) {
// Test handler for prompt list changes
}

}

}