camel-ai 0.2.60__py3-none-any.whl → 0.2.62__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of camel-ai might be problematic. Click here for more details.

Files changed (53) hide show
  1. camel/__init__.py +1 -1
  2. camel/agents/chat_agent.py +159 -8
  3. camel/agents/mcp_agent.py +5 -5
  4. camel/configs/anthropic_config.py +6 -5
  5. camel/{data_collector → data_collectors}/alpaca_collector.py +1 -1
  6. camel/{data_collector → data_collectors}/sharegpt_collector.py +1 -1
  7. camel/datagen/evol_instruct/scorer.py +22 -23
  8. camel/datagen/evol_instruct/templates.py +46 -46
  9. camel/datasets/static_dataset.py +144 -0
  10. camel/loaders/__init__.py +5 -2
  11. camel/loaders/chunkr_reader.py +117 -91
  12. camel/loaders/mistral_reader.py +148 -0
  13. camel/memories/blocks/chat_history_block.py +1 -2
  14. camel/models/model_manager.py +7 -3
  15. camel/retrievers/auto_retriever.py +20 -1
  16. camel/{runtime → runtimes}/daytona_runtime.py +1 -1
  17. camel/{runtime → runtimes}/docker_runtime.py +1 -1
  18. camel/{runtime → runtimes}/llm_guard_runtime.py +2 -2
  19. camel/{runtime → runtimes}/remote_http_runtime.py +1 -1
  20. camel/{runtime → runtimes}/ubuntu_docker_runtime.py +1 -1
  21. camel/societies/workforce/base.py +7 -3
  22. camel/societies/workforce/single_agent_worker.py +2 -1
  23. camel/societies/workforce/worker.py +5 -3
  24. camel/societies/workforce/workforce.py +65 -24
  25. camel/storages/__init__.py +2 -0
  26. camel/storages/vectordb_storages/__init__.py +2 -0
  27. camel/storages/vectordb_storages/faiss.py +712 -0
  28. camel/toolkits/__init__.py +4 -0
  29. camel/toolkits/async_browser_toolkit.py +75 -523
  30. camel/toolkits/bohrium_toolkit.py +318 -0
  31. camel/toolkits/browser_toolkit.py +215 -538
  32. camel/toolkits/browser_toolkit_commons.py +568 -0
  33. camel/toolkits/file_write_toolkit.py +80 -31
  34. camel/toolkits/mcp_toolkit.py +477 -665
  35. camel/toolkits/pptx_toolkit.py +777 -0
  36. camel/toolkits/wolfram_alpha_toolkit.py +5 -1
  37. camel/types/enums.py +13 -1
  38. camel/utils/__init__.py +2 -0
  39. camel/utils/commons.py +27 -0
  40. camel/utils/mcp_client.py +979 -0
  41. {camel_ai-0.2.60.dist-info → camel_ai-0.2.62.dist-info}/METADATA +14 -1
  42. {camel_ai-0.2.60.dist-info → camel_ai-0.2.62.dist-info}/RECORD +53 -47
  43. /camel/{data_collector → data_collectors}/__init__.py +0 -0
  44. /camel/{data_collector → data_collectors}/base.py +0 -0
  45. /camel/{runtime → runtimes}/__init__.py +0 -0
  46. /camel/{runtime → runtimes}/api.py +0 -0
  47. /camel/{runtime → runtimes}/base.py +0 -0
  48. /camel/{runtime → runtimes}/configs.py +0 -0
  49. /camel/{runtime → runtimes}/utils/__init__.py +0 -0
  50. /camel/{runtime → runtimes}/utils/function_risk_toolkit.py +0 -0
  51. /camel/{runtime → runtimes}/utils/ignore_risk_toolkit.py +0 -0
  52. {camel_ai-0.2.60.dist-info → camel_ai-0.2.62.dist-info}/WHEEL +0 -0
  53. {camel_ai-0.2.60.dist-info → camel_ai-0.2.62.dist-info}/licenses/LICENSE +0 -0
@@ -11,792 +11,604 @@
11
11
  # See the License for the specific language governing permissions and
12
12
  # limitations under the License.
13
13
  # ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
14
- import inspect
14
+
15
15
  import json
16
16
  import os
17
- import shlex
18
- from contextlib import AsyncExitStack, asynccontextmanager
19
- from datetime import timedelta
20
- from typing import (
21
- TYPE_CHECKING,
22
- Any,
23
- AsyncGenerator,
24
- Callable,
25
- Dict,
26
- List,
27
- Optional,
28
- Set,
29
- Union,
30
- cast,
31
- )
32
- from urllib.parse import urlparse
33
-
34
- if TYPE_CHECKING:
35
- from mcp import ClientSession, ListToolsResult, Tool
36
-
17
+ from contextlib import AsyncExitStack
18
+ from typing import Any, Dict, List, Optional
37
19
 
38
20
  from camel.logger import get_logger
39
21
  from camel.toolkits import BaseToolkit, FunctionTool
22
+ from camel.utils.commons import run_async
23
+ from camel.utils.mcp_client import MCPClient, create_mcp_client
40
24
 
41
25
  logger = get_logger(__name__)
42
26
 
43
27
 
44
- class MCPClient(BaseToolkit):
45
- r"""Internal class that provides an abstraction layer to interact with
46
- external tools using the Model Context Protocol (MCP). It supports three
47
- modes of connection:
28
+ class MCPConnectionError(Exception):
29
+ r"""Raised when MCP connection fails."""
30
+
31
+ pass
32
+
33
+
34
+ class MCPToolError(Exception):
35
+ r"""Raised when MCP tool execution fails."""
48
36
 
49
- 1. stdio mode: Connects via standard input/output streams for local
50
- command-line interactions.
37
+ pass
51
38
 
52
- 2. SSE mode (HTTP Server-Sent Events): Connects via HTTP for persistent,
53
- event-based interactions.
54
39
 
55
- 3. streamable-http mode: Connects via HTTP for persistent, streamable
56
- interactions.
40
+ class MCPToolkit(BaseToolkit):
41
+ r"""MCPToolkit provides a unified interface for managing multiple
42
+ MCP server connections and their tools.
43
+
44
+ This class handles the lifecycle of multiple MCP server connections and
45
+ offers a centralized configuration mechanism for both local and remote
46
+ MCP services. The toolkit manages multiple :obj:`MCPClient` instances and
47
+ aggregates their tools into a unified interface compatible with the CAMEL
48
+ framework.
57
49
 
