Skip to content

Commit 9c2af5e

Browse files
committed
Simplify ToolCallbackResolver auto-configuration.
- In febf86c, we broke a dependency cycle ChatClient -> McpClient - With the introduction of ClientMcpSyncHandlersRegistry and the async variant, there is no dependency McpClient -> MCP handlers anymore, breaking the cycle in a simpler way. - Here, we revert most of the changes of febf86c, but keep the tests. Signed-off-by: Daniel Garnier-Moiroux <[email protected]>
1 parent 1068d28 commit 9c2af5e

File tree

2 files changed

+14
-84
lines changed

2 files changed

+14
-84
lines changed

auto-configurations/mcp/spring-ai-autoconfigure-mcp-client-webflux/src/test/java/org/springframework/ai/mcp/client/webflux/autoconfigure/McpToolsConfigurationTests.java

Lines changed: 11 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -120,26 +120,7 @@ void toolCallbacksRegistered() {
120120
assertThat(resolver.resolve("customToolCallbackProvider")).isNotNull();
121121

122122
// MCP toolcallback providers are never added to the resolver
123-
124-
// Bean graph setup
125-
var injectedProviders = (List<ToolCallbackProvider>) ctx.getBean(
126-
"org.springframework.ai.model.tool.autoconfigure.ToolCallingAutoConfiguration.toolcallbackprovider.mcp-excluded");
127-
// Beans exposed as non-MCP
128-
var toolCallbackProvider = (ToolCallbackProvider) ctx.getBean("toolCallbackProvider");
129-
var customToolCallbackProvider = (ToolCallbackProvider) ctx.getBean("customToolCallbackProvider");
130-
// This is injected in the resolver bean, because it's exposed as a
131-
// ToolCallbackProvider, but it's not added to the resolver
132-
var genericMcpToolCallbackProvider = (ToolCallbackProvider) ctx.getBean("genericMcpToolCallbackProvider");
133-
134-
// beans exposed as MCP
135-
var mcpToolCallbackProvider = (ToolCallbackProvider) ctx.getBean("mcpToolCallbackProvider");
136-
var customMcpToolCallbackProvider = (ToolCallbackProvider) ctx.getBean("customMcpToolCallbackProvider");
137-
138-
assertThat(injectedProviders)
139-
.containsExactlyInAnyOrder(toolCallbackProvider, customToolCallbackProvider,
140-
genericMcpToolCallbackProvider)
141-
.doesNotContain(mcpToolCallbackProvider, customMcpToolCallbackProvider);
142-
123+
// Otherwise, they would throw.
143124
});
144125
}
145126

@@ -192,29 +173,27 @@ ToolCallbackProvider toolCallbackProvider() {
192173
return tcp;
193174
}
194175

