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.
Files changed (601) hide show
  1. aria_bots/__init__.py +7 -0
  2. aria_bots/channels/README.md +97 -0
  3. aria_bots/channels/__init__.py +1 -0
  4. aria_bots/channels/approvals.py +263 -0
  5. aria_bots/channels/conversation.py +280 -0
  6. aria_bots/channels/digest.py +186 -0
  7. aria_bots/channels/registry.py +95 -0
  8. aria_bots/engine.py +144 -0
  9. aria_bots/feishu_bot.py +1385 -0
  10. aria_bots/logistics/__init__.py +1 -0
  11. aria_bots/logistics/carriers.py +253 -0
  12. aria_bots/logistics/freight.py +154 -0
  13. aria_bots/logistics/inventory.py +242 -0
  14. aria_bots/logistics/mcp_server.py +156 -0
  15. aria_bots/logistics/records.py +119 -0
  16. aria_bots/logistics/tenancy.py +152 -0
  17. aria_bots/market.py +91 -0
  18. aria_bots/relay_client.py +326 -0
  19. aria_bots/telegram_bot.py +299 -0
  20. aria_code/__init__.py +111 -0
  21. aria_code/_bin/aria-native.exe +0 -0
  22. aria_code/_bin/aria-runtime.exe +0 -0
  23. aria_code/_version.py +20 -0
  24. aria_code/account_memory.py +296 -0
  25. aria_code/account_session.py +166 -0
  26. aria_code/adk_apps/__init__.py +1 -0
  27. aria_code/adk_apps/aria_code_review/__init__.py +1 -0
  28. aria_code/adk_apps/aria_code_review/agent.py +32 -0
  29. aria_code/adk_apps/aria_research/__init__.py +1 -0
  30. aria_code/adk_apps/aria_research/agent.py +31 -0
  31. aria_code/agents/__init__.py +49 -0
  32. aria_code/agents/base.py +329 -0
  33. aria_code/agents/code_review.py +200 -0
  34. aria_code/agents/deep/__init__.py +37 -0
  35. aria_code/agents/deep/calibration_loop.py +144 -0
  36. aria_code/agents/deep/critic.py +125 -0
  37. aria_code/agents/deep/deepen.py +193 -0
  38. aria_code/agents/deep/models.py +149 -0
  39. aria_code/agents/deep/pipeline.py +164 -0
  40. aria_code/agents/deep/quant_fusion.py +192 -0
  41. aria_code/agents/deep/themes.py +95 -0
  42. aria_code/agents/deep/tiers.py +106 -0
  43. aria_code/agents/engineering/__init__.py +14 -0
  44. aria_code/agents/engineering/coder.py +222 -0
  45. aria_code/agents/engineering/tester.py +145 -0
  46. aria_code/agents/financial/__init__.py +21 -0
  47. aria_code/agents/financial/cashflow_burn.py +119 -0
  48. aria_code/agents/financial/catalyst.py +279 -0
  49. aria_code/agents/financial/corporate_finance.py +175 -0
  50. aria_code/agents/financial/debate.py +224 -0
  51. aria_code/agents/financial/earnings.py +315 -0
  52. aria_code/agents/financial/fundamental.py +178 -0
  53. aria_code/agents/financial/macro.py +111 -0
  54. aria_code/agents/financial/news.py +231 -0
  55. aria_code/agents/financial/risk.py +146 -0
  56. aria_code/agents/financial/sector.py +279 -0
  57. aria_code/agents/financial/strategist.py +121 -0
  58. aria_code/agents/financial/stripe_revenue.py +127 -0
  59. aria_code/agents/financial/synthesis.py +274 -0
  60. aria_code/agents/financial/technical.py +277 -0
  61. aria_code/agents/orchestrator.py +44 -0
  62. aria_code/agents/portfolio_agent.py +392 -0
  63. aria_code/agents/realty/__init__.py +62 -0
  64. aria_code/agents/realty/asset_diagnosis.py +152 -0
  65. aria_code/agents/realty/business_match.py +166 -0
  66. aria_code/agents/realty/cashflow_verify.py +209 -0
  67. aria_code/agents/realty/contract_rules.py +210 -0
  68. aria_code/agents/realty/energy_anomaly.py +189 -0
  69. aria_code/agents/realty/exit_settlement.py +208 -0
  70. aria_code/agents/realty/fulfillment_risk.py +206 -0
  71. aria_code/agents/realty/ops_optimize.py +160 -0
  72. aria_code/agents/realty/revenue_share.py +215 -0
  73. aria_code/agents/registry.py +168 -0
  74. aria_code/agents/signal_scheme.py +111 -0
  75. aria_code/agents/sports/__init__.py +0 -0
  76. aria_code/agents/sports/football_agent.py +197 -0
  77. aria_code/agents/supervisor.py +113 -0
  78. aria_code/agents/team.py +500 -0
  79. aria_code/agents/video/__init__.py +16 -0
  80. aria_code/agents/video/director.py +376 -0
  81. aria_code/agents/video/qa_agent.py +143 -0
  82. aria_code/agents/video/script_doctor.py +112 -0
  83. aria_code/agents/warehouse/__init__.py +25 -0
  84. aria_code/agents/warehouse/contracts.py +26 -0
  85. aria_code/agents/warehouse/dashboard.py +155 -0
  86. aria_code/agents/warehouse/fulfillment_leadtime.py +95 -0
  87. aria_code/agents/warehouse/inbound_exceptions.py +25 -0
  88. aria_code/agents/warehouse/inventory_health.py +24 -0
  89. aria_code/agents/warehouse/local_run.py +71 -0
  90. aria_code/agents/warehouse/local_snapshot.py +132 -0
  91. aria_code/agents/warehouse/logistics_cost.py +84 -0
  92. aria_code/agents/warehouse/logistics_sync.py +21 -0
  93. aria_code/agents/warehouse/navigation.schema.json +110 -0
  94. aria_code/agents/warehouse/task_agent.py +172 -0
  95. aria_code/agents/warehouse/workflow.py +31 -0
  96. aria_code/aliyun_data_client.py +4 -0
  97. aria_code/apps/README.md +12 -0
  98. aria_code/apps/__init__.py +2 -0
  99. aria_code/apps/channels/README.md +9 -0
  100. aria_code/apps/channels/__init__.py +17 -0
  101. aria_code/apps/channels/approvals.py +11 -0
  102. aria_code/apps/channels/conversation.py +11 -0
  103. aria_code/apps/channels/digest.py +11 -0
  104. aria_code/apps/channels/intake.py +92 -0
  105. aria_code/apps/channels/registry.py +11 -0
  106. aria_code/apps/channels/tradingview.py +94 -0
  107. aria_code/apps/cli/README.md +13 -0
  108. aria_code/apps/cli/__init__.py +2 -0
  109. aria_code/apps/cli/_optional.py +120 -0
  110. aria_code/apps/cli/app_server.py +656 -0
  111. aria_code/apps/cli/auto_update.py +125 -0
  112. aria_code/apps/cli/bootstrap.py +206 -0
  113. aria_code/apps/cli/broker_render.py +206 -0
  114. aria_code/apps/cli/chat_turn.py +1327 -0
  115. aria_code/apps/cli/cloud_models.py +231 -0
  116. aria_code/apps/cli/codegen_paths.py +29 -0
  117. aria_code/apps/cli/commands/__init__.py +34 -0
  118. aria_code/apps/cli/commands/_business_render.py +78 -0
  119. aria_code/apps/cli/commands/_ui.py +71 -0
  120. aria_code/apps/cli/commands/analysis_cmds.py +378 -0
  121. aria_code/apps/cli/commands/ashare_prediction_cmds.py +327 -0
  122. aria_code/apps/cli/commands/auth_cmds.py +215 -0
  123. aria_code/apps/cli/commands/backtest_cmds.py +2518 -0
  124. aria_code/apps/cli/commands/broker_cmds.py +1236 -0
  125. aria_code/apps/cli/commands/business_workflow_cmds.py +379 -0
  126. aria_code/apps/cli/commands/canvas_cmds.py +77 -0
  127. aria_code/apps/cli/commands/catalog.py +189 -0
  128. aria_code/apps/cli/commands/core_cmds.py +2748 -0
  129. aria_code/apps/cli/commands/data_cmds.py +521 -0
  130. aria_code/apps/cli/commands/diagnostic_cmds.py +285 -0
  131. aria_code/apps/cli/commands/diagnostic_ops_cmds.py +813 -0
  132. aria_code/apps/cli/commands/engineering_cmds.py +399 -0
  133. aria_code/apps/cli/commands/file_cmds.py +312 -0
  134. aria_code/apps/cli/commands/finance_service_cmds.py +60 -0
  135. aria_code/apps/cli/commands/fx_commodity_cmds.py +123 -0
  136. aria_code/apps/cli/commands/market.py +544 -0
  137. aria_code/apps/cli/commands/market_cmds.py +1419 -0
  138. aria_code/apps/cli/commands/market_context.py +432 -0
  139. aria_code/apps/cli/commands/market_render.py +7 -0
  140. aria_code/apps/cli/commands/model_cmds.py +2374 -0
  141. aria_code/apps/cli/commands/ops_cmds.py +721 -0
  142. aria_code/apps/cli/commands/orchestrator_cmds.py +181 -0
  143. aria_code/apps/cli/commands/pdf_export_cmds.py +125 -0
  144. aria_code/apps/cli/commands/portfolio_cmds.py +1316 -0
  145. aria_code/apps/cli/commands/report.py +476 -0
  146. aria_code/apps/cli/commands/scaffold_templates.py +617 -0
  147. aria_code/apps/cli/commands/session_cmds.py +236 -0
  148. aria_code/apps/cli/commands/session_ux_cmds.py +359 -0
  149. aria_code/apps/cli/commands/team.py +723 -0
  150. aria_code/apps/cli/commands/team_render.py +8 -0
  151. aria_code/apps/cli/commands/ui_cmds.py +401 -0
  152. aria_code/apps/cli/commands/warehouse_cmds.py +297 -0
  153. aria_code/apps/cli/commands/workflow_cmds.py +629 -0
  154. aria_code/apps/cli/commands/workspace_cmds.py +1543 -0
  155. aria_code/apps/cli/config_paths.py +73 -0
  156. aria_code/apps/cli/config_store.py +158 -0
  157. aria_code/apps/cli/constants.py +88 -0
  158. aria_code/apps/cli/context.py +34 -0
  159. aria_code/apps/cli/conversation_policy.py +53 -0
  160. aria_code/apps/cli/deterministic.py +200 -0
  161. aria_code/apps/cli/direct.py +48 -0
  162. aria_code/apps/cli/earnings_view.py +172 -0
  163. aria_code/apps/cli/endpoint_migration.py +64 -0
  164. aria_code/apps/cli/exec_events.py +255 -0
  165. aria_code/apps/cli/file_references.py +155 -0
  166. aria_code/apps/cli/finance_service_catalog.py +111 -0
  167. aria_code/apps/cli/football_reports.py +231 -0
  168. aria_code/apps/cli/github_app_auth.py +135 -0
  169. aria_code/apps/cli/google_login.py +239 -0
  170. aria_code/apps/cli/handlers/__init__.py +11 -0
  171. aria_code/apps/cli/handlers/broker_handlers.py +122 -0
  172. aria_code/apps/cli/handlers/chart_handlers.py +1314 -0
  173. aria_code/apps/cli/handlers/market_handlers.py +2570 -0
  174. aria_code/apps/cli/handlers/realty_handlers.py +114 -0
  175. aria_code/apps/cli/handlers/sports_handlers.py +22 -0
  176. aria_code/apps/cli/handlers/strategy_advice.py +82 -0
  177. aria_code/apps/cli/headless.py +352 -0
  178. aria_code/apps/cli/helpers.py +326 -0
  179. aria_code/apps/cli/hooks.py +180 -0
  180. aria_code/apps/cli/i18n.py +310 -0
  181. aria_code/apps/cli/intent.py +136 -0
  182. aria_code/apps/cli/intent_router.py +305 -0
  183. aria_code/apps/cli/intent_shadow.py +72 -0
  184. aria_code/apps/cli/intent_signals.py +347 -0
  185. aria_code/apps/cli/lifecycle_hooks.py +48 -0
  186. aria_code/apps/cli/local_tool_schemas.py +550 -0
  187. aria_code/apps/cli/main.py +39 -0
  188. aria_code/apps/cli/market_hours.py +100 -0
  189. aria_code/apps/cli/market_metadata.py +135 -0
  190. aria_code/apps/cli/market_universe.py +487 -0
  191. aria_code/apps/cli/message_processing.py +310 -0
  192. aria_code/apps/cli/mixin_binding.py +32 -0
  193. aria_code/apps/cli/model_catalog.py +652 -0
  194. aria_code/apps/cli/model_probe.py +169 -0
  195. aria_code/apps/cli/model_retirement.py +65 -0
  196. aria_code/apps/cli/native_branding.py +12 -0
  197. aria_code/apps/cli/native_bridge.py +118 -0
  198. aria_code/apps/cli/native_entry.py +58 -0
  199. aria_code/apps/cli/pdf_report.py +977 -0
  200. aria_code/apps/cli/plan_mode.py +139 -0
  201. aria_code/apps/cli/plotly_html.py +15 -0
  202. aria_code/apps/cli/prediction_feedback.py +202 -0
  203. aria_code/apps/cli/preflight.py +502 -0
  204. aria_code/apps/cli/project_aria.py +60 -0
  205. aria_code/apps/cli/prompt_assembly.py +78 -0
  206. aria_code/apps/cli/prompts/__init__.py +0 -0
  207. aria_code/apps/cli/prompts/coding.py +71 -0
  208. aria_code/apps/cli/prompts/select.py +193 -0
  209. aria_code/apps/cli/prompts/system_prompts.py +562 -0
  210. aria_code/apps/cli/prompts/ui.py +436 -0
  211. aria_code/apps/cli/provider_endpoints.py +237 -0
  212. aria_code/apps/cli/provider_keys.py +73 -0
  213. aria_code/apps/cli/providers/__init__.py +1 -0
  214. aria_code/apps/cli/providers/base.py +775 -0
  215. aria_code/apps/cli/providers/chat_routing.py +139 -0
  216. aria_code/apps/cli/providers/collaboration.py +98 -0
  217. aria_code/apps/cli/providers/llm/__init__.py +1 -0
  218. aria_code/apps/cli/providers/llm/offline_responses.py +68 -0
  219. aria_code/apps/cli/providers/llm/ollama_stream.py +1574 -0
  220. aria_code/apps/cli/providers/llm/response_cache.py +60 -0
  221. aria_code/apps/cli/providers/llm/sse_stream.py +298 -0
  222. aria_code/apps/cli/providers/runtime_bridge.py +586 -0
  223. aria_code/apps/cli/providers/vertexai_stream.py +592 -0
  224. aria_code/apps/cli/release_download.py +159 -0
  225. aria_code/apps/cli/review_runner.py +111 -0
  226. aria_code/apps/cli/runtime_consumer.py +769 -0
  227. aria_code/apps/cli/sandbox.py +32 -0
  228. aria_code/apps/cli/session_export.py +120 -0
  229. aria_code/apps/cli/session_jsonl.py +208 -0
  230. aria_code/apps/cli/session_refresh.py +123 -0
  231. aria_code/apps/cli/session_store.py +125 -0
  232. aria_code/apps/cli/skills_catalog.py +273 -0
  233. aria_code/apps/cli/task_isolation.py +211 -0
  234. aria_code/apps/cli/terminal_chrome.py +412 -0
  235. aria_code/apps/cli/todo_tracker.py +190 -0
  236. aria_code/apps/cli/tool_executor.py +932 -0
  237. aria_code/apps/cli/tool_registry.py +34 -0
  238. aria_code/apps/cli/tool_scope.py +113 -0
  239. aria_code/apps/cli/tools/__init__.py +40 -0
  240. aria_code/apps/cli/tools/context.py +46 -0
  241. aria_code/apps/cli/tools/file_tools.py +181 -0
  242. aria_code/apps/cli/tools/market_tools.py +626 -0
  243. aria_code/apps/cli/tools/notebook_tools.py +111 -0
  244. aria_code/apps/cli/tools/system_tools.py +656 -0
  245. aria_code/apps/cli/tools/write_tools.py +961 -0
  246. aria_code/apps/cli/tradingview_bridge.py +529 -0
  247. aria_code/apps/cli/transactions.py +269 -0
  248. aria_code/apps/cli/turn_planning.py +137 -0
  249. aria_code/apps/cli/update_check.py +257 -0
  250. aria_code/apps/cli/updater.py +373 -0
  251. aria_code/apps/cli/utils/__init__.py +0 -0
  252. aria_code/apps/cli/utils/market_detect.py +1663 -0
  253. aria_code/apps/cli/web_login.py +136 -0
  254. aria_code/apps/cli/workflow_runner.py +127 -0
  255. aria_code/apps/cli/workspace_route.py +121 -0
  256. aria_code/apps/daemon/README.md +14 -0
  257. aria_code/apps/vscode/README.md +115 -0
  258. aria_code/apps/vscode/package.json +70 -0
  259. aria_code/aria_cli.py +5244 -0
  260. aria_code/aria_daemon.py +1372 -0
  261. aria_code/aria_feishu_bot.py +16 -0
  262. aria_code/aria_mcp_server.py +61 -0
  263. aria_code/aria_relay_client.py +4 -0
  264. aria_code/aria_telegram_bot.py +11 -0
  265. aria_code/ariarc.py +385 -0
  266. aria_code/artifacts.py +581 -0
  267. aria_code/backtest_engine.py +4 -0
  268. aria_code/backtest_report.py +4 -0
  269. aria_code/brokers/__init__.py +78 -0
  270. aria_code/brokers/automation.py +193 -0
  271. aria_code/brokers/base.py +231 -0
  272. aria_code/brokers/capabilities.py +264 -0
  273. aria_code/brokers/cn/__init__.py +10 -0
  274. aria_code/brokers/cn/easytrader_broker.py +193 -0
  275. aria_code/brokers/cn/futu_broker.py +194 -0
  276. aria_code/brokers/cn/longbridge_broker.py +190 -0
  277. aria_code/brokers/cn/tiger_broker.py +196 -0
  278. aria_code/brokers/cn/xtquant_broker.py +225 -0
  279. aria_code/brokers/config.py +414 -0
  280. aria_code/brokers/intl/__init__.py +5 -0
  281. aria_code/brokers/intl/alpaca_broker.py +183 -0
  282. aria_code/brokers/intl/ibkr_broker.py +215 -0
  283. aria_code/brokers/intl/trading212_broker.py +183 -0
  284. aria_code/brokers/intl/webull_broker.py +156 -0
  285. aria_code/brokers/paper_broker.py +266 -0
  286. aria_code/brokers/planning.py +296 -0
  287. aria_code/brokers/registry.py +213 -0
  288. aria_code/brokers/trading.py +267 -0
  289. aria_code/canva_client.py +4 -0
  290. aria_code/change_store.py +127 -0
  291. aria_code/clients/__init__.py +0 -0
  292. aria_code/clients/aliyun_data_client.py +660 -0
  293. aria_code/clients/aria_relay_client.py +16 -0
  294. aria_code/clients/canva_client.py +284 -0
  295. aria_code/clients/figma_client.py +100 -0
  296. aria_code/clients/football_data_client.py +1671 -0
  297. aria_code/clients/kling_video_client.py +182 -0
  298. aria_code/clients/market_data_client.py +1985 -0
  299. aria_code/clients/openai_image_client.py +242 -0
  300. aria_code/clients/runway_video_client.py +157 -0
  301. aria_code/clients/warehouse_erp_client.py +105 -0
  302. aria_code/cloud_memory.py +190 -0
  303. aria_code/command_safety.py +19 -0
  304. aria_code/computer_use_server.py +276 -0
  305. aria_code/computer_use_tools.py +4 -0
  306. aria_code/dashboard_generator.py +579 -0
  307. aria_code/data_analysis_tools.py +4 -0
  308. aria_code/data_cleaner.py +483 -0
  309. aria_code/data_service.py +513 -0
  310. aria_code/datasources/__init__.py +23 -0
  311. aria_code/datasources/base.py +166 -0
  312. aria_code/datasources/router.py +253 -0
  313. aria_code/datasources/sec_statements.py +217 -0
  314. aria_code/datasources/sources/__init__.py +15 -0
  315. aria_code/datasources/sources/akshare_source.py +269 -0
  316. aria_code/datasources/sources/alpha_vantage_source.py +203 -0
  317. aria_code/datasources/sources/edgar_source.py +219 -0
  318. aria_code/datasources/sources/finnhub_source.py +198 -0
  319. aria_code/datasources/sources/fred_source.py +223 -0
  320. aria_code/datasources/sources/tushare_source.py +143 -0
  321. aria_code/datasources/sources/web_scraper_source.py +278 -0
  322. aria_code/datasources/sources/world_bank_source.py +206 -0
  323. aria_code/datasources/sources/yfinance_source.py +152 -0
  324. aria_code/doctor.py +860 -0
  325. aria_code/domain/__init__.py +0 -0
  326. aria_code/domain/_finance_core/__init__.py +5 -0
  327. aria_code/domain/_finance_core/backtest_engine.py +325 -0
  328. aria_code/domain/_finance_core/performance_stats.py +101 -0
  329. aria_code/domain/backtest_engine.py +75 -0
  330. aria_code/domain/backtest_report.py +488 -0
  331. aria_code/domain/finance_formulas.py +389 -0
  332. aria_code/domain/performance_stats.py +6 -0
  333. aria_code/domain/portfolio_ledger.py +307 -0
  334. aria_code/domain/strategy_vault.py +421 -0
  335. aria_code/evals/__init__.py +37 -0
  336. aria_code/evals/behavior.py +113 -0
  337. aria_code/evals/gap.py +93 -0
  338. aria_code/evals/harness.py +949 -0
  339. aria_code/evals/review_probe.py +132 -0
  340. aria_code/evals/runner.py +425 -0
  341. aria_code/evals/scorecard.py +322 -0
  342. aria_code/evals/summary.py +125 -0
  343. aria_code/external_agent_runner.py +115 -0
  344. aria_code/figma_client.py +4 -0
  345. aria_code/file_analysis_tools.py +4 -0
  346. aria_code/finance_formulas.py +4 -0
  347. aria_code/football_data_client.py +4 -0
  348. aria_code/image_gen_tools.py +4 -0
  349. aria_code/intent_classifier.py +459 -0
  350. aria_code/kling_video_client.py +4 -0
  351. aria_code/licensing.py +151 -0
  352. aria_code/local_finance_tools.py +4 -0
  353. aria_code/local_image_provider.py +4 -0
  354. aria_code/local_llm_provider.py +4 -0
  355. aria_code/macro_tools.py +4 -0
  356. aria_code/markdown_pdf.py +289 -0
  357. aria_code/market_data_client.py +4 -0
  358. aria_code/mcp_client.py +744 -0
  359. aria_code/memory_manager.py +273 -0
  360. aria_code/model_capability.py +512 -0
  361. aria_code/notification_tools.py +4 -0
  362. aria_code/openai_image_client.py +4 -0
  363. aria_code/packages/__init__.py +26 -0
  364. aria_code/packages/adk_bridge/__init__.py +11 -0
  365. aria_code/packages/adk_bridge/code_review_tools.py +49 -0
  366. aria_code/packages/adk_bridge/market_tools.py +125 -0
  367. aria_code/packages/aria_agents/__init__.py +5 -0
  368. aria_code/packages/aria_agents/manifest.py +78 -0
  369. aria_code/packages/aria_core/__init__.py +34 -0
  370. aria_code/packages/aria_core/architecture.py +269 -0
  371. aria_code/packages/aria_core/export.py +147 -0
  372. aria_code/packages/aria_core/manifest.py +65 -0
  373. aria_code/packages/aria_core/paths.py +57 -0
  374. aria_code/packages/aria_core/secure_file.py +68 -0
  375. aria_code/packages/aria_infra/__init__.py +15 -0
  376. aria_code/packages/aria_infra/arthera.py +58 -0
  377. aria_code/packages/aria_infra/doctor.py +246 -0
  378. aria_code/packages/aria_infra/product.py +40 -0
  379. aria_code/packages/aria_mcp/__init__.py +25 -0
  380. aria_code/packages/aria_mcp/bridge.py +232 -0
  381. aria_code/packages/aria_mcp/circuit.py +82 -0
  382. aria_code/packages/aria_mcp/config.py +162 -0
  383. aria_code/packages/aria_mcp/installer.py +95 -0
  384. aria_code/packages/aria_mcp/server.py +1705 -0
  385. aria_code/packages/aria_mcp/tools.py +81 -0
  386. aria_code/packages/aria_sdk/__init__.py +19 -0
  387. aria_code/packages/aria_sdk/client.py +393 -0
  388. aria_code/packages/aria_sdk/providers.py +70 -0
  389. aria_code/packages/aria_sdk/streaming.py +75 -0
  390. aria_code/packages/aria_sdk/types.py +86 -0
  391. aria_code/packages/aria_services/__init__.py +92 -0
  392. aria_code/packages/aria_services/cache.py +60 -0
  393. aria_code/packages/aria_services/context.py +356 -0
  394. aria_code/packages/aria_services/context_checkpoints.py +157 -0
  395. aria_code/packages/aria_services/data.py +11 -0
  396. aria_code/packages/aria_services/provider_health.py +304 -0
  397. aria_code/packages/aria_services/references.py +247 -0
  398. aria_code/packages/aria_services/registry.py +249 -0
  399. aria_code/packages/aria_services/research_protocol.py +137 -0
  400. aria_code/packages/aria_services/research_quality.py +186 -0
  401. aria_code/packages/aria_services/settings.py +160 -0
  402. aria_code/packages/aria_services/usage.py +151 -0
  403. aria_code/packages/aria_skills/__init__.py +46 -0
  404. aria_code/packages/aria_skills/catalog.py +149 -0
  405. aria_code/packages/aria_skills/loader.py +455 -0
  406. aria_code/packages/aria_skills/registry.py +84 -0
  407. aria_code/packages/aria_tools/__init__.py +5 -0
  408. aria_code/packages/aria_tools/financial/__init__.py +5 -0
  409. aria_code/packages/aria_tools/financial/_paths.py +92 -0
  410. aria_code/packages/aria_tools/registry.py +129 -0
  411. aria_code/packages/quant_engine/__init__.py +33 -0
  412. aria_code/packages/quant_engine/_namespace.py +24 -0
  413. aria_code/packages/quant_engine/backtest/__init__.py +10 -0
  414. aria_code/packages/quant_engine/backtest/core.py +53 -0
  415. aria_code/packages/quant_engine/backtest/engine.py +102 -0
  416. aria_code/packages/quant_engine/portfolio/__init__.py +9 -0
  417. aria_code/packages/quant_engine/portfolio/optimizer.py +85 -0
  418. aria_code/packages/quant_engine/sports/__init__.py +76 -0
  419. aria_code/packages/quant_engine/sports/calibrator.py +354 -0
  420. aria_code/packages/quant_engine/sports/dixon_coles.py +234 -0
  421. aria_code/packages/quant_engine/sports/elo.py +300 -0
  422. aria_code/packages/quant_engine/sports/form.py +188 -0
  423. aria_code/packages/quant_engine/sports/h2h.py +195 -0
  424. aria_code/packages/quant_engine/sports/predictor.py +312 -0
  425. aria_code/packages/quant_engine/sports/tracker.py +665 -0
  426. aria_code/packages/quant_engine/stochastic/__init__.py +31 -0
  427. aria_code/packages/quant_engine/stochastic/gbm_enhanced.py +195 -0
  428. aria_code/packages/quant_engine/stochastic/ito_calculus.py +477 -0
  429. aria_code/packages/quant_engine/stochastic/kelly_criterion.py +181 -0
  430. aria_code/packages/quant_engine/stochastic/monte_carlo_advanced.py +95 -0
  431. aria_code/packages/quant_engine/stochastic/options_pricing.py +576 -0
  432. aria_code/packages/quant_engine/stochastic/stochastic_processes.py +90 -0
  433. aria_code/packs/__init__.py +85 -0
  434. aria_code/packs/base.py +168 -0
  435. aria_code/packs/finance/__init__.py +250 -0
  436. aria_code/packs/healthcare/__init__.py +40 -0
  437. aria_code/packs/logistics/__init__.py +171 -0
  438. aria_code/packs/payments/__init__.py +139 -0
  439. aria_code/packs/realty/__init__.py +190 -0
  440. aria_code/packs/registry.py +202 -0
  441. aria_code/packs/rules.py +90 -0
  442. aria_code/plan_utils.py +194 -0
  443. aria_code/plugin_loader.py +328 -0
  444. aria_code/portfolio_ledger.py +4 -0
  445. aria_code/preview_server.py +449 -0
  446. aria_code/privacy/__init__.py +5 -0
  447. aria_code/privacy/feedback.py +123 -0
  448. aria_code/project_review.py +278 -0
  449. aria_code/project_review_api.py +235 -0
  450. aria_code/project_review_server.py +18 -0
  451. aria_code/project_tools.py +4 -0
  452. aria_code/providers/__init__.py +30 -0
  453. aria_code/providers/google_video_provider.py +256 -0
  454. aria_code/providers/llm/__init__.py +19 -0
  455. aria_code/providers/llm/anthropic.py +184 -0
  456. aria_code/providers/llm/arthera_gateway.py +90 -0
  457. aria_code/providers/llm/autoconfig.py +213 -0
  458. aria_code/providers/llm/base.py +139 -0
  459. aria_code/providers/llm/custom.py +158 -0
  460. aria_code/providers/llm/ollama.py +137 -0
  461. aria_code/providers/llm/openai_compat.py +350 -0
  462. aria_code/providers/llm/registry.py +457 -0
  463. aria_code/providers/local_image_provider.py +202 -0
  464. aria_code/providers/local_llm_provider.py +549 -0
  465. aria_code/providers/ollama_models.py +79 -0
  466. aria_code/providers/openai_params.py +32 -0
  467. aria_code/providers/tool_messages.py +100 -0
  468. aria_code/realty_data_tools.py +4 -0
  469. aria_code/report_exporters.py +150 -0
  470. aria_code/report_generator.py +1764 -0
  471. aria_code/review_service.py +497 -0
  472. aria_code/runtime/__init__.py +176 -0
  473. aria_code/runtime/acceptance.py +632 -0
  474. aria_code/runtime/agent_loop.py +1851 -0
  475. aria_code/runtime/agent_profiles.py +75 -0
  476. aria_code/runtime/approval.py +71 -0
  477. aria_code/runtime/baseline.py +219 -0
  478. aria_code/runtime/budget.py +232 -0
  479. aria_code/runtime/checkpoints.py +481 -0
  480. aria_code/runtime/consult.py +65 -0
  481. aria_code/runtime/contract.py +282 -0
  482. aria_code/runtime/delivery.py +432 -0
  483. aria_code/runtime/events.py +113 -0
  484. aria_code/runtime/gateway.py +167 -0
  485. aria_code/runtime/lsp.py +484 -0
  486. aria_code/runtime/native_index.py +153 -0
  487. aria_code/runtime/orchestrator.py +55 -0
  488. aria_code/runtime/processes.py +343 -0
  489. aria_code/runtime/project_graph.py +614 -0
  490. aria_code/runtime/repo_map.py +712 -0
  491. aria_code/runtime/research_bundle.py +105 -0
  492. aria_code/runtime/review.py +269 -0
  493. aria_code/runtime/run_state.py +127 -0
  494. aria_code/runtime/run_store.py +522 -0
  495. aria_code/runtime/scoped_runs.py +113 -0
  496. aria_code/runtime/self_healing.py +422 -0
  497. aria_code/runtime/semantic_diff.py +171 -0
  498. aria_code/runtime/subagent.py +721 -0
  499. aria_code/runtime/symbol_edit.py +117 -0
  500. aria_code/runtime/task_brief.py +46 -0
  501. aria_code/runtime/task_context.py +145 -0
  502. aria_code/runtime/task_ledger.py +108 -0
  503. aria_code/runtime/task_orchestrator.py +90 -0
  504. aria_code/runtime/task_outcome.py +33 -0
  505. aria_code/runtime/task_verification.py +266 -0
  506. aria_code/runtime/task_worktree.py +448 -0
  507. aria_code/runtime/tool_executor.py +329 -0
  508. aria_code/runtime/tool_policy.py +107 -0
  509. aria_code/runtime/transactions.py +216 -0
  510. aria_code/runtime/windows_job.py +115 -0
  511. aria_code/runtime/workflows.py +222 -0
  512. aria_code/runtime/worktrees.py +215 -0
  513. aria_code/runway_video_client.py +4 -0
  514. aria_code/safety/__init__.py +27 -0
  515. aria_code/safety/client_isolation.py +178 -0
  516. aria_code/safety/permissions.py +339 -0
  517. aria_code/safety/risk.py +699 -0
  518. aria_code/safety/sandbox.py +148 -0
  519. aria_code/safety/service.py +102 -0
  520. aria_code/service_endpoints.py +14 -0
  521. aria_code/setup_wizard.py +684 -0
  522. aria_code/spreadsheet_tools.py +4 -0
  523. aria_code/strategy_vault.py +4 -0
  524. aria_code/tools/__init__.py +0 -0
  525. aria_code/tools/artifact_tools.py +252 -0
  526. aria_code/tools/broker_tools.py +92 -0
  527. aria_code/tools/build_quant_engine.py +97 -0
  528. aria_code/tools/code_audit_tools.py +192 -0
  529. aria_code/tools/computer_use_tools.py +509 -0
  530. aria_code/tools/data_analysis_tools.py +831 -0
  531. aria_code/tools/enterprise_finance_tools.py +202 -0
  532. aria_code/tools/extended_tools.py +240 -0
  533. aria_code/tools/file_analysis_tools.py +880 -0
  534. aria_code/tools/image_gen_tools.py +114 -0
  535. aria_code/tools/local_finance_tools.py +3488 -0
  536. aria_code/tools/logistics_carriers.py +11 -0
  537. aria_code/tools/logistics_inventory.py +11 -0
  538. aria_code/tools/logistics_io.py +11 -0
  539. aria_code/tools/logistics_tenancy.py +11 -0
  540. aria_code/tools/logistics_tools.py +11 -0
  541. aria_code/tools/macro_tools.py +412 -0
  542. aria_code/tools/notification_tools.py +249 -0
  543. aria_code/tools/project_tools.py +525 -0
  544. aria_code/tools/realty_data_tools.py +662 -0
  545. aria_code/tools/spreadsheet_tools.py +399 -0
  546. aria_code/tools/stripe_tools.py +115 -0
  547. aria_code/tools/video_tools.py +500 -0
  548. aria_code/training/__init__.py +1 -0
  549. aria_code/training/compare.py +117 -0
  550. aria_code/training/prelude_commands.py +111 -0
  551. aria_code/training/prelude_eval.py +390 -0
  552. aria_code/training/prelude_real.py +183 -0
  553. aria_code/training/prelude_train.py +248 -0
  554. aria_code/training/sft_dataset.py +230 -0
  555. aria_code/training/trajectory_dataset.py +263 -0
  556. aria_code/ui/__init__.py +105 -0
  557. aria_code/ui/banner.py +404 -0
  558. aria_code/ui/completer.py +571 -0
  559. aria_code/ui/console.py +285 -0
  560. aria_code/ui/input_box.py +505 -0
  561. aria_code/ui/mentions.py +206 -0
  562. aria_code/ui/picker.py +248 -0
  563. aria_code/ui/render/__init__.py +11 -0
  564. aria_code/ui/render/actions.py +297 -0
  565. aria_code/ui/render/finance.py +1732 -0
  566. aria_code/ui/render/market.py +253 -0
  567. aria_code/ui/render/output.py +954 -0
  568. aria_code/ui/render/responsive.py +75 -0
  569. aria_code/ui/render/team.py +367 -0
  570. aria_code/ui/robot.py +239 -0
  571. aria_code/ui/startup_dashboard.py +110 -0
  572. aria_code/video_analysis.py +147 -0
  573. aria_code/video_editor.py +518 -0
  574. aria_code/video_workflow/__init__.py +8 -0
  575. aria_code/video_workflow/__main__.py +61 -0
  576. aria_code/video_workflow/engines/__init__.py +18 -0
  577. aria_code/video_workflow/engines/base.py +69 -0
  578. aria_code/video_workflow/engines/custom_model.py +204 -0
  579. aria_code/video_workflow/engines/google_veo.py +94 -0
  580. aria_code/video_workflow/engines/local_engine.py +71 -0
  581. aria_code/video_workflow/engines/registry.py +98 -0
  582. aria_code/video_workflow/google_models.py +278 -0
  583. aria_code/video_workflow/music/__init__.py +5 -0
  584. aria_code/video_workflow/music/foley_service.py +132 -0
  585. aria_code/video_workflow/music/service.py +212 -0
  586. aria_code/video_workflow/pipeline.py +620 -0
  587. aria_code/video_workflow/plan.py +202 -0
  588. aria_code/video_workflow/session.py +297 -0
  589. aria_code/workspace/__init__.py +6 -0
  590. aria_code/workspace/files.py +219 -0
  591. aria_code/workspace/verify.py +167 -0
  592. aria_code/workspace_changes.py +199 -0
  593. aria_code/workspace_service.py +196 -0
  594. aria_code/workspace_tools.py +150 -0
  595. aria_code-0.167.0.dist-info/METADATA +330 -0
  596. aria_code-0.167.0.dist-info/RECORD +601 -0
  597. aria_code-0.167.0.dist-info/WHEEL +5 -0
  598. aria_code-0.167.0.dist-info/entry_points.txt +5 -0
  599. aria_code-0.167.0.dist-info/licenses/LICENSE +201 -0
  600. aria_code-0.167.0.dist-info/licenses/NOTICE +49 -0
  601. aria_code-0.167.0.dist-info/top_level.txt +2 -0
