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.

Files changed (32) hide show
  1. camel/__init__.py +1 -1
  2. camel/agents/chat_agent.py +1 -1
  3. camel/agents/mcp_agent.py +5 -5
  4. camel/{data_collector → data_collectors}/alpaca_collector.py +1 -1
  5. camel/{data_collector → data_collectors}/sharegpt_collector.py +1 -1
  6. camel/retrievers/auto_retriever.py +20 -1
  7. camel/{runtime → runtimes}/daytona_runtime.py +1 -1
  8. camel/{runtime → runtimes}/docker_runtime.py +1 -1
  9. camel/{runtime → runtimes}/llm_guard_runtime.py +2 -2
  10. camel/{runtime → runtimes}/remote_http_runtime.py +1 -1
  11. camel/{runtime → runtimes}/ubuntu_docker_runtime.py +1 -1
  12. camel/societies/workforce/base.py +7 -3
  13. camel/societies/workforce/single_agent_worker.py +2 -1
  14. camel/societies/workforce/worker.py +5 -3
  15. camel/toolkits/__init__.py +2 -0
  16. camel/toolkits/file_write_toolkit.py +4 -2
  17. camel/toolkits/mcp_toolkit.py +469 -733
  18. camel/toolkits/pptx_toolkit.py +777 -0
  19. camel/utils/mcp_client.py +979 -0
  20. {camel_ai-0.2.61.dist-info → camel_ai-0.2.62.dist-info}/METADATA +4 -1
  21. {camel_ai-0.2.61.dist-info → camel_ai-0.2.62.dist-info}/RECORD +32 -30
  22. /camel/{data_collector → data_collectors}/__init__.py +0 -0
  23. /camel/{data_collector → data_collectors}/base.py +0 -0
  24. /camel/{runtime → runtimes}/__init__.py +0 -0
  25. /camel/{runtime → runtimes}/api.py +0 -0
  26. /camel/{runtime → runtimes}/base.py +0 -0
  27. /camel/{runtime → runtimes}/configs.py +0 -0
  28. /camel/{runtime → runtimes}/utils/__init__.py +0 -0
  29. /camel/{runtime → runtimes}/utils/function_risk_toolkit.py +0 -0
  30. /camel/{runtime → runtimes}/utils/ignore_risk_toolkit.py +0 -0
  31. {camel_ai-0.2.61.dist-info → camel_ai-0.2.62.dist-info}/WHEEL +0 -0
  32. {camel_ai-0.2.61.dist-info → camel_ai-0.2.62.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,979 @@
1
+ # ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
2
+ # Licensed under the Apache License, Version 2.0 (the "License");
3
+ # you may not use this file except in compliance with the License.
4
+ # You may obtain a copy of the License at
5
+ #
6
+ # http://www.apache.org/licenses/LICENSE-2.0
7
+ #
8
+ # Unless required by applicable law or agreed to in writing, software
9
+ # distributed under the License is distributed on an "AS IS" BASIS,
10
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ # See the License for the specific language governing permissions and
12
+ # limitations under the License.
13
+ # ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
14
+ """
15
+ Unified MCP Client
16
+
17
+ This module provides a unified interface for connecting to MCP servers
18
+ using different transport protocols (stdio, sse, streamable-http, websocket).
19
+ The client can automatically detect the transport type based on configuration.
20
+ """
21
+
22
+ import inspect
23
+ from contextlib import asynccontextmanager
24
+ from datetime import timedelta
25
+ from enum import Enum
26
+ from pathlib import Path
27
+ from typing import (
28
+ Any,
29
+ Callable,
30
+ Dict,
31
+ List,
32
+ Optional,
33
+ Set,
34
+ Union,
35
+ )
36
+
37
+ import httpx
38
+ import mcp.types as types
39
+ from pydantic import BaseModel, model_validator
40
+
41
+ try:
42
+ from mcp.shared._httpx_utils import create_mcp_http_client
43
+ except ImportError:
44
+
45
+ def create_mcp_http_client(
46
+ headers: Optional[Dict[str, str]] = None,
47
+ timeout: Optional[httpx.Timeout] = None,
48
+ auth: Optional[httpx.Auth] = None,
49
+ ) -> httpx.AsyncClient:
50
+ """Fallback implementation if not available."""
51
+ kwargs: Dict[str, Any] = {"follow_redirects": True}
52
+
53
+ if timeout is None:
54
+ kwargs["timeout"] = httpx.Timeout(30.0)
55
+ else:
56
+ kwargs["timeout"] = timeout
57
+
58
+ if headers is not None:
59
+ kwargs["headers"] = headers
60
+
61
+ if auth is not None:
62
+ kwargs["auth"] = auth
63
+
64
+ return httpx.AsyncClient(**kwargs)
65
+
66
+
67
+ from mcp import ClientSession
68
+
69
+
70
+ class TransportType(str, Enum):
71
+ r"""Supported transport types."""
72
+
73
+ STDIO = "stdio"
74
+ SSE = "sse"
75
+ STREAMABLE_HTTP = "streamable_http"
76
+ WEBSOCKET = "websocket"
77
+
78
+
79
+ class ServerConfig(BaseModel):
80
+ r"""Unified server configuration that automatically detects transport type.
81
+
82
+ Examples:
83
+ # STDIO server
84
+ config = ServerConfig(
85
+ command="npx",
86
+ args=["-y", "@modelcontextprotocol/server-filesystem", "/path"]
87
+ )
88
+
89
+ # HTTP/SSE server
90
+ config = ServerConfig(
91
+ url="https://api.example.com/mcp",
92
+ headers={"Authorization": "Bearer token"}
93
+ )
94
+
95
+ # WebSocket server
96
+ config = ServerConfig(url="ws://localhost:8080/mcp")
97
+ """
98
+
99
+ # STDIO configuration
100
+ command: Optional[str] = None
101
+ args: Optional[List[str]] = None
102
+ env: Optional[Dict[str, str]] = None
103
+ cwd: Optional[Union[str, Path]] = None
104
+
105
+ # HTTP/WebSocket configuration
106
+ url: Optional[str] = None
107
+ headers: Optional[Dict[str, Any]] = None
108
+
109
+ # Common configuration
110
+ timeout: float = 30.0
111
+ encoding: str = "utf-8"
112
+
113
+ # Advanced options
114
+ sse_read_timeout: float = 300.0 # 5 minutes
115
+ terminate_on_close: bool = True
116
+ # For HTTP URLs, prefer SSE over StreamableHTTP
117
+ prefer_sse: bool = False
118
+
119
+ @model_validator(mode='after')
120
+ def validate_config(self):
121
+ r"""Validate that either command or url is provided."""
122
+ if not self.command and not self.url:
123
+ raise ValueError(
124
+ "Either 'command' (for stdio) or 'url' "
125
+ "(for http/websocket) must be provided"
126
+ )
127
+
128
+ if self.command and self.url:
129
+ raise ValueError("Cannot specify both 'command' and 'url'")
130
+
131
+ return self
132
+
133
+ @property
134
+ def transport_type(self) -> TransportType:
135
+ r"""Automatically detect transport type based on configuration."""
136
+ if self.command:
137
+ return TransportType.STDIO
138
+ elif self.url:
139
+ if self.url.startswith(("ws://", "wss://")):
140
+ return TransportType.WEBSOCKET
141
+ elif self.url.startswith(("http://", "https://")):
142
+ # Default to StreamableHTTP, unless user prefers SSE
143
+ if self.prefer_sse:
144
+ return TransportType.SSE
145
+ else:
146
+ return TransportType.STREAMABLE_HTTP
147
+ else:
148
+ raise ValueError(f"Unsupported URL scheme: {self.url}")
149
+ else:
150
+ raise ValueError("Cannot determine transport type")
151
+
152
+
153
+ class MCPClient:
154
+ r"""Unified MCP client that automatically detects and connects to servers
155
+ using the appropriate transport protocol.
156
+
157
+ This client provides a unified interface for connecting to Model Context
158
+ Protocol (MCP) servers using different transport protocols including STDIO,
159
+ HTTP/HTTPS, WebSocket, and Server-Sent Events (SSE). The client
160
+ automatically detects the appropriate transport type based on the
161
+ configuration provided.
162
+
163
+ The client should be used as an async context manager for automatic
164
+ connectionmanagement.
165
+
166
+ Args:
167
+ config (Union[ServerConfig, Dict[str, Any]]): Server configuration
168
+ as either a :obj:`ServerConfig` object or a dictionary that will
169
+ be converted to a :obj:`ServerConfig`. The configuration determines
170
+ the transport type and connection parameters.
171
+ client_info (Optional[types.Implementation], optional): Client
172
+ implementation information to send to the server during
173
+ initialization. (default: :obj:`None`)
174
+ timeout (Optional[float], optional): Timeout for waiting for messages
175
+ from the server in seconds. (default: :obj:`10.0`)
176
+ strict (Optional[bool], optional): Strict mode for generating
177
+ FunctionTool objects. (default: :obj:`False`)
178
+
179
+ Examples:
180
+ STDIO server:
181
+
182
+ .. code-block:: python
183
+
184
+ async with MCPClient({
185
+ "command": "npx",
186
+ "args": [
187
+ "-y",
188
+ "@modelcontextprotocol/server-filesystem",
189
+ "/path"
190
+ ]
191
+ }) as client:
192
+ tools = client.get_tools()
193
+ result = await client.call_tool("tool_name", {"arg": "value"})
194
+
195
+ HTTP server:
196
+
197
+ .. code-block:: python
198
+
199
+ async with MCPClient({
200
+ "url": "https://api.example.com/mcp",
201
+ "headers": {"Authorization": "Bearer token"}
202
+ }) as client:
203
+ tools = client.get_tools()
204
+
205
+ WebSocket server:
206
+
207
+ .. code-block:: python
208
+
209
+ async with MCPClient({"url": "ws://localhost:8080/mcp"}) as client:
210
+ tools = client.get_tools()
211
+
212
+ With strict mode enabled:
213
+
214
+ .. code-block:: python
215
+
216
+ async with MCPClient({
217
+ "command": "npx",
218
+ "args": [
219
+ "-y",
220
+ "@modelcontextprotocol/server-filesystem",
221
+ "/path"
222
+ ]
223
+ }, strict=True) as client:
224
+ tools = client.get_tools()
225
+
226
+ Attributes:
227
+ config (ServerConfig): The server configuration object.
228
+ client_info (Optional[types.Implementation]): Client implementation
229
+ information.
230
+ read_timeout_seconds (timedelta): Timeout for reading from the server.
231
+ """
232
+
233
+ def __init__(
234
+ self,
235
+ config: Union[ServerConfig, Dict[str, Any]],
236
+ client_info: Optional[types.Implementation] = None,
237
+ timeout: Optional[float] = 10.0,
238
+ strict: Optional[bool] = False,
239
+ ):
240
+ # Convert dict config to ServerConfig if needed
241
+ if isinstance(config, dict):
242
+ config = ServerConfig(**config)
243
+
244
+ self.config = config
245
+ self.strict = strict
246
+
247
+ # Validate transport type early (this will raise ValueError if invalid)
248
+ _ = self.config.transport_type
249
+
250
+ self.client_info = client_info
251
+ self.read_timeout_seconds = timedelta(seconds=timeout or 10.0)
252
+
253
+ self._session: Optional[ClientSession] = None
254
+ self._tools: List[types.Tool] = []
255
+ self._connection_context = None
256
+
257
+ @property
258
+ def transport_type(self) -> TransportType:
259
+ r"""Get the detected transport type."""
260
+ return self.config.transport_type
261
+
262
+ async def __aenter__(self):
263
+ r"""Async context manager entry point.
264
+
265
+ Establishes connection to the MCP server and initializes the session.
266
+
267
+ Returns:
268
+ MCPClient: The connected client instance.
269
+ """
270
+ await self._establish_connection()
271
+ return self
272
+
273
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
274
+ r"""Async context manager exit point.
275
+
276
+ Cleans up the connection and resources.
277
+ """
278
+ await self._cleanup_connection()
279
+
280
+ async def _establish_connection(self):
281
+ r"""Establish connection to the MCP server."""
282
+ try:
283
+ self._connection_context = self._create_transport()
284
+ streams = await self._connection_context.__aenter__()
285
+
286
+ # Handle extra returns safely
287
+ read_stream, write_stream = streams[:2]
288
+
289
+ self._session = ClientSession(
290
+ read_stream=read_stream,
291
+ write_stream=write_stream,
292
+ client_info=self.client_info,
293
+ read_timeout_seconds=self.read_timeout_seconds,
294
+ )
295
+
296
+ # Start the session's message processing loop
297
+ await self._session.__aenter__()
298
+
299
+ # Initialize the session and load tools
300
+ await self._session.initialize()
301
+ tools_response = await self._session.list_tools()
302
+ self._tools = tools_response.tools if tools_response else []
303
+
304
+ except Exception as e:
305
+ # Clean up on error
306
+ await self._cleanup_connection()
307
+
308
+ # Convert complex exceptions to simpler, more understandable ones
309
+ from camel.logger import get_logger
310
+
311
+ logger = get_logger(__name__)
312
+
313
+ error_msg = self._simplify_connection_error(e)
314
+ logger.error(f"MCP connection failed: {error_msg}")
315
+
316
+ # Raise a simpler exception
317
+ raise ConnectionError(error_msg) from e
318
+
319
+ async def _cleanup_connection(self):
320
+ r"""Clean up connection resources."""
321
+ try:
322
+ if self._session:
323
+ try:
324
+ await self._session.__aexit__(None, None, None)
325
+ except Exception:
326
+ pass # Ignore cleanup errors
327
+ finally:
328
+ self._session = None
329
+
330
+ if self._connection_context:
331
+ try:
332
+ await self._connection_context.__aexit__(None, None, None)
333
+ except Exception:
334
+ pass # Ignore cleanup errors
335
+ finally:
336
+ self._connection_context = None
337
+
338
+ finally:
339
+ # Ensure state is reset
340
+ self._tools = []
341
+
342
+ def _simplify_connection_error(self, error: Exception) -> str:
343
+ r"""Convert complex MCP connection errors to simple, understandable
344
+ messages.
345
+ """
346
+ error_str = str(error).lower()
347
+
348
+ # Handle different types of errors
349
+ if "exceptiongroup" in error_str or "taskgroup" in error_str:
350
+ # Often happens when the command fails to start
351
+ if "processlookuperror" in error_str:
352
+ return (
353
+ f"Failed to start MCP server command "
354
+ f"'{self.config.command}'. The command may have "
355
+ "exited unexpectedly."
356
+ )
357
+ elif "cancelled" in error_str:
358
+ return (
359
+ "Connection to MCP server was cancelled, "
360
+ "likely due to timeout or server startup failure."
361
+ )
362
+ else:
363
+ return (
364
+ f"MCP server process error. The server command "
365
+ f"'{self.config.command}' failed to start properly."
366
+ )
367
+
368
+ elif "timeout" in error_str:
369
+ return (
370
+ f"Connection timeout after {self.config.timeout}s. "
371
+ "The MCP server may be taking too long to respond."
372
+ )
373
+
374
+ elif "not found" in error_str or "404" in error_str:
375
+ command_parts = []
376
+ if self.config.command:
377
+ command_parts.append(self.config.command)
378
+ if self.config.args:
379
+ command_parts.extend(self.config.args)
380
+ command_str = (
381
+ ' '.join(command_parts) if command_parts else "unknown command"
382
+ )
383
+ return (
384
+ f"MCP server package not found. Check if '{command_str}' "
385
+ "is correct."
386
+ )
387
+
388
+ elif "permission" in error_str:
389
+ return (
390
+ f"Permission denied when trying to run MCP server "
391
+ f"command '{self.config.command}'."
392
+ )
393
+
394
+ elif "connection" in error_str:
395
+ if self.config.url:
396
+ return f"Failed to connect to MCP server at {self.config.url}."
397
+ else:
398
+ return "Connection failed to MCP server."
399
+
400
+ else:
401
+ # Generic fallback
402
+ command_info = (
403
+ f"'{self.config.command}'"
404
+ if self.config.command
405
+ else f"URL: {self.config.url}"
406
+ )
407
+ return (
408
+ f"MCP connection failed for {command_info}. Error: "
409
+ f"{str(error)[:100]}{'...' if len(str(error)) > 100 else ''}"
410
+ )
411
+
412
+ @asynccontextmanager
413
+ async def _create_transport(self):
414
+ """Create the appropriate transport based on detected type."""
415
+ transport_type = self.config.transport_type
416
+
417
+ if transport_type == TransportType.STDIO:
418
+ from mcp import StdioServerParameters
419
+ from mcp.client.stdio import stdio_client
420
+
421
+ # Ensure command is not None for STDIO
422
+ if not self.config.command:
423
+ raise ValueError("Command is required for STDIO transport")
424
+
425
+ server_params = StdioServerParameters(
426
+ command=self.config.command,
427
+ args=self.config.args or [],
428
+ env=self.config.env,
429
+ cwd=self.config.cwd,
430
+ encoding=self.config.encoding,
431
+ )
432
+
433
+ async with stdio_client(server_params) as (
434
+ read_stream,
435
+ write_stream,
436
+ ):
437
+ yield read_stream, write_stream
438
+
439
+ elif transport_type == TransportType.SSE:
440
+ from mcp.client.sse import sse_client
441
+
442
+ # Ensure URL is not None for SSE
443
+ if not self.config.url:
444
+ raise ValueError("URL is required for SSE transport")
445
+
446
+ try:
447
+ # Try with httpx_client_factory first (newer versions)
448
+ async with sse_client(
449
+ url=self.config.url,
450
+ headers=self.config.headers,
451
+ timeout=self.config.timeout,
452
+ sse_read_timeout=self.config.sse_read_timeout,
453
+ httpx_client_factory=create_mcp_http_client,
454
+ ) as (read_stream, write_stream):
455
+ yield read_stream, write_stream
456
+ except TypeError:
457
+ # Fall back to basic call without httpx_client_factory
458
+ async with sse_client(
459
+ url=self.config.url,
460
+ headers=self.config.headers,
461
+ timeout=self.config.timeout,
462
+ sse_read_timeout=self.config.sse_read_timeout,
463
+ ) as (read_stream, write_stream):
464
+ yield read_stream, write_stream
465
+
466
+ elif transport_type == TransportType.STREAMABLE_HTTP:
467
+ from mcp.client.streamable_http import streamablehttp_client
468
+
469
+ # Ensure URL is not None for StreamableHTTP
470
+ if not self.config.url:
471
+ raise ValueError(
472
+ "URL is required for StreamableHTTP transport"
473
+ )
474
+
475
+ try:
476
+ # Try with httpx_client_factory first (newer versions)
477
+ async with streamablehttp_client(
478
+ url=self.config.url,
479
+ headers=self.config.headers,
480
+ timeout=timedelta(seconds=self.config.timeout),
481
+ sse_read_timeout=timedelta(
482
+ seconds=self.config.sse_read_timeout
483
+ ),
484
+ terminate_on_close=self.config.terminate_on_close,
485
+ httpx_client_factory=create_mcp_http_client,
486
+ ) as (read_stream, write_stream, get_session_id):
487
+ yield read_stream, write_stream, get_session_id
488
+ except TypeError:
489
+ # Fall back to basic call without httpx_client_factory
490
+ async with streamablehttp_client(
491
+ url=self.config.url,
492
+ headers=self.config.headers,
493
+ timeout=timedelta(seconds=self.config.timeout),
494
+ sse_read_timeout=timedelta(
495
+ seconds=self.config.sse_read_timeout
496
+ ),
497
+ terminate_on_close=self.config.terminate_on_close,
498
+ ) as (read_stream, write_stream, get_session_id):
499
+ yield read_stream, write_stream, get_session_id
500
+
501
+ elif transport_type == TransportType.WEBSOCKET:
502
+ from mcp.client.websocket import websocket_client
503
+
504
+ # Ensure URL is not None for WebSocket
505
+ if not self.config.url:
506
+ raise ValueError("URL is required for WebSocket transport")
507
+
508
+ async with websocket_client(url=self.config.url) as (
509
+ read_stream,
510
+ write_stream,
511
+ ):
512
+ yield read_stream, write_stream
513
+
514
+ else:
515
+ raise ValueError(f"Unsupported transport type: {transport_type}")
516
+
517
+ @property
518
+ def session(self) -> Optional[ClientSession]:
519
+ r"""Get the current session if connected."""
520
+ return self._session
521
+
522
+ def is_connected(self) -> bool:
523
+ r"""Check if the client is currently connected."""
524
+ return self._session is not None
525
+
526
+ async def list_mcp_tools(self):
527
+ r"""Retrieves the list of available tools from the connected MCP
528
+ server.
529
+
530
+ Returns:
531
+ ListToolsResult: Result containing available MCP tools.
532
+ """
533
+ if not self._session:
534
+ return "MCP Client is not connected. Call `connection()` first."
535
+ try:
536
+ return await self._session.list_tools()
537
+ except Exception as e:
538
+ raise e
539
+
540
+ def list_mcp_tools_sync(self):
541
+ r"""Synchronously retrieves the list of available tools from the
542
+ connected MCP server.
543
+
544
+ Returns:
545
+ ListToolsResult: Result containing available MCP tools.
546
+ """
547
+ from camel.utils.commons import run_async
548
+
549
+ return run_async(self.list_mcp_tools)()
550
+
551
+ def generate_function_from_mcp_tool(
552
+ self, mcp_tool: types.Tool
553
+ ) -> Callable:
554
+ r"""Dynamically generates a Python callable function corresponding to
555
+ a given MCP tool.
556
+
557
+ Args:
558
+ mcp_tool (types.Tool): The MCP tool definition received from the
559
+ MCP server.
560
+
561
+ Returns:
562
+ Callable: A dynamically created async Python function that wraps
563
+ the MCP tool.
564
+ """
565
+ func_name = mcp_tool.name
566
+ func_desc = mcp_tool.description or "No description provided."
567
+ parameters_schema = mcp_tool.inputSchema.get("properties", {})
568
+ required_params = mcp_tool.inputSchema.get("required", [])
569
+
570
+ type_map = {
571
+ "string": str,
572
+ "integer": int,
573
+ "number": float,
574
+ "boolean": bool,
575
+ "array": list,
576
+ "object": dict,
577
+ }
578
+ annotations = {} # used to type hints
579
+ defaults: Dict[str, Any] = {} # store default values
580
+
581
+ func_params = []
582
+ for param_name, param_schema in parameters_schema.items():
583
+ param_type = param_schema.get("type", "Any")
584
+ param_type = type_map.get(param_type, Any)
585
+
586
+ annotations[param_name] = param_type
587
+ if param_name not in required_params:
588
+ defaults[param_name] = None
589
+
590
+ func_params.append(param_name)
591
+
592
+ async def dynamic_function(**kwargs) -> str:
593
+ r"""Auto-generated function for MCP Tool interaction.
594
+
595
+ Args:
596
+ kwargs: Keyword arguments corresponding to MCP tool parameters.
597
+
598
+ Returns:
599
+ str: The textual result returned by the MCP tool.
600
+ """
601
+
602
+ missing_params: Set[str] = set(required_params) - set(
603
+ kwargs.keys()
604
+ )
605
+ if missing_params:
606
+ from camel.logger import get_logger
607
+
608
+ logger = get_logger(__name__)
609
+ logger.warning(
610
+ f"Missing required parameters: {missing_params}"
611
+ )
612
+ return "Missing required parameters."
613
+
614
+ if not self._session:
615
+ from camel.logger import get_logger
616
+
617
+ logger = get_logger(__name__)
618
+ logger.error(
619
+ "MCP Client is not connected. Call `connection()` first."
620
+ )
621
+ raise RuntimeError(
622
+ "MCP Client is not connected. Call `connection()` first."
623
+ )
624
+
625
+ try:
626
+ result = await self._session.call_tool(func_name, kwargs)
627
+ except Exception as e:
628
+ from camel.logger import get_logger
629
+
630
+ logger = get_logger(__name__)
631
+ logger.error(f"Failed to call MCP tool '{func_name}': {e!s}")
632
+ raise e
633
+
634
+ if not result.content or len(result.content) == 0:
635
+ return "No data available for this request."
636
+
637
+ # Handle different content types
638
+ try:
639
+ content = result.content[0]
640
+ if content.type == "text":
641
+ return content.text
642
+ elif content.type == "image":
643
+ # Return image URL or data URI if available
644
+ if hasattr(content, "url") and content.url:
645
+ return f"Image available at: {content.url}"
646
+ return "Image content received (data URI not shown)"
647
+ elif content.type == "embedded_resource":
648
+ # Return resource information if available
649
+ if hasattr(content, "name") and content.name:
650
+ return f"Embedded resource: {content.name}"
651
+ return "Embedded resource received"
652
+ else:
653
+ msg = f"Received content of type '{content.type}'"
654
+ return f"{msg} which is not fully supported yet."
655
+ except (IndexError, AttributeError) as e:
656
+ from camel.logger import get_logger
657
+
658
+ logger = get_logger(__name__)
659
+ logger.error(
660
+ f"Error processing content from MCP tool response: {e!s}"
661
+ )
662
+ raise e
663
+
664
+ dynamic_function.__name__ = func_name
665
+ dynamic_function.__doc__ = func_desc
666
+ dynamic_function.__annotations__ = annotations
667
+
668
+ sig = inspect.Signature(
669
+ parameters=[
670
+ inspect.Parameter(
671
+ name=param,
672
+ kind=inspect.Parameter.KEYWORD_ONLY,
673
+ default=defaults.get(param, inspect.Parameter.empty),
674
+ annotation=annotations[param],
675
+ )
676
+ for param in func_params
677
+ ]
678
+ )
679
+ dynamic_function.__signature__ = sig # type: ignore[attr-defined]
680
+
681
+ return dynamic_function
682
+
683
+ def _build_tool_schema(self, mcp_tool: types.Tool) -> Dict[str, Any]:
684
+ r"""Build tool schema for OpenAI function calling format."""
685
+ input_schema = mcp_tool.inputSchema
686
+ properties = input_schema.get("properties", {})
687
+ required = input_schema.get("required", [])
688
+
689
+ parameters = {
690
+ "type": "object",
691
+ "properties": properties,
692
+ "required": required,
693
+ "additionalProperties": False,
694
+ }
695
+
696
+ return {
697
+ "type": "function",
698
+ "function": {
699
+ "name": mcp_tool.name,
700
+ "description": mcp_tool.description
701
+ or "No description provided.",
702
+ "strict": self.strict,
703
+ "parameters": parameters,
704
+ },
705
+ }
706
+
707
+ def get_tools(self):
708
+ r"""Get available tools as CAMEL FunctionTool objects.
709
+
710
+ Retrieves all available tools from the connected MCP server and
711
+ converts them to CAMEL-compatible :obj:`FunctionTool` objects. The
712
+ tools are automatically wrapped to handle the MCP protocol
713
+ communication.
714
+
715
+ Returns:
716
+ List[FunctionTool]: A list of :obj:`FunctionTool` objects
717
+ representing the available tools from the MCP server. Returns
718
+ an empty list if the client is not connected.
719
+
720
+ Note:
721
+ This method requires an active connection to the MCP server.
722
+ If the client is not connected, an empty list will be returned.
723
+ """
724
+ if not self.is_connected():
725
+ return []
726
+
727
+ # Import FunctionTool here to avoid circular imports
728
+ try:
729
+ from camel.toolkits import FunctionTool
730
+ except ImportError:
731
+ from camel.logger import get_logger
732
+
733
+ logger = get_logger(__name__)
734
+ logger.error(
735
+ "Failed to import FunctionTool. Please ensure "
736
+ "camel.toolkits is available."
737
+ )
738
+ return []
739
+
740
+ camel_tools = []
741
+ for tool in self._tools:
742
+ try:
743
+ # Generate the function and build the tool schema
744
+ func = self.generate_function_from_mcp_tool(tool)
745
+ schema = self._build_tool_schema(tool)
746
+
747
+ # Create CAMEL FunctionTool
748
+ camel_tool = FunctionTool(
749
+ func,
750
+ openai_tool_schema=schema,
751
+ )
752
+ camel_tools.append(camel_tool)
753
+ except Exception as e:
754
+ # Log error but continue with other tools
755
+ from camel.logger import get_logger
756
+
757
+ logger = get_logger(__name__)
758
+ logger.warning(f"Failed to convert tool {tool.name}: {e}")
759
+
760
+ return camel_tools
761
+
762
+ def get_text_tools(self) -> str:
763
+ r"""Get a text description of available tools.
764
+
765
+ Returns:
766
+ str: Text description of tools
767
+ """
768
+ if not self.is_connected():
769
+ return "Client not connected"
770
+
771
+ if not self._tools:
772
+ return "No tools available"
773
+
774
+ tool_descriptions = []
775
+ for tool in self._tools:
776
+ desc = tool.description or 'No description'
777
+ description = f"- {tool.name}: {desc}"
778
+ tool_descriptions.append(description)
779
+
780
+ return "\n".join(tool_descriptions)
781
+
782
+ async def call_tool(
783
+ self, tool_name: str, arguments: Dict[str, Any]
784
+ ) -> Any:
785
+ r"""Call a tool by name with the provided arguments.
786
+
787
+ Executes a specific tool on the connected MCP server with the given
788
+ arguments. The tool must be available in the server's tool list.
789
+
790
+ Args:
791
+ tool_name (str): The name of the tool to call. Must match a tool
792
+ name returned by :obj:`get_tools()`.
793
+ arguments (Dict[str, Any]): A dictionary of arguments to pass to
794
+ the tool. The argument names and types must match the tool's
795
+ expected parameters.
796
+
797
+ Returns:
798
+ Any: The result returned by the tool execution. The type and
799
+ structure depend on the specific tool being called.
800
+
801
+ Raises:
802
+ RuntimeError: If the client is not connected to an MCP server.
803
+ ValueError: If the specified tool name is not found in the list
804
+ of available tools.
805
+
806
+ Example:
807
+ .. code-block:: python
808
+
809
+ # Call a file reading tool
810
+ result = await client.call_tool(
811
+ "read_file",
812
+ {"path": "/tmp/example.txt"}
813
+ )
814
+ """
815
+ if not self.is_connected():
816
+ raise RuntimeError("Client is not connected")
817
+
818
+ # Check if tool exists
819
+ tool_names = [tool.name for tool in self._tools]
820
+ if tool_name not in tool_names:
821
+ available_tools = ', '.join(tool_names)
822
+ raise ValueError(
823
+ f"Tool '{tool_name}' not found. "
824
+ f"Available tools: {available_tools}"
825
+ )
826
+
827
+ # Call the tool using the correct API
828
+ if self._session is None:
829
+ raise RuntimeError("Client session is not available")
830
+
831
+ result = await self._session.call_tool(
832
+ name=tool_name, arguments=arguments
833
+ )
834
+
835
+ return result
836
+
837
+ def call_tool_sync(self, tool_name: str, arguments: Dict[str, Any]) -> Any:
838
+ r"""Synchronously call a tool by name with the provided arguments.
839
+
840
+ Args:
841
+ tool_name (str): The name of the tool to call.
842
+ arguments (Dict[str, Any]): A dictionary of arguments to pass to
843
+ the tool.
844
+
845
+ Returns:
846
+ Any: The result returned by the tool execution.
847
+ """
848
+ from camel.utils.commons import run_async
849
+
850
+ return run_async(self.call_tool)(tool_name, arguments)
851
+
852
+
853
+ def create_mcp_client(
854
+ config: Union[Dict[str, Any], ServerConfig], **kwargs: Any
855
+ ) -> MCPClient:
856
+ r"""Create an MCP client from configuration.
857
+
858
+ Factory function that creates an :obj:`MCPClient` instance from various
859
+ configuration formats. This is the recommended way to create MCP clients
860
+ as it handles configuration validation and type conversion automatically.
861
+
862
+ Args:
863
+ config (Union[Dict[str, Any], ServerConfig]): Server configuration
864
+ as either a dictionary or a :obj:`ServerConfig` object. If a
865
+ dictionary is provided, it will be automatically converted to
866
+ a :obj:`ServerConfig`.
867
+ **kwargs: Additional keyword arguments passed to the :obj:`MCPClient`
868
+ constructor, such as :obj:`client_info`, :obj:`timeout`, and
869
+ :obj:`strict`.
870
+
871
+ Returns:
872
+ MCPClient: A configured :obj:`MCPClient` instance ready for use as
873
+ an async context manager.
874
+
875
+ Examples:
876
+ STDIO server:
877
+
878
+ .. code-block:: python
879
+
880
+ async with create_mcp_client({
881
+ "command": "npx",
882
+ "args": [
883
+ "-y",
884
+ "@modelcontextprotocol/server-filesystem",
885
+ "/path",
886
+ ],
887
+ }) as client:
888
+ tools = client.get_tools()
889
+
890
+ HTTP server:
891
+
892
+ .. code-block:: python
893
+
894
+ async with create_mcp_client({
895
+ "url": "https://api.example.com/mcp",
896
+ "headers": {"Authorization": "Bearer token"}
897
+ }) as client:
898
+ result = await client.call_tool("tool_name", {"arg": "value"})
899
+
900
+ WebSocket server:
901
+
902
+ .. code-block:: python
903
+
904
+ async with create_mcp_client({
905
+ "url": "ws://localhost:8080/mcp"
906
+ }) as client:
907
+ tools = client.get_tools()
908
+
909
+ With strict mode enabled:
910
+
911
+ .. code-block:: python
912
+
913
+ async with create_mcp_client({
914
+ "command": "npx",
915
+ "args": [
916
+ "-y",
917
+ "@modelcontextprotocol/server-filesystem",
918
+ "/path",
919
+ ],
920
+ }, strict=True) as client:
921
+ tools = client.get_tools()
922
+ """
923
+ return MCPClient(config, **kwargs)
924
+
925
+
926
+ def create_mcp_client_from_config_file(
927
+ config_path: Union[str, Path], server_name: str, **kwargs: Any
928
+ ) -> MCPClient:
929
+ r"""Create an MCP client from a configuration file.
930
+
931
+ Args:
932
+ config_path (Union[str, Path]): Path to configuration file (JSON).
933
+ server_name (str): Name of the server in the config.
934
+ **kwargs: Additional arguments passed to MCPClient constructor.
935
+
936
+ Returns:
937
+ MCPClient: MCPClient instance as an async context manager.
938
+ Example config file:
939
+ {
940
+ "mcpServers": {
941
+ "filesystem": {
942
+ "command": "npx",
943
+ "args": [
944
+ "-y",
945
+ "@modelcontextprotocol/server-filesystem",
946
+ "/path"
947
+ ]
948
+ },
949
+ "remote-server": {
950
+ "url": "https://api.example.com/mcp",
951
+ "headers": {"Authorization": "Bearer token"}
952
+ }
953
+ }
954
+ }
955
+
956
+ Usage:
957
+ .. code-block:: python
958
+
959
+ async with create_mcp_client_from_config_file(
960
+ "config.json", "filesystem"
961
+ ) as client:
962
+ tools = client.get_tools()
963
+ """
964
+ import json
965
+
966
+ config_path = Path(config_path)
967
+ with open(config_path, 'r') as f:
968
+ config_data = json.load(f)
969
+
970
+ servers = config_data.get("mcpServers", {})
971
+ if server_name not in servers:
972
+ available = list(servers.keys())
973
+ raise ValueError(
974
+ f"Server '{server_name}' not found in config. "
975
+ f"Available: {available}"
976
+ )
977
+
978
+ server_config = servers[server_name]
979
+ return create_mcp_client(server_config, **kwargs)