autobyteus 1.1.3__py3-none-any.whl → 1.1.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (284) hide show
  1. autobyteus/agent/agent.py +1 -1
  2. autobyteus/agent/bootstrap_steps/system_prompt_processing_step.py +4 -2
  3. autobyteus/agent/context/__init__.py +4 -2
  4. autobyteus/agent/context/agent_config.py +35 -8
  5. autobyteus/agent/context/agent_context_registry.py +73 -0
  6. autobyteus/agent/events/notifiers.py +4 -0
  7. autobyteus/agent/events/worker_event_dispatcher.py +1 -2
  8. autobyteus/agent/handlers/inter_agent_message_event_handler.py +8 -3
  9. autobyteus/agent/handlers/llm_complete_response_received_event_handler.py +19 -19
  10. autobyteus/agent/handlers/llm_user_message_ready_event_handler.py +2 -2
  11. autobyteus/agent/handlers/tool_result_event_handler.py +48 -20
  12. autobyteus/agent/handlers/user_input_message_event_handler.py +16 -1
  13. autobyteus/agent/input_processor/__init__.py +1 -7
  14. autobyteus/agent/message/context_file_type.py +6 -0
  15. autobyteus/agent/message/send_message_to.py +74 -99
  16. autobyteus/agent/phases/discover.py +2 -1
  17. autobyteus/agent/runtime/agent_runtime.py +10 -2
  18. autobyteus/agent/runtime/agent_worker.py +1 -0
  19. autobyteus/agent/sender_type.py +15 -0
  20. autobyteus/agent/streaming/agent_event_stream.py +6 -0
  21. autobyteus/agent/streaming/stream_event_payloads.py +12 -0
  22. autobyteus/agent/streaming/stream_events.py +3 -0
  23. autobyteus/agent/system_prompt_processor/tool_manifest_injector_processor.py +7 -4
  24. autobyteus/agent/tool_execution_result_processor/__init__.py +9 -0
  25. autobyteus/agent/tool_execution_result_processor/base_processor.py +46 -0
  26. autobyteus/agent/tool_execution_result_processor/processor_definition.py +36 -0
  27. autobyteus/agent/tool_execution_result_processor/processor_meta.py +36 -0
  28. autobyteus/agent/tool_execution_result_processor/processor_registry.py +70 -0
  29. autobyteus/agent/workspace/base_workspace.py +17 -2
  30. autobyteus/agent_team/__init__.py +1 -0
  31. autobyteus/agent_team/agent_team.py +93 -0
  32. autobyteus/agent_team/agent_team_builder.py +184 -0
  33. autobyteus/agent_team/base_agent_team.py +86 -0
  34. autobyteus/agent_team/bootstrap_steps/__init__.py +24 -0
  35. autobyteus/agent_team/bootstrap_steps/agent_configuration_preparation_step.py +73 -0
  36. autobyteus/agent_team/bootstrap_steps/agent_team_bootstrapper.py +54 -0
  37. autobyteus/agent_team/bootstrap_steps/agent_team_runtime_queue_initialization_step.py +25 -0
  38. autobyteus/agent_team/bootstrap_steps/base_agent_team_bootstrap_step.py +23 -0
  39. autobyteus/agent_team/bootstrap_steps/coordinator_initialization_step.py +41 -0
  40. autobyteus/agent_team/bootstrap_steps/coordinator_prompt_preparation_step.py +85 -0
  41. autobyteus/agent_team/bootstrap_steps/task_notifier_initialization_step.py +51 -0
  42. autobyteus/agent_team/bootstrap_steps/team_context_initialization_step.py +45 -0
  43. autobyteus/agent_team/context/__init__.py +17 -0
  44. autobyteus/agent_team/context/agent_team_config.py +33 -0
  45. autobyteus/agent_team/context/agent_team_context.py +61 -0
  46. autobyteus/agent_team/context/agent_team_runtime_state.py +56 -0
  47. autobyteus/agent_team/context/team_manager.py +147 -0
  48. autobyteus/agent_team/context/team_node_config.py +76 -0
  49. autobyteus/agent_team/events/__init__.py +29 -0
  50. autobyteus/agent_team/events/agent_team_event_dispatcher.py +39 -0
  51. autobyteus/agent_team/events/agent_team_events.py +53 -0
  52. autobyteus/agent_team/events/agent_team_input_event_queue_manager.py +21 -0
  53. autobyteus/agent_team/exceptions.py +8 -0
  54. autobyteus/agent_team/factory/__init__.py +9 -0
  55. autobyteus/agent_team/factory/agent_team_factory.py +99 -0
  56. autobyteus/agent_team/handlers/__init__.py +19 -0
  57. autobyteus/agent_team/handlers/agent_team_event_handler_registry.py +23 -0
  58. autobyteus/agent_team/handlers/base_agent_team_event_handler.py +16 -0
  59. autobyteus/agent_team/handlers/inter_agent_message_request_event_handler.py +61 -0
  60. autobyteus/agent_team/handlers/lifecycle_agent_team_event_handler.py +27 -0
  61. autobyteus/agent_team/handlers/process_user_message_event_handler.py +46 -0
  62. autobyteus/agent_team/handlers/tool_approval_team_event_handler.py +48 -0
  63. autobyteus/agent_team/phases/__init__.py +11 -0
  64. autobyteus/agent_team/phases/agent_team_operational_phase.py +19 -0
  65. autobyteus/agent_team/phases/agent_team_phase_manager.py +48 -0
  66. autobyteus/agent_team/runtime/__init__.py +13 -0
  67. autobyteus/agent_team/runtime/agent_team_runtime.py +82 -0
  68. autobyteus/agent_team/runtime/agent_team_worker.py +117 -0
  69. autobyteus/agent_team/shutdown_steps/__init__.py +17 -0
  70. autobyteus/agent_team/shutdown_steps/agent_team_shutdown_orchestrator.py +35 -0
  71. autobyteus/agent_team/shutdown_steps/agent_team_shutdown_step.py +42 -0
  72. autobyteus/agent_team/shutdown_steps/base_agent_team_shutdown_step.py +16 -0
  73. autobyteus/agent_team/shutdown_steps/bridge_cleanup_step.py +28 -0
  74. autobyteus/agent_team/shutdown_steps/sub_team_shutdown_step.py +41 -0
  75. autobyteus/agent_team/streaming/__init__.py +26 -0
  76. autobyteus/agent_team/streaming/agent_event_bridge.py +48 -0
  77. autobyteus/agent_team/streaming/agent_event_multiplexer.py +70 -0
  78. autobyteus/agent_team/streaming/agent_team_event_notifier.py +64 -0
  79. autobyteus/agent_team/streaming/agent_team_event_stream.py +33 -0
  80. autobyteus/agent_team/streaming/agent_team_stream_event_payloads.py +32 -0
  81. autobyteus/agent_team/streaming/agent_team_stream_events.py +56 -0
  82. autobyteus/agent_team/streaming/team_event_bridge.py +50 -0
  83. autobyteus/agent_team/task_notification/__init__.py +11 -0
  84. autobyteus/agent_team/task_notification/system_event_driven_agent_task_notifier.py +164 -0
  85. autobyteus/agent_team/task_notification/task_notification_mode.py +24 -0
  86. autobyteus/agent_team/utils/__init__.py +9 -0
  87. autobyteus/agent_team/utils/wait_for_idle.py +46 -0
  88. autobyteus/cli/__init__.py +1 -1
  89. autobyteus/cli/agent_team_tui/__init__.py +4 -0
  90. autobyteus/cli/agent_team_tui/app.py +210 -0
  91. autobyteus/cli/agent_team_tui/state.py +180 -0
  92. autobyteus/cli/agent_team_tui/widgets/__init__.py +6 -0
  93. autobyteus/cli/agent_team_tui/widgets/agent_list_sidebar.py +149 -0
  94. autobyteus/cli/agent_team_tui/widgets/focus_pane.py +320 -0
  95. autobyteus/cli/agent_team_tui/widgets/logo.py +20 -0
  96. autobyteus/cli/agent_team_tui/widgets/renderables.py +77 -0
  97. autobyteus/cli/agent_team_tui/widgets/shared.py +60 -0
  98. autobyteus/cli/agent_team_tui/widgets/status_bar.py +14 -0
  99. autobyteus/cli/agent_team_tui/widgets/task_board_panel.py +82 -0
  100. autobyteus/cli/cli_display.py +1 -1
  101. autobyteus/cli/workflow_tui/__init__.py +4 -0
  102. autobyteus/cli/workflow_tui/app.py +210 -0
  103. autobyteus/cli/workflow_tui/state.py +189 -0
  104. autobyteus/cli/workflow_tui/widgets/__init__.py +6 -0
  105. autobyteus/cli/workflow_tui/widgets/agent_list_sidebar.py +149 -0
  106. autobyteus/cli/workflow_tui/widgets/focus_pane.py +335 -0
  107. autobyteus/cli/workflow_tui/widgets/logo.py +27 -0
  108. autobyteus/cli/workflow_tui/widgets/renderables.py +70 -0
  109. autobyteus/cli/workflow_tui/widgets/shared.py +51 -0
  110. autobyteus/cli/workflow_tui/widgets/status_bar.py +14 -0
  111. autobyteus/events/event_types.py +8 -0
  112. autobyteus/llm/api/autobyteus_llm.py +11 -12
  113. autobyteus/llm/api/lmstudio_llm.py +34 -0
  114. autobyteus/llm/api/ollama_llm.py +8 -13
  115. autobyteus/llm/api/openai_compatible_llm.py +20 -3
  116. autobyteus/llm/autobyteus_provider.py +73 -46
  117. autobyteus/llm/llm_factory.py +103 -139
  118. autobyteus/llm/lmstudio_provider.py +104 -0
  119. autobyteus/llm/models.py +83 -53
  120. autobyteus/llm/ollama_provider.py +69 -61
  121. autobyteus/llm/ollama_provider_resolver.py +1 -0
  122. autobyteus/llm/providers.py +13 -12
  123. autobyteus/llm/runtimes.py +11 -0
  124. autobyteus/llm/token_counter/token_counter_factory.py +2 -0
  125. autobyteus/task_management/__init__.py +43 -0
  126. autobyteus/task_management/base_task_board.py +68 -0
  127. autobyteus/task_management/converters/__init__.py +11 -0
  128. autobyteus/task_management/converters/task_board_converter.py +64 -0
  129. autobyteus/task_management/converters/task_plan_converter.py +48 -0
  130. autobyteus/task_management/deliverable.py +16 -0
  131. autobyteus/task_management/deliverables/__init__.py +8 -0
  132. autobyteus/task_management/deliverables/file_deliverable.py +15 -0
  133. autobyteus/task_management/events.py +27 -0
  134. autobyteus/task_management/in_memory_task_board.py +126 -0
  135. autobyteus/task_management/schemas/__init__.py +15 -0
  136. autobyteus/task_management/schemas/deliverable_schema.py +13 -0
  137. autobyteus/task_management/schemas/plan_definition.py +35 -0
  138. autobyteus/task_management/schemas/task_status_report.py +27 -0
  139. autobyteus/task_management/task_plan.py +110 -0
  140. autobyteus/task_management/tools/__init__.py +14 -0
  141. autobyteus/task_management/tools/get_task_board_status.py +68 -0
  142. autobyteus/task_management/tools/publish_task_plan.py +113 -0
  143. autobyteus/task_management/tools/update_task_status.py +135 -0
  144. autobyteus/tools/__init__.py +2 -0
  145. autobyteus/tools/ask_user_input.py +2 -1
  146. autobyteus/tools/bash/bash_executor.py +61 -15
  147. autobyteus/tools/browser/session_aware/browser_session_aware_navigate_to.py +2 -0
  148. autobyteus/tools/browser/session_aware/browser_session_aware_web_element_trigger.py +3 -0
  149. autobyteus/tools/browser/session_aware/browser_session_aware_webpage_reader.py +3 -0
  150. autobyteus/tools/browser/session_aware/browser_session_aware_webpage_screenshot_taker.py +3 -0
  151. autobyteus/tools/browser/standalone/google_search_ui.py +2 -0
  152. autobyteus/tools/browser/standalone/navigate_to.py +2 -0
  153. autobyteus/tools/browser/standalone/web_page_pdf_generator.py +3 -0
  154. autobyteus/tools/browser/standalone/webpage_image_downloader.py +3 -0
  155. autobyteus/tools/browser/standalone/webpage_reader.py +2 -0
  156. autobyteus/tools/browser/standalone/webpage_screenshot_taker.py +3 -0
  157. autobyteus/tools/file/file_reader.py +36 -9
  158. autobyteus/tools/file/file_writer.py +37 -9
  159. autobyteus/tools/functional_tool.py +5 -4
  160. autobyteus/tools/image_downloader.py +2 -0
  161. autobyteus/tools/mcp/config_service.py +63 -58
  162. autobyteus/tools/mcp/server/http_managed_mcp_server.py +14 -2
  163. autobyteus/tools/mcp/server/stdio_managed_mcp_server.py +14 -2
  164. autobyteus/tools/mcp/server_instance_manager.py +30 -4
  165. autobyteus/tools/mcp/tool_registrar.py +106 -51
  166. autobyteus/tools/parameter_schema.py +17 -11
  167. autobyteus/tools/pdf_downloader.py +2 -1
  168. autobyteus/tools/registry/tool_definition.py +36 -37
  169. autobyteus/tools/registry/tool_registry.py +50 -2
  170. autobyteus/tools/timer.py +2 -0
  171. autobyteus/tools/tool_category.py +15 -4
  172. autobyteus/tools/tool_meta.py +6 -1
  173. autobyteus/tools/tool_origin.py +10 -0
  174. autobyteus/tools/usage/formatters/default_json_example_formatter.py +78 -3
  175. autobyteus/tools/usage/formatters/default_xml_example_formatter.py +23 -3
  176. autobyteus/tools/usage/formatters/gemini_json_example_formatter.py +6 -0
  177. autobyteus/tools/usage/formatters/google_json_example_formatter.py +7 -0
  178. autobyteus/tools/usage/formatters/openai_json_example_formatter.py +6 -4
  179. autobyteus/tools/usage/parsers/gemini_json_tool_usage_parser.py +23 -7
  180. autobyteus/tools/usage/parsers/provider_aware_tool_usage_parser.py +14 -25
  181. autobyteus/tools/usage/providers/__init__.py +2 -12
  182. autobyteus/tools/usage/providers/tool_manifest_provider.py +36 -29
  183. autobyteus/tools/usage/registries/__init__.py +7 -12
  184. autobyteus/tools/usage/registries/tool_formatter_pair.py +15 -0
  185. autobyteus/tools/usage/registries/tool_formatting_registry.py +58 -0
  186. autobyteus/tools/usage/registries/tool_usage_parser_registry.py +55 -0
  187. autobyteus/workflow/agentic_workflow.py +93 -0
  188. autobyteus/{agent/workflow → workflow}/base_agentic_workflow.py +19 -27
  189. autobyteus/workflow/bootstrap_steps/__init__.py +20 -0
  190. autobyteus/workflow/bootstrap_steps/agent_tool_injection_step.py +34 -0
  191. autobyteus/workflow/bootstrap_steps/base_workflow_bootstrap_step.py +23 -0
  192. autobyteus/workflow/bootstrap_steps/coordinator_initialization_step.py +41 -0
  193. autobyteus/workflow/bootstrap_steps/coordinator_prompt_preparation_step.py +108 -0
  194. autobyteus/workflow/bootstrap_steps/workflow_bootstrapper.py +50 -0
  195. autobyteus/workflow/bootstrap_steps/workflow_runtime_queue_initialization_step.py +25 -0
  196. autobyteus/workflow/context/__init__.py +17 -0
  197. autobyteus/workflow/context/team_manager.py +147 -0
  198. autobyteus/workflow/context/workflow_config.py +30 -0
  199. autobyteus/workflow/context/workflow_context.py +61 -0
  200. autobyteus/workflow/context/workflow_node_config.py +76 -0
  201. autobyteus/workflow/context/workflow_runtime_state.py +53 -0
  202. autobyteus/workflow/events/__init__.py +29 -0
  203. autobyteus/workflow/events/workflow_event_dispatcher.py +39 -0
  204. autobyteus/workflow/events/workflow_events.py +53 -0
  205. autobyteus/workflow/events/workflow_input_event_queue_manager.py +21 -0
  206. autobyteus/workflow/exceptions.py +8 -0
  207. autobyteus/workflow/factory/__init__.py +9 -0
  208. autobyteus/workflow/factory/workflow_factory.py +99 -0
  209. autobyteus/workflow/handlers/__init__.py +19 -0
  210. autobyteus/workflow/handlers/base_workflow_event_handler.py +16 -0
  211. autobyteus/workflow/handlers/inter_agent_message_request_event_handler.py +61 -0
  212. autobyteus/workflow/handlers/lifecycle_workflow_event_handler.py +27 -0
  213. autobyteus/workflow/handlers/process_user_message_event_handler.py +46 -0
  214. autobyteus/workflow/handlers/tool_approval_workflow_event_handler.py +39 -0
  215. autobyteus/workflow/handlers/workflow_event_handler_registry.py +23 -0
  216. autobyteus/workflow/phases/__init__.py +11 -0
  217. autobyteus/workflow/phases/workflow_operational_phase.py +19 -0
  218. autobyteus/workflow/phases/workflow_phase_manager.py +48 -0
  219. autobyteus/workflow/runtime/__init__.py +13 -0
  220. autobyteus/workflow/runtime/workflow_runtime.py +82 -0
  221. autobyteus/workflow/runtime/workflow_worker.py +117 -0
  222. autobyteus/workflow/shutdown_steps/__init__.py +17 -0
  223. autobyteus/workflow/shutdown_steps/agent_team_shutdown_step.py +42 -0
  224. autobyteus/workflow/shutdown_steps/base_workflow_shutdown_step.py +16 -0
  225. autobyteus/workflow/shutdown_steps/bridge_cleanup_step.py +28 -0
  226. autobyteus/workflow/shutdown_steps/sub_workflow_shutdown_step.py +41 -0
  227. autobyteus/workflow/shutdown_steps/workflow_shutdown_orchestrator.py +35 -0
  228. autobyteus/workflow/streaming/__init__.py +26 -0
  229. autobyteus/workflow/streaming/agent_event_bridge.py +48 -0
  230. autobyteus/workflow/streaming/agent_event_multiplexer.py +70 -0
  231. autobyteus/workflow/streaming/workflow_event_bridge.py +50 -0
  232. autobyteus/workflow/streaming/workflow_event_notifier.py +83 -0
  233. autobyteus/workflow/streaming/workflow_event_stream.py +33 -0
  234. autobyteus/workflow/streaming/workflow_stream_event_payloads.py +28 -0
  235. autobyteus/workflow/streaming/workflow_stream_events.py +45 -0
  236. autobyteus/workflow/utils/__init__.py +9 -0
  237. autobyteus/workflow/utils/wait_for_idle.py +46 -0
  238. autobyteus/workflow/workflow_builder.py +151 -0
  239. {autobyteus-1.1.3.dist-info → autobyteus-1.1.5.dist-info}/METADATA +16 -14
  240. autobyteus-1.1.5.dist-info/RECORD +455 -0
  241. {autobyteus-1.1.3.dist-info → autobyteus-1.1.5.dist-info}/top_level.txt +1 -0
  242. examples/__init__.py +1 -0
  243. examples/agent_team/__init__.py +1 -0
  244. examples/discover_phase_transitions.py +104 -0
  245. examples/run_browser_agent.py +262 -0
  246. examples/run_google_slides_agent.py +287 -0
  247. examples/run_mcp_browser_client.py +174 -0
  248. examples/run_mcp_google_slides_client.py +270 -0
  249. examples/run_mcp_list_tools.py +189 -0
  250. examples/run_poem_writer.py +284 -0
  251. examples/run_sqlite_agent.py +295 -0
  252. autobyteus/agent/context/agent_phase_manager.py +0 -264
  253. autobyteus/agent/context/phases.py +0 -49
  254. autobyteus/agent/group/__init__.py +0 -0
  255. autobyteus/agent/group/agent_group.py +0 -164
  256. autobyteus/agent/group/agent_group_context.py +0 -81
  257. autobyteus/agent/input_processor/content_prefixing_input_processor.py +0 -41
  258. autobyteus/agent/input_processor/metadata_appending_input_processor.py +0 -34
  259. autobyteus/agent/input_processor/passthrough_input_processor.py +0 -33
  260. autobyteus/agent/workflow/__init__.py +0 -11
  261. autobyteus/agent/workflow/agentic_workflow.py +0 -89
  262. autobyteus/tools/mcp/call_handlers/__init__.py +0 -16
  263. autobyteus/tools/mcp/call_handlers/base_handler.py +0 -40
  264. autobyteus/tools/mcp/call_handlers/stdio_handler.py +0 -76
  265. autobyteus/tools/mcp/call_handlers/streamable_http_handler.py +0 -55
  266. autobyteus/tools/mcp/registrar.py +0 -202
  267. autobyteus/tools/usage/providers/json_example_provider.py +0 -32
  268. autobyteus/tools/usage/providers/json_schema_provider.py +0 -35
  269. autobyteus/tools/usage/providers/json_tool_usage_parser_provider.py +0 -28
  270. autobyteus/tools/usage/providers/xml_example_provider.py +0 -28
  271. autobyteus/tools/usage/providers/xml_schema_provider.py +0 -29
  272. autobyteus/tools/usage/providers/xml_tool_usage_parser_provider.py +0 -26
  273. autobyteus/tools/usage/registries/json_example_formatter_registry.py +0 -51
  274. autobyteus/tools/usage/registries/json_schema_formatter_registry.py +0 -51
  275. autobyteus/tools/usage/registries/json_tool_usage_parser_registry.py +0 -42
  276. autobyteus/tools/usage/registries/xml_example_formatter_registry.py +0 -30
  277. autobyteus/tools/usage/registries/xml_schema_formatter_registry.py +0 -33
  278. autobyteus/tools/usage/registries/xml_tool_usage_parser_registry.py +0 -30
  279. autobyteus/workflow/simple_task.py +0 -98
  280. autobyteus/workflow/task.py +0 -147
  281. autobyteus/workflow/workflow.py +0 -49
  282. autobyteus-1.1.3.dist-info/RECORD +0 -312
  283. {autobyteus-1.1.3.dist-info → autobyteus-1.1.5.dist-info}/WHEEL +0 -0
  284. {autobyteus-1.1.3.dist-info → autobyteus-1.1.5.dist-info}/licenses/LICENSE +0 -0
