Skip to content
Open
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 @@ -57,26 +57,26 @@ static class SyncClientSpecificationConfiguration {

@Bean
List<SyncLoggingSpecification> loggingSpecs(ClientMcpAnnotatedBeans beansWithMcpMethodAnnotations) {
return SyncMcpAnnotationProviders
.loggingSpecifications(beansWithMcpMethodAnnotations.getBeansByAnnotation(McpLogging.class));
return new SupplierBackedList<>(() -> SyncMcpAnnotationProviders
.loggingSpecifications(beansWithMcpMethodAnnotations.getBeansByAnnotation(McpLogging.class)));
}

@Bean
List<SyncSamplingSpecification> samplingSpecs(ClientMcpAnnotatedBeans beansWithMcpMethodAnnotations) {
return SyncMcpAnnotationProviders
.samplingSpecifications(beansWithMcpMethodAnnotations.getBeansByAnnotation(McpSampling.class));
return new SupplierBackedList<>(() -> SyncMcpAnnotationProviders
.samplingSpecifications(beansWithMcpMethodAnnotations.getBeansByAnnotation(McpSampling.class)));
}

@Bean
List<SyncElicitationSpecification> elicitationSpecs(ClientMcpAnnotatedBeans beansWithMcpMethodAnnotations) {
return SyncMcpAnnotationProviders
.elicitationSpecifications(beansWithMcpMethodAnnotations.getBeansByAnnotation(McpElicitation.class));
return new SupplierBackedList<>(() -> SyncMcpAnnotationProviders
.elicitationSpecifications(beansWithMcpMethodAnnotations.getBeansByAnnotation(McpElicitation.class)));
}

@Bean
List<SyncProgressSpecification> progressSpecs(ClientMcpAnnotatedBeans beansWithMcpMethodAnnotations) {
return SyncMcpAnnotationProviders
.progressSpecifications(beansWithMcpMethodAnnotations.getBeansByAnnotation(McpProgress.class));
return new SupplierBackedList<>(() -> SyncMcpAnnotationProviders
.progressSpecifications(beansWithMcpMethodAnnotations.getBeansByAnnotation(McpProgress.class)));
}

}
Expand All @@ -87,22 +87,26 @@ static class AsyncClientSpecificationConfiguration {

@Bean
List<AsyncLoggingSpecification> loggingSpecs(ClientMcpAnnotatedBeans beanRegistry) {
return AsyncMcpAnnotationProviders.loggingSpecifications(beanRegistry.getAllAnnotatedBeans());
return new SupplierBackedList<>(
() -> AsyncMcpAnnotationProviders.loggingSpecifications(beanRegistry.getAllAnnotatedBeans()));
}

@Bean
List<AsyncSamplingSpecification> samplingSpecs(ClientMcpAnnotatedBeans beanRegistry) {
return AsyncMcpAnnotationProviders.samplingSpecifications(beanRegistry.getAllAnnotatedBeans());
return new SupplierBackedList<>(
() -> AsyncMcpAnnotationProviders.samplingSpecifications(beanRegistry.getAllAnnotatedBeans()));
}

@Bean
List<AsyncElicitationSpecification> elicitationSpecs(ClientMcpAnnotatedBeans beanRegistry) {
return AsyncMcpAnnotationProviders.elicitationSpecifications(beanRegistry.getAllAnnotatedBeans());
return new SupplierBackedList<>(
() -> AsyncMcpAnnotationProviders.elicitationSpecifications(beanRegistry.getAllAnnotatedBeans()));
}

@Bean
List<AsyncProgressSpecification> progressSpecs(ClientMcpAnnotatedBeans beanRegistry) {
return AsyncMcpAnnotationProviders.progressSpecifications(beanRegistry.getAllAnnotatedBeans());
return new SupplierBackedList<>(
() -> AsyncMcpAnnotationProviders.progressSpecifications(beanRegistry.getAllAnnotatedBeans()));
}

}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* 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.AbstractList;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.function.Supplier;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

/**
* A simple {@link java.util.List} backed by a {@link Supplier} of lists. Each access
* reads from the supplier, so the contents reflect the supplier's current state.
*/
/**
* @author Kuntal Maity
*/
final class SupplierBackedList<T> extends AbstractList<T> {

private final Supplier<List<T>> supplier;

SupplierBackedList(Supplier<List<T>> supplier) {
this.supplier = Objects.requireNonNull(supplier, "supplier must not be null");
}

@Override
public T get(int index) {
return this.supplier.get().get(index);
}

@Override
public int size() {
return this.supplier.get().size();
}

@Override
public Iterator<T> iterator() {
// Iterate over a snapshot for iteration consistency
return List.copyOf(this.supplier.get()).iterator();
}

@Override
public Spliterator<T> spliterator() {
return Spliterators.spliterator(iterator(), size(), Spliterator.ORDERED | Spliterator.SIZED);
}

@Override
public Stream<T> stream() {
return StreamSupport.stream(spliterator(), false);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* 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.ProgressNotification;
import org.junit.jupiter.api.Test;
import org.springaicommunity.mcp.annotation.McpProgress;
import org.springaicommunity.mcp.method.progress.SyncProgressSpecification;

import org.springframework.ai.mcp.client.common.autoconfigure.annotations.McpClientAnnotationScannerAutoConfiguration.ClientMcpAnnotatedBeans;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;

import static org.assertj.core.api.Assertions.assertThat;

/**
* Reproduction test for ordering bug where McpClientSpecificationFactoryAutoConfiguration
* is created before any @Component beans with @McpProgress (or other MCP annotations) are
* instantiated, resulting in empty specification lists.
*/
class McpClientSpecOrderingReproTests {

private final ApplicationContextRunner runner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(McpClientAnnotationScannerAutoConfiguration.class,
McpClientSpecificationFactoryAutoConfiguration.class))
.withUserConfiguration(ScanConfig.class);

@Test
void progressSpecsIncludeScannedComponent_evenWhenCreatedAfterSpecsBean() {
this.runner.run(ctx -> {
// 1) Trigger spec list bean creation early
@SuppressWarnings("unchecked")
List<SyncProgressSpecification> specs = (List<SyncProgressSpecification>) ctx.getBean("progressSpecs");

// 2) Now force creation of the scanned @Component (post-processor runs here)
ctx.getBean(ScannedClientHandlers.class);

// 3) Registry sees the component…
ClientMcpAnnotatedBeans registry = ctx.getBean(ClientMcpAnnotatedBeans.class);
assertThat(registry.getBeansByAnnotation(McpProgress.class)).hasSize(1);

// 4) Expected behavior: specs reflect newly-registered handler
// Under the bug, this assertion fails (list stays empty)
assertThat(specs).hasSize(1);
});
}

@Configuration
@ComponentScan(basePackageClasses = ScannedClientHandlers.class)
static class ScanConfig {

}

@Component
@Lazy
static class ScannedClientHandlers {

@McpProgress(clients = "server1")
public void onProgress(ProgressNotification pn) {
}

}

}