feat(autoconfigure): Support both FunctionCallback and ToolCallback in ToolCallingAutoConfiguration

- Extends the ToolCallingAutoConfiguration to support both FunctionCallback and ToolCallback types.
- The toolCallbackResolver bean now handles both callback types through ObjectProvider injection.
- Added comprehensive tests to verify the resolution of multiple function and tool callbacks.
- Introduce new StaticToolCallbackProvider implementation
- Update ToolCallbackProvider to return FunctionCallback[]
- Migrate from List to ToolCallbackProvider in configurations
- Update tests to use new provider pattern
- Enhance tool callback providers to support multiple clients
  - Refactor AsyncMcpToolCallbackProvider and SyncMcpToolCallbackProvider to handle multiple MCP clients
  - Add ToolCallbackProvider support to ChatClient API
  - Deprecate direct tool callback list methods in favor of providers
  - Fix typos in Closeable class names
  - Update MCP documentation with new examples and usage patterns

Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
This commit is contained in:
Christian Tzolov
2025-02-13 13:11:09 +01:00
parent 68ad742f4a
commit 1fdda61db8
16 changed files with 486 additions and 108 deletions

View File

@@ -27,10 +27,12 @@ import io.modelcontextprotocol.spec.McpSchema;
import org.springframework.ai.autoconfigure.mcp.client.configurer.McpAsyncClientConfigurer;
import org.springframework.ai.autoconfigure.mcp.client.configurer.McpSyncClientConfigurer;
import org.springframework.ai.autoconfigure.mcp.client.properties.McpClientCommonProperties;
import org.springframework.ai.mcp.McpToolUtils;
import org.springframework.ai.mcp.AsyncMcpToolCallbackProvider;
import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
import org.springframework.ai.mcp.customizer.McpAsyncClientCustomizer;
import org.springframework.ai.mcp.customizer.McpSyncClientCustomizer;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
@@ -176,9 +178,22 @@ public class McpClientAutoConfiguration {
@Bean
@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "SYNC",
matchIfMissing = true)
public List<ToolCallback> toolCallbacks(ObjectProvider<List<McpSyncClient>> mcpClientsProvider) {
public ToolCallbackProvider toolCallbacks(ObjectProvider<List<McpSyncClient>> mcpClientsProvider) {
List<McpSyncClient> mcpClients = mcpClientsProvider.stream().flatMap(List::stream).toList();
return McpToolUtils.getToolCallbacksFromSyncClients(mcpClients);
return new SyncMcpToolCallbackProvider(mcpClients);
}
/**
* @deprecated replaced by {@link #toolCallbacks(ObjectProvider)} that returns a
* {@link ToolCallbackProvider} instead of a list of {@link ToolCallback}
*/
@Deprecated
@Bean
@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "SYNC",
matchIfMissing = true)
public List<ToolCallback> toolCallbacksDeprecated(ObjectProvider<List<McpSyncClient>> mcpClientsProvider) {
List<McpSyncClient> mcpClients = mcpClientsProvider.stream().flatMap(List::stream).toList();
return List.of(new SyncMcpToolCallbackProvider(mcpClients).getToolCallbacks());
}
/**
@@ -189,7 +204,7 @@ public class McpClientAutoConfiguration {
* This class is responsible for closing all MCP sync clients when the application
* context is closed, preventing resource leaks.
*/
public record ClosebleMcpSyncClients(List<McpSyncClient> clients) implements AutoCloseable {
public record CloseableMcpSyncClients(List<McpSyncClient> clients) implements AutoCloseable {
@Override
public void close() {
@@ -205,8 +220,8 @@ public class McpClientAutoConfiguration {
@Bean
@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "SYNC",
matchIfMissing = true)
public ClosebleMcpSyncClients makeSyncClientsClosable(List<McpSyncClient> clients) {
return new ClosebleMcpSyncClients(clients);
public CloseableMcpSyncClients makeSyncClientsClosable(List<McpSyncClient> clients) {
return new CloseableMcpSyncClients(clients);
}
/**
@@ -263,14 +278,26 @@ public class McpClientAutoConfiguration {
return mcpSyncClients;
}
/**
* @deprecated replaced by {@link #asyncToolCallbacks(ObjectProvider)} that returns a
* {@link ToolCallbackProvider} instead of a list of {@link ToolCallback}
*/
@Deprecated
@Bean
@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC")
public List<ToolCallback> asyncToolCallbacks(ObjectProvider<List<McpAsyncClient>> mcpClientsProvider) {
public List<ToolCallback> asyncToolCallbacksDeprecated(ObjectProvider<List<McpAsyncClient>> mcpClientsProvider) {
List<McpAsyncClient> mcpClients = mcpClientsProvider.stream().flatMap(List::stream).toList();
return McpToolUtils.getToolCallbacksFromAsyncClinents(mcpClients);
return List.of(new AsyncMcpToolCallbackProvider(mcpClients).getToolCallbacks());
}
public record ClosebleMcpAsyncClients(List<McpAsyncClient> clients) implements AutoCloseable {
@Bean
@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC")
public ToolCallbackProvider asyncToolCallbacks(ObjectProvider<List<McpAsyncClient>> mcpClientsProvider) {
List<McpAsyncClient> mcpClients = mcpClientsProvider.stream().flatMap(List::stream).toList();
return new AsyncMcpToolCallbackProvider(mcpClients);
}
public record CloseableMcpAsyncClients(List<McpAsyncClient> clients) implements AutoCloseable {
@Override
public void close() {
this.clients.forEach(McpAsyncClient::close);
@@ -279,8 +306,8 @@ public class McpClientAutoConfiguration {
@Bean
@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC")
public ClosebleMcpAsyncClients makeAsynClientsClosable(List<McpAsyncClient> clients) {
return new ClosebleMcpAsyncClients(clients);
public CloseableMcpAsyncClients makeAsynClientsClosable(List<McpAsyncClient> clients) {
return new CloseableMcpAsyncClients(clients);
}
@Bean

View File

@@ -122,7 +122,7 @@ public class McpClientAutoConfigurationIT {
@Test
void closeableWrappersCreation() {
this.contextRunner.withUserConfiguration(TestTransportConfiguration.class).run(context -> {
assertThat(context).hasSingleBean(McpClientAutoConfiguration.ClosebleMcpSyncClients.class);
assertThat(context).hasSingleBean(McpClientAutoConfiguration.CloseableMcpSyncClients.class);
});
}

View File

@@ -16,9 +16,11 @@
package org.springframework.ai.autoconfigure.mcp.server;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Stream;
import io.modelcontextprotocol.server.McpAsyncServer;
import io.modelcontextprotocol.server.McpServer;
@@ -39,7 +41,9 @@ import io.modelcontextprotocol.spec.ServerMcpTransport;
import reactor.core.publisher.Mono;
import org.springframework.ai.mcp.McpToolUtils;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
@@ -135,7 +139,8 @@ public class MpcServerAutoConfiguration {
McpSchema.ServerCapabilities.Builder capabilitiesBuilder, McpServerProperties serverProperties,
ObjectProvider<List<SyncToolRegistration>> tools, ObjectProvider<List<SyncResourceRegistration>> resources,
ObjectProvider<List<SyncPromptRegistration>> prompts,
ObjectProvider<Consumer<List<McpSchema.Root>>> rootsChangeConsumers) {
ObjectProvider<Consumer<List<McpSchema.Root>>> rootsChangeConsumers,
List<ToolCallbackProvider> toolCallbackProvider) {
McpSchema.Implementation serverInfo = new Implementation(serverProperties.getName(),
serverProperties.getVersion());
@@ -143,7 +148,14 @@ public class MpcServerAutoConfiguration {
// Create the server with both tool and resource capabilities
SyncSpec serverBuilder = McpServer.sync(transport).serverInfo(serverInfo);
List<SyncToolRegistration> toolResgistrations = tools.stream().flatMap(List::stream).toList();
List<SyncToolRegistration> toolResgistrations = new ArrayList<>(tools.stream().flatMap(List::stream).toList());
List<ToolCallback> providerToolCallbacks = toolCallbackProvider.stream()
.map(pr -> List.of(pr.getToolCallbacks()))
.flatMap(List::stream)
.filter(fc -> fc instanceof ToolCallback)
.map(fc -> (ToolCallback) fc)
.toList();
toolResgistrations.addAll(McpToolUtils.toSyncToolRegistration(providerToolCallbacks));
if (!CollectionUtils.isEmpty(toolResgistrations)) {
serverBuilder.tools(toolResgistrations);
capabilitiesBuilder.tools(serverProperties.isToolChangeNotification());
@@ -191,7 +203,8 @@ public class MpcServerAutoConfiguration {
ObjectProvider<List<AsyncToolRegistration>> tools,
ObjectProvider<List<AsyncResourceRegistration>> resources,
ObjectProvider<List<AsyncPromptRegistration>> prompts,
ObjectProvider<Consumer<List<McpSchema.Root>>> rootsChangeConsumer) {
ObjectProvider<Consumer<List<McpSchema.Root>>> rootsChangeConsumer,
List<ToolCallbackProvider> toolCallbackProvider) {
McpSchema.Implementation serverInfo = new Implementation(serverProperties.getName(),
serverProperties.getVersion());
@@ -199,7 +212,14 @@ public class MpcServerAutoConfiguration {
// Create the server with both tool and resource capabilities
AsyncSpec serverBilder = McpServer.async(transport).serverInfo(serverInfo);
List<AsyncToolRegistration> toolResgistrations = tools.stream().flatMap(List::stream).toList();
List<AsyncToolRegistration> toolResgistrations = new ArrayList<>(tools.stream().flatMap(List::stream).toList());
List<ToolCallback> providerToolCallbacks = toolCallbackProvider.stream()
.map(pr -> List.of(pr.getToolCallbacks()))
.flatMap(List::stream)
.filter(fc -> fc instanceof ToolCallback)
.map(fc -> (ToolCallback) fc)
.toList();
toolResgistrations.addAll(McpToolUtils.toAsyncToolRegistration(providerToolCallbacks));
if (!CollectionUtils.isEmpty(toolResgistrations)) {
serverBilder.tools(toolResgistrations);
capabilitiesBuilder.tools(serverProperties.isToolChangeNotification());

View File

@@ -15,11 +15,12 @@
*/
package org.springframework.ai.mcp;
import java.util.ArrayList;
import java.util.List;
import io.modelcontextprotocol.client.McpAsyncClient;
import io.modelcontextprotocol.util.Assert;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.ToolCallbackProvider;
@@ -28,18 +29,20 @@ import org.springframework.util.CollectionUtils;
/**
* Implementation of {@link ToolCallbackProvider} that discovers and provides MCP tools
* asynchronously.
* asynchronously from one or more MCP servers.
* <p>
* This class acts as a tool provider for Spring AI, automatically discovering tools from
* an MCP server and making them available as Spring AI tools. It:
* multiple MCP servers and making them available as Spring AI tools. It:
* <ul>
* <li>Connects to an MCP server through an async client</li>
* <li>Lists and retrieves available tools from the server</li>
* <li>Connects to MCP servers through async clients</li>
* <li>Lists and retrieves available tools from each server asynchronously</li>
* <li>Creates {@link AsyncMcpToolCallback} instances for each discovered tool</li>
* <li>Validates tool names to prevent duplicates</li>
* <li>Validates tool names to prevent duplicates across all servers</li>
* </ul>
* <p>
* Example usage: <pre>{@code
* Example usage with a single client:
*
* <pre>{@code
* McpAsyncClient mcpClient = // obtain MCP client
* ToolCallbackProvider provider = new AsyncMcpToolCallbackProvider(mcpClient);
*
@@ -47,6 +50,19 @@ import org.springframework.util.CollectionUtils;
* ToolCallback[] tools = provider.getToolCallbacks();
* }</pre>
*
* Example usage with multiple clients:
*
* <pre>{@code
* List<McpAsyncClient> mcpClients = // obtain multiple MCP clients
* ToolCallbackProvider provider = new AsyncMcpToolCallbackProvider(mcpClients);
*
* // Get tools from all clients
* ToolCallback[] tools = provider.getToolCallbacks();
*
* // Or use the reactive API
* Flux<ToolCallback> toolsFlux = AsyncMcpToolCallbackProvider.asyncToolCallbacks(mcpClients);
* }</pre>
*
* @author Christian Tzolov
* @since 1.0.0
* @see ToolCallbackProvider
@@ -55,40 +71,61 @@ import org.springframework.util.CollectionUtils;
*/
public class AsyncMcpToolCallbackProvider implements ToolCallbackProvider {
private final McpAsyncClient mcpClient;
private final List<McpAsyncClient> mcpClients;
/**
* Creates a new {@code AsyncMcpToolCallbackProvider} instance.
* @param mcpClient the MCP client to use for discovering tools
* Creates a new {@code AsyncMcpToolCallbackProvider} instance with a list of MCP
* clients.
* @param mcpClients the list of MCP clients to use for discovering tools. Each client
* typically connects to a different MCP server, allowing tool discovery from multiple
* sources.
* @throws IllegalArgumentException if mcpClients is null
*/
public AsyncMcpToolCallbackProvider(McpAsyncClient mcpClient) {
this.mcpClient = mcpClient;
public AsyncMcpToolCallbackProvider(List<McpAsyncClient> mcpClients) {
Assert.notNull(mcpClients, "McpClients must not be null");
this.mcpClients = mcpClients;
}
public AsyncMcpToolCallbackProvider(McpAsyncClient... mcpClients) {
Assert.notNull(mcpClients, "McpClients must not be null");
this.mcpClients = List.of(mcpClients);
}
/**
* Discovers and returns all available tools from the MCP server asynchronously.
* Discovers and returns all available tools from the configured MCP servers.
* <p>
* This method:
* <ol>
* <li>Retrieves the list of tools from the MCP server</li>
* <li>Creates a {@link AsyncMcpToolCallback} for each tool</li>
* <li>Validates that there are no duplicate tool names</li>
* <li>Retrieves the list of tools from each MCP server asynchronously</li>
* <li>Creates a {@link AsyncMcpToolCallback} for each discovered tool</li>
* <li>Validates that there are no duplicate tool names across all servers</li>
* </ol>
* <p>
* Note: While the underlying tool discovery is asynchronous, this method blocks until
* all tools are discovered from all servers.
* @return an array of tool callbacks, one for each discovered tool
* @throws IllegalStateException if duplicate tool names are found
*/
@Override
public ToolCallback[] getToolCallbacks() {
var toolCallbacks = this.mcpClient.listTools()
.map(response -> response.tools()
.stream()
.map(tool -> new AsyncMcpToolCallback(this.mcpClient, tool))
.toArray(ToolCallback[]::new))
.block();
validateToolCallbacks(toolCallbacks);
List<ToolCallback> toolCallbackList = new ArrayList<>();
return toolCallbacks;
for (McpAsyncClient mcpClient : this.mcpClients) {
ToolCallback[] toolCallbacks = mcpClient.listTools()
.map(response -> response.tools()
.stream()
.map(tool -> new AsyncMcpToolCallback(mcpClient, tool))
.toArray(ToolCallback[]::new))
.block();
validateToolCallbacks(toolCallbacks);
toolCallbackList.addAll(List.of(toolCallbacks));
}
return toolCallbackList.toArray(new ToolCallback[0]);
}
/**
@@ -110,12 +147,19 @@ public class AsyncMcpToolCallbackProvider implements ToolCallbackProvider {
/**
* Creates a reactive stream of tool callbacks from multiple MCP clients.
* <p>
* This utility method:
* This utility method provides a reactive way to work with tool callbacks from
* multiple MCP clients in a single operation. It:
* <ol>
* <li>Takes a list of MCP clients</li>
* <li>Creates a provider for each client</li>
* <li>Retrieves and flattens all tool callbacks into a single stream</li>
* <li>Takes a list of MCP clients as input</li>
* <li>Creates a provider instance to manage all clients</li>
* <li>Retrieves tools from all clients asynchronously</li>
* <li>Combines them into a single reactive stream</li>
* <li>Ensures there are no naming conflicts between tools from different clients</li>
* </ol>
* <p>
* Unlike {@link #getToolCallbacks()}, this method provides a fully reactive way to
* work with tool callbacks, making it suitable for non-blocking applications. Any
* errors during tool discovery will be propagated through the returned Flux.
* @param mcpClients the list of MCP clients to create callbacks from
* @return a Flux of tool callbacks from all provided clients
*/
@@ -124,9 +168,7 @@ public class AsyncMcpToolCallbackProvider implements ToolCallbackProvider {
return Flux.empty();
}
return Flux.fromIterable(mcpClients)
.flatMap(mcpClient -> Mono.just(new AsyncMcpToolCallbackProvider(mcpClient).getToolCallbacks()))
.flatMap(callbacks -> Flux.fromArray(callbacks));
return Flux.fromArray(new AsyncMcpToolCallbackProvider(mcpClients).getToolCallbacks());
}
}

View File

@@ -211,10 +211,7 @@ public final class McpToolUtils {
if (CollectionUtils.isEmpty(mcpClients)) {
return List.of();
}
return mcpClients.stream()
.map(mcpClient -> List.of((new SyncMcpToolCallbackProvider(mcpClient).getToolCallbacks())))
.flatMap(List::stream)
.toList();
return List.of((new SyncMcpToolCallbackProvider(mcpClients).getToolCallbacks()));
}
/**
@@ -247,10 +244,7 @@ public final class McpToolUtils {
if (CollectionUtils.isEmpty(asynMcpClients)) {
return List.of();
}
return asynMcpClients.stream()
.map(mcpClient -> List.of((new AsyncMcpToolCallbackProvider(mcpClient).getToolCallbacks())))
.flatMap(List::stream)
.toList();
return List.of((new AsyncMcpToolCallbackProvider(asynMcpClients).getToolCallbacks()));
}
}

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.ai.mcp;
import java.util.ArrayList;
import java.util.List;
import io.modelcontextprotocol.client.McpSyncClient;
@@ -25,18 +26,21 @@ import org.springframework.ai.tool.util.ToolUtils;
import org.springframework.util.CollectionUtils;
/**
* Implementation of {@link ToolCallbackProvider} that discovers and provides MCP tools.
* Implementation of {@link ToolCallbackProvider} that discovers and provides MCP tools
* from one or more MCP servers.
* <p>
* This class acts as a tool provider for Spring AI, automatically discovering tools from
* an MCP server and making them available as Spring AI tools. It:
* multiple MCP servers and making them available as Spring AI tools. It:
* <ul>
* <li>Connects to an MCP server through a sync client</li>
* <li>Lists and retrieves available tools from the server</li>
* <li>Connects to one or more MCP servers through sync clients</li>
* <li>Lists and retrieves available tools from all connected servers</li>
* <li>Creates {@link SyncMcpToolCallback} instances for each discovered tool</li>
* <li>Validates tool names to prevent duplicates</li>
* <li>Validates tool names to prevent duplicates across all servers</li>
* </ul>
* <p>
* Example usage: <pre>{@code
* Example usage with a single client:
*
* <pre>{@code
* McpSyncClient mcpClient = // obtain MCP client
* ToolCallbackProvider provider = new SyncMcpToolCallbackProvider(mcpClient);
*
@@ -44,6 +48,16 @@ import org.springframework.util.CollectionUtils;
* ToolCallback[] tools = provider.getToolCallbacks();
* }</pre>
*
* Example usage with multiple clients:
*
* <pre>{@code
* List<McpSyncClient> mcpClients = // obtain multiple MCP clients
* ToolCallbackProvider provider = new SyncMcpToolCallbackProvider(mcpClients);
*
* // Get tools from all clients
* ToolCallback[] tools = provider.getToolCallbacks();
* }</pre>
*
* @author Christian Tzolov
* @since 1.0.0
* @see ToolCallbackProvider
@@ -53,24 +67,29 @@ import org.springframework.util.CollectionUtils;
public class SyncMcpToolCallbackProvider implements ToolCallbackProvider {
private final McpSyncClient mcpClient;
private final List<McpSyncClient> mcpClients;
/**
* Creates a new {@code SyncMcpToolCallbackProvider} instance.
* @param mcpClient the MCP client to use for discovering tools
* Creates a new {@code SyncMcpToolCallbackProvider} instance with a list of MCP
* clients.
* @param mcpClients the list of MCP clients to use for discovering tools
*/
public SyncMcpToolCallbackProvider(McpSyncClient mcpClient) {
this.mcpClient = mcpClient;
public SyncMcpToolCallbackProvider(List<McpSyncClient> mcpClients) {
this.mcpClients = mcpClients;
}
public SyncMcpToolCallbackProvider(McpSyncClient... mcpClients) {
this.mcpClients = List.of(mcpClients);
}
/**
* Discovers and returns all available tools from the MCP server.
* Discovers and returns all available tools from all connected MCP servers.
* <p>
* This method:
* <ol>
* <li>Retrieves the list of tools from the MCP server</li>
* <li>Creates a {@link SyncMcpToolCallback} for each tool</li>
* <li>Validates that there are no duplicate tool names</li>
* <li>Retrieves the list of tools from each connected MCP server</li>
* <li>Creates a {@link SyncMcpToolCallback} for each discovered tool</li>
* <li>Validates that there are no duplicate tool names across all servers</li>
* </ol>
* @return an array of tool callbacks, one for each discovered tool
* @throws IllegalStateException if duplicate tool names are found
@@ -78,16 +97,18 @@ public class SyncMcpToolCallbackProvider implements ToolCallbackProvider {
@Override
public ToolCallback[] getToolCallbacks() {
var toolCallbacks = this.mcpClient.listTools()
.tools()
.stream()
.map(tool -> new SyncMcpToolCallback(this.mcpClient, tool))
.toArray(ToolCallback[]::new);
validateToolCallbacks(toolCallbacks);
return toolCallbacks;
var toolCallbacks = new ArrayList<>();
mcpClients.stream().forEach(mcpClient -> {
toolCallbacks.addAll(mcpClient.listTools()
.tools()
.stream()
.map(tool -> new SyncMcpToolCallback(mcpClient, tool))
.toList());
});
var array = toolCallbacks.toArray(new ToolCallback[0]);
validateToolCallbacks(array);
return array;
}
/**
@@ -107,13 +128,15 @@ public class SyncMcpToolCallbackProvider implements ToolCallbackProvider {
}
/**
* Creates a list of tool callbacks from multiple MCP clients.
* Creates a consolidated list of tool callbacks from multiple MCP clients.
* <p>
* This utility method:
* This utility method provides a convenient way to create tool callbacks from
* multiple MCP clients in a single operation. It:
* <ol>
* <li>Takes a list of MCP clients</li>
* <li>Creates a provider for each client</li>
* <li>Retrieves and combines all tool callbacks into a single list</li>
* <li>Takes a list of MCP clients as input</li>
* <li>Creates a provider instance to manage all clients</li>
* <li>Retrieves tools from all clients and combines them into a single list</li>
* <li>Ensures there are no naming conflicts between tools from different clients</li>
* </ol>
* @param mcpClients the list of MCP clients to create callbacks from
* @return a list of tool callbacks from all provided clients
@@ -123,10 +146,7 @@ public class SyncMcpToolCallbackProvider implements ToolCallbackProvider {
if (CollectionUtils.isEmpty(mcpClients)) {
return List.of();
}
return mcpClients.stream()
.map(mcpClient -> List.of((new SyncMcpToolCallbackProvider(mcpClient).getToolCallbacks())))
.flatMap(List::stream)
.toList();
return List.of((new SyncMcpToolCallbackProvider(mcpClients).getToolCallbacks()));
}
}

View File

@@ -38,7 +38,7 @@ import org.springframework.ai.chat.client.advisor.api.CallAroundAdvisor;
import org.springframework.ai.chat.client.advisor.api.CallAroundAdvisorChain;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.model.tool.ToolCallingManager;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.tool.ToolCallbacks;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.execution.DefaultToolExecutionExceptionProcessor;
@@ -183,9 +183,8 @@ public class VertexAiGeminiPaymentTransactionMethodIT {
public static class TestConfiguration {
@Bean
public List<ToolCallback> paymentServiceTools() {
var tools = List.of(ToolCallbacks.from(new PaymentService()));
return tools;
public ToolCallbackProvider paymentServiceTools() {
return ToolCallbackProvider.from(List.of(ToolCallbacks.from(new PaymentService())));
}
@Bean
@@ -221,11 +220,11 @@ public class VertexAiGeminiPaymentTransactionMethodIT {
@Bean
ToolCallingManager toolCallingManager(GenericApplicationContext applicationContext,
List<ToolCallback> toolCallbacks, List<FunctionCallback> functionCallbacks,
List<ToolCallbackProvider> tcps, List<FunctionCallback> functionCallbacks,
ObjectProvider<ObservationRegistry> observationRegistry) {
List<FunctionCallback> allFunctionCallbacks = new ArrayList(functionCallbacks);
allFunctionCallbacks.addAll(toolCallbacks.stream().map(tc -> (FunctionCallback) tc).toList());
tcps.stream().map(pr -> List.of(pr.getToolCallbacks())).forEach(allFunctionCallbacks::addAll);
var staticToolCallbackResolver = new StaticToolCallbackResolver(allFunctionCallbacks);

View File

@@ -36,6 +36,7 @@ import org.springframework.ai.converter.StructuredOutputConverter;
import org.springframework.ai.model.Media;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.io.Resource;
import org.springframework.lang.Nullable;
@@ -223,6 +224,8 @@ public interface ChatClient {
ChatClientRequestSpec tools(Object... toolObjects);
ChatClientRequestSpec tools(ToolCallbackProvider... toolCallbackProviders);
@Deprecated
<I, O> ChatClientRequestSpec functions(FunctionCallback... functionCallbacks);
@@ -290,6 +293,8 @@ public interface ChatClient {
Builder defaultTools(Object... toolObjects);
Builder defaultTools(ToolCallbackProvider... toolCallbackProviders);
/**
* @deprecated in favor of {@link #defaultTools(String...)}
*/

View File

@@ -35,6 +35,7 @@ import io.micrometer.observation.ObservationRegistry;
import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.tool.ToolCallbacks;
import reactor.core.publisher.Flux;
import reactor.core.scheduler.Schedulers;
@@ -871,6 +872,16 @@ public class DefaultChatClient implements ChatClient {
return this;
}
@Override
public ChatClientRequestSpec tools(ToolCallbackProvider... toolCallbackProviders) {
Assert.notNull(toolCallbackProviders, "toolCallbackProviders cannot be null");
Assert.noNullElements(toolCallbackProviders, "toolCallbackProviders cannot contain null elements");
for (ToolCallbackProvider toolCallbackProvider : toolCallbackProviders) {
this.functionCallbacks.addAll(List.of(toolCallbackProvider.getToolCallbacks()));
}
return this;
}
@Deprecated // Use tools()
public ChatClientRequestSpec functions(String... functionBeanNames) {
return tools(functionBeanNames);

View File

@@ -36,6 +36,7 @@ import org.springframework.ai.chat.model.ToolContext;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.core.io.Resource;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -173,6 +174,12 @@ public class DefaultChatClientBuilder implements Builder {
return this;
}
@Override
public Builder defaultTools(ToolCallbackProvider... toolCallbackProviders) {
this.defaultRequest.tools(toolCallbackProviders);
return this;
}
@Deprecated // Use defaultTools()
public <I, O> Builder defaultFunction(String name, String description, java.util.function.Function<I, O> function) {
this.defaultRequest

View File

@@ -0,0 +1,90 @@
/*
* 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.tool;
import java.util.List;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.util.Assert;
/**
* A simple implementation of {@link ToolCallbackProvider} that maintains a static array
* of {@link FunctionCallback} objects. This provider is immutable after construction and
* provides a straightforward way to supply a fixed set of tool callbacks to AI models.
*
* <p>
* This implementation is thread-safe as it maintains an immutable array of callbacks that
* is set during construction and cannot be modified afterwards.
*
* <p>
* Example usage: <pre>{@code
* FunctionCallback callback1 = new MyFunctionCallback();
* FunctionCallback callback2 = new AnotherFunctionCallback();
*
* // Create provider with varargs constructor
* ToolCallbackProvider provider1 = new StaticToolCallbackProvider(callback1, callback2);
*
* // Or create provider with List constructor
* List<FunctionCallback> callbacks = Arrays.asList(callback1, callback2);
* ToolCallbackProvider provider2 = new StaticToolCallbackProvider(callbacks);
* }</pre>
*
* @author Christian Tzolov
* @since 1.0.0
* @see ToolCallbackProvider
* @see FunctionCallback
*/
public class StaticToolCallbackProvider implements ToolCallbackProvider {
private final FunctionCallback[] toolCallbacks;
/**
* Constructs a new StaticToolCallbackProvider with the specified array of function
* callbacks.
* @param toolCallbacks the array of function callbacks to be provided by this
* provider. Must not be null, though an empty array is permitted.
* @throws IllegalArgumentException if the toolCallbacks array is null
*/
public StaticToolCallbackProvider(FunctionCallback... toolCallbacks) {
Assert.notNull(toolCallbacks, "ToolCallbacks must not be null");
this.toolCallbacks = toolCallbacks;
}
/**
* Constructs a new StaticToolCallbackProvider with the specified list of function
* callbacks. The list is converted to an array internally.
* @param toolCallbacks the list of function callbacks to be provided by this
* provider. Must not be null and must not contain null elements.
* @throws IllegalArgumentException if the toolCallbacks list is null or contains null
* elements
*/
public StaticToolCallbackProvider(List<? extends FunctionCallback> toolCallbacks) {
Assert.noNullElements(toolCallbacks, "toolCallbacks cannot contain null elements");
this.toolCallbacks = toolCallbacks.toArray(new FunctionCallback[0]);
}
/**
* Returns the array of function callbacks held by this provider.
* @return an array containing all function callbacks provided during construction.
* The returned array is a direct reference to the internal array, as the callbacks
* are expected to be immutable.
*/
@Override
public FunctionCallback[] getToolCallbacks() {
return this.toolCallbacks;
}
}

View File

@@ -16,6 +16,10 @@
package org.springframework.ai.tool;
import java.util.List;
import org.springframework.ai.model.function.FunctionCallback;
/**
* Provides {@link ToolCallback} instances for tools defined in different sources.
*
@@ -24,6 +28,14 @@ package org.springframework.ai.tool;
*/
public interface ToolCallbackProvider {
ToolCallback[] getToolCallbacks();
FunctionCallback[] getToolCallbacks();
public static ToolCallbackProvider from(List<? extends FunctionCallback> toolCallbacks) {
return new StaticToolCallbackProvider(toolCallbacks);
}
public static ToolCallbackProvider from(FunctionCallback... toolCallbacks) {
return new StaticToolCallbackProvider(toolCallbacks);
}
}

View File

@@ -351,12 +351,14 @@ private List<McpSyncClient> mcpSyncClients; // For sync client
private List<McpAsyncClient> mcpAsyncClients; // For async client
----
Additionally, the registered MCP Tools with all MCP clients are provided as a list of ToolCallback instances:
Additionally, the registered MCP Tools with all MCP clients are provided as a list of ToolCallback
through a ToolCallbackProvider instance:
[source,java]
----
@Autowired
private List<ToolCallback> toolCallbacks;
private SyncMcpToolCallbackProvider toolCallbackProvider;
ToolCallback[] toolCallbacks = toolCallbackProvider.getToolCallbacks();
----
== Example Applications

View File

@@ -126,9 +126,9 @@ Allows servers to expose tools that can be invoked by language models. The MCP S
[source,java]
----
@Bean
public List<ToolCallback> myTools(...) {
public ToolCallbackProvider myTools(...) {
List<ToolCallback> tools = ...
return tools;
return ToolCallbackProvider.from(tools);
}
----
@@ -284,15 +284,15 @@ public class McpServerApplication {
SpringApplication.run(McpServerApplication.class, args);
}
@Bean
public List<ToolCallback> tools(WeatherService weatherService) {
return ToolCallbacks.from(weatherService);
}
@Bean
public ToolCallbackProvider weatherTools(WeatherService weatherService) {
return MethodToolCallbackProvider.builder().toolObjects(weatherService).build();
}
}
----
The auto-configuration will automatically register the tool callbacks as MCP tools.
You can have multiple beans producing lists of ToolCallbacks. The auto-configuration will merge them.
You can have multiple beans producing ToolCallbacks. The auto-configuration will merge them.
== Example Applications
* link:https://github.com/spring-projects/spring-ai-examples/tree/main/model-context-protocol/weather/starter-webflux-server[Weather Server (WebFlux)] - Spring AI MCP Server Boot Starter with WebFlux transport.

View File

@@ -16,10 +16,15 @@
package org.springframework.ai.autoconfigure.chat.model;
import java.util.ArrayList;
import java.util.List;
import io.micrometer.observation.ObservationRegistry;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.model.tool.ToolCallingManager;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.tool.execution.DefaultToolExecutionExceptionProcessor;
import org.springframework.ai.tool.execution.ToolExecutionExceptionProcessor;
import org.springframework.ai.tool.resolution.DelegatingToolCallbackResolver;
@@ -33,12 +38,11 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.context.annotation.Bean;
import org.springframework.context.support.GenericApplicationContext;
import java.util.List;
/**
* Auto-configuration for common tool calling features of {@link ChatModel}.
*
* @author Thomas Vitale
* @author Christian Tzolov
* @since 1.0.0
*/
@AutoConfiguration
@@ -48,8 +52,13 @@ public class ToolCallingAutoConfiguration {
@Bean
@ConditionalOnMissingBean
ToolCallbackResolver toolCallbackResolver(GenericApplicationContext applicationContext,
List<FunctionCallback> toolCallbacks) {
var staticToolCallbackResolver = new StaticToolCallbackResolver(toolCallbacks);
List<FunctionCallback> functionCallbacks, List<ToolCallbackProvider> tcbProviders) {
List<FunctionCallback> allFunctionAndToolCallbacks = new ArrayList<>(functionCallbacks);
tcbProviders.stream().map(pr -> List.of(pr.getToolCallbacks())).forEach(allFunctionAndToolCallbacks::addAll);
var staticToolCallbackResolver = new StaticToolCallbackResolver(allFunctionAndToolCallbacks);
var springBeanToolCallbackResolver = SpringBeanToolCallbackResolver.builder()
.applicationContext(applicationContext)
.build();

View File

@@ -16,15 +16,31 @@
package org.springframework.ai.autoconfigure.chat.model;
import java.util.function.Function;
import org.junit.jupiter.api.Test;
import org.springframework.ai.model.function.FunctionCallback;
import org.springframework.ai.model.tool.DefaultToolCallingManager;
import org.springframework.ai.model.tool.ToolCallingManager;
import org.springframework.ai.tool.StaticToolCallbackProvider;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.ToolCallbackProvider;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.definition.ToolDefinition;
import org.springframework.ai.tool.execution.DefaultToolExecutionExceptionProcessor;
import org.springframework.ai.tool.execution.ToolExecutionExceptionProcessor;
import org.springframework.ai.tool.function.FunctionToolCallback;
import org.springframework.ai.tool.method.MethodToolCallback;
import org.springframework.ai.tool.method.MethodToolCallbackProvider;
import org.springframework.ai.tool.resolution.DelegatingToolCallbackResolver;
import org.springframework.ai.tool.resolution.ToolCallbackResolver;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Description;
import org.springframework.util.ReflectionUtils;
import static org.assertj.core.api.Assertions.assertThat;
@@ -32,6 +48,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* Unit tests for {@link ToolCallingAutoConfiguration}.
*
* @author Thomas Vitale
* @author Christian Tzolov
*/
class ToolCallingAutoConfigurationTests {
@@ -50,4 +67,127 @@ class ToolCallingAutoConfigurationTests {
});
}
@Test
void resolveMultipleFuncitonAndToolCallbacks() {
new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(ToolCallingAutoConfiguration.class))
.withUserConfiguration(Config.class)
.run(context -> {
var toolCallbackResolver = context.getBean(ToolCallbackResolver.class);
assertThat(toolCallbackResolver).isInstanceOf(DelegatingToolCallbackResolver.class);
assertThat(toolCallbackResolver.resolve("getForecast")).isNotNull();
assertThat(toolCallbackResolver.resolve("getForecast").getName()).isEqualTo("getForecast");
assertThat(toolCallbackResolver.resolve("getAlert")).isNotNull();
assertThat(toolCallbackResolver.resolve("getAlert").getName()).isEqualTo("getAlert");
assertThat(toolCallbackResolver.resolve("weatherFunction1")).isNotNull();
assertThat(toolCallbackResolver.resolve("weatherFunction1").getName()).isEqualTo("weatherFunction1");
assertThat(toolCallbackResolver.resolve("getCurrentWeather3")).isNotNull();
assertThat(toolCallbackResolver.resolve("getCurrentWeather3").getName())
.isEqualTo("getCurrentWeather3");
assertThat(toolCallbackResolver.resolve("getCurrentWeather4")).isNotNull();
assertThat(toolCallbackResolver.resolve("getCurrentWeather4").getName())
.isEqualTo("getCurrentWeather4");
assertThat(toolCallbackResolver.resolve("getCurrentWeather5")).isNotNull();
assertThat(toolCallbackResolver.resolve("getCurrentWeather5").getName())
.isEqualTo("getCurrentWeather5");
});
}
static class WeatherService {
@Tool(description = "Get the weather in location. Return temperature in 36°F or 36°C format.")
public String getForecast(String location) {
return "30";
}
@Tool(description = "Get the weather in location. Return temperature in 36°F or 36°C format.")
public String getForecast2(String location) {
return "30";
}
public String getAlert(String usState) {
return "Alert";
}
}
@Configuration
static class Config {
// Note: Currently we do not have ToolCallbackResolver implementation that can
// resolve the ToolCallback from the Tool annotation.
// Therefore we need to provide the ToolCallback instances explicitly using the
// ToolCallbacks.from(...) utility method.
@Bean
public ToolCallbackProvider toolCallbacks() {
return MethodToolCallbackProvider.builder().toolObjects(new WeatherService()).build();
}
public record Request(String location) {
}
public record Response(String temperature) {
}
@Bean
@Description("Get the weather in location. Return temperature in 36°F or 36°C format.")
public Function<Request, Response> weatherFunction1() {
return request -> new Response("30");
}
@Bean
public FunctionCallback functionCallbacks3() {
return FunctionCallback.builder()
.function("getCurrentWeather3", (Request request) -> "15.0°C")
.description("Gets the weather in location")
.inputType(Request.class)
.build();
}
@Bean
public FunctionCallback functionCallbacks4() {
return FunctionCallback.builder()
.function("getCurrentWeather4", (Request request) -> "15.0°C")
.description("Gets the weather in location")
.inputType(Request.class)
.build();
}
@Bean
public ToolCallback toolCallbacks5() {
return FunctionToolCallback.builder("getCurrentWeather5", (Request request) -> "15.0°C")
.description("Gets the weather in location")
.inputType(Request.class)
.build();
}
@Bean
public ToolCallbackProvider blabla() {
return new StaticToolCallbackProvider(
FunctionToolCallback.builder("getCurrentWeather5", (Request request) -> "15.0°C")
.description("Gets the weather in location")
.inputType(Request.class)
.build());
}
@Bean
public ToolCallback toolCallbacks6() {
var toolMethod = ReflectionUtils.findMethod(WeatherService.class, "getAlert", String.class);
return MethodToolCallback.builder()
.toolDefinition(ToolDefinition.builder(toolMethod).build())
.toolMethod(toolMethod)
.toolObject(new WeatherService())
.build();
}
}
}