58
50
  Connection Lifecycle:
59
51
  There are three ways to manage the connection lifecycle:
60
52
 
61
- 1. Using the async context manager:
62
- ```python
63
- async with MCPClient(command_or_url="...") as client:
64
- # Client is connected here
65
- result = await client.some_tool()
66
- # Client is automatically disconnected here
67
- ```
53
+ 1. Using the async context manager (recommended):
54
+
55
+ .. code-block:: python
56
+
57
+ async with MCPToolkit(config_path="config.json") as toolkit:
58
+ # Toolkit is connected here
59
+ tools = toolkit.get_tools()
60
+ # Toolkit is automatically disconnected here
68
61
 
69
62
  2. Using the factory method:
70
- ```python
71
- client = await MCPClient.create(command_or_url="...")
72
- # Client is connected here
73
- result = await client.some_tool()
74
- # Don't forget to disconnect when done!
75
- await client.disconnect()
76
- ```
63
+
64
+ .. code-block:: python
65
+
66
+ toolkit = await MCPToolkit.create(config_path="config.json")
67
+ # Toolkit is connected here
68
+ tools = toolkit.get_tools()
69
+ # Don't forget to disconnect when done!
70
+ await toolkit.disconnect()
77
71
 
78
72
  3. Using explicit connect/disconnect:
79
- ```python
80
- client = MCPClient(command_or_url="...")
81
- await client.connect()
82
- # Client is connected here
83
- result = await client.some_tool()
84
- # Don't forget to disconnect when done!
85
- await client.disconnect()
86
- ```
87
73
 
88
- Attributes:
89
- command_or_url (str): URL for SSE mode or command executable for stdio
90
- mode. (default: :obj:`None`)
91
- args (List[str]): List of command-line arguments if stdio mode is used.
92
- (default: :obj:`None`)
93
- env (Dict[str, str]): Environment variables for the stdio mode command.
94
- (default: :obj:`None`)
95
- timeout (Optional[float]): Connection timeout.
96
- (default: :obj:`None`)
97
- headers (Dict[str, str]): Headers for the HTTP request.
74
+ .. code-block:: python
75
+
76
+ toolkit = MCPToolkit(config_path="config.json")
77
+ await toolkit.connect()
78
+ # Toolkit is connected here
79
+ tools = toolkit.get_tools()
80
+ # Don't forget to disconnect when done!
81
+ await toolkit.disconnect()
82
+
83
+ Note:
84
+ Both MCPClient and MCPToolkit now use the same async context manager
85
+ pattern for consistent connection management. MCPToolkit automatically
86
+ manages multiple MCPClient instances using AsyncExitStack.
87
+
88
+ Args:
89
+ clients (Optional[List[MCPClient]], optional): List of :obj:`MCPClient`
90
+ instances to manage. (default: :obj:`None`)
91
+ config_path (Optional[str], optional): Path to a JSON configuration
92
+ file defining MCP servers. The file should contain server
93
+ configurations in the standard MCP format. (default: :obj:`None`)
94
+ config_dict (Optional[Dict[str, Any]], optional): Dictionary containing
95
+ MCP server configurations in the same format as the config file.
96
+ This allows for programmatic configuration without file I/O.
98
97
  (default: :obj:`None`)
99
- mode (Optional[str]): Connection mode. Can be "sse" for Server-Sent
100
- Events, "streamable-http" for streaming HTTP,
101
- or None for stdio mode.
98
+ timeout (Optional[float], optional): Timeout for connection attempts
99
+ in seconds. This timeout applies to individual client connections.
102
100
  (default: :obj:`None`)
103
- strict (Optional[bool]): Whether to enforce strict mode for the
104
- function call. (default: :obj:`False`)
101
+
102
+ Note:
103
+ At least one of :obj:`clients`, :obj:`config_path`, or
104
+ :obj:`config_dict` must be provided. If multiple sources are provided,
105
+ clients from all sources will be combined.
106
+
107
+ For web servers in the config, you can specify authorization headers
108
+ using the "headers" field to connect to protected MCP server endpoints.
109
+
110
+ Example configuration:
111
+
112
+ .. code-block:: json
113
+
114
+ {
115
+ "mcpServers": {
116
+ "filesystem": {
117
+ "command": "npx",
118
+ "args": ["-y", "@modelcontextprotocol/server-filesystem",
119
+ "/path"]
120
+ },
121
+ "protected-server": {
122
+ "url": "https://example.com/mcp",
123
+ "timeout": 30,
124
+ "headers": {
125
+ "Authorization": "Bearer YOUR_TOKEN",
126
+ "X-API-Key": "YOUR_API_KEY"
127
+ }
128
+ }
129
+ }
130
+ }
131
+
132
+ Attributes:
133
+ clients (List[MCPClient]): List of :obj:`MCPClient` instances being
134
+ managed by this toolkit.
135
+
136
+ Raises:
137
+ ValueError: If no configuration sources are provided or if the
138
+ configuration is invalid.
139
+ MCPConnectionError: If connection to any MCP server fails during
140
+ initialization.
105
141
  """
106
142
 
107
143
  def __init__(
108
144
  self,
109
- command_or_url: str,
110
- args: Optional[List[str]] = None,
111
- env: Optional[Dict[str, str]] = None,
145
+ clients: Optional[List[MCPClient]] = None,
146
+ config_path: Optional[str] = None,
147
+ config_dict: Optional[Dict[str, Any]] = None,
112
148
  timeout: Optional[float] = None,
113
- headers: Optional[Dict[str, str]] = None,
114
- mode: Optional[str] = None,
115
- strict: Optional[bool] = False,
116
149
  ):
117
- from mcp import Tool
118
-
150
+ # Call parent constructor first
119
151
  super().__init__(timeout=timeout)
120
152
 
121
- self.command_or_url = command_or_url
122
- self.args = args or []
123
- self.env = env or {}
124
- self.headers = headers or {}
125
- self.strict = strict
126
- self.mode = mode
153
+ # Validate input parameters
154
+ sources_provided = sum(
155
+ 1 for src in [clients, config_path, config_dict] if src is not None
156
+ )
157
+ if sources_provided == 0:
158
+ error_msg = (
159
+ "At least one of clients, config_path, or "
160
+ "config_dict must be provided"
161
+ )
162
+ raise ValueError(error_msg)
127
163
 
128
- self._mcp_tools: List[Tool] = []
129
- self._session: Optional['ClientSession'] = None
130
- self._exit_stack = AsyncExitStack()
164
+ self.clients: List[MCPClient] = clients or []
131
165
  self._is_connected = False
166
+ self._exit_stack: Optional[AsyncExitStack] = None
167
+
168
+ # Load clients from config sources
169
+ if config_path:
170
+ self.clients.extend(self._load_clients_from_config(config_path))
171
+
172
+ if config_dict:
173
+ self.clients.extend(self._load_clients_from_dict(config_dict))
174
+
175
+ if not self.clients:
176
+ raise ValueError("No valid MCP clients could be created")
132
177
 
133
- async def connect(self):
134
- r"""Explicitly connect to the MCP server.
178
+ async def connect(self) -> "MCPToolkit":
179
+ r"""Connect to all MCP servers using AsyncExitStack.
180
+
181
+ Establishes connections to all configured MCP servers sequentially.
182
+ Uses :obj:`AsyncExitStack` to manage the lifecycle of all connections,
183
+ ensuring proper cleanup on exit or error.
135
184
 
136
185
  Returns:
137
- MCPClient: The client used to connect to the server.
138
- """
139
- from mcp.client.session import ClientSession
140
- from mcp.client.sse import sse_client
141
- from mcp.client.stdio import StdioServerParameters, stdio_client
142
- from mcp.client.streamable_http import streamablehttp_client
186
+ MCPToolkit: Returns :obj:`self` for method chaining, allowing for
187
+ fluent interface usage.
188
+
189
+ Raises:
190
+ MCPConnectionError: If connection to any MCP server fails. The
191
+ error message will include details about which client failed
192
+ to connect and the underlying error reason.
193
+
194
+ Warning:
195
+ If any client fails to connect, all previously established
196
+ connections will be automatically cleaned up before raising
197
+ the exception.
143
198
 
