camel-ai 0.2.59__py3-none-any.whl → 0.2.82__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 (506) hide show
  1. camel/__init__.py +3 -3
  2. camel/agents/__init__.py +2 -2
  3. camel/agents/_types.py +9 -4
  4. camel/agents/_utils.py +40 -2
  5. camel/agents/base.py +2 -2
  6. camel/agents/chat_agent.py +5012 -902
  7. camel/agents/critic_agent.py +2 -2
  8. camel/agents/deductive_reasoner_agent.py +56 -56
  9. camel/agents/embodied_agent.py +2 -2
  10. camel/agents/knowledge_graph_agent.py +20 -20
  11. camel/agents/mcp_agent.py +39 -36
  12. camel/agents/multi_hop_generator_agent.py +3 -3
  13. camel/agents/programmed_agent_instruction.py +2 -2
  14. camel/agents/repo_agent.py +4 -3
  15. camel/agents/role_assignment_agent.py +2 -2
  16. camel/agents/search_agent.py +2 -2
  17. camel/agents/task_agent.py +2 -2
  18. camel/agents/tool_agents/__init__.py +2 -2
  19. camel/agents/tool_agents/base.py +2 -2
  20. camel/agents/tool_agents/hugging_face_tool_agent.py +3 -3
  21. camel/benchmarks/__init__.py +2 -2
  22. camel/benchmarks/apibank.py +5 -5
  23. camel/benchmarks/apibench.py +2 -2
  24. camel/benchmarks/base.py +2 -2
  25. camel/benchmarks/browsecomp.py +44 -33
  26. camel/benchmarks/gaia.py +17 -13
  27. camel/benchmarks/mock_website/README.md +94 -0
  28. camel/benchmarks/mock_website/mock_web.py +299 -0
  29. camel/benchmarks/mock_website/requirements.txt +3 -0
  30. camel/benchmarks/mock_website/shopping_mall/app.py +465 -0
  31. camel/benchmarks/mock_website/task.json +104 -0
  32. camel/benchmarks/nexus.py +3 -3
  33. camel/benchmarks/ragbench.py +2 -2
  34. camel/bots/__init__.py +2 -2
  35. camel/bots/discord/__init__.py +2 -2
  36. camel/bots/discord/discord_app.py +2 -2
  37. camel/bots/discord/discord_installation.py +2 -2
  38. camel/bots/discord/discord_store.py +3 -3
  39. camel/bots/slack/__init__.py +2 -2
  40. camel/bots/slack/models.py +4 -4
  41. camel/bots/slack/slack_app.py +2 -2
  42. camel/bots/telegram_bot.py +2 -2
  43. camel/configs/__init__.py +26 -2
  44. camel/configs/aihubmix_config.py +90 -0
  45. camel/configs/aiml_config.py +2 -2
  46. camel/configs/amd_config.py +70 -0
  47. camel/configs/anthropic_config.py +8 -7
  48. camel/configs/base_config.py +2 -2
  49. camel/configs/bedrock_config.py +5 -3
  50. camel/configs/cerebras_config.py +98 -0
  51. camel/configs/cohere_config.py +3 -3
  52. camel/configs/cometapi_config.py +106 -0
  53. camel/configs/crynux_config.py +94 -0
  54. camel/configs/deepseek_config.py +9 -8
  55. camel/configs/gemini_config.py +6 -4
  56. camel/configs/groq_config.py +6 -4
  57. camel/configs/internlm_config.py +6 -4
  58. camel/configs/litellm_config.py +2 -2
  59. camel/configs/lmstudio_config.py +6 -4
  60. camel/configs/minimax_config.py +95 -0
  61. camel/configs/mistral_config.py +3 -3
  62. camel/configs/modelscope_config.py +5 -3
  63. camel/configs/moonshot_config.py +2 -2
  64. camel/configs/nebius_config.py +105 -0
  65. camel/configs/netmind_config.py +2 -2
  66. camel/configs/novita_config.py +2 -2
  67. camel/configs/nvidia_config.py +2 -2
  68. camel/configs/ollama_config.py +2 -2
  69. camel/configs/openai_config.py +8 -3
  70. camel/configs/openrouter_config.py +6 -4
  71. camel/configs/ppio_config.py +2 -2
  72. camel/configs/qianfan_config.py +85 -0
  73. camel/configs/qwen_config.py +2 -2
  74. camel/configs/reka_config.py +3 -3
  75. camel/configs/samba_config.py +8 -6
  76. camel/configs/sglang_config.py +2 -2
  77. camel/configs/siliconflow_config.py +2 -2
  78. camel/configs/togetherai_config.py +2 -2
  79. camel/configs/vllm_config.py +4 -2
  80. camel/configs/watsonx_config.py +2 -2
  81. camel/configs/yi_config.py +6 -4
  82. camel/configs/zhipuai_config.py +6 -4
  83. camel/{data_collector → data_collectors}/__init__.py +2 -2
  84. camel/{data_collector → data_collectors}/alpaca_collector.py +19 -10
  85. camel/{data_collector → data_collectors}/base.py +2 -2
  86. camel/{data_collector → data_collectors}/sharegpt_collector.py +3 -3
  87. camel/datagen/__init__.py +2 -2
  88. camel/datagen/cot_datagen.py +32 -37
  89. camel/datagen/evol_instruct/__init__.py +2 -2
  90. camel/datagen/evol_instruct/evol_instruct.py +2 -2
  91. camel/datagen/evol_instruct/scorer.py +24 -25
  92. camel/datagen/evol_instruct/templates.py +48 -48
  93. camel/datagen/self_improving_cot.py +5 -5
  94. camel/datagen/self_instruct/__init__.py +2 -2
  95. camel/datagen/self_instruct/filter/__init__.py +2 -2
  96. camel/datagen/self_instruct/filter/filter_function.py +2 -2
  97. camel/datagen/self_instruct/filter/filter_registry.py +2 -2
  98. camel/datagen/self_instruct/filter/instruction_filter.py +2 -2
  99. camel/datagen/self_instruct/self_instruct.py +2 -2
  100. camel/datagen/self_instruct/templates.py +47 -47
  101. camel/datagen/source2synth/__init__.py +2 -2
  102. camel/datagen/source2synth/data_processor.py +2 -2
  103. camel/datagen/source2synth/models.py +2 -2
  104. camel/datagen/source2synth/user_data_processor_config.py +2 -2
  105. camel/datahubs/__init__.py +2 -2
  106. camel/datahubs/base.py +2 -2
  107. camel/datahubs/huggingface.py +2 -2
  108. camel/datahubs/models.py +2 -2
  109. camel/datasets/__init__.py +2 -2
  110. camel/datasets/base_generator.py +41 -12
  111. camel/datasets/few_shot_generator.py +18 -18
  112. camel/datasets/models.py +3 -3
  113. camel/datasets/self_instruct_generator.py +2 -2
  114. camel/datasets/static_dataset.py +152 -2
  115. camel/embeddings/__init__.py +2 -2
  116. camel/embeddings/azure_embedding.py +2 -2
  117. camel/embeddings/base.py +2 -2
  118. camel/embeddings/gemini_embedding.py +2 -2
  119. camel/embeddings/jina_embedding.py +10 -3
  120. camel/embeddings/mistral_embedding.py +2 -2
  121. camel/embeddings/openai_compatible_embedding.py +2 -2
  122. camel/embeddings/openai_embedding.py +2 -2
  123. camel/embeddings/sentence_transformers_embeddings.py +4 -4
  124. camel/embeddings/together_embedding.py +2 -2
  125. camel/embeddings/vlm_embedding.py +11 -4
  126. camel/environments/__init__.py +14 -2
  127. camel/environments/models.py +2 -2
  128. camel/environments/multi_step.py +2 -2
  129. camel/environments/rlcards_env.py +860 -0
  130. camel/environments/single_step.py +30 -5
  131. camel/environments/tic_tac_toe.py +3 -3
  132. camel/extractors/__init__.py +2 -2
  133. camel/extractors/base.py +2 -2
  134. camel/extractors/python_strategies.py +2 -2
  135. camel/generators.py +2 -2
  136. camel/human.py +2 -2
  137. camel/interpreters/__init__.py +4 -2
  138. camel/interpreters/base.py +16 -3
  139. camel/interpreters/docker/Dockerfile +53 -7
  140. camel/interpreters/docker_interpreter.py +70 -11
  141. camel/interpreters/e2b_interpreter.py +59 -11
  142. camel/interpreters/internal_python_interpreter.py +81 -4
  143. camel/interpreters/interpreter_error.py +2 -2
  144. camel/interpreters/ipython_interpreter.py +23 -5
  145. camel/interpreters/microsandbox_interpreter.py +395 -0
  146. camel/interpreters/subprocess_interpreter.py +36 -4
  147. camel/loaders/__init__.py +17 -5
  148. camel/loaders/apify_reader.py +2 -2
  149. camel/loaders/base_io.py +2 -2
  150. camel/loaders/base_loader.py +85 -0
  151. camel/loaders/chunkr_reader.py +128 -93
  152. camel/loaders/crawl4ai_reader.py +2 -2
  153. camel/loaders/firecrawl_reader.py +6 -6
  154. camel/loaders/jina_url_reader.py +2 -2
  155. camel/loaders/markitdown.py +2 -2
  156. camel/loaders/mineru_extractor.py +2 -2
  157. camel/loaders/mistral_reader.py +148 -0
  158. camel/loaders/scrapegraph_reader.py +2 -2
  159. camel/loaders/unstructured_io.py +2 -2
  160. camel/logger.py +5 -5
  161. camel/memories/__init__.py +2 -2
  162. camel/memories/agent_memories.py +86 -3
  163. camel/memories/base.py +36 -2
  164. camel/memories/blocks/__init__.py +2 -2
  165. camel/memories/blocks/chat_history_block.py +126 -9
  166. camel/memories/blocks/vectordb_block.py +10 -3
  167. camel/memories/context_creators/__init__.py +2 -2
  168. camel/memories/context_creators/score_based.py +31 -239
  169. camel/memories/records.py +98 -13
  170. camel/messages/__init__.py +2 -2
  171. camel/messages/base.py +193 -46
  172. camel/messages/conversion/__init__.py +2 -2
  173. camel/messages/conversion/alpaca.py +2 -2
  174. camel/messages/conversion/conversation_models.py +2 -2
  175. camel/messages/conversion/sharegpt/__init__.py +2 -2
  176. camel/messages/conversion/sharegpt/function_call_formatter.py +2 -2
  177. camel/messages/conversion/sharegpt/hermes/__init__.py +2 -2
  178. camel/messages/conversion/sharegpt/hermes/hermes_function_formatter.py +2 -2
  179. camel/messages/func_message.py +54 -17
  180. camel/models/__init__.py +18 -2
  181. camel/models/_utils.py +3 -3
  182. camel/models/aihubmix_model.py +83 -0
  183. camel/models/aiml_model.py +11 -18
  184. camel/models/amd_model.py +101 -0
  185. camel/models/anthropic_model.py +127 -20
  186. camel/models/aws_bedrock_model.py +12 -35
  187. camel/models/azure_openai_model.py +263 -63
  188. camel/models/base_audio_model.py +5 -3
  189. camel/models/base_model.py +195 -26
  190. camel/models/cerebras_model.py +83 -0
  191. camel/models/cohere_model.py +81 -21
  192. camel/models/cometapi_model.py +83 -0
  193. camel/models/crynux_model.py +87 -0
  194. camel/models/deepseek_model.py +61 -59
  195. camel/models/fish_audio_model.py +8 -2
  196. camel/models/gemini_model.py +439 -30
  197. camel/models/groq_model.py +11 -19
  198. camel/models/internlm_model.py +11 -18
  199. camel/models/litellm_model.py +94 -34
  200. camel/models/lmstudio_model.py +17 -20
  201. camel/models/minimax_model.py +83 -0
  202. camel/models/mistral_model.py +84 -19
  203. camel/models/model_factory.py +49 -6
  204. camel/models/model_manager.py +33 -11
  205. camel/models/modelscope_model.py +13 -193
  206. camel/models/moonshot_model.py +195 -21
  207. camel/models/nebius_model.py +83 -0
  208. camel/models/nemotron_model.py +19 -9
  209. camel/models/netmind_model.py +11 -18
  210. camel/models/novita_model.py +11 -18
  211. camel/models/nvidia_model.py +11 -18
  212. camel/models/ollama_model.py +14 -21
  213. camel/models/openai_audio_models.py +2 -2
  214. camel/models/openai_compatible_model.py +234 -27
  215. camel/models/openai_model.py +255 -39
  216. camel/models/openrouter_model.py +11 -19
  217. camel/models/ppio_model.py +11 -18
  218. camel/models/qianfan_model.py +89 -0
  219. camel/models/qwen_model.py +13 -193
  220. camel/models/reka_model.py +90 -21
  221. camel/models/reward/__init__.py +2 -2
  222. camel/models/reward/base_reward_model.py +2 -2
  223. camel/models/reward/evaluator.py +2 -2
  224. camel/models/reward/nemotron_model.py +2 -2
  225. camel/models/reward/skywork_model.py +2 -2
  226. camel/models/samba_model.py +117 -49
  227. camel/models/sglang_model.py +162 -42
  228. camel/models/siliconflow_model.py +12 -35
  229. camel/models/stub_model.py +10 -7
  230. camel/models/togetherai_model.py +11 -18
  231. camel/models/vllm_model.py +10 -18
  232. camel/models/volcano_model.py +16 -20
  233. camel/models/watsonx_model.py +69 -19
  234. camel/models/yi_model.py +11 -18
  235. camel/models/zhipuai_model.py +70 -18
  236. camel/parsers/__init__.py +18 -0
  237. camel/parsers/mcp_tool_call_parser.py +176 -0
  238. camel/personas/__init__.py +2 -2
  239. camel/personas/persona.py +2 -2
  240. camel/personas/persona_hub.py +2 -2
  241. camel/prompts/__init__.py +2 -2
  242. camel/prompts/ai_society.py +2 -2
  243. camel/prompts/base.py +2 -2
  244. camel/prompts/code.py +2 -2
  245. camel/prompts/evaluation.py +2 -2
  246. camel/prompts/generate_text_embedding_data.py +2 -2
  247. camel/prompts/image_craft.py +2 -2
  248. camel/prompts/misalignment.py +2 -2
  249. camel/prompts/multi_condition_image_craft.py +2 -2
  250. camel/prompts/object_recognition.py +2 -2
  251. camel/prompts/persona_hub.py +3 -3
  252. camel/prompts/prompt_templates.py +2 -2
  253. camel/prompts/role_description_prompt_template.py +2 -2
  254. camel/prompts/solution_extraction.py +8 -8
  255. camel/prompts/task_prompt_template.py +2 -2
  256. camel/prompts/translation.py +2 -2
  257. camel/prompts/video_description_prompt.py +3 -3
  258. camel/responses/__init__.py +2 -2
  259. camel/responses/agent_responses.py +2 -2
  260. camel/retrievers/__init__.py +2 -2
  261. camel/retrievers/auto_retriever.py +23 -3
  262. camel/retrievers/base.py +2 -2
  263. camel/retrievers/bm25_retriever.py +3 -4
  264. camel/retrievers/cohere_rerank_retriever.py +2 -2
  265. camel/retrievers/hybrid_retrival.py +4 -4
  266. camel/retrievers/vector_retriever.py +2 -2
  267. camel/runtimes/Dockerfile.multi-toolkit +90 -0
  268. camel/{runtime → runtimes}/__init__.py +2 -2
  269. camel/runtimes/api.py +153 -0
  270. camel/{runtime → runtimes}/base.py +2 -2
  271. camel/{runtime → runtimes}/configs.py +13 -13
  272. camel/{runtime → runtimes}/daytona_runtime.py +18 -19
  273. camel/{runtime → runtimes}/docker_runtime.py +13 -13
  274. camel/{runtime → runtimes}/llm_guard_runtime.py +28 -28
  275. camel/{runtime → runtimes}/remote_http_runtime.py +12 -12
  276. camel/{runtime → runtimes}/ubuntu_docker_runtime.py +3 -3
  277. camel/{runtime → runtimes}/utils/__init__.py +2 -2
  278. camel/{runtime → runtimes}/utils/function_risk_toolkit.py +2 -2
  279. camel/{runtime → runtimes}/utils/ignore_risk_toolkit.py +2 -2
  280. camel/schemas/__init__.py +2 -2
  281. camel/schemas/base.py +2 -2
  282. camel/schemas/openai_converter.py +3 -3
  283. camel/schemas/outlines_converter.py +2 -2
  284. camel/services/agent_openapi_server.py +380 -0
  285. camel/societies/__init__.py +4 -2
  286. camel/societies/babyagi_playing.py +2 -2
  287. camel/societies/role_playing.py +201 -80
  288. camel/societies/workforce/__init__.py +10 -3
  289. camel/societies/workforce/base.py +9 -5
  290. camel/societies/workforce/events.py +143 -0
  291. camel/societies/workforce/prompts.py +258 -33
  292. camel/societies/workforce/role_playing_worker.py +95 -30
  293. camel/societies/workforce/single_agent_worker.py +659 -30
  294. camel/societies/workforce/structured_output_handler.py +512 -0
  295. camel/societies/workforce/task_channel.py +182 -38
  296. camel/societies/workforce/utils.py +784 -18
  297. camel/societies/workforce/worker.py +96 -28
  298. camel/societies/workforce/workflow_memory_manager.py +1746 -0
  299. camel/societies/workforce/workforce.py +5730 -366
  300. camel/societies/workforce/workforce_callback.py +103 -0
  301. camel/societies/workforce/workforce_logger.py +647 -0
  302. camel/societies/workforce/workforce_metrics.py +33 -0
  303. camel/storages/__init__.py +10 -2
  304. camel/storages/graph_storages/__init__.py +2 -2
  305. camel/storages/graph_storages/base.py +2 -2
  306. camel/storages/graph_storages/graph_element.py +2 -2
  307. camel/storages/graph_storages/nebula_graph.py +4 -4
  308. camel/storages/graph_storages/neo4j_graph.py +7 -7
  309. camel/storages/key_value_storages/__init__.py +2 -2
  310. camel/storages/key_value_storages/base.py +2 -2
  311. camel/storages/key_value_storages/in_memory.py +2 -2
  312. camel/storages/key_value_storages/json.py +17 -4
  313. camel/storages/key_value_storages/mem0_cloud.py +50 -49
  314. camel/storages/key_value_storages/redis.py +2 -2
  315. camel/storages/object_storages/__init__.py +2 -2
  316. camel/storages/object_storages/amazon_s3.py +2 -2
  317. camel/storages/object_storages/azure_blob.py +2 -2
  318. camel/storages/object_storages/base.py +2 -2
  319. camel/storages/object_storages/google_cloud.py +3 -3
  320. camel/storages/vectordb_storages/__init__.py +12 -2
  321. camel/storages/vectordb_storages/base.py +2 -2
  322. camel/storages/vectordb_storages/chroma.py +731 -0
  323. camel/storages/vectordb_storages/faiss.py +712 -0
  324. camel/storages/vectordb_storages/milvus.py +2 -2
  325. camel/storages/vectordb_storages/oceanbase.py +16 -17
  326. camel/storages/vectordb_storages/pgvector.py +349 -0
  327. camel/storages/vectordb_storages/qdrant.py +6 -6
  328. camel/storages/vectordb_storages/surreal.py +372 -0
  329. camel/storages/vectordb_storages/tidb.py +11 -8
  330. camel/storages/vectordb_storages/weaviate.py +714 -0
  331. camel/tasks/__init__.py +2 -2
  332. camel/tasks/task.py +366 -27
  333. camel/tasks/task_prompt.py +3 -3
  334. camel/terminators/__init__.py +2 -2
  335. camel/terminators/base.py +2 -2
  336. camel/terminators/response_terminator.py +2 -2
  337. camel/terminators/token_limit_terminator.py +2 -2
  338. camel/toolkits/__init__.py +58 -10
  339. camel/toolkits/aci_toolkit.py +66 -21
  340. camel/toolkits/arxiv_toolkit.py +8 -8
  341. camel/toolkits/ask_news_toolkit.py +2 -2
  342. camel/toolkits/async_browser_toolkit.py +174 -575
  343. camel/toolkits/audio_analysis_toolkit.py +3 -3
  344. camel/toolkits/base.py +65 -7
  345. camel/toolkits/bohrium_toolkit.py +318 -0
  346. camel/toolkits/browser_toolkit.py +306 -566
  347. camel/toolkits/browser_toolkit_commons.py +568 -0
  348. camel/toolkits/code_execution.py +67 -11
  349. camel/toolkits/context_summarizer_toolkit.py +684 -0
  350. camel/toolkits/craw4ai_toolkit.py +93 -0
  351. camel/toolkits/dappier_toolkit.py +12 -8
  352. camel/toolkits/data_commons_toolkit.py +2 -2
  353. camel/toolkits/dingtalk.py +1135 -0
  354. camel/toolkits/earth_science_toolkit.py +5367 -0
  355. camel/toolkits/edgeone_pages_mcp_toolkit.py +49 -0
  356. camel/toolkits/excel_toolkit.py +910 -70
  357. camel/toolkits/file_toolkit.py +1402 -0
  358. camel/toolkits/function_tool.py +128 -20
  359. camel/toolkits/github_toolkit.py +148 -43
  360. camel/toolkits/gmail_toolkit.py +1839 -0
  361. camel/toolkits/google_calendar_toolkit.py +40 -6
  362. camel/toolkits/google_drive_mcp_toolkit.py +54 -0
  363. camel/toolkits/google_maps_toolkit.py +2 -2
  364. camel/toolkits/google_scholar_toolkit.py +2 -2
  365. camel/toolkits/human_toolkit.py +36 -12
  366. camel/toolkits/hybrid_browser_toolkit/__init__.py +18 -0
  367. camel/toolkits/hybrid_browser_toolkit/config_loader.py +185 -0
  368. camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit.py +246 -0
  369. camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit_ts.py +1973 -0
  370. camel/toolkits/hybrid_browser_toolkit/installer.py +203 -0
  371. camel/toolkits/hybrid_browser_toolkit/ts/package-lock.json +4589 -0
  372. camel/toolkits/hybrid_browser_toolkit/ts/package.json +33 -0
  373. camel/toolkits/hybrid_browser_toolkit/ts/src/browser-scripts.js +125 -0
  374. camel/toolkits/hybrid_browser_toolkit/ts/src/browser-session.ts +1929 -0
  375. camel/toolkits/hybrid_browser_toolkit/ts/src/config-loader.ts +233 -0
  376. camel/toolkits/hybrid_browser_toolkit/ts/src/hybrid-browser-toolkit.ts +589 -0
  377. camel/toolkits/hybrid_browser_toolkit/ts/src/index.ts +7 -0
  378. camel/toolkits/hybrid_browser_toolkit/ts/src/parent-child-filter.ts +226 -0
  379. camel/toolkits/hybrid_browser_toolkit/ts/src/snapshot-parser.ts +219 -0
  380. camel/toolkits/hybrid_browser_toolkit/ts/src/som-screenshot-injected.ts +543 -0
  381. camel/toolkits/hybrid_browser_toolkit/ts/src/types.ts +129 -0
  382. camel/toolkits/hybrid_browser_toolkit/ts/tsconfig.json +27 -0
  383. camel/toolkits/hybrid_browser_toolkit/ts/websocket-server.js +319 -0
  384. camel/toolkits/hybrid_browser_toolkit/ws_wrapper.py +1037 -0
  385. camel/toolkits/hybrid_browser_toolkit_py/__init__.py +17 -0
  386. camel/toolkits/hybrid_browser_toolkit_py/actions.py +575 -0
  387. camel/toolkits/hybrid_browser_toolkit_py/agent.py +311 -0
  388. camel/toolkits/hybrid_browser_toolkit_py/browser_session.py +787 -0
  389. camel/toolkits/hybrid_browser_toolkit_py/config_loader.py +490 -0
  390. camel/toolkits/hybrid_browser_toolkit_py/hybrid_browser_toolkit.py +2390 -0
  391. camel/toolkits/hybrid_browser_toolkit_py/snapshot.py +233 -0
  392. camel/toolkits/hybrid_browser_toolkit_py/stealth_script.js +0 -0
  393. camel/toolkits/hybrid_browser_toolkit_py/unified_analyzer.js +1043 -0
  394. camel/toolkits/image_analysis_toolkit.py +3 -3
  395. camel/toolkits/image_generation_toolkit.py +390 -0
  396. camel/toolkits/jina_reranker_toolkit.py +195 -79
  397. camel/toolkits/klavis_toolkit.py +7 -3
  398. camel/toolkits/linkedin_toolkit.py +2 -2
  399. camel/toolkits/markitdown_toolkit.py +104 -0
  400. camel/toolkits/math_toolkit.py +66 -12
  401. camel/toolkits/mcp_toolkit.py +841 -600
  402. camel/toolkits/memory_toolkit.py +7 -3
  403. camel/toolkits/meshy_toolkit.py +2 -2
  404. camel/toolkits/message_agent_toolkit.py +608 -0
  405. camel/toolkits/message_integration.py +724 -0
  406. camel/toolkits/mineru_toolkit.py +2 -2
  407. camel/toolkits/minimax_mcp_toolkit.py +195 -0
  408. camel/toolkits/networkx_toolkit.py +2 -2
  409. camel/toolkits/note_taking_toolkit.py +277 -0
  410. camel/toolkits/notion_mcp_toolkit.py +224 -0
  411. camel/toolkits/notion_toolkit.py +2 -2
  412. camel/toolkits/open_api_specs/biztoc/__init__.py +2 -2
  413. camel/toolkits/open_api_specs/biztoc/ai-plugin.json +1 -1
  414. camel/toolkits/open_api_specs/coursera/__init__.py +2 -2
  415. camel/toolkits/open_api_specs/create_qr_code/__init__.py +2 -2
  416. camel/toolkits/open_api_specs/klarna/__init__.py +2 -2
  417. camel/toolkits/open_api_specs/nasa_apod/__init__.py +2 -2
  418. camel/toolkits/open_api_specs/outschool/__init__.py +2 -2
  419. camel/toolkits/open_api_specs/outschool/ai-plugin.json +1 -1
  420. camel/toolkits/open_api_specs/outschool/openapi.yaml +1 -1
  421. camel/toolkits/open_api_specs/outschool/paths/__init__.py +2 -2
  422. camel/toolkits/open_api_specs/outschool/paths/get_classes.py +2 -2
  423. camel/toolkits/open_api_specs/outschool/paths/search_teachers.py +2 -2
  424. camel/toolkits/open_api_specs/security_config.py +2 -2
  425. camel/toolkits/open_api_specs/speak/__init__.py +2 -2
  426. camel/toolkits/open_api_specs/web_scraper/__init__.py +2 -2
  427. camel/toolkits/open_api_specs/web_scraper/ai-plugin.json +1 -1
  428. camel/toolkits/open_api_specs/web_scraper/paths/__init__.py +2 -2
  429. camel/toolkits/open_api_specs/web_scraper/paths/scraper.py +2 -2
  430. camel/toolkits/open_api_toolkit.py +2 -2
  431. camel/toolkits/openbb_toolkit.py +7 -3
  432. camel/toolkits/origene_mcp_toolkit.py +56 -0
  433. camel/toolkits/page_script.js +86 -74
  434. camel/toolkits/playwright_mcp_toolkit.py +27 -32
  435. camel/toolkits/pptx_toolkit.py +790 -0
  436. camel/toolkits/pubmed_toolkit.py +2 -2
  437. camel/toolkits/pulse_mcp_search_toolkit.py +2 -2
  438. camel/toolkits/pyautogui_toolkit.py +2 -2
  439. camel/toolkits/reddit_toolkit.py +2 -2
  440. camel/toolkits/resend_toolkit.py +168 -0
  441. camel/toolkits/retrieval_toolkit.py +2 -2
  442. camel/toolkits/screenshot_toolkit.py +213 -0
  443. camel/toolkits/search_toolkit.py +539 -146
  444. camel/toolkits/searxng_toolkit.py +2 -2
  445. camel/toolkits/semantic_scholar_toolkit.py +2 -2
  446. camel/toolkits/slack_toolkit.py +108 -58
  447. camel/toolkits/sql_toolkit.py +712 -0
  448. camel/toolkits/stripe_toolkit.py +2 -2
  449. camel/toolkits/sympy_toolkit.py +3 -3
  450. camel/toolkits/task_planning_toolkit.py +134 -0
  451. camel/toolkits/terminal_toolkit/__init__.py +18 -0
  452. camel/toolkits/terminal_toolkit/terminal_toolkit.py +1070 -0
  453. camel/toolkits/terminal_toolkit/utils.py +532 -0
  454. camel/toolkits/thinking_toolkit.py +3 -3
  455. camel/toolkits/twitter_toolkit.py +8 -3
  456. camel/toolkits/vertex_ai_veo_toolkit.py +590 -0
  457. camel/toolkits/video_analysis_toolkit.py +112 -29
  458. camel/toolkits/video_download_toolkit.py +22 -16
  459. camel/toolkits/weather_toolkit.py +2 -2
  460. camel/toolkits/web_deploy_toolkit.py +1219 -0
  461. camel/toolkits/wechat_official_toolkit.py +483 -0
  462. camel/toolkits/whatsapp_toolkit.py +2 -2
  463. camel/toolkits/wolfram_alpha_toolkit.py +53 -25
  464. camel/toolkits/zapier_toolkit.py +7 -3
  465. camel/types/__init__.py +4 -4
  466. camel/types/agents/__init__.py +2 -2
  467. camel/types/agents/tool_calling_record.py +6 -3
  468. camel/types/enums.py +454 -35
  469. camel/types/mcp_registries.py +2 -2
  470. camel/types/openai_types.py +4 -4
  471. camel/types/unified_model_type.py +43 -6
  472. camel/utils/__init__.py +20 -2
  473. camel/utils/async_func.py +2 -2
  474. camel/utils/chunker/__init__.py +2 -2
  475. camel/utils/chunker/base.py +2 -2
  476. camel/utils/chunker/code_chunker.py +2 -2
  477. camel/utils/chunker/uio_chunker.py +2 -2
  478. camel/utils/commons.py +65 -7
  479. camel/utils/constants.py +5 -2
  480. camel/utils/context_utils.py +1134 -0
  481. camel/utils/deduplication.py +2 -2
  482. camel/utils/filename.py +2 -2
  483. camel/utils/langfuse.py +258 -0
  484. camel/utils/mcp.py +140 -6
  485. camel/utils/mcp_client.py +1056 -0
  486. camel/utils/message_summarizer.py +148 -0
  487. camel/utils/response_format.py +2 -2
  488. camel/utils/token_counting.py +45 -22
  489. camel/utils/tool_result.py +44 -0
  490. camel/verifiers/__init__.py +2 -2
  491. camel/verifiers/base.py +2 -2
  492. camel/verifiers/math_verifier.py +2 -2
  493. camel/verifiers/models.py +2 -2
  494. camel/verifiers/physics_verifier.py +2 -2
  495. camel/verifiers/python_verifier.py +2 -2
  496. {camel_ai-0.2.59.dist-info → camel_ai-0.2.82.dist-info}/METADATA +349 -108
  497. camel_ai-0.2.82.dist-info/RECORD +507 -0
  498. {camel_ai-0.2.59.dist-info → camel_ai-0.2.82.dist-info}/WHEEL +1 -1
  499. {camel_ai-0.2.59.dist-info → camel_ai-0.2.82.dist-info}/licenses/LICENSE +1 -1
  500. camel/loaders/pandas_reader.py +0 -368
  501. camel/runtime/api.py +0 -97
  502. camel/toolkits/dalle_toolkit.py +0 -171
  503. camel/toolkits/file_write_toolkit.py +0 -395
  504. camel/toolkits/openai_agent_toolkit.py +0 -135
  505. camel/toolkits/terminal_toolkit.py +0 -1037
  506. camel_ai-0.2.59.dist-info/RECORD +0 -410
