Skip to content

Commit 74f8bff

Browse files
committed
Defer call of getToolCallbacks() to prevent initializing ToolCallbackProvider eagerly
Before this commit, `McpSyncClient` is initialized even if `spring.ai.mcp.client.initialized` is set to `false`. See GH-3232 Signed-off-by: Yanming Zhou <[email protected]>
1 parent 84efb6a commit 74f8bff

File tree

3 files changed

+91
-3
lines changed

3 files changed

+91
-3
lines changed

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.springframework.ai.tool.observation.ToolCallingContentObservationFilter;
3333
import org.springframework.ai.tool.observation.ToolCallingObservationConvention;
3434
import org.springframework.ai.tool.resolution.DelegatingToolCallbackResolver;
35+
import org.springframework.ai.tool.resolution.ProviderToolCallbackResolver;
3536
import org.springframework.ai.tool.resolution.SpringBeanToolCallbackResolver;
3637
import org.springframework.ai.tool.resolution.StaticToolCallbackResolver;
3738
import org.springframework.ai.tool.resolution.ToolCallbackResolver;
@@ -51,6 +52,7 @@
5152
* @author Thomas Vitale
5253
* @author Christian Tzolov
5354
* @author Daniel Garnier-Moiroux
55+
* @author Yanming Zhou
5456
* @since 1.0.0
5557
*/
5658
@AutoConfiguration
@@ -66,15 +68,17 @@ ToolCallbackResolver toolCallbackResolver(GenericApplicationContext applicationC
6668
List<ToolCallback> toolCallbacks, List<ToolCallbackProvider> tcbProviders) {
6769

6870
List<ToolCallback> allFunctionAndToolCallbacks = new ArrayList<>(toolCallbacks);
69-
tcbProviders.stream().map(pr -> List.of(pr.getToolCallbacks())).forEach(allFunctionAndToolCallbacks::addAll);
7071

7172
var staticToolCallbackResolver = new StaticToolCallbackResolver(allFunctionAndToolCallbacks);
7273

74+
var providerToolCallbackResolver = new ProviderToolCallbackResolver(tcbProviders);
75+
7376
var springBeanToolCallbackResolver = SpringBeanToolCallbackResolver.builder()
7477
.applicationContext(applicationContext)
7578
.build();
7679

77-
return new DelegatingToolCallbackResolver(List.of(staticToolCallbackResolver, springBeanToolCallbackResolver));
80+
return new DelegatingToolCallbackResolver(
81+
List.of(staticToolCallbackResolver, providerToolCallbackResolver, springBeanToolCallbackResolver));
7882
}
7983

8084
@Bean

auto-configurations/models/tool/spring-ai-autoconfigure-model-tool/src/test/java/org/springframework/ai/model/tool/autoconfigure/ToolCallingAutoConfigurationTests.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.util.function.Function;
2020

2121
import org.junit.jupiter.api.Test;
22+
import org.mockito.Mockito;
2223

2324
import org.springframework.ai.model.tool.DefaultToolCallingManager;
2425
import org.springframework.ai.model.tool.ToolCallingManager;
@@ -45,12 +46,16 @@
4546
import org.springframework.util.ReflectionUtils;
4647

4748
import static org.assertj.core.api.Assertions.assertThat;
49+
import static org.mockito.BDDMockito.then;
50+
import static org.mockito.Mockito.never;
51+
import static org.mockito.Mockito.times;
4852

4953
/**
5054
* Unit tests for {@link ToolCallingAutoConfiguration}.
5155
*
5256
* @author Thomas Vitale
5357
* @author Christian Tzolov
58+
* @author Yanming Zhou
5459
*/
5560
class ToolCallingAutoConfigurationTests {
5661

@@ -69,6 +74,19 @@ void beansAreCreated() {
6974
});
7075
}
7176

77+
@Test
78+
void deferGetToolCallbacks() {
79+
new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(ToolCallingAutoConfiguration.class))
80+
.withUserConfiguration(Config.class)
81+
.run(context -> {
82+
var toolCallbackResolver = context.getBean(ToolCallbackResolver.class);
83+
var toolCallbackProvider = context.getBean("toolCallbacks", ToolCallbackProvider.class);
84+
then(toolCallbackProvider).should(never()).getToolCallbacks();
85+
assertThat(toolCallbackResolver.resolve("getForecast")).isNotNull();
86+
then(toolCallbackProvider).should(times(1)).getToolCallbacks();
87+
});
88+
}
89+
7290
@Test
7391
void resolveMultipleFunctionAndToolCallbacks() {
7492
new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(ToolCallingAutoConfiguration.class))
@@ -212,7 +230,10 @@ static class Config {
212230
// ToolCallbacks.from(...) utility method.
213231
@Bean
214232
public ToolCallbackProvider toolCallbacks() {
215-
return MethodToolCallbackProvider.builder().toolObjects(new WeatherService()).build();
233+
ToolCallbackProvider toolCallbackProvider = MethodToolCallbackProvider.builder()
234+
.toolObjects(new WeatherService())
235+
.build();
236+
return Mockito.spy(toolCallbackProvider);
216237
}
217238

218239
@Bean
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Copyright 2023-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.ai.tool.resolution;
18+
19+
import java.util.List;
20+
import java.util.stream.Stream;
21+
22+
import org.slf4j.Logger;
23+
import org.slf4j.LoggerFactory;
24+
25+
import org.springframework.ai.tool.ToolCallback;
26+
import org.springframework.ai.tool.ToolCallbackProvider;
27+
import org.springframework.lang.Nullable;
28+
import org.springframework.util.Assert;
29+
import org.springframework.util.function.SingletonSupplier;
30+
31+
/**
32+
* A {@link ToolCallbackResolver} that resolves tool callbacks from
33+
* {@link ToolCallbackProvider} lazily.
34+
*
35+
* @author Yanming Zhou
36+
*/
37+
public class ProviderToolCallbackResolver implements ToolCallbackResolver {
38+
39+
private static final Logger logger = LoggerFactory.getLogger(ProviderToolCallbackResolver.class);
40+
41+
private final SingletonSupplier<List<ToolCallback>> toolCallbackSupplier;
42+
43+
public ProviderToolCallbackResolver(List<ToolCallbackProvider> toolCallbackProviders) {
44+
Assert.notNull(toolCallbackProviders, "toolCallbackProviders cannot be null");
45+
46+
this.toolCallbackSupplier = SingletonSupplier.of(() -> toolCallbackProviders.stream()
47+
.flatMap(provider -> Stream.of(provider.getToolCallbacks()))
48+
.toList());
49+
}
50+
51+
@Override
52+
@Nullable
53+
public ToolCallback resolve(String toolName) {
54+
Assert.hasText(toolName, "toolName cannot be null or empty");
55+
logger.debug("ToolCallback resolution attempt from tool callback provider");
56+
return this.toolCallbackSupplier.obtain()
57+
.stream()
58+
.filter(toolCallback -> toolName.equals(toolCallback.getToolDefinition().name()))
59+
.findAny()
60+
.orElse(null);
61+
}
62+
63+
}

0 commit comments

Comments
 (0)