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.
- camel/__init__.py +1 -1
- camel/agents/chat_agent.py +159 -8
- camel/agents/mcp_agent.py +5 -5
- camel/configs/anthropic_config.py +6 -5
- camel/{data_collector → data_collectors}/alpaca_collector.py +1 -1
- camel/{data_collector → data_collectors}/sharegpt_collector.py +1 -1
- camel/datagen/evol_instruct/scorer.py +22 -23
- camel/datagen/evol_instruct/templates.py +46 -46
- camel/datasets/static_dataset.py +144 -0
- camel/loaders/__init__.py +5 -2
- camel/loaders/chunkr_reader.py +117 -91
- camel/loaders/mistral_reader.py +148 -0
- camel/memories/blocks/chat_history_block.py +1 -2
- camel/models/model_manager.py +7 -3
- camel/retrievers/auto_retriever.py +20 -1
- camel/{runtime → runtimes}/daytona_runtime.py +1 -1
- camel/{runtime → runtimes}/docker_runtime.py +1 -1
- camel/{runtime → runtimes}/llm_guard_runtime.py +2 -2
- camel/{runtime → runtimes}/remote_http_runtime.py +1 -1
- camel/{runtime → runtimes}/ubuntu_docker_runtime.py +1 -1
- camel/societies/workforce/base.py +7 -3
- camel/societies/workforce/single_agent_worker.py +2 -1
- camel/societies/workforce/worker.py +5 -3
- camel/societies/workforce/workforce.py +65 -24
- camel/storages/__init__.py +2 -0
- camel/storages/vectordb_storages/__init__.py +2 -0
- camel/storages/vectordb_storages/faiss.py +712 -0
- camel/toolkits/__init__.py +4 -0
- camel/toolkits/async_browser_toolkit.py +75 -523
- camel/toolkits/bohrium_toolkit.py +318 -0
- camel/toolkits/browser_toolkit.py +215 -538
- camel/toolkits/browser_toolkit_commons.py +568 -0
- camel/toolkits/file_write_toolkit.py +80 -31
- camel/toolkits/mcp_toolkit.py +477 -665
- camel/toolkits/pptx_toolkit.py +777 -0
- camel/toolkits/wolfram_alpha_toolkit.py +5 -1
- camel/types/enums.py +13 -1
- camel/utils/__init__.py +2 -0
- camel/utils/commons.py +27 -0
- camel/utils/mcp_client.py +979 -0
- {camel_ai-0.2.60.dist-info → camel_ai-0.2.62.dist-info}/METADATA +14 -1
- {camel_ai-0.2.60.dist-info → camel_ai-0.2.62.dist-info}/RECORD +53 -47
- /camel/{data_collector → data_collectors}/__init__.py +0 -0
- /camel/{data_collector → data_collectors}/base.py +0 -0
- /camel/{runtime → runtimes}/__init__.py +0 -0
- /camel/{runtime → runtimes}/api.py +0 -0
- /camel/{runtime → runtimes}/base.py +0 -0
- /camel/{runtime → runtimes}/configs.py +0 -0
- /camel/{runtime → runtimes}/utils/__init__.py +0 -0
- /camel/{runtime → runtimes}/utils/function_risk_toolkit.py +0 -0
- /camel/{runtime → runtimes}/utils/ignore_risk_toolkit.py +0 -0
- {camel_ai-0.2.60.dist-info → camel_ai-0.2.62.dist-info}/WHEEL +0 -0
- {camel_ai-0.2.60.dist-info → camel_ai-0.2.62.dist-info}/licenses/LICENSE +0 -0
camel/toolkits/mcp_toolkit.py
CHANGED
|
@@ -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
|
-
|
|
14
|
+
|
|
15
15
|
import json
|
|
16
16
|
import os
|
|
17
|
-
import
|
|
18
|
-
from
|
|
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
|
|
45
|
-
r"""
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
56
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
100
|
-
|
|
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
|
-
|
|
104
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
118
|
-
|
|
150
|
+
# Call parent constructor first
|
|
119
151
|
super().__init__(timeout=timeout)
|
|
120
152
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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.
|
|
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"""
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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("
|
|
211
|
+
logger.warning("MCPToolkit is already connected")
|
|
146
212
|
return self
|
|
147
213
|
|
|
214
|
+
self._exit_stack = AsyncExitStack()
|
|
215
|
+
|
|
148
216
|
try:
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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"""
|
|
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
|
-
|
|
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.
|
|
251
|
+
await self._exit_stack.aclose()
|
|
254
252
|
except Exception as e:
|
|
255
|
-
logger.warning(f"Error: {e}")
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
a given MCP tool.
|
|
257
|
+
self._is_connected = False
|
|
258
|
+
logger.debug("Disconnected from all MCP servers")
|
|
275
259
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
260
|
+
@property
|
|
261
|
+
def is_connected(self) -> bool:
|
|
262
|
+
r"""Check if toolkit is connected.
|
|
279
263
|
|
|
280
264
|
Returns:
|
|
281
|
-
|
|
282
|
-
|
|
265
|
+
bool: True if the toolkit is connected to all MCP servers,
|
|
266
|
+
False otherwise.
|
|
283
267
|
"""
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
348
|
-
|
|
271
|
+
# Check if all clients are connected
|
|
272
|
+
return all(client.is_connected() for client in self.clients)
|
|
349
273
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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
|
|
434
|
-
r"""
|
|
435
|
-
in the toolkit.
|
|
282
|
+
async def __aenter__(self) -> "MCPToolkit":
|
|
283
|
+
r"""Async context manager entry point.
|
|
436
284
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
285
|
+
Usage:
|
|
286
|
+
async with MCPToolkit(config_dict=config) as toolkit:
|
|
287
|
+
tools = toolkit.get_tools()
|
|
440
288
|
"""
|
|
441
|
-
|
|
442
|
-
|
|
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
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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
|
-
|
|
467
|
-
|
|
468
|
-
return self.
|
|
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
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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
|
-
|
|
478
|
-
|
|
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
|
-
|
|
483
|
-
|
|
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
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
the
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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
|
-
|
|
331
|
+
MCPToolkit: A fully initialized and connected :obj:`MCPToolkit`
|
|
332
|
+
instance with all servers ready for use.
|
|
503
333
|
|
|
504
334
|
Raises:
|
|
505
|
-
|
|
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
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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
|
|
517
|
-
return
|
|
359
|
+
await toolkit.connect()
|
|
360
|
+
return toolkit
|
|
518
361
|
except Exception as e:
|
|
519
362
|
# Ensure cleanup on initialization failure
|
|
520
|
-
await
|
|
521
|
-
logger.error(f"Failed to initialize
|
|
522
|
-
raise
|
|
523
|
-
|
|
524
|
-
|
|
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
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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
|
-
|
|
595
|
-
|
|
596
|
-
|
|
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
|
-
|
|
397
|
+
return self._load_clients_from_dict(data)
|
|
599
398
|
|
|
600
|
-
|
|
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
|
-
|
|
604
|
-
|
|
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
|
-
|
|
616
|
-
servers (List[MCPClient]): List of MCPClient instances being managed.
|
|
617
|
-
"""
|
|
410
|
+
clients = []
|
|
618
411
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
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
|
-
|
|
424
|
+
return clients
|
|
639
425
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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
|
-
|
|
646
|
-
|
|
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
|
-
|
|
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
|
|
651
|
-
|
|
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
|
-
|
|
656
|
-
|
|
657
|
-
|
|
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[
|
|
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
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
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
|
-
|
|
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
|
-
|
|
679
|
-
|
|
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
|
-
|
|
684
|
-
|
|
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
|
-
|
|
498
|
+
str: A string containing the descriptions of all tools.
|
|
691
499
|
"""
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
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
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
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
|
-
|
|
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
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
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
|
-
|
|
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
|
-
|
|
731
|
-
|
|
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
|
-
|
|
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
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
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
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
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
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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
|
-
|
|
784
|
-
|
|
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
|
-
|
|
787
|
-
|
|
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
|
|
795
|
-
r"""
|
|
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:
|
|
800
|
-
|
|
601
|
+
Dict[str, List[str]]: Dictionary mapping client indices to tool
|
|
602
|
+
names.
|
|
801
603
|
"""
|
|
802
|
-
|
|
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
|