199
+ Example:
200
+ .. code-block:: python
201
+
202
+ toolkit = MCPToolkit(config_dict=config)
203
+ try:
204
+ await toolkit.connect()
205
+ # Use the toolkit
206
+ tools = toolkit.get_tools()
207
+ finally:
208
+ await toolkit.disconnect()
209
+ """
144
210
  if self._is_connected:
145
- logger.warning("Server is already connected")
211
+ logger.warning("MCPToolkit is already connected")
146
212
  return self
147
213
 
214
+ self._exit_stack = AsyncExitStack()
215
+
148
216
  try:
149
- if urlparse(self.command_or_url).scheme in ("http", "https"):
150
- if self.mode == "sse" or self.mode is None:
151
- (
152
- read_stream,
153
- write_stream,
154
- ) = await self._exit_stack.enter_async_context(
155
- sse_client(
156
- self.command_or_url,
157
- headers=self.headers,
158
- timeout=self.timeout,
159
- )
160
- )
161
- elif self.mode == "streamable-http":
162
- try:
163
- (
164
- read_stream,
165
- write_stream,
166
- _,
167
- ) = await self._exit_stack.enter_async_context(
168
- streamablehttp_client(
169
- self.command_or_url,
170
- headers=self.headers,
171
- timeout=timedelta(seconds=self.timeout),
172
- )
173
- )
174
- except Exception as e:
175
- # Handle anyio task group errors
176
- logger.error(f"Streamable HTTP client error: {e}")
177
- else:
178
- raise ValueError(
179
- f"Invalid mode '{self.mode}' for HTTP URL"
180
- )
181
- else:
182
- command = self.command_or_url
183
- arguments = self.args
184
- if not self.args:
185
- argv = shlex.split(command)
186
- if not argv:
187
- raise ValueError("Command is empty")
188
-
189
- command = argv[0]
190
- arguments = argv[1:]
191
-
192
- if os.name == "nt" and command.lower() == "npx":
193
- command = "npx.cmd"
194
-
195
- server_parameters = StdioServerParameters(
196
- command=command, args=arguments, env=self.env
197
- )
198
- (
199
- read_stream,
200
- write_stream,
201
- ) = await self._exit_stack.enter_async_context(
202
- stdio_client(server_parameters)
203
- )
217
+ # Connect to all clients using AsyncExitStack
218
+ for i, client in enumerate(self.clients):
219
+ try:
220
+ # Use MCPClient directly as async context manager
221
+ await self._exit_stack.enter_async_context(client)
222
+ msg = f"Connected to client {i+1}/{len(self.clients)}"
223
+ logger.debug(msg)
224
+ except Exception as e:
225
+ logger.error(f"Failed to connect to client {i+1}: {e}")
226
+ # AsyncExitStack will handle cleanup of already connected
227
+ await self._exit_stack.aclose()
228
+ self._exit_stack = None
229
+ error_msg = f"Failed to connect to client {i+1}: {e}"
230
+ raise MCPConnectionError(error_msg) from e
204
231
 
205
- self._session = await self._exit_stack.enter_async_context(
206
- ClientSession(
207
- read_stream,
208
- write_stream,
209
- timedelta(seconds=self.timeout) if self.timeout else None,
210
- )
211
- )
212
- await self._session.initialize()
213
- list_tools_result = await self.list_mcp_tools()
214
- self._mcp_tools = list_tools_result.tools
215
232
  self._is_connected = True
233
+ msg = f"Successfully connected to {len(self.clients)} MCP servers"
234
+ logger.info(msg)
216
235
  return self
217
- except Exception as e:
218
- # Ensure resources are cleaned up on connection failure
219
- await self.disconnect()
220
- logger.error(f"Failed to connect to MCP server: {e}")
221
- raise e
236
+
237
+ except Exception:
238
+ self._is_connected = False
239
+ if self._exit_stack:
240
+ await self._exit_stack.aclose()
241
+ self._exit_stack = None
242
+ raise
222
243
 
223
244
  async def disconnect(self):
224
- r"""Explicitly disconnect from the MCP server."""
225
- # If the server is not connected, do nothing
245
+ r"""Disconnect from all MCP servers."""
226
246
  if not self._is_connected:
227
247
  return
228
- self._is_connected = False
229
248
 
230
- try:
231
- await self._exit_stack.aclose()
232
- except Exception as e:
233
- logger.warning(f"{e}")
234
- finally:
235
- self._exit_stack = AsyncExitStack()
236
- self._session = None
237
-
238
- @asynccontextmanager
239
- async def connection(self):
240
- r"""Async context manager for establishing and managing the connection
241
- with the MCP server. Automatically selects SSE or stdio mode based
242
- on the provided `command_or_url`.
243
-
244
- Yields:
245
- MCPClient: Instance with active connection ready for tool
246
- interaction.
247
- """
248
- try:
249
- await self.connect()
250
- yield self
251
- finally:
249
+ if self._exit_stack:
252
250
  try:
253
- await self.disconnect()
251
+ await self._exit_stack.aclose()
254
252
  except Exception as e:
255
- logger.warning(f"Error: {e}")
256
-
257
- async def list_mcp_tools(self) -> Union[str, "ListToolsResult"]:
258
- r"""Retrieves the list of available tools from the connected MCP
259
- server.
260
-
261
- Returns:
262
- ListToolsResult: Result containing available MCP tools.
263
- """
264
- if not self._session:
265
- return "MCP Client is not connected. Call `connection()` first."
266
- try:
267
- return await self._session.list_tools()
268
- except Exception as e:
269
- logger.exception("Failed to list MCP tools")
270
- raise e
253
+ logger.warning(f"Error during disconnect: {e}")
254
+ finally:
255
+ self._exit_stack = None
271
256
 
272
- def generate_function_from_mcp_tool(self, mcp_tool: "Tool") -> Callable:
273
- r"""Dynamically generates a Python callable function corresponding to
274
- a given MCP tool.
257
+ self._is_connected = False
258
+ logger.debug("Disconnected from all MCP servers")
275
259
 