@@ -3,29 +3,57 @@ import logging
3
3
  from typing import TYPE_CHECKING
4
4
 
5
5
  from autobyteus.tools import tool
6
+ from autobyteus.tools.tool_category import ToolCategory
6
7
 
7
8
  if TYPE_CHECKING:
8
9
  from autobyteus.agent.context import AgentContext
9
10
 
10
11
  logger = logging.getLogger(__name__)
11
12
 
12
- @tool(name="FileWriter")
13
+ @tool(name="FileWriter", category=ToolCategory.FILE_SYSTEM)
13
14
  async def file_writer(context: 'AgentContext', path: str, content: str) -> str:
14
15
  """
15
16
  Creates or overwrites a file with specified content.
16
- 'path' is the path where the file will be written.
17
+ 'path' is the path where the file will be written. If relative, it must be resolved against a configured agent workspace.
17
18
  'content' is the string content to write.
18
19
  Creates parent directories if they don't exist.
20
+ Raises ValueError if a relative path is given without a valid workspace.
19
21
  Raises IOError if file writing fails.
20
22
  """
21
- logger.debug(f"Functional FileWriter tool for agent {context.agent_id}, path: {path}")
23
+ logger.debug(f"Functional FileWriter tool for agent {context.agent_id}, initial path: {path}")
24
+
25
+ final_path: str
26
+ if os.path.isabs(path):
27
+ final_path = path
28
+ logger.debug(f"Path '{path}' is absolute. Using it directly.")
29
+ else:
30
+ if not context.workspace:
31
+ error_msg = f"Relative path '{path}' provided, but no workspace is configured for agent '{context.agent_id}'. A workspace is required to resolve relative paths."
32
+ logger.error(error_msg)
33
+ raise ValueError(error_msg)
34
+
35
+ base_path = context.workspace.get_base_path()
36
+ if not base_path or not isinstance(base_path, str):
37
+ error_msg = f"Agent '{context.agent_id}' has a configured workspace, but it provided an invalid base path ('{base_path}'). Cannot resolve relative path '{path}'."
38
+ logger.error(error_msg)
39
+ raise ValueError(error_msg)
40
+
41
+ final_path = os.path.join(base_path, path)
42
+ logger.debug(f"Path '{path}' is relative. Resolved to '{final_path}' using workspace base path '{base_path}'.")
43
+
22
44
  try:
23
- dir_path = os.path.dirname(path)
24
- if dir_path: # Only if path includes a directory part
45
+ # It's good practice to normalize the path to handle things like '..'
46
+ final_path = os.path.normpath(final_path)
47
+
48
+ dir_path = os.path.dirname(final_path)
49
+ if dir_path:
25
50
  os.makedirs(dir_path, exist_ok=True)
26
- with open(path, 'w', encoding='utf-8') as file:
51
+
52
+ with open(final_path, 'w', encoding='utf-8') as file:
27
53
  file.write(content)
28
- return f"File created/updated at {path}"
54
+
55
+ logger.info(f"File successfully written to '{final_path}' for agent '{context.agent_id}'.")
56
+ return f"File created/updated at {final_path}"
29
57
  except Exception as e:
30
- logger.error(f"Error writing file {path} for agent {context.agent_id}: {e}", exc_info=True)
31
- raise IOError(f"Could not write file at {path}: {str(e)}")
58
+ logger.error(f"Error writing file to final path '{final_path}' for agent {context.agent_id}: {e}", exc_info=True)
59
+ raise IOError(f"Could not write file at '{final_path}': {str(e)}")
@@ -8,6 +8,7 @@ from autobyteus.tools.base_tool import BaseTool
8
8
  from autobyteus.tools.parameter_schema import ParameterSchema, ParameterDefinition, ParameterType
