aria-code 0.167.0__py3-none-win_amd64.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.
- aria_bots/__init__.py +7 -0
- aria_bots/channels/README.md +97 -0
- aria_bots/channels/__init__.py +1 -0
- aria_bots/channels/approvals.py +263 -0
- aria_bots/channels/conversation.py +280 -0
- aria_bots/channels/digest.py +186 -0
- aria_bots/channels/registry.py +95 -0
- aria_bots/engine.py +144 -0
- aria_bots/feishu_bot.py +1385 -0
- aria_bots/logistics/__init__.py +1 -0
- aria_bots/logistics/carriers.py +253 -0
- aria_bots/logistics/freight.py +154 -0
- aria_bots/logistics/inventory.py +242 -0
- aria_bots/logistics/mcp_server.py +156 -0
- aria_bots/logistics/records.py +119 -0
- aria_bots/logistics/tenancy.py +152 -0
- aria_bots/market.py +91 -0
- aria_bots/relay_client.py +326 -0
- aria_bots/telegram_bot.py +299 -0
- aria_code/__init__.py +111 -0
- aria_code/_bin/aria-native.exe +0 -0
- aria_code/_bin/aria-runtime.exe +0 -0
- aria_code/_version.py +20 -0
- aria_code/account_memory.py +296 -0
- aria_code/account_session.py +166 -0
- aria_code/adk_apps/__init__.py +1 -0
- aria_code/adk_apps/aria_code_review/__init__.py +1 -0
- aria_code/adk_apps/aria_code_review/agent.py +32 -0
- aria_code/adk_apps/aria_research/__init__.py +1 -0
- aria_code/adk_apps/aria_research/agent.py +31 -0
- aria_code/agents/__init__.py +49 -0
- aria_code/agents/base.py +329 -0
- aria_code/agents/code_review.py +200 -0
- aria_code/agents/deep/__init__.py +37 -0
- aria_code/agents/deep/calibration_loop.py +144 -0
- aria_code/agents/deep/critic.py +125 -0
- aria_code/agents/deep/deepen.py +193 -0
- aria_code/agents/deep/models.py +149 -0
- aria_code/agents/deep/pipeline.py +164 -0
- aria_code/agents/deep/quant_fusion.py +192 -0
- aria_code/agents/deep/themes.py +95 -0
- aria_code/agents/deep/tiers.py +106 -0
- aria_code/agents/engineering/__init__.py +14 -0
- aria_code/agents/engineering/coder.py +222 -0
- aria_code/agents/engineering/tester.py +145 -0
- aria_code/agents/financial/__init__.py +21 -0
- aria_code/agents/financial/cashflow_burn.py +119 -0
- aria_code/agents/financial/catalyst.py +279 -0
- aria_code/agents/financial/corporate_finance.py +175 -0
- aria_code/agents/financial/debate.py +224 -0
- aria_code/agents/financial/earnings.py +315 -0
- aria_code/agents/financial/fundamental.py +178 -0
- aria_code/agents/financial/macro.py +111 -0
- aria_code/agents/financial/news.py +231 -0
- aria_code/agents/financial/risk.py +146 -0
- aria_code/agents/financial/sector.py +279 -0
- aria_code/agents/financial/strategist.py +121 -0
- aria_code/agents/financial/stripe_revenue.py +127 -0
- aria_code/agents/financial/synthesis.py +274 -0
- aria_code/agents/financial/technical.py +277 -0
- aria_code/agents/orchestrator.py +44 -0
- aria_code/agents/portfolio_agent.py +392 -0
- aria_code/agents/realty/__init__.py +62 -0
- aria_code/agents/realty/asset_diagnosis.py +152 -0
- aria_code/agents/realty/business_match.py +166 -0
- aria_code/agents/realty/cashflow_verify.py +209 -0
- aria_code/agents/realty/contract_rules.py +210 -0
- aria_code/agents/realty/energy_anomaly.py +189 -0
- aria_code/agents/realty/exit_settlement.py +208 -0
- aria_code/agents/realty/fulfillment_risk.py +206 -0
- aria_code/agents/realty/ops_optimize.py +160 -0
- aria_code/agents/realty/revenue_share.py +215 -0
- aria_code/agents/registry.py +168 -0
- aria_code/agents/signal_scheme.py +111 -0
- aria_code/agents/sports/__init__.py +0 -0
- aria_code/agents/sports/football_agent.py +197 -0
- aria_code/agents/supervisor.py +113 -0
- aria_code/agents/team.py +500 -0
- aria_code/agents/video/__init__.py +16 -0
- aria_code/agents/video/director.py +376 -0
- aria_code/agents/video/qa_agent.py +143 -0
- aria_code/agents/video/script_doctor.py +112 -0
- aria_code/agents/warehouse/__init__.py +25 -0
- aria_code/agents/warehouse/contracts.py +26 -0
- aria_code/agents/warehouse/dashboard.py +155 -0
- aria_code/agents/warehouse/fulfillment_leadtime.py +95 -0
- aria_code/agents/warehouse/inbound_exceptions.py +25 -0
- aria_code/agents/warehouse/inventory_health.py +24 -0
- aria_code/agents/warehouse/local_run.py +71 -0
- aria_code/agents/warehouse/local_snapshot.py +132 -0
- aria_code/agents/warehouse/logistics_cost.py +84 -0
- aria_code/agents/warehouse/logistics_sync.py +21 -0
- aria_code/agents/warehouse/navigation.schema.json +110 -0
- aria_code/agents/warehouse/task_agent.py +172 -0
- aria_code/agents/warehouse/workflow.py +31 -0
- aria_code/aliyun_data_client.py +4 -0
- aria_code/apps/README.md +12 -0
- aria_code/apps/__init__.py +2 -0
- aria_code/apps/channels/README.md +9 -0
- aria_code/apps/channels/__init__.py +17 -0
- aria_code/apps/channels/approvals.py +11 -0
- aria_code/apps/channels/conversation.py +11 -0
- aria_code/apps/channels/digest.py +11 -0
- aria_code/apps/channels/intake.py +92 -0
- aria_code/apps/channels/registry.py +11 -0
- aria_code/apps/channels/tradingview.py +94 -0
- aria_code/apps/cli/README.md +13 -0
- aria_code/apps/cli/__init__.py +2 -0
- aria_code/apps/cli/_optional.py +120 -0
- aria_code/apps/cli/app_server.py +656 -0
- aria_code/apps/cli/auto_update.py +125 -0
- aria_code/apps/cli/bootstrap.py +206 -0
- aria_code/apps/cli/broker_render.py +206 -0
- aria_code/apps/cli/chat_turn.py +1327 -0
- aria_code/apps/cli/cloud_models.py +231 -0
- aria_code/apps/cli/codegen_paths.py +29 -0
- aria_code/apps/cli/commands/__init__.py +34 -0
- aria_code/apps/cli/commands/_business_render.py +78 -0
- aria_code/apps/cli/commands/_ui.py +71 -0
- aria_code/apps/cli/commands/analysis_cmds.py +378 -0
- aria_code/apps/cli/commands/ashare_prediction_cmds.py +327 -0
- aria_code/apps/cli/commands/auth_cmds.py +215 -0
- aria_code/apps/cli/commands/backtest_cmds.py +2518 -0
- aria_code/apps/cli/commands/broker_cmds.py +1236 -0
- aria_code/apps/cli/commands/business_workflow_cmds.py +379 -0
- aria_code/apps/cli/commands/canvas_cmds.py +77 -0
- aria_code/apps/cli/commands/catalog.py +189 -0
- aria_code/apps/cli/commands/core_cmds.py +2748 -0
- aria_code/apps/cli/commands/data_cmds.py +521 -0
- aria_code/apps/cli/commands/diagnostic_cmds.py +285 -0
- aria_code/apps/cli/commands/diagnostic_ops_cmds.py +813 -0
- aria_code/apps/cli/commands/engineering_cmds.py +399 -0
- aria_code/apps/cli/commands/file_cmds.py +312 -0
- aria_code/apps/cli/commands/finance_service_cmds.py +60 -0
- aria_code/apps/cli/commands/fx_commodity_cmds.py +123 -0
- aria_code/apps/cli/commands/market.py +544 -0
- aria_code/apps/cli/commands/market_cmds.py +1419 -0
- aria_code/apps/cli/commands/market_context.py +432 -0
- aria_code/apps/cli/commands/market_render.py +7 -0
- aria_code/apps/cli/commands/model_cmds.py +2374 -0
- aria_code/apps/cli/commands/ops_cmds.py +721 -0
- aria_code/apps/cli/commands/orchestrator_cmds.py +181 -0
- aria_code/apps/cli/commands/pdf_export_cmds.py +125 -0
- aria_code/apps/cli/commands/portfolio_cmds.py +1316 -0
- aria_code/apps/cli/commands/report.py +476 -0
- aria_code/apps/cli/commands/scaffold_templates.py +617 -0
- aria_code/apps/cli/commands/session_cmds.py +236 -0
- aria_code/apps/cli/commands/session_ux_cmds.py +359 -0
- aria_code/apps/cli/commands/team.py +723 -0
- aria_code/apps/cli/commands/team_render.py +8 -0
- aria_code/apps/cli/commands/ui_cmds.py +401 -0
- aria_code/apps/cli/commands/warehouse_cmds.py +297 -0
- aria_code/apps/cli/commands/workflow_cmds.py +629 -0
- aria_code/apps/cli/commands/workspace_cmds.py +1543 -0
- aria_code/apps/cli/config_paths.py +73 -0
- aria_code/apps/cli/config_store.py +158 -0
- aria_code/apps/cli/constants.py +88 -0
- aria_code/apps/cli/context.py +34 -0
- aria_code/apps/cli/conversation_policy.py +53 -0
- aria_code/apps/cli/deterministic.py +200 -0
- aria_code/apps/cli/direct.py +48 -0
- aria_code/apps/cli/earnings_view.py +172 -0
- aria_code/apps/cli/endpoint_migration.py +64 -0
- aria_code/apps/cli/exec_events.py +255 -0
- aria_code/apps/cli/file_references.py +155 -0
- aria_code/apps/cli/finance_service_catalog.py +111 -0
- aria_code/apps/cli/football_reports.py +231 -0
- aria_code/apps/cli/github_app_auth.py +135 -0
- aria_code/apps/cli/google_login.py +239 -0
- aria_code/apps/cli/handlers/__init__.py +11 -0
- aria_code/apps/cli/handlers/broker_handlers.py +122 -0
- aria_code/apps/cli/handlers/chart_handlers.py +1314 -0
- aria_code/apps/cli/handlers/market_handlers.py +2570 -0
- aria_code/apps/cli/handlers/realty_handlers.py +114 -0
- aria_code/apps/cli/handlers/sports_handlers.py +22 -0
- aria_code/apps/cli/handlers/strategy_advice.py +82 -0
- aria_code/apps/cli/headless.py +352 -0
- aria_code/apps/cli/helpers.py +326 -0
- aria_code/apps/cli/hooks.py +180 -0
- aria_code/apps/cli/i18n.py +310 -0
- aria_code/apps/cli/intent.py +136 -0
- aria_code/apps/cli/intent_router.py +305 -0
- aria_code/apps/cli/intent_shadow.py +72 -0
- aria_code/apps/cli/intent_signals.py +347 -0
- aria_code/apps/cli/lifecycle_hooks.py +48 -0
- aria_code/apps/cli/local_tool_schemas.py +550 -0
- aria_code/apps/cli/main.py +39 -0
- aria_code/apps/cli/market_hours.py +100 -0
- aria_code/apps/cli/market_metadata.py +135 -0
- aria_code/apps/cli/market_universe.py +487 -0
- aria_code/apps/cli/message_processing.py +310 -0
- aria_code/apps/cli/mixin_binding.py +32 -0
- aria_code/apps/cli/model_catalog.py +652 -0
- aria_code/apps/cli/model_probe.py +169 -0
- aria_code/apps/cli/model_retirement.py +65 -0
- aria_code/apps/cli/native_branding.py +12 -0
- aria_code/apps/cli/native_bridge.py +118 -0
- aria_code/apps/cli/native_entry.py +58 -0
- aria_code/apps/cli/pdf_report.py +977 -0
- aria_code/apps/cli/plan_mode.py +139 -0
- aria_code/apps/cli/plotly_html.py +15 -0
- aria_code/apps/cli/prediction_feedback.py +202 -0
- aria_code/apps/cli/preflight.py +502 -0
- aria_code/apps/cli/project_aria.py +60 -0
- aria_code/apps/cli/prompt_assembly.py +78 -0
- aria_code/apps/cli/prompts/__init__.py +0 -0
- aria_code/apps/cli/prompts/coding.py +71 -0
- aria_code/apps/cli/prompts/select.py +193 -0
- aria_code/apps/cli/prompts/system_prompts.py +562 -0
- aria_code/apps/cli/prompts/ui.py +436 -0
- aria_code/apps/cli/provider_endpoints.py +237 -0
- aria_code/apps/cli/provider_keys.py +73 -0
- aria_code/apps/cli/providers/__init__.py +1 -0
- aria_code/apps/cli/providers/base.py +775 -0
- aria_code/apps/cli/providers/chat_routing.py +139 -0
- aria_code/apps/cli/providers/collaboration.py +98 -0
- aria_code/apps/cli/providers/llm/__init__.py +1 -0
- aria_code/apps/cli/providers/llm/offline_responses.py +68 -0
- aria_code/apps/cli/providers/llm/ollama_stream.py +1574 -0
- aria_code/apps/cli/providers/llm/response_cache.py +60 -0
- aria_code/apps/cli/providers/llm/sse_stream.py +298 -0
- aria_code/apps/cli/providers/runtime_bridge.py +586 -0
- aria_code/apps/cli/providers/vertexai_stream.py +592 -0
- aria_code/apps/cli/release_download.py +159 -0
- aria_code/apps/cli/review_runner.py +111 -0
- aria_code/apps/cli/runtime_consumer.py +769 -0
- aria_code/apps/cli/sandbox.py +32 -0
- aria_code/apps/cli/session_export.py +120 -0
- aria_code/apps/cli/session_jsonl.py +208 -0
- aria_code/apps/cli/session_refresh.py +123 -0
- aria_code/apps/cli/session_store.py +125 -0
- aria_code/apps/cli/skills_catalog.py +273 -0
- aria_code/apps/cli/task_isolation.py +211 -0
- aria_code/apps/cli/terminal_chrome.py +412 -0
- aria_code/apps/cli/todo_tracker.py +190 -0
- aria_code/apps/cli/tool_executor.py +932 -0
- aria_code/apps/cli/tool_registry.py +34 -0
- aria_code/apps/cli/tool_scope.py +113 -0
- aria_code/apps/cli/tools/__init__.py +40 -0
- aria_code/apps/cli/tools/context.py +46 -0
- aria_code/apps/cli/tools/file_tools.py +181 -0
- aria_code/apps/cli/tools/market_tools.py +626 -0
- aria_code/apps/cli/tools/notebook_tools.py +111 -0
- aria_code/apps/cli/tools/system_tools.py +656 -0
- aria_code/apps/cli/tools/write_tools.py +961 -0
- aria_code/apps/cli/tradingview_bridge.py +529 -0
- aria_code/apps/cli/transactions.py +269 -0
- aria_code/apps/cli/turn_planning.py +137 -0
- aria_code/apps/cli/update_check.py +257 -0
- aria_code/apps/cli/updater.py +373 -0
- aria_code/apps/cli/utils/__init__.py +0 -0
- aria_code/apps/cli/utils/market_detect.py +1663 -0
- aria_code/apps/cli/web_login.py +136 -0
- aria_code/apps/cli/workflow_runner.py +127 -0
- aria_code/apps/cli/workspace_route.py +121 -0
- aria_code/apps/daemon/README.md +14 -0
- aria_code/apps/vscode/README.md +115 -0
- aria_code/apps/vscode/package.json +70 -0
- aria_code/aria_cli.py +5244 -0
- aria_code/aria_daemon.py +1372 -0
- aria_code/aria_feishu_bot.py +16 -0
- aria_code/aria_mcp_server.py +61 -0
- aria_code/aria_relay_client.py +4 -0
- aria_code/aria_telegram_bot.py +11 -0
- aria_code/ariarc.py +385 -0
- aria_code/artifacts.py +581 -0
- aria_code/backtest_engine.py +4 -0
- aria_code/backtest_report.py +4 -0
- aria_code/brokers/__init__.py +78 -0
- aria_code/brokers/automation.py +193 -0
- aria_code/brokers/base.py +231 -0
- aria_code/brokers/capabilities.py +264 -0
- aria_code/brokers/cn/__init__.py +10 -0
- aria_code/brokers/cn/easytrader_broker.py +193 -0
- aria_code/brokers/cn/futu_broker.py +194 -0
- aria_code/brokers/cn/longbridge_broker.py +190 -0
- aria_code/brokers/cn/tiger_broker.py +196 -0
- aria_code/brokers/cn/xtquant_broker.py +225 -0
- aria_code/brokers/config.py +414 -0
- aria_code/brokers/intl/__init__.py +5 -0
- aria_code/brokers/intl/alpaca_broker.py +183 -0
- aria_code/brokers/intl/ibkr_broker.py +215 -0
- aria_code/brokers/intl/trading212_broker.py +183 -0
- aria_code/brokers/intl/webull_broker.py +156 -0
- aria_code/brokers/paper_broker.py +266 -0
- aria_code/brokers/planning.py +296 -0
- aria_code/brokers/registry.py +213 -0
- aria_code/brokers/trading.py +267 -0
- aria_code/canva_client.py +4 -0
- aria_code/change_store.py +127 -0
- aria_code/clients/__init__.py +0 -0
- aria_code/clients/aliyun_data_client.py +660 -0
- aria_code/clients/aria_relay_client.py +16 -0
- aria_code/clients/canva_client.py +284 -0
- aria_code/clients/figma_client.py +100 -0
- aria_code/clients/football_data_client.py +1671 -0
- aria_code/clients/kling_video_client.py +182 -0
- aria_code/clients/market_data_client.py +1985 -0
- aria_code/clients/openai_image_client.py +242 -0
- aria_code/clients/runway_video_client.py +157 -0
- aria_code/clients/warehouse_erp_client.py +105 -0
- aria_code/cloud_memory.py +190 -0
- aria_code/command_safety.py +19 -0
- aria_code/computer_use_server.py +276 -0
- aria_code/computer_use_tools.py +4 -0
- aria_code/dashboard_generator.py +579 -0
- aria_code/data_analysis_tools.py +4 -0
- aria_code/data_cleaner.py +483 -0
- aria_code/data_service.py +513 -0
- aria_code/datasources/__init__.py +23 -0
- aria_code/datasources/base.py +166 -0
- aria_code/datasources/router.py +253 -0
- aria_code/datasources/sec_statements.py +217 -0
- aria_code/datasources/sources/__init__.py +15 -0
- aria_code/datasources/sources/akshare_source.py +269 -0
- aria_code/datasources/sources/alpha_vantage_source.py +203 -0
- aria_code/datasources/sources/edgar_source.py +219 -0
- aria_code/datasources/sources/finnhub_source.py +198 -0
- aria_code/datasources/sources/fred_source.py +223 -0
- aria_code/datasources/sources/tushare_source.py +143 -0
- aria_code/datasources/sources/web_scraper_source.py +278 -0
- aria_code/datasources/sources/world_bank_source.py +206 -0
- aria_code/datasources/sources/yfinance_source.py +152 -0
- aria_code/doctor.py +860 -0
- aria_code/domain/__init__.py +0 -0
- aria_code/domain/_finance_core/__init__.py +5 -0
- aria_code/domain/_finance_core/backtest_engine.py +325 -0
- aria_code/domain/_finance_core/performance_stats.py +101 -0
- aria_code/domain/backtest_engine.py +75 -0
- aria_code/domain/backtest_report.py +488 -0
- aria_code/domain/finance_formulas.py +389 -0
- aria_code/domain/performance_stats.py +6 -0
- aria_code/domain/portfolio_ledger.py +307 -0
- aria_code/domain/strategy_vault.py +421 -0
- aria_code/evals/__init__.py +37 -0
- aria_code/evals/behavior.py +113 -0
- aria_code/evals/gap.py +93 -0
- aria_code/evals/harness.py +949 -0
- aria_code/evals/review_probe.py +132 -0
- aria_code/evals/runner.py +425 -0
- aria_code/evals/scorecard.py +322 -0
- aria_code/evals/summary.py +125 -0
- aria_code/external_agent_runner.py +115 -0
- aria_code/figma_client.py +4 -0
- aria_code/file_analysis_tools.py +4 -0
- aria_code/finance_formulas.py +4 -0
- aria_code/football_data_client.py +4 -0
- aria_code/image_gen_tools.py +4 -0
- aria_code/intent_classifier.py +459 -0
- aria_code/kling_video_client.py +4 -0
- aria_code/licensing.py +151 -0
- aria_code/local_finance_tools.py +4 -0
- aria_code/local_image_provider.py +4 -0
- aria_code/local_llm_provider.py +4 -0
- aria_code/macro_tools.py +4 -0
- aria_code/markdown_pdf.py +289 -0
- aria_code/market_data_client.py +4 -0
- aria_code/mcp_client.py +744 -0
- aria_code/memory_manager.py +273 -0
- aria_code/model_capability.py +512 -0
- aria_code/notification_tools.py +4 -0
- aria_code/openai_image_client.py +4 -0
- aria_code/packages/__init__.py +26 -0
- aria_code/packages/adk_bridge/__init__.py +11 -0
- aria_code/packages/adk_bridge/code_review_tools.py +49 -0
- aria_code/packages/adk_bridge/market_tools.py +125 -0
- aria_code/packages/aria_agents/__init__.py +5 -0
- aria_code/packages/aria_agents/manifest.py +78 -0
- aria_code/packages/aria_core/__init__.py +34 -0
- aria_code/packages/aria_core/architecture.py +269 -0
- aria_code/packages/aria_core/export.py +147 -0
- aria_code/packages/aria_core/manifest.py +65 -0
- aria_code/packages/aria_core/paths.py +57 -0
- aria_code/packages/aria_core/secure_file.py +68 -0
- aria_code/packages/aria_infra/__init__.py +15 -0
- aria_code/packages/aria_infra/arthera.py +58 -0
- aria_code/packages/aria_infra/doctor.py +246 -0
- aria_code/packages/aria_infra/product.py +40 -0
- aria_code/packages/aria_mcp/__init__.py +25 -0
- aria_code/packages/aria_mcp/bridge.py +232 -0
- aria_code/packages/aria_mcp/circuit.py +82 -0
- aria_code/packages/aria_mcp/config.py +162 -0
- aria_code/packages/aria_mcp/installer.py +95 -0
- aria_code/packages/aria_mcp/server.py +1705 -0
- aria_code/packages/aria_mcp/tools.py +81 -0
- aria_code/packages/aria_sdk/__init__.py +19 -0
- aria_code/packages/aria_sdk/client.py +393 -0
- aria_code/packages/aria_sdk/providers.py +70 -0
- aria_code/packages/aria_sdk/streaming.py +75 -0
- aria_code/packages/aria_sdk/types.py +86 -0
- aria_code/packages/aria_services/__init__.py +92 -0
- aria_code/packages/aria_services/cache.py +60 -0
- aria_code/packages/aria_services/context.py +356 -0
- aria_code/packages/aria_services/context_checkpoints.py +157 -0
- aria_code/packages/aria_services/data.py +11 -0
- aria_code/packages/aria_services/provider_health.py +304 -0
- aria_code/packages/aria_services/references.py +247 -0
- aria_code/packages/aria_services/registry.py +249 -0
- aria_code/packages/aria_services/research_protocol.py +137 -0
- aria_code/packages/aria_services/research_quality.py +186 -0
- aria_code/packages/aria_services/settings.py +160 -0
- aria_code/packages/aria_services/usage.py +151 -0
- aria_code/packages/aria_skills/__init__.py +46 -0
- aria_code/packages/aria_skills/catalog.py +149 -0
- aria_code/packages/aria_skills/loader.py +455 -0
- aria_code/packages/aria_skills/registry.py +84 -0
- aria_code/packages/aria_tools/__init__.py +5 -0
- aria_code/packages/aria_tools/financial/__init__.py +5 -0
- aria_code/packages/aria_tools/financial/_paths.py +92 -0
- aria_code/packages/aria_tools/registry.py +129 -0
- aria_code/packages/quant_engine/__init__.py +33 -0
- aria_code/packages/quant_engine/_namespace.py +24 -0
- aria_code/packages/quant_engine/backtest/__init__.py +10 -0
- aria_code/packages/quant_engine/backtest/core.py +53 -0
- aria_code/packages/quant_engine/backtest/engine.py +102 -0
- aria_code/packages/quant_engine/portfolio/__init__.py +9 -0
- aria_code/packages/quant_engine/portfolio/optimizer.py +85 -0
- aria_code/packages/quant_engine/sports/__init__.py +76 -0
- aria_code/packages/quant_engine/sports/calibrator.py +354 -0
- aria_code/packages/quant_engine/sports/dixon_coles.py +234 -0
- aria_code/packages/quant_engine/sports/elo.py +300 -0
- aria_code/packages/quant_engine/sports/form.py +188 -0
- aria_code/packages/quant_engine/sports/h2h.py +195 -0
- aria_code/packages/quant_engine/sports/predictor.py +312 -0
- aria_code/packages/quant_engine/sports/tracker.py +665 -0
- aria_code/packages/quant_engine/stochastic/__init__.py +31 -0
- aria_code/packages/quant_engine/stochastic/gbm_enhanced.py +195 -0
- aria_code/packages/quant_engine/stochastic/ito_calculus.py +477 -0
- aria_code/packages/quant_engine/stochastic/kelly_criterion.py +181 -0
- aria_code/packages/quant_engine/stochastic/monte_carlo_advanced.py +95 -0
- aria_code/packages/quant_engine/stochastic/options_pricing.py +576 -0
- aria_code/packages/quant_engine/stochastic/stochastic_processes.py +90 -0
- aria_code/packs/__init__.py +85 -0
- aria_code/packs/base.py +168 -0
- aria_code/packs/finance/__init__.py +250 -0
- aria_code/packs/healthcare/__init__.py +40 -0
- aria_code/packs/logistics/__init__.py +171 -0
- aria_code/packs/payments/__init__.py +139 -0
- aria_code/packs/realty/__init__.py +190 -0
- aria_code/packs/registry.py +202 -0
- aria_code/packs/rules.py +90 -0
- aria_code/plan_utils.py +194 -0
- aria_code/plugin_loader.py +328 -0
- aria_code/portfolio_ledger.py +4 -0
- aria_code/preview_server.py +449 -0
- aria_code/privacy/__init__.py +5 -0
- aria_code/privacy/feedback.py +123 -0
- aria_code/project_review.py +278 -0
- aria_code/project_review_api.py +235 -0
- aria_code/project_review_server.py +18 -0
- aria_code/project_tools.py +4 -0
- aria_code/providers/__init__.py +30 -0
- aria_code/providers/google_video_provider.py +256 -0
- aria_code/providers/llm/__init__.py +19 -0
- aria_code/providers/llm/anthropic.py +184 -0
- aria_code/providers/llm/arthera_gateway.py +90 -0
- aria_code/providers/llm/autoconfig.py +213 -0
- aria_code/providers/llm/base.py +139 -0
- aria_code/providers/llm/custom.py +158 -0
- aria_code/providers/llm/ollama.py +137 -0
- aria_code/providers/llm/openai_compat.py +350 -0
- aria_code/providers/llm/registry.py +457 -0
- aria_code/providers/local_image_provider.py +202 -0
- aria_code/providers/local_llm_provider.py +549 -0
- aria_code/providers/ollama_models.py +79 -0
- aria_code/providers/openai_params.py +32 -0
- aria_code/providers/tool_messages.py +100 -0
- aria_code/realty_data_tools.py +4 -0
- aria_code/report_exporters.py +150 -0
- aria_code/report_generator.py +1764 -0
- aria_code/review_service.py +497 -0
- aria_code/runtime/__init__.py +176 -0
- aria_code/runtime/acceptance.py +632 -0
- aria_code/runtime/agent_loop.py +1851 -0
- aria_code/runtime/agent_profiles.py +75 -0
- aria_code/runtime/approval.py +71 -0
- aria_code/runtime/baseline.py +219 -0
- aria_code/runtime/budget.py +232 -0
- aria_code/runtime/checkpoints.py +481 -0
- aria_code/runtime/consult.py +65 -0
- aria_code/runtime/contract.py +282 -0
- aria_code/runtime/delivery.py +432 -0
- aria_code/runtime/events.py +113 -0
- aria_code/runtime/gateway.py +167 -0
- aria_code/runtime/lsp.py +484 -0
- aria_code/runtime/native_index.py +153 -0
- aria_code/runtime/orchestrator.py +55 -0
- aria_code/runtime/processes.py +343 -0
- aria_code/runtime/project_graph.py +614 -0
- aria_code/runtime/repo_map.py +712 -0
- aria_code/runtime/research_bundle.py +105 -0
- aria_code/runtime/review.py +269 -0
- aria_code/runtime/run_state.py +127 -0
- aria_code/runtime/run_store.py +522 -0
- aria_code/runtime/scoped_runs.py +113 -0
- aria_code/runtime/self_healing.py +422 -0
- aria_code/runtime/semantic_diff.py +171 -0
- aria_code/runtime/subagent.py +721 -0
- aria_code/runtime/symbol_edit.py +117 -0
- aria_code/runtime/task_brief.py +46 -0
- aria_code/runtime/task_context.py +145 -0
- aria_code/runtime/task_ledger.py +108 -0
- aria_code/runtime/task_orchestrator.py +90 -0
- aria_code/runtime/task_outcome.py +33 -0
- aria_code/runtime/task_verification.py +266 -0
- aria_code/runtime/task_worktree.py +448 -0
- aria_code/runtime/tool_executor.py +329 -0
- aria_code/runtime/tool_policy.py +107 -0
- aria_code/runtime/transactions.py +216 -0
- aria_code/runtime/windows_job.py +115 -0
- aria_code/runtime/workflows.py +222 -0
- aria_code/runtime/worktrees.py +215 -0
- aria_code/runway_video_client.py +4 -0
- aria_code/safety/__init__.py +27 -0
- aria_code/safety/client_isolation.py +178 -0
- aria_code/safety/permissions.py +339 -0
- aria_code/safety/risk.py +699 -0
- aria_code/safety/sandbox.py +148 -0
- aria_code/safety/service.py +102 -0
- aria_code/service_endpoints.py +14 -0
- aria_code/setup_wizard.py +684 -0
- aria_code/spreadsheet_tools.py +4 -0
- aria_code/strategy_vault.py +4 -0
- aria_code/tools/__init__.py +0 -0
- aria_code/tools/artifact_tools.py +252 -0
- aria_code/tools/broker_tools.py +92 -0
- aria_code/tools/build_quant_engine.py +97 -0
- aria_code/tools/code_audit_tools.py +192 -0
- aria_code/tools/computer_use_tools.py +509 -0
- aria_code/tools/data_analysis_tools.py +831 -0
- aria_code/tools/enterprise_finance_tools.py +202 -0
- aria_code/tools/extended_tools.py +240 -0
- aria_code/tools/file_analysis_tools.py +880 -0
- aria_code/tools/image_gen_tools.py +114 -0
- aria_code/tools/local_finance_tools.py +3488 -0
- aria_code/tools/logistics_carriers.py +11 -0
- aria_code/tools/logistics_inventory.py +11 -0
- aria_code/tools/logistics_io.py +11 -0
- aria_code/tools/logistics_tenancy.py +11 -0
- aria_code/tools/logistics_tools.py +11 -0
- aria_code/tools/macro_tools.py +412 -0
- aria_code/tools/notification_tools.py +249 -0
- aria_code/tools/project_tools.py +525 -0
- aria_code/tools/realty_data_tools.py +662 -0
- aria_code/tools/spreadsheet_tools.py +399 -0
- aria_code/tools/stripe_tools.py +115 -0
- aria_code/tools/video_tools.py +500 -0
- aria_code/training/__init__.py +1 -0
- aria_code/training/compare.py +117 -0
- aria_code/training/prelude_commands.py +111 -0
- aria_code/training/prelude_eval.py +390 -0
- aria_code/training/prelude_real.py +183 -0
- aria_code/training/prelude_train.py +248 -0
- aria_code/training/sft_dataset.py +230 -0
- aria_code/training/trajectory_dataset.py +263 -0
- aria_code/ui/__init__.py +105 -0
- aria_code/ui/banner.py +404 -0
- aria_code/ui/completer.py +571 -0
- aria_code/ui/console.py +285 -0
- aria_code/ui/input_box.py +505 -0
- aria_code/ui/mentions.py +206 -0
- aria_code/ui/picker.py +248 -0
- aria_code/ui/render/__init__.py +11 -0
- aria_code/ui/render/actions.py +297 -0
- aria_code/ui/render/finance.py +1732 -0
- aria_code/ui/render/market.py +253 -0
- aria_code/ui/render/output.py +954 -0
- aria_code/ui/render/responsive.py +75 -0
- aria_code/ui/render/team.py +367 -0
- aria_code/ui/robot.py +239 -0
- aria_code/ui/startup_dashboard.py +110 -0
- aria_code/video_analysis.py +147 -0
- aria_code/video_editor.py +518 -0
- aria_code/video_workflow/__init__.py +8 -0
- aria_code/video_workflow/__main__.py +61 -0
- aria_code/video_workflow/engines/__init__.py +18 -0
- aria_code/video_workflow/engines/base.py +69 -0
- aria_code/video_workflow/engines/custom_model.py +204 -0
- aria_code/video_workflow/engines/google_veo.py +94 -0
- aria_code/video_workflow/engines/local_engine.py +71 -0
- aria_code/video_workflow/engines/registry.py +98 -0
- aria_code/video_workflow/google_models.py +278 -0
- aria_code/video_workflow/music/__init__.py +5 -0
- aria_code/video_workflow/music/foley_service.py +132 -0
- aria_code/video_workflow/music/service.py +212 -0
- aria_code/video_workflow/pipeline.py +620 -0
- aria_code/video_workflow/plan.py +202 -0
- aria_code/video_workflow/session.py +297 -0
- aria_code/workspace/__init__.py +6 -0
- aria_code/workspace/files.py +219 -0
- aria_code/workspace/verify.py +167 -0
- aria_code/workspace_changes.py +199 -0
- aria_code/workspace_service.py +196 -0
- aria_code/workspace_tools.py +150 -0
- aria_code-0.167.0.dist-info/METADATA +330 -0
- aria_code-0.167.0.dist-info/RECORD +601 -0
- aria_code-0.167.0.dist-info/WHEEL +5 -0
- aria_code-0.167.0.dist-info/entry_points.txt +5 -0
- aria_code-0.167.0.dist-info/licenses/LICENSE +201 -0
- aria_code-0.167.0.dist-info/licenses/NOTICE +49 -0
- aria_code-0.167.0.dist-info/top_level.txt +2 -0
aria_code/mcp_client.py
ADDED
|
@@ -0,0 +1,744 @@
|
|
|
1
|
+
"""
|
|
2
|
+
mcp_client.py — MCP (Model Context Protocol) client for Aria Code.
|
|
3
|
+
|
|
4
|
+
Connects to any MCP server via stdio transport and exposes its tools as
|
|
5
|
+
first-class LOCAL_TOOLS in the Aria Code tool loop.
|
|
6
|
+
|
|
7
|
+
Config file: ~/.arthera/mcp_servers.json
|
|
8
|
+
Example::
|
|
9
|
+
|
|
10
|
+
{
|
|
11
|
+
"servers": [
|
|
12
|
+
{
|
|
13
|
+
"name": "quant_engine",
|
|
14
|
+
"command": "python3",
|
|
15
|
+
"args": ["/path/to/mcp_server.py"],
|
|
16
|
+
"env": {"PYTHONPATH": "/path/to/project"},
|
|
17
|
+
"description": "Arthera quant engine MCP tools"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"name": "filesystem",
|
|
21
|
+
"command": "npx",
|
|
22
|
+
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/projects"],
|
|
23
|
+
"description": "Filesystem access"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"name": "apify",
|
|
27
|
+
"command": "npx",
|
|
28
|
+
"args": ["-y", "@apify/actors-mcp-server@latest", "--tools", "actors,docs,apify/rag-web-browser"],
|
|
29
|
+
"env": {"APIFY_TOKEN": "${APIFY_TOKEN}"},
|
|
30
|
+
"enabled": false,
|
|
31
|
+
"description": "Approved Apify web research Actors (enable only after token and data-use review)"
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
Usage::
|
|
37
|
+
|
|
38
|
+
registry = MCPToolRegistry()
|
|
39
|
+
await registry.start_all()
|
|
40
|
+
registry.register_into(LOCAL_TOOLS, LOCAL_TOOL_SCHEMAS)
|
|
41
|
+
|
|
42
|
+
# Tools are now callable as regular LOCAL_TOOLS entries
|
|
43
|
+
result = await registry.call_tool("quant_engine/backtest_strategy", {"symbol": "AAPL"})
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
from __future__ import annotations
|
|
47
|
+
|
|
48
|
+
import asyncio
|
|
49
|
+
import base64
|
|
50
|
+
import json
|
|
51
|
+
import logging
|
|
52
|
+
import os
|
|
53
|
+
import pathlib
|
|
54
|
+
import re
|
|
55
|
+
import traceback
|
|
56
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
57
|
+
|
|
58
|
+
from aria_code.packages.aria_mcp.circuit import ServerCircuit
|
|
59
|
+
from aria_code.packages.aria_core.paths import aria_home
|
|
60
|
+
|
|
61
|
+
logger = logging.getLogger(__name__)
|
|
62
|
+
|
|
63
|
+
MCP_CONFIG_PATH = aria_home() / "mcp_servers.json"
|
|
64
|
+
|
|
65
|
+
# MCP JSON-RPC protocol version
|
|
66
|
+
MCP_PROTOCOL_VERSION = "2024-11-05"
|
|
67
|
+
|
|
68
|
+
# Marks a call that failed because the server could not be reached at all
|
|
69
|
+
# (timeout, closed pipe, exited process). Only these, and the explicit
|
|
70
|
+
# server-side "could not produce a result" codes below, count toward the
|
|
71
|
+
# per-server circuit breaker; a tool refusing its input does not.
|
|
72
|
+
TRANSPORT_FAILURE = "transport"
|
|
73
|
+
SERVER_UNAVAILABLE_CODES = frozenset({"source_unavailable", "source_timeout"})
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def model_safe_tool_name(server_name: str, tool_name: str) -> str:
|
|
77
|
+
"""Return an OpenAI/Ollama-compatible qualified MCP function name."""
|
|
78
|
+
server = re.sub(r"[^A-Za-z0-9_-]+", "_", str(server_name)).strip("_") or "server"
|
|
79
|
+
tool = re.sub(r"[^A-Za-z0-9_-]+", "_", str(tool_name)).strip("_") or "tool"
|
|
80
|
+
return f"mcp__{server}__{tool}"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# ---------------------------------------------------------------------------
|
|
84
|
+
# JSON-RPC helpers
|
|
85
|
+
# ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
def _make_request(method: str, params: Any = None, req_id: int = 1) -> bytes:
|
|
88
|
+
msg = {"jsonrpc": "2.0", "id": req_id, "method": method}
|
|
89
|
+
if params is not None:
|
|
90
|
+
msg["params"] = params
|
|
91
|
+
return (json.dumps(msg) + "\n").encode()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _make_notification(method: str, params: Any = None) -> bytes:
|
|
95
|
+
msg = {"jsonrpc": "2.0", "method": method}
|
|
96
|
+
if params is not None:
|
|
97
|
+
msg["params"] = params
|
|
98
|
+
return (json.dumps(msg) + "\n").encode()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# ---------------------------------------------------------------------------
|
|
102
|
+
# MCPServer — one subprocess connection
|
|
103
|
+
# ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
class MCPServer:
|
|
106
|
+
"""
|
|
107
|
+
Manages a single MCP server subprocess (stdio transport).
|
|
108
|
+
|
|
109
|
+
Lifecycle: start() → list_tools() → call_tool(…) → stop()
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
def __init__(
|
|
113
|
+
self,
|
|
114
|
+
name: str,
|
|
115
|
+
command: str,
|
|
116
|
+
args: List[str],
|
|
117
|
+
env: Optional[Dict[str, str]] = None,
|
|
118
|
+
description: str = "",
|
|
119
|
+
timeout: float = 30.0,
|
|
120
|
+
):
|
|
121
|
+
self.name = name
|
|
122
|
+
self.command = command
|
|
123
|
+
self.args = args
|
|
124
|
+
self.env = env or {}
|
|
125
|
+
self.description = description
|
|
126
|
+
self.timeout = timeout
|
|
127
|
+
|
|
128
|
+
self._proc: Optional[asyncio.subprocess.Process] = None
|
|
129
|
+
self._req_id: int = 0
|
|
130
|
+
self._pending: Dict[int, asyncio.Future] = {}
|
|
131
|
+
self._reader_task: Optional[asyncio.Task] = None
|
|
132
|
+
self._tools: List[Dict[str, Any]] = []
|
|
133
|
+
self._running = False
|
|
134
|
+
|
|
135
|
+
# ── internal ───────────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
def _next_id(self) -> int:
|
|
138
|
+
self._req_id += 1
|
|
139
|
+
return self._req_id
|
|
140
|
+
|
|
141
|
+
async def _send(self, data: bytes):
|
|
142
|
+
if self._proc and self._proc.stdin:
|
|
143
|
+
self._proc.stdin.write(data)
|
|
144
|
+
await self._proc.stdin.drain()
|
|
145
|
+
|
|
146
|
+
async def _reader_loop(self):
|
|
147
|
+
"""Background task: read lines from server stdout and dispatch to futures."""
|
|
148
|
+
if not self._proc or not self._proc.stdout:
|
|
149
|
+
return
|
|
150
|
+
try:
|
|
151
|
+
async for raw in self._proc.stdout:
|
|
152
|
+
line = raw.decode("utf-8", errors="ignore").strip()
|
|
153
|
+
if not line:
|
|
154
|
+
continue
|
|
155
|
+
try:
|
|
156
|
+
msg = json.loads(line)
|
|
157
|
+
except json.JSONDecodeError:
|
|
158
|
+
logger.debug("[MCP:%s] Non-JSON: %s", self.name, line[:100])
|
|
159
|
+
continue
|
|
160
|
+
|
|
161
|
+
req_id = msg.get("id")
|
|
162
|
+
if req_id is not None and req_id in self._pending:
|
|
163
|
+
fut = self._pending.pop(req_id)
|
|
164
|
+
if not fut.done():
|
|
165
|
+
if "error" in msg:
|
|
166
|
+
fut.set_exception(RuntimeError(str(msg["error"])))
|
|
167
|
+
else:
|
|
168
|
+
fut.set_result(msg.get("result"))
|
|
169
|
+
except asyncio.CancelledError:
|
|
170
|
+
return # stop() cancelled us; nothing is waiting on an answer
|
|
171
|
+
except Exception as exc:
|
|
172
|
+
logger.debug("[MCP:%s] Reader loop error: %s", self.name, exc)
|
|
173
|
+
# stdout closed: the server has exited. Nothing can answer a request that
|
|
174
|
+
# is still waiting, so fail those now. Without this, a server that
|
|
175
|
+
# refuses to start (a missing environment variable, say) is waited on
|
|
176
|
+
# for the whole timeout, 30 s at every launch, and the reason it gave on
|
|
177
|
+
# stderr is never shown.
|
|
178
|
+
await self._fail_pending()
|
|
179
|
+
|
|
180
|
+
async def _fail_pending(self):
|
|
181
|
+
reason = await self._exit_reason()
|
|
182
|
+
for rid, fut in list(self._pending.items()):
|
|
183
|
+
self._pending.pop(rid, None)
|
|
184
|
+
if not fut.done():
|
|
185
|
+
fut.set_exception(RuntimeError(f"MCP server {self.name!r} exited before answering{reason}"))
|
|
186
|
+
|
|
187
|
+
async def _exit_reason(self) -> str:
|
|
188
|
+
"""Exit code and the server's own last words, for the error message."""
|
|
189
|
+
proc = self._proc
|
|
190
|
+
if proc is None:
|
|
191
|
+
return ""
|
|
192
|
+
try:
|
|
193
|
+
code = await asyncio.wait_for(proc.wait(), timeout=2)
|
|
194
|
+
except Exception:
|
|
195
|
+
code = None
|
|
196
|
+
said = ""
|
|
197
|
+
try:
|
|
198
|
+
if proc.stderr is not None:
|
|
199
|
+
said = (await asyncio.wait_for(proc.stderr.read(2000), timeout=1)).decode("utf-8", errors="ignore").strip()
|
|
200
|
+
except Exception:
|
|
201
|
+
pass
|
|
202
|
+
said = " ".join(said.split())[:300]
|
|
203
|
+
detail = f" (exit {code})" if code is not None else ""
|
|
204
|
+
return f"{detail}: {said}" if said else detail
|
|
205
|
+
|
|
206
|
+
async def _request(self, method: str, params: Any = None) -> Any:
|
|
207
|
+
if self._proc is not None and self._proc.returncode is not None:
|
|
208
|
+
raise RuntimeError(f"MCP server {self.name!r} has exited (exit {self._proc.returncode})")
|
|
209
|
+
rid = self._next_id()
|
|
210
|
+
fut = asyncio.get_event_loop().create_future()
|
|
211
|
+
self._pending[rid] = fut
|
|
212
|
+
try:
|
|
213
|
+
await self._send(_make_request(method, params, rid))
|
|
214
|
+
except (BrokenPipeError, ConnectionResetError) as exc:
|
|
215
|
+
self._pending.pop(rid, None)
|
|
216
|
+
raise RuntimeError(f"MCP server {self.name!r} closed its input: {exc}") from exc
|
|
217
|
+
try:
|
|
218
|
+
return await asyncio.wait_for(fut, timeout=self.timeout)
|
|
219
|
+
except asyncio.TimeoutError:
|
|
220
|
+
self._pending.pop(rid, None)
|
|
221
|
+
raise TimeoutError(f"MCP {self.name}/{method} timed out after {self.timeout}s")
|
|
222
|
+
|
|
223
|
+
# ── public API ─────────────────────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
async def start(self) -> bool:
|
|
226
|
+
"""Start the server subprocess and perform MCP handshake."""
|
|
227
|
+
if self._running:
|
|
228
|
+
return True
|
|
229
|
+
try:
|
|
230
|
+
env = {**os.environ, **self.env}
|
|
231
|
+
self._proc = await asyncio.create_subprocess_exec(
|
|
232
|
+
self.command, *self.args,
|
|
233
|
+
stdin=asyncio.subprocess.PIPE,
|
|
234
|
+
stdout=asyncio.subprocess.PIPE,
|
|
235
|
+
stderr=asyncio.subprocess.PIPE,
|
|
236
|
+
# PNG MCP content routinely exceeds asyncio's 64 KiB line limit.
|
|
237
|
+
limit=24 << 20,
|
|
238
|
+
env=env,
|
|
239
|
+
)
|
|
240
|
+
except FileNotFoundError as exc:
|
|
241
|
+
logger.warning("[MCP:%s] Command not found: %s", self.name, exc)
|
|
242
|
+
return False
|
|
243
|
+
except Exception as exc:
|
|
244
|
+
logger.warning("[MCP:%s] Failed to start: %s", self.name, exc)
|
|
245
|
+
return False
|
|
246
|
+
|
|
247
|
+
self._reader_task = asyncio.create_task(self._reader_loop())
|
|
248
|
+
|
|
249
|
+
# MCP initialize handshake
|
|
250
|
+
try:
|
|
251
|
+
await self._request("initialize", {
|
|
252
|
+
"protocolVersion": MCP_PROTOCOL_VERSION,
|
|
253
|
+
"capabilities": {"tools": {}},
|
|
254
|
+
"clientInfo": {"name": "aria-code", "version": "3.0"},
|
|
255
|
+
})
|
|
256
|
+
await self._send(_make_notification("notifications/initialized"))
|
|
257
|
+
except Exception as exc:
|
|
258
|
+
logger.warning("[MCP:%s] Handshake failed: %s", self.name, exc)
|
|
259
|
+
await self.stop()
|
|
260
|
+
return False
|
|
261
|
+
|
|
262
|
+
# Discover tools
|
|
263
|
+
await self._refresh_tools()
|
|
264
|
+
self._running = True
|
|
265
|
+
logger.info("[MCP:%s] Started with %d tools", self.name, len(self._tools))
|
|
266
|
+
return True
|
|
267
|
+
|
|
268
|
+
async def _refresh_tools(self):
|
|
269
|
+
try:
|
|
270
|
+
result = await self._request("tools/list")
|
|
271
|
+
self._tools = result.get("tools", []) if result else []
|
|
272
|
+
except Exception as exc:
|
|
273
|
+
logger.debug("[MCP:%s] tools/list failed: %s", self.name, exc)
|
|
274
|
+
self._tools = []
|
|
275
|
+
|
|
276
|
+
@property
|
|
277
|
+
def tools(self) -> List[Dict[str, Any]]:
|
|
278
|
+
return self._tools
|
|
279
|
+
|
|
280
|
+
async def call_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
|
281
|
+
"""Call a tool on this server. Returns {"success": bool, "result": …}."""
|
|
282
|
+
if not self._running:
|
|
283
|
+
return {"success": False, "error": f"MCP server {self.name!r} not running",
|
|
284
|
+
"failure_kind": TRANSPORT_FAILURE}
|
|
285
|
+
# Strip server prefix if present (e.g. "quant_engine/backtest_strategy")
|
|
286
|
+
short_name = tool_name.split("/")[-1]
|
|
287
|
+
try:
|
|
288
|
+
result = await self._request("tools/call", {
|
|
289
|
+
"name": short_name,
|
|
290
|
+
"arguments": arguments,
|
|
291
|
+
})
|
|
292
|
+
if result is None:
|
|
293
|
+
return {"success": True, "result": None}
|
|
294
|
+
if not isinstance(result, dict):
|
|
295
|
+
return {"success": False, "error": "Invalid MCP tool result"}
|
|
296
|
+
if "isError" in result and not isinstance(result["isError"], bool):
|
|
297
|
+
return {"success": False, "error": "Invalid MCP tool error flag"}
|
|
298
|
+
if result.get("isError"):
|
|
299
|
+
error_content = result.get("content", [])
|
|
300
|
+
detail = "\n".join(str(item.get("text", "")) for item in error_content
|
|
301
|
+
if isinstance(item, dict)) if isinstance(error_content, list) else ""
|
|
302
|
+
return {"success": False, "error_code": "mcp_tool_error",
|
|
303
|
+
"error": detail[:4000] or "MCP tool reported an error"}
|
|
304
|
+
# MCP returns {content: [{type, text}]} or plain value
|
|
305
|
+
content = result.get("content", result)
|
|
306
|
+
if isinstance(content, list):
|
|
307
|
+
vision = {}
|
|
308
|
+
for item in content:
|
|
309
|
+
if isinstance(item, dict) and item.get("type") == "image" and item.get("mimeType") == "image/png":
|
|
310
|
+
data = item.get("data", "")
|
|
311
|
+
if isinstance(data, str) and len(data) <= 20_000_000:
|
|
312
|
+
try:
|
|
313
|
+
base64.b64decode(data, validate=True)
|
|
314
|
+
vision = {"_vision_image": data}
|
|
315
|
+
break
|
|
316
|
+
except ValueError:
|
|
317
|
+
pass
|
|
318
|
+
parts = [c.get("text", "") if isinstance(c, dict) else str(c) for c in content]
|
|
319
|
+
text = "\n".join(parts)
|
|
320
|
+
try:
|
|
321
|
+
parsed = json.loads(text)
|
|
322
|
+
except Exception:
|
|
323
|
+
return {"success": True, "result": text, **vision}
|
|
324
|
+
if isinstance(parsed, dict):
|
|
325
|
+
# Only this client decides what was a transport failure;
|
|
326
|
+
# a server cannot claim one (or hide one) via its payload.
|
|
327
|
+
parsed.pop("failure_kind", None)
|
|
328
|
+
return {"success": True, **parsed, **vision}
|
|
329
|
+
return {"success": True, "result": parsed, **vision}
|
|
330
|
+
return {"success": True, "result": content}
|
|
331
|
+
except Exception as exc:
|
|
332
|
+
# Timeout, closed pipe, exited process: the server itself failed.
|
|
333
|
+
return {"success": False, "error": str(exc), "failure_kind": TRANSPORT_FAILURE}
|
|
334
|
+
|
|
335
|
+
@property
|
|
336
|
+
def is_alive(self) -> bool:
|
|
337
|
+
"""Process exists and has not exited (handshake state tracked separately)."""
|
|
338
|
+
return self._proc is not None and self._proc.returncode is None
|
|
339
|
+
|
|
340
|
+
async def restart(self) -> bool:
|
|
341
|
+
"""Tear down and re-launch the subprocess (reconnect policy)."""
|
|
342
|
+
await self.stop()
|
|
343
|
+
return await self.start()
|
|
344
|
+
|
|
345
|
+
async def stop(self):
|
|
346
|
+
self._running = False
|
|
347
|
+
if self._reader_task:
|
|
348
|
+
self._reader_task.cancel()
|
|
349
|
+
try:
|
|
350
|
+
await asyncio.wait_for(self._reader_task, timeout=1.0)
|
|
351
|
+
except Exception:
|
|
352
|
+
pass
|
|
353
|
+
self._reader_task = None
|
|
354
|
+
if self._proc:
|
|
355
|
+
try:
|
|
356
|
+
self._proc.stdin.close()
|
|
357
|
+
await asyncio.wait_for(self._proc.wait(), timeout=3.0)
|
|
358
|
+
except Exception:
|
|
359
|
+
try:
|
|
360
|
+
self._proc.kill()
|
|
361
|
+
except Exception:
|
|
362
|
+
pass
|
|
363
|
+
self._proc = None
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
# ---------------------------------------------------------------------------
|
|
367
|
+
# MCPToolRegistry — manages multiple servers + tool registration
|
|
368
|
+
# ---------------------------------------------------------------------------
|
|
369
|
+
|
|
370
|
+
class MCPToolRegistry:
|
|
371
|
+
"""
|
|
372
|
+
Loads MCP server config from ``~/.arthera/mcp_servers.json``, starts
|
|
373
|
+
each server, discovers tools, and registers them in the CLI's tool loop.
|
|
374
|
+
|
|
375
|
+
Internal naming convention: ``{server_name}/{tool_name}``.
|
|
376
|
+
Model-facing schemas use ``mcp__{server_name}__{tool_name}`` because tool
|
|
377
|
+
APIs reject slashes in function names.
|
|
378
|
+
This avoids collisions when two servers expose a tool with the same name.
|
|
379
|
+
"""
|
|
380
|
+
|
|
381
|
+
def __init__(self, config_path: pathlib.Path = MCP_CONFIG_PATH):
|
|
382
|
+
self.config_path = config_path
|
|
383
|
+
self._servers: Dict[str, MCPServer] = {}
|
|
384
|
+
self._tool_map: Dict[str, Tuple[str, str]] = {} # qualified_name → (server_name, tool_name)
|
|
385
|
+
self._circuits: Dict[str, "ServerCircuit"] = {} # per-server failure isolation
|
|
386
|
+
self._loaded = False
|
|
387
|
+
self._event_loop: Optional[asyncio.AbstractEventLoop] = None
|
|
388
|
+
|
|
389
|
+
# ── config ─────────────────────────────────────────────────────────────
|
|
390
|
+
|
|
391
|
+
def load_config(self) -> List[Dict[str, Any]]:
|
|
392
|
+
if not self.config_path.exists():
|
|
393
|
+
return []
|
|
394
|
+
try:
|
|
395
|
+
with open(self.config_path) as f:
|
|
396
|
+
data = json.load(f)
|
|
397
|
+
return data.get("servers", [])
|
|
398
|
+
except Exception as exc:
|
|
399
|
+
logger.warning("MCP config load failed: %s", exc)
|
|
400
|
+
return []
|
|
401
|
+
|
|
402
|
+
def save_example_config(self):
|
|
403
|
+
"""Write an example mcp_servers.json if none exists."""
|
|
404
|
+
if self.config_path.exists():
|
|
405
|
+
return
|
|
406
|
+
self.config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
407
|
+
example = {
|
|
408
|
+
"servers": [
|
|
409
|
+
{
|
|
410
|
+
"name": "quant_engine",
|
|
411
|
+
"command": "python3",
|
|
412
|
+
"args": ["path/to/mcp_server.py"],
|
|
413
|
+
"env": {"PYTHONPATH": "path/to/project"},
|
|
414
|
+
"description": "Your Arthera quant engine (backtest, signals, factors)"
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
"name": "apify",
|
|
418
|
+
"command": "npx",
|
|
419
|
+
"args": ["-y", "@apify/actors-mcp-server@latest", "--tools", "actors,docs,apify/rag-web-browser"],
|
|
420
|
+
"env": {"APIFY_TOKEN": "${APIFY_TOKEN}"},
|
|
421
|
+
"enabled": False,
|
|
422
|
+
"description": "Apify web research (review Actor terms and enable explicitly)"
|
|
423
|
+
}
|
|
424
|
+
]
|
|
425
|
+
}
|
|
426
|
+
with open(self.config_path, "w") as f:
|
|
427
|
+
json.dump(example, f, indent=2, ensure_ascii=False)
|
|
428
|
+
|
|
429
|
+
# ── lifecycle ──────────────────────────────────────────────────────────
|
|
430
|
+
|
|
431
|
+
async def start_all(self) -> Dict[str, bool]:
|
|
432
|
+
"""Start all configured MCP servers. Returns {name: started_ok}."""
|
|
433
|
+
self._event_loop = asyncio.get_running_loop()
|
|
434
|
+
server_configs = self.load_config()
|
|
435
|
+
results = {}
|
|
436
|
+
for cfg in server_configs:
|
|
437
|
+
name = cfg.get("name", "unnamed")
|
|
438
|
+
command = cfg.get("command", "")
|
|
439
|
+
args = cfg.get("args", [])
|
|
440
|
+
env = cfg.get("env", {})
|
|
441
|
+
desc = cfg.get("description", "")
|
|
442
|
+
if not command:
|
|
443
|
+
continue
|
|
444
|
+
if not cfg.get("enabled", True):
|
|
445
|
+
logger.debug("MCP server %r disabled in config, skipping", name)
|
|
446
|
+
continue
|
|
447
|
+
# expand ${VAR} placeholders in args and env values
|
|
448
|
+
def _expand(s: str) -> str:
|
|
449
|
+
import re as _re
|
|
450
|
+
return _re.sub(r'\$\{(\w+)\}', lambda m: os.environ.get(m.group(1), m.group(0)), s)
|
|
451
|
+
args = [_expand(a) for a in args]
|
|
452
|
+
env = {k: _expand(v) for k, v in env.items() if v}
|
|
453
|
+
try:
|
|
454
|
+
timeout = float(cfg.get("timeout", 30))
|
|
455
|
+
if not 1 <= timeout <= 600:
|
|
456
|
+
raise ValueError("timeout out of bounds")
|
|
457
|
+
except (TypeError, ValueError):
|
|
458
|
+
logger.warning("MCP server %r has an invalid timeout", name)
|
|
459
|
+
results[name] = False
|
|
460
|
+
continue
|
|
461
|
+
srv = MCPServer(name=name, command=command, args=args, env=env, description=desc, timeout=timeout)
|
|
462
|
+
ok = await srv.start()
|
|
463
|
+
if ok:
|
|
464
|
+
self._servers[name] = srv
|
|
465
|
+
for tool in srv.tools:
|
|
466
|
+
qualified = f"{name}/{tool['name']}"
|
|
467
|
+
self._tool_map[qualified] = (name, tool["name"])
|
|
468
|
+
results[name] = ok
|
|
469
|
+
self._loaded = True
|
|
470
|
+
return results
|
|
471
|
+
|
|
472
|
+
async def stop_all(self):
|
|
473
|
+
for srv in self._servers.values():
|
|
474
|
+
await srv.stop()
|
|
475
|
+
self._servers.clear()
|
|
476
|
+
self._tool_map.clear()
|
|
477
|
+
self._loaded = False
|
|
478
|
+
self._event_loop = None
|
|
479
|
+
|
|
480
|
+
# ── tool info ──────────────────────────────────────────────────────────
|
|
481
|
+
|
|
482
|
+
def all_tools(self) -> List[Dict[str, Any]]:
|
|
483
|
+
"""Return all discovered tools with qualified names."""
|
|
484
|
+
result = []
|
|
485
|
+
for srv_name, srv in self._servers.items():
|
|
486
|
+
for tool in srv.tools:
|
|
487
|
+
t = dict(tool)
|
|
488
|
+
t["qualified_name"] = f"{srv_name}/{tool['name']}"
|
|
489
|
+
t["server"] = srv_name
|
|
490
|
+
result.append(t)
|
|
491
|
+
return result
|
|
492
|
+
|
|
493
|
+
# ── call ───────────────────────────────────────────────────────────────
|
|
494
|
+
|
|
495
|
+
def _circuit_for(self, srv_name: str) -> ServerCircuit:
|
|
496
|
+
circuit = self._circuits.get(srv_name)
|
|
497
|
+
if circuit is None:
|
|
498
|
+
circuit = self._circuits[srv_name] = ServerCircuit()
|
|
499
|
+
return circuit
|
|
500
|
+
|
|
501
|
+
async def call_tool(self, qualified_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
|
502
|
+
if "/" in qualified_name:
|
|
503
|
+
srv_name, tool_name = qualified_name.split("/", 1)
|
|
504
|
+
else:
|
|
505
|
+
# Try to find by short name
|
|
506
|
+
srv_name, tool_name = self._resolve_short_name(qualified_name)
|
|
507
|
+
|
|
508
|
+
srv = self._servers.get(srv_name)
|
|
509
|
+
if srv is None:
|
|
510
|
+
return {"success": False, "error": f"MCP server {srv_name!r} not found"}
|
|
511
|
+
|
|
512
|
+
# ToolExecutor adds its own context to every call (_run_id,
|
|
513
|
+
# _session_id, _skip_confirm, ...) for local tools to read. An MCP
|
|
514
|
+
# server only knows the parameters in its schema, and a strict one
|
|
515
|
+
# rejects the whole call over an extra keyword, so they stop here.
|
|
516
|
+
arguments = {k: v for k, v in (arguments or {}).items() if not str(k).startswith("_")}
|
|
517
|
+
|
|
518
|
+
# Per-server failure isolation: a wedged/dead server fails fast instead
|
|
519
|
+
# of costing every call its full request timeout for the whole session.
|
|
520
|
+
circuit = self._circuit_for(srv_name)
|
|
521
|
+
if not circuit.allow_call():
|
|
522
|
+
return {
|
|
523
|
+
"success": False,
|
|
524
|
+
"error": (
|
|
525
|
+
f"MCP server {srv_name!r} temporarily disabled after repeated "
|
|
526
|
+
f"failures ({circuit.describe()}). It will be probed again "
|
|
527
|
+
f"automatically; /mcp reload {srv_name} forces a restart now."
|
|
528
|
+
),
|
|
529
|
+
"circuit": circuit.state,
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
# Reconnect policy: if the subprocess died, use this (probe) call to
|
|
533
|
+
# relaunch it before dispatching.
|
|
534
|
+
if not srv.is_alive:
|
|
535
|
+
logger.info("[MCP:%s] Process dead — attempting restart", srv_name)
|
|
536
|
+
if not await srv.restart():
|
|
537
|
+
circuit.record_failure()
|
|
538
|
+
return {
|
|
539
|
+
"success": False,
|
|
540
|
+
"error": f"MCP server {srv_name!r} is down and restart failed",
|
|
541
|
+
"circuit": circuit.state,
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
result = await srv.call_tool(tool_name, arguments)
|
|
545
|
+
|
|
546
|
+
# 上游限流(429/too many requests):短退避后重试一次。
|
|
547
|
+
# 与 market_data_client 的报价路径同策略,MCP 工具在此统一收口。
|
|
548
|
+
if not result.get("success") and self._looks_rate_limited(result):
|
|
549
|
+
logger.warning(
|
|
550
|
+
"[MCP:%s] %s rate-limited upstream; retrying once in 3s",
|
|
551
|
+
srv_name, tool_name,
|
|
552
|
+
)
|
|
553
|
+
await asyncio.sleep(3.0)
|
|
554
|
+
result = await srv.call_tool(tool_name, arguments)
|
|
555
|
+
|
|
556
|
+
if result.get("success"):
|
|
557
|
+
circuit.record_success()
|
|
558
|
+
elif self._looks_rate_limited(result):
|
|
559
|
+
# 限流是上游数据源配额问题,不是本服务器故障——不计入熔断,
|
|
560
|
+
# 否则一波限流会把整个服务器禁用,殃及无关工具。错误仍如实上抛。
|
|
561
|
+
pass
|
|
562
|
+
elif self._counts_as_server_failure(result):
|
|
563
|
+
circuit.record_failure()
|
|
564
|
+
else:
|
|
565
|
+
# A tool-level refusal (invalid_input, network_disabled,
|
|
566
|
+
# broker_access_disabled, an MCP isError result, ...) means the
|
|
567
|
+
# server is up and answered. Counting those opened the circuit
|
|
568
|
+
# after three bad arguments and disabled every tool on the server.
|
|
569
|
+
# The server answered, so it is healthy as far as the breaker cares.
|
|
570
|
+
circuit.record_success()
|
|
571
|
+
return result
|
|
572
|
+
|
|
573
|
+
@staticmethod
|
|
574
|
+
def _counts_as_server_failure(result: Dict[str, Any]) -> bool:
|
|
575
|
+
"""True when a failed call says the server, not the request, is broken."""
|
|
576
|
+
if result.get("failure_kind") == TRANSPORT_FAILURE:
|
|
577
|
+
return True
|
|
578
|
+
return str(result.get("error_code") or "") in SERVER_UNAVAILABLE_CODES
|
|
579
|
+
|
|
580
|
+
@staticmethod
|
|
581
|
+
def _looks_rate_limited(result: Dict[str, Any]) -> bool:
|
|
582
|
+
text = str(result.get("error") or result.get("result") or "").lower()
|
|
583
|
+
return any(t in text for t in ("rate limit", "rate-limit", "rate limited", "too many requests", "429"))
|
|
584
|
+
|
|
585
|
+
async def reload_server(self, srv_name: str) -> bool:
|
|
586
|
+
"""Force stop/start of one server and reset its circuit (reload policy)."""
|
|
587
|
+
srv = self._servers.get(srv_name)
|
|
588
|
+
if srv is None:
|
|
589
|
+
return False
|
|
590
|
+
ok = await srv.restart()
|
|
591
|
+
if ok:
|
|
592
|
+
self._circuit_for(srv_name).record_success()
|
|
593
|
+
self._tool_map = {q: st for q, st in self._tool_map.items() if st[0] != srv_name}
|
|
594
|
+
for tool in srv.tools:
|
|
595
|
+
self._tool_map[f"{srv_name}/{tool['name']}"] = (srv_name, tool["name"])
|
|
596
|
+
return ok
|
|
597
|
+
|
|
598
|
+
def _resolve_short_name(self, name: str) -> Tuple[str, str]:
|
|
599
|
+
for qname, (srv, tool) in self._tool_map.items():
|
|
600
|
+
if tool == name:
|
|
601
|
+
return srv, tool
|
|
602
|
+
return "unknown", name
|
|
603
|
+
|
|
604
|
+
# ── registration ───────────────────────────────────────────────────────
|
|
605
|
+
|
|
606
|
+
def register_into(
|
|
607
|
+
self,
|
|
608
|
+
tool_registry: Dict,
|
|
609
|
+
schema_registry: List,
|
|
610
|
+
overwrite: bool = False,
|
|
611
|
+
) -> int:
|
|
612
|
+
"""
|
|
613
|
+
Add all MCP tools into the CLI's LOCAL_TOOLS and LOCAL_TOOL_SCHEMAS.
|
|
614
|
+
|
|
615
|
+
Model-facing tool names use ``mcp__server__tool`` while handlers retain
|
|
616
|
+
the MCP server's internal ``server/tool`` qualified name.
|
|
617
|
+
|
|
618
|
+
Returns number of tools registered.
|
|
619
|
+
"""
|
|
620
|
+
import asyncio as _asyncio
|
|
621
|
+
|
|
622
|
+
added = 0
|
|
623
|
+
existing_names = set(tool_registry.keys())
|
|
624
|
+
|
|
625
|
+
for srv_name, srv in self._servers.items():
|
|
626
|
+
for tool in srv.tools:
|
|
627
|
+
qname = f"{srv_name}/{tool['name']}"
|
|
628
|
+
model_name = model_safe_tool_name(srv_name, tool["name"])
|
|
629
|
+
if model_name in existing_names and not overwrite:
|
|
630
|
+
continue
|
|
631
|
+
|
|
632
|
+
# ToolExecutor invokes synchronous handlers in a worker thread.
|
|
633
|
+
# Route the coroutine back to the event loop that owns the MCP
|
|
634
|
+
# subprocess streams; asyncio.run() in the worker creates a
|
|
635
|
+
# different loop and fails with cross-loop Future errors.
|
|
636
|
+
def _make_sync_handler(qn: str, timeout: float) -> Callable:
|
|
637
|
+
def _handler(params: dict) -> dict:
|
|
638
|
+
try:
|
|
639
|
+
current_loop = _asyncio.get_running_loop()
|
|
640
|
+
except RuntimeError:
|
|
641
|
+
current_loop = None
|
|
642
|
+
owner_loop = self._event_loop
|
|
643
|
+
if owner_loop and owner_loop.is_running():
|
|
644
|
+
if current_loop is owner_loop:
|
|
645
|
+
return {
|
|
646
|
+
"success": False,
|
|
647
|
+
"error": "MCP sync handler called on its owner event loop; use ToolExecutor.execute",
|
|
648
|
+
}
|
|
649
|
+
future = _asyncio.run_coroutine_threadsafe(
|
|
650
|
+
self.call_tool(qn, params), owner_loop
|
|
651
|
+
)
|
|
652
|
+
return future.result(timeout=timeout + 5)
|
|
653
|
+
return _asyncio.run(self.call_tool(qn, params))
|
|
654
|
+
return _handler
|
|
655
|
+
|
|
656
|
+
tool_registry[model_name] = (
|
|
657
|
+
_make_sync_handler(qname, getattr(srv, "timeout", 30)),
|
|
658
|
+
tool.get("description", f"MCP tool from {srv_name}"),
|
|
659
|
+
)
|
|
660
|
+
existing_names.add(model_name)
|
|
661
|
+
added += 1
|
|
662
|
+
|
|
663
|
+
# Build schema
|
|
664
|
+
input_schema = tool.get("inputSchema") or tool.get("parameters") or {
|
|
665
|
+
"type": "object", "properties": {}, "required": []
|
|
666
|
+
}
|
|
667
|
+
schema = {
|
|
668
|
+
"type": "function",
|
|
669
|
+
"function": {
|
|
670
|
+
"name": model_name,
|
|
671
|
+
"description": f"[{srv_name}] {tool.get('description', '')}",
|
|
672
|
+
"parameters": input_schema,
|
|
673
|
+
},
|
|
674
|
+
}
|
|
675
|
+
# Replace in place rather than append. The handler dict above
|
|
676
|
+
# is keyed by name so re-registering is idempotent there, but
|
|
677
|
+
# the schema list is not — reconnecting MCP (which core_cmds
|
|
678
|
+
# does with overwrite=True) appended a second copy of every
|
|
679
|
+
# tool. Most providers tolerate that and silently spend the
|
|
680
|
+
# context; Vertex rejects the whole request with "Duplicate
|
|
681
|
+
# function declaration found".
|
|
682
|
+
for index, existing in enumerate(schema_registry):
|
|
683
|
+
if (existing.get("function") or existing).get("name") == model_name:
|
|
684
|
+
schema_registry[index] = schema
|
|
685
|
+
break
|
|
686
|
+
else:
|
|
687
|
+
schema_registry.append(schema)
|
|
688
|
+
|
|
689
|
+
return added
|
|
690
|
+
|
|
691
|
+
# ── async call tool (for use inside async context) ─────────────────────
|
|
692
|
+
|
|
693
|
+
async def call_tool_async(
|
|
694
|
+
self,
|
|
695
|
+
qualified_name: str,
|
|
696
|
+
arguments: Dict[str, Any],
|
|
697
|
+
) -> Dict[str, Any]:
|
|
698
|
+
return await self.call_tool(qualified_name, arguments)
|
|
699
|
+
|
|
700
|
+
# ── status ─────────────────────────────────────────────────────────────
|
|
701
|
+
|
|
702
|
+
def status(self) -> List[Dict[str, Any]]:
|
|
703
|
+
return [
|
|
704
|
+
{
|
|
705
|
+
"name": name,
|
|
706
|
+
"running": srv._running,
|
|
707
|
+
"alive": srv.is_alive,
|
|
708
|
+
"circuit": self._circuit_for(name).describe(),
|
|
709
|
+
"tool_count": len(srv.tools),
|
|
710
|
+
"description": srv.description,
|
|
711
|
+
"tools": [t["name"] for t in srv.tools],
|
|
712
|
+
}
|
|
713
|
+
for name, srv in self._servers.items()
|
|
714
|
+
]
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
# ---------------------------------------------------------------------------
|
|
718
|
+
# Module-level singleton (lazy-initialized)
|
|
719
|
+
# ---------------------------------------------------------------------------
|
|
720
|
+
|
|
721
|
+
_registry: Optional[MCPToolRegistry] = None
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
def get_registry() -> MCPToolRegistry:
|
|
725
|
+
global _registry
|
|
726
|
+
if _registry is None:
|
|
727
|
+
_registry = MCPToolRegistry()
|
|
728
|
+
return _registry
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
async def init_mcp(tool_registry: Dict, schema_registry: List) -> Dict[str, bool]:
|
|
732
|
+
"""
|
|
733
|
+
Convenience function: load config, start all servers, register tools.
|
|
734
|
+
Call once at startup.
|
|
735
|
+
|
|
736
|
+
Returns {server_name: started_ok} dict.
|
|
737
|
+
"""
|
|
738
|
+
reg = get_registry()
|
|
739
|
+
results = await reg.start_all()
|
|
740
|
+
n = reg.register_into(tool_registry, schema_registry)
|
|
741
|
+
if n > 0:
|
|
742
|
+
logger.info("MCP: registered %d tools from %d servers", n, len(results))
|
|
743
|
+
return results
|
|
744
|
+
|