@@ -1,4 +1,4 @@
1
- # ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
1
+ # ========= Copyright 2023-2025 @ CAMEL-AI.org. All Rights Reserved. =========
2
2
  # Licensed under the Apache License, Version 2.0 (the "License");
3
3
  # you may not use this file except in compliance with the License.
4
4
  # You may obtain a copy of the License at
@@ -10,491 +10,221 @@
10
10
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
11
  # See the License for the specific language governing permissions and
12
12
  # limitations under the License.
13
- # ========= Copyright 2023-2024 @ CAMEL-AI.org. All Rights Reserved. =========
14
- import inspect
13
+ # ========= Copyright 2023-2025 @ CAMEL-AI.org. All Rights Reserved. =========
14
+
15
15
  import json
16
16
  import os
17
- import shlex
18
- from contextlib import AsyncExitStack, asynccontextmanager
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
17
+ import warnings
18
+ from contextlib import AsyncExitStack
19
+ from typing import Any, Dict, List, Optional
33
20
 
34
- if TYPE_CHECKING:
35
- from mcp import ClientSession, ListToolsResult, Tool
21
+ from typing_extensions import TypeGuard
36
22
 
37
23
  from camel.logger import get_logger
38
- from camel.toolkits import BaseToolkit, FunctionTool
24
+ from camel.toolkits.base import BaseToolkit
25
+ from camel.toolkits.function_tool import FunctionTool
26
+ from camel.utils.commons import run_async
27
+ from camel.utils.mcp_client import MCPClient, create_mcp_client
39
28
 