9
9
  from autobyteus.tools.tool_config import ToolConfig
10
10
  from autobyteus.tools.registry import default_tool_registry, ToolDefinition
11
+ from autobyteus.tools.tool_origin import ToolOrigin
11
12
  from autobyteus.tools.tool_category import ToolCategory
12
13
 
13
14
  if TYPE_CHECKING:
@@ -200,7 +201,8 @@ def tool(
200
201
  name: Optional[str] = None,
201
202
  description: Optional[str] = None,
202
203
  argument_schema: Optional[ParameterSchema] = None,
203
- config_schema: Optional[ParameterSchema] = None
204
+ config_schema: Optional[ParameterSchema] = None,
205
+ category: str = ToolCategory.GENERAL
204
206
  ):
205
207
  def decorator(func: Callable) -> FunctionalTool:
206
208
  tool_name = name or func.__name__
@@ -227,8 +229,6 @@ def tool(
227
229
  instantiation_config=inst_config.params if inst_config else None
228
230
  )
229
231
 
230
- # The decorator's responsibility is now just to assemble the raw metadata
231
- # and create the definition. It does NOT generate usage strings.
232
232
  tool_def = ToolDefinition(
233
233
  name=tool_name,
234
234
  description=tool_desc,
@@ -236,7 +236,8 @@ def tool(
236
236
  config_schema=config_schema,
237
237
  custom_factory=factory,
238
238
  tool_class=None,
239
- category=ToolCategory.LOCAL
239
+ origin=ToolOrigin.LOCAL,
240
+ category=category
240
241
  )
241
242
  default_tool_registry.register_tool(tool_def)
242
243
 
@@ -7,6 +7,7 @@ from typing import Optional, TYPE_CHECKING, Any
7
7
  from autobyteus.tools.base_tool import BaseTool
8
8
  from autobyteus.tools.tool_config import ToolConfig
9
9
  from autobyteus.tools.parameter_schema import ParameterSchema, ParameterDefinition, ParameterType
10
+ from autobyteus.tools.tool_category import ToolCategory
10
11
  from PIL import Image
11
12
  from io import BytesIO
12
13
  from autobyteus.utils.file_utils import get_default_download_folder
@@ -18,6 +19,7 @@ if TYPE_CHECKING:
18
19
  logger = logging.getLogger(__name__)
19
20
 
20
21
  class ImageDownloader(BaseTool):
22
+ CATEGORY = ToolCategory.WEB
21
23
  supported_formats = ['.jpeg', '.jpg', '.gif', '.png', '.webp']
22
24
 
23
25
  def __init__(self, config: Optional[ToolConfig] = None):
@@ -118,9 +118,10 @@ class McpConfigService(metaclass=SingletonMeta):
118
118
  f"Total unique configs stored: {len(self._configs)}.")
