elicitationHandler(McpSchema.ElicitRequest request) {
+ logger.info("MCP ELICITATION: {}", request);
+ StreamableMcpAnnotationsWithLLMIT.ElicitInput elicitData = new StreamableMcpAnnotationsWithLLMIT.ElicitInput(
+ request.message());
+ return StructuredElicitResult.builder().structuredContent(elicitData).build();
+ }
+
+ }
+
+}
diff --git a/auto-configurations/models/tool/spring-ai-autoconfigure-model-tool/src/main/java/org/springframework/ai/model/tool/autoconfigure/ToolCallingAutoConfiguration.java b/auto-configurations/models/tool/spring-ai-autoconfigure-model-tool/src/main/java/org/springframework/ai/model/tool/autoconfigure/ToolCallingAutoConfiguration.java
index a7dfbc74f40..e5d6699cd69 100644
--- a/auto-configurations/models/tool/spring-ai-autoconfigure-model-tool/src/main/java/org/springframework/ai/model/tool/autoconfigure/ToolCallingAutoConfiguration.java
+++ b/auto-configurations/models/tool/spring-ai-autoconfigure-model-tool/src/main/java/org/springframework/ai/model/tool/autoconfigure/ToolCallingAutoConfiguration.java
@@ -17,6 +17,7 @@
package org.springframework.ai.model.tool.autoconfigure;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
import io.micrometer.observation.ObservationRegistry;
@@ -35,7 +36,14 @@
import org.springframework.ai.tool.resolution.SpringBeanToolCallbackResolver;
import org.springframework.ai.tool.resolution.StaticToolCallbackResolver;
import org.springframework.ai.tool.resolution.ToolCallbackResolver;
+import org.springframework.beans.BeansException;
import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.beans.factory.support.BeanDefinitionBuilder;
+import org.springframework.beans.factory.support.BeanDefinitionRegistry;
+import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
+import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
@@ -43,6 +51,7 @@
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.support.GenericApplicationContext;
+import org.springframework.core.ResolvableType;
import org.springframework.util.ClassUtils;
/**
@@ -56,17 +65,31 @@
@AutoConfiguration
@ConditionalOnClass(ChatModel.class)
@EnableConfigurationProperties(ToolCallingProperties.class)
-public class ToolCallingAutoConfiguration {
+public class ToolCallingAutoConfiguration implements BeanDefinitionRegistryPostProcessor {
private static final Logger logger = LoggerFactory.getLogger(ToolCallingAutoConfiguration.class);
+ // Marker qualifier to exclude MCP-related ToolCallbackProviders
+ private static final String EXCLUDE_MCP_TOOL_CALLBACK_PROVIDER = "org.springframework.ai.model.tool.autoconfigure.ToolCallingAutoConfiguration.toolcallbackprovider.mcp-excluded";
+
+ /**
+ * The default {@link ToolCallbackResolver} resolves tools by name for methods,
+ * functions, and {@link ToolCallbackProvider} beans.
+ *
+ * MCP providers should not be injected to avoid cyclic dependencies. If some MCP
+ * providers are injected, we filter them out to avoid eagerly calling
+ * #getToolCallbacks.
+ */
@Bean
@ConditionalOnMissingBean
ToolCallbackResolver toolCallbackResolver(GenericApplicationContext applicationContext,
- List toolCallbacks, List tcbProviders) {
-
+ List toolCallbacks,
+ @Qualifier(EXCLUDE_MCP_TOOL_CALLBACK_PROVIDER) List tcbProviders) {
List allFunctionAndToolCallbacks = new ArrayList<>(toolCallbacks);
- tcbProviders.stream().map(pr -> List.of(pr.getToolCallbacks())).forEach(allFunctionAndToolCallbacks::addAll);
+ tcbProviders.stream()
+ .filter(pr -> !isMcpToolCallbackProvider(ResolvableType.forInstance(pr)))
+ .map(pr -> List.of(pr.getToolCallbacks()))
+ .forEach(allFunctionAndToolCallbacks::addAll);
var staticToolCallbackResolver = new StaticToolCallbackResolver(allFunctionAndToolCallbacks);
@@ -77,6 +100,50 @@ ToolCallbackResolver toolCallbackResolver(GenericApplicationContext applicationC
return new DelegatingToolCallbackResolver(List.of(staticToolCallbackResolver, springBeanToolCallbackResolver));
}
+ /**
+ * Wrap {@link ToolCallbackProvider} beans that are not MCP-related into a named bean,
+ * which will be picked up by the
+ * {@link ToolCallingAutoConfiguration#toolCallbackResolver}.
+ *
+ * MCP providers must be excluded, because they may depend on a {@code ChatClient} to
+ * do sampling. The chat client, in turn, depends on a {@link ToolCallbackResolver}.
+ * To do the detection, we depend on the exposed bean type. If a bean uses a factory
+ * method which returns a {@link ToolCallbackProvider}, which is an MCP provider under
+ * the hood, it will be included in the list.
+ */
+ @Override
+ public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
+ if (!(registry instanceof DefaultListableBeanFactory beanFactory)) {
+ return;
+ }
+
+ var excludeMcpToolCallbackProviderBeanDefinition = BeanDefinitionBuilder
+ .genericBeanDefinition(List.class, () -> {
+ var providerNames = beanFactory.getBeanNamesForType(ToolCallbackProvider.class);
+ return Arrays.stream(providerNames)
+ .filter(name -> !isMcpToolCallbackProvider(beanFactory.getBeanDefinition(name).getResolvableType()))
+ .map(beanFactory::getBean)
+ .filter(ToolCallbackProvider.class::isInstance)
+ .map(ToolCallbackProvider.class::cast)
+ .toList();
+ })
+ .setScope(BeanDefinition.SCOPE_SINGLETON)
+ .setLazyInit(true)
+ .getBeanDefinition();
+
+ registry.registerBeanDefinition(EXCLUDE_MCP_TOOL_CALLBACK_PROVIDER,
+ excludeMcpToolCallbackProviderBeanDefinition);
+ }
+
+ private static boolean isMcpToolCallbackProvider(ResolvableType type) {
+ if (type.getType().getTypeName().equals("org.springframework.ai.mcp.SyncMcpToolCallbackProvider")
+ || type.getType().getTypeName().equals("org.springframework.ai.mcp.AsyncMcpToolCallbackProvider")) {
+ return true;
+ }
+ var superType = type.getSuperType();
+ return superType != ResolvableType.NONE && isMcpToolCallbackProvider(superType);
+ }
+
@Bean
@ConditionalOnMissingBean
ToolExecutionExceptionProcessor toolExecutionExceptionProcessor(ToolCallingProperties properties) {