40
29
  logger = get_logger(__name__)
41
30
 
31
+ # Suppress parameter description warnings for MCP tools
32
+ warnings.filterwarnings(
33
+ "ignore", message="Parameter description is missing", category=UserWarning
34
+ )
42
35
 
43
- class MCPClient(BaseToolkit):
44
- r"""Internal class that provides an abstraction layer to interact with
45
- external tools using the Model Context Protocol (MCP). It supports three
46
- modes of connection:
47
36
 
48
- 1. stdio mode: Connects via standard input/output streams for local
49
- command-line interactions.
37
+ class MCPConnectionError(Exception):
38
+ r"""Raised when MCP connection fails."""
50
39
 
51
- 2. SSE mode (HTTP Server-Sent Events): Connects via HTTP for persistent,
52
- event-based interactions.
40
+ pass
53
41
 
54
- 3. streamable-http mode: Connects via HTTP for persistent, streamable
55
- interactions.
56
42
 
57
- Connection Lifecycle:
58
- There are three ways to manage the connection lifecycle:
43
+ class MCPToolError(Exception):
44
+ r"""Raised when MCP tool execution fails."""
59
45
 
60
- 1. Using the async context manager:
61
- ```python
62
- async with MCPClient(command_or_url="...") as client:
63
- # Client is connected here
64
- result = await client.some_tool()
65
- # Client is automatically disconnected here
66
- ```
46
+ pass
67
47
 
68
- 2. Using the factory method:
69
- ```python
70
- client = await MCPClient.create(command_or_url="...")
71
- # Client is connected here
72
- result = await client.some_tool()
73
- # Don't forget to disconnect when done!
74
- await client.disconnect()
75
- ```
76
48
 
77
- 3. Using explicit connect/disconnect:
78
- ```python
79
- client = MCPClient(command_or_url="...")
80
- await client.connect()
81
- # Client is connected here
82
- result = await client.some_tool()
83
- # Don't forget to disconnect when done!
84
- await client.disconnect()
85
- ```
49
+ _EMPTY_SCHEMA = {
50
+ "additionalProperties": False,
51
+ "type": "object",
52
+ "properties": {},
53
+ "required": [],
54
+ }
86
55
 
87
56
 
88
- Attributes:
89
- command_or_url (str): URL for SSE mode or command executable for stdio
90
- mode. (default: :obj:`None`)
91
- args (List[str]): List of command-line arguments if stdio mode is used.
92
- (default: :obj:`None`)
93
- env (Dict[str, str]): Environment variables for the stdio mode command.
94
- (default: :obj:`None`)
95
- timeout (Optional[float]): Connection timeout.
96
- (default: :obj:`None`)
97
- headers (Dict[str, str]): Headers for the HTTP request.
98
- (default: :obj:`None`)
99
- strict (Optional[bool]): Whether to enforce strict mode for the
100
- function call. (default: :obj:`False`)
57
+ def ensure_strict_json_schema(schema: dict[str, Any]) -> dict[str, Any]:
58
+ r"""Mutates the given JSON schema to ensure it conforms to the
59
+ `strict` standard that the OpenAI API expects.
101
60
  """
61
+ if schema == {}:
62
+ return _EMPTY_SCHEMA
63
+ return _ensure_strict_json_schema(schema, path=(), root=schema)
64
+
65
+
66
+ def _ensure_strict_json_schema(
67
+ json_schema: object,
68
+ *,
69
+ path: tuple[str, ...],
70
+ root: dict[str, object],
71
+ ) -> dict[str, Any]:
72
+ if not is_dict(json_schema):
73
+ raise TypeError(
74
+ f"Expected {json_schema} to be a dictionary; path={path}"
75
+ )
102
76
 
103
- def __init__(
104
- self,
105
- command_or_url: str,
106
- args: Optional[List[str]] = None,
107
- env: Optional[Dict[str, str]] = None,
108
- timeout: Optional[float] = None,
109
- headers: Optional[Dict[str, str]] = None,
110
- strict: Optional[bool] = False,
111
- ):
112
- from mcp import Tool
113
-
114
- super().__init__(timeout=timeout)
115
-
116
- self.command_or_url = command_or_url
117
- self.args = args or []
118
- self.env = env or {}
119
- self.headers = headers or {}
120
- self.strict = strict
121
-
122
- self._mcp_tools: List[Tool] = []
123
- self._session: Optional['ClientSession'] = None
124
- self._exit_stack = AsyncExitStack()
125
- self._is_connected = False
126
-
127
- async def connect(self):
128
- r"""Explicitly connect to the MCP server.
129
-
130
- Returns:
131
- MCPClient: The client used to connect to the server.
132
- """
133
- from mcp.client.session import ClientSession
134
- from mcp.client.sse import sse_client
135
- from mcp.client.stdio import StdioServerParameters, stdio_client
136
-
137
- if self._is_connected:
138
- logger.warning("Server is already connected")
139
- return self
140
-
141
- try:
142
- if urlparse(self.command_or_url).scheme in ("http", "https"):
143
- (
144
- read_stream,
145
- write_stream,
146
- ) = await self._exit_stack.enter_async_context(
147
- sse_client(
148
- self.command_or_url,
149
- headers=self.headers,
150
- timeout=self.timeout,
151
- )
152
- )
153
- else:
154
- command = self.command_or_url
155
- arguments = self.args
156
- if not self.args:
157
- argv = shlex.split(command)
158
- if not argv:
159
- raise ValueError("Command is empty")
160
-
161
- command = argv[0]
162
- arguments = argv[1:]
163
-
164
- if os.name == "nt" and command.lower() == "npx":
165
- command = "npx.cmd"
166
-
167
- server_parameters = StdioServerParameters(
168
- command=command, args=arguments, env=self.env
169
- )
170
- (
171
- read_stream,
172
- write_stream,
173
- ) = await self._exit_stack.enter_async_context(
174
- stdio_client(server_parameters)
175
- )
176
-
177
- self._session = await self._exit_stack.enter_async_context(
178
- ClientSession(
179
- read_stream,
180
- write_stream,
181
- timedelta(seconds=self.timeout) if self.timeout else None,
182
- )
77
+ defs = json_schema.get("$defs")
78
+ if is_dict(defs):
79
+ for def_name, def_schema in defs.items():
80
+ _ensure_strict_json_schema(
81
+ def_schema, path=(*path, "$defs", def_name), root=root
183
82
  )
184
- await self._session.initialize()
185
- list_tools_result = await self.list_mcp_tools()
186
- self._mcp_tools = list_tools_result.tools
187
- self._is_connected = True
188
- return self
189
- except Exception as e:
190
- # Ensure resources are cleaned up on connection failure
191
- await self.disconnect()
192
- logger.error(f"Failed to connect to MCP server: {e}")
193
- raise e
194
83
 
195
- async def disconnect(self):
196
- r"""Explicitly disconnect from the MCP server."""
197
- # If the server is not connected, do nothing
198
- if not self._is_connected:
199
- return
200
- self._is_connected = False
201
- await self._exit_stack.aclose()
202
- # Reset the exit stack and session for future reuse purposes
203
- self._exit_stack = AsyncExitStack()
204
- self._session = None
205
-
206
- @asynccontextmanager
207
- async def connection(self):
208
- r"""Async context manager for establishing and managing the connection
209
- with the MCP server. Automatically selects SSE or stdio mode based
210
- on the provided `command_or_url`.
211
-
212
- Yields:
213
- MCPClient: Instance with active connection ready for tool
214
- interaction.
215
- """
216
- try:
217
- await self.connect()
218
- yield self
219
- finally:
220
- await self.disconnect()
221
-
222
- async def list_mcp_tools(self) -> Union[str, "ListToolsResult"]:
223
- r"""Retrieves the list of available tools from the connected MCP
224
- server.
225
-
226
- Returns:
227
- ListToolsResult: Result containing available MCP tools.
228
- """
229
- if not self._session:
230
- return "MCP Client is not connected. Call `connection()` first."
231
- try:
232
- return await self._session.list_tools()
233
- except Exception as e:
234
- logger.exception("Failed to list MCP tools")
235
- raise e
236
-
237
- def generate_function_from_mcp_tool(self, mcp_tool: "Tool") -> Callable:
238
- r"""Dynamically generates a Python callable function corresponding to
239
- a given MCP tool.
84
+ definitions = json_schema.get("definitions")
85
+ if is_dict(definitions):
86
+ for definition_name, definition_schema in definitions.items():
87
+ _ensure_strict_json_schema(
88
+ definition_schema,
89
+ path=(*path, "definitions", definition_name),
90
+ root=root,
91
+ )
240
92
 