119
119
  return config_object
120
120
 
121
- def load_config(self, config_dict: Dict[str, Any]) -> BaseMcpConfig:
121
+ def load_config_from_dict(self, config_dict: Dict[str, Any]) -> BaseMcpConfig:
122
122
  """
123
123
  Parses a single raw configuration dictionary and adds it to the service.
124
+ This method handles loading a single configuration.
124
125
 
125
126
  Args:
126
127
  config_dict: A dictionary representing a single server config,
@@ -132,77 +133,81 @@ class McpConfigService(metaclass=SingletonMeta):
132
133
  config_object = self.parse_mcp_config_dict(config_dict)
133
134
  return self.add_config(config_object)
134
135
 
135
-
136
- def load_configs(self, source: Union[str, List[Dict[str, Any]], Dict[str, Any]]) -> List[BaseMcpConfig]:
136
+ def load_configs_from_dict(self, configs_data: Dict[str, Dict[str, Any]]) -> List[BaseMcpConfig]:
137
137
  """
138
- Loads multiple MCP configurations from a source, parsing and adding them.
138
+ Loads multiple MCP configurations from a dictionary where keys are server_ids.
139
139
  This will overwrite any existing configurations with the same server_id.
140
140
 
141
141
  Args:
142
- source: The data source. Can be:
143
- 1. A file path (str) to a JSON file.
144
- 2. A list of MCP server configuration dictionaries.
145
- 3. A dictionary of configurations, keyed by server_id.
142
+ configs_data: A dictionary of configurations, keyed by server_id.
146
143
 
147
144
  Returns:
148
145
  A list of the successfully added McpServerConfig objects.
