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