camel-ai 0.2.52__py3-none-any.whl → 0.2.53__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.

@@ -0,0 +1,456 @@
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
+ import os
16
+ from typing import TYPE_CHECKING, Dict, List, Optional, Union
17
+
18
+ if TYPE_CHECKING:
19
+ from aci.types.app_configurations import AppConfiguration
20
+ from aci.types.apps import AppBasic, AppDetails
21
+ from aci.types.linked_accounts import LinkedAccount
22
+
23
+ from camel.logger import get_logger
24
+ from camel.toolkits import FunctionTool
25
+ from camel.toolkits.base import BaseToolkit
26
+ from camel.utils import api_keys_required, dependencies_required
27
+
28
+ logger = get_logger(__name__)
29
+
30
+
31
+ @api_keys_required(
32
+ [
33
+ (None, 'ACI_API_KEY'),
34
+ ]
35
+ )
36
+ class ACIToolkit(BaseToolkit):
37
+ r"""A toolkit for interacting with the ACI API."""
38
+
39
+ @dependencies_required('aci')
40
+ def __init__(
41
+ self,
42
+ api_key: Optional[str] = None,
43
+ base_url: Optional[str] = None,
44
+ linked_account_owner_id: Optional[str] = None,
45
+ timeout: Optional[float] = None,
46
+ ) -> None:
47
+ r"""Initialize the ACI toolkit.
48
+
49
+ Args:
50
+ api_key (Optional[str]): The API key for authentication.
51
+ (default: :obj: `None`)
52
+ base_url (Optional[str]): The base URL for the ACI API.
53
+ (default: :obj: `None`)
54
+ linked_account_owner_id (Optional[str]): ID of the owner of the
55
+ linked account, e.g., "johndoe"
56
+ (default: :obj: `None`)
57
+ timeout (Optional[float]): Request timeout.
58
+ (default: :obj: `None`)
59
+ """
60
+ from aci import ACI
61
+
62
+ super().__init__(timeout)
63
+
64
+ self._api_key = api_key or os.getenv("ACI_API_KEY")
65
+ self._base_url = base_url or os.getenv("ACI_BASE_URL")
66
+ self.client = ACI(api_key=self._api_key, base_url=self._base_url)
67
+ self.linked_account_owner_id = linked_account_owner_id
68
+
69
+ def search_tool(
70
+ self,
71
+ intent: Optional[str] = None,
72
+ allowed_app_only: bool = True,
73
+ include_functions: bool = False,
74
+ categories: Optional[List[str]] = None,
75
+ limit: Optional[int] = 10,
76
+ offset: Optional[int] = 0,
77
+ ) -> Union[List["AppBasic"], str]:
78
+ r"""Search for apps based on intent.
79
+
80
+ Args:
81
+ intent (Optional[str]): Search results will be sorted by relevance
82
+ to this intent.
83
+ (default: :obj: `None`)
84
+ allowed_app_only (bool): If true, only return apps that
85
+ are allowed by the agent/accessor, identified by the api key.
86
+ (default: :obj: `True`)
87
+ include_functions (bool): If true, include functions
88
+ (name and description) in the search results.
89
+ (default: :obj: `False`)
90
+ categories (Optional[List[str]]): List of categories to filter the
91
+ search results. Defaults to an empty list.
92
+ (default: :obj: `None`)
93
+ limit (Optional[int]): Maximum number of results to return.
94
+ (default: :obj: `10`)
95
+ offset (Optional[int]): Offset for pagination.
96
+ (default: :obj: `0`)
97
+
98
+ Returns:
99
+ Optional[List[AppBasic]]: List of matching apps if successful,
100
+ error message otherwise.
101
+ """
102
+ try:
103
+ apps = self.client.apps.search(
104
+ intent=intent,
105
+ allowed_apps_only=allowed_app_only,
106
+ include_functions=include_functions,
107
+ categories=categories,
108
+ limit=limit,
109
+ offset=offset,
110
+ )
111
+ return apps
112
+ except Exception as e:
113
+ logger.error(f"Error: {e}")
114
+ return str(e)
115
+
116
+ def list_configured_apps(
117
+ self,
118
+ app_names: Optional[List[str]] = None,
119
+ limit: Optional[int] = 10,
120
+ offset: Optional[int] = 0,
121
+ ) -> Union[List["AppConfiguration"], str]:
122
+ r"""List all configured apps.
123
+
124
+ Args:
125
+ app_names (Optional[List[str]]): List of app names to filter the
126
+ results. (default: :obj: `None`)
127
+ limit (Optional[int]): Maximum number of results to return.
128
+ (default: :obj: `10`)
129
+ offset (Optional[int]): Offset for pagination. (default: :obj: `0`)
130
+
131
+ Returns:
132
+ Union[List[AppConfiguration], str]: List of configured apps if
133
+ successful, error message otherwise.
134
+ """
135
+ try:
136
+ apps = self.client.app_configurations.list(
137
+ app_names=app_names, limit=limit, offset=offset
138
+ )
139
+ return apps
140
+ except Exception as e:
141
+ logger.error(f"Error: {e}")
142
+ return str(e)
143
+
144
+ def configure_app(self, app_name: str) -> Union[Dict, str]:
145
+ r"""Configure an app with specified authentication type.
146
+
147
+ Args:
148
+ app_name (str): Name of the app to configure.
149
+
150
+ Returns:
151
+ Union[Dict, str]: Configuration result or error message.
152
+ """
153
+ from aci.types.enums import SecurityScheme
154
+
155
+ try:
156
+ app_details = self.get_app_details(app_name)
157
+ if app_details and app_details.security_schemes[0] == "api_key":
158
+ security_scheme = SecurityScheme.API_KEY
159
+ elif app_details and app_details.security_schemes[0] == "oauth2":
160
+ security_scheme = SecurityScheme.OAUTH2
161
+ else:
162
+ security_scheme = SecurityScheme.NO_AUTH
163
+ configuration = self.client.app_configurations.create(
164
+ app_name=app_name, security_scheme=security_scheme
165
+ )
166
+ return configuration
167
+ except Exception as e:
168
+ logger.error(f"Error: {e}")
169
+ return str(e)
170
+
171
+ def get_app_configuration(
172
+ self, app_name: str
173
+ ) -> Union["AppConfiguration", str]:
174
+ r"""Get app configuration by app name.
175
+
176
+ Args:
177
+ app_name (str): Name of the app to get configuration for.
178
+
179
+ Returns:
180
+ Union[AppConfiguration, str]: App configuration if successful,
181
+ error message otherwise.
182
+ """
183
+ try:
184
+ app = self.client.app_configurations.get(app_name=app_name)
185
+ return app
186
+ except Exception as e:
187
+ logger.error(f"Error: {e}")
188
+ return str(e)
189
+
190
+ def delete_app(self, app_name: str) -> Optional[str]:
191
+ r"""Delete an app configuration.
192
+
193
+ Args:
194
+ app_name (str): Name of the app to delete.
195
+
196
+ Returns:
197
+ Optional[str]: None if successful, error message otherwise.
198
+ """
199
+ try:
200
+ self.client.app_configurations.delete(app_name=app_name)
201
+ return None
202
+ except Exception as e:
203
+ logger.error(f"Error: {e}")
204
+ return str(e)
205
+
206
+ def link_account(
207
+ self,
208
+ app_name: str,
209
+ ) -> Union["LinkedAccount", str]:
210
+ r"""Link an account to a configured app.
211
+
212
+ Args:
213
+ app_name (str): Name of the app to link the account to.
214
+
215
+ Returns:
216
+ Union[LinkedAccount, str]: LinkedAccount object if successful,
217
+ error message otherwise.
218
+ """
219
+ from aci.types.enums import SecurityScheme
220
+
221
+ try:
222
+ security_scheme = self.client.app_configurations.get(
223
+ app_name=app_name
224
+ ).security_scheme
225
+
226
+ if security_scheme == SecurityScheme.API_KEY:
227
+ return self.client.linked_accounts.link(
228
+ app_name=app_name,
229
+ linked_account_owner_id=self.linked_account_owner_id,
230
+ security_scheme=security_scheme,
231
+ api_key=self._api_key,
232
+ )
233
+ else:
234
+ return self.client.linked_accounts.link(
235
+ app_name=app_name,
236
+ linked_account_owner_id=self.linked_account_owner_id,
237
+ security_scheme=security_scheme,
238
+ )
239
+ except Exception as e:
240
+ logger.error(f"Error linking account: {e!s}")
241
+ return str(e)
242
+
243
+ def get_app_details(self, app_name: str) -> "AppDetails":
244
+ r"""Get details of an app.
245
+
246
+ Args:
247
+ app_name (str): Name of the app to get details for.
248
+
249
+ Returns:
250
+ AppDetails: App details.
251
+ """
252
+ app = self.client.apps.get(app_name=app_name)
253
+ return app
254
+
255
+ def get_linked_accounts(
256
+ self, app_name: str
257
+ ) -> Union[List["LinkedAccount"], str]:
258
+ r"""List all linked accounts for a specific app.
259
+
260
+ Args:
261
+ app_name (str): Name of the app to get linked accounts for.
262
+
263
+ Returns:
264
+ Union[List[LinkedAccount], str]: List of linked accounts if
265
+ successful, error message otherwise.
266
+ """
267
+ try:
268
+ accounts = self.client.linked_accounts.list(app_name=app_name)
269
+ return accounts
270
+ except Exception as e:
271
+ logger.error(f"Error: {e}")
272
+ return str(e)
273
+
274
+ def enable_linked_account(
275
+ self, linked_account_id: str
276
+ ) -> Union["LinkedAccount", str]:
277
+ r"""Enable a linked account.
278
+
279
+ Args:
280
+ linked_account_id (str): ID of the linked account to enable.
281
+
282
+ Returns:
283
+ Union[LinkedAccount, str]: Linked account if successful, error
284
+ message otherwise.
285
+ """
286
+ try:
287
+ linked_account = self.client.linked_accounts.enable(
288
+ linked_account_id=linked_account_id
289
+ )
290
+ return linked_account
291
+ except Exception as e:
292
+ logger.error(f"Error: {e}")
293
+ return str(e)
294
+
295
+ def disable_linked_account(
296
+ self, linked_account_id: str
297
+ ) -> Union["LinkedAccount", str]:
298
+ r"""Disable a linked account.
299
+
300
+ Args:
301
+ linked_account_id (str): ID of the linked account to disable.
302
+
303
+ Returns:
304
+ Union[LinkedAccount, str]: The updated linked account if
305
+ successful, error message otherwise.
306
+ """
307
+ try:
308
+ linked_account = self.client.linked_accounts.disable(
309
+ linked_account_id=linked_account_id
310
+ )
311
+ return linked_account
312
+ except Exception as e:
313
+ logger.error(f"Error: {e}")
314
+ return str(e)
315
+
316
+ def delete_linked_account(self, linked_account_id: str) -> str:
317
+ r"""Delete a linked account.
318
+
319
+ Args:
320
+ linked_account_id (str): ID of the linked account to delete.
321
+
322
+ Returns:
323
+ str: Success message if successful, error message otherwise.
324
+ """
325
+ try:
326
+ self.client.linked_accounts.delete(
327
+ linked_account_id=linked_account_id
328
+ )
329
+ return (
330
+ f"linked_account_id: {linked_account_id} deleted successfully"
331
+ )
332
+ except Exception as e:
333
+ logger.error(f"Error: {e}")
334
+ return str(e)
335
+
336
+ def function_definition(self, func_name: str) -> Dict:
337
+ r"""Get the function definition for an app.
338
+
339
+ Args:
340
+ app_name (str): Name of the app to get function definition for
341
+
342
+ Returns:
343
+ Dict: Function definition dictionary.
344
+ """
345
+ return self.client.functions.get_definition(func_name)
346
+
347
+ def search_function(
348
+ self,
349
+ app_names: Optional[List[str]] = None,
350
+ intent: Optional[str] = None,
351
+ allowed_apps_only: bool = True,
352
+ limit: Optional[int] = 10,
353
+ offset: Optional[int] = 0,
354
+ ) -> List[Dict]:
355
+ r"""Search for functions based on intent.
356
+
357
+ Args:
358
+ app_names (Optional[List[str]]): List of app names to filter the
359
+ search results. (default: :obj: `None`)
360
+ intent (Optional[str]): The search query/intent.
361
+ (default: :obj: `None`)
362
+ allowed_apps_only (bool): If true, only return
363
+ functions from allowed apps. (default: :obj: `True`)
364
+ limit (Optional[int]): Maximum number of results to return.
365
+ (default: :obj: `10`)
366
+ offset (Optional[int]): Offset for pagination.
367
+ (default: :obj: `0`)
368
+
369
+ Returns:
370
+ List[Dict]: List of matching functions
371
+ """
372
+ return self.client.functions.search(
373
+ app_names=app_names,
374
+ intent=intent,
375
+ allowed_apps_only=allowed_apps_only,
376
+ limit=limit,
377
+ offset=offset,
378
+ )
379
+
380
+ def execute_function(
381
+ self,
382
+ function_name: str,
383
+ function_arguments: Dict,
384
+ linked_account_owner_id: str,
385
+ allowed_apps_only: bool = False,
386
+ ) -> Dict:
387
+ r"""Execute a function call.
388
+
389
+ Args:
390
+ function_name (str): Name of the function to execute.
391
+ function_arguments (Dict): Arguments to pass to the function.
392
+ linked_account_owner_id (str): To specify the end-user (account
393
+ owner) on behalf of whom you want to execute functions
394
+ You need to first link corresponding account with the same
395
+ owner id in the ACI dashboard (https://platform.aci.dev).
396
+ allowed_apps_only (bool): If true, only returns functions/apps
397
+ that are allowed to be used by the agent/accessor, identified
398
+ by the api key. (default: :obj: `False`)
399
+
400
+ Returns:
401
+ Dict: Result of the function execution
402
+ """
403
+ result = self.client.handle_function_call(
404
+ function_name,
405
+ function_arguments,
406
+ linked_account_owner_id,
407
+ allowed_apps_only,
408
+ )
409
+ return result
410
+
411
+ def get_tools(self) -> List[FunctionTool]:
412
+ r"""Get a list of tools (functions) available in the configured apps.
413
+
414
+ Returns:
415
+ List[FunctionTool]: List of FunctionTool objects representing
416
+ available functions
417
+ """
418
+ _configure_app = [
419
+ app.app_name # type: ignore[union-attr]
420
+ for app in self.list_configured_apps() or []
421
+ ]
422
+ _all_function = self.search_function(app_names=_configure_app)
423
+ tools = [
424
+ FunctionTool(self.search_tool),
425
+ FunctionTool(self.list_configured_apps),
426
+ FunctionTool(self.configure_app),
427
+ FunctionTool(self.get_app_configuration),
428
+ FunctionTool(self.delete_app),
429
+ FunctionTool(self.link_account),
430
+ FunctionTool(self.get_app_details),
431
+ FunctionTool(self.get_linked_accounts),
432
+ FunctionTool(self.enable_linked_account),
433
+ FunctionTool(self.disable_linked_account),
434
+ FunctionTool(self.delete_linked_account),
435
+ FunctionTool(self.function_definition),
436
+ FunctionTool(self.search_function),
437
+ ]
438
+
439
+ for function in _all_function:
440
+ schema = self.client.functions.get_definition(
441
+ function['function']['name']
442
+ )
443
+
444
+ def dummy_func(*, schema=schema, **kwargs):
445
+ return self.execute_function(
446
+ function_name=schema['function']['name'],
447
+ function_arguments=kwargs,
448
+ linked_account_owner_id=self.linked_account_owner_id,
449
+ )
450
+
451
+ tool = FunctionTool(
452
+ func=dummy_func,
453
+ openai_tool_schema=schema,
454
+ )
455
+ tools.append(tool)
456
+ return tools
@@ -458,9 +458,11 @@ class FunctionTool:
458
458
  for param_name in properties.keys():