276
- Args:
277
- mcp_tool (Tool): The MCP tool definition received from the MCP
278
- server.
260
+ @property
261
+ def is_connected(self) -> bool:
262
+ r"""Check if toolkit is connected.
279
263
 
280
264
  Returns:
281
- Callable: A dynamically created async Python function that wraps
282
- the MCP tool.
265
+ bool: True if the toolkit is connected to all MCP servers,
266
+ False otherwise.
283
267
  """
284
- func_name = mcp_tool.name
285
- func_desc = mcp_tool.description or "No description provided."
286
- parameters_schema = mcp_tool.inputSchema.get("properties", {})
287
- required_params = mcp_tool.inputSchema.get("required", [])
288
-
289
- type_map = {
290
- "string": str,
291
- "integer": int,
292
- "number": float,
293
- "boolean": bool,
294
- "array": list,
295
- "object": dict,
296
- }
297
- annotations = {} # used to type hints
298
- defaults: Dict[str, Any] = {} # store default values
299
-
300
- func_params = []
301
- for param_name, param_schema in parameters_schema.items():
302
- param_type = param_schema.get("type", "Any")
303
- param_type = type_map.get(param_type, Any)
304
-
305
- annotations[param_name] = param_type
306
- if param_name not in required_params:
307
- defaults[param_name] = None
308
-
309
- func_params.append(param_name)
310
-
311
- async def dynamic_function(**kwargs) -> str:
312
- r"""Auto-generated function for MCP Tool interaction.
313
-
314
- Args:
315
- kwargs: Keyword arguments corresponding to MCP tool parameters.
316
-
317
- Returns:
318
- str: The textual result returned by the MCP tool.
319
- """
320
- from mcp.types import CallToolResult
321
-
322
- missing_params: Set[str] = set(required_params) - set(
323
- kwargs.keys()
324
- )
325
- if missing_params:
326
- logger.warning(
327
- f"Missing required parameters: {missing_params}"
328
- )
329
- return "Missing required parameters."
330
-
331
- if not self._session:
332
- logger.error(
333
- "MCP Client is not connected. Call `connection()` first."
334
- )
335
- raise RuntimeError(
336
- "MCP Client is not connected. Call `connection()` first."
337
- )
338
-
339
- try:
340
- result: CallToolResult = await self._session.call_tool(
341
- func_name, kwargs
342
- )
343
- except Exception as e:
344
- logger.error(f"Failed to call MCP tool '{func_name}': {e!s}")
345
- raise e
268
+ if not self._is_connected:
269
+ return False
346
270
 
347
- if not result.content or len(result.content) == 0:
348
- return "No data available for this request."
271
+ # Check if all clients are connected
272
+ return all(client.is_connected() for client in self.clients)
349
273
 
350
- # Handle different content types
351
- try:
352
- content = result.content[0]
353
- if content.type == "text":
354
- return content.text
355
- elif content.type == "image":
356
- # Return image URL or data URI if available
357
- if hasattr(content, "url") and content.url:
358
- return f"Image available at: {content.url}"
359
- return "Image content received (data URI not shown)"
360
- elif content.type == "embedded_resource":
361
- # Return resource information if available
362
- if hasattr(content, "name") and content.name:
363
- return f"Embedded resource: {content.name}"
364
- return "Embedded resource received"
365
- else:
366
- msg = f"Received content of type '{content.type}'"
367
- return f"{msg} which is not fully supported yet."
368
- except (IndexError, AttributeError) as e:
369
- logger.error(
370
- f"Error processing content from MCP tool response: {e!s}"
371
- )
372
- raise e
373
-
374
- dynamic_function.__name__ = func_name
375
- dynamic_function.__doc__ = func_desc
376
- dynamic_function.__annotations__ = annotations
377
-
378
- sig = inspect.Signature(
379
- parameters=[
380
- inspect.Parameter(
381
- name=param,
382
- kind=inspect.Parameter.KEYWORD_ONLY,
383
- default=defaults.get(param, inspect.Parameter.empty),
384
- annotation=annotations[param],
385
- )
386
- for param in func_params
387
- ]
388
- )
389
- dynamic_function.__signature__ = sig # type: ignore[attr-defined]
390
-
391
- return dynamic_function
392
-
393
- def _build_tool_schema(self, mcp_tool: "Tool") -> Dict[str, Any]:
394
- input_schema = mcp_tool.inputSchema
395
- properties = input_schema.get("properties", {})
396
- required = input_schema.get("required", [])
397
-
398
- parameters = {
399
- "type": "object",
400
- "properties": properties,
401
- "required": required,
402
- "additionalProperties": False,
403
- }
404
-
405
- return {
406
- "type": "function",
407
- "function": {
408
- "name": mcp_tool.name,
409
- "description": mcp_tool.description
410
- or "No description provided.",
411
- "strict": self.strict,
412
- "parameters": parameters,
413
- },
414
- }
415
-
416
- def get_tools(self) -> List[FunctionTool]:
417
- r"""Returns a list of FunctionTool objects representing the
418
- functions in the toolkit. Each function is dynamically generated
419
- based on the MCP tool definitions received from the server.
274
+ def connect_sync(self):
275
+ r"""Synchronously connect to all MCP servers."""
276
+ return run_async(self.connect)()
420
277
 
421
- Returns:
422
- List[FunctionTool]: A list of FunctionTool objects
423
- representing the functions in the toolkit.
424
- """
425
- return [
426
- FunctionTool(
427
- self.generate_function_from_mcp_tool(mcp_tool),
428
- openai_tool_schema=self._build_tool_schema(mcp_tool),
429
- )
430
- for mcp_tool in self._mcp_tools
431
- ]
278
+ def disconnect_sync(self):
279
+ r"""Synchronously disconnect from all MCP servers."""
280
+ return run_async(self.disconnect)()
432
281
 
