camel-ai 0.2.61__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 +1 -1
- camel/agents/mcp_agent.py +5 -5
- camel/{data_collector → data_collectors}/alpaca_collector.py +1 -1
- camel/{data_collector → data_collectors}/sharegpt_collector.py +1 -1
- 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/toolkits/__init__.py +2 -0
- camel/toolkits/file_write_toolkit.py +4 -2
- camel/toolkits/mcp_toolkit.py +469 -733
- camel/toolkits/pptx_toolkit.py +777 -0
- camel/utils/mcp_client.py +979 -0
- {camel_ai-0.2.61.dist-info → camel_ai-0.2.62.dist-info}/METADATA +4 -1
- {camel_ai-0.2.61.dist-info → camel_ai-0.2.62.dist-info}/RECORD +32 -30
- /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.61.dist-info → camel_ai-0.2.62.dist-info}/WHEEL +0 -0
- {camel_ai-0.2.61.dist-info → camel_ai-0.2.62.dist-info}/licenses/LICENSE +0 -0
camel/toolkits/mcp_toolkit.py
CHANGED
|
@@ -11,868 +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
|
|
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
|
|
46
|
-
r"""
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
54
|
-
|
|
34
|
+
class MCPToolError(Exception):
|
|
35
|
+
r"""Raised when MCP tool execution fails."""
|
|
55
36
|
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
101
|
-
|
|
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
|
-
|
|
105
|
-
|
|
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.
|
|
106
141
|
"""
|
|
107
142
|
|
|
108
143
|
def __init__(
|
|
109
144
|
self,
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
145
|
+
clients: Optional[List[MCPClient]] = None,
|
|
146
|
+
config_path: Optional[str] = None,
|
|
147
|
+
config_dict: Optional[Dict[str, Any]] = None,
|
|
113
148
|
timeout: Optional[float] = None,
|
|
114
|
-
headers: Optional[Dict[str, str]] = None,
|
|
115
|
-
mode: Optional[str] = None,
|
|
116
|
-
strict: Optional[bool] = False,
|
|
117
149
|
):
|
|
118
|
-
|
|
119
|
-
|
|
150
|
+
# Call parent constructor first
|
|
120
151
|
super().__init__(timeout=timeout)
|
|
121
152
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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)
|
|
128
163
|
|
|
129
|
-
self.
|
|
130
|
-
self._session: Optional['ClientSession'] = None
|
|
131
|
-
self._exit_stack = AsyncExitStack()
|
|
164
|
+
self.clients: List[MCPClient] = clients or []
|
|
132
165
|
self._is_connected = False
|
|
166
|
+
self._exit_stack: Optional[AsyncExitStack] = None
|
|
133
167
|
|
|
134
|
-
|
|
135
|
-
|
|
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
|
|
168
|
+
# Load clients from config sources
|
|
169
|
+
if config_path:
|
|
170
|
+
self.clients.extend(self._load_clients_from_config(config_path))
|
|
144
171
|
|
|
145
|
-
if
|
|
146
|
-
|
|
147
|
-
return self
|
|
172
|
+
if config_dict:
|
|
173
|
+
self.clients.extend(self._load_clients_from_dict(config_dict))
|
|
148
174
|
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
)
|
|
175
|
+
if not self.clients:
|
|
176
|
+
raise ValueError("No valid MCP clients could be created")
|
|
205
177
|
|
|
206
|
-
|
|
207
|
-
|
|
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
|
|
178
|
+
async def connect(self) -> "MCPToolkit":
|
|
179
|
+
r"""Connect to all MCP servers using AsyncExitStack.
|
|
223
180
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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.
|
|
227
184
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
if not self._is_connected:
|
|
232
|
-
return
|
|
233
|
-
self._is_connected = False
|
|
185
|
+
Returns:
|
|
186
|
+
MCPToolkit: Returns :obj:`self` for method chaining, allowing for
|
|
187
|
+
fluent interface usage.
|
|
234
188
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
finally:
|
|
240
|
-
self._exit_stack = AsyncExitStack()
|
|
241
|
-
self._session = None
|
|
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.
|
|
242
193
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
194
|
+
Warning:
|
|
195
|
+
If any client fails to connect, all previously established
|
|
196
|
+
connections will be automatically cleaned up before raising
|
|
197
|
+
the exception.
|
|
246
198
|
|
|
247
|
-
|
|
248
|
-
|
|
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`.
|
|
199
|
+
Example:
|
|
200
|
+
.. code-block:: python
|
|
252
201
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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()
|
|
256
209
|
"""
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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)()
|
|
210
|
+
if self._is_connected:
|
|
211
|
+
logger.warning("MCPToolkit is already connected")
|
|
212
|
+
return self
|
|
269
213
|
|
|
270
|
-
|
|
271
|
-
r"""Retrieves the list of available tools from the connected MCP
|
|
272
|
-
server.
|
|
214
|
+
self._exit_stack = AsyncExitStack()
|
|
273
215
|
|
|
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
216
|
try:
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
|
293
231
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
232
|
+
self._is_connected = True
|
|
233
|
+
msg = f"Successfully connected to {len(self.clients)} MCP servers"
|
|
234
|
+
logger.info(msg)
|
|
235
|
+
return self
|
|
297
236
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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."
|
|
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
|
|
348
243
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
raise RuntimeError(
|
|
354
|
-
"MCP Client is not connected. Call `connection()` first."
|
|
355
|
-
)
|
|
244
|
+
async def disconnect(self):
|
|
245
|
+
r"""Disconnect from all MCP servers."""
|
|
246
|
+
if not self._is_connected:
|
|
247
|
+
return
|
|
356
248
|
|
|
249
|
+
if self._exit_stack:
|
|
357
250
|
try:
|
|
358
|
-
|
|
359
|
-
func_name, kwargs
|
|
360
|
-
)
|
|
251
|
+
await self._exit_stack.aclose()
|
|
361
252
|
except Exception as e:
|
|
362
|
-
logger.
|
|
363
|
-
|
|
253
|
+
logger.warning(f"Error during disconnect: {e}")
|
|
254
|
+
finally:
|
|
255
|
+
self._exit_stack = None
|
|
364
256
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
-
}
|
|
257
|
+
self._is_connected = False
|
|
258
|
+
logger.debug("Disconnected from all MCP servers")
|
|
437
259
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
based on the MCP tool definitions received from the server.
|
|
260
|
+
@property
|
|
261
|
+
def is_connected(self) -> bool:
|
|
262
|
+
r"""Check if toolkit is connected.
|
|
442
263
|
|
|
443
264
|
Returns:
|
|
444
|
-
|
|
445
|
-
|
|
265
|
+
bool: True if the toolkit is connected to all MCP servers,
|
|
266
|
+
False otherwise.
|
|
446
267
|
"""
|
|
447
|
-
|
|
448
|
-
|
|
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
|
-
]
|
|
268
|
+
if not self._is_connected:
|
|
269
|
+
return False
|
|
454
270
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
in the toolkit.
|
|
271
|
+
# Check if all clients are connected
|
|
272
|
+
return all(client.is_connected() for client in self.clients)
|
|
458
273
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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
|
-
)
|
|
274
|
+
def connect_sync(self):
|
|
275
|
+
r"""Synchronously connect to all MCP servers."""
|
|
276
|
+
return run_async(self.connect)()
|
|
469
277
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
r"""Calls the specified tool with the provided arguments.
|
|
278
|
+
def disconnect_sync(self):
|
|
279
|
+
r"""Synchronously disconnect from all MCP servers."""
|
|
280
|
+
return run_async(self.disconnect)()
|
|
474
281
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
tool_args (Dict[str, Any]): Arguments to pass to the tool
|
|
478
|
-
(default: :obj:`{}`).
|
|
282
|
+
async def __aenter__(self) -> "MCPToolkit":
|
|
283
|
+
r"""Async context manager entry point.
|
|
479
284
|
|
|
480
|
-
|
|
481
|
-
|
|
285
|
+
Usage:
|
|
286
|
+
async with MCPToolkit(config_dict=config) as toolkit:
|
|
287
|
+
tools = toolkit.get_tools()
|
|
482
288
|
"""
|
|
483
|
-
|
|
484
|
-
|
|
289
|
+
await self.connect()
|
|
290
|
+
return self
|
|
485
291
|
|
|
486
|
-
|
|
292
|
+
def __enter__(self) -> "MCPToolkit":
|
|
293
|
+
r"""Synchronously enter the async context manager."""
|
|
294
|
+
return run_async(self.__aenter__)()
|
|
487
295
|
|
|
488
|
-
def
|
|
489
|
-
r"""
|
|
490
|
-
|
|
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
|
|
491
300
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
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)
|
|
495
304
|
|
|
496
305
|
@classmethod
|
|
497
306
|
async def create(
|
|
498
307
|
cls,
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
308
|
+
clients: Optional[List[MCPClient]] = None,
|
|
309
|
+
config_path: Optional[str] = None,
|
|
310
|
+
config_dict: Optional[Dict[str, Any]] = None,
|
|
502
311
|
timeout: Optional[float] = None,
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
) -> "MCPClient":
|
|
506
|
-
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.
|
|
507
314
|
|
|
508
|
-
|
|
509
|
-
|
|
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.
|
|
510
318
|
|
|
511
319
|
Args:
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
the
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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`)
|
|
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`)
|
|
526
329
|
|
|
527
330
|
Returns:
|
|
528
|
-
|
|
331
|
+
MCPToolkit: A fully initialized and connected :obj:`MCPToolkit`
|
|
332
|
+
instance with all servers ready for use.
|
|
529
333
|
|
|
530
334
|
Raises:
|
|
531
|
-
|
|
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()
|
|
532
351
|
"""
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
352
|
+
toolkit = cls(
|
|
353
|
+
clients=clients,
|
|
354
|
+
config_path=config_path,
|
|
355
|
+
config_dict=config_dict,
|
|
537
356
|
timeout=timeout,
|
|
538
|
-
headers=headers,
|
|
539
|
-
mode=mode,
|
|
540
357
|
)
|
|
541
358
|
try:
|
|
542
|
-
await
|
|
543
|
-
return
|
|
359
|
+
await toolkit.connect()
|
|
360
|
+
return toolkit
|
|
544
361
|
except Exception as e:
|
|
545
362
|
# Ensure cleanup on initialization failure
|
|
546
|
-
await
|
|
547
|
-
logger.error(f"Failed to initialize
|
|
548
|
-
raise
|
|
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
|
|
549
368
|
|
|
550
369
|
@classmethod
|
|
551
370
|
def create_sync(
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
371
|
+
cls,
|
|
372
|
+
clients: Optional[List[MCPClient]] = None,
|
|
373
|
+
config_path: Optional[str] = None,
|
|
374
|
+
config_dict: Optional[Dict[str, Any]] = None,
|
|
556
375
|
timeout: Optional[float] = None,
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
return run_async(self.create)(
|
|
562
|
-
command_or_url, args, env, timeout, headers, mode
|
|
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
|
|
563
380
|
)
|
|
564
381
|
|
|
565
|
-
|
|
566
|
-
r"""
|
|
567
|
-
|
|
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}'")
|
|
568
386
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
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
|
-
"""
|
|
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
|
|
682
396
|
|
|
683
|
-
|
|
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__()
|
|
397
|
+
return self._load_clients_from_dict(data)
|
|
691
398
|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
f"({sources_provided}). Servers from all sources "
|
|
699
|
-
"will be combined."
|
|
700
|
-
)
|
|
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")
|
|
701
405
|
|
|
702
|
-
|
|
406
|
+
mcp_servers = config.get("mcpServers", {})
|
|
407
|
+
if not isinstance(mcp_servers, dict):
|
|
408
|
+
raise ValueError("'mcpServers' must be a dictionary")
|
|
703
409
|
|
|
704
|
-
|
|
705
|
-
self.servers.extend(
|
|
706
|
-
self._load_servers_from_config(config_path, strict)
|
|
707
|
-
)
|
|
410
|
+
clients = []
|
|
708
411
|
|
|
709
|
-
|
|
710
|
-
|
|
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
|
|
711
416
|
|
|
712
|
-
|
|
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
|
|
713
423
|
|
|
714
|
-
|
|
715
|
-
self, config_path: str, strict: Optional[bool] = False
|
|
716
|
-
) -> List[MCPClient]:
|
|
717
|
-
r"""Loads MCP server configurations from a JSON file.
|
|
424
|
+
return clients
|
|
718
425
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
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)
|
|
723
433
|
|
|
724
|
-
Returns:
|
|
725
|
-
List[MCPClient]: List of configured MCPClient instances.
|
|
726
|
-
"""
|
|
727
434
|
try:
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
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
|
|
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
|
|
739
440
|
|
|
740
|
-
|
|
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
|
|
741
446
|
|
|
742
|
-
def
|
|
743
|
-
|
|
744
|
-
) -> List[MCPClient]:
|
|
745
|
-
r"""Loads MCP server configurations from a dictionary.
|
|
447
|
+
def get_tools(self) -> List[FunctionTool]:
|
|
448
|
+
r"""Aggregates all tools from the managed MCP client instances.
|
|
746
449
|
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
strict (bool): Whether to enforce strict mode for the
|
|
751
|
-
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.
|
|
752
453
|
|
|
753
454
|
Returns:
|
|
754
|
-
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__}")
|
|
755
473
|
"""
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
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
|
|
474
|
+
if not self.is_connected:
|
|
475
|
+
logger.warning(
|
|
476
|
+
"MCPToolkit is not connected. "
|
|
477
|
+
"Tools may not be available until connected."
|
|
478
|
+
)
|
|
769
479
|
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
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"
|
|
774
487
|
)
|
|
775
|
-
|
|
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)
|
|
488
|
+
except Exception as e:
|
|
489
|
+
logger.error(f"Failed to get tools from client {i+1}: {e}")
|
|
791
490
|
|
|
792
|
-
|
|
491
|
+
logger.info(f"Total tools available: {len(all_tools)}")
|
|
492
|
+
return all_tools
|
|
793
493
|
|
|
794
|
-
|
|
795
|
-
r"""
|
|
494
|
+
def get_text_tools(self) -> str:
|
|
495
|
+
r"""Returns a string containing the descriptions of the tools.
|
|
796
496
|
|
|
797
497
|
Returns:
|
|
798
|
-
|
|
498
|
+
str: A string containing the descriptions of all tools.
|
|
799
499
|
"""
|
|
800
|
-
if self.
|
|
801
|
-
logger.warning(
|
|
802
|
-
|
|
500
|
+
if not self.is_connected:
|
|
501
|
+
logger.warning(
|
|
502
|
+
"MCPToolkit is not connected. "
|
|
503
|
+
"Tool descriptions may not be available until connected."
|
|
504
|
+
)
|
|
803
505
|
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
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}"
|
|
517
|
+
)
|
|
815
518
|
|
|
816
|
-
|
|
817
|
-
r"""Synchronously connect to all MCP servers."""
|
|
818
|
-
return run_async(self.connect)()
|
|
519
|
+
return "\n\n".join(tool_descriptions)
|
|
819
520
|
|
|
820
|
-
async def
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
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.
|
|
824
525
|
|
|
825
|
-
for
|
|
826
|
-
|
|
827
|
-
|
|
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.
|
|
828
529
|
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
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.
|
|
832
536
|
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
to all managed MCP server instances.
|
|
537
|
+
Returns:
|
|
538
|
+
Any: The result of the tool call. The type and structure depend
|
|
539
|
+
on the specific tool being called.
|
|
837
540
|
|
|
838
|
-
|
|
839
|
-
|
|
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}")
|
|
840
558
|
"""
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
await self.disconnect()
|
|
559
|
+
if not self.is_connected:
|
|
560
|
+
raise MCPConnectionError(
|
|
561
|
+
"MCPToolkit is not connected. Call connect() first."
|
|
562
|
+
)
|
|
846
563
|
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
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
|
|
850
583
|
|
|
851
|
-
|
|
852
|
-
|
|
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")
|
|
853
592
|
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
return self._connected
|
|
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)
|
|
858
596
|
|
|
859
|
-
def
|
|
860
|
-
r"""
|
|
597
|
+
def list_available_tools(self) -> Dict[str, List[str]]:
|
|
598
|
+
r"""List all available tools organized by client.
|
|
861
599
|
|
|
862
600
|
Returns:
|
|
863
|
-
List[
|
|
601
|
+
Dict[str, List[str]]: Dictionary mapping client indices to tool
|
|
602
|
+
names.
|
|
864
603
|
"""
|
|
865
|
-
|
|
866
|
-
for
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
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}"] = []
|
|
873
613
|
|
|
874
|
-
|
|
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)
|
|
614
|
+
return available_tools
|