1515 */
1616package org .springframework .ai .mcp ;
1717
18+ import java .util .ArrayList ;
1819import java .util .List ;
1920
2021import io .modelcontextprotocol .client .McpAsyncClient ;
22+ import io .modelcontextprotocol .util .Assert ;
2123import reactor .core .publisher .Flux ;
22- import reactor .core .publisher .Mono ;
2324
2425import org .springframework .ai .tool .ToolCallback ;
2526import org .springframework .ai .tool .ToolCallbackProvider ;
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
5571 */
5672public 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}
0 commit comments