149
146
  """
147
+ if not isinstance(configs_data, dict):
148
+ raise TypeError("configs_data must be a dictionary of server configurations keyed by server_id.")
149
+
150
150
  loaded_mcp_configs: List[BaseMcpConfig] = []
151
+ logger.info(f"McpConfigService loading {len(configs_data)} configs from dictionary.")
151
152
 
152
- if isinstance(source, str):
153
- if not os.path.exists(source):
154
- logger.error(f"MCP configuration file not found at path: {source}")
155
- raise FileNotFoundError(f"MCP configuration file not found: {source}")
153
+ for server_id, single_config_data in configs_data.items():
154
+ if not isinstance(single_config_data, dict):
155
+ raise ValueError(f"Configuration for server_id '{server_id}' must be a dictionary.")
156
156
  try:
157
- with open(source, 'r', encoding='utf-8') as f:
158
- json_data = json.load(f)
159
- logger.info(f"Successfully loaded JSON data from file: {source}")
160
- return self.load_configs(json_data)
161
- except json.JSONDecodeError as e:
162
- raise ValueError(f"Invalid JSON in MCP configuration file {source}: {e}") from e
163
- except Exception as e:
164
- raise ValueError(f"Could not read MCP configuration file {source}: {e}") from e
165
-
166
- elif isinstance(source, list):
167
- for i, config_item_dict in enumerate(source):
168
- if not isinstance(config_item_dict, dict):
169
- raise ValueError(f"Item at index {i} in source list is not a dictionary.")
170
-
171
- server_id = config_item_dict.get('server_id')
172
- if not server_id:
173
- raise ValueError(f"Item at index {i} in source list is missing 'server_id' field.")
174
-
175
- try:
176
- # A list item is a single config, but doesn't have the server_id as the key,
177
- # so we wrap it to use the parser.
178
- config_obj = McpConfigService.parse_mcp_config_dict({server_id: config_item_dict})
179
- self.add_config(config_obj)
180
- loaded_mcp_configs.append(config_obj)
181
- except ValueError as e:
182
- logger.error(f"Invalid MCP configuration for list item at index {i}: {e}")
183
- raise
157
+ # The parser expects the server_id to be the key, so we reconstruct that.
158
+ config_obj = McpConfigService.parse_mcp_config_dict({server_id: single_config_data})
159
+ self.add_config(config_obj)
160
+ loaded_mcp_configs.append(config_obj)
161
+ except ValueError as e:
162
+ logger.error(f"Invalid MCP configuration for server_id '{server_id}': {e}")
163
+ raise
184
164
 
185
- elif isinstance(source, dict):
186
- logger.info("Loading MCP server configurations from a dictionary of configs (keyed by server_id).")
187
- for server_id, config_data in source.items():
188
- if not isinstance(config_data, dict):
189
- raise ValueError(f"Configuration for server_id '{server_id}' must be a dictionary.")
190
-
191
- try:
192
- config_obj = McpConfigService.parse_mcp_config_dict({server_id: config_data})
193
- self.add_config(config_obj)
194
- loaded_mcp_configs.append(config_obj)
195
- except ValueError as e:
196
- logger.error(f"Invalid MCP configuration for server_id '{server_id}': {e}")
197
- raise
198
- else:
199
- raise TypeError(f"Unsupported source type for load_configs: {type(source)}. "
200
- "Expected file path, list of dicts, or dict of dicts.")
201
-
202
- logger.info(f"McpConfigService load_configs completed. {len(loaded_mcp_configs)} new configurations processed. "
203
- f"Total unique configs stored: {len(self._configs)}.")
165
+ logger.info(f"McpConfigService load_configs_from_dict completed. {len(loaded_mcp_configs)} configurations processed.")
204
166
  return loaded_mcp_configs
205
167
 
168
+ def load_configs_from_file(self, filepath: str) -> List[BaseMcpConfig]:
169
+ """
170
+ Loads MCP configurations from a JSON file. The file should contain a single
171
+ JSON object where keys are server_ids.
172
+
173
+ Args:
174
+ filepath: The path to the JSON configuration file.
175
+
176
+ Returns:
177
+ A list of the successfully added McpServerConfig objects.
178
+ """
179
+ if not os.path.exists(filepath):
180
+ logger.error(f"MCP configuration file not found at path: {filepath}")
181
+ raise FileNotFoundError(f"MCP configuration file not found: {filepath}")
182
+ try:
183
+ with open(filepath, 'r', encoding='utf-8') as f:
184
+ json_data = json.load(f)
185
+ logger.info(f"Successfully loaded JSON data from file: {filepath}")
186
+
187
+ if isinstance(json_data, dict):
188
+ return self.load_configs_from_dict(json_data)
189
+ else:
190
+ # To maintain some flexibility, we can check for the list format.
191
+ # But the primary documented format should be the dict.
192
+ logger.warning("Loading MCP configs from a list in a JSON file is supported but deprecated. "
193
+ "The recommended format is a dictionary keyed by server_id.")
194
+ configs_as_dict = {}
195
+ if isinstance(json_data, list):
196
+ for item in json_data:
197
+ if isinstance(item, dict) and 'server_id' in item:
198
+ server_id = item['server_id']
199
+ configs_as_dict[server_id] = item
200
+ else:
201
+ raise ValueError("When loading from a list, each item must be a dict with a 'server_id'.")
202
+ return self.load_configs_from_dict(configs_as_dict)
203
+
204
+ raise TypeError(f"Unsupported JSON structure in {filepath}. Expected a dictionary of configurations.")
205
+
206
+ except json.JSONDecodeError as e:
207
+ raise ValueError(f"Invalid JSON in MCP configuration file {filepath}: {e}") from e
208
+ except Exception as e:
209
+ raise IOError(f"Could not read or process MCP configuration file {filepath}: {e}") from e
210
+
206
211
  def get_config(self, server_id: str) -> Optional[BaseMcpConfig]:
207
212
  """Retrieves an MCP server configuration by its unique server ID."""
208
213
  return self._configs.get(server_id)
@@ -1,5 +1,5 @@
1
- # file: autobyteus/autobyteus/tools/mcp/server/http_managed_mcp_server.py
2
1
  import logging
2
+ import asyncio
3
3
  from typing import cast
4
4
 
5
5
  from mcp import ClientSession
@@ -10,6 +10,8 @@ from ..types import StreamableHttpMcpServerConfig
10
10
 
11
11
  logger = logging.getLogger(__name__)
12
12
 
13
+ INITIALIZE_TIMEOUT = 10 # seconds
14
+
13
15
  class HttpManagedMcpServer(BaseManagedMcpServer):
14
16
  """Manages the lifecycle of a streamable_http-based MCP server."""
15
17
 
@@ -25,5 +27,15 @@ class HttpManagedMcpServer(BaseManagedMcpServer):
25
27
  streamablehttp_client(config.url, headers=config.headers)
26
28
  )
27
29
  session = await self._exit_stack.enter_async_context(ClientSession(read_stream, write_stream))
28
- logger.debug(f"ClientSession established for HTTP server '{self.server_id}'.")
30
+
31
+ # --- FIX: Initialize the session after creation with a timeout ---
32
+ try:
33
+ logger.debug(f"Initializing ClientSession for HTTP server '{self.server_id}' with a {INITIALIZE_TIMEOUT}s timeout.")
34
+ await asyncio.wait_for(session.initialize(), timeout=INITIALIZE_TIMEOUT)
35
+ except asyncio.TimeoutError:
36
+ logger.error(f"Timeout occurred while initializing session for HTTP server '{self.server_id}'. The server did not respond in time.")
37
+ # Re-raise as a standard exception to be handled by the BaseManagedMcpServer's connect method.
38
+ raise ConnectionError(f"Server '{self.server_id}' failed to initialize within the timeout period.")
39
+
40
+ logger.debug(f"ClientSession established and initialized for HTTP server '{self.server_id}'.")
29
41
  return session
@@ -1,5 +1,5 @@
1
- # file: autobyteus/autobyteus/tools/mcp/server/stdio_managed_mcp_server.py
2
1
  import logging
2
+ import asyncio
3
3
  from typing import cast
4
4
 
5
5
  from mcp import ClientSession, StdioServerParameters
@@ -10,6 +10,8 @@ from ..types import StdioMcpServerConfig
10
10
 
11
11
  logger = logging.getLogger(__name__)
12
12
 
13
+ INITIALIZE_TIMEOUT = 10 # seconds
14
+
13
15
  class StdioManagedMcpServer(BaseManagedMcpServer):
14
16
  """Manages the lifecycle of a stdio-based MCP server."""
15
17
 
@@ -29,5 +31,15 @@ class StdioManagedMcpServer(BaseManagedMcpServer):
29
31
  logger.debug(f"Establishing stdio connection for server '{self.server_id}' with command: {config.command}")
30
32
  read_stream, write_stream = await self._exit_stack.enter_async_context(stdio_client(stdio_params))
31
33
  session = await self._exit_stack.enter_async_context(ClientSession(read_stream, write_stream))
32
- logger.debug(f"ClientSession established for stdio server '{self.server_id}'.")
34
+
35
+ # --- FIX: Initialize the session after creation with a timeout ---
36
+ try:
37
+ logger.debug(f"Initializing ClientSession for stdio server '{self.server_id}' with a {INITIALIZE_TIMEOUT}s timeout.")
38
+ await asyncio.wait_for(session.initialize(), timeout=INITIALIZE_TIMEOUT)
39
+ except asyncio.TimeoutError:
40
+ logger.error(f"Timeout occurred while initializing session for server '{self.server_id}'. The server did not respond in time.")
41
+ # Re-raise as a standard exception to be handled by the BaseManagedMcpServer's connect method.
42
+ raise ConnectionError(f"Server '{self.server_id}' failed to initialize within the timeout period.")
43
+
44
+ logger.debug(f"ClientSession established and initialized for stdio server '{self.server_id}'.")
33
45
  return session
@@ -1,12 +1,14 @@
1
1
  # file: autobyteus/autobyteus/tools/mcp/server_instance_manager.py
2
2
  import logging
3
+ import copy
3
4
  from typing import Dict, List, AsyncIterator
4
5
  from contextlib import asynccontextmanager
5
6
 
6
7
  from autobyteus.utils.singleton import SingletonMeta
8
+ from autobyteus.agent.context import AgentContextRegistry
7
9
  from .config_service import McpConfigService
8
10
  from .server import BaseManagedMcpServer, StdioManagedMcpServer, HttpManagedMcpServer
9
- from .types import McpTransportType, McpServerInstanceKey, BaseMcpConfig
11
+ from .types import McpTransportType, McpServerInstanceKey, BaseMcpConfig, StdioMcpServerConfig
10
12
 
11
13
  logger = logging.getLogger(__name__)
12
14
 
@@ -17,6 +19,7 @@ class McpServerInstanceManager(metaclass=SingletonMeta):
17
19
  """