459
459
  param_dict = properties[param_name]
460
460
  if "description" not in param_dict:
461
- warnings.warn(f"""Parameter description is missing for
462
- {param_dict}. This may affect the quality of tool
463
- calling.""")
461
+ warnings.warn(
462
+ f"Parameter description is missing "
463
+ f"for {param_dict}. This may affect the "
464
+ f"quality of tool calling."
465
+ )
464
466
 
465
467
  def get_openai_tool_schema(self) -> Dict[str, Any]:
466
468
  r"""Gets the OpenAI tool schema for this function.
@@ -227,7 +227,7 @@ class MCPClient(BaseToolkit):
227
227
 
228
228
  func_params.append(param_name)
229
229
 
230
- async def dynamic_function(**kwargs):
230
+ async def dynamic_function(**kwargs) -> str:
231
231
  r"""Auto-generated function for MCP Tool interaction.
232
232
 
233
233
  Args:
@@ -351,6 +351,39 @@ class MCPClient(BaseToolkit):
351
351
  for mcp_tool in self._mcp_tools
352
352
  ]
353
353
 
354
+ def get_text_tools(self) -> str:
355
+ r"""Returns a string containing the descriptions of the tools
356
+ in the toolkit.
357
+
358
+ Returns:
359
+ str: A string containing the descriptions of the tools
360
+ in the toolkit.
361
+ """
362
+ return "\n".join(
363
+ f"tool_name: {tool.name}\n"
364
+ + f"description: {tool.description or 'No description'}\n"
365
+ + f"input Schema: {tool.inputSchema}\n"
366
+ for tool in self._mcp_tools
367
+ )
368
+
369
+ async def call_tool(
370
+ self, tool_name: str, tool_args: Dict[str, Any]
371
+ ) -> Any:
372
+ r"""Calls the specified tool with the provided arguments.
373
+
374
+ Args:
375
+ tool_name (str): Name of the tool to call.
376
+ tool_args (Dict[str, Any]): Arguments to pass to the tool
377
+ (default: :obj:`{}`).
378
+
379
+ Returns:
380
+ Any: The result of the tool call.
381
+ """
382
+ if self._session is None:
383
+ raise RuntimeError("Session is not initialized.")
384
+
385
+ return await self._session.call_tool(tool_name, tool_args)
386
+
354
387
  @property