241
- Args:
242
- mcp_tool (Tool): The MCP tool definition received from the MCP
243
- server.
93
+ typ = json_schema.get("type")
94
+ if typ == "object" and "additionalProperties" not in json_schema:
95
+ json_schema["additionalProperties"] = False
96
+ elif (
97
+ typ == "object"
98
+ and "additionalProperties" in json_schema
99
+ and json_schema["additionalProperties"]
100
+ ):
101
+ raise ValueError(
102
+ "additionalProperties should not be set for object types. This "
103
+ "could be because you're using an older version of Pydantic, or "
104
+ "because you configured additional properties to be allowed. If "
105
+ "you really need this, update the function or output tool "
106
+ "to not use a strict schema."
107
+ )
244
108
 
245
- Returns:
246
- Callable: A dynamically created async Python function that wraps
247
- the MCP tool.
248
- """
249
- func_name = mcp_tool.name
250
- func_desc = mcp_tool.description or "No description provided."
251
- parameters_schema = mcp_tool.inputSchema.get("properties", {})
252
- required_params = mcp_tool.inputSchema.get("required", [])
253
-
254
- type_map = {
255
- "string": str,
256
- "integer": int,
257
- "number": float,
258
- "boolean": bool,
259
- "array": list,
260
- "object": dict,
109
+ # object types
110
+ # { 'type': 'object', 'properties': { 'a': {...} } }
111
+ properties = json_schema.get("properties")
112
+ if is_dict(properties):
113
+ json_schema["required"] = list(properties.keys())
114
+ json_schema["properties"] = {
115
+ key: _ensure_strict_json_schema(
116
+ prop_schema, path=(*path, "properties", key), root=root
117
+ )
118
+ for key, prop_schema in properties.items()
261
119
  }
262
- annotations = {} # used to type hints
263
- defaults: Dict[str, Any] = {} # store default values
264
-
265
- func_params = []
266
- for param_name, param_schema in parameters_schema.items():
267
- param_type = param_schema.get("type", "Any")
268
- param_type = type_map.get(param_type, Any)
269
-
270
- annotations[param_name] = param_type
271
- if param_name not in required_params:
272
- defaults[param_name] = None
273
120
 
274
- func_params.append(param_name)
275
-
276
- async def dynamic_function(**kwargs) -> str:
277
- r"""Auto-generated function for MCP Tool interaction.
278
-
279
- Args:
280
- kwargs: Keyword arguments corresponding to MCP tool parameters.
281
-
282
- Returns:
283
- str: The textual result returned by the MCP tool.
284
- """
285
- from mcp.types import CallToolResult
121
+ # arrays
122
+ # { 'type': 'array', 'items': {...} }
123
+ items = json_schema.get("items")
124
+ if is_dict(items):
125
+ json_schema["items"] = _ensure_strict_json_schema(
126
+ items, path=(*path, "items"), root=root
127
+ )
286
128
 
287
- missing_params: Set[str] = set(required_params) - set(
288
- kwargs.keys()
129
+ # unions
130
+ any_of = json_schema.get("anyOf")
131
+ if is_list(any_of):
132
+ json_schema["anyOf"] = [
133
+ _ensure_strict_json_schema(
134
+ variant, path=(*path, "anyOf", str(i)), root=root
289
135
  )
290
- if missing_params:
291
- logger.warning(
292
- f"Missing required parameters: {missing_params}"
293
- )
294
- return "Missing required parameters."
295
-
296
- if not self._session:
297
- logger.error(
298
- "MCP Client is not connected. Call `connection()` first."
299
- )
300
- raise RuntimeError(
301
- "MCP Client is not connected. Call `connection()` first."
302
- )
303
-
304
- try:
305
- result: CallToolResult = await self._session.call_tool(
306
- func_name, kwargs
307
- )
308
- except Exception as e:
309
- logger.error(f"Failed to call MCP tool '{func_name}': {e!s}")
310
- raise e
311
-
312
- if not result.content or len(result.content) == 0:
313
- return "No data available for this request."
136
+ for i, variant in enumerate(any_of)
137
+ ]
314
138
 
315
- # Handle different content types
316
- try:
317
- content = result.content[0]
318
- if content.type == "text":
319
- return content.text
320
- elif content.type == "image":
321
- # Return image URL or data URI if available
322
- if hasattr(content, "url") and content.url:
323
- return f"Image available at: {content.url}"
324
- return "Image content received (data URI not shown)"
325
- elif content.type == "embedded_resource":
326
- # Return resource information if available
327
- if hasattr(content, "name") and content.name:
328
- return f"Embedded resource: {content.name}"
329
- return "Embedded resource received"
330
- else:
331
- msg = f"Received content of type '{content.type}'"
332
- return f"{msg} which is not fully supported yet."
333
- except (IndexError, AttributeError) as e:
334
- logger.error(
335
- f"Error processing content from MCP tool response: {e!s}"
139
+ # intersections
140
+ all_of = json_schema.get("allOf")
141
+ if is_list(all_of):
142
+ if len(all_of) == 1:
143
+ json_schema.update(
144
+ _ensure_strict_json_schema(
145
+ all_of[0], path=(*path, "allOf", "0"), root=root
336
146
  )
337
- raise e
338
-
339
- dynamic_function.__name__ = func_name
340
- dynamic_function.__doc__ = func_desc
341
- dynamic_function.__annotations__ = annotations
342
-
343
- sig = inspect.Signature(
344
- parameters=[
345
- inspect.Parameter(
346
- name=param,
347
- kind=inspect.Parameter.KEYWORD_ONLY,
348
- default=defaults.get(param, inspect.Parameter.empty),
349
- annotation=annotations[param],
147
+ )
148
+ json_schema.pop("allOf")
149
+ else:
150
+ json_schema["allOf"] = [
151
+ _ensure_strict_json_schema(
152
+ entry, path=(*path, "allOf", str(i)), root=root
350
153
  )
351
- for param in func_params
154
+ for i, entry in enumerate(all_of)
352
155
  ]
353
- )
354
- dynamic_function.__signature__ = sig # type: ignore[attr-defined]
355
-
356
- return dynamic_function
357
-
358
- def _build_tool_schema(self, mcp_tool: "Tool") -> Dict[str, Any]:
359
- input_schema = mcp_tool.inputSchema
360
- properties = input_schema.get("properties", {})
361
- required = input_schema.get("required", [])
362
-
363
- parameters = {
364
- "type": "object",
365
- "properties": properties,
366
- "required": required,
367
- "additionalProperties": False,
368
- }
369
-
370
- return {
371
- "type": "function",
372
- "function": {
373
- "name": mcp_tool.name,
374
- "description": mcp_tool.description
375
- or "No description provided.",
376
- "strict": self.strict,
377
- "parameters": parameters,
378
- },
379
- }
380
-
381
- def get_tools(self) -> List[FunctionTool]:
382
- r"""Returns a list of FunctionTool objects representing the
383
- functions in the toolkit. Each function is dynamically generated
384
- based on the MCP tool definitions received from the server.
385
156
 
