camel-ai 0.2.61__py3-none-any.whl → 0.2.64__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of camel-ai might be problematic. Click here for more details.
- camel/__init__.py +1 -1
- camel/agents/chat_agent.py +75 -16
- camel/agents/mcp_agent.py +10 -6
- camel/configs/__init__.py +3 -0
- camel/configs/crynux_config.py +94 -0
- camel/{data_collector → data_collectors}/alpaca_collector.py +1 -1
- camel/{data_collector → data_collectors}/sharegpt_collector.py +1 -1
- camel/interpreters/base.py +14 -1
- camel/interpreters/docker/Dockerfile +63 -7
- camel/interpreters/docker_interpreter.py +65 -7
- camel/interpreters/e2b_interpreter.py +23 -8
- camel/interpreters/internal_python_interpreter.py +30 -2
- camel/interpreters/ipython_interpreter.py +21 -3
- camel/interpreters/subprocess_interpreter.py +34 -2
- camel/memories/records.py +5 -3
- camel/models/__init__.py +2 -0
- camel/models/azure_openai_model.py +101 -25
- camel/models/cohere_model.py +65 -0
- camel/models/crynux_model.py +94 -0
- camel/models/deepseek_model.py +43 -1
- camel/models/gemini_model.py +50 -4
- camel/models/litellm_model.py +38 -0
- camel/models/mistral_model.py +66 -0
- camel/models/model_factory.py +10 -1
- camel/models/openai_compatible_model.py +81 -17
- camel/models/openai_model.py +86 -16
- camel/models/reka_model.py +69 -0
- camel/models/samba_model.py +69 -2
- camel/models/sglang_model.py +74 -2
- camel/models/watsonx_model.py +62 -0
- 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/role_playing_worker.py +2 -2
- camel/societies/workforce/single_agent_worker.py +25 -1
- camel/societies/workforce/worker.py +5 -3
- camel/societies/workforce/workforce.py +409 -7
- camel/storages/__init__.py +2 -0
- camel/storages/vectordb_storages/__init__.py +2 -0
- camel/storages/vectordb_storages/weaviate.py +714 -0
- camel/tasks/task.py +19 -10
- camel/toolkits/__init__.py +2 -0
- camel/toolkits/code_execution.py +37 -8
- camel/toolkits/file_write_toolkit.py +4 -2
- camel/toolkits/mcp_toolkit.py +480 -733
- camel/toolkits/pptx_toolkit.py +777 -0
- camel/types/enums.py +56 -1
- camel/types/unified_model_type.py +5 -0
- camel/utils/__init__.py +16 -0
- camel/utils/langfuse.py +258 -0
- camel/utils/mcp_client.py +1046 -0
- {camel_ai-0.2.61.dist-info → camel_ai-0.2.64.dist-info}/METADATA +9 -1
- {camel_ai-0.2.61.dist-info → camel_ai-0.2.64.dist-info}/RECORD +68 -62
- /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.64.dist-info}/WHEEL +0 -0
- {camel_ai-0.2.61.dist-info → camel_ai-0.2.64.dist-info}/licenses/LICENSE +0 -0
camel/toolkits/mcp_toolkit.py
CHANGED
|
@@ -11,868 +11,615 @@
|
|
|
11
11
|
# See the License for the specific language governing permissions and
|
|
12
12
|
# limitations under the License.
|
|
13
13
|
# ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
|
|
14
|
-
|
|
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
|
-
strict (Optional[bool]):
|
|
105
|
-
|
|
101
|
+
strict (Optional[bool], optional): Flag to indicate strict mode.
|
|
102
|
+
(default: :obj:`False`)
|
|
103
|
+
|
|
104
|
+
Note:
|
|
105
|
+
At least one of :obj:`clients`, :obj:`config_path`, or
|
|
106
|
+
:obj:`config_dict` must be provided. If multiple sources are provided,
|
|
107
|
+
clients from all sources will be combined.
|
|
108
|
+
|
|
109
|
+
For web servers in the config, you can specify authorization headers
|
|
110
|
+
using the "headers" field to connect to protected MCP server endpoints.
|
|
111
|
+
|
|
112
|
+
Example configuration:
|
|
113
|
+
|
|
114
|
+
.. code-block:: json
|
|
115
|
+
|
|
116
|
+
{
|
|
117
|
+
"mcpServers": {
|
|
118
|
+
"filesystem": {
|
|
119
|
+
"command": "npx",
|
|
120
|
+
"args": ["-y", "@modelcontextprotocol/server-filesystem",
|
|
121
|
+
"/path"]
|
|
122
|
+
},
|
|
123
|
+
"protected-server": {
|
|
124
|
+
"url": "https://example.com/mcp",
|
|
125
|
+
"timeout": 30,
|
|
126
|
+
"headers": {
|
|
127
|
+
"Authorization": "Bearer YOUR_TOKEN",
|
|
128
|
+
"X-API-Key": "YOUR_API_KEY"
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
Attributes:
|
|
135
|
+
clients (List[MCPClient]): List of :obj:`MCPClient` instances being
|
|
136
|
+
managed by this toolkit.
|
|
137
|
+
|
|
138
|
+
Raises:
|
|
139
|
+
ValueError: If no configuration sources are provided or if the
|
|
140
|
+
configuration is invalid.
|
|
141
|
+
MCPConnectionError: If connection to any MCP server fails during
|
|
142
|
+
initialization.
|
|
106
143
|
"""
|
|
107
144
|
|
|
108
145
|
def __init__(
|
|
109
146
|
self,
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
147
|
+
clients: Optional[List[MCPClient]] = None,
|
|
148
|
+
config_path: Optional[str] = None,
|
|
149
|
+
config_dict: Optional[Dict[str, Any]] = None,
|
|
113
150
|
timeout: Optional[float] = None,
|
|
114
|
-
headers: Optional[Dict[str, str]] = None,
|
|
115
|
-
mode: Optional[str] = None,
|
|
116
151
|
strict: Optional[bool] = False,
|
|
117
152
|
):
|
|
118
|
-
|
|
119
|
-
|
|
153
|
+
# Call parent constructor first
|
|
120
154
|
super().__init__(timeout=timeout)
|
|
121
155
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
156
|
+
# Validate input parameters
|
|
157
|
+
sources_provided = sum(
|
|
158
|
+
1 for src in [clients, config_path, config_dict] if src is not None
|
|
159
|
+
)
|
|
160
|
+
if sources_provided == 0:
|
|
161
|
+
error_msg = (
|
|
162
|
+
"At least one of clients, config_path, or "
|
|
163
|
+
"config_dict must be provided"
|
|
164
|
+
)
|
|
165
|
+
raise ValueError(error_msg)
|
|
128
166
|
|
|
129
|
-
self.
|
|
130
|
-
self.
|
|
131
|
-
self._exit_stack = AsyncExitStack()
|
|
167
|
+
self.clients: List[MCPClient] = clients or []
|
|
168
|
+
self.strict = strict # Store strict parameter
|
|
132
169
|
self._is_connected = False
|
|
170
|
+
self._exit_stack: Optional[AsyncExitStack] = None
|
|
133
171
|
|
|
134
|
-
|
|
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
|
|
172
|
+
# Load clients from config sources
|
|
173
|
+
if config_path:
|
|
174
|
+
self.clients.extend(self._load_clients_from_config(config_path))
|
|
144
175
|
|
|
145
|
-
if
|
|
146
|
-
|
|
147
|
-
return self
|
|
176
|
+
if config_dict:
|
|
177
|
+
self.clients.extend(self._load_clients_from_dict(config_dict))
|
|
148
178
|
|
|
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
|
-
)
|
|
179
|
+
if not self.clients:
|
|
180
|
+
raise ValueError("No valid MCP clients could be created")
|
|
205
181
|
|
|
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
|
|
182
|
+
async def connect(self) -> "MCPToolkit":
|
|
183
|
+
r"""Connect to all MCP servers using AsyncExitStack.
|
|
223
184
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
185
|
+
Establishes connections to all configured MCP servers sequentially.
|
|
186
|
+
Uses :obj:`AsyncExitStack` to manage the lifecycle of all connections,
|
|
187
|
+
ensuring proper cleanup on exit or error.
|
|
227
188
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
if not self._is_connected:
|
|
232
|
-
return
|
|
233
|
-
self._is_connected = False
|
|
189
|
+
Returns:
|
|
190
|
+
MCPToolkit: Returns :obj:`self` for method chaining, allowing for
|
|
191
|
+
fluent interface usage.
|
|
234
192
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
finally:
|
|
240
|
-
self._exit_stack = AsyncExitStack()
|
|
241
|
-
self._session = None
|
|
193
|
+
Raises:
|
|
194
|
+
MCPConnectionError: If connection to any MCP server fails. The
|
|
195
|
+
error message will include details about which client failed
|
|
196
|
+
to connect and the underlying error reason.
|
|
242
197
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
198
|
+
Warning:
|
|
199
|
+
If any client fails to connect, all previously established
|
|
200
|
+
connections will be automatically cleaned up before raising
|
|
201
|
+
the exception.
|
|
246
202
|
|
|
247
|
-
|
|
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`.
|
|
203
|
+
Example:
|
|
204
|
+
.. code-block:: python
|
|
252
205
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
206
|
+
toolkit = MCPToolkit(config_dict=config)
|
|
207
|
+
try:
|
|
208
|
+
await toolkit.connect()
|
|
209
|
+
# Use the toolkit
|
|
210
|
+
tools = toolkit.get_tools()
|
|
211
|
+
finally:
|
|
212
|
+
await toolkit.disconnect()
|
|
256
213
|
"""
|
|
257
|
-
|
|
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)()
|
|
214
|
+
if self._is_connected:
|
|
215
|
+
logger.warning("MCPToolkit is already connected")
|
|
216
|
+
return self
|
|
269
217
|
|
|
270
|
-
|
|
271
|
-
r"""Retrieves the list of available tools from the connected MCP
|
|
272
|
-
server.
|
|
218
|
+
self._exit_stack = AsyncExitStack()
|
|
273
219
|
|
|
274
|
-
Returns:
|
|
275
|
-
ListToolsResult: Result containing available MCP tools.
|
|
276
|
-
"""
|
|
277
|
-
if not self._session:
|
|
278
|
-
return "MCP Client is not connected. Call `connection()` first."
|
|
279
220
|
try:
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
221
|
+
# Connect to all clients using AsyncExitStack
|
|
222
|
+
for i, client in enumerate(self.clients):
|
|
223
|
+
try:
|
|
224
|
+
# Use MCPClient directly as async context manager
|
|
225
|
+
await self._exit_stack.enter_async_context(client)
|
|
226
|
+
msg = f"Connected to client {i+1}/{len(self.clients)}"
|
|
227
|
+
logger.debug(msg)
|
|
228
|
+
except Exception as e:
|
|
229
|
+
logger.error(f"Failed to connect to client {i+1}: {e}")
|
|
230
|
+
# AsyncExitStack will handle cleanup of already connected
|
|
231
|
+
await self._exit_stack.aclose()
|
|
232
|
+
self._exit_stack = None
|
|
233
|
+
error_msg = f"Failed to connect to client {i+1}: {e}"
|
|
234
|
+
raise MCPConnectionError(error_msg) from e
|
|
293
235
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
236
|
+
self._is_connected = True
|
|
237
|
+
msg = f"Successfully connected to {len(self.clients)} MCP servers"
|
|
238
|
+
logger.info(msg)
|
|
239
|
+
return self
|
|
297
240
|
|
|
298
|
-
|
|
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."
|
|
241
|
+
except Exception:
|
|
242
|
+
self._is_connected = False
|
|
243
|
+
if self._exit_stack:
|
|
244
|
+
await self._exit_stack.aclose()
|
|
245
|
+
self._exit_stack = None
|
|
246
|
+
raise
|
|
348
247
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
raise RuntimeError(
|
|
354
|
-
"MCP Client is not connected. Call `connection()` first."
|
|
355
|
-
)
|
|
248
|
+
async def disconnect(self):
|
|
249
|
+
r"""Disconnect from all MCP servers."""
|
|
250
|
+
if not self._is_connected:
|
|
251
|
+
return
|
|
356
252
|
|
|
253
|
+
if self._exit_stack:
|
|
357
254
|
try:
|
|
358
|
-
|
|
359
|
-
func_name, kwargs
|
|
360
|
-
)
|
|
255
|
+
await self._exit_stack.aclose()
|
|
361
256
|
except Exception as e:
|
|
362
|
-
logger.
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
if not result.content or len(result.content) == 0:
|
|
366
|
-
return "No data available for this request."
|
|
257
|
+
logger.warning(f"Error during disconnect: {e}")
|
|
258
|
+
finally:
|
|
259
|
+
self._exit_stack = None
|
|
367
260
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
content = result.content[0]
|
|
371
|
-
if content.type == "text":
|
|
372
|
-
return content.text
|
|
373
|
-
elif content.type == "image":
|
|
374
|
-
# Return image URL or data URI if available
|
|
375
|
-
if hasattr(content, "url") and content.url:
|
|
376
|
-
return f"Image available at: {content.url}"
|
|
377
|
-
return "Image content received (data URI not shown)"
|
|
378
|
-
elif content.type == "embedded_resource":
|
|
379
|
-
# Return resource information if available
|
|
380
|
-
if hasattr(content, "name") and content.name:
|
|
381
|
-
return f"Embedded resource: {content.name}"
|
|
382
|
-
return "Embedded resource received"
|
|
383
|
-
else:
|
|
384
|
-
msg = f"Received content of type '{content.type}'"
|
|
385
|
-
return f"{msg} which is not fully supported yet."
|
|
386
|
-
except (IndexError, AttributeError) as e:
|
|
387
|
-
logger.error(
|
|
388
|
-
f"Error processing content from MCP tool response: {e!s}"
|
|
389
|
-
)
|
|
390
|
-
raise e
|
|
391
|
-
|
|
392
|
-
dynamic_function.__name__ = func_name
|
|
393
|
-
dynamic_function.__doc__ = func_desc
|
|
394
|
-
dynamic_function.__annotations__ = annotations
|
|
395
|
-
|
|
396
|
-
sig = inspect.Signature(
|
|
397
|
-
parameters=[
|
|
398
|
-
inspect.Parameter(
|
|
399
|
-
name=param,
|
|
400
|
-
kind=inspect.Parameter.KEYWORD_ONLY,
|
|
401
|
-
default=defaults.get(param, inspect.Parameter.empty),
|
|
402
|
-
annotation=annotations[param],
|
|
403
|
-
)
|
|
404
|
-
for param in func_params
|
|
405
|
-
]
|
|
406
|
-
)
|
|
407
|
-
dynamic_function.__signature__ = sig # type: ignore[attr-defined]
|
|
408
|
-
|
|
409
|
-
return dynamic_function
|
|
410
|
-
|
|
411
|
-
def generate_function_from_mcp_tool_sync(self, mcp_tool: "Tool") -> Any:
|
|
412
|
-
r"""Synchronously generate a function from an MCP tool."""
|
|
413
|
-
return run_async(self.generate_function_from_mcp_tool)(mcp_tool)
|
|
414
|
-
|
|
415
|
-
def _build_tool_schema(self, mcp_tool: "Tool") -> Dict[str, Any]:
|
|
416
|
-
input_schema = mcp_tool.inputSchema
|
|
417
|
-
properties = input_schema.get("properties", {})
|
|
418
|
-
required = input_schema.get("required", [])
|
|
419
|
-
|
|
420
|
-
parameters = {
|
|
421
|
-
"type": "object",
|
|
422
|
-
"properties": properties,
|
|
423
|
-
"required": required,
|
|
424
|
-
"additionalProperties": False,
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
return {
|
|
428
|
-
"type": "function",
|
|
429
|
-
"function": {
|
|
430
|
-
"name": mcp_tool.name,
|
|
431
|
-
"description": mcp_tool.description
|
|
432
|
-
or "No description provided.",
|
|
433
|
-
"strict": self.strict,
|
|
434
|
-
"parameters": parameters,
|
|
435
|
-
},
|
|
436
|
-
}
|
|
261
|
+
self._is_connected = False
|
|
262
|
+
logger.debug("Disconnected from all MCP servers")
|
|
437
263
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
based on the MCP tool definitions received from the server.
|
|
264
|
+
@property
|
|
265
|
+
def is_connected(self) -> bool:
|
|
266
|
+
r"""Check if toolkit is connected.
|
|
442
267
|
|
|
443
268
|
Returns:
|
|
444
|
-
|
|
445
|
-
|
|
269
|
+
bool: True if the toolkit is connected to all MCP servers,
|
|
270
|
+
False otherwise.
|
|
446
271
|
"""
|
|
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
|
-
]
|
|
272
|
+
if not self._is_connected:
|
|
273
|
+
return False
|
|
454
274
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
in the toolkit.
|
|
275
|
+
# Check if all clients are connected
|
|
276
|
+
return all(client.is_connected() for client in self.clients)
|
|
458
277
|
|
|
459
|
-
|
|
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
|
-
)
|
|
278
|
+
def connect_sync(self):
|
|
279
|
+
r"""Synchronously connect to all MCP servers."""
|
|
280
|
+
return run_async(self.connect)()
|
|
469
281
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
r"""Calls the specified tool with the provided arguments.
|
|
282
|
+
def disconnect_sync(self):
|
|
283
|
+
r"""Synchronously disconnect from all MCP servers."""
|
|
284
|
+
return run_async(self.disconnect)()
|
|
474
285
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
tool_args (Dict[str, Any]): Arguments to pass to the tool
|
|
478
|
-
(default: :obj:`{}`).
|
|
286
|
+
async def __aenter__(self) -> "MCPToolkit":
|
|
287
|
+
r"""Async context manager entry point.
|
|
479
288
|
|
|
480
|
-
|
|
481
|
-
|
|
289
|
+
Usage:
|
|
290
|
+
async with MCPToolkit(config_dict=config) as toolkit:
|
|
291
|
+
tools = toolkit.get_tools()
|
|
482
292
|
"""
|
|
483
|
-
|
|
484
|
-
|
|
293
|
+
await self.connect()
|
|
294
|
+
return self
|
|
485
295
|
|
|
486
|
-
|
|
296
|
+
def __enter__(self) -> "MCPToolkit":
|
|
297
|
+
r"""Synchronously enter the async context manager."""
|
|
298
|
+
return run_async(self.__aenter__)()
|
|
487
299
|
|
|
488
|
-
def
|
|
489
|
-
r"""
|
|
490
|
-
|
|
300
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
301
|
+
r"""Async context manager exit point."""
|
|
302
|
+
await self.disconnect()
|
|
303
|
+
return None
|
|
491
304
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
return self.
|
|
305
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
306
|
+
r"""Synchronously exit the async context manager."""
|
|
307
|
+
return run_async(self.__aexit__)(exc_type, exc_val, exc_tb)
|
|
495
308
|
|
|
496
309
|
@classmethod
|
|
497
310
|
async def create(
|
|
498
311
|
cls,
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
312
|
+
clients: Optional[List[MCPClient]] = None,
|
|
313
|
+
config_path: Optional[str] = None,
|
|
314
|
+
config_dict: Optional[Dict[str, Any]] = None,
|
|
502
315
|
timeout: Optional[float] = None,
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
r"""Factory method that creates and connects to the MCP server.
|
|
316
|
+
strict: Optional[bool] = False,
|
|
317
|
+
) -> "MCPToolkit":
|
|
318
|
+
r"""Factory method that creates and connects to all MCP servers.
|
|
507
319
|
|
|
508
|
-
|
|
509
|
-
|
|
320
|
+
Creates a new :obj:`MCPToolkit` instance and automatically establishes
|
|
321
|
+
connections to all configured MCP servers. This is a convenience method
|
|
322
|
+
that combines instantiation and connection in a single call.
|
|
510
323
|
|
|
511
324
|
Args:
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
the
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
Server-Sent Events, "streamable-http" for
|
|
524
|
-
streaming HTTP, or None for stdio mode.
|
|
525
|
-
(default: :obj:`None`)
|
|
325
|
+
clients (Optional[List[MCPClient]], optional): List of
|
|
326
|
+
:obj:`MCPClient` instances to manage. (default: :obj:`None`)
|
|
327
|
+
config_path (Optional[str], optional): Path to a JSON configuration
|
|
328
|
+
file defining MCP servers. (default: :obj:`None`)
|
|
329
|
+
config_dict (Optional[Dict[str, Any]], optional): Dictionary
|
|
330
|
+
containing MCP server configurations in the same format as the
|
|
331
|
+
config file. (default: :obj:`None`)
|
|
332
|
+
timeout (Optional[float], optional): Timeout for connection
|
|
333
|
+
attempts in seconds. (default: :obj:`None`)
|
|
334
|
+
strict (Optional[bool], optional): Flag to indicate strict mode.
|
|
335
|
+
(default: :obj:`False`)
|
|
526
336
|
|
|
527
337
|
Returns:
|
|
528
|
-
|
|
338
|
+
MCPToolkit: A fully initialized and connected :obj:`MCPToolkit`
|
|
339
|
+
instance with all servers ready for use.
|
|
529
340
|
|
|
530
341
|
Raises:
|
|
531
|
-
|
|
342
|
+
MCPConnectionError: If connection to any MCP server fails during
|
|
343
|
+
initialization. All successfully connected servers will be
|
|
344
|
+
properly disconnected before raising the exception.
|
|
345
|
+
ValueError: If no configuration sources are provided or if the
|
|
346
|
+
configuration is invalid.
|
|
347
|
+
|
|
348
|
+
Example:
|
|
349
|
+
.. code-block:: python
|
|
350
|
+
|
|
351
|
+
# Create and connect in one step
|
|
352
|
+
toolkit = await MCPToolkit.create(config_path="servers.json")
|
|
353
|
+
try:
|
|
354
|
+
tools = toolkit.get_tools()
|
|
355
|
+
# Use the toolkit...
|
|
356
|
+
finally:
|
|
357
|
+
await toolkit.disconnect()
|
|
532
358
|
"""
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
359
|
+
toolkit = cls(
|
|
360
|
+
clients=clients,
|
|
361
|
+
config_path=config_path,
|
|
362
|
+
config_dict=config_dict,
|
|
537
363
|
timeout=timeout,
|
|
538
|
-
|
|
539
|
-
mode=mode,
|
|
364
|
+
strict=strict,
|
|
540
365
|
)
|
|
541
366
|
try:
|
|
542
|
-
await
|
|
543
|
-
return
|
|
367
|
+
await toolkit.connect()
|
|
368
|
+
return toolkit
|
|
544
369
|
except Exception as e:
|
|
545
370
|
# Ensure cleanup on initialization failure
|
|
546
|
-
await
|
|
547
|
-
logger.error(f"Failed to initialize
|
|
548
|
-
raise
|
|
371
|
+
await toolkit.disconnect()
|
|
372
|
+
logger.error(f"Failed to initialize MCPToolkit: {e}")
|
|
373
|
+
raise MCPConnectionError(
|
|
374
|
+
f"Failed to initialize MCPToolkit: {e}"
|
|
375
|
+
) from e
|
|
549
376
|
|
|
550
377
|
@classmethod
|
|
551
378
|
def create_sync(
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
379
|
+
cls,
|
|
380
|
+
clients: Optional[List[MCPClient]] = None,
|
|
381
|
+
config_path: Optional[str] = None,
|
|
382
|
+
config_dict: Optional[Dict[str, Any]] = None,
|
|
556
383
|
timeout: Optional[float] = None,
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
command_or_url, args, env, timeout, headers, mode
|
|
384
|
+
strict: Optional[bool] = False,
|
|
385
|
+
) -> "MCPToolkit":
|
|
386
|
+
r"""Synchronously create and connect to all MCP servers."""
|
|
387
|
+
return run_async(cls.create)(
|
|
388
|
+
clients, config_path, config_dict, timeout, strict
|
|
563
389
|
)
|
|
564
390
|
|
|
565
|
-
|
|
566
|
-
r"""
|
|
567
|
-
|
|
391
|
+
def _load_clients_from_config(self, config_path: str) -> List[MCPClient]:
|
|
392
|
+
r"""Load clients from configuration file."""
|
|
393
|
+
if not os.path.exists(config_path):
|
|
394
|
+
raise FileNotFoundError(f"Config file not found: '{config_path}'")
|
|
568
395
|
|
|
569
|
-
|
|
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
|
-
"""
|
|
396
|
+
try:
|
|
397
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
|
398
|
+
data = json.load(f)
|
|
399
|
+
except json.JSONDecodeError as e:
|
|
400
|
+
error_msg = f"Invalid JSON in config file '{config_path}': {e}"
|
|
401
|
+
raise ValueError(error_msg) from e
|
|
402
|
+
except Exception as e:
|
|
403
|
+
error_msg = f"Error reading config file '{config_path}': {e}"
|
|
404
|
+
raise IOError(error_msg) from e
|
|
682
405
|
|
|
683
|
-
|
|
684
|
-
self,
|
|
685
|
-
servers: Optional[List[MCPClient]] = None,
|
|
686
|
-
config_path: Optional[str] = None,
|
|
687
|
-
config_dict: Optional[Dict[str, Any]] = None,
|
|
688
|
-
strict: Optional[bool] = False,
|
|
689
|
-
):
|
|
690
|
-
super().__init__()
|
|
406
|
+
return self._load_clients_from_dict(data)
|
|
691
407
|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
f"({sources_provided}). Servers from all sources "
|
|
699
|
-
"will be combined."
|
|
700
|
-
)
|
|
408
|
+
def _load_clients_from_dict(
|
|
409
|
+
self, config: Dict[str, Any]
|
|
410
|
+
) -> List[MCPClient]:
|
|
411
|
+
r"""Load clients from configuration dictionary."""
|
|
412
|
+
if not isinstance(config, dict):
|
|
413
|
+
raise ValueError("Config must be a dictionary")
|
|
701
414
|
|
|
702
|
-
|
|
415
|
+
mcp_servers = config.get("mcpServers", {})
|
|
416
|
+
if not isinstance(mcp_servers, dict):
|
|
417
|
+
raise ValueError("'mcpServers' must be a dictionary")
|
|
703
418
|
|
|
704
|
-
|
|
705
|
-
self.servers.extend(
|
|
706
|
-
self._load_servers_from_config(config_path, strict)
|
|
707
|
-
)
|
|
419
|
+
clients = []
|
|
708
420
|
|
|
709
|
-
|
|
710
|
-
|
|
421
|
+
for name, cfg in mcp_servers.items():
|
|
422
|
+
try:
|
|
423
|
+
if "timeout" not in cfg and self.timeout is not None:
|
|
424
|
+
cfg["timeout"] = self.timeout
|
|
711
425
|
|
|
712
|
-
|
|
426
|
+
client = self._create_client_from_config(name, cfg)
|
|
427
|
+
clients.append(client)
|
|
428
|
+
except Exception as e:
|
|
429
|
+
logger.error(f"Failed to create client for '{name}': {e}")
|
|
430
|
+
error_msg = f"Invalid configuration for server '{name}': {e}"
|
|
431
|
+
raise ValueError(error_msg) from e
|
|
713
432
|
|
|
714
|
-
|
|
715
|
-
self, config_path: str, strict: Optional[bool] = False
|
|
716
|
-
) -> List[MCPClient]:
|
|
717
|
-
r"""Loads MCP server configurations from a JSON file.
|
|
433
|
+
return clients
|
|
718
434
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
435
|
+
def _create_client_from_config(
|
|
436
|
+
self, name: str, cfg: Dict[str, Any]
|
|
437
|
+
) -> MCPClient:
|
|
438
|
+
r"""Create a single MCP client from configuration."""
|
|
439
|
+
if not isinstance(cfg, dict):
|
|
440
|
+
error_msg = f"Configuration for server '{name}' must be a dict"
|
|
441
|
+
raise ValueError(error_msg)
|
|
723
442
|
|
|
724
|
-
Returns:
|
|
725
|
-
List[MCPClient]: List of configured MCPClient instances.
|
|
726
|
-
"""
|
|
727
443
|
try:
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
444
|
+
# Use the new mcp_client factory function
|
|
445
|
+
# Pass timeout and strict from toolkit if available
|
|
446
|
+
kwargs = {}
|
|
447
|
+
if hasattr(self, "timeout") and self.timeout is not None:
|
|
448
|
+
kwargs["timeout"] = self.timeout
|
|
449
|
+
if hasattr(self, "strict") and self.strict is not None:
|
|
450
|
+
kwargs["strict"] = self.strict
|
|
451
|
+
|
|
452
|
+
client = create_mcp_client(cfg, **kwargs)
|
|
453
|
+
return client
|
|
454
|
+
except Exception as e:
|
|
455
|
+
error_msg = f"Failed to create client for server '{name}': {e}"
|
|
456
|
+
raise ValueError(error_msg) from e
|
|
741
457
|
|
|
742
|
-
def
|
|
743
|
-
|
|
744
|
-
) -> List[MCPClient]:
|
|
745
|
-
r"""Loads MCP server configurations from a dictionary.
|
|
458
|
+
def get_tools(self) -> List[FunctionTool]:
|
|
459
|
+
r"""Aggregates all tools from the managed MCP client instances.
|
|
746
460
|
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
strict (bool): Whether to enforce strict mode for the
|
|
751
|
-
function call. (default: :obj:`False`)
|
|
461
|
+
Collects and combines tools from all connected MCP clients into a
|
|
462
|
+
single unified list. Each tool is converted to a CAMEL-compatible
|
|
463
|
+
:obj:`FunctionTool` that can be used with CAMEL agents.
|
|
752
464
|
|
|
753
465
|
Returns:
|
|
754
|
-
List[
|
|
466
|
+
List[FunctionTool]: Combined list of all available function tools
|
|
467
|
+
from all connected MCP servers. Returns an empty list if no
|
|
468
|
+
clients are connected or if no tools are available.
|
|
469
|
+
|
|
470
|
+
Note:
|
|
471
|
+
This method can be called even when the toolkit is not connected,
|
|
472
|
+
but it will log a warning and may return incomplete results.
|
|
473
|
+
For best results, ensure the toolkit is connected before calling
|
|
474
|
+
this method.
|
|
475
|
+
|
|
476
|
+
Example:
|
|
477
|
+
.. code-block:: python
|
|
478
|
+
|
|
479
|
+
async with MCPToolkit(config_dict=config) as toolkit:
|
|
480
|
+
tools = toolkit.get_tools()
|
|
481
|
+
print(f"Available tools: {len(tools)}")
|
|
482
|
+
for tool in tools:
|
|
483
|
+
print(f" - {tool.func.__name__}")
|
|
755
484
|
"""
|
|
756
|
-
|
|
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
|
|
485
|
+
if not self.is_connected:
|
|
486
|
+
logger.warning(
|
|
487
|
+
"MCPToolkit is not connected. "
|
|
488
|
+
"Tools may not be available until connected."
|
|
489
|
+
)
|
|
769
490
|
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
491
|
+
all_tools = []
|
|
492
|
+
for i, client in enumerate(self.clients):
|
|
493
|
+
try:
|
|
494
|
+
client_tools = client.get_tools()
|
|
495
|
+
all_tools.extend(client_tools)
|
|
496
|
+
logger.debug(
|
|
497
|
+
f"Client {i+1} contributed {len(client_tools)} tools"
|
|
774
498
|
)
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
# Include headers if provided in the configuration
|
|
778
|
-
headers = cfg.get("headers", {})
|
|
779
|
-
|
|
780
|
-
cmd_or_url = cast(str, cfg.get("command") or cfg.get("url"))
|
|
781
|
-
server = MCPClient(
|
|
782
|
-
command_or_url=cmd_or_url,
|
|
783
|
-
args=cfg.get("args", []),
|
|
784
|
-
env={**os.environ, **cfg.get("env", {})},
|
|
785
|
-
timeout=cfg.get("timeout", None),
|
|
786
|
-
headers=headers,
|
|
787
|
-
mode=cfg.get("mode", None),
|
|
788
|
-
strict=strict,
|
|
789
|
-
)
|
|
790
|
-
all_servers.append(server)
|
|
499
|
+
except Exception as e:
|
|
500
|
+
logger.error(f"Failed to get tools from client {i+1}: {e}")
|
|
791
501
|
|
|
792
|
-
|
|
502
|
+
logger.info(f"Total tools available: {len(all_tools)}")
|
|
503
|
+
return all_tools
|
|
793
504
|
|
|
794
|
-
|
|
795
|
-
r"""
|
|
505
|
+
def get_text_tools(self) -> str:
|
|
506
|
+
r"""Returns a string containing the descriptions of the tools.
|
|
796
507
|
|
|
797
508
|
Returns:
|
|
798
|
-
|
|
509
|
+
str: A string containing the descriptions of all tools.
|
|
799
510
|
"""
|
|
800
|
-
if self.
|
|
801
|
-
logger.warning(
|
|
802
|
-
|
|
511
|
+
if not self.is_connected:
|
|
512
|
+
logger.warning(
|
|
513
|
+
"MCPToolkit is not connected. "
|
|
514
|
+
"Tool descriptions may not be available until connected."
|
|
515
|
+
)
|
|
803
516
|
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
517
|
+
tool_descriptions = []
|
|
518
|
+
for i, client in enumerate(self.clients):
|
|
519
|
+
try:
|
|
520
|
+
client_tools_text = client.get_text_tools()
|
|
521
|
+
if client_tools_text:
|
|
522
|
+
tool_descriptions.append(
|
|
523
|
+
f"=== Client {i+1} Tools ===\n{client_tools_text}"
|
|
524
|
+
)
|
|
525
|
+
except Exception as e:
|
|
526
|
+
logger.error(
|
|
527
|
+
f"Failed to get tool descriptions from client {i+1}: {e}"
|
|
528
|
+
)
|
|
815
529
|
|
|
816
|
-
|
|
817
|
-
r"""Synchronously connect to all MCP servers."""
|
|
818
|
-
return run_async(self.connect)()
|
|
530
|
+
return "\n\n".join(tool_descriptions)
|
|
819
531
|
|
|
820
|
-
async def
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
532
|
+
async def call_tool(
|
|
533
|
+
self, tool_name: str, tool_args: Dict[str, Any]
|
|
534
|
+
) -> Any:
|
|
535
|
+
r"""Call a tool by name across all managed clients.
|
|
824
536
|
|
|
825
|
-
for
|
|
826
|
-
|
|
827
|
-
|
|
537
|
+
Searches for and executes a tool with the specified name across all
|
|
538
|
+
connected MCP clients. The method will try each client in sequence
|
|
539
|
+
until the tool is found and successfully executed.
|
|
828
540
|
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
541
|
+
Args:
|
|
542
|
+
tool_name (str): Name of the tool to call. Must match a tool name
|
|
543
|
+
available from one of the connected MCP servers.
|
|
544
|
+
tool_args (Dict[str, Any]): Arguments to pass to the tool. The
|
|
545
|
+
argument names and types must match the tool's expected
|
|
546
|
+
parameters.
|
|
832
547
|
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
to all managed MCP server instances.
|
|
548
|
+
Returns:
|
|
549
|
+
Any: The result of the tool call. The type and structure depend
|
|
550
|
+
on the specific tool being called.
|
|
837
551
|
|
|
838
|
-
|
|
839
|
-
|
|
552
|
+
Raises:
|
|
553
|
+
MCPConnectionError: If the toolkit is not connected to any MCP
|
|
554
|
+
servers.
|
|
555
|
+
MCPToolError: If the tool is not found in any client, or if all
|
|
556
|
+
attempts to call the tool fail. The error message will include
|
|
557
|
+
details about the last failure encountered.
|
|
558
|
+
|
|
559
|
+
Example:
|
|
560
|
+
.. code-block:: python
|
|
561
|
+
|
|
562
|
+
async with MCPToolkit(config_dict=config) as toolkit:
|
|
563
|
+
# Call a file reading tool
|
|
564
|
+
result = await toolkit.call_tool(
|
|
565
|
+
"read_file",
|
|
566
|
+
{"path": "/tmp/example.txt"}
|
|
567
|
+
)
|
|
568
|
+
print(f"File contents: {result}")
|
|
840
569
|
"""
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
await self.disconnect()
|
|
570
|
+
if not self.is_connected:
|
|
571
|
+
raise MCPConnectionError(
|
|
572
|
+
"MCPToolkit is not connected. Call connect() first."
|
|
573
|
+
)
|
|
846
574
|
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
575
|
+
# Try to find and call the tool from any client
|
|
576
|
+
last_error = None
|
|
577
|
+
for i, client in enumerate(self.clients):
|
|
578
|
+
try:
|
|
579
|
+
# Check if this client has the tool
|
|
580
|
+
tools = client.get_tools()
|
|
581
|
+
tool_names = [tool.func.__name__ for tool in tools]
|
|
582
|
+
|
|
583
|
+
if tool_name in tool_names:
|
|
584
|
+
result = await client.call_tool(tool_name, tool_args)
|
|
585
|
+
logger.debug(
|
|
586
|
+
f"Tool '{tool_name}' called successfully "
|
|
587
|
+
f"on client {i+1}"
|
|
588
|
+
)
|
|
589
|
+
return result
|
|
590
|
+
except Exception as e:
|
|
591
|
+
last_error = e
|
|
592
|
+
logger.debug(f"Tool '{tool_name}' failed on client {i+1}: {e}")
|
|
593
|
+
continue
|
|
850
594
|
|
|
851
|
-
|
|
852
|
-
|
|
595
|
+
# If we get here, the tool wasn't found or all calls failed
|
|
596
|
+
if last_error:
|
|
597
|
+
raise MCPToolError(
|
|
598
|
+
f"Tool '{tool_name}' failed on all clients. "
|
|
599
|
+
f"Last error: {last_error}"
|
|
600
|
+
) from last_error
|
|
601
|
+
else:
|
|
602
|
+
raise MCPToolError(f"Tool '{tool_name}' not found in any client")
|
|
853
603
|
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
return self._connected
|
|
604
|
+
def call_tool_sync(self, tool_name: str, tool_args: Dict[str, Any]) -> Any:
|
|
605
|
+
r"""Synchronously call a tool."""
|
|
606
|
+
return run_async(self.call_tool)(tool_name, tool_args)
|
|
858
607
|
|
|
859
|
-
def
|
|
860
|
-
r"""
|
|
608
|
+
def list_available_tools(self) -> Dict[str, List[str]]:
|
|
609
|
+
r"""List all available tools organized by client.
|
|
861
610
|
|
|
862
611
|
Returns:
|
|
863
|
-
List[
|
|
612
|
+
Dict[str, List[str]]: Dictionary mapping client indices to tool
|
|
613
|
+
names.
|
|
864
614
|
"""
|
|
865
|
-
|
|
866
|
-
for
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
615
|
+
available_tools = {}
|
|
616
|
+
for i, client in enumerate(self.clients):
|
|
617
|
+
try:
|
|
618
|
+
tools = client.get_tools()
|
|
619
|
+
tool_names = [tool.func.__name__ for tool in tools]
|
|
620
|
+
available_tools[f"client_{i+1}"] = tool_names
|
|
621
|
+
except Exception as e:
|
|
622
|
+
logger.error(f"Failed to list tools from client {i+1}: {e}")
|
|
623
|
+
available_tools[f"client_{i+1}"] = []
|
|
873
624
|
|
|
874
|
-
|
|
875
|
-
str: A string containing the descriptions of the tools
|
|
876
|
-
in the toolkit.
|
|
877
|
-
"""
|
|
878
|
-
return "\n".join(server.get_text_tools() for server in self.servers)
|
|
625
|
+
return available_tools
|