195-
// This bean depends on the resolver, to ensure there are no cyclic dependencies
196176
@Bean
197-
SyncMcpToolCallbackProvider mcpToolCallbackProvider(ToolCallbackResolver resolver) {
177+
CustomToolCallbackProvider customToolCallbackProvider() {
178+
return new CustomToolCallbackProvider("customToolCallbackProvider");
179+
}
180+
181+
// Ignored by the resolver
182+
@Bean
183+
SyncMcpToolCallbackProvider mcpToolCallbackProvider() {
198184
var tcp = mock(SyncMcpToolCallbackProvider.class);
199185
when(tcp.getToolCallbacks())
200186
.thenThrow(new RuntimeException("mcpToolCallbackProvider#getToolCallbacks should not be called"));
201187
return tcp;
202188
}
203189

190+
// Ignored by the resolver
204191
@Bean
205-
CustomToolCallbackProvider customToolCallbackProvider() {
206-
return new CustomToolCallbackProvider("customToolCallbackProvider");
207-
}
208-
209-
// This bean depends on the resolver, to ensure there are no cyclic dependencies
210-
@Bean
211-
CustomMcpToolCallbackProvider customMcpToolCallbackProvider(ToolCallbackResolver resolver) {
192+
CustomMcpToolCallbackProvider customMcpToolCallbackProvider() {
212193
return new CustomMcpToolCallbackProvider();
213194
}
214195

215-
// This will be added to the resolver, because the visible type of the bean
216-
// is ToolCallbackProvider ; we would need to actually instantiate the bean
217-
// to find out that it is MCP-related
196+
// Ignored by the resolver
218197
@Bean
219198
ToolCallbackProvider genericMcpToolCallbackProvider() {
220199
return new CustomMcpToolCallbackProvider();

auto-configurations/models/tool/spring-ai-autoconfigure-model-tool/src/main/java/org/springframework/ai/model/tool/autoconfigure/ToolCallingAutoConfiguration.java

Lines changed: 3 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
package org.springframework.ai.model.tool.autoconfigure;
1818

1919
import java.util.ArrayList;
20-
import java.util.Arrays;
2120
import java.util.List;
2221

2322
import io.micrometer.observation.ObservationRegistry;
@@ -36,14 +35,7 @@
3635
import org.springframework.ai.tool.resolution.SpringBeanToolCallbackResolver;
3736
import org.springframework.ai.tool.resolution.StaticToolCallbackResolver;
3837
import org.springframework.ai.tool.resolution.ToolCallbackResolver;
39-
import org.springframework.beans.BeansException;
4038
import org.springframework.beans.factory.ObjectProvider;
41-
import org.springframework.beans.factory.annotation.Qualifier;
42-
import org.springframework.beans.factory.config.BeanDefinition;
43-
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
44-
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
45-
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
46-
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
4739
import org.springframework.boot.autoconfigure.AutoConfiguration;
4840
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
4941
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
@@ -65,26 +57,20 @@
6557
@AutoConfiguration
6658
@ConditionalOnClass(ChatModel.class)
6759
@EnableConfigurationProperties(ToolCallingProperties.class)
68-
public class ToolCallingAutoConfiguration implements BeanDefinitionRegistryPostProcessor {
60+
public class ToolCallingAutoConfiguration {
6961

7062
private static final Logger logger = LoggerFactory.getLogger(ToolCallingAutoConfiguration.class);
7163

72-
// Marker qualifier to exclude MCP-related ToolCallbackProviders
73-
private static final String EXCLUDE_MCP_TOOL_CALLBACK_PROVIDER = "org.springframework.ai.model.tool.autoconfigure.ToolCallingAutoConfiguration.toolcallbackprovider.mcp-excluded";
74-
7564
/**
7665
* The default {@link ToolCallbackResolver} resolves tools by name for methods,
7766
* functions, and {@link ToolCallbackProvider} beans.
7867
* <p>
79-
* MCP providers should not be injected to avoid cyclic dependencies. If some MCP
80-
* providers are injected, we filter them out to avoid eagerly calling
81-
* #getToolCallbacks.
68+
* MCP providers are excluded, to avoid initializing them early with #listTools().
8269
*/
8370
@Bean
8471
@ConditionalOnMissingBean
8572
ToolCallbackResolver toolCallbackResolver(GenericApplicationContext applicationContext,
86-
List<ToolCallback> toolCallbacks,
87-
@Qualifier(EXCLUDE_MCP_TOOL_CALLBACK_PROVIDER) List<ToolCallbackProvider> tcbProviders) {
73+
List<ToolCallback> toolCallbacks, List<ToolCallbackProvider> tcbProviders) {
8874
List<ToolCallback> allFunctionAndToolCallbacks = new ArrayList<>(toolCallbacks);
8975
tcbProviders.stream()
9076
.filter(pr -> !isMcpToolCallbackProvider(ResolvableType.forInstance(pr)))
@@ -100,41 +86,6 @@ ToolCallbackResolver toolCallbackResolver(GenericApplicationContext applicationC
10086
return new DelegatingToolCallbackResolver(List.of(staticToolCallbackResolver, springBeanToolCallbackResolver));
10187
}
10288

103-
/**
104-
* Wrap {@link ToolCallbackProvider} beans that are not MCP-related into a named bean,
105-
* which will be picked up by the
106-
* {@link ToolCallingAutoConfiguration#toolCallbackResolver}.
107-
* <p>
108-
* MCP providers must be excluded, because they may depend on a {@code ChatClient} to
109-
* do sampling. The chat client, in turn, depends on a {@link ToolCallbackResolver}.
110-
* To do the detection, we depend on the exposed bean type. If a bean uses a factory
111-
* method which returns a {@link ToolCallbackProvider}, which is an MCP provider under
112-
* the hood, it will be included in the list.
113-
*/
114-
@Override
115-
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
116-
if (!(registry instanceof DefaultListableBeanFactory beanFactory)) {
117-
return;
118-
}
119-
120-
var excludeMcpToolCallbackProviderBeanDefinition = BeanDefinitionBuilder
121-
.genericBeanDefinition(List.class, () -> {
122-
var providerNames = beanFactory.getBeanNamesForType(ToolCallbackProvider.class);
123-
return Arrays.stream(providerNames)
124-
.filter(name -> !isMcpToolCallbackProvider(beanFactory.getBeanDefinition(name).getResolvableType()))
125-
.map(beanFactory::getBean)
126-
.filter(ToolCallbackProvider.class::isInstance)
127-
.map(ToolCallbackProvider.class::cast)
128-
.toList();
129-
})
130-
.setScope(BeanDefinition.SCOPE_SINGLETON)
131-
.setLazyInit(true)
132-
.getBeanDefinition();
133-
134-
registry.registerBeanDefinition(EXCLUDE_MCP_TOOL_CALLBACK_PROVIDER,
135-
excludeMcpToolCallbackProviderBeanDefinition);
136-
}
137-
13889
private static boolean isMcpToolCallbackProvider(ResolvableType type) {
13990
if (type.getType().getTypeName().equals("org.springframework.ai.mcp.SyncMcpToolCallbackProvider")
14091
|| type.getType().getTypeName().equals("org.springframework.ai.mcp.AsyncMcpToolCallbackProvider")) {

0 commit comments

Comments
 (0)