386
- Returns:
387
- List[FunctionTool]: A list of FunctionTool objects
388
- representing the functions in the toolkit.
389
- """
390
- return [
391
- FunctionTool(
392
- self.generate_function_from_mcp_tool(mcp_tool),
393
- openai_tool_schema=self._build_tool_schema(mcp_tool),
157
+ # strip `None` defaults as there's no meaningful distinction here
158
+ # the schema will still be `nullable` and the model will default
159
+ # to using `None` anyway
160
+ if json_schema.get("default", None) is None:
161
+ json_schema.pop("default", None)
162
+
163
+ # we can't use `$ref`s if there are also other properties defined, e.g.
164
+ # `{"$ref": "...", "description": "my description"}`
165
+ #
166
+ # so we unravel the ref
167
+ # `{"type": "string", "description": "my description"}`
168
+ ref = json_schema.get("$ref")
169
+ if ref and has_more_than_n_keys(json_schema, 1):
170
+ assert isinstance(ref, str), f"Received non-string $ref - {ref}"
171
+
172
+ resolved = resolve_ref(root=root, ref=ref)
173
+ if not is_dict(resolved):
174
+ raise ValueError(
175
+ f"Expected `$ref: {ref}` to resolved to a dictionary but got "
176
+ f"{resolved}"
394
177
  )
395
- for mcp_tool in self._mcp_tools
396
- ]
397
-
398
- def get_text_tools(self) -> str:
399
- r"""Returns a string containing the descriptions of the tools
400
- in the toolkit.
401
-
402
- Returns:
403
- str: A string containing the descriptions of the tools
404
- in the toolkit.
405
- """
406
- return "\n".join(
407
- f"tool_name: {tool.name}\n"
408
- + f"description: {tool.description or 'No description'}\n"
409
- + f"input Schema: {tool.inputSchema}\n"
410
- for tool in self._mcp_tools
411
- )
412
-
413
- async def call_tool(
414
- self, tool_name: str, tool_args: Dict[str, Any]
415
- ) -> Any:
416
- r"""Calls the specified tool with the provided arguments.
417
178
 
418
- Args:
419
- tool_name (str): Name of the tool to call.
420
- tool_args (Dict[str, Any]): Arguments to pass to the tool
421
- (default: :obj:`{}`).
179
+ # properties from the json schema take priority
180
+ # over the ones on the `$ref`
181
+ json_schema.update({**resolved, **json_schema})
182
+ json_schema.pop("$ref")
183
+ # Since the schema expanded from `$ref` might not
184
+ # have `additionalProperties: false` applied
185
+ # we call `_ensure_strict_json_schema` again to fix the inlined
186
+ # schema and ensure it's valid
187
+ return _ensure_strict_json_schema(json_schema, path=path, root=root)
422
188
 
423
- Returns:
424
- Any: The result of the tool call.
425
- """
426
- if self._session is None:
427
- raise RuntimeError("Session is not initialized.")
189
+ return json_schema
428
190
 
429
- return await self._session.call_tool(tool_name, tool_args)
430
191
 
431
- @property
432
- def session(self) -> Optional["ClientSession"]:
433
- return self._session
192
+ def resolve_ref(*, root: dict[str, object], ref: str) -> object:
193
+ if not ref.startswith("#/"):
194
+ raise ValueError(
195
+ f"Unexpected $ref format {ref!r}; Does not start with #/"
196
+ )
434
197
 
435
- @classmethod
436
- async def create(
437
- cls,
438
- command_or_url: str,
439
- args: Optional[List[str]] = None,
440
- env: Optional[Dict[str, str]] = None,
441
- timeout: Optional[float] = None,
442
- headers: Optional[Dict[str, str]] = None,
443
- ) -> "MCPClient":
444
- r"""Factory method that creates and connects to the MCP server.
198
+ path = ref[2:].split("/")
199
+ resolved = root
200
+ for key in path:
201
+ value = resolved[key]
202
+ assert is_dict(value), (
203
+ f"encountered non-dictionary entry while resolving {ref} - "
204
+ f"{resolved}"
205
+ )
206
+ resolved = value
445
207
 
446
- This async factory method ensures the connection to the MCP server is
447
- established before the client object is fully constructed.
208
+ return resolved
448
209
 
449
- Args:
450
- command_or_url (str): URL for SSE mode or command executable
451
- for stdio mode.
452
- args (Optional[List[str]]): List of command-line arguments if
453
- stdio mode is used. (default: :obj:`None`)
454
- env (Optional[Dict[str, str]]): Environment variables for
455
- the stdio mode command. (default: :obj:`None`)
456
- timeout (Optional[float]): Connection timeout.
457
- (default: :obj:`None`)
458
- headers (Optional[Dict[str, str]]): Headers for the HTTP request.
459
- (default: :obj:`None`)
460
210
 
461
- Returns:
462
- MCPClient: A fully initialized and connected MCPClient instance.
211
+ def is_dict(obj: object) -> TypeGuard[dict[str, object]]:
212
+ # just pretend that we know there are only `str` keys
213
+ # as that check is not worth the performance cost
214
+ return isinstance(obj, dict)
463
215
 
464
- Raises:
465
- RuntimeError: If connection to the MCP server fails.
466
- """
467
- client = cls(
468
- command_or_url=command_or_url,
469
- args=args,
470
- env=env,
471
- timeout=timeout,
472
- headers=headers,
473
- )
474
- try:
475
- await client.connect()
476
- return client
477
- except Exception as e:
478
- # Ensure cleanup on initialization failure
479
- await client.disconnect()
480
- logger.error(f"Failed to initialize MCPClient: {e}")
481
- raise RuntimeError(f"Failed to initialize MCPClient: {e}") from e
482
216
 
483
- async def __aenter__(self) -> "MCPClient":
484
- r"""Async context manager entry point. Automatically connects to the
485
- MCP server when used in an async with statement.
217
+ def is_list(obj: object) -> TypeGuard[list[object]]:
218
+ return isinstance(obj, list)
486
219
 