355
388
  def session(self) -> Optional["ClientSession"]:
356
389
  return self._session
@@ -548,3 +581,13 @@ class MCPToolkit(BaseToolkit):
548
581
  for server in self.servers:
549
582
  all_tools.extend(server.get_tools())
550
583
  return all_tools
584
+
585
+ def get_text_tools(self) -> str:
586
+ r"""Returns a string containing the descriptions of the tools
587
+ in the toolkit.
588
+
589
+ Returns:
590
+ str: A string containing the descriptions of the tools
591
+ in the toolkit.
592
+ """
593
+ return "\n".join(server.get_text_tools() for server in self.servers)
camel/types/__init__.py CHANGED
@@ -14,6 +14,7 @@
14
14
  from .enums import (
15
15
  AudioModelType,
16
16
  EmbeddingModelType,
17
+ GeminiEmbeddingTaskType,
17
18
  HuggingFaceRepoType,
18
19
  ModelPlatformType,
19
20
  ModelType,
@@ -75,6 +76,7 @@ __all__ = [
75
76
  'UnifiedModelType',
76
77
  'ParsedChatCompletion',
77
78
  'HuggingFaceRepoType',
79
+ 'GeminiEmbeddingTaskType',
78
80
  'NOT_GIVEN',
79
81
  'NotGiven',
80
82
  ]
camel/types/enums.py CHANGED
@@ -1202,6 +1202,8 @@ class EmbeddingModelType(Enum):
1202
1202
 
1203
1203
  MISTRAL_EMBED = "mistral-embed"
1204
1204
 
1205
+ GEMINI_EMBEDDING_EXP = "gemini-embedding-exp-03-07"
1206
+
1205
1207
  @property
1206
1208
  def is_openai(self) -> bool:
1207
1209
  r"""Returns whether this type of models is an OpenAI-released model."""
@@ -1230,6 +1232,13 @@ class EmbeddingModelType(Enum):
1230
1232
  EmbeddingModelType.MISTRAL_EMBED,
1231
1233
  }
