Skip to content

Commit d18262e

Browse files
committed
refactor(mcp): 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 <[email protected]>
1 parent 0c1abec commit d18262e

File tree

11 files changed

+201
-93
lines changed

11 files changed

+201
-93
lines changed

auto-configurations/spring-ai-mcp-client/src/main/java/org/springframework/ai/autoconfigure/mcp/client/McpClientAutoConfiguration.java

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,11 @@
2727
import org.springframework.ai.autoconfigure.mcp.client.configurer.McpAsyncClientConfigurer;
2828
import org.springframework.ai.autoconfigure.mcp.client.configurer.McpSyncClientConfigurer;
2929
import org.springframework.ai.autoconfigure.mcp.client.properties.McpClientCommonProperties;
30-
import org.springframework.ai.mcp.McpToolUtils;
30+
import org.springframework.ai.mcp.AsyncMcpToolCallbackProvider;
31+
import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
3132
import org.springframework.ai.mcp.customizer.McpAsyncClientCustomizer;
3233
import org.springframework.ai.mcp.customizer.McpSyncClientCustomizer;
34+
import org.springframework.ai.tool.ToolCallback;
3335
import org.springframework.ai.tool.ToolCallbackProvider;
3436
import org.springframework.beans.factory.ObjectProvider;
3537
import org.springframework.boot.autoconfigure.AutoConfiguration;
@@ -178,7 +180,20 @@ public List<McpSyncClient> mcpSyncClients(McpSyncClientConfigurer mcpSyncClientC
178180
matchIfMissing = true)
179181
public ToolCallbackProvider toolCallbacks(ObjectProvider<List<McpSyncClient>> mcpClientsProvider) {
180182
List<McpSyncClient> mcpClients = mcpClientsProvider.stream().flatMap(List::stream).toList();
181-
return ToolCallbackProvider.from(McpToolUtils.getToolCallbacksFromSyncClients(mcpClients));
183+
return new SyncMcpToolCallbackProvider(mcpClients);
184+
}
185+
186+
/**
187+
* @deprecated replaced by {@link #toolCallbacks(ObjectProvider)} that returns a
188+
* {@link ToolCallbackProvider} instead of a list of {@link ToolCallback}
189+
*/
190+
@Deprecated
191+
@Bean
192+
@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "SYNC",
193+
matchIfMissing = true)
194+
public List<ToolCallback> toolCallbacksDeprecated(ObjectProvider<List<McpSyncClient>> mcpClientsProvider) {
195+
List<McpSyncClient> mcpClients = mcpClientsProvider.stream().flatMap(List::stream).toList();
196+
return List.of(new SyncMcpToolCallbackProvider(mcpClients).getToolCallbacks());
182197
}
183198