487
- Returns:
488
- MCPClient: Self with active connection.
489
- """
490
- await self.connect()
491
- return self
492
220
 
493
- async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
494
- r"""Async context manager exit point. Automatically disconnects from
495
- the MCP server when exiting an async with statement.
496
- """
497
- await self.disconnect()
221
+ def has_more_than_n_keys(obj: dict[str, object], n: int) -> bool:
222
+ i = 0
223
+ for _ in obj.keys():
224
+ i += 1
225
+ if i > n:
226
+ return True
227
+ return False
498
228
 
499
229
 
500
230
  class MCPToolkit(BaseToolkit):
@@ -503,56 +233,69 @@ class MCPToolkit(BaseToolkit):
503
233
 
504
234
  This class handles the lifecycle of multiple MCP server connections and
505
235
  offers a centralized configuration mechanism for both local and remote
506
- MCP services.
236
+ MCP services. The toolkit manages multiple :obj:`MCPClient` instances and
237
+ aggregates their tools into a unified interface compatible with the CAMEL
238
+ framework.
507
239
 
508
240
  Connection Lifecycle:
509
241
  There are three ways to manage the connection lifecycle:
510
242
 
511
- 1. Using the async context manager:
512
- ```python
513
- async with MCPToolkit(config_path="config.json") as toolkit:
514
- # Toolkit is connected here
515
- tools = toolkit.get_tools()
516
- # Toolkit is automatically disconnected here
517
- ```
243
+ 1. Using the async context manager (recommended):
244
+
245
+ .. code-block:: python
246
+
247
+ async with MCPToolkit(config_path="config.json") as toolkit:
248
+ # Toolkit is connected here
249
+ tools = toolkit.get_tools()
250
+ # Toolkit is automatically disconnected here
518
251
 
519
252
  2. Using the factory method:
520
- ```python
521
- toolkit = await MCPToolkit.create(config_path="config.json")
522
- # Toolkit is connected here
523
- tools = toolkit.get_tools()
524
- # Don't forget to disconnect when done!
525
- await toolkit.disconnect()
526
- ```
253
+
254
+ .. code-block:: python
255
+
256
+ toolkit = await MCPToolkit.create(config_path="config.json")
257
+ # Toolkit is connected here
258
+ tools = toolkit.get_tools()
259
+ # Don't forget to disconnect when done!
260
+ await toolkit.disconnect()
527
261
 
528
262
  3. Using explicit connect/disconnect:
529
- ```python
530
- toolkit = MCPToolkit(config_path="config.json")
531
- await toolkit.connect()
532
- # Toolkit is connected here
533
- tools = toolkit.get_tools()
534
- # Don't forget to disconnect when done!
535
- await toolkit.disconnect()
536
- ```
263
+
264
+ .. code-block:: python
265
+
266
+ toolkit = MCPToolkit(config_path="config.json")
267
+ await toolkit.connect()
268
+ # Toolkit is connected here
269
+ tools = toolkit.get_tools()
270
+ # Don't forget to disconnect when done!
271
+ await toolkit.disconnect()
272
+
273
+ Note:
274
+ Both MCPClient and MCPToolkit now use the same async context manager
275
+ pattern for consistent connection management. MCPToolkit automatically
276
+ manages multiple MCPClient instances using AsyncExitStack.
537
277
 
538
278
  Args:
539
- servers (Optional[List[MCPClient]]): List of MCPClient
279
+ clients (Optional[List[MCPClient]], optional): List of :obj:`MCPClient`
540
280
  instances to manage. (default: :obj:`None`)
541
- config_path (Optional[str]): Path to a JSON configuration file
542
- defining MCP servers. (default: :obj:`None`)
543
- config_dict (Optional[Dict[str, Any]]): Dictionary containing MCP
544
- server configurations in the same format as the config file.
281
+ config_path (Optional[str], optional): Path to a JSON configuration
282
+ file defining MCP servers. The file should contain server
283
+ configurations in the standard MCP format. (default: :obj:`None`)
284
+ config_dict (Optional[Dict[str, Any]], optional): Dictionary containing
285
+ MCP server configurations in the same format as the config file.
286
+ This allows for programmatic configuration without file I/O.
287
+ (default: :obj:`None`)
288
+ timeout (Optional[float], optional): Timeout for connection attempts
289
+ in seconds. This timeout applies to individual client connections.
545
290
  (default: :obj:`None`)
546
- strict (Optional[bool]): Whether to enforce strict mode for the
547
- function call. (default: :obj:`False`)
548
291
 
549
292
  Note:
550
- Either `servers`, `config_path`, or `config_dict` must be provided.
551
- If multiple are provided, servers from all sources will be combined.
293
+ At least one of :obj:`clients`, :obj:`config_path`, or
294
+ :obj:`config_dict` must be provided. If multiple sources are provided,
295
+ clients from all sources will be combined.
552
296
 
553
- For web servers in the config, you can specify authorization
554
- headers using the "headers" field to connect to protected MCP server
555
- endpoints.
297
+ For web servers in the config, you can specify authorization headers
298
+ using the "headers" field to connect to protected MCP server endpoints.
556
299
 
557
300
  Example configuration:
558
301
 
@@ -560,6 +303,11 @@ class MCPToolkit(BaseToolkit):
560
303
 
561
304
  {
562
305
  "mcpServers": {
306
+ "filesystem": {
307
+ "command": "npx",
308
+ "args": ["-y", "@modelcontextprotocol/server-filesystem",
309
+ "/path"]
310
+ },
563
311
  "protected-server": {
564
312
  "url": "https://example.com/mcp",
565
313
  "timeout": 30,
@@ -572,189 +320,682 @@ class MCPToolkit(BaseToolkit):
572
320
  }
573
321
 
574
322
  Attributes:
575
- servers (List[MCPClient]): List of MCPClient instances being managed.
323
+ clients (List[MCPClient]): List of :obj:`MCPClient` instances being
324
+ managed by this toolkit.
325
+
326
+ Raises:
327
+ ValueError: If no configuration sources are provided or if the
328
+ configuration is invalid.
329
+ MCPConnectionError: If connection to any MCP server fails during
330
+ initialization.
576
331
  """
577
332
 
578
333
  def __init__(
579
334
  self,
580
- servers: Optional[List[MCPClient]] = None,
335
+ clients: Optional[List[MCPClient]] = None,
581
336
  config_path: Optional[str] = None,
582
337
  config_dict: Optional[Dict[str, Any]] = None,
583
- strict: Optional[bool] = False,
338
+ timeout: Optional[float] = None,
584
339
  ):
585
- super().__init__()
340
+ # Call parent constructor first
341
+ super().__init__(timeout=timeout)
586
342
 
343
+ # Validate input parameters
587
344
  sources_provided = sum(
588
- 1 for src in [servers, config_path, config_dict] if src is not None
345
+ 1 for src in [clients, config_path, config_dict] if src is not None
589
346
  )