18
20
  def __init__(self):
19
21
  self._config_service = McpConfigService()
22
+ self._context_registry = AgentContextRegistry()
20
23
  self._active_servers: Dict[McpServerInstanceKey, BaseManagedMcpServer] = {}
21
24
  logger.info("McpServerInstanceManager initialized.")
22
25
 
@@ -40,11 +43,34 @@ class McpServerInstanceManager(metaclass=SingletonMeta):
40
43
  return self._active_servers[instance_key]
41
44
 
42
45
  logger.info(f"Creating new persistent server instance for {instance_key}.")
43
- config = self._config_service.get_config(server_id)
44
- if not config:
46
+
47
+ base_config = self._config_service.get_config(server_id)
48
+ if not base_config:
45
49
  raise ValueError(f"No configuration found for server_id '{server_id}'.")
46
50
 
47
- server_instance = self._create_server_instance(config)
51
+ final_config = base_config
52
+ # --- DYNAMIC WORKSPACE ENV VARIABLE INJECTION ---
53
+ if isinstance(base_config, StdioMcpServerConfig):
54
+ agent_context = self._context_registry.get_context(agent_id)
55
+ if agent_context and agent_context.workspace:
56
+ workspace_path = agent_context.workspace.get_base_path()
57
+ if workspace_path:
58
+ logger.info(f"Agent '{agent_id}' has a workspace. Injecting AUTOBYTEUS_AGENT_WORKSPACE='{workspace_path}' for MCP server '{server_id}'.")
59
+ # Create a copy of the config to avoid modifying the global one
60
+ config_copy = copy.deepcopy(base_config)
61
+ # Ensure env dict exists
62
+ if config_copy.env is None:
63
+ config_copy.env = {}
64
+ # Add our environment variable
65
+ config_copy.env['AUTOBYTEUS_AGENT_WORKSPACE'] = workspace_path
66
+ final_config = config_copy
67
+ else:
68
+ logger.warning(f"Agent '{agent_id}' workspace for server '{server_id}' did not provide a base path. No workspace environment variable will be set.")
69
+ else:
70
+ logger.debug(f"No workspace found for agent '{agent_id}'. No workspace environment variable will be set for MCP server '{server_id}'.")
71
+ # --- END DYNAMIC WORKSPACE ENV VARIABLE INJECTION ---
72
+
73
+ server_instance = self._create_server_instance(final_config)
48
74
  self._active_servers[instance_key] = server_instance
49
75
  return server_instance
50
76
 
@@ -11,6 +11,7 @@ from .types import BaseMcpConfig
11
11
  from .server import BaseManagedMcpServer
12
12
 
13
13
  from autobyteus.tools.registry import ToolRegistry, ToolDefinition
14
+ from autobyteus.tools.tool_origin import ToolOrigin
14
15
  from autobyteus.tools.tool_category import ToolCategory
15
16
  from autobyteus.utils.singleton import SingletonMeta
16
17
  from mcp import types as mcp_types
@@ -70,74 +71,128 @@ class McpToolRegistrar(metaclass=SingletonMeta):
70
71
  name=registered_name,
71
72
  description=actual_desc,
72
73
  argument_schema=actual_arg_schema,
73
- category=ToolCategory.MCP,
74
+ origin=ToolOrigin.MCP,
75
+ category=server_config.server_id, # Use server_id as the category
74
76
  metadata={"mcp_server_id": server_config.server_id}, # Store origin in generic metadata
75
77
  custom_factory=tool_factory.create_tool,
76
78
  config_schema=None,
77
79
  tool_class=None
78
80
  )
79
81
 
80
- async def discover_and_register_tools(self, mcp_config: Optional[Union[BaseMcpConfig, Dict[str, Any]]] = None) -> List[ToolDefinition]:
82
+ async def _discover_and_register_from_config(self, server_config: BaseMcpConfig, schema_mapper: McpSchemaMapper) -> List[ToolDefinition]:
81
83
  """
82
- Discovers tools from MCP servers and registers them.
84
+ Performs the core discovery and registration logic for a single server configuration.
85
+ This method does NOT handle un-registration of existing tools.
83
86
  """
84
- configs_to_process: List[BaseMcpConfig]
87
+ registered_tool_definitions: List[ToolDefinition] = []
88
+ if not server_config.enabled:
89
+ logger.info(f"MCP server '{server_config.server_id}' is disabled. Skipping.")
90
+ return registered_tool_definitions
91
+
92
+ logger.info(f"Discovering tools from MCP server: '{server_config.server_id}'")
85
93
 
86
- if mcp_config:
87
- validated_config: BaseMcpConfig
88
- if isinstance(mcp_config, dict):
94
+ try:
95
+ remote_tools = await self._fetch_tools_from_server(server_config)
96
+ logger.info(f"Discovered {len(remote_tools)} tools from server '{server_config.server_id}'.")
97
+
98
+ for remote_tool in remote_tools:
89
99
  try:
90
- validated_config = self._config_service.load_config(mcp_config)
91
- except ValueError as e:
92
- logger.error(f"Failed to parse provided MCP config dictionary: {e}")
93
- raise
94
- elif isinstance(mcp_config, BaseMcpConfig):
95
- validated_config = self._config_service.add_config(mcp_config)
96
- else:
97
- raise TypeError(f"mcp_config must be a BaseMcpConfig object or a dictionary, not {type(mcp_config)}.")
98
-
99
- logger.info(f"Starting targeted MCP tool discovery for server: {validated_config.server_id}")
100
- self.unregister_tools_from_server(validated_config.server_id)
101
- configs_to_process = [validated_config]
102
- else:
103
- logger.info("Starting full MCP tool discovery. Unregistering all existing MCP tools first.")
104
- all_server_ids = list(self._registered_tools_by_server.keys())
105
- for server_id in all_server_ids:
106
- self.unregister_tools_from_server(server_id)
107
- self._registered_tools_by_server.clear()
108
- configs_to_process = self._config_service.get_all_configs()
100
+ tool_def = self._create_tool_definition_from_remote(remote_tool, server_config, schema_mapper)
101
+ self._tool_registry.register_tool(tool_def)
102
+ self._registered_tools_by_server.setdefault(server_config.server_id, []).append(tool_def)
103
+ registered_tool_definitions.append(tool_def)
104
+ except Exception as e_tool:
105
+ logger.error(f"Failed to process or register remote tool '{remote_tool.name}': {e_tool}", exc_info=True)
106
+
107
+ except Exception as e_server:
108
+ logger.error(f"Failed to discover tools from MCP server '{server_config.server_id}': {e_server}", exc_info=True)
109
+ # Re-raise to signal failure to the caller
110
+ raise
111
+
112
+ return registered_tool_definitions
113
+
114
+ async def register_server(self, config_object: BaseMcpConfig) -> List[ToolDefinition]:
115
+ """
116
+ Discovers and registers tools from a single MCP server using a validated
117
+ config object. This will overwrite any existing tools from that server.
118
+
119
+ Args:
120
+ config_object: A pre-instantiated and validated BaseMcpConfig object.
121
+
122
+ Returns:
123
+ A list of the successfully registered ToolDefinition objects from this server.
124
+ """
125
+ if not isinstance(config_object, BaseMcpConfig):
126
+ raise TypeError(f"config_object must be a BaseMcpConfig object, not {type(config_object)}.")
127
+
128
+ # Add/update the config in the service
129
+ self._config_service.add_config(config_object)
130
+
131
+ logger.info(f"Starting targeted MCP tool registration for server: {config_object.server_id}")
132
+
133
+ # Unregister existing tools for this specific server before re-registering
134
+ self.unregister_tools_from_server(config_object.server_id)
135
+
136
+ schema_mapper = McpSchemaMapper()
137
+
138
+ return await self._discover_and_register_from_config(config_object, schema_mapper)
139
+
140
+ async def load_and_register_server(self, config_dict: Dict[str, Any]) -> List[ToolDefinition]:
141
+ """
142
+ Loads a server configuration from a dictionary, then discovers and registers its tools.
143
+ This is a convenience method that wraps the parsing and registration process.
144
+
145
+ Args:
146
+ config_dict: The raw dictionary configuration for a single MCP server.
147
+
148
+ Returns:
149
+ A list of the successfully registered ToolDefinition objects from this server.
150
+ """
151
+ logger.debug(f"Attempting to load and register server from dictionary: {config_dict.get(next(iter(config_dict), 'unknown'))}")
152
+ try:
153
+ validated_config = self._config_service.load_config_from_dict(config_dict)
154
+ except ValueError as e:
155
+ logger.error(f"Failed to parse provided MCP config dictionary: {e}")
156
+ raise
157
+
158
+ return await self.register_server(validated_config)
159
+
160
+ async def reload_all_mcp_tools(self) -> List[ToolDefinition]:
161
+ """
162
+ Performs a full refresh of tools from ALL MCP servers currently configured
163
+ in the McpConfigService. This first unregisters all previously registered
164
+ MCP tools, then re-discovers and re-registers them. This process is resilient
165
+ to failures from individual servers.
109
166
 
167
+ Returns:
168
+ A list of all successfully registered ToolDefinition objects.
169
+ """
170
+ logger.info("Reloading all MCP tools. Unregistering existing MCP tools first.")
171
+
172
+ # Unregister all previously known MCP tools
173
+ all_server_ids = list(self._registered_tools_by_server.keys())
174
+ for server_id in all_server_ids:
175
+ self.unregister_tools_from_server(server_id)
176
+
177
+ configs_to_process = self._config_service.get_all_configs()
110
178
  if not configs_to_process:
111
- logger.info("No MCP server configurations to process. Skipping discovery.")
179
+ logger.info("No MCP server configurations to process. Skipping reload.")
112
180
  return []
113
181
 
114
182
  schema_mapper = McpSchemaMapper()
115
- registered_tool_definitions: List[ToolDefinition] = []
183
+ all_registered_definitions: List[ToolDefinition] = []
184
+
116
185
  for server_config in configs_to_process:
117
- if not server_config.enabled:
118
- logger.info(f"MCP server '{server_config.server_id}' is disabled. Skipping.")
119
- continue
120
-
121
- logger.info(f"Discovering tools from MCP server: '{server_config.server_id}'")
122
-
123
186
  try:
124
- remote_tools = await self._fetch_tools_from_server(server_config)
125
- logger.info(f"Discovered {len(remote_tools)} tools from server '{server_config.server_id}'.")
126
-
127
- for remote_tool in remote_tools:
128
- try:
129
- tool_def = self._create_tool_definition_from_remote(remote_tool, server_config, schema_mapper)
130
- self._tool_registry.register_tool(tool_def)
131
- self._registered_tools_by_server.setdefault(server_config.server_id, []).append(tool_def)
132
- registered_tool_definitions.append(tool_def)
133
- except Exception as e_tool:
134
- logger.error(f"Failed to process or register remote tool '{remote_tool.name}': {e_tool}", exc_info=True)
135
-
136
- except Exception as e_server:
137
- logger.error(f"Failed to discover tools from MCP server '{server_config.server_id}': {e_server}", exc_info=True)
138
-
139
- logger.info(f"MCP tool discovery and registration process completed. Total tools registered: {len(registered_tool_definitions)}.")
140
- return registered_tool_definitions
187
+ newly_registered = await self._discover_and_register_from_config(server_config, schema_mapper)
188
+ all_registered_definitions.extend(newly_registered)
189
+ except Exception as e:
190
+ # Log the error but continue to the next server. This makes the process resilient.
191
+ # exc_info is False because the inner method already logged the full stack trace.
192
+ logger.error(f"Failed to complete discovery for server '{server_config.server_id}', it will be skipped. Error: {e}", exc_info=False)
193
+
194
+ logger.info(f"Finished reloading all MCP tools. Total tools registered: {len(all_registered_definitions)}.")
195
+ return all_registered_definitions
141
196
 
142
197
  async def list_remote_tools(self, mcp_config: Union[BaseMcpConfig, Dict[str, Any]]) -> List[ToolDefinition]:
143
198
  validated_config: BaseMcpConfig