@@ -0,0 +1,1705 @@
1
+ """aria-code MCP server — exposes read-only aria-code capabilities over stdio JSON-RPC.
2
+
3
+ Lets any MCP-compatible client (Claude Code, Codex, Cursor, ...) call into
4
+ aria-code's market data, multi-agent research team, and generated-artifact
5
+ list. Pattern mirrors Arthera's packages/quant_engine/mcp_server.py (hand-rolled
6
+ stdio JSON-RPC, no `mcp` SDK dependency).
7
+
8
+ Usage (the entry point is src/aria_code/aria_mcp_server.py)::
9
+
10
+ aria-code-mcp # console script from pyproject
11
+ python3 -m aria_code.aria_mcp_server # same, without the script
12
+
13
+ Register with Claude Code::
14
+
15
+ claude mcp add aria-code -- aria-code-mcp
16
+
17
+ Scope: ``skill:``-backed exposures (report/backtest skills) are still never
18
+ registered as *executable* tools — a skill is a prompt-time instruction
19
+ document, not a clean JSON-in/JSON-out callable. They are instead readable
20
+ via ``aria.skill.list``/``aria.skill.get``, which hand the SKILL.md text to
21
+ the calling model so it can follow the workflow itself. Beyond
22
+ that, everything is read-only EXCEPT the small, explicit ``_WRITE_SAFE``
23
+ allowlist below (order previews, chart/PDF generation, cloud video
24
+ generation behind its own confirm gate) — each entry there is individually
25
+ justified as unable to move money without a further, explicit gate of its
26
+ own.
27
+
28
+ Order *execution* is the one capability with real financial stakes reachable
29
+ from this server at all, and it has two independent gates, both of which
30
+ must pass — this is intentional defense in depth, not redundancy to trim:
31
+ 1. The broker itself must have chat-confirm explicitly opted in via
32
+ ``/trade allow-chat-confirm <broker_id>`` — typed at the aria-code
33
+ terminal keyboard, by a human, with the broker id retyped as
34
+ confirmation. This can NEVER be turned on from MCP/chat — see
35
+ ``brokers/config.py``'s ``set_chat_confirm_enabled`` and its callers.
36
+ 2. Even with that broker opted in, each individual ``aria.broker.confirm_order``
37
+ call still requires an explicit ``confirmed: true`` argument (same
38
+ pattern as ``aria.video.generate_submit``'s cost gate).
39
+ Without opt-in #1, ``aria.broker.confirm_order`` refuses unconditionally —
40
+ default behavior for every broker is unchanged from before: only
41
+ ``/trade confirm`` at the terminal can execute a trade.
42
+ """
43
+
44
+ from __future__ import annotations
45
+
46
+ import json
47
+ import logging
48
+ import os
49
+ import sys
50
+ import traceback
51
+ from pathlib import Path
52
+ from typing import Any, Awaitable, Callable, Dict, List
53
+
54
+ _ROOT = str(Path(__file__).resolve().parents[2])
55
+ if _ROOT not in sys.path:
56
+ sys.path.insert(0, _ROOT)
57
+
58
+ logging.basicConfig(level=logging.WARNING, stream=sys.stderr)
59
+ logger = logging.getLogger("aria_mcp_server")
60
+
61
+ MCP_PROTOCOL_VERSION = "2024-11-05"
62
+
63
+ # ---------------------------------------------------------------------------
64
+ # Input schemas for the exposed tools (default_exposures() carries names +
65
+ # descriptions but not JSON Schema — kept here, next to the handlers that
66
+ # actually consume the arguments).
67
+ # ---------------------------------------------------------------------------
68
+
69
+ from aria_code.agents.warehouse.task_agent import SCHEMA as WAREHOUSE_SCHEMA, WAREHOUSE_AGENT
70
+
71
+ _INPUT_SCHEMAS: Dict[str, Dict[str, Any]] = {
72
+ "aria.warehouse.task": WAREHOUSE_SCHEMA,
73
+ "aria.risk.assess": {
74
+ "type": "object",
75
+ "properties": {
76
+ "command": {"type": "string", "description": "A shell command to assess, e.g. 'npm install jose'."},
77
+ "tool": {"type": "string", "description": "Or a tool name (write_file, edit_file, run_command, ...)."},
78
+ "params": {"type": "object", "description": "The tool's parameters."},
79
+ "root": {"type": "string", "description": "Project directory (default: the server's working directory)."},
80
+ },
81
+ },
82
+ "aria.contract.check": {
83
+ "type": "object",
84
+ "properties": {
85
+ "tool": {"type": "string", "description": "The tool call to check, e.g. write_file or run_command."},
86
+ "params": {"type": "object", "description": "The tool's parameters, e.g. {path: 'src/a.py'}."},
87
+ "root": {"type": "string", "description": "Project directory (default: the server's working directory)."},
88
+ },
89
+ "required": ["tool"],
90
+ },
91
+ "aria.project.impact": {
92
+ "type": "object",
93
+ "properties": {
94
+ "targets": {"type": "array", "items": {"type": "string"},
95
+ "description": "Paths or symbols, e.g. ['src/auth/session.py', 'Session.refresh']."},
96
+ "root": {"type": "string", "description": "Project directory (default: the server's working directory)."},
97
+ },
98
+ "required": ["targets"],
99
+ },
100
+ "aria.project.commands": {
101
+ "type": "object",
102
+ "properties": {
103
+ "root": {"type": "string", "description": "Project directory (default: the server's working directory)."},
104
+ },
105
+ },
106
+ "aria.market.quote": {
107
+ "type": "object",
108
+ "properties": {
109
+ "symbol": {"type": "string", "description": "Ticker/symbol, e.g. AAPL, 600519, BTC-USD"},
110
+ },
111
+ "required": ["symbol"],
112
+ },
113
+ "aria.agent.team": {
114
+ "type": "object",
115
+ "properties": {
116
+ "symbol": {"type": "string", "description": "Ticker/symbol to research"},
117
+ "agents": {
118
+ "type": "array",
119
+ "items": {"type": "string"},
120
+ "description": "Subset of agent names to run (default: all)",
121
+ },
122
+ },
123
+ "required": ["symbol"],
124
+ },
125
+ "aria.artifacts.list": {
126
+ "type": "object",
127
+ "properties": {
128
+ "limit": {"type": "integer", "description": "Max artifacts to return (default 20)"},
129
+ },
130
+ "required": [],
131
+ },
132
+ "aria.skill.list": {
133
+ "type": "object",
134
+ "properties": {
135
+ "query": {"type": "string", "description": "Task description to rank skills against, e.g. \"design a landing page for a healthcare startup\". Omit to list all installed skills."},
136
+ },
137
+ "required": [],
138
+ },
139
+ "aria.skill.get": {
140
+ "type": "object",
141
+ "properties": {
142
+ "name": {"type": "string", "description": "Skill name from aria.skill.list — either the bare name (\"ui-design-system\") or the qualified form (\"app-engineering-skills:ui-design-system\")"},
143
+ "reference": {"type": "string", "description": "Optional. Fetch one of the skill's bundled reference docs instead of its main instructions — pass a filename from the `references` list returned by a plain aria.skill.get call, e.g. \"density_and_semantics.md\"."},
144
+ },
145
+ "required": ["name"],
146
+ },
147
+ "aria.broker.positions": {
148
+ "type": "object",
149
+ "properties": {
150
+ "broker_id": {"type": "string", "description": "Configured broker id (~/.aria-code/brokers.json). Omit to use the default broker."},
151
+ },
152
+ "required": [],
153
+ },
154
+ "aria.broker.list_previews": {
155
+ "type": "object",
156
+ "properties": {
157
+ "limit": {"type": "integer", "description": "Max previews to return (default 10)"},
158
+ },
159
+ "required": [],
160
+ },
161
+ "aria.broker.preview_order": {
162
+ "type": "object",
163
+ "properties": {
164
+ "broker_id": {"type": "string", "description": "Configured broker id. Omit to use the default broker."},
165
+ "symbol": {"type": "string", "description": "Ticker/symbol to trade"},
166
+ "side": {"type": "string", "enum": ["buy", "sell"]},
167
+ "quantity": {"type": "number", "description": "Share/unit quantity"},
168
+ "order_type": {"type": "string", "enum": ["limit", "market"], "description": "Default: limit"},
169
+ "price": {"type": "number", "description": "Limit price (required for limit orders)"},
170
+ },
171
+ "required": ["symbol", "side"],
172
+ },
173
+ "aria.broker.confirm_order": {
174
+ "type": "object",
175
+ "properties": {
176
+ "broker_id": {"type": "string", "description": "Configured broker id. Omit to use the default broker."},
177
+ "preview_id": {"type": "string", "description": "preview_id returned by aria.broker.preview_order"},
178
+ "confirmed": {"type": "boolean", "description": "Must be true. Also requires the broker to have chat-confirm opted in via /trade allow-chat-confirm <broker_id> at the aria-code terminal — see this tool's description."},
179
+ },
180
+ "required": ["preview_id", "confirmed"],
181
+ },
182
+ "aria.report.chart": {
183
+ "type": "object",
184
+ "properties": {
185
+ "symbol": {"type": "string", "description": "Ticker/symbol to chart"},
186
+ },
187
+ "required": ["symbol"],
188
+ },
189
+ "aria.report.indicator_chart": {
190
+ "type": "object",
191
+ "properties": {
192
+ "symbol": {"type": "string", "description": "Ticker/symbol to chart"},
193
+ },
194
+ "required": ["symbol"],
195
+ },
196
+ "aria.report.comparison_chart": {
197
+ "type": "object",
198
+ "properties": {
199
+ "symbols": {"type": "array", "items": {"type": "string"}, "description": "2+ tickers to compare, normalized to base=100"},
200
+ "title": {"type": "string", "description": "Optional chart title"},
201
+ },
202
+ "required": ["symbols"],
203
+ },
204
+ "aria.report.allocation_chart": {
205
+ "type": "object",
206
+ "properties": {
207
+ "broker_id": {"type": "string", "description": "Configured broker id. Omit to use the default broker."},
208
+ "title": {"type": "string", "description": "Optional chart title"},
209
+ },
210
+ "required": [],
211
+ },
212
+ "aria.report.pdf": {
213
+ "type": "object",
214
+ "properties": {
215
+ "markdown": {"type": "string", "description": "Markdown text to render"},
216
+ "title": {"type": "string", "description": "Report title, used for the filename"},
217
+ },
218
+ "required": ["markdown"],
219
+ },
220
+ "aria.report.estimate_image_cost": {
221
+ "type": "object",
222
+ "properties": {
223
+ "size": {"type": "string", "enum": ["1024x1024", "1536x1024", "1024x1536", "auto"], "description": "Default: 1024x1536"},
224
+ "quality": {"type": "string", "enum": ["low", "medium", "high", "auto"], "description": "Default: high"},
225
+ },
226
+ "required": [],
227
+ },
228
+ "aria.report.generate_image": {
229
+ "type": "object",
230
+ "properties": {
231
+ "prompt": {"type": "string", "description": "Text prompt (e.g. minimal-editorial-poster's compiled prompt)"},
232
+ "size": {"type": "string", "enum": ["1024x1024", "1536x1024", "1024x1536", "auto"], "description": "Default: 1024x1536 (portrait poster)"},
233
+ "quality": {"type": "string", "enum": ["low", "medium", "high", "auto"], "description": "Default: high"},
234
+ "confirmed": {"type": "boolean", "description": "Must be true — this spends real money the instant it succeeds"},
235
+ },
236
+ "required": ["prompt", "confirmed"],
237
+ },
238
+ "aria.report.edit_image": {
239
+ "type": "object",
240
+ "properties": {
241
+ "image_path": {"type": "string", "description": "Local path to the existing photo to transform"},
242
+ "prompt": {"type": "string", "description": "How to transform it, e.g. \"convert to duotone with warm ochre accent, simplify the busy rock background into flat negative space, add subtle scan-noise texture\""},
243
+ "mask_path": {"type": "string", "description": "Optional local path to a mask PNG for inpainting — transparent (alpha=0) areas mark where the edit applies; everything opaque is left untouched. Omit to edit the whole image."},
244
+ "size": {"type": "string", "enum": ["1024x1024", "1536x1024", "1024x1536", "auto"], "description": "Default: 1024x1536"},
245
+ "quality": {"type": "string", "enum": ["low", "medium", "high", "auto"], "description": "Default: high"},
246
+ "confirmed": {"type": "boolean", "description": "Must be true — this spends real money the instant it succeeds"},
247
+ },
248
+ "required": ["image_path", "prompt", "confirmed"],
249
+ },
250
+ "aria.report.generate_image_local": {
251
+ "type": "object",
252
+ "properties": {
253
+ "prompt": {"type": "string", "description": "Text prompt (e.g. minimal-editorial-poster's compiled prompt)"},
254
+ "width": {"type": "integer", "description": "Default: 1024"},
255
+ "height": {"type": "integer", "description": "Default: 1024"},
256
+ "steps": {"type": "integer", "description": "Inference steps; default 4 (tuned for SDXL-Turbo)"},
257
+ },
258
+ "required": ["prompt"],
259
+ },
260
+ "aria.report.edit_image_local": {
261
+ "type": "object",
262
+ "properties": {
263
+ "image_path": {"type": "string", "description": "Local path to the existing photo to transform"},
264
+ "prompt": {"type": "string", "description": "How to transform it"},
265
+ "strength": {"type": "number", "description": "0-1, how much to diverge from the original (default 0.6)"},
266
+ },
267
+ "required": ["image_path", "prompt"],
268
+ },
269
+ "aria.report.canva_design": {
270
+ "type": "object",
271
+ "properties": {
272
+ "template_id": {"type": "string", "description": "Canva brand template id"},
273
+ "data": {"type": "object", "description": "Field name -> typed DatasetValue, e.g. {\"headline\": {\"type\": \"text\", \"text\": \"Q3 Report\"}, \"chart_img\": {\"type\": \"image\", \"asset_id\": \"<from aria.report.canva_upload_asset>\"}}. Value types: text, image, video, chart, sheet — see Canva's Autofill API docs."},
274
+ },
275
+ "required": ["template_id", "data"],
276
+ },
277
+ "aria.report.canva_upload_asset": {
278
+ "type": "object",
279
+ "properties": {
280
+ "file_path": {"type": "string", "description": "Local path to the image file to upload (e.g. the path returned by aria.report.chart)"},
281
+ },
282
+ "required": ["file_path"],
283
+ },
284
+ "aria.report.docx": {
285
+ "type": "object",
286
+ "properties": {
287
+ "markdown": {"type": "string", "description": "Markdown text to render"},
288
+ "title": {"type": "string", "description": "Document title, used for the filename and cover heading"},
289
+ },
290
+ "required": ["markdown"],
291
+ },
292
+ "aria.report.pptx": {
293
+ "type": "object",
294
+ "properties": {
295
+ "markdown": {"type": "string", "description": "Markdown text to render (each heading becomes a slide)"},
296
+ "title": {"type": "string", "description": "Deck title, used for the filename and title slide"},
297
+ },
298
+ "required": ["markdown"],
299
+ },
300
+ "aria.figma.read_file": {
301
+ "type": "object",
302
+ "properties": {
303
+ "file_key": {"type": "string", "description": "Figma file key (from the file's URL: figma.com/file/<file_key>/...)"},
304
+ "depth": {"type": "integer", "description": "How many tree levels to include per page (default 2)"},
305
+ },
306
+ "required": ["file_key"],
307
+ },
308
+ "aria.figma.comments": {
309
+ "type": "object",
310
+ "properties": {
311
+ "file_key": {"type": "string", "description": "Figma file key"},
312
+ },
313
+ "required": ["file_key"],
314
+ },
315
+ "aria.report.generate": {
316
+ "type": "object",
317
+ "properties": {
318
+ "symbol": {"type": "string", "description": "Ticker/symbol to research"},
319
+ },
320
+ "required": ["symbol"],
321
+ },
322
+ "aria.backtest.run": {
323
+ "type": "object",
324
+ "properties": {
325
+ "symbol": {"type": "string", "description": "Ticker/symbol to backtest"},
326
+ "strategy": {"type": "string", "enum": ["buy_hold", "sma_cross"], "description": "Default: sma_cross"},
327
+ "days": {"type": "integer", "description": "History window in days (default 365)"},
328
+ },
329
+ "required": ["symbol"],
330
+ },
331
+ "aria.video.probe": {
332
+ "type": "object",
333
+ "properties": {
334
+ "input_path": {"type": "string", "description": "Local path to the video"},
335
+ },
336
+ "required": ["input_path"],
337
+ },
338
+ "aria.video.trim": {
339
+ "type": "object",
340
+ "properties": {
341
+ "input_path": {"type": "string"},
342
+ "start": {"type": "number", "description": "Start time in seconds"},
343
+ "end": {"type": "number", "description": "End time in seconds"},
344
+ },
345
+ "required": ["input_path", "start", "end"],
346
+ },
347
+ "aria.video.concat": {
348
+ "type": "object",
349
+ "properties": {
350
+ "input_paths": {"type": "array", "items": {"type": "string"}, "description": "Video paths, in order"},
351
+ },
352
+ "required": ["input_paths"],
353
+ },
354
+ "aria.video.overlay_text": {
355
+ "type": "object",
356
+ "properties": {
357
+ "input_path": {"type": "string"},
358
+ "text": {"type": "string"},
359
+ "position": {"type": "string", "enum": ["top", "bottom", "center"], "description": "Default: bottom"},
360
+ "font_size": {"type": "integer", "description": "Default: 36"},
361
+ "font_color": {"type": "string", "description": "Default: white"},
362
+ },
363
+ "required": ["input_path", "text"],
364
+ },
365
+ "aria.video.overlay_audio": {
366
+ "type": "object",
367
+ "properties": {
368
+ "input_path": {"type": "string"},
369
+ "audio_path": {"type": "string"},
370
+ "replace": {"type": "boolean", "description": "true = drop original audio; false (default) = mix under it"},
371
+ },
372
+ "required": ["input_path", "audio_path"],
373
+ },
374
+ "aria.video.convert": {
375
+ "type": "object",
376
+ "properties": {
377
+ "input_path": {"type": "string"},
378
+ "output_format": {"type": "string", "description": "e.g. mp4, mov, webm"},
379
+ "aspect": {"type": "string", "description": "e.g. \"9:16\" — center-crop to this aspect ratio"},
380
+ },
381
+ "required": ["input_path"],
382
+ },
383
+ "aria.video.change_speed": {
384
+ "type": "object",
385
+ "properties": {
386
+ "input_path": {"type": "string"},
387
+ "factor": {"type": "number", "description": ">1 speeds up, <1 slows down"},
388
+ },
389
+ "required": ["input_path", "factor"],
390
+ },
391
+ "aria.video.transcribe": {
392
+ "type": "object",
393
+ "properties": {
394
+ "input_path": {"type": "string"},
395
+ "model_size": {"type": "string", "description": "faster-whisper model size, default 'base'"},
396
+ "language": {"type": "string", "description": "ISO language code; omit to auto-detect"},
397
+ },
398
+ "required": ["input_path"],
399
+ },
400
+ "aria.video.detect_scenes": {
401
+ "type": "object",
402
+ "properties": {
403
+ "input_path": {"type": "string"},
404
+ "threshold": {"type": "number", "description": "Higher = less sensitive (default 30.0)"},
405
+ },
406
+ "required": ["input_path"],
407
+ },
408
+ "aria.video.generate_estimate": {
409
+ "type": "object",
410
+ "properties": {
411
+ "provider": {"type": "string", "enum": ["kling", "runway"]},
412
+ "duration": {"type": "integer", "description": "Video length in seconds"},
413
+ },
414
+ "required": ["provider", "duration"],
415
+ },
416
+ "aria.video.generate_submit": {
417
+ "type": "object",
418
+ "properties": {
419
+ "provider": {"type": "string", "enum": ["kling", "runway"]},
420
+ "prompt": {"type": "string"},
421
+ "duration": {"type": "integer", "description": "Default: 5"},
422
+ "aspect_ratio": {"type": "string", "description": "e.g. \"16:9\" (kling) or \"1280:720\" (runway) — format is provider-specific"},
423
+ "confirmed": {"type": "boolean", "description": "Must be true — this spends real money the instant it succeeds"},
424
+ },
425
+ "required": ["provider", "prompt", "confirmed"],
426
+ },
427
+ "aria.video.generate_status": {
428
+ "type": "object",
429
+ "properties": {
430
+ "provider": {"type": "string", "enum": ["kling", "runway"]},
431
+ "task_id": {"type": "string"},
432
+ },
433
+ "required": ["provider", "task_id"],
434
+ },
435
+ }
436
+
437
+ # Exposures that are NOT read_only (they write a local file, a preview
438
+ # record, a Canva design draft, or a paid cloud generation job) but are
439
+ # individually verified safe to reach from MCP without a human in this
440
+ # module blocking it first. Almost all of them can't move money or place a
441
+ # live trade — the exceptions are aria.report.generate_image / edit_image
442
+ # (real per-call OpenAI cost, billed the instant it succeeds — safe here
443
+ # only because their handlers hard-refuse without confirmed=true, see
444
+ # _call_generate_image / _call_edit_image), aria.video.generate_submit (same
445
+ # pattern for cloud video generation, see _call_video_generate_submit), and
446
+ # aria.broker.confirm_order (real trade execution — safe here only because
447
+ # its handler hard-refuses unless the broker was separately, explicitly
448
+ # opted into chat-confirm at the aria-code terminal keyboard via
449
+ # /trade allow-chat-confirm, AND confirmed=true is passed; see
450
+ # _call_broker_confirm_order and the module docstring). Every other
451
+ # non-read_only exposure stays blocked by default — see _build_tools().
452
+ _WRITE_SAFE = {
453
+ "aria.warehouse.task", # simulation state only; native executor has no hardware mode
454
+ "aria.broker.preview_order", "aria.broker.confirm_order",
455
+ "aria.report.chart", "aria.report.indicator_chart", "aria.report.comparison_chart",
456
+ "aria.report.allocation_chart", "aria.report.pdf",
457
+ "aria.report.canva_design", "aria.report.canva_upload_asset",
458
+ "aria.report.docx", "aria.report.pptx",
459
+ "aria.report.generate", "aria.backtest.run",
460
+ "aria.report.generate_image", "aria.report.edit_image",
461
+ "aria.report.generate_image_local", "aria.report.edit_image_local",
462
+ "aria.video.trim", "aria.video.concat", "aria.video.overlay_text",
463
+ "aria.video.overlay_audio", "aria.video.convert", "aria.video.change_speed",
464
+ "aria.video.generate_submit",
465
+ }
466
+
467
+
468
+ # ---------------------------------------------------------------------------
469
+ # Handlers — each is `async def handler(arguments: dict) -> dict`
470
+ # ---------------------------------------------------------------------------
471
+
472
+ async def _call_market_quote(args: Dict[str, Any]) -> Dict[str, Any]:
473
+ from runtime.tool_executor import ToolExecutor
474
+ from apps.cli.tools.market_tools import tool_get_market_data
475
+
476
+ executor = ToolExecutor({"get_market_data": (tool_get_market_data, "quote + technicals")})
477
+ return await executor.execute("get_market_data", {"symbol": args.get("symbol", "")})
478
+
479
+
480
+ async def _call_agent_team(args: Dict[str, Any]) -> Dict[str, Any]:
481
+ from dataclasses import asdict
482
+
483
+ from agents.team import run_team
484
+ from datasources.router import get_router
485
+
486
+ symbol = str(args.get("symbol", "")).strip()
487
+ if not symbol:
488
+ return {"success": False, "error": "symbol is required"}
489
+ try:
490
+ data_router = get_router()
491
+ except Exception as exc:
492
+ logger.debug("get_router() unavailable, running without live data bundle: %s", exc)
493
+ data_router = None
494
+ # llm_provider intentionally left None: agents fall back to their
495
+ # deterministic template mode (see agents/base.py) rather than this
496
+ # server needing to bootstrap the CLI's full provider/config stack.
497
+ # Output is real (real data, real rule-based signals) but not
498
+ # LLM-narrated — documented in the tool description surfaced to clients.
499
+ result = await run_team(symbol, agents=args.get("agents"), llm_provider=None, data_router=data_router)
500
+ return {"success": True, **asdict(result)}
501
+
502
+
503
+ async def _call_artifacts_list(args: Dict[str, Any]) -> Dict[str, Any]:
504
+ from artifacts import recent_artifacts_all
505
+
506
+ limit = int(args.get("limit", 20) or 20)
507
+ return {"success": True, "artifacts": recent_artifacts_all(limit=limit)}
508
+
509
+
510
+ def _skill_summary(skill: Any) -> Dict[str, Any]:
511
+ """Listing shape — deliberately excludes `instructions` so a list call
512
+ stays small; aria.skill.get fetches the full text for one skill."""
513
+ return {
514
+ "name": skill.name,
515
+ "qualified_name": skill.qualified_name,
516
+ "description": skill.description,
517
+ "plugin": skill.plugin_name,
518
+ # Surfaced so the caller can tell a signed catalog skill from an
519
+ # unverified local drop-in — these are instruction documents the
520
+ # model will follow, so provenance is worth showing, not hiding.
521
+ "integrity": skill.integrity,
522
+ }
523
+
524
+
525
+ async def _call_skill_list(args: Dict[str, Any]) -> Dict[str, Any]:
526
+ from packages.aria_skills.loader import discover_external_skills, select_external_skills
527
+
528
+ query = str(args.get("query", "")).strip()
529
+ try:
530
+ skills = discover_external_skills()
531
+ except Exception as exc:
532
+ return {"success": False, "error": str(exc)}
533
+
534
+ if query:
535
+ ranked = select_external_skills(query, skills)
536
+ # select_* returns only what clears its relevance bar, which can be
537
+ # empty for an off-topic query — fall back to the full list rather
538
+ # than answering "no skills exist", which would be misleading.
539
+ chosen = ranked or skills
540
+ matched = bool(ranked)
541
+ else:
542
+ chosen = skills
543
+ matched = False
544
+
545
+ return {
546
+ "success": True,
547
+ "query": query,
548
+ "matched_by_relevance": matched,
549
+ "skills": [_skill_summary(s) for s in chosen],
550
+ }
551
+
552
+
553
+ def _skill_reference_names(skill: Any) -> List[str]:
554
+ """Reference docs bundled beside a SKILL.md. Several skills' instructions
555
+ say things like "read references/density_and_semantics.md first" — over
556
+ MCP the caller has no filesystem access to this machine, so those files
557
+ have to be fetchable through this tool or that instruction is a dead end."""
558
+ try:
559
+ ref_dir = Path(skill.path).parent / "references"
560
+ if not ref_dir.is_dir():
561
+ return []
562
+ return sorted(p.name for p in ref_dir.glob("*.md"))
563
+ except Exception:
564
+ return []
565
+
566
+
567
+ async def _call_skill_get(args: Dict[str, Any]) -> Dict[str, Any]:
568
+ from packages.aria_skills.loader import discover_external_skills
569
+
570
+ name = str(args.get("name", "")).strip()
571
+ if not name:
572
+ return {"success": False, "error": "name is required"}
573
+
574
+ try:
575
+ skills = discover_external_skills()
576
+ except Exception as exc:
577
+ return {"success": False, "error": str(exc)}
578
+
579
+ match = next(
580
+ (s for s in skills if name in (s.name, s.qualified_name)),
581
+ None,
582
+ )
583
+ if match is None:
584
+ return {
585
+ "success": False,
586
+ "error": f"No skill named {name!r}. Call aria.skill.list to see installed skills.",
587
+ "available": [s.qualified_name for s in skills],
588
+ }
589
+
590
+ references = _skill_reference_names(match)
591
+ requested = str(args.get("reference", "")).strip()
592
+ if requested:
593
+ # `reference` is caller-controlled and lands in a filesystem path, so
594
+ # resolve it and require the result to stay inside this skill's own
595
+ # references/ dir — a bare name check would still let "../../.ssh/id_rsa"
596
+ # or a symlink out of the sandbox through.
597
+ ref_dir = (Path(match.path).parent / "references").resolve()
598
+ target = (ref_dir / requested).resolve()
599
+ if not str(target).startswith(str(ref_dir) + os.sep) or not target.is_file():
600
+ return {
601
+ "success": False,
602
+ "error": f"No reference {requested!r} in skill {match.qualified_name!r}.",
603
+ "references": references,
604
+ }
605
+ try:
606
+ content = target.read_text(encoding="utf-8")
607
+ except Exception as exc:
608
+ return {"success": False, "error": str(exc)}
609
+ return {
610
+ "success": True,
611
+ **_skill_summary(match),
612
+ "reference": requested,
613
+ "content": content,
614
+ }
615
+
616
+ return {
617
+ "success": True,
618
+ **_skill_summary(match),
619
+ "instructions": match.instructions,
620
+ "references": references,
621
+ }
622
+
623
+
624
+ def _get_broker(broker_id: str = ""):
625
+ from brokers.registry import get_registry
626
+
627
+ reg = get_registry()
628
+ if broker_id:
629
+ return reg.connect(broker_id)
630
+ broker = reg.connect_default()
631
+ if broker is None:
632
+ raise ValueError("No broker_id given and no default broker configured (~/.aria-code/brokers.json)")
633
+ return broker
634
+
635
+
636
+ async def _call_broker_positions(args: Dict[str, Any]) -> Dict[str, Any]:
637
+ import asyncio
638
+ from dataclasses import asdict
639
+
640
+ loop = asyncio.get_event_loop()
641
+
642
+ def _run():
643
+ broker = _get_broker(str(args.get("broker_id", "")))
644
+ return asdict(broker.account_info()), [asdict(p) for p in broker.positions()]
645
+
646
+ try:
647
+ account, positions = await loop.run_in_executor(None, _run)
648
+ except Exception as exc:
649
+ return {"success": False, "error": str(exc)}
650
+ return {"success": True, "account": account, "positions": positions}
651
+
652
+
653
+ async def _call_broker_list_previews(args: Dict[str, Any]) -> Dict[str, Any]:
654
+ from brokers.trading import list_order_previews
655
+
656
+ limit = int(args.get("limit", 10) or 10)
657
+ return {"success": True, "previews": list_order_previews(limit=limit)}
658
+
659
+
660
+ async def _call_broker_preview_order(args: Dict[str, Any]) -> Dict[str, Any]:
661
+ # Building a preview here can never by itself result in a live order —
662
+ # execute_order_preview is only imported by _call_broker_confirm_order,
663
+ # which has its own two-gate check. See module docstring.
664
+ import asyncio
665
+
666
+ from brokers.trading import OrderIntent, build_order_preview
667
+
668
+ symbol = str(args.get("symbol", "")).strip()
669
+ side = str(args.get("side", "")).strip().lower()
670
+ if not symbol or side not in ("buy", "sell"):
671
+ return {"success": False, "error": "symbol and side (buy/sell) are required"}
672
+
673
+ intent = OrderIntent(
674
+ symbol=symbol,
675
+ side=side,
676
+ quantity=args.get("quantity"),
677
+ order_type=str(args.get("order_type") or "limit"),
678
+ price=args.get("price"),
679
+ source="mcp",
680
+ )
681
+ loop = asyncio.get_event_loop()
682
+
683
+ def _run():
684
+ broker = _get_broker(str(args.get("broker_id", "")))
685
+ return build_order_preview(broker, intent)
686
+
687
+ try:
688
+ preview = await loop.run_in_executor(None, _run)
689
+ except Exception as exc:
690
+ return {"success": False, "error": str(exc)}
691
+ return {"success": True, **preview}
692
+
693
+
694
+ async def _call_broker_confirm_order(args: Dict[str, Any]) -> Dict[str, Any]:
695
+ # The one place in this module that imports execute_order_preview. Two
696
+ # gates, both required, checked here (not just trusted from the caller):
697
+ # 1. is_chat_confirm_enabled(broker_id) — set only by a human running
698
+ # /trade allow-chat-confirm <broker_id> at the aria-code terminal
699
+ # keyboard and retyping the exact broker id. Off by default for
700
+ # every broker. This gate cannot be flipped on from MCP/chat.
701
+ # 2. confirmed is True on this specific call.
702
+ # Both must pass before execute_order_preview(confirmed=True) is ever
703
+ # reached. See the module docstring for the full rationale.
704
+ import asyncio
705
+
706
+ from brokers.config import is_chat_confirm_enabled
707
+ from brokers.trading import execute_order_preview
708
+
709
+ preview_id = str(args.get("preview_id", "")).strip()
710
+ if not preview_id:
711
+ return {"success": False, "error": "preview_id is required"}
712
+ if args.get("confirmed") is not True:
713
+ return {
714
+ "success": False,
715
+ "error": "confirmed must be true to execute a real order — this places a live trade.",
716
+ }
717
+
718
+ broker_id = str(args.get("broker_id", "")).strip()
719
+ loop = asyncio.get_event_loop()
720
+
721
+ def _run():
722
+ broker = _get_broker(broker_id)
723
+ resolved_broker_id = broker_id or getattr(broker, "broker_id", "")
724
+ if not is_chat_confirm_enabled(resolved_broker_id):
725
+ return {
726
+ "success": False,
727
+ "error": (
728
+ f"Chat-confirmed execution is not enabled for broker '{resolved_broker_id}'. "
729
+ f"Run /trade allow-chat-confirm {resolved_broker_id} in the aria-code terminal "
730
+ "yourself first — this cannot be turned on from chat/MCP."
731
+ ),
732
+ }
733
+ return execute_order_preview(broker, preview_id, confirmed=True, source="chat_mcp")
734
+
735
+ try:
736
+ return await loop.run_in_executor(None, _run)
737
+ except Exception as exc:
738
+ return {"success": False, "error": str(exc)}
739
+
740
+
741
+ async def _call_report_chart(args: Dict[str, Any]) -> Dict[str, Any]:
742
+ import asyncio
743
+ import base64
744
+
745
+ from artifacts import create_user_artifact
746
+ from report_generator import _fetch_report_data_sync, generate_price_chart
747
+
748
+ symbol = str(args.get("symbol", "")).strip().upper()
749
+ if not symbol:
750
+ return {"success": False, "error": "symbol is required"}
751
+
752
+ loop = asyncio.get_event_loop()
753
+
754
+ def _run():
755
+ df, _clean_result, fundamentals = _fetch_report_data_sync(symbol)
756
+ if df is None or df.empty:
757
+ return None
758
+ return generate_price_chart(df, symbol, fundamentals)
759
+
760
+ try:
761
+ b64_png = await loop.run_in_executor(None, _run)
762
+ except Exception as exc:
763
+ return {"success": False, "error": str(exc)}
764
+ if not b64_png:
765
+ return {"success": False, "error": f"No price data available for {symbol}"}
766
+
767
+ artifact = create_user_artifact("chart", symbol, f"{symbol}_chart", ".png")
768
+ artifact.path.write_bytes(base64.b64decode(b64_png))
769
+ return {"success": True, "path": str(artifact.path)}
770
+
771
+
772
+ async def _call_indicator_chart(args: Dict[str, Any]) -> Dict[str, Any]:
773
+ import asyncio
774
+ import base64
775
+
776
+ from artifacts import create_user_artifact
777
+ from report_generator import _fetch_report_data_sync, generate_indicator_chart
778
+
779
+ symbol = str(args.get("symbol", "")).strip().upper()
780
+ if not symbol:
781
+ return {"success": False, "error": "symbol is required"}
782
+
783
+ loop = asyncio.get_event_loop()
784
+
785
+ def _run():
786
+ df, _clean_result, _fundamentals = _fetch_report_data_sync(symbol)
787
+ if df is None or df.empty:
788
+ return None
789
+ return generate_indicator_chart(df, symbol)
790
+
791
+ try:
792
+ b64_png = await loop.run_in_executor(None, _run)
793
+ except Exception as exc:
794
+ return {"success": False, "error": str(exc)}
795
+ if not b64_png:
796
+ return {"success": False, "error": f"No price data available for {symbol}"}
797
+
798
+ artifact = create_user_artifact("chart", symbol, f"{symbol}_indicators", ".png")
799
+ artifact.path.write_bytes(base64.b64decode(b64_png))
800
+ return {"success": True, "path": str(artifact.path)}
801
+
802
+
803
+ async def _call_comparison_chart(args: Dict[str, Any]) -> Dict[str, Any]:
804
+ import asyncio
805
+ import base64
806
+
807
+ from artifacts import create_user_artifact
808
+ from report_generator import _fetch_report_data_sync, generate_comparison_chart
809
+
810
+ symbols = args.get("symbols") or []
811
+ if not isinstance(symbols, list) or len(symbols) < 2:
812
+ return {"success": False, "error": "symbols must be a list of at least 2 tickers"}
813
+ symbols = [str(s).strip().upper() for s in symbols if str(s).strip()]
814
+ title = str(args.get("title") or "").strip()
815
+
816
+ loop = asyncio.get_event_loop()
817
+
818
+ def _run():
819
+ price_data = {}
820
+ for symbol in symbols:
821
+ try:
822
+ df, _clean_result, _fundamentals = _fetch_report_data_sync(symbol)
823
+ if df is not None and not df.empty:
824
+ price_data[symbol] = df
825
+ except Exception as exc:
826
+ logger.debug("[mcp] comparison_chart: skipping %s (%s)", symbol, exc)
827
+ return generate_comparison_chart(price_data, title)
828
+
829
+ try:
830
+ b64_png = await loop.run_in_executor(None, _run)
831
+ except Exception as exc:
832
+ return {"success": False, "error": str(exc)}
833
+ if not b64_png:
834
+ return {"success": False, "error": f"No usable price data for any of {symbols}"}
835
+
836
+ artifact = create_user_artifact("chart", "_".join(symbols[:4]), "comparison", ".png")
837
+ artifact.path.write_bytes(base64.b64decode(b64_png))
838
+ return {"success": True, "path": str(artifact.path)}
839
+
840
+
841
+ async def _call_allocation_chart(args: Dict[str, Any]) -> Dict[str, Any]:
842
+ import asyncio
843
+ import base64
844
+
845
+ from artifacts import create_user_artifact
846
+ from report_generator import generate_allocation_chart
847
+
848
+ title = str(args.get("title") or "").strip()
849
+ loop = asyncio.get_event_loop()
850
+
851
+ def _asdict_position(p):
852
+ from dataclasses import asdict, is_dataclass
853
+ return asdict(p) if is_dataclass(p) else dict(p)
854
+
855
+ def _run():
856
+ broker = _get_broker(str(args.get("broker_id", "")))
857
+ positions = [_asdict_position(p) for p in broker.positions()]
858
+ return generate_allocation_chart(positions, title)
859
+
860
+ try:
861
+ b64_png = await loop.run_in_executor(None, _run)
862
+ except Exception as exc:
863
+ return {"success": False, "error": str(exc)}
864
+ if not b64_png:
865
+ return {"success": False, "error": "No positions with usable market value found"}
866
+
867
+ artifact = create_user_artifact("chart", "portfolio", "allocation", ".png")
868
+ artifact.path.write_bytes(base64.b64decode(b64_png))
869
+ return {"success": True, "path": str(artifact.path)}
870
+
871
+
872
+ async def _call_report_pdf(args: Dict[str, Any]) -> Dict[str, Any]:
873
+ import asyncio
874
+
875
+ from artifacts import create_user_artifact
876
+ from markdown_pdf import markdown_to_pdf
877
+
878
+ md = str(args.get("markdown", ""))
879
+ if not md.strip():
880
+ return {"success": False, "error": "markdown is required"}
881
+ title = str(args.get("title") or "report").strip() or "report"
882
+
883
+ artifact = create_user_artifact("report", title, title, ".pdf")
884
+ loop = asyncio.get_event_loop()
885
+ try:
886
+ result_path = await loop.run_in_executor(None, markdown_to_pdf, md, artifact.path)
887
+ except Exception as exc:
888
+ return {"success": False, "error": str(exc)}
889
+ if not result_path:
890
+ return {"success": False, "error": "PDF rendering failed (no HTML→PDF engine available)"}
891
+ return {"success": True, "path": str(result_path)}
892
+
893
+
894
+ async def _call_report_generate(args: Dict[str, Any]) -> Dict[str, Any]:
895
+ from report_generator import generate_report
896
+
897
+ symbol = str(args.get("symbol", "")).strip().upper()
898
+ if not symbol:
899
+ return {"success": False, "error": "symbol is required"}
900
+ try:
901
+ path = await generate_report(symbol)
902
+ except Exception as exc:
903
+ return {"success": False, "error": str(exc)}
904
+ if not path:
905
+ return {"success": False, "error": f"Report generation failed for {symbol} (no data?)"}
906
+ return {"success": True, "path": str(path)}
907
+
908
+
909
+ async def _call_backtest_run(args: Dict[str, Any]) -> Dict[str, Any]:
910
+ import asyncio
911
+ from dataclasses import asdict
912
+
913
+ from backtest_engine import BacktestEngine, get_strategy, load_bars
914
+
915
+ symbol = str(args.get("symbol", "")).strip().upper()
916
+ if not symbol:
917
+ return {"success": False, "error": "symbol is required"}
918
+ strategy_name = str(args.get("strategy") or "sma_cross")
919
+ days = int(args.get("days", 365) or 365)
920
+
921
+ loop = asyncio.get_event_loop()
922
+
923
+ def _run():
924
+ bars = load_bars(symbol, days=days)
925
+ if not bars:
926
+ raise ValueError(f"No price history available for {symbol}")
927
+ strategy = get_strategy(strategy_name)
928
+ engine = BacktestEngine()
929
+ return engine.run({symbol: bars}, strategy)
930
+
931
+ try:
932
+ result = await loop.run_in_executor(None, _run)
933
+ except Exception as exc:
934
+ return {"success": False, "error": str(exc)}
935
+ return {"success": True, **asdict(result)}
936
+
937
+
938
+ async def _call_figma_read_file(args: Dict[str, Any]) -> Dict[str, Any]:
939
+ import asyncio
940
+
941
+ from figma_client import get_file_summary
942
+
943
+ file_key = str(args.get("file_key", "")).strip()
944
+ if not file_key:
945
+ return {"success": False, "error": "file_key is required"}
946
+ depth = int(args.get("depth", 2) or 2)
947
+ loop = asyncio.get_event_loop()
948
+ try:
949
+ return await loop.run_in_executor(None, lambda: get_file_summary(file_key, depth=depth))
950
+ except Exception as exc:
951
+ return {"success": False, "error": str(exc)}
952
+
953
+
954
+ async def _call_figma_comments(args: Dict[str, Any]) -> Dict[str, Any]:
955
+ import asyncio
956
+
957
+ from figma_client import list_comments
958
+
959
+ file_key = str(args.get("file_key", "")).strip()
960
+ if not file_key:
961
+ return {"success": False, "error": "file_key is required"}
962
+ loop = asyncio.get_event_loop()
963
+ try:
964
+ return await loop.run_in_executor(None, list_comments, file_key)
965
+ except Exception as exc:
966
+ return {"success": False, "error": str(exc)}
967
+
968
+
969
+ async def _call_video_probe(args: Dict[str, Any]) -> Dict[str, Any]:
970
+ import asyncio
971
+
972
+ from video_editor import probe_video
973
+
974
+ input_path = str(args.get("input_path", "")).strip()
975
+ if not input_path:
976
+ return {"success": False, "error": "input_path is required"}
977
+ loop = asyncio.get_event_loop()
978
+ try:
979
+ return await loop.run_in_executor(None, probe_video, input_path)
980
+ except Exception as exc:
981
+ return {"success": False, "error": str(exc)}
982
+
983
+
984
+ async def _call_video_trim(args: Dict[str, Any]) -> Dict[str, Any]:
985
+ import asyncio
986
+ from functools import partial
987
+
988
+ from video_editor import trim_video
989
+
990
+ input_path = str(args.get("input_path", "")).strip()
991
+ if not input_path or "start" not in args or "end" not in args:
992
+ return {"success": False, "error": "input_path, start, and end are required"}
993
+ loop = asyncio.get_event_loop()
994
+ fn = partial(trim_video, input_path, float(args["start"]), float(args["end"]))
995
+ try:
996
+ return await loop.run_in_executor(None, fn)
997
+ except Exception as exc:
998
+ return {"success": False, "error": str(exc)}
999
+
1000
+
1001
+ async def _call_video_concat(args: Dict[str, Any]) -> Dict[str, Any]:
1002
+ import asyncio
1003
+
1004
+ from video_editor import concat_videos
1005
+
1006
+ input_paths = args.get("input_paths") or []
1007
+ if not isinstance(input_paths, list) or len(input_paths) < 2:
1008
+ return {"success": False, "error": "input_paths must be a list of at least 2 paths"}
1009
+ loop = asyncio.get_event_loop()
1010
+ try:
1011
+ return await loop.run_in_executor(None, concat_videos, [str(p) for p in input_paths])
1012
+ except Exception as exc:
1013
+ return {"success": False, "error": str(exc)}
1014
+
1015
+
1016
+ async def _call_video_overlay_text(args: Dict[str, Any]) -> Dict[str, Any]:
1017
+ import asyncio
1018
+ from functools import partial
1019
+
1020
+ from video_editor import overlay_text
1021
+
1022
+ input_path = str(args.get("input_path", "")).strip()
1023
+ text = str(args.get("text", ""))
1024
+ if not input_path or not text:
1025
+ return {"success": False, "error": "input_path and text are required"}
1026
+ loop = asyncio.get_event_loop()
1027
+ fn = partial(
1028
+ overlay_text, input_path, text,
1029
+ position=args.get("position") or "bottom",
1030
+ font_size=int(args.get("font_size", 36) or 36),
1031
+ font_color=args.get("font_color") or "white",
1032
+ )
1033
+ try:
1034
+ return await loop.run_in_executor(None, fn)
1035
+ except Exception as exc:
1036
+ return {"success": False, "error": str(exc)}
1037
+
1038
+
1039
+ async def _call_video_overlay_audio(args: Dict[str, Any]) -> Dict[str, Any]:
1040
+ import asyncio
1041
+ from functools import partial
1042
+
1043
+ from video_editor import overlay_audio
1044
+
1045
+ input_path = str(args.get("input_path", "")).strip()
1046
+ audio_path = str(args.get("audio_path", "")).strip()
1047
+ if not input_path or not audio_path:
1048
+ return {"success": False, "error": "input_path and audio_path are required"}
1049
+ loop = asyncio.get_event_loop()
1050
+ fn = partial(overlay_audio, input_path, audio_path, replace=bool(args.get("replace", False)))
1051
+ try:
1052
+ return await loop.run_in_executor(None, fn)
1053
+ except Exception as exc:
1054
+ return {"success": False, "error": str(exc)}
1055
+
1056
+
1057
+ async def _call_video_convert(args: Dict[str, Any]) -> Dict[str, Any]:
1058
+ import asyncio
1059
+ from functools import partial
1060
+
1061
+ from video_editor import convert_video
1062
+
1063
+ input_path = str(args.get("input_path", "")).strip()
1064
+ if not input_path:
1065
+ return {"success": False, "error": "input_path is required"}
1066
+ loop = asyncio.get_event_loop()
1067
+ fn = partial(convert_video, input_path, output_format=args.get("output_format"), aspect=args.get("aspect"))
1068
+ try:
1069
+ return await loop.run_in_executor(None, fn)
1070
+ except Exception as exc:
1071
+ return {"success": False, "error": str(exc)}
1072
+
1073
+
1074
+ async def _call_video_change_speed(args: Dict[str, Any]) -> Dict[str, Any]:
1075
+ import asyncio
1076
+ from functools import partial
1077
+
1078
+ from video_editor import change_speed
1079
+
1080
+ input_path = str(args.get("input_path", "")).strip()
1081
+ if not input_path or "factor" not in args:
1082
+ return {"success": False, "error": "input_path and factor are required"}
1083
+ loop = asyncio.get_event_loop()
1084
+ fn = partial(change_speed, input_path, float(args["factor"]))
1085
+ try:
1086
+ return await loop.run_in_executor(None, fn)
1087
+ except Exception as exc:
1088
+ return {"success": False, "error": str(exc)}
1089
+
1090
+
1091
+ async def _call_video_transcribe(args: Dict[str, Any]) -> Dict[str, Any]:
1092
+ import asyncio
1093
+ from functools import partial
1094
+
1095
+ from video_analysis import transcribe_video
1096
+
1097
+ input_path = str(args.get("input_path", "")).strip()
1098
+ if not input_path:
1099
+ return {"success": False, "error": "input_path is required"}
1100
+ loop = asyncio.get_event_loop()
1101
+ fn = partial(
1102
+ transcribe_video, input_path,
1103
+ model_size=args.get("model_size") or "base",
1104
+ language=args.get("language"),
1105
+ )
1106
+ try:
1107
+ return await loop.run_in_executor(None, fn)
1108
+ except Exception as exc:
1109
+ return {"success": False, "error": str(exc)}
1110
+
1111
+
1112
+ async def _call_video_detect_scenes(args: Dict[str, Any]) -> Dict[str, Any]:
1113
+ import asyncio
1114
+ from functools import partial
1115
+
1116
+ from video_analysis import detect_scenes
1117
+
1118
+ input_path = str(args.get("input_path", "")).strip()
1119
+ if not input_path:
1120
+ return {"success": False, "error": "input_path is required"}
1121
+ loop = asyncio.get_event_loop()
1122
+ fn = partial(detect_scenes, input_path, threshold=float(args.get("threshold", 30.0) or 30.0))
1123
+ try:
1124
+ return await loop.run_in_executor(None, fn)
1125
+ except Exception as exc:
1126
+ return {"success": False, "error": str(exc)}
1127
+
1128
+
1129
+ _VIDEO_GEN_PROVIDERS = {"kling": "kling_video_client", "runway": "runway_video_client"}
1130
+
1131
+
1132
+ async def _call_video_generate_estimate(args: Dict[str, Any]) -> Dict[str, Any]:
1133
+ provider = str(args.get("provider", "")).strip().lower()
1134
+ module_name = _VIDEO_GEN_PROVIDERS.get(provider)
1135
+ if not module_name:
1136
+ return {"success": False, "error": f"provider must be one of {sorted(_VIDEO_GEN_PROVIDERS)}"}
1137
+ duration = int(args.get("duration", 0) or 0)
1138
+ if duration <= 0:
1139
+ return {"success": False, "error": "duration must be a positive number of seconds"}
1140
+
1141
+ import importlib
1142
+ client = importlib.import_module(module_name)
1143
+ return {"success": True, **client.estimate_cost(duration)}
1144
+
1145
+
1146
+ async def _call_video_generate_submit(args: Dict[str, Any]) -> Dict[str, Any]:
1147
+ # Real money is spent the instant client.submit_video() succeeds — this
1148
+ # check is the actual safety boundary for aria.video.generate_submit
1149
+ # being in _WRITE_SAFE, not just the allowlist membership itself.
1150
+ if args.get("confirmed") is not True:
1151
+ return {
1152
+ "success": False,
1153
+ "error": (
1154
+ "confirmed must be true to submit a real (paid) video generation job. "
1155
+ "Call aria.video.generate_estimate first to see the cost, then resubmit "
1156
+ "with confirmed=true."
1157
+ ),
1158
+ }
1159
+
1160
+ provider = str(args.get("provider", "")).strip().lower()
1161
+ module_name = _VIDEO_GEN_PROVIDERS.get(provider)
1162
+ if not module_name:
1163
+ return {"success": False, "error": f"provider must be one of {sorted(_VIDEO_GEN_PROVIDERS)}"}
1164
+ prompt = str(args.get("prompt", "")).strip()
1165
+ if not prompt:
1166
+ return {"success": False, "error": "prompt is required"}
1167
+
1168
+ import asyncio
1169
+ import importlib
1170
+ from functools import partial
1171
+
1172
+ client = importlib.import_module(module_name)
1173
+ kwargs: Dict[str, Any] = {"duration": int(args.get("duration", 5) or 5)}
1174
+ aspect_ratio = args.get("aspect_ratio")
1175
+ if aspect_ratio:
1176
+ kwargs["aspect_ratio" if provider == "kling" else "ratio"] = aspect_ratio
1177
+
1178
+ loop = asyncio.get_event_loop()
1179
+ fn = partial(client.submit_video, prompt, **kwargs)
1180
+ try:
1181
+ return await loop.run_in_executor(None, fn)
1182
+ except Exception as exc:
1183
+ return {"success": False, "error": str(exc)}
1184
+
1185
+
1186
+ async def _call_video_generate_status(args: Dict[str, Any]) -> Dict[str, Any]:
1187
+ provider = str(args.get("provider", "")).strip().lower()
1188
+ module_name = _VIDEO_GEN_PROVIDERS.get(provider)
1189
+ if not module_name:
1190
+ return {"success": False, "error": f"provider must be one of {sorted(_VIDEO_GEN_PROVIDERS)}"}
1191
+ task_id = str(args.get("task_id", "")).strip()
1192
+ if not task_id:
1193
+ return {"success": False, "error": "task_id is required"}
1194
+
1195
+ import asyncio
1196
+ import importlib
1197
+
1198
+ client = importlib.import_module(module_name)
1199
+ loop = asyncio.get_event_loop()
1200
+ try:
1201
+ return await loop.run_in_executor(None, client.poll_video, task_id)
1202
+ except Exception as exc:
1203
+ return {"success": False, "error": str(exc)}
1204
+
1205
+
1206
+ async def _call_report_docx(args: Dict[str, Any]) -> Dict[str, Any]:
1207
+ import asyncio
1208
+
1209
+ from artifacts import create_user_artifact
1210
+ from report_exporters import markdown_to_docx
1211
+
1212
+ md = str(args.get("markdown", ""))
1213
+ if not md.strip():
1214
+ return {"success": False, "error": "markdown is required"}
1215
+ title = str(args.get("title") or "report").strip() or "report"
1216
+
1217
+ artifact = create_user_artifact("report", title, title, ".docx")
1218
+ loop = asyncio.get_event_loop()
1219
+ try:
1220
+ result_path = await loop.run_in_executor(None, markdown_to_docx, md, artifact.path, title)
1221
+ except Exception as exc:
1222
+ return {"success": False, "error": str(exc)}
1223
+ if not result_path:
1224
+ return {"success": False, "error": "DOCX rendering failed (python-docx not installed?)"}
1225
+ return {"success": True, "path": str(result_path)}
1226
+
1227
+
1228
+ async def _call_report_pptx(args: Dict[str, Any]) -> Dict[str, Any]:
1229
+ import asyncio
1230
+
1231
+ from artifacts import create_user_artifact
1232
+ from report_exporters import markdown_to_pptx
1233
+
1234
+ md = str(args.get("markdown", ""))
1235
+ if not md.strip():
1236
+ return {"success": False, "error": "markdown is required"}
1237
+ title = str(args.get("title") or "report").strip() or "report"
1238
+
1239
+ artifact = create_user_artifact("report", title, title, ".pptx")
1240
+ loop = asyncio.get_event_loop()
1241
+ try:
1242
+ result_path = await loop.run_in_executor(None, markdown_to_pptx, md, artifact.path, title)
1243
+ except Exception as exc:
1244
+ return {"success": False, "error": str(exc)}
1245
+ if not result_path:
1246
+ return {"success": False, "error": "PPTX rendering failed (python-pptx not installed?)"}
1247
+ return {"success": True, "path": str(result_path)}
1248
+
1249
+
1250
+ async def _call_estimate_image_cost(args: Dict[str, Any]) -> Dict[str, Any]:
1251
+ from openai_image_client import estimate_cost
1252
+
1253
+ return {"success": True, **estimate_cost(size=args.get("size") or "1024x1536", quality=args.get("quality") or "high")}
1254
+
1255
+
1256
+ async def _call_generate_image(args: Dict[str, Any]) -> Dict[str, Any]:
1257
+ # Real money is spent the instant generate_image() calls OpenAI — this
1258
+ # check is the actual safety boundary, not just _WRITE_SAFE membership
1259
+ # (same pattern as _call_video_generate_submit).
1260
+ if args.get("confirmed") is not True:
1261
+ return {
1262
+ "success": False,
1263
+ "error": (
1264
+ "confirmed must be true to generate a real (paid) image. "
1265
+ "Call aria.report.estimate_image_cost first to see the cost, then resubmit with confirmed=true."
1266
+ ),
1267
+ }
1268
+
1269
+ import asyncio
1270
+ from functools import partial
1271
+
1272
+ from openai_image_client import generate_image
1273
+
1274
+ prompt = str(args.get("prompt", "")).strip()
1275
+ if not prompt:
1276
+ return {"success": False, "error": "prompt is required"}
1277
+
1278
+ loop = asyncio.get_event_loop()
1279
+ try:
1280
+ fn = partial(
1281
+ generate_image, prompt,
1282
+ size=args.get("size") or "1024x1536", quality=args.get("quality") or "high",
1283
+ confirmed=True,
1284
+ )
1285
+ return await loop.run_in_executor(None, fn)
1286
+ except Exception as exc:
1287
+ return {"success": False, "error": str(exc)}
1288
+
1289
+
1290
+ async def _call_edit_image(args: Dict[str, Any]) -> Dict[str, Any]:
1291
+ # Same real-money gate as _call_generate_image above.
1292
+ if args.get("confirmed") is not True:
1293
+ return {
1294
+ "success": False,
1295
+ "error": (
1296
+ "confirmed must be true to generate a real (paid) image edit. "
1297
+ "Call aria.report.estimate_image_cost first to see the cost, then resubmit with confirmed=true."
1298
+ ),
1299
+ }
1300
+
1301
+ import asyncio
1302
+ from functools import partial
1303
+
1304
+ from openai_image_client import edit_image
1305
+
1306
+ image_path = str(args.get("image_path", "")).strip()
1307
+ prompt = str(args.get("prompt", "")).strip()
1308
+ if not image_path or not prompt:
1309
+ return {"success": False, "error": "image_path and prompt are required"}
1310
+
1311
+ mask_path = args.get("mask_path")
1312
+ mask_path = str(mask_path).strip() if mask_path else None
1313
+
1314
+ loop = asyncio.get_event_loop()
1315
+ try:
1316
+ fn = partial(
1317
+ edit_image, image_path, prompt,
1318
+ size=args.get("size") or "1024x1536", quality=args.get("quality") or "high",
1319
+ mask_path=mask_path, confirmed=True,
1320
+ )
1321
+ return await loop.run_in_executor(None, fn)
1322
+ except Exception as exc:
1323
+ return {"success": False, "error": str(exc)}
1324
+
1325
+
1326
+ async def _call_generate_image_local(args: Dict[str, Any]) -> Dict[str, Any]:
1327
+ import asyncio
1328
+ from functools import partial
1329
+
1330
+ from local_image_provider import generate_image_local
1331
+
1332
+ prompt = str(args.get("prompt", "")).strip()
1333
+ if not prompt:
1334
+ return {"success": False, "error": "prompt is required"}
1335
+
1336
+ loop = asyncio.get_event_loop()
1337
+ fn = partial(
1338
+ generate_image_local,
1339
+ prompt,
1340
+ width=int(args.get("width", 1024) or 1024),
1341
+ height=int(args.get("height", 1024) or 1024),
1342
+ steps=int(args.get("steps", 4) or 4),
1343
+ )
1344
+ try:
1345
+ return await loop.run_in_executor(None, fn)
1346
+ except Exception as exc:
1347
+ return {"success": False, "error": str(exc)}
1348
+
1349
+
1350
+ async def _call_edit_image_local(args: Dict[str, Any]) -> Dict[str, Any]:
1351
+ import asyncio
1352
+ from functools import partial
1353
+
1354
+ from local_image_provider import edit_image_local
1355
+
1356
+ image_path = str(args.get("image_path", "")).strip()
1357
+ prompt = str(args.get("prompt", "")).strip()
1358
+ if not image_path or not prompt:
1359
+ return {"success": False, "error": "image_path and prompt are required"}
1360
+
1361
+ loop = asyncio.get_event_loop()
1362
+ fn = partial(edit_image_local, image_path, prompt, strength=float(args.get("strength", 0.6) or 0.6))
1363
+ try:
1364
+ return await loop.run_in_executor(None, fn)
1365
+ except Exception as exc:
1366
+ return {"success": False, "error": str(exc)}
1367
+
1368
+
1369
+ async def _call_canva_design(args: Dict[str, Any]) -> Dict[str, Any]:
1370
+ import asyncio
1371
+
1372
+ from canva_client import autofill_design
1373
+
1374
+ template_id = str(args.get("template_id", "")).strip()
1375
+ data = args.get("data") or {}
1376
+ if not template_id or not isinstance(data, dict):
1377
+ return {"success": False, "error": "template_id and data (object) are required"}
1378
+
1379
+ loop = asyncio.get_event_loop()
1380
+ try:
1381
+ result = await loop.run_in_executor(None, autofill_design, template_id, data)
1382
+ except Exception as exc:
1383
+ return {"success": False, "error": str(exc)}
1384
+ return result
1385
+
1386
+
1387
+ async def _call_canva_upload_asset(args: Dict[str, Any]) -> Dict[str, Any]:
1388
+ import asyncio
1389
+
1390
+ from canva_client import upload_asset
1391
+
1392
+ file_path = str(args.get("file_path", "")).strip()
1393
+ if not file_path:
1394
+ return {"success": False, "error": "file_path is required"}
1395
+
1396
+ loop = asyncio.get_event_loop()
1397
+ try:
1398
+ result = await loop.run_in_executor(None, upload_asset, file_path)
1399
+ except Exception as exc:
1400
+ return {"success": False, "error": str(exc)}
1401
+ return result
1402
+
1403
+
1404
+ def _project_root(value: Any) -> Path:
1405
+ """The project directory a governance tool reads: an existing directory under the
1406
+ server's working directory or the user's home, never an arbitrary path."""
1407
+ root = Path(str(value) if value else os.getcwd()).expanduser().resolve()
1408
+ if not root.is_dir():
1409
+ raise ValueError(f"root is not a directory: {root}")
1410
+ allowed = [Path(os.getcwd()).resolve(), Path.home().resolve()]
1411
+ if not any(root == base or base in root.parents for base in allowed):
1412
+ raise ValueError("root must be inside the server's working directory or your home directory")
1413
+ return root
1414
+
1415
+
1416
+ def _risk_view(assessment: Any) -> Dict[str, Any]:
1417
+ from aria_code.safety.risk import approval_requirement
1418
+ from aria_code.ui.render.output import format_risk_card
1419
+
1420
+ return {
1421
+ "success": True,
1422
+ "assessment": assessment.as_dict(),
1423
+ "approval": {
1424
+ "manual": approval_requirement(assessment, mode="manual"),
1425
+ "risk_mode": approval_requirement(assessment, mode="risk"),
1426
+ },
1427
+ "card": [line for _style, line in format_risk_card(assessment)],
1428
+ }
1429
+
1430
+
1431
+ async def _call_risk_assess(args: Dict[str, Any]) -> Dict[str, Any]:
1432
+ from aria_code.safety.risk import assess_command, assess_tool
1433
+
1434
+ command, tool = args.get("command"), args.get("tool")
1435
+ if isinstance(command, str) and command.strip():
1436
+ return _risk_view(assess_command(command))
1437
+ if isinstance(tool, str) and tool.strip():
1438
+ params = args.get("params") if isinstance(args.get("params"), dict) else {}
1439
+ try:
1440
+ root = _project_root(args.get("root"))
1441
+ except ValueError as exc:
1442
+ return {"success": False, "error": str(exc)}
1443
+ return _risk_view(assess_tool(tool, params, root=root))
1444
+ return {"success": False, "error": "give a `command`, or a `tool` and its `params`"}
1445
+
1446
+
1447
+ async def _call_contract_check(args: Dict[str, Any]) -> Dict[str, Any]:
1448
+ from aria_code.runtime.contract import ChangeContract, ContractError
1449
+
1450
+ tool = args.get("tool")
1451
+ if not isinstance(tool, str) or not tool.strip():
1452
+ return {"success": False, "error": "`tool` is required"}
1453
+ params = args.get("params") if isinstance(args.get("params"), dict) else {}
1454
+ try:
1455
+ root = _project_root(args.get("root"))
1456
+ except ValueError as exc:
1457
+ return {"success": False, "error": str(exc)}
1458
+ try:
1459
+ contract = ChangeContract.load(root)
1460
+ except ContractError as exc:
1461
+ # A policy that cannot be read bounds the project to reads, as it does for Aria itself.
1462
+ contract = ChangeContract.fail_closed(root, str(exc))
1463
+ if contract is None:
1464
+ return {"success": True, "contract": None, "allowed": True,
1465
+ "note": "this project declares no change contract (.aria/policy.yaml)"}
1466
+ verdict = contract.check(tool, params)
1467
+ return {
1468
+ "success": True,
1469
+ "allowed": verdict.allowed,
1470
+ "rule": verdict.rule,
1471
+ "reason": verdict.reason,
1472
+ "level": verdict.assessment.level if verdict.assessment else None,
1473
+ "contract": contract.render(),
1474
+ }
1475
+
1476
+
1477
+ async def _call_project_impact(args: Dict[str, Any]) -> Dict[str, Any]:
1478
+ targets = args.get("targets")
1479
+ if not isinstance(targets, list) or not all(isinstance(t, str) and t for t in targets) or not targets:
1480
+ return {"success": False, "error": "`targets` must be a non-empty list of paths or symbols"}
1481
+ try:
1482
+ root = _project_root(args.get("root"))
1483
+ except ValueError as exc:
1484
+ return {"success": False, "error": str(exc)}
1485
+ from aria_code.runtime.project_graph import load_project_graph
1486
+
1487
+ import asyncio
1488
+
1489
+ loop = asyncio.get_running_loop()
1490
+ impact = await loop.run_in_executor(None, lambda: load_project_graph(root).impact(targets[:20]))
1491
+ return {"success": True, "breadth": impact.breadth, "impact": impact.as_dict(), "text": impact.render()}
1492
+
1493
+
1494
+ async def _call_project_commands(args: Dict[str, Any]) -> Dict[str, Any]:
1495
+ try:
1496
+ root = _project_root(args.get("root"))
1497
+ except ValueError as exc:
1498
+ return {"success": False, "error": str(exc)}
1499
+ from aria_code.apps.cli.commands.engineering_cmds import detect_commands
1500
+
1501
+ return {"success": True, "root": str(root), "commands": detect_commands(root)}
1502
+
1503
+
1504
+ async def _call_warehouse_task(args: Dict[str, Any]) -> Dict[str, Any]:
1505
+ import asyncio
1506
+ public = {k: v for k, v in args.items() if not k.startswith("_")}
1507
+ return await asyncio.to_thread(WAREHOUSE_AGENT.handle, {**public, "_warehouse_scope": "mcp-stdio-connection"})
1508
+
1509
+
1510
+ _HANDLERS: Dict[str, Callable[[Dict[str, Any]], Awaitable[Dict[str, Any]]]] = {
1511
+ "aria.warehouse.task": _call_warehouse_task,
1512
+ "aria.risk.assess": _call_risk_assess,
1513
+ "aria.contract.check": _call_contract_check,
1514
+ "aria.project.impact": _call_project_impact,
1515
+ "aria.project.commands": _call_project_commands,
1516
+ "aria.market.quote": _call_market_quote,
1517
+ "aria.agent.team": _call_agent_team,
1518
+ "aria.artifacts.list": _call_artifacts_list,
1519
+ "aria.skill.list": _call_skill_list,
1520
+ "aria.skill.get": _call_skill_get,
1521
+ "aria.broker.positions": _call_broker_positions,
1522
+ "aria.broker.list_previews": _call_broker_list_previews,
1523
+ "aria.broker.preview_order": _call_broker_preview_order,
1524
+ "aria.broker.confirm_order": _call_broker_confirm_order,
1525
+ "aria.report.chart": _call_report_chart,
1526
+ "aria.report.indicator_chart": _call_indicator_chart,
1527
+ "aria.report.comparison_chart": _call_comparison_chart,
1528
+ "aria.report.allocation_chart": _call_allocation_chart,
1529
+ "aria.report.pdf": _call_report_pdf,
1530
+ "aria.report.estimate_image_cost": _call_estimate_image_cost,
1531
+ "aria.report.generate_image": _call_generate_image,
1532
+ "aria.report.edit_image": _call_edit_image,
1533
+ "aria.report.generate_image_local": _call_generate_image_local,
1534
+ "aria.report.edit_image_local": _call_edit_image_local,
1535
+ "aria.report.canva_design": _call_canva_design,
1536
+ "aria.report.canva_upload_asset": _call_canva_upload_asset,
1537
+ "aria.report.docx": _call_report_docx,
1538
+ "aria.report.pptx": _call_report_pptx,
1539
+ "aria.figma.read_file": _call_figma_read_file,
1540
+ "aria.figma.comments": _call_figma_comments,
1541
+ "aria.video.probe": _call_video_probe,
1542
+ "aria.video.trim": _call_video_trim,
1543
+ "aria.video.concat": _call_video_concat,
1544
+ "aria.video.overlay_text": _call_video_overlay_text,
1545
+ "aria.video.overlay_audio": _call_video_overlay_audio,
1546
+ "aria.video.convert": _call_video_convert,
1547
+ "aria.video.change_speed": _call_video_change_speed,
1548
+ "aria.video.transcribe": _call_video_transcribe,
1549
+ "aria.video.detect_scenes": _call_video_detect_scenes,
1550
+ "aria.video.generate_estimate": _call_video_generate_estimate,
1551
+ "aria.video.generate_submit": _call_video_generate_submit,
1552
+ "aria.video.generate_status": _call_video_generate_status,
1553
+ "aria.report.generate": _call_report_generate,
1554
+ "aria.backtest.run": _call_backtest_run,
1555
+ }
1556
+
1557
+
1558
+ def _build_tools() -> List[Dict[str, Any]]:
1559
+ """Real MCP tool descriptors — read-only exposures, plus the explicit
1560
+ _WRITE_SAFE allowlist.
1561
+
1562
+ Hard filter, not just trust in the exposure list: any exposure whose
1563
+ target is skill-backed (not a clean callable in v1), or that isn't
1564
+ read_only AND isn't in _WRITE_SAFE, is dropped here — so a future
1565
+ careless edit to default_exposures() can't silently make a live-trading
1566
+ or otherwise unreviewed write tool reachable. Adding something to
1567
+ _WRITE_SAFE is a deliberate, individually-justified code change, not a
1568
+ side effect of editing the exposure list.
1569
+ """
1570
+ from aria_code.packages.aria_mcp.bridge import default_exposures
1571
+
1572
+ tools = []
1573
+ for exposure in default_exposures():
1574
+ if not exposure.read_only and exposure.name not in _WRITE_SAFE:
1575
+ continue
1576
+ if exposure.target.startswith("skill:"):
1577
+ continue
1578
+ if exposure.name not in _HANDLERS:
1579
+ continue
1580
+ tools.append({
1581
+ "name": exposure.name,
1582
+ "description": exposure.description,
1583
+ "inputSchema": _INPUT_SCHEMAS.get(exposure.name, {"type": "object", "properties": {}}),
1584
+ })
1585
+ return tools
1586
+
1587
+
1588
+ TOOLS: List[Dict[str, Any]] = _build_tools()
1589
+ _TOOL_NAMES = {t["name"] for t in TOOLS}
1590
+
1591
+
1592
+ # ---------------------------------------------------------------------------
1593
+ # MCP stdio server (JSON-RPC 2.0) — same wire format as
1594
+ # Arthera's packages/quant_engine/mcp_server.py
1595
+ # ---------------------------------------------------------------------------
1596
+
1597
+ def _reply(req_id: Any, result: Any) -> None:
1598
+ msg = json.dumps({"jsonrpc": "2.0", "id": req_id, "result": result}, ensure_ascii=False)
1599
+ sys.stdout.write(msg + "\n")
1600
+ sys.stdout.flush()
1601
+
1602
+
1603
+ def _error(req_id: Any, code: int, message: str) -> None:
1604
+ msg = json.dumps(
1605
+ {"jsonrpc": "2.0", "id": req_id, "error": {"code": code, "message": message}},
1606
+ ensure_ascii=False,
1607
+ )
1608
+ sys.stdout.write(msg + "\n")
1609
+ sys.stdout.flush()
1610
+
1611
+
1612
+ async def _handle(req: Dict[str, Any]) -> None:
1613
+ method = req.get("method", "")
1614
+ req_id = req.get("id")
1615
+ params = req.get("params", {}) or {}
1616
+
1617
+ if method == "initialize":
1618
+ _reply(req_id, {
1619
+ "protocolVersion": MCP_PROTOCOL_VERSION,
1620
+ "capabilities": {"tools": {"listChanged": False}},
1621
+ "serverInfo": {"name": "aria-code", "version": "1.0.0"},
1622
+ })
1623
+
1624
+ elif method == "notifications/initialized":
1625
+ pass # no response needed for notifications
1626
+
1627
+ elif method == "tools/list":
1628
+ _reply(req_id, {"tools": TOOLS})
1629
+
1630
+ elif method == "tools/call":
1631
+ name = params.get("name", "")
1632
+ arguments = params.get("arguments", {}) or {}
1633
+ if name not in _TOOL_NAMES:
1634
+ _error(req_id, -32601, f"Unknown or non-exposed tool: {name}")
1635
+ return
1636
+ try:
1637
+ result = await _HANDLERS[name](arguments)
1638
+ text = json.dumps(result, ensure_ascii=False, default=str)
1639
+ _reply(req_id, {"content": [{"type": "text", "text": text}]})
1640
+ except TypeError as exc:
1641
+ _error(req_id, -32602, f"Invalid arguments for {name}: {exc}")
1642
+ except Exception as exc:
1643
+ # Details stay on this side: a traceback carries file paths,
1644
+ # argument values and sometimes credentials from the environment,
1645
+ # and the client is another program's model. Log it to stderr
1646
+ # (stdout is the JSON-RPC channel) and return a generic error.
1647
+ logger.error("Tool %s failed: %s\n%s", name, exc, traceback.format_exc())
1648
+ _error(req_id, -32603, f"Tool error: {name} failed ({type(exc).__name__}); "
1649
+ "details are in the server's log")
1650
+
1651
+ elif method == "ping":
1652
+ _reply(req_id, {})
1653
+
1654
+ else:
1655
+ if req_id is not None:
1656
+ _error(req_id, -32601, f"Method not found: {method}")
1657
+
1658
+
1659
+ def main() -> None:
1660
+ import asyncio
1661
+
1662
+ # --version/-V is not part of the MCP wire protocol — it exists purely
1663
+ # so a packaged binary (PyInstaller, `aria-code-mcp --version`) can be
1664
+ # smoke-tested the same way aria_cli.py's binary is, without having to
1665
+ # speak JSON-RPC over stdin just to prove the executable launches.
1666
+ # Reads the version via importlib.metadata rather than `from aria_cli
1667
+ # import __version__` deliberately — aria_cli.py's top-level imports
1668
+ # pull in the whole interactive-CLI stack (prompt_toolkit, bootstrap
1669
+ # hooks, ...), which is unnecessary weight for a binary whose only job
1670
+ # here is speaking JSON-RPC over stdio.
1671
+ if any(arg in ("--version", "-V") for arg in sys.argv[1:]):
1672
+ try:
1673
+ from importlib.metadata import version as _pkg_version
1674
+ v = _pkg_version("aria-code")
1675
+ except Exception:
1676
+ v = "unknown"
1677
+ print(f"aria-code-mcp {v}")
1678
+ return
1679
+
1680
+ logger.info("aria-code MCP server starting (stdio transport, %d tools)", len(TOOLS))
1681
+
1682
+ async def _run() -> None:
1683
+ loop = asyncio.get_event_loop()
1684
+ while True:
1685
+ line = await loop.run_in_executor(None, sys.stdin.readline)
1686
+ if not line:
1687
+ break
1688
+ line = line.strip()
1689
+ if not line:
1690
+ continue
1691
+ try:
1692
+ req = json.loads(line)
1693
+ except json.JSONDecodeError as exc:
1694
+ logger.warning("JSON parse error: %s", exc)
1695
+ continue
1696
+ try:
1697
+ await _handle(req)
1698
+ except Exception as exc:
1699
+ logger.error("Unhandled error: %s\n%s", exc, traceback.format_exc())
1700
+
1701
+ asyncio.run(_run())
1702
+
1703
+
1704
+ if __name__ == "__main__":
1705
+ main()