Skip to content

Commit 9e83282

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 9e83282

File tree

3 files changed

+89
-3
lines changed

3 files changed

+89
-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: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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+
21+
import org.slf4j.Logger;
22+
import org.slf4j.LoggerFactory;
23+
24+
import org.springframework.ai.tool.ToolCallback;
25+
import org.springframework.ai.tool.ToolCallbackProvider;
26+
import org.springframework.lang.Nullable;
27+
import org.springframework.util.Assert;
28+
29+
/**
30+
* A {@link ToolCallbackResolver} that resolves tool callbacks from
31+
* {@link ToolCallbackProvider} lazily.
32+
*
33+
* @author Yanming Zhou
34+
*/
35+
public class ProviderToolCallbackResolver implements ToolCallbackResolver {
36+
37+
private static final Logger logger = LoggerFactory.getLogger(ProviderToolCallbackResolver.class);
38+
39+
private final List<ToolCallbackProvider> toolCallbackProviders;
40+
41+
public ProviderToolCallbackResolver(List<ToolCallbackProvider> toolCallbackProviders) {
42+
Assert.notNull(toolCallbackProviders, "toolCallbackProviders cannot be null");
43+
44+
this.toolCallbackProviders = toolCallbackProviders;
45+
}
46+
47+
@Override
48+
@Nullable
49+
public ToolCallback resolve(String toolName) {
50+
Assert.hasText(toolName, "toolName cannot be null or empty");
51+
for (ToolCallbackProvider provider : toolCallbackProviders) {
52+
for (ToolCallback toolCallback : provider.getToolCallbacks()) {
53+
if (toolName.equals(toolCallback.getToolDefinition().name())) {
54+
return toolCallback;
55+
}
56+
}
57+
}
58+
return null;
59+
}
60+
61+
}

0 commit comments

Comments
 (0)