433
- def get_text_tools(self) -> str:
434
- r"""Returns a string containing the descriptions of the tools
435
- in the toolkit.
282
+ async def __aenter__(self) -> "MCPToolkit":
283
+ r"""Async context manager entry point.
436
284
 
437
- Returns:
438
- str: A string containing the descriptions of the tools
439
- in the toolkit.
285
+ Usage:
286
+ async with MCPToolkit(config_dict=config) as toolkit:
287
+ tools = toolkit.get_tools()
440
288
  """
441
- return "\n".join(
442
- f"tool_name: {tool.name}\n"
443
- + f"description: {tool.description or 'No description'}\n"
444
- + f"input Schema: {tool.inputSchema}\n"
445
- for tool in self._mcp_tools
446
- )
447
-
448
- async def call_tool(
449
- self, tool_name: str, tool_args: Dict[str, Any]
450
- ) -> Any:
451
- r"""Calls the specified tool with the provided arguments.
289
+ await self.connect()
290
+ return self
452
291
 
453
- Args:
454
- tool_name (str): Name of the tool to call.
455
- tool_args (Dict[str, Any]): Arguments to pass to the tool
456
- (default: :obj:`{}`).
292
+ def __enter__(self) -> "MCPToolkit":
293
+ r"""Synchronously enter the async context manager."""
294
+ return run_async(self.__aenter__)()
457
295
 
458
- Returns:
459
- Any: The result of the tool call.
460
- """
461
- if self._session is None:
462
- raise RuntimeError("Session is not initialized.")
463
-
464
- return await self._session.call_tool(tool_name, tool_args)
296
+ async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
297
+ r"""Async context manager exit point."""
298
+ await self.disconnect()
299
+ return None
465
300
 
466
- @property
467
- def session(self) -> Optional["ClientSession"]:
468
- return self._session
301
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
302
+ r"""Synchronously exit the async context manager."""
303
+ return run_async(self.__aexit__)(exc_type, exc_val, exc_tb)
469
304
 
470
305
  @classmethod
471
306
  async def create(
472
307
  cls,
473
- command_or_url: str,
474
- args: Optional[List[str]] = None,
475
- env: Optional[Dict[str, str]] = None,
308
+ clients: Optional[List[MCPClient]] = None,
309
+ config_path: Optional[str] = None,
310
+ config_dict: Optional[Dict[str, Any]] = None,
476
311
  timeout: Optional[float] = None,
477
- headers: Optional[Dict[str, str]] = None,
478
- mode: Optional[str] = None,
479
- ) -> "MCPClient":
480
- r"""Factory method that creates and connects to the MCP server.
312
+ ) -> "MCPToolkit":
313
+ r"""Factory method that creates and connects to all MCP servers.
481
314
 
482
- This async factory method ensures the connection to the MCP server is
483
- established before the client object is fully constructed.
315
+ Creates a new :obj:`MCPToolkit` instance and automatically establishes
316
+ connections to all configured MCP servers. This is a convenience method
317
+ that combines instantiation and connection in a single call.
484
318
 
485
319
  Args:
486
- command_or_url (str): URL for SSE mode or command executable
487
- for stdio mode.
488
- args (Optional[List[str]]): List of command-line arguments if
489
- stdio mode is used. (default: :obj:`None`)
490
- env (Optional[Dict[str, str]]): Environment variables for
491
- the stdio mode command. (default: :obj:`None`)
492
- timeout (Optional[float]): Connection timeout.
493
- (default: :obj:`None`)
494
- headers (Optional[Dict[str, str]]): Headers for the HTTP request.
495
- (default: :obj:`None`)
496
- mode (Optional[str]): Connection mode. Can be "sse" for
497
- Server-Sent Events, "streamable-http" for
498
- streaming HTTP, or None for stdio mode.
499
- (default: :obj:`None`)
320
+ clients (Optional[List[MCPClient]], optional): List of
321
+ :obj:`MCPClient` instances to manage. (default: :obj:`None`)
322
+ config_path (Optional[str], optional): Path to a JSON configuration
323
+ file defining MCP servers. (default: :obj:`None`)
324
+ config_dict (Optional[Dict[str, Any]], optional): Dictionary
325
+ containing MCP server configurations in the same format as the
326
+ config file. (default: :obj:`None`)
327
+ timeout (Optional[float], optional): Timeout for connection
328
+ attempts in seconds. (default: :obj:`None`)
500
329
 
501
330
  Returns:
502
- MCPClient: A fully initialized and connected MCPClient instance.
331
+ MCPToolkit: A fully initialized and connected :obj:`MCPToolkit`
332
+ instance with all servers ready for use.
503
333
 
504
334
  Raises:
505
- RuntimeError: If connection to the MCP server fails.
335
+ MCPConnectionError: If connection to any MCP server fails during
336
+ initialization. All successfully connected servers will be
337
+ properly disconnected before raising the exception.
338
+ ValueError: If no configuration sources are provided or if the
339
+ configuration is invalid.
340
+
341
+ Example:
342
+ .. code-block:: python
343
+
344
+ # Create and connect in one step
345
+ toolkit = await MCPToolkit.create(config_path="servers.json")
346
+ try:
347
+ tools = toolkit.get_tools()
348
+ # Use the toolkit...
349
+ finally:
350
+ await toolkit.disconnect()
506
351
  """