184199
/**
@@ -189,7 +204,7 @@ public ToolCallbackProvider toolCallbacks(ObjectProvider<List<McpSyncClient>> mc
189204
* This class is responsible for closing all MCP sync clients when the application
190205
* context is closed, preventing resource leaks.
191206
*/
192-
public record ClosebleMcpSyncClients(List<McpSyncClient> clients) implements AutoCloseable {
207+
public record CloseableMcpSyncClients(List<McpSyncClient> clients) implements AutoCloseable {
193208

194209
@Override
195210
public void close() {
@@ -205,8 +220,8 @@ public void close() {
205220
@Bean
206221
@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "SYNC",
207222
matchIfMissing = true)
208-
public ClosebleMcpSyncClients makeSyncClientsClosable(List<McpSyncClient> clients) {
209-
return new ClosebleMcpSyncClients(clients);
223+
public CloseableMcpSyncClients makeSyncClientsClosable(List<McpSyncClient> clients) {
224+
return new CloseableMcpSyncClients(clients);
210225
}
211226

212227
/**
@@ -263,14 +278,26 @@ public List<McpAsyncClient> mcpAsyncClients(McpAsyncClientConfigurer mcpSyncClie
263278
return mcpSyncClients;
264279
}
265280

281+
/**
282+
* @deprecated replaced by {@link #asyncToolCallbacks(ObjectProvider)} that returns a
283+
* {@link ToolCallbackProvider} instead of a list of {@link ToolCallback}
284+
*/
285+
@Deprecated
286+
@Bean
287+
@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC")
288+
public List<ToolCallback> asyncToolCallbacksDeprecated(ObjectProvider<List<McpAsyncClient>> mcpClientsProvider) {
289+
List<McpAsyncClient> mcpClients = mcpClientsProvider.stream().flatMap(List::stream).toList();
290+
return List.of(new AsyncMcpToolCallbackProvider(mcpClients).getToolCallbacks());
291+
}
292+
266293
@Bean
267294
@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC")
268295
public ToolCallbackProvider asyncToolCallbacks(ObjectProvider<List<McpAsyncClient>> mcpClientsProvider) {
269296
List<McpAsyncClient> mcpClients = mcpClientsProvider.stream().flatMap(List::stream).toList();
270-
return ToolCallbackProvider.from(McpToolUtils.getToolCallbacksFromAsyncClinents(mcpClients));
297+
return new AsyncMcpToolCallbackProvider(mcpClients);
271298
}
272299

273-
public record ClosebleMcpAsyncClients(List<McpAsyncClient> clients) implements AutoCloseable {
300+
public record CloseableMcpAsyncClients(List<McpAsyncClient> clients) implements AutoCloseable {
274301
@Override
275302
public void close() {
276303
this.clients.forEach(McpAsyncClient::close);
@@ -279,8 +306,8 @@ public void close() {
279306

280307
@Bean
281308
@ConditionalOnProperty(prefix = McpClientCommonProperties.CONFIG_PREFIX, name = "type", havingValue = "ASYNC")
282-
public ClosebleMcpAsyncClients makeAsynClientsClosable(List<McpAsyncClient> clients) {
283-
return new ClosebleMcpAsyncClients(clients);
309+
public CloseableMcpAsyncClients makeAsynClientsClosable(List<McpAsyncClient> clients) {
310+
return new CloseableMcpAsyncClients(clients);
284311
}
285312

286313
@Bean

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ void toolCallbacksCreation() {
122122
@Test
123123
void closeableWrappersCreation() {
124124
this.contextRunner.withUserConfiguration(TestTransportConfiguration.class).run(context -> {
125-
assertThat(context).hasSingleBean(McpClientAutoConfiguration.ClosebleMcpSyncClients.class);
125+
assertThat(context).hasSingleBean(McpClientAutoConfiguration.CloseableMcpSyncClients.class);
126126
});
127127
}
128128

mcp/common/src/main/java/org/springframework/ai/mcp/AsyncMcpToolCallbackProvider.java

Lines changed: 73 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,12 @@
1515
*/
1616
package org.springframework.ai.mcp;
1717

18+
import java.util.ArrayList;
1819
import java.util.List;
1920

2021
import io.modelcontextprotocol.client.McpAsyncClient;
22+
import io.modelcontextprotocol.util.Assert;
2123
import reactor.core.publisher.Flux;
22-
import reactor.core.publisher.Mono;
2324

2425
import org.springframework.ai.tool.ToolCallback;
2526
import org.springframework.ai.tool.ToolCallbackProvider;
@@ -28,25 +29,40 @@
2829

2930
/**
3031
* Implementation of {@link ToolCallbackProvider} that discovers and provides MCP tools
31-
* asynchronously.
32+
* asynchronously from one or more MCP servers.
3233
* <p>
3334
* This class acts as a tool provider for Spring AI, automatically discovering tools from
34-
* an MCP server and making them available as Spring AI tools. It:
35+
* multiple MCP servers and making them available as Spring AI tools. It:
3536
* <ul>
36-
* <li>Connects to an MCP server through an async client</li>
37-
* <li>Lists and retrieves available tools from the server</li>
37+
* <li>Connects to MCP servers through async clients</li>
38+
* <li>Lists and retrieves available tools from each server asynchronously</li>
3839
* <li>Creates {@link AsyncMcpToolCallback} instances for each discovered tool</li>
39-
* <li>Validates tool names to prevent duplicates</li>
40+
* <li>Validates tool names to prevent duplicates across all servers</li>
4041
* </ul>
4142
* <p>
42-
* Example usage: <pre>{@code
43+
* Example usage with a single client:
44+
*
45+
* <pre>{@code
4346
* McpAsyncClient mcpClient = // obtain MCP client
4447
* ToolCallbackProvider provider = new AsyncMcpToolCallbackProvider(mcpClient);
4548
*
4649
* // Get all available tools
4750
* ToolCallback[] tools = provider.getToolCallbacks();
4851
* }</pre>
4952
*
53+
* Example usage with multiple clients:
54+
*
55+
* <pre>{@code
56+
* List<McpAsyncClient> mcpClients = // obtain multiple MCP clients
57+
* ToolCallbackProvider provider = new AsyncMcpToolCallbackProvider(mcpClients);
58+
*
59+
* // Get tools from all clients
60+
* ToolCallback[] tools = provider.getToolCallbacks();
61+
*
62+
* // Or use the reactive API
63+
* Flux<ToolCallback> toolsFlux = AsyncMcpToolCallbackProvider.asyncToolCallbacks(mcpClients);
64+
* }</pre>
65+
*
5066
* @author Christian Tzolov
5167
* @since 1.0.0
5268
* @see ToolCallbackProvider
@@ -55,40 +71,61 @@
5571
*/
5672
public class AsyncMcpToolCallbackProvider implements ToolCallbackProvider {
5773

58-
private final McpAsyncClient mcpClient;
74+
private final List<McpAsyncClient> mcpClients;
5975

6076
/**
61-
* Creates a new {@code AsyncMcpToolCallbackProvider} instance.
62-
* @param mcpClient the MCP client to use for discovering tools
77+
* Creates a new {@code AsyncMcpToolCallbackProvider} instance with a list of MCP
78+
* clients.
79+
* @param mcpClients the list of MCP clients to use for discovering tools. Each client
80+
* typically connects to a different MCP server, allowing tool discovery from multiple
81+
* sources.
82+
* @throws IllegalArgumentException if mcpClients is null
6383
*/
64-
public AsyncMcpToolCallbackProvider(McpAsyncClient mcpClient) {
65-
this.mcpClient = mcpClient;
84+
public AsyncMcpToolCallbackProvider(List<McpAsyncClient> mcpClients) {
85+
Assert.notNull(mcpClients, "McpClients must not be null");
86+
this.mcpClients = mcpClients;
87+
}
88+
89+
public AsyncMcpToolCallbackProvider(McpAsyncClient... mcpClients) {
90+
Assert.notNull(mcpClients, "McpClients must not be null");
91+
this.mcpClients = List.of(mcpClients);
6692
}
6793

6894
/**
69-
* Discovers and returns all available tools from the MCP server asynchronously.
95+
* Discovers and returns all available tools from the configured MCP servers.
7096
* <p>
7197
* This method:
7298
* <ol>
73-
* <li>Retrieves the list of tools from the MCP server</li>
74-
* <li>Creates a {@link AsyncMcpToolCallback} for each tool</li>
75-
* <li>Validates that there are no duplicate tool names</li>
99+
* <li>Retrieves the list of tools from each MCP server asynchronously</li>
100+
* <li>Creates a {@link AsyncMcpToolCallback} for each discovered tool</li>
101+
* <li>Validates that there are no duplicate tool names across all servers</li>
76102
* </ol>
103+
* <p>
104+
* Note: While the underlying tool discovery is asynchronous, this method blocks until
105+
* all tools are discovered from all servers.
77106
* @return an array of tool callbacks, one for each discovered tool
78107
* @throws IllegalStateException if duplicate tool names are found
79108
*/
80109
@Override
81110
public ToolCallback[] getToolCallbacks() {
82-
var toolCallbacks = this.mcpClient.listTools()
83-
.map(response -> response.tools()
84-
.stream()
85-
.map(tool -> new AsyncMcpToolCallback(this.mcpClient, tool))
86-
.toArray(ToolCallback[]::new))
87-
.block();
88111

89-
validateToolCallbacks(toolCallbacks);
112+
List<ToolCallback> toolCallbackList = new ArrayList<>();
113+
114+
for (McpAsyncClient mcpClient : this.mcpClients) {
115+
116+
ToolCallback[] toolCallbacks = mcpClient.listTools()
117+
.map(response -> response.tools()
118+
.stream()
119+
.map(tool -> new AsyncMcpToolCallback(mcpClient, tool))
120+
.toArray(ToolCallback[]::new))
121+
.block();
90122

91-
return toolCallbacks;
123+
validateToolCallbacks(toolCallbacks);
124+
125+
toolCallbackList.addAll(List.of(toolCallbacks));
126+
}
127+
128+
return toolCallbackList.toArray(new ToolCallback[0]);
92129
}
93130

94131
/**
@@ -110,12 +147,19 @@ private void validateToolCallbacks(ToolCallback[] toolCallbacks) {
110147
/**
111148
* Creates a reactive stream of tool callbacks from multiple MCP clients.
112149
* <p>
113-
* This utility method:
150+
* This utility method provides a reactive way to work with tool callbacks from
151+
* multiple MCP clients in a single operation. It:
114152
* <ol>
115-
* <li>Takes a list of MCP clients</li>
116-
* <li>Creates a provider for each client</li>
117-
* <li>Retrieves and flattens all tool callbacks into a single stream</li>
153+
* <li>Takes a list of MCP clients as input</li>
154+
* <li>Creates a provider instance to manage all clients</li>
155+
* <li>Retrieves tools from all clients asynchronously</li>
156+
* <li>Combines them into a single reactive stream</li>
157+
* <li>Ensures there are no naming conflicts between tools from different clients</li>
118158
* </ol>
159+
* <p>
160+
* Unlike {@link #getToolCallbacks()}, this method provides a fully reactive way to
161+
* work with tool callbacks, making it suitable for non-blocking applications. Any
162+
* errors during tool discovery will be propagated through the returned Flux.
119163
* @param mcpClients the list of MCP clients to create callbacks from
120164
* @return a Flux of tool callbacks from all provided clients
121165
*/
@@ -124,9 +168,7 @@ public static Flux<ToolCallback> asyncToolCallbacks(List<McpAsyncClient> mcpClie
124168
return Flux.empty();
125169
}
126170

127-
return Flux.fromIterable(mcpClients)
128-
.flatMap(mcpClient -> Mono.just(new AsyncMcpToolCallbackProvider(mcpClient).getToolCallbacks()))
129-
.flatMap(callbacks -> Flux.fromArray(callbacks));
171+
return Flux.fromArray(new AsyncMcpToolCallbackProvider(mcpClients).getToolCallbacks());
130172
}
131173

132174
}

mcp/common/src/main/java/org/springframework/ai/mcp/McpToolUtils.java

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -211,10 +211,7 @@ public static List<ToolCallback> getToolCallbacksFromSyncClients(List<McpSyncCli
211211
if (CollectionUtils.isEmpty(mcpClients)) {
212212
return List.of();
213213
}
214-
return mcpClients.stream()
215-
.map(mcpClient -> List.of((new SyncMcpToolCallbackProvider(mcpClient).getToolCallbacks())))
216-
.flatMap(List::stream)
217-
.toList();
214+
return List.of((new SyncMcpToolCallbackProvider(mcpClients).getToolCallbacks()));
218215
}
219216

220217
/**
@@ -247,10 +244,7 @@ public static List<ToolCallback> getToolCallbacksFromAsyncClinents(List<McpAsync
247244
if (CollectionUtils.isEmpty(asynMcpClients)) {
248245
return List.of();
249246
}
250-
return asynMcpClients.stream()
251-
.map(mcpClient -> List.of((new AsyncMcpToolCallbackProvider(mcpClient).getToolCallbacks())))
252-
.flatMap(List::stream)
253-
.toList();
247+
return List.of((new AsyncMcpToolCallbackProvider(asynMcpClients).getToolCallbacks()));
254248
}
255249

256250
}

0 commit comments

Comments
 (0)