590
- if sources_provided > 1:
591
- logger.warning(
592
- "Multiple configuration sources provided "
593
- f"({sources_provided}). Servers from all sources "
594
- "will be combined."
347
+ if sources_provided == 0:
348
+ error_msg = (
349
+ "At least one of clients, config_path, or "
350
+ "config_dict must be provided"
595
351
  )
352
+ raise ValueError(error_msg)
596
353
 
597
- self.servers: List[MCPClient] = servers or []
354
+ self.clients: List[MCPClient] = clients or []
355
+ self._is_connected = False
356
+ self._exit_stack: Optional[AsyncExitStack] = None
598
357
 
358
+ # Load clients from config sources
599
359
  if config_path:
600
- self.servers.extend(
601
- self._load_servers_from_config(config_path, strict)
602
- )
360
+ self.clients.extend(self._load_clients_from_config(config_path))
603
361
 
604
362
  if config_dict:
605
- self.servers.extend(self._load_servers_from_dict(config_dict))
363
+ self.clients.extend(self._load_clients_from_dict(config_dict))
606
364
 
607
- self._connected = False
365
+ if not self.clients:
366
+ raise ValueError("No valid MCP clients could be created")
608
367
 
609
- def _load_servers_from_config(
610
- self, config_path: str, strict: Optional[bool] = False
611
- ) -> List[MCPClient]:
612
- r"""Loads MCP server configurations from a JSON file.
368
+ async def connect(self) -> "MCPToolkit":
369
+ r"""Connect to all MCP servers using AsyncExitStack.
613
370
 
614
- Args:
615
- config_path (str): Path to the JSON configuration file.
616
- strict (bool): Whether to enforce strict mode for the
617
- function call. (default: :obj:`False`)
371
+ Establishes connections to all configured MCP servers sequentially.
372
+ Uses :obj:`AsyncExitStack` to manage the lifecycle of all connections,
373
+ ensuring proper cleanup on exit or error.
618
374
 
619
375
  Returns:
620
- List[MCPClient]: List of configured MCPClient instances.
376
+ MCPToolkit: Returns :obj:`self` for method chaining, allowing for
377
+ fluent interface usage.
378
+
379
+ Raises:
380
+ MCPConnectionError: If connection to any MCP server fails. The
381
+ error message will include details about which client failed
382
+ to connect and the underlying error reason.
383
+
384
+ Warning:
385
+ If any client fails to connect, all previously established
386
+ connections will be automatically cleaned up before raising
387
+ the exception.
388
+
389
+ Example:
390
+ .. code-block:: python
391
+
392
+ toolkit = MCPToolkit(config_dict=config)
393
+ try:
394
+ await toolkit.connect()
395
+ # Use the toolkit
396
+ tools = toolkit.get_tools()
397
+ finally:
398
+ await toolkit.disconnect()
621
399
  """
400
+ if self._is_connected:
401
+ logger.warning("MCPToolkit is already connected")
402
+ return self
403
+
404
+ self._exit_stack = AsyncExitStack()
405
+
622
406
  try:
623
- with open(config_path, "r", encoding="utf-8") as f:
624
- try:
625
- data = json.load(f)
626
- except json.JSONDecodeError as e:
627
- logger.warning(
628
- f"Invalid JSON in config file '{config_path}': {e!s}"
629
- )
630
- raise e
631
- except FileNotFoundError as e:
632
- logger.warning(f"Config file not found: '{config_path}'")
633
- raise e
407
+ # Apply timeout to the entire connection process
408
+ import asyncio
634
409
 
635
- return self._load_servers_from_dict(config=data, strict=strict)
410
+ timeout_seconds = self.timeout or 30.0
411
+ await asyncio.wait_for(
412
+ self._connect_all_clients(), timeout=timeout_seconds
413
+ )
636
414
 
637
- def _load_servers_from_dict(
638
- self, config: Dict[str, Any], strict: Optional[bool] = False
639
- ) -> List[MCPClient]:
640
- r"""Loads MCP server configurations from a dictionary.
415
+ self._is_connected = True
416
+ msg = f"Successfully connected to {len(self.clients)} MCP servers"
417
+ logger.info(msg)
418
+ return self
641
419
 
642
- Args:
643
- config (Dict[str, Any]): Dictionary containing server
644
- configurations.
645
- strict (bool): Whether to enforce strict mode for the
646
- function call. (default: :obj:`False`)
420
+ except (asyncio.TimeoutError, asyncio.CancelledError):
421
+ self._is_connected = False
422
+ if self._exit_stack:
423
+ await self._exit_stack.aclose()
424
+ self._exit_stack = None
425
+
426
+ timeout_seconds = self.timeout or 30.0
427
+ error_msg = (
428
+ f"Connection timeout after {timeout_seconds}s. "
429
+ f"One or more MCP servers are not responding. "
430
+ f"Please check if the servers are running and accessible."
431
+ )
432
+ logger.error(error_msg)
433
+ raise MCPConnectionError(error_msg)
434
+
435
+ except Exception:
436
+ self._is_connected = False
437
+ if self._exit_stack:
438
+ await self._exit_stack.aclose()
439
+ self._exit_stack = None
440
+ raise
441
+
442
+ async def _connect_all_clients(self):
443
+ r"""Connect to all clients sequentially."""
444
+ # Connect to all clients using AsyncExitStack
445
+ for i, client in enumerate(self.clients):
446
+ try:
447
+ # Use MCPClient directly as async context manager
448
+ await self._exit_stack.enter_async_context(client)
449
+ msg = f"Connected to client {i+1}/{len(self.clients)}"
450
+ logger.debug(msg)
451
+ except Exception as e:
452
+ logger.error(f"Failed to connect to client {i+1}: {e}")
453
+ # AsyncExitStack will cleanup already connected clients
454
+ await self._exit_stack.aclose()
455
+ self._exit_stack = None
456
+ error_msg = f"Failed to connect to client {i+1}: {e}"
457
+ raise MCPConnectionError(error_msg) from e
458
+
459
+ async def disconnect(self):
460
+ r"""Disconnect from all MCP servers."""
461
+ if not self._is_connected:
462
+ return
463
+
464
+ if self._exit_stack:
465
+ try:
466
+ await self._exit_stack.aclose()
467
+ except Exception as e:
468
+ logger.warning(f"Error during disconnect: {e}")
469
+ finally:
470
+ self._exit_stack = None
471
+
472
+ self._is_connected = False
473
+ logger.debug("Disconnected from all MCP servers")
474
+
475
+ @property
476
+ def is_connected(self) -> bool:
477
+ r"""Check if toolkit is connected.
647
478
 
648
479
  Returns:
649
- List[MCPClient]: List of configured MCPClient instances.
480
+ bool: True if the toolkit is connected to all MCP servers,
481
+ False otherwise.
650
482
  """
651
- all_servers = []
483
+ if not self._is_connected:
484
+ return False
652
485
 
653
- mcp_servers = config.get("mcpServers", {})
654
- if not isinstance(mcp_servers, dict):
655
- logger.warning("'mcpServers' is not a dictionary, skipping...")
656
- mcp_servers = {}
486
+ # Check if all clients are connected
487
+ return all(client.is_connected() for client in self.clients)
657
488
 
658
- for name, cfg in mcp_servers.items():
659
- if not isinstance(cfg, dict):
660
- logger.warning(
661
- f"Configuration for server '{name}' must be a dictionary"
662
- )
663
- continue
489
+ def connect_sync(self):
490
+ r"""Synchronously connect to all MCP servers."""
491
+ return run_async(self.connect)()
664
492
 
665
- if "command" not in cfg and "url" not in cfg:
666
- logger.warning(
667
- f"Missing required 'command' or 'url' field for server "
668
- f"'{name}'"
669
- )
670
- continue
493
+ def disconnect_sync(self):
494
+ r"""Synchronously disconnect from all MCP servers."""
495
+ return run_async(self.disconnect)()
671
496
 
672
- # Include headers if provided in the configuration
673
- headers = cfg.get("headers", {})
674
-
675
- cmd_or_url = cast(str, cfg.get("command") or cfg.get("url"))
676
- server = MCPClient(
677
- command_or_url=cmd_or_url,
678
- args=cfg.get("args", []),
679
- env={**os.environ, **cfg.get("env", {})},
680
- timeout=cfg.get("timeout", None),
681
- headers=headers,
682
- strict=strict,
683
- )
684
- all_servers.append(server)
497
+ async def __aenter__(self) -> "MCPToolkit":
498
+ r"""Async context manager entry point.
499
+
500
+ Usage:
501
+ async with MCPToolkit(config_dict=config) as toolkit:
502
+ tools = toolkit.get_tools()
503
+ """
504
+ await self.connect()
505
+ return self
506
+
507
+ def __enter__(self) -> "MCPToolkit":
508
+ r"""Synchronously enter the async context manager."""
509
+ return run_async(self.__aenter__)()
510
+
511
+ async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
512
+ r"""Async context manager exit point."""
513
+ await self.disconnect()
514
+ return None
515
+
516
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
517
+ r"""Synchronously exit the async context manager."""
518
+ return run_async(self.__aexit__)(exc_type, exc_val, exc_tb)
685
519
 
686
- return all_servers
520
+ @classmethod
521
+ async def create(
522
+ cls,
523
+ clients: Optional[List[MCPClient]] = None,
524
+ config_path: Optional[str] = None,
525
+ config_dict: Optional[Dict[str, Any]] = None,
526
+ timeout: Optional[float] = None,
527
+ ) -> "MCPToolkit":
528
+ r"""Factory method that creates and connects to all MCP servers.
529
+
530
+ Creates a new :obj:`MCPToolkit` instance and automatically establishes
531
+ connections to all configured MCP servers. This is a convenience method
532
+ that combines instantiation and connection in a single call.
687
533
 
688
- async def connect(self):
689
- r"""Explicitly connect to all MCP servers.
534
+ Args:
535
+ clients (Optional[List[MCPClient]], optional): List of
536
+ :obj:`MCPClient` instances to manage. (default: :obj:`None`)
537
+ config_path (Optional[str], optional): Path to a JSON configuration
538
+ file defining MCP servers. (default: :obj:`None`)
539
+ config_dict (Optional[Dict[str, Any]], optional): Dictionary
540
+ containing MCP server configurations in the same format as the
541
+ config file. (default: :obj:`None`)
542
+ timeout (Optional[float], optional): Timeout for connection
543
+ attempts in seconds. (default: :obj:`None`)
690
544
 
691
545
  Returns:
692
- MCPToolkit: The connected toolkit instance
546
+ MCPToolkit: A fully initialized and connected :obj:`MCPToolkit`
547
+ instance with all servers ready for use.
548
+
549
+ Raises:
550
+ MCPConnectionError: If connection to any MCP server fails during
551
+ initialization. All successfully connected servers will be
552
+ properly disconnected before raising the exception.
553
+ ValueError: If no configuration sources are provided or if the
554
+ configuration is invalid.
555
+
556
+ Example:
557
+ .. code-block:: python
558
+
559
+ # Create and connect in one step
560
+ toolkit = await MCPToolkit.create(config_path="servers.json")
561
+ try:
562
+ tools = toolkit.get_tools()
563
+ # Use the toolkit...
564
+ finally:
565
+ await toolkit.disconnect()
693
566
  """
694
- if self._connected:
695
- logger.warning("MCPToolkit is already connected")
696
- return self
567
+ toolkit = cls(
568
+ clients=clients,
569
+ config_path=config_path,
570
+ config_dict=config_dict,
571
+ timeout=timeout,
572
+ )
573
+ try:
574
+ await toolkit.connect()
575
+ return toolkit
576
+ except Exception as e:
577
+ # Ensure cleanup on initialization failure
578
+ await toolkit.disconnect()
579
+ logger.error(f"Failed to initialize MCPToolkit: {e}")
580
+ raise MCPConnectionError(
581
+ f"Failed to initialize MCPToolkit: {e}"
582
+ ) from e
583
+
584
+ @classmethod
585
+ def create_sync(
586
+ cls,
587
+ clients: Optional[List[MCPClient]] = None,
588
+ config_path: Optional[str] = None,
589
+ config_dict: Optional[Dict[str, Any]] = None,
590
+ timeout: Optional[float] = None,
591
+ ) -> "MCPToolkit":
592
+ r"""Synchronously create and connect to all MCP servers."""
593
+ return run_async(cls.create)(
594
+ clients, config_path, config_dict, timeout
595
+ )
596
+
597
+ def _load_clients_from_config(self, config_path: str) -> List[MCPClient]:
598
+ r"""Load clients from configuration file."""
599
+ if not os.path.exists(config_path):
600
+ raise FileNotFoundError(f"Config file not found: '{config_path}'")
697
601
 
698
602
  try:
699
- # Sequentially connect to each server
700
- for server in self.servers:
701
- await server.connect()
702
- self._connected = True
703
- return self
603
+ with open(config_path, "r", encoding="utf-8") as f:
604
+ data = json.load(f)
605
+ except json.JSONDecodeError as e:
606
+ error_msg = f"Invalid JSON in config file '{config_path}': {e}"
607
+ raise ValueError(error_msg) from e
704
608
  except Exception as e:
705
- # Ensure resources are cleaned up on connection failure
706
- await self.disconnect()
707
- logger.error(f"Failed to connect to one or more MCP servers: {e}")
708
- raise e
609
+ error_msg = f"Error reading config file '{config_path}': {e}"
610
+ raise IOError(error_msg) from e
709
611
 
710
- async def disconnect(self):
711
- r"""Explicitly disconnect from all MCP servers."""
712
- if not self._connected:
713
- return
612
+ return self._load_clients_from_dict(data)
613
+
614
+ def _load_clients_from_dict(
615
+ self, config: Dict[str, Any]
616
+ ) -> List[MCPClient]:
617
+ r"""Load clients from configuration dictionary."""
618
+ if not isinstance(config, dict):
619
+ raise ValueError("Config must be a dictionary")
620
+
621
+ mcp_servers = config.get("mcpServers", {})
622
+ if not isinstance(mcp_servers, dict):
623
+ raise ValueError("'mcpServers' must be a dictionary")
624
+
625
+ clients = []
626
+
627
+ for name, cfg in mcp_servers.items():
628
+ try:
629
+ if "timeout" not in cfg and self.timeout is not None:
630
+ cfg["timeout"] = self.timeout
631
+
632
+ client = self._create_client_from_config(name, cfg)
633
+ clients.append(client)
634
+ except Exception as e:
635
+ logger.error(f"Failed to create client for '{name}': {e}")
636
+ error_msg = f"Invalid configuration for server '{name}': {e}"
637
+ raise ValueError(error_msg) from e
714
638
 
715
- for server in self.servers:
716
- await server.disconnect()
717
- self._connected = False
639
+ return clients
718
640
 
719
- @asynccontextmanager
720
- async def connection(self) -> AsyncGenerator["MCPToolkit", None]:
721
- r"""Async context manager that simultaneously establishes connections
722
- to all managed MCP server instances.
641
+ def _create_client_from_config(
642
+ self, name: str, cfg: Dict[str, Any]
643
+ ) -> MCPClient:
644
+ r"""Create a single MCP client from configuration."""
645
+ if not isinstance(cfg, dict):
646
+ error_msg = f"Configuration for server '{name}' must be a dict"
647
+ raise ValueError(error_msg)
648
+
649
+ try:
650
+ # Use the new mcp_client factory function
651
+ # Pass timeout from toolkit if available
652
+ kwargs = {}
653
+ if hasattr(self, "timeout") and self.timeout is not None:
654
+ kwargs["timeout"] = self.timeout
723
655
 
724
- Yields:
725
- MCPToolkit: Self with all servers connected.
656
+ client = create_mcp_client(cfg, **kwargs)
657
+ return client
658
+ except Exception as e:
659
+ error_msg = f"Failed to create client for server '{name}': {e}"
660
+ raise ValueError(error_msg) from e
661
+
662
+ def _ensure_strict_tool_schema(self, tool: FunctionTool) -> FunctionTool:
663
+ r"""Ensure a tool has a strict schema compatible with
664
+ OpenAI's requirements.
665
+
666
+ Strategy:
667
+ - Ensure parameters exist with at least an empty properties object
668
+ (OpenAI requirement).
669
+ - Try converting parameters to strict using ensure_strict_json_schema.
670
+ - If conversion fails, mark function.strict = False and
671
+ keep best-effort parameters.
726
672
  """
727
673
  try:
728
- await self.connect()
729
- yield self
730
- finally:
731
- await self.disconnect()
674
+ schema = tool.get_openai_tool_schema()
675
+
676
+ def _has_strict_mode_incompatible_features(json_schema):
677
+ r"""Check if schema has features incompatible
678
+ with OpenAI strict mode."""
679
+
680
+ def _check_incompatible(obj, path=""):
681
+ if not isinstance(obj, dict):
682
+ return False
683
+
684
+ # Check for allOf in array items (known to cause issues)
685
+ if "items" in obj and isinstance(obj["items"], dict):
686
+ items_schema = obj["items"]
687
+ if "allOf" in items_schema:
688
+ logger.debug(
689
+ f"Found allOf in array items at {path}"
690
+ )
691
+ return True
692
+ # Recursively check items schema
693
+ if _check_incompatible(items_schema, f"{path}.items"):
694
+ return True
695
+
696
+ # Check for other potentially problematic patterns
697
+ # anyOf/oneOf in certain contexts can also cause issues
698
+ if (
699
+ "anyOf" in obj and len(obj["anyOf"]) > 10
700
+ ): # Large unions can be problematic
701
+ return True
702
+
703
+ # Recursively check nested objects
704
+ for key in [
705
+ "properties",
706
+ "additionalProperties",
707
+ "patternProperties",
708
+ ]:
709
+ if key in obj and isinstance(obj[key], dict):
710
+ if key == "properties":
711
+ for prop_name, prop_schema in obj[key].items():
712
+ if isinstance(
713
+ prop_schema, dict
714
+ ) and _check_incompatible(
715
+ prop_schema,
716
+ f"{path}.{key}.{prop_name}",
717
+ ):
718
+ return True
719
+ elif _check_incompatible(
720
+ obj[key], f"{path}.{key}"
721
+ ):
722
+ return True
723
+
724
+ # Check arrays and unions
725
+ for key in ["allOf", "anyOf", "oneOf"]:
726
+ if key in obj and isinstance(obj[key], list):
727
+ for i, item in enumerate(obj[key]):
728
+ if isinstance(
729
+ item, dict
730
+ ) and _check_incompatible(
731
+ item, f"{path}.{key}[{i}]"
732
+ ):
733
+ return True
734
+
735
+ return False
736
+
737
+ return _check_incompatible(json_schema)
738
+
739
+ # Apply sanitization if available
740
+ if "function" in schema:
741
+ try:
742
+ from camel.toolkits.function_tool import (
743
+ sanitize_and_enforce_required,
744
+ )
732
745
 
733
- def is_connected(self) -> bool:
734
- r"""Checks if all the managed servers are connected.
746
+ schema = sanitize_and_enforce_required(schema)
747
+ except ImportError:
748
+ logger.debug("sanitize_and_enforce_required not available")
749
+
750
+ parameters = schema["function"].get("parameters", {})
751
+ if not parameters:
752
+ # Empty parameters - use minimal valid schema
753
+ parameters = {
754
+ "type": "object",
755
+ "properties": {},
756
+ "additionalProperties": False,
757
+ }
758
+ schema["function"]["parameters"] = parameters
759
+
760
+ # MCP spec doesn't require 'properties', but OpenAI spec does
761
+ if (
762
+ parameters.get("type") == "object"
763
+ and "properties" not in parameters
764
+ ):
765
+ parameters["properties"] = {}
735
766
 
736
- Returns:
737
- bool: True if connected, False otherwise.
738
- """
739
- return self._connected
767
+ try:
768
+ # _check_schema_limits(parameters)
769
+
770
+ # Check for OpenAI strict mode incompatible features
771
+ if _has_strict_mode_incompatible_features(parameters):
772
+ raise ValueError(
773
+ "Schema contains features "
774
+ "incompatible with strict mode"
775
+ )
776
+
777
+ strict_params = ensure_strict_json_schema(parameters)
778
+ schema["function"]["parameters"] = strict_params
779
+ schema["function"]["strict"] = True
780
+ except Exception as e:
781
+ # Fallback to non-strict mode on any failure
782
+ schema["function"]["strict"] = False
783
+ logger.warning(
784
+ f"Tool '{tool.get_function_name()}' "
785
+ f"cannot use strict mode: {e}"
786
+ )
787
+
788
+ tool.set_openai_tool_schema(schema)
789
+
790
+ except Exception as e:
791
+ # Final fallback - ensure tool still works
792
+ try:
793
+ current_schema = tool.get_openai_tool_schema()
794
+ if "function" in current_schema:
795
+ current_schema["function"]["strict"] = False
796
+ tool.set_openai_tool_schema(current_schema)
797
+ logger.warning(
798
+ f"Error processing schema for tool "
799
+ f"'{tool.get_function_name()}': {str(e)[:100]}. "
800
+ f"Using non-strict mode."
801
+ )
802
+ except Exception as inner_e:
803
+ logger.error(
804
+ f"Critical error processing tool "
805
+ f"'{tool.get_function_name()}': {inner_e}. "
806
+ f"Tool may not function correctly."
807
+ )
808
+
809
+ return tool
740
810
 
741
811
  def get_tools(self) -> List[FunctionTool]:
742
- r"""Aggregates all tools from the managed MCP server instances.
812
+ r"""Aggregates all tools from the managed MCP client instances.
813
+
814
+ Collects and combines tools from all connected MCP clients into a
815
+ single unified list. Each tool is converted to a CAMEL-compatible
816
+ :obj:`FunctionTool` that can be used with CAMEL agents. All tools
817
+ are ensured to have strict schemas compatible with OpenAI's
818
+ requirements.
743
819
 
744
820
  Returns:
745
- List[FunctionTool]: Combined list of all available function tools.
821
+ List[FunctionTool]: Combined list of all available function tools
822
+ from all connected MCP servers with strict schemas. Returns an
823
+ empty list if no clients are connected or if no tools are
824
+ available.
825
+
826
+ Note:
827
+ This method can be called even when the toolkit is not connected,
828
+ but it will log a warning and may return incomplete results.
829
+ For best results, ensure the toolkit is connected before calling
830
+ this method.
831
+
832
+ Example:
833
+ .. code-block:: python
834
+
835
+ async with MCPToolkit(config_dict=config) as toolkit:
836
+ tools = toolkit.get_tools()
837
+ print(f"Available tools: {len(tools)}")
838
+ for tool in tools:
839
+ print(f" - {tool.func.__name__}")
746
840
  """
841
+ if not self.is_connected:
842
+ logger.warning(
843
+ "MCPToolkit is not connected. "
844
+ "Tools may not be available until connected."
845
+ )
846
+
747
847
  all_tools = []
748
- for server in self.servers:
749
- all_tools.extend(server.get_tools())
848
+ seen_names: set[str] = set()
849
+ for i, client in enumerate(self.clients):
850
+ try:
851
+ client_tools = client.get_tools()
852
+
853
+ # Ensure all tools have strict schemas
854
+ strict_tools = []
855
+ for tool in client_tools:
856
+ strict_tool = self._ensure_strict_tool_schema(tool)
857
+ name = strict_tool.get_function_name()
858
+ if name in seen_names:
859
+ logger.warning(
860
+ f"Duplicate tool name detected and "
861
+ f"skipped: '{name}' from client {i+1}"
862
+ )
863
+ continue
864
+ seen_names.add(name)
865
+ strict_tools.append(strict_tool)
866
+
867
+ all_tools.extend(strict_tools)
868
+ logger.debug(
869
+ f"Client {i+1} contributed {len(strict_tools)} "
870
+ f"tools (strict mode enabled)"
871
+ )
872
+ except Exception as e:
873
+ logger.error(f"Failed to get tools from client {i+1}: {e}")
874
+
875
+ logger.info(
876
+ f"Total tools available: {len(all_tools)} (all with strict "
877
+ f"schemas)"
878
+ )
750
879
  return all_tools
751
880
 
752
881
  def get_text_tools(self) -> str:
753
- r"""Returns a string containing the descriptions of the tools
754
- in the toolkit.
882
+ r"""Returns a string containing the descriptions of the tools.
883
+
884
+ Returns:
885
+ str: A string containing the descriptions of all tools.
886
+ """
887
+ if not self.is_connected:
888
+ logger.warning(
889
+ "MCPToolkit is not connected. "
890
+ "Tool descriptions may not be available until connected."
891
+ )
892
+
893
+ tool_descriptions = []
894
+ for i, client in enumerate(self.clients):
895
+ try:
896
+ client_tools_text = client.get_text_tools()
897
+ if client_tools_text:
898
+ tool_descriptions.append(
899
+ f"=== Client {i+1} Tools ===\n{client_tools_text}"
900
+ )
901
+ except Exception as e:
902
+ logger.error(
903
+ f"Failed to get tool descriptions from client {i+1}: {e}"
904
+ )
905
+
906
+ return "\n\n".join(tool_descriptions)
907
+
908
+ async def call_tool(
909
+ self, tool_name: str, tool_args: Dict[str, Any]
910
+ ) -> Any:
911
+ r"""Call a tool by name across all managed clients.
912
+
913
+ Searches for and executes a tool with the specified name across all
914
+ connected MCP clients. The method will try each client in sequence
915
+ until the tool is found and successfully executed.
916
+
917
+ Args:
918
+ tool_name (str): Name of the tool to call. Must match a tool name
919
+ available from one of the connected MCP servers.
920
+ tool_args (Dict[str, Any]): Arguments to pass to the tool. The
921
+ argument names and types must match the tool's expected
922
+ parameters.
923
+
924
+ Returns:
925
+ Any: The result of the tool call. The type and structure depend
926
+ on the specific tool being called.
927
+
928
+ Raises:
929
+ MCPConnectionError: If the toolkit is not connected to any MCP
930
+ servers.
931
+ MCPToolError: If the tool is not found in any client, or if all
932
+ attempts to call the tool fail. The error message will include
933
+ details about the last failure encountered.
934
+
935
+ Example:
936
+ .. code-block:: python
937
+
938
+ async with MCPToolkit(config_dict=config) as toolkit:
939
+ # Call a file reading tool
940
+ result = await toolkit.call_tool(
941
+ "read_file",
942
+ {"path": "/tmp/example.txt"}
943
+ )
944
+ print(f"File contents: {result}")
945
+ """
946
+ if not self.is_connected:
947
+ raise MCPConnectionError(
948
+ "MCPToolkit is not connected. Call connect() first."
949
+ )
950
+
951
+ # Try to find and call the tool from any client
952
+ last_error = None
953
+ for i, client in enumerate(self.clients):
954
+ try:
955
+ # Check if this client has the tool
956
+ tools = client.get_tools()
957
+ tool_names = [tool.func.__name__ for tool in tools]
958
+
959
+ if tool_name in tool_names:
960
+ result = await client.call_tool(tool_name, tool_args)
961
+ logger.debug(
962
+ f"Tool '{tool_name}' called successfully "
963
+ f"on client {i+1}"
964
+ )
965
+ return result
966
+ except Exception as e:
967
+ last_error = e
968
+ logger.debug(f"Tool '{tool_name}' failed on client {i+1}: {e}")
969
+ continue
970
+
971
+ # If we get here, the tool wasn't found or all calls failed
972
+ if last_error:
973
+ raise MCPToolError(
974
+ f"Tool '{tool_name}' failed on all clients. "
975
+ f"Last error: {last_error}"
976
+ ) from last_error
977
+ else:
978
+ raise MCPToolError(f"Tool '{tool_name}' not found in any client")
979
+
980
+ def call_tool_sync(self, tool_name: str, tool_args: Dict[str, Any]) -> Any:
981
+ r"""Synchronously call a tool."""
982
+ return run_async(self.call_tool)(tool_name, tool_args)
983
+
984
+ def list_available_tools(self) -> Dict[str, List[str]]:
985
+ r"""List all available tools organized by client.
755
986
 
756
987
  Returns:
757
- str: A string containing the descriptions of the tools
758
- in the toolkit.
988
+ Dict[str, List[str]]: Dictionary mapping client indices to tool
989
+ names.
759
990
  """
760
- return "\n".join(server.get_text_tools() for server in self.servers)
991
+ available_tools = {}
992
+ for i, client in enumerate(self.clients):
993
+ try:
994
+ tools = client.get_tools()
995
+ tool_names = [tool.func.__name__ for tool in tools]
996
+ available_tools[f"client_{i+1}"] = tool_names
997
+ except Exception as e:
998
+ logger.error(f"Failed to list tools from client {i+1}: {e}")
999
+ available_tools[f"client_{i+1}"] = []
1000
+
1001
+ return available_tools