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.
- camel/__init__.py +3 -3
- camel/agents/__init__.py +2 -2
- camel/agents/_types.py +9 -4
- camel/agents/_utils.py +40 -2
- camel/agents/base.py +2 -2
- camel/agents/chat_agent.py +5012 -902
- camel/agents/critic_agent.py +2 -2
- camel/agents/deductive_reasoner_agent.py +56 -56
- camel/agents/embodied_agent.py +2 -2
- camel/agents/knowledge_graph_agent.py +20 -20
- camel/agents/mcp_agent.py +39 -36
- camel/agents/multi_hop_generator_agent.py +3 -3
- camel/agents/programmed_agent_instruction.py +2 -2
- camel/agents/repo_agent.py +4 -3
- camel/agents/role_assignment_agent.py +2 -2
- camel/agents/search_agent.py +2 -2
- camel/agents/task_agent.py +2 -2
- camel/agents/tool_agents/__init__.py +2 -2
- camel/agents/tool_agents/base.py +2 -2
- camel/agents/tool_agents/hugging_face_tool_agent.py +3 -3
- camel/benchmarks/__init__.py +2 -2
- camel/benchmarks/apibank.py +5 -5
- camel/benchmarks/apibench.py +2 -2
- camel/benchmarks/base.py +2 -2
- camel/benchmarks/browsecomp.py +44 -33
- camel/benchmarks/gaia.py +17 -13
- camel/benchmarks/mock_website/README.md +94 -0
- camel/benchmarks/mock_website/mock_web.py +299 -0
- camel/benchmarks/mock_website/requirements.txt +3 -0
- camel/benchmarks/mock_website/shopping_mall/app.py +465 -0
- camel/benchmarks/mock_website/task.json +104 -0
- camel/benchmarks/nexus.py +3 -3
- camel/benchmarks/ragbench.py +2 -2
- camel/bots/__init__.py +2 -2
- camel/bots/discord/__init__.py +2 -2
- camel/bots/discord/discord_app.py +2 -2
- camel/bots/discord/discord_installation.py +2 -2
- camel/bots/discord/discord_store.py +3 -3
- camel/bots/slack/__init__.py +2 -2
- camel/bots/slack/models.py +4 -4
- camel/bots/slack/slack_app.py +2 -2
- camel/bots/telegram_bot.py +2 -2
- camel/configs/__init__.py +26 -2
- camel/configs/aihubmix_config.py +90 -0
- camel/configs/aiml_config.py +2 -2
- camel/configs/amd_config.py +70 -0
- camel/configs/anthropic_config.py +8 -7
- camel/configs/base_config.py +2 -2
- camel/configs/bedrock_config.py +5 -3
- camel/configs/cerebras_config.py +98 -0
- camel/configs/cohere_config.py +3 -3
- camel/configs/cometapi_config.py +106 -0
- camel/configs/crynux_config.py +94 -0
- camel/configs/deepseek_config.py +9 -8
- camel/configs/gemini_config.py +6 -4
- camel/configs/groq_config.py +6 -4
- camel/configs/internlm_config.py +6 -4
- camel/configs/litellm_config.py +2 -2
- camel/configs/lmstudio_config.py +6 -4
- camel/configs/minimax_config.py +95 -0
- camel/configs/mistral_config.py +3 -3
- camel/configs/modelscope_config.py +5 -3
- camel/configs/moonshot_config.py +2 -2
- camel/configs/nebius_config.py +105 -0
- camel/configs/netmind_config.py +2 -2
- camel/configs/novita_config.py +2 -2
- camel/configs/nvidia_config.py +2 -2
- camel/configs/ollama_config.py +2 -2
- camel/configs/openai_config.py +8 -3
- camel/configs/openrouter_config.py +6 -4
- camel/configs/ppio_config.py +2 -2
- camel/configs/qianfan_config.py +85 -0
- camel/configs/qwen_config.py +2 -2
- camel/configs/reka_config.py +3 -3
- camel/configs/samba_config.py +8 -6
- camel/configs/sglang_config.py +2 -2
- camel/configs/siliconflow_config.py +2 -2
- camel/configs/togetherai_config.py +2 -2
- camel/configs/vllm_config.py +4 -2
- camel/configs/watsonx_config.py +2 -2
- camel/configs/yi_config.py +6 -4
- camel/configs/zhipuai_config.py +6 -4
- camel/{data_collector → data_collectors}/__init__.py +2 -2
- camel/{data_collector → data_collectors}/alpaca_collector.py +19 -10
- camel/{data_collector → data_collectors}/base.py +2 -2
- camel/{data_collector → data_collectors}/sharegpt_collector.py +3 -3
- camel/datagen/__init__.py +2 -2
- camel/datagen/cot_datagen.py +32 -37
- camel/datagen/evol_instruct/__init__.py +2 -2
- camel/datagen/evol_instruct/evol_instruct.py +2 -2
- camel/datagen/evol_instruct/scorer.py +24 -25
- camel/datagen/evol_instruct/templates.py +48 -48
- camel/datagen/self_improving_cot.py +5 -5
- camel/datagen/self_instruct/__init__.py +2 -2
- camel/datagen/self_instruct/filter/__init__.py +2 -2
- camel/datagen/self_instruct/filter/filter_function.py +2 -2
- camel/datagen/self_instruct/filter/filter_registry.py +2 -2
- camel/datagen/self_instruct/filter/instruction_filter.py +2 -2
- camel/datagen/self_instruct/self_instruct.py +2 -2
- camel/datagen/self_instruct/templates.py +47 -47
- camel/datagen/source2synth/__init__.py +2 -2
- camel/datagen/source2synth/data_processor.py +2 -2
- camel/datagen/source2synth/models.py +2 -2
- camel/datagen/source2synth/user_data_processor_config.py +2 -2
- camel/datahubs/__init__.py +2 -2
- camel/datahubs/base.py +2 -2
- camel/datahubs/huggingface.py +2 -2
- camel/datahubs/models.py +2 -2
- camel/datasets/__init__.py +2 -2
- camel/datasets/base_generator.py +41 -12
- camel/datasets/few_shot_generator.py +18 -18
- camel/datasets/models.py +3 -3
- camel/datasets/self_instruct_generator.py +2 -2
- camel/datasets/static_dataset.py +152 -2
- camel/embeddings/__init__.py +2 -2
- camel/embeddings/azure_embedding.py +2 -2
- camel/embeddings/base.py +2 -2
- camel/embeddings/gemini_embedding.py +2 -2
- camel/embeddings/jina_embedding.py +10 -3
- camel/embeddings/mistral_embedding.py +2 -2
- camel/embeddings/openai_compatible_embedding.py +2 -2
- camel/embeddings/openai_embedding.py +2 -2
- camel/embeddings/sentence_transformers_embeddings.py +4 -4
- camel/embeddings/together_embedding.py +2 -2
- camel/embeddings/vlm_embedding.py +11 -4
- camel/environments/__init__.py +14 -2
- camel/environments/models.py +2 -2
- camel/environments/multi_step.py +2 -2
- camel/environments/rlcards_env.py +860 -0
- camel/environments/single_step.py +30 -5
- camel/environments/tic_tac_toe.py +3 -3
- camel/extractors/__init__.py +2 -2
- camel/extractors/base.py +2 -2
- camel/extractors/python_strategies.py +2 -2
- camel/generators.py +2 -2
- camel/human.py +2 -2
- camel/interpreters/__init__.py +4 -2
- camel/interpreters/base.py +16 -3
- camel/interpreters/docker/Dockerfile +53 -7
- camel/interpreters/docker_interpreter.py +70 -11
- camel/interpreters/e2b_interpreter.py +59 -11
- camel/interpreters/internal_python_interpreter.py +81 -4
- camel/interpreters/interpreter_error.py +2 -2
- camel/interpreters/ipython_interpreter.py +23 -5
- camel/interpreters/microsandbox_interpreter.py +395 -0
- camel/interpreters/subprocess_interpreter.py +36 -4
- camel/loaders/__init__.py +17 -5
- camel/loaders/apify_reader.py +2 -2
- camel/loaders/base_io.py +2 -2
- camel/loaders/base_loader.py +85 -0
- camel/loaders/chunkr_reader.py +128 -93
- camel/loaders/crawl4ai_reader.py +2 -2
- camel/loaders/firecrawl_reader.py +6 -6
- camel/loaders/jina_url_reader.py +2 -2
- camel/loaders/markitdown.py +2 -2
- camel/loaders/mineru_extractor.py +2 -2
- camel/loaders/mistral_reader.py +148 -0
- camel/loaders/scrapegraph_reader.py +2 -2
- camel/loaders/unstructured_io.py +2 -2
- camel/logger.py +5 -5
- camel/memories/__init__.py +2 -2
- camel/memories/agent_memories.py +86 -3
- camel/memories/base.py +36 -2
- camel/memories/blocks/__init__.py +2 -2
- camel/memories/blocks/chat_history_block.py +126 -9
- camel/memories/blocks/vectordb_block.py +10 -3
- camel/memories/context_creators/__init__.py +2 -2
- camel/memories/context_creators/score_based.py +31 -239
- camel/memories/records.py +98 -13
- camel/messages/__init__.py +2 -2
- camel/messages/base.py +193 -46
- camel/messages/conversion/__init__.py +2 -2
- camel/messages/conversion/alpaca.py +2 -2
- camel/messages/conversion/conversation_models.py +2 -2
- camel/messages/conversion/sharegpt/__init__.py +2 -2
- camel/messages/conversion/sharegpt/function_call_formatter.py +2 -2
- camel/messages/conversion/sharegpt/hermes/__init__.py +2 -2
- camel/messages/conversion/sharegpt/hermes/hermes_function_formatter.py +2 -2
- camel/messages/func_message.py +54 -17
- camel/models/__init__.py +18 -2
- camel/models/_utils.py +3 -3
- camel/models/aihubmix_model.py +83 -0
- camel/models/aiml_model.py +11 -18
- camel/models/amd_model.py +101 -0
- camel/models/anthropic_model.py +127 -20
- camel/models/aws_bedrock_model.py +12 -35
- camel/models/azure_openai_model.py +263 -63
- camel/models/base_audio_model.py +5 -3
- camel/models/base_model.py +195 -26
- camel/models/cerebras_model.py +83 -0
- camel/models/cohere_model.py +81 -21
- camel/models/cometapi_model.py +83 -0
- camel/models/crynux_model.py +87 -0
- camel/models/deepseek_model.py +61 -59
- camel/models/fish_audio_model.py +8 -2
- camel/models/gemini_model.py +439 -30
- camel/models/groq_model.py +11 -19
- camel/models/internlm_model.py +11 -18
- camel/models/litellm_model.py +94 -34
- camel/models/lmstudio_model.py +17 -20
- camel/models/minimax_model.py +83 -0
- camel/models/mistral_model.py +84 -19
- camel/models/model_factory.py +49 -6
- camel/models/model_manager.py +33 -11
- camel/models/modelscope_model.py +13 -193
- camel/models/moonshot_model.py +195 -21
- camel/models/nebius_model.py +83 -0
- camel/models/nemotron_model.py +19 -9
- camel/models/netmind_model.py +11 -18
- camel/models/novita_model.py +11 -18
- camel/models/nvidia_model.py +11 -18
- camel/models/ollama_model.py +14 -21
- camel/models/openai_audio_models.py +2 -2
- camel/models/openai_compatible_model.py +234 -27
- camel/models/openai_model.py +255 -39
- camel/models/openrouter_model.py +11 -19
- camel/models/ppio_model.py +11 -18
- camel/models/qianfan_model.py +89 -0
- camel/models/qwen_model.py +13 -193
- camel/models/reka_model.py +90 -21
- camel/models/reward/__init__.py +2 -2
- camel/models/reward/base_reward_model.py +2 -2
- camel/models/reward/evaluator.py +2 -2
- camel/models/reward/nemotron_model.py +2 -2
- camel/models/reward/skywork_model.py +2 -2
- camel/models/samba_model.py +117 -49
- camel/models/sglang_model.py +162 -42
- camel/models/siliconflow_model.py +12 -35
- camel/models/stub_model.py +10 -7
- camel/models/togetherai_model.py +11 -18
- camel/models/vllm_model.py +10 -18
- camel/models/volcano_model.py +16 -20
- camel/models/watsonx_model.py +69 -19
- camel/models/yi_model.py +11 -18
- camel/models/zhipuai_model.py +70 -18
- camel/parsers/__init__.py +18 -0
- camel/parsers/mcp_tool_call_parser.py +176 -0
- camel/personas/__init__.py +2 -2
- camel/personas/persona.py +2 -2
- camel/personas/persona_hub.py +2 -2
- camel/prompts/__init__.py +2 -2
- camel/prompts/ai_society.py +2 -2
- camel/prompts/base.py +2 -2
- camel/prompts/code.py +2 -2
- camel/prompts/evaluation.py +2 -2
- camel/prompts/generate_text_embedding_data.py +2 -2
- camel/prompts/image_craft.py +2 -2
- camel/prompts/misalignment.py +2 -2
- camel/prompts/multi_condition_image_craft.py +2 -2
- camel/prompts/object_recognition.py +2 -2
- camel/prompts/persona_hub.py +3 -3
- camel/prompts/prompt_templates.py +2 -2
- camel/prompts/role_description_prompt_template.py +2 -2
- camel/prompts/solution_extraction.py +8 -8
- camel/prompts/task_prompt_template.py +2 -2
- camel/prompts/translation.py +2 -2
- camel/prompts/video_description_prompt.py +3 -3
- camel/responses/__init__.py +2 -2
- camel/responses/agent_responses.py +2 -2
- camel/retrievers/__init__.py +2 -2
- camel/retrievers/auto_retriever.py +23 -3
- camel/retrievers/base.py +2 -2
- camel/retrievers/bm25_retriever.py +3 -4
- camel/retrievers/cohere_rerank_retriever.py +2 -2
- camel/retrievers/hybrid_retrival.py +4 -4
- camel/retrievers/vector_retriever.py +2 -2
- camel/runtimes/Dockerfile.multi-toolkit +90 -0
- camel/{runtime → runtimes}/__init__.py +2 -2
- camel/runtimes/api.py +153 -0
- camel/{runtime → runtimes}/base.py +2 -2
- camel/{runtime → runtimes}/configs.py +13 -13
- camel/{runtime → runtimes}/daytona_runtime.py +18 -19
- camel/{runtime → runtimes}/docker_runtime.py +13 -13
- camel/{runtime → runtimes}/llm_guard_runtime.py +28 -28
- camel/{runtime → runtimes}/remote_http_runtime.py +12 -12
- camel/{runtime → runtimes}/ubuntu_docker_runtime.py +3 -3
- camel/{runtime → runtimes}/utils/__init__.py +2 -2
- camel/{runtime → runtimes}/utils/function_risk_toolkit.py +2 -2
- camel/{runtime → runtimes}/utils/ignore_risk_toolkit.py +2 -2
- camel/schemas/__init__.py +2 -2
- camel/schemas/base.py +2 -2
- camel/schemas/openai_converter.py +3 -3
- camel/schemas/outlines_converter.py +2 -2
- camel/services/agent_openapi_server.py +380 -0
- camel/societies/__init__.py +4 -2
- camel/societies/babyagi_playing.py +2 -2
- camel/societies/role_playing.py +201 -80
- camel/societies/workforce/__init__.py +10 -3
- camel/societies/workforce/base.py +9 -5
- camel/societies/workforce/events.py +143 -0
- camel/societies/workforce/prompts.py +258 -33
- camel/societies/workforce/role_playing_worker.py +95 -30
- camel/societies/workforce/single_agent_worker.py +659 -30
- camel/societies/workforce/structured_output_handler.py +512 -0
- camel/societies/workforce/task_channel.py +182 -38
- camel/societies/workforce/utils.py +784 -18
- camel/societies/workforce/worker.py +96 -28
- camel/societies/workforce/workflow_memory_manager.py +1746 -0
- camel/societies/workforce/workforce.py +5730 -366
- camel/societies/workforce/workforce_callback.py +103 -0
- camel/societies/workforce/workforce_logger.py +647 -0
- camel/societies/workforce/workforce_metrics.py +33 -0
- camel/storages/__init__.py +10 -2
- camel/storages/graph_storages/__init__.py +2 -2
- camel/storages/graph_storages/base.py +2 -2
- camel/storages/graph_storages/graph_element.py +2 -2
- camel/storages/graph_storages/nebula_graph.py +4 -4
- camel/storages/graph_storages/neo4j_graph.py +7 -7
- camel/storages/key_value_storages/__init__.py +2 -2
- camel/storages/key_value_storages/base.py +2 -2
- camel/storages/key_value_storages/in_memory.py +2 -2
- camel/storages/key_value_storages/json.py +17 -4
- camel/storages/key_value_storages/mem0_cloud.py +50 -49
- camel/storages/key_value_storages/redis.py +2 -2
- camel/storages/object_storages/__init__.py +2 -2
- camel/storages/object_storages/amazon_s3.py +2 -2
- camel/storages/object_storages/azure_blob.py +2 -2
- camel/storages/object_storages/base.py +2 -2
- camel/storages/object_storages/google_cloud.py +3 -3
- camel/storages/vectordb_storages/__init__.py +12 -2
- camel/storages/vectordb_storages/base.py +2 -2
- camel/storages/vectordb_storages/chroma.py +731 -0
- camel/storages/vectordb_storages/faiss.py +712 -0
- camel/storages/vectordb_storages/milvus.py +2 -2
- camel/storages/vectordb_storages/oceanbase.py +16 -17
- camel/storages/vectordb_storages/pgvector.py +349 -0
- camel/storages/vectordb_storages/qdrant.py +6 -6
- camel/storages/vectordb_storages/surreal.py +372 -0
- camel/storages/vectordb_storages/tidb.py +11 -8
- camel/storages/vectordb_storages/weaviate.py +714 -0
- camel/tasks/__init__.py +2 -2
- camel/tasks/task.py +366 -27
- camel/tasks/task_prompt.py +3 -3
- camel/terminators/__init__.py +2 -2
- camel/terminators/base.py +2 -2
- camel/terminators/response_terminator.py +2 -2
- camel/terminators/token_limit_terminator.py +2 -2
- camel/toolkits/__init__.py +58 -10
- camel/toolkits/aci_toolkit.py +66 -21
- camel/toolkits/arxiv_toolkit.py +8 -8
- camel/toolkits/ask_news_toolkit.py +2 -2
- camel/toolkits/async_browser_toolkit.py +174 -575
- camel/toolkits/audio_analysis_toolkit.py +3 -3
- camel/toolkits/base.py +65 -7
- camel/toolkits/bohrium_toolkit.py +318 -0
- camel/toolkits/browser_toolkit.py +306 -566
- camel/toolkits/browser_toolkit_commons.py +568 -0
- camel/toolkits/code_execution.py +67 -11
- camel/toolkits/context_summarizer_toolkit.py +684 -0
- camel/toolkits/craw4ai_toolkit.py +93 -0
- camel/toolkits/dappier_toolkit.py +12 -8
- camel/toolkits/data_commons_toolkit.py +2 -2
- camel/toolkits/dingtalk.py +1135 -0
- camel/toolkits/earth_science_toolkit.py +5367 -0
- camel/toolkits/edgeone_pages_mcp_toolkit.py +49 -0
- camel/toolkits/excel_toolkit.py +910 -70
- camel/toolkits/file_toolkit.py +1402 -0
- camel/toolkits/function_tool.py +128 -20
- camel/toolkits/github_toolkit.py +148 -43
- camel/toolkits/gmail_toolkit.py +1839 -0
- camel/toolkits/google_calendar_toolkit.py +40 -6
- camel/toolkits/google_drive_mcp_toolkit.py +54 -0
- camel/toolkits/google_maps_toolkit.py +2 -2
- camel/toolkits/google_scholar_toolkit.py +2 -2
- camel/toolkits/human_toolkit.py +36 -12
- camel/toolkits/hybrid_browser_toolkit/__init__.py +18 -0
- camel/toolkits/hybrid_browser_toolkit/config_loader.py +185 -0
- camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit.py +246 -0
- camel/toolkits/hybrid_browser_toolkit/hybrid_browser_toolkit_ts.py +1973 -0
- camel/toolkits/hybrid_browser_toolkit/installer.py +203 -0
- camel/toolkits/hybrid_browser_toolkit/ts/package-lock.json +4589 -0
- camel/toolkits/hybrid_browser_toolkit/ts/package.json +33 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/browser-scripts.js +125 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/browser-session.ts +1929 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/config-loader.ts +233 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/hybrid-browser-toolkit.ts +589 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/index.ts +7 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/parent-child-filter.ts +226 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/snapshot-parser.ts +219 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/som-screenshot-injected.ts +543 -0
- camel/toolkits/hybrid_browser_toolkit/ts/src/types.ts +129 -0
- camel/toolkits/hybrid_browser_toolkit/ts/tsconfig.json +27 -0
- camel/toolkits/hybrid_browser_toolkit/ts/websocket-server.js +319 -0
- camel/toolkits/hybrid_browser_toolkit/ws_wrapper.py +1037 -0
- camel/toolkits/hybrid_browser_toolkit_py/__init__.py +17 -0
- camel/toolkits/hybrid_browser_toolkit_py/actions.py +575 -0
- camel/toolkits/hybrid_browser_toolkit_py/agent.py +311 -0
- camel/toolkits/hybrid_browser_toolkit_py/browser_session.py +787 -0
- camel/toolkits/hybrid_browser_toolkit_py/config_loader.py +490 -0
- camel/toolkits/hybrid_browser_toolkit_py/hybrid_browser_toolkit.py +2390 -0
- camel/toolkits/hybrid_browser_toolkit_py/snapshot.py +233 -0
- camel/toolkits/hybrid_browser_toolkit_py/stealth_script.js +0 -0
- camel/toolkits/hybrid_browser_toolkit_py/unified_analyzer.js +1043 -0
- camel/toolkits/image_analysis_toolkit.py +3 -3
- camel/toolkits/image_generation_toolkit.py +390 -0
- camel/toolkits/jina_reranker_toolkit.py +195 -79
- camel/toolkits/klavis_toolkit.py +7 -3
- camel/toolkits/linkedin_toolkit.py +2 -2
- camel/toolkits/markitdown_toolkit.py +104 -0
- camel/toolkits/math_toolkit.py +66 -12
- camel/toolkits/mcp_toolkit.py +841 -600
- camel/toolkits/memory_toolkit.py +7 -3
- camel/toolkits/meshy_toolkit.py +2 -2
- camel/toolkits/message_agent_toolkit.py +608 -0
- camel/toolkits/message_integration.py +724 -0
- camel/toolkits/mineru_toolkit.py +2 -2
- camel/toolkits/minimax_mcp_toolkit.py +195 -0
- camel/toolkits/networkx_toolkit.py +2 -2
- camel/toolkits/note_taking_toolkit.py +277 -0
- camel/toolkits/notion_mcp_toolkit.py +224 -0
- camel/toolkits/notion_toolkit.py +2 -2
- camel/toolkits/open_api_specs/biztoc/__init__.py +2 -2
- camel/toolkits/open_api_specs/biztoc/ai-plugin.json +1 -1
- camel/toolkits/open_api_specs/coursera/__init__.py +2 -2
- camel/toolkits/open_api_specs/create_qr_code/__init__.py +2 -2
- camel/toolkits/open_api_specs/klarna/__init__.py +2 -2
- camel/toolkits/open_api_specs/nasa_apod/__init__.py +2 -2
- camel/toolkits/open_api_specs/outschool/__init__.py +2 -2
- camel/toolkits/open_api_specs/outschool/ai-plugin.json +1 -1
- camel/toolkits/open_api_specs/outschool/openapi.yaml +1 -1
- camel/toolkits/open_api_specs/outschool/paths/__init__.py +2 -2
- camel/toolkits/open_api_specs/outschool/paths/get_classes.py +2 -2
- camel/toolkits/open_api_specs/outschool/paths/search_teachers.py +2 -2
- camel/toolkits/open_api_specs/security_config.py +2 -2
- camel/toolkits/open_api_specs/speak/__init__.py +2 -2
- camel/toolkits/open_api_specs/web_scraper/__init__.py +2 -2
- camel/toolkits/open_api_specs/web_scraper/ai-plugin.json +1 -1
- camel/toolkits/open_api_specs/web_scraper/paths/__init__.py +2 -2
- camel/toolkits/open_api_specs/web_scraper/paths/scraper.py +2 -2
- camel/toolkits/open_api_toolkit.py +2 -2
- camel/toolkits/openbb_toolkit.py +7 -3
- camel/toolkits/origene_mcp_toolkit.py +56 -0
- camel/toolkits/page_script.js +86 -74
- camel/toolkits/playwright_mcp_toolkit.py +27 -32
- camel/toolkits/pptx_toolkit.py +790 -0
- camel/toolkits/pubmed_toolkit.py +2 -2
- camel/toolkits/pulse_mcp_search_toolkit.py +2 -2
- camel/toolkits/pyautogui_toolkit.py +2 -2
- camel/toolkits/reddit_toolkit.py +2 -2
- camel/toolkits/resend_toolkit.py +168 -0
- camel/toolkits/retrieval_toolkit.py +2 -2
- camel/toolkits/screenshot_toolkit.py +213 -0
- camel/toolkits/search_toolkit.py +539 -146
- camel/toolkits/searxng_toolkit.py +2 -2
- camel/toolkits/semantic_scholar_toolkit.py +2 -2
- camel/toolkits/slack_toolkit.py +108 -58
- camel/toolkits/sql_toolkit.py +712 -0
- camel/toolkits/stripe_toolkit.py +2 -2
- camel/toolkits/sympy_toolkit.py +3 -3
- camel/toolkits/task_planning_toolkit.py +134 -0
- camel/toolkits/terminal_toolkit/__init__.py +18 -0
- camel/toolkits/terminal_toolkit/terminal_toolkit.py +1070 -0
- camel/toolkits/terminal_toolkit/utils.py +532 -0
- camel/toolkits/thinking_toolkit.py +3 -3
- camel/toolkits/twitter_toolkit.py +8 -3
- camel/toolkits/vertex_ai_veo_toolkit.py +590 -0
- camel/toolkits/video_analysis_toolkit.py +112 -29
- camel/toolkits/video_download_toolkit.py +22 -16
- camel/toolkits/weather_toolkit.py +2 -2
- camel/toolkits/web_deploy_toolkit.py +1219 -0
- camel/toolkits/wechat_official_toolkit.py +483 -0
- camel/toolkits/whatsapp_toolkit.py +2 -2
- camel/toolkits/wolfram_alpha_toolkit.py +53 -25
- camel/toolkits/zapier_toolkit.py +7 -3
- camel/types/__init__.py +4 -4
- camel/types/agents/__init__.py +2 -2
- camel/types/agents/tool_calling_record.py +6 -3
- camel/types/enums.py +454 -35
- camel/types/mcp_registries.py +2 -2
- camel/types/openai_types.py +4 -4
- camel/types/unified_model_type.py +43 -6
- camel/utils/__init__.py +20 -2
- camel/utils/async_func.py +2 -2
- camel/utils/chunker/__init__.py +2 -2
- camel/utils/chunker/base.py +2 -2
- camel/utils/chunker/code_chunker.py +2 -2
- camel/utils/chunker/uio_chunker.py +2 -2
- camel/utils/commons.py +65 -7
- camel/utils/constants.py +5 -2
- camel/utils/context_utils.py +1134 -0
- camel/utils/deduplication.py +2 -2
- camel/utils/filename.py +2 -2
- camel/utils/langfuse.py +258 -0
- camel/utils/mcp.py +140 -6
- camel/utils/mcp_client.py +1056 -0
- camel/utils/message_summarizer.py +148 -0
- camel/utils/response_format.py +2 -2
- camel/utils/token_counting.py +45 -22
- camel/utils/tool_result.py +44 -0
- camel/verifiers/__init__.py +2 -2
- camel/verifiers/base.py +2 -2
- camel/verifiers/math_verifier.py +2 -2
- camel/verifiers/models.py +2 -2
- camel/verifiers/physics_verifier.py +2 -2
- camel/verifiers/python_verifier.py +2 -2
- {camel_ai-0.2.59.dist-info → camel_ai-0.2.82.dist-info}/METADATA +349 -108
- camel_ai-0.2.82.dist-info/RECORD +507 -0
- {camel_ai-0.2.59.dist-info → camel_ai-0.2.82.dist-info}/WHEEL +1 -1
- {camel_ai-0.2.59.dist-info → camel_ai-0.2.82.dist-info}/licenses/LICENSE +1 -1
- camel/loaders/pandas_reader.py +0 -368
- camel/runtime/api.py +0 -97
- camel/toolkits/dalle_toolkit.py +0 -171
- camel/toolkits/file_write_toolkit.py +0 -395
- camel/toolkits/openai_agent_toolkit.py +0 -135
- camel/toolkits/terminal_toolkit.py +0 -1037
- camel_ai-0.2.59.dist-info/RECORD +0 -410
camel/toolkits/mcp_toolkit.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# ========= Copyright 2023-
|
|
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-
|
|
14
|
-
|
|
13
|
+
# ========= Copyright 2023-2025 @ CAMEL-AI.org. All Rights Reserved. =========
|
|
14
|
+
|
|
15
15
|
import json
|
|
16
16
|
import os
|
|
17
|
-
import
|
|
18
|
-
from contextlib import AsyncExitStack
|
|
19
|
-
from
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
49
|
-
|
|
37
|
+
class MCPConnectionError(Exception):
|
|
38
|
+
r"""Raised when MCP connection fails."""
|
|
50
39
|
|
|
51
|
-
|
|
52
|
-
event-based interactions.
|
|
40
|
+
pass
|
|
53
41
|
|
|
54
|
-
3. streamable-http mode: Connects via HTTP for persistent, streamable
|
|
55
|
-
interactions.
|
|
56
42
|
|
|
57
|
-
|
|
58
|
-
|
|
43
|
+
class MCPToolError(Exception):
|
|
44
|
+
r"""Raised when MCP tool execution fails."""
|
|
59
45
|
|
|
60
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
|
|
288
|
-
|
|
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
|
-
|
|
291
|
-
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
|
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
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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
|
-
|
|
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
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
|
|
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
|
-
|
|
462
|
-
|
|
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
|
-
|
|
484
|
-
|
|
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
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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
|
-
|
|
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
|
|
542
|
-
defining MCP servers.
|
|
543
|
-
|
|
544
|
-
|
|
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
|
-
|
|
551
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
335
|
+
clients: Optional[List[MCPClient]] = None,
|
|
581
336
|
config_path: Optional[str] = None,
|
|
582
337
|
config_dict: Optional[Dict[str, Any]] = None,
|
|
583
|
-
|
|
338
|
+
timeout: Optional[float] = None,
|
|
584
339
|
):
|
|
585
|
-
|
|
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 [
|
|
345
|
+
1 for src in [clients, config_path, config_dict] if src is not None
|
|
589
346
|
)
|
|
590
|
-
if sources_provided
|
|
591
|
-
|
|
592
|
-
"
|
|
593
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
363
|
+
self.clients.extend(self._load_clients_from_dict(config_dict))
|
|
606
364
|
|
|
607
|
-
self.
|
|
365
|
+
if not self.clients:
|
|
366
|
+
raise ValueError("No valid MCP clients could be created")
|
|
608
367
|
|
|
609
|
-
def
|
|
610
|
-
|
|
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
|
-
|
|
615
|
-
|
|
616
|
-
|
|
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
|
-
|
|
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
|
-
|
|
624
|
-
|
|
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
|
-
|
|
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
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
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
|
-
|
|
480
|
+
bool: True if the toolkit is connected to all MCP servers,
|
|
481
|
+
False otherwise.
|
|
650
482
|
"""
|
|
651
|
-
|
|
483
|
+
if not self._is_connected:
|
|
484
|
+
return False
|
|
652
485
|
|
|
653
|
-
|
|
654
|
-
|
|
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
|
-
|
|
659
|
-
|
|
660
|
-
|
|
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
|
-
|
|
666
|
-
|
|
667
|
-
|
|
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
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
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
|
-
|
|
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
|
-
|
|
689
|
-
|
|
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:
|
|
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
|
-
|
|
695
|
-
|
|
696
|
-
|
|
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
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
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
|
-
|
|
706
|
-
|
|
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
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
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
|
-
|
|
716
|
-
await server.disconnect()
|
|
717
|
-
self._connected = False
|
|
639
|
+
return clients
|
|
718
640
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
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
|
-
|
|
725
|
-
|
|
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
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
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
|
-
|
|
734
|
-
|
|
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
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
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
|
|
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
|
-
|
|
749
|
-
|
|
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
|
-
|
|
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:
|
|
758
|
-
|
|
988
|
+
Dict[str, List[str]]: Dictionary mapping client indices to tool
|
|
989
|
+
names.
|
|
759
990
|
"""
|
|
760
|
-
|
|
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
|