507
- client = cls(
508
- command_or_url=command_or_url,
509
- args=args,
510
- env=env,
352
+ toolkit = cls(
353
+ clients=clients,
354
+ config_path=config_path,
355
+ config_dict=config_dict,
511
356
  timeout=timeout,
512
- headers=headers,
513
- mode=mode,
514
357
  )
515
358
  try:
516
- await client.connect()
517
- return client
359
+ await toolkit.connect()
360
+ return toolkit
518
361
  except Exception as e:
519
362
  # Ensure cleanup on initialization failure
520
- await client.disconnect()
521
- logger.error(f"Failed to initialize MCPClient: {e}")
522
- raise RuntimeError(f"Failed to initialize MCPClient: {e}") from e
523
-
524
- async def __aenter__(self) -> "MCPClient":
525
- r"""Async context manager entry point. Automatically connects to the
526
- MCP server when used in an async with statement.
363
+ await toolkit.disconnect()
364
+ logger.error(f"Failed to initialize MCPToolkit: {e}")
365
+ raise MCPConnectionError(
366
+ f"Failed to initialize MCPToolkit: {e}"
367
+ ) from e
527
368
 
528
- Returns:
529
- MCPClient: Self with active connection.
530
- """
531
- await self.connect()
532
- return self
533
-
534
- async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
535
- r"""Async context manager exit point. Automatically disconnects from
536
- the MCP server when exiting an async with statement.
537
- """
538
- await self.disconnect()
539
-
540
-
541
- class MCPToolkit(BaseToolkit):
542
- r"""MCPToolkit provides a unified interface for managing multiple
543
- MCP server connections and their tools.
544
-
545
- This class handles the lifecycle of multiple MCP server connections and
546
- offers a centralized configuration mechanism for both local and remote
547
- MCP services.
548
-
549
- Connection Lifecycle:
550
- There are three ways to manage the connection lifecycle:
551
-
552
- 1. Using the async context manager:
553
- ```python
554
- async with MCPToolkit(config_path="config.json") as toolkit:
555
- # Toolkit is connected here
556
- tools = toolkit.get_tools()
557
- # Toolkit is automatically disconnected here
558
- ```
559
-
560
- 2. Using the factory method:
561
- ```python
562
- toolkit = await MCPToolkit.create(config_path="config.json")
563
- # Toolkit is connected here
564
- tools = toolkit.get_tools()
565
- # Don't forget to disconnect when done!
566
- await toolkit.disconnect()
567
- ```
568
-
569
- 3. Using explicit connect/disconnect:
570
- ```python
571
- toolkit = MCPToolkit(config_path="config.json")
572
- await toolkit.connect()
573
- # Toolkit is connected here
574
- tools = toolkit.get_tools()
575
- # Don't forget to disconnect when done!
576
- await toolkit.disconnect()
577
- ```
578
-
579
- Args:
580
- servers (Optional[List[MCPClient]]): List of MCPClient
581
- instances to manage. (default: :obj:`None`)
582
- config_path (Optional[str]): Path to a JSON configuration file
583
- defining MCP servers. (default: :obj:`None`)
584
- config_dict (Optional[Dict[str, Any]]): Dictionary containing MCP
585
- server configurations in the same format as the config file.
586
- (default: :obj:`None`)
587
- strict (Optional[bool]): Whether to enforce strict mode for the
588
- function call. (default: :obj:`False`)
369
+ @classmethod
370
+ def create_sync(
371
+ cls,
372
+ clients: Optional[List[MCPClient]] = None,
373
+ config_path: Optional[str] = None,
374
+ config_dict: Optional[Dict[str, Any]] = None,
375
+ timeout: Optional[float] = None,
376
+ ) -> "MCPToolkit":
377
+ r"""Synchronously create and connect to all MCP servers."""
378
+ return run_async(cls.create)(
379
+ clients, config_path, config_dict, timeout
380
+ )
589
381
 
590
- Note:
591
- Either `servers`, `config_path`, or `config_dict` must be provided.
592
- If multiple are provided, servers from all sources will be combined.
382
+ def _load_clients_from_config(self, config_path: str) -> List[MCPClient]:
383
+ r"""Load clients from configuration file."""
384
+ if not os.path.exists(config_path):
385
+ raise FileNotFoundError(f"Config file not found: '{config_path}'")
593
386
 
594
- For web servers in the config, you can specify authorization
595
- headers using the "headers" field to connect to protected MCP server
596
- endpoints.
387
+ try:
388
+ with open(config_path, "r", encoding="utf-8") as f:
389
+ data = json.load(f)
390
+ except json.JSONDecodeError as e:
391
+ error_msg = f"Invalid JSON in config file '{config_path}': {e}"
392
+ raise ValueError(error_msg) from e
393
+ except Exception as e:
394
+ error_msg = f"Error reading config file '{config_path}': {e}"
395
+ raise IOError(error_msg) from e
597
396
 
598
- Example configuration:
397
+ return self._load_clients_from_dict(data)
599
398
 
600
- .. code-block:: json
399
+ def _load_clients_from_dict(
400
+ self, config: Dict[str, Any]
401
+ ) -> List[MCPClient]:
402
+ r"""Load clients from configuration dictionary."""
403
+ if not isinstance(config, dict):
404
+ raise ValueError("Config must be a dictionary")
601
405
 
602
- {
603
- "mcpServers": {
604
- "protected-server": {
605
- "url": "https://example.com/mcp",
606
- "timeout": 30,
607
- "headers": {
608
- "Authorization": "Bearer YOUR_TOKEN",
609
- "X-API-Key": "YOUR_API_KEY"
610
- }
611
- }
612
- }
613
- }
406
+ mcp_servers = config.get("mcpServers", {})
407
+ if not isinstance(mcp_servers, dict):
408
+ raise ValueError("'mcpServers' must be a dictionary")
614
409
 
615
- Attributes:
616
- servers (List[MCPClient]): List of MCPClient instances being managed.
617
- """
410
+ clients = []
618
411
 
619
- def __init__(
620
- self,
621
- servers: Optional[List[MCPClient]] = None,
622
- config_path: Optional[str] = None,
623
- config_dict: Optional[Dict[str, Any]] = None,
624
- strict: Optional[bool] = False,
625
- ):
626
- super().__init__()
412
+ for name, cfg in mcp_servers.items():
413
+ try:
414
+ if "timeout" not in cfg and self.timeout is not None:
415
+ cfg["timeout"] = self.timeout
627
416
 
628
- sources_provided = sum(
629
- 1 for src in [servers, config_path, config_dict] if src is not None
630
- )
631
- if sources_provided > 1:
632
- logger.warning(
633
- "Multiple configuration sources provided "
634
- f"({sources_provided}). Servers from all sources "
635
- "will be combined."
636
- )
417
+ client = self._create_client_from_config(name, cfg)
418
+ clients.append(client)
419
+ except Exception as e:
420
+ logger.error(f"Failed to create client for '{name}': {e}")
421
+ error_msg = f"Invalid configuration for server '{name}': {e}"
422
+ raise ValueError(error_msg) from e
637
423
 
638
- self.servers: List[MCPClient] = servers or []
424
+ return clients
639
425
 
640
- if config_path:
641
- self.servers.extend(
642
- self._load_servers_from_config(config_path, strict)
643
- )
426
+ def _create_client_from_config(
427
+ self, name: str, cfg: Dict[str, Any]
428
+ ) -> MCPClient:
429
+ r"""Create a single MCP client from configuration."""
430
+ if not isinstance(cfg, dict):
431
+ error_msg = f"Configuration for server '{name}' must be a dict"
432
+ raise ValueError(error_msg)
644
433
 
645
- if config_dict:
646
- self.servers.extend(self._load_servers_from_dict(config_dict))
434
+ try:
435
+ # Use the new mcp_client factory function
436
+ # Pass timeout from toolkit if available
437
+ kwargs = {}
438
+ if hasattr(self, "timeout") and self.timeout is not None:
439
+ kwargs["timeout"] = self.timeout
647
440
 
648
- self._connected = False
441
+ client = create_mcp_client(cfg, **kwargs)
442
+ return client
443
+ except Exception as e:
444
+ error_msg = f"Failed to create client for server '{name}': {e}"
445
+ raise ValueError(error_msg) from e
649
446
 
650
- def _load_servers_from_config(
651
- self, config_path: str, strict: Optional[bool] = False
652
- ) -> List[MCPClient]:
653
- r"""Loads MCP server configurations from a JSON file.
447
+ def get_tools(self) -> List[FunctionTool]:
448
+ r"""Aggregates all tools from the managed MCP client instances.
654
449
 
655
- Args:
656
- config_path (str): Path to the JSON configuration file.
657
- strict (bool): Whether to enforce strict mode for the
658
- function call. (default: :obj:`False`)
450
+ Collects and combines tools from all connected MCP clients into a
451
+ single unified list. Each tool is converted to a CAMEL-compatible
452
+ :obj:`FunctionTool` that can be used with CAMEL agents.
659
453
 
660
454
  Returns:
661
- List[MCPClient]: List of configured MCPClient instances.
455
+ List[FunctionTool]: Combined list of all available function tools
456
+ from all connected MCP servers. Returns an empty list if no
457
+ clients are connected or if no tools are available.
458
+
459
+ Note:
460
+ This method can be called even when the toolkit is not connected,
461
+ but it will log a warning and may return incomplete results.
462
+ For best results, ensure the toolkit is connected before calling
463
+ this method.
464
+
465
+ Example:
466
+ .. code-block:: python
467
+
468
+ async with MCPToolkit(config_dict=config) as toolkit:
469
+ tools = toolkit.get_tools()
470
+ print(f"Available tools: {len(tools)}")
471
+ for tool in tools:
472
+ print(f" - {tool.func.__name__}")
662
473
  """
663
- try:
664
- with open(config_path, "r", encoding="utf-8") as f:
665
- try:
666
- data = json.load(f)
667
- except json.JSONDecodeError as e:
668
- logger.warning(
669
- f"Invalid JSON in config file '{config_path}': {e!s}"
670
- )
671
- raise e
672
- except FileNotFoundError as e:
673
- logger.warning(f"Config file not found: '{config_path}'")
674
- raise e
474
+ if not self.is_connected:
475
+ logger.warning(
476
+ "MCPToolkit is not connected. "
477
+ "Tools may not be available until connected."
478
+ )
675
479
 
676
- return self._load_servers_from_dict(config=data, strict=strict)
480
+ all_tools = []
481
+ for i, client in enumerate(self.clients):
482
+ try:
483
+ client_tools = client.get_tools()
484
+ all_tools.extend(client_tools)
485
+ logger.debug(
486
+ f"Client {i+1} contributed {len(client_tools)} tools"
487
+ )
488
+ except Exception as e:
489
+ logger.error(f"Failed to get tools from client {i+1}: {e}")
677
490
 
678
- def _load_servers_from_dict(
679
- self, config: Dict[str, Any], strict: Optional[bool] = False
680
- ) -> List[MCPClient]:
681
- r"""Loads MCP server configurations from a dictionary.
491
+ logger.info(f"Total tools available: {len(all_tools)}")
492
+ return all_tools
682
493
 
683
- Args:
684
- config (Dict[str, Any]): Dictionary containing server
685
- configurations.
686
- strict (bool): Whether to enforce strict mode for the
687
- function call. (default: :obj:`False`)
494
+ def get_text_tools(self) -> str:
495
+ r"""Returns a string containing the descriptions of the tools.
688
496
 
689
497
  Returns:
690
- List[MCPClient]: List of configured MCPClient instances.
498
+ str: A string containing the descriptions of all tools.
691
499
  """
692
- all_servers = []
693
-
694
- mcp_servers = config.get("mcpServers", {})
695
- if not isinstance(mcp_servers, dict):
696
- logger.warning("'mcpServers' is not a dictionary, skipping...")
697
- mcp_servers = {}
500
+ if not self.is_connected:
501
+ logger.warning(
502
+ "MCPToolkit is not connected. "
503
+ "Tool descriptions may not be available until connected."
504
+ )
698
505
 
699
- for name, cfg in mcp_servers.items():
700
- if not isinstance(cfg, dict):
701
- logger.warning(
702
- f"Configuration for server '{name}' must be a dictionary"
506
+ tool_descriptions = []
507
+ for i, client in enumerate(self.clients):
508
+ try:
509
+ client_tools_text = client.get_text_tools()
510
+ if client_tools_text:
511
+ tool_descriptions.append(
512
+ f"=== Client {i+1} Tools ===\n{client_tools_text}"
513
+ )
514
+ except Exception as e:
515
+ logger.error(
516
+ f"Failed to get tool descriptions from client {i+1}: {e}"
703
517
  )
704
- continue
705
518
 
706
- if "command" not in cfg and "url" not in cfg:
707
- logger.warning(
708
- f"Missing required 'command' or 'url' field for server "
709
- f"'{name}'"
710
- )
711
- continue
519
+ return "\n\n".join(tool_descriptions)
712
520
 
713
- # Include headers if provided in the configuration
714
- headers = cfg.get("headers", {})
715
-
716
- cmd_or_url = cast(str, cfg.get("command") or cfg.get("url"))
717
- server = MCPClient(
718
- command_or_url=cmd_or_url,
719
- args=cfg.get("args", []),
720
- env={**os.environ, **cfg.get("env", {})},
721
- timeout=cfg.get("timeout", None),
722
- headers=headers,
723
- mode=cfg.get("mode", None),
724
- strict=strict,
725
- )
726
- all_servers.append(server)
521
+ async def call_tool(
522
+ self, tool_name: str, tool_args: Dict[str, Any]
523
+ ) -> Any:
524
+ r"""Call a tool by name across all managed clients.
727
525
 
728
- return all_servers
526
+ Searches for and executes a tool with the specified name across all
527
+ connected MCP clients. The method will try each client in sequence
528
+ until the tool is found and successfully executed.
729
529
 
730
- async def connect(self):
731
- r"""Explicitly connect to all MCP servers.
530
+ Args:
531
+ tool_name (str): Name of the tool to call. Must match a tool name
532
+ available from one of the connected MCP servers.
533
+ tool_args (Dict[str, Any]): Arguments to pass to the tool. The
534
+ argument names and types must match the tool's expected
535
+ parameters.
732
536
 
733
537
  Returns:
734
- MCPToolkit: The connected toolkit instance
735
- """
736
- if self._connected:
737
- logger.warning("MCPToolkit is already connected")
738
- return self
739
-
740
- try:
741
- # Sequentially connect to each server
742
- for server in self.servers:
743
- await server.connect()
744
- self._connected = True
745
- return self
746
- except Exception as e:
747
- # Ensure resources are cleaned up on connection failure
748
- await self.disconnect()
749
- logger.error(f"Failed to connect to one or more MCP servers: {e}")
750
- raise e
538
+ Any: The result of the tool call. The type and structure depend
539
+ on the specific tool being called.
751
540
 
752
- async def disconnect(self):
753
- r"""Explicitly disconnect from all MCP servers."""
754
- if not self._connected:
755
- return
756
-
757
- for server in self.servers:
758
- await server.disconnect()
759
- self._connected = False
760
-
761
- @asynccontextmanager
762
- async def connection(self) -> AsyncGenerator["MCPToolkit", None]:
763
- r"""Async context manager that simultaneously establishes connections
764
- to all managed MCP server instances.
765
-
766
- Yields:
767
- MCPToolkit: Self with all servers connected.
541
+ Raises:
542
+ MCPConnectionError: If the toolkit is not connected to any MCP
543
+ servers.
544
+ MCPToolError: If the tool is not found in any client, or if all
545
+ attempts to call the tool fail. The error message will include
546
+ details about the last failure encountered.
547
+
548
+ Example:
549
+ .. code-block:: python
550
+
551
+ async with MCPToolkit(config_dict=config) as toolkit:
552
+ # Call a file reading tool
553
+ result = await toolkit.call_tool(
554
+ "read_file",
555
+ {"path": "/tmp/example.txt"}
556
+ )
557
+ print(f"File contents: {result}")
768
558
  """
769
- try:
770
- await self.connect()
771
- yield self
772
- finally:
773
- await self.disconnect()
774
-
775
- def is_connected(self) -> bool:
776
- r"""Checks if all the managed servers are connected.
559
+ if not self.is_connected:
560
+ raise MCPConnectionError(
561
+ "MCPToolkit is not connected. Call connect() first."
562
+ )
777
563
 
778
- Returns:
779
- bool: True if connected, False otherwise.
780
- """
781
- return self._connected
564
+ # Try to find and call the tool from any client
565
+ last_error = None
566
+ for i, client in enumerate(self.clients):
567
+ try:
568
+ # Check if this client has the tool
569
+ tools = client.get_tools()
570
+ tool_names = [tool.func.__name__ for tool in tools]
571
+
572
+ if tool_name in tool_names:
573
+ result = await client.call_tool(tool_name, tool_args)
574
+ logger.debug(
575
+ f"Tool '{tool_name}' called successfully "
576
+ f"on client {i+1}"
577
+ )
578
+ return result
579
+ except Exception as e:
580
+ last_error = e
581
+ logger.debug(f"Tool '{tool_name}' failed on client {i+1}: {e}")
582
+ continue
782
583
 
783
- def get_tools(self) -> List[FunctionTool]:
784
- r"""Aggregates all tools from the managed MCP server instances.
584
+ # If we get here, the tool wasn't found or all calls failed
585
+ if last_error:
586
+ raise MCPToolError(
587
+ f"Tool '{tool_name}' failed on all clients. "
588
+ f"Last error: {last_error}"
589
+ ) from last_error
590
+ else:
591
+ raise MCPToolError(f"Tool '{tool_name}' not found in any client")
785
592
 
786
- Returns:
787
- List[FunctionTool]: Combined list of all available function tools.
788
- """
789
- all_tools = []
790
- for server in self.servers:
791
- all_tools.extend(server.get_tools())
792
- return all_tools
593
+ def call_tool_sync(self, tool_name: str, tool_args: Dict[str, Any]) -> Any:
594
+ r"""Synchronously call a tool."""
595
+ return run_async(self.call_tool)(tool_name, tool_args)
793
596
 
794
- def get_text_tools(self) -> str:
795
- r"""Returns a string containing the descriptions of the tools
796
- in the toolkit.
597
+ def list_available_tools(self) -> Dict[str, List[str]]:
598
+ r"""List all available tools organized by client.
797
599
 
798
600
  Returns:
799
- str: A string containing the descriptions of the tools
800
- in the toolkit.
601
+ Dict[str, List[str]]: Dictionary mapping client indices to tool
602
+ names.
801
603
  """
802
- return "\n".join(server.get_text_tools() for server in self.servers)
604
+ available_tools = {}
605
+ for i, client in enumerate(self.clients):
606
+ try:
607
+ tools = client.get_tools()
608
+ tool_names = [tool.func.__name__ for tool in tools]
609
+ available_tools[f"client_{i+1}"] = tool_names
610
+ except Exception as e:
611
+ logger.error(f"Failed to list tools from client {i+1}: {e}")
612
+ available_tools[f"client_{i+1}"] = []
613
+
614
+ return available_tools