1232
1234
 
1235
+ @property
1236
+ def is_gemini(self) -> bool:
1237
+ r"""Returns whether this type of models is an Gemini-released model."""
1238
+ return self in {
1239
+ EmbeddingModelType.GEMINI_EMBEDDING_EXP,
1240
+ }
1241
+
1233
1242
  @property
1234
1243
  def output_dim(self) -> int:
1235
1244
  if self in {
@@ -1253,10 +1262,29 @@ class EmbeddingModelType(Enum):
1253
1262
  return 3072
1254
1263
  elif self is EmbeddingModelType.MISTRAL_EMBED:
1255
1264
  return 1024
1265
+ elif self is EmbeddingModelType.GEMINI_EMBEDDING_EXP:
1266
+ return 3072
1256
1267
  else:
1257
1268
  raise ValueError(f"Unknown model type {self}.")
1258
1269
 
1259
1270
 
1271
+ class GeminiEmbeddingTaskType(str, Enum):
1272
+ r"""Task types for Gemini embedding models.
1273
+
1274
+ For more information, please refer to:
1275
+ https://ai.google.dev/gemini-api/docs/embeddings#task-types
1276
+ """
1277
+
1278
+ SEMANTIC_SIMILARITY = "SEMANTIC_SIMILARITY"
1279
+ CLASSIFICATION = "CLASSIFICATION"
1280
+ CLUSTERING = "CLUSTERING"
1281
+ RETRIEVAL_DOCUMENT = "RETRIEVAL_DOCUMENT"
1282
+ RETRIEVAL_QUERY = "RETRIEVAL_QUERY"
1283
+ QUESTION_ANSWERING = "QUESTION_ANSWERING"
1284
+ FACT_VERIFICATION = "FACT_VERIFICATION"
1285
+ CODE_RETRIEVAL_QUERY = "CODE_RETRIEVAL_QUERY"
1286
+
1287
+
1260
1288
  class TaskType(Enum):
1261
1289
  AI_SOCIETY = "ai_society"
1262
1290
  CODE = "code"
camel/utils/mcp.py CHANGED
@@ -17,6 +17,42 @@ from typing import Any, Callable, Optional
17
17
 
18
18
 
19
19
  class MCPServer:
20
+ r"""Decorator class for registering functions of a class as tools in an MCP
21
+ (Model Context Protocol) server.
22
+
23
+ This class is typically used to wrap a toolkit or service class and
24
+ automatically register specified methods (or methods derived from
25
+ `BaseToolkit`) with a FastMCP server.
26
+
27
+ Args:
28
+ function_names (Optional[list[str]]): A list of method names to expose
29
+ via the MCP server. If not provided and the class is a subclass of
30
+ `BaseToolkit`, method names will be inferred from the tools
31
+ returned by `get_tools()`.
32
+ server_name (Optional[str]): A name for the MCP server. If not
33
+ provided, the class name of the decorated object is used.
34
+
35
+ Example:
36
+ ```
37
+ @MCPServer(function_names=["run", "status"])
38
+ class MyTool:
39
+ def run(self): ...
40
+ def status(self): ...
41
+ ```
42
+ Or, with a class inheriting from BaseToolkit (no need to specify
43
+ `function_names`):
44
+ ```
45
+ @MCPServer()
46
+ class MyToolkit(BaseToolkit):
47
+ ...
48
+ ```
49
+
50
+ Raises:
51
+ ValueError: If no function names are provided and the class does not
52
+ inherit from BaseToolkit, or if any specified method is not found
53
+ or not callable.
54
+ """
55
+
20
56
  def __init__(
21
57
  self,
22
58
  function_names: Optional[list[str]] = None,
@@ -26,6 +62,19 @@ class MCPServer:
26
62
  self.server_name = server_name
27
63
 
28
64
  def make_wrapper(self, func: Callable[..., Any]) -> Callable[..., Any]:
65
+ r"""Wraps a function (sync or async) to preserve its signature and
66
+ metadata.
67
+
68
+ This is used to ensure the MCP server can correctly call and introspect
69
+ the method.
70
+
71
+ Args:
72
+ func (Callable[..., Any]): The function to wrap.
73
+
74
+ Returns:
75
+ Callable[..., Any]: The wrapped function, with preserved signature
76
+ and async support.
77
+ """
29
78
  if inspect.iscoroutinefunction(func):
30
79
 
31
80
  @functools.wraps(func)
@@ -41,6 +90,20 @@ class MCPServer:
41
90
  return wrapper
42
91
 
43
92
  def __call__(self, cls):
93
+ r"""Decorates a class by injecting an MCP server instance and
94
+ registering specified methods.
95
+
96
+ Args:
97
+ cls (type): The class being decorated.
98
+
99
+ Returns:
100
+ type: The modified class with MCP integration.
101
+
102
+ Raises:
103
+ ValueError: If function names are missing and the class is not a
104
+ `BaseToolkit` subclass,
105
+ or if a specified method cannot be found or is not callable.
106
+ """
44
107
  from mcp.server.fastmcp import FastMCP
45
108
 
46
109
  from camel.toolkits.base import BaseToolkit