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.
- autobyteus/agent/agent.py +1 -1
- autobyteus/agent/bootstrap_steps/system_prompt_processing_step.py +4 -2
- autobyteus/agent/context/__init__.py +4 -2
- autobyteus/agent/context/agent_config.py +35 -8
- autobyteus/agent/context/agent_context_registry.py +73 -0
- autobyteus/agent/events/notifiers.py +4 -0
- autobyteus/agent/events/worker_event_dispatcher.py +1 -2
- autobyteus/agent/handlers/inter_agent_message_event_handler.py +8 -3
- autobyteus/agent/handlers/llm_complete_response_received_event_handler.py +19 -19
- autobyteus/agent/handlers/llm_user_message_ready_event_handler.py +2 -2
- autobyteus/agent/handlers/tool_result_event_handler.py +48 -20
- autobyteus/agent/handlers/user_input_message_event_handler.py +16 -1
- autobyteus/agent/input_processor/__init__.py +1 -7
- autobyteus/agent/message/context_file_type.py +6 -0
- autobyteus/agent/message/send_message_to.py +74 -99
- autobyteus/agent/phases/discover.py +2 -1
- autobyteus/agent/runtime/agent_runtime.py +10 -2
- autobyteus/agent/runtime/agent_worker.py +1 -0
- autobyteus/agent/sender_type.py +15 -0
- autobyteus/agent/streaming/agent_event_stream.py +6 -0
- autobyteus/agent/streaming/stream_event_payloads.py +12 -0
- autobyteus/agent/streaming/stream_events.py +3 -0
- autobyteus/agent/system_prompt_processor/tool_manifest_injector_processor.py +7 -4
- autobyteus/agent/tool_execution_result_processor/__init__.py +9 -0
- autobyteus/agent/tool_execution_result_processor/base_processor.py +46 -0
- autobyteus/agent/tool_execution_result_processor/processor_definition.py +36 -0
- autobyteus/agent/tool_execution_result_processor/processor_meta.py +36 -0
- autobyteus/agent/tool_execution_result_processor/processor_registry.py +70 -0
- autobyteus/agent/workspace/base_workspace.py +17 -2
- autobyteus/agent_team/__init__.py +1 -0
- autobyteus/agent_team/agent_team.py +93 -0
- autobyteus/agent_team/agent_team_builder.py +184 -0
- autobyteus/agent_team/base_agent_team.py +86 -0
- autobyteus/agent_team/bootstrap_steps/__init__.py +24 -0
- autobyteus/agent_team/bootstrap_steps/agent_configuration_preparation_step.py +73 -0
- autobyteus/agent_team/bootstrap_steps/agent_team_bootstrapper.py +54 -0
- autobyteus/agent_team/bootstrap_steps/agent_team_runtime_queue_initialization_step.py +25 -0
- autobyteus/agent_team/bootstrap_steps/base_agent_team_bootstrap_step.py +23 -0
- autobyteus/agent_team/bootstrap_steps/coordinator_initialization_step.py +41 -0
- autobyteus/agent_team/bootstrap_steps/coordinator_prompt_preparation_step.py +85 -0
- autobyteus/agent_team/bootstrap_steps/task_notifier_initialization_step.py +51 -0
- autobyteus/agent_team/bootstrap_steps/team_context_initialization_step.py +45 -0
- autobyteus/agent_team/context/__init__.py +17 -0
- autobyteus/agent_team/context/agent_team_config.py +33 -0
- autobyteus/agent_team/context/agent_team_context.py +61 -0
- autobyteus/agent_team/context/agent_team_runtime_state.py +56 -0
- autobyteus/agent_team/context/team_manager.py +147 -0
- autobyteus/agent_team/context/team_node_config.py +76 -0
- autobyteus/agent_team/events/__init__.py +29 -0
- autobyteus/agent_team/events/agent_team_event_dispatcher.py +39 -0
- autobyteus/agent_team/events/agent_team_events.py +53 -0
- autobyteus/agent_team/events/agent_team_input_event_queue_manager.py +21 -0
- autobyteus/agent_team/exceptions.py +8 -0
- autobyteus/agent_team/factory/__init__.py +9 -0
- autobyteus/agent_team/factory/agent_team_factory.py +99 -0
- autobyteus/agent_team/handlers/__init__.py +19 -0
- autobyteus/agent_team/handlers/agent_team_event_handler_registry.py +23 -0
- autobyteus/agent_team/handlers/base_agent_team_event_handler.py +16 -0
- autobyteus/agent_team/handlers/inter_agent_message_request_event_handler.py +61 -0
- autobyteus/agent_team/handlers/lifecycle_agent_team_event_handler.py +27 -0
- autobyteus/agent_team/handlers/process_user_message_event_handler.py +46 -0
- autobyteus/agent_team/handlers/tool_approval_team_event_handler.py +48 -0
- autobyteus/agent_team/phases/__init__.py +11 -0
- autobyteus/agent_team/phases/agent_team_operational_phase.py +19 -0
- autobyteus/agent_team/phases/agent_team_phase_manager.py +48 -0
- autobyteus/agent_team/runtime/__init__.py +13 -0
- autobyteus/agent_team/runtime/agent_team_runtime.py +82 -0
- autobyteus/agent_team/runtime/agent_team_worker.py +117 -0
- autobyteus/agent_team/shutdown_steps/__init__.py +17 -0
- autobyteus/agent_team/shutdown_steps/agent_team_shutdown_orchestrator.py +35 -0
- autobyteus/agent_team/shutdown_steps/agent_team_shutdown_step.py +42 -0
- autobyteus/agent_team/shutdown_steps/base_agent_team_shutdown_step.py +16 -0
- autobyteus/agent_team/shutdown_steps/bridge_cleanup_step.py +28 -0
- autobyteus/agent_team/shutdown_steps/sub_team_shutdown_step.py +41 -0
- autobyteus/agent_team/streaming/__init__.py +26 -0
- autobyteus/agent_team/streaming/agent_event_bridge.py +48 -0
- autobyteus/agent_team/streaming/agent_event_multiplexer.py +70 -0
- autobyteus/agent_team/streaming/agent_team_event_notifier.py +64 -0
- autobyteus/agent_team/streaming/agent_team_event_stream.py +33 -0
- autobyteus/agent_team/streaming/agent_team_stream_event_payloads.py +32 -0
- autobyteus/agent_team/streaming/agent_team_stream_events.py +56 -0
- autobyteus/agent_team/streaming/team_event_bridge.py +50 -0
- autobyteus/agent_team/task_notification/__init__.py +11 -0
- autobyteus/agent_team/task_notification/system_event_driven_agent_task_notifier.py +164 -0
- autobyteus/agent_team/task_notification/task_notification_mode.py +24 -0
- autobyteus/agent_team/utils/__init__.py +9 -0
- autobyteus/agent_team/utils/wait_for_idle.py +46 -0
- autobyteus/cli/__init__.py +1 -1
- autobyteus/cli/agent_team_tui/__init__.py +4 -0
- autobyteus/cli/agent_team_tui/app.py +210 -0
- autobyteus/cli/agent_team_tui/state.py +180 -0
- autobyteus/cli/agent_team_tui/widgets/__init__.py +6 -0
- autobyteus/cli/agent_team_tui/widgets/agent_list_sidebar.py +149 -0
- autobyteus/cli/agent_team_tui/widgets/focus_pane.py +320 -0
- autobyteus/cli/agent_team_tui/widgets/logo.py +20 -0
- autobyteus/cli/agent_team_tui/widgets/renderables.py +77 -0
- autobyteus/cli/agent_team_tui/widgets/shared.py +60 -0
- autobyteus/cli/agent_team_tui/widgets/status_bar.py +14 -0
- autobyteus/cli/agent_team_tui/widgets/task_board_panel.py +82 -0
- autobyteus/cli/cli_display.py +1 -1
- autobyteus/cli/workflow_tui/__init__.py +4 -0
- autobyteus/cli/workflow_tui/app.py +210 -0
- autobyteus/cli/workflow_tui/state.py +189 -0
- autobyteus/cli/workflow_tui/widgets/__init__.py +6 -0
- autobyteus/cli/workflow_tui/widgets/agent_list_sidebar.py +149 -0
- autobyteus/cli/workflow_tui/widgets/focus_pane.py +335 -0
- autobyteus/cli/workflow_tui/widgets/logo.py +27 -0
- autobyteus/cli/workflow_tui/widgets/renderables.py +70 -0
- autobyteus/cli/workflow_tui/widgets/shared.py +51 -0
- autobyteus/cli/workflow_tui/widgets/status_bar.py +14 -0
- autobyteus/events/event_types.py +8 -0
- autobyteus/llm/api/autobyteus_llm.py +11 -12
- autobyteus/llm/api/lmstudio_llm.py +34 -0
- autobyteus/llm/api/ollama_llm.py +8 -13
- autobyteus/llm/api/openai_compatible_llm.py +20 -3
- autobyteus/llm/autobyteus_provider.py +73 -46
- autobyteus/llm/llm_factory.py +103 -139
- autobyteus/llm/lmstudio_provider.py +104 -0
- autobyteus/llm/models.py +83 -53
- autobyteus/llm/ollama_provider.py +69 -61
- autobyteus/llm/ollama_provider_resolver.py +1 -0
- autobyteus/llm/providers.py +13 -12
- autobyteus/llm/runtimes.py +11 -0
- autobyteus/llm/token_counter/token_counter_factory.py +2 -0
- autobyteus/task_management/__init__.py +43 -0
- autobyteus/task_management/base_task_board.py +68 -0
- autobyteus/task_management/converters/__init__.py +11 -0
- autobyteus/task_management/converters/task_board_converter.py +64 -0
- autobyteus/task_management/converters/task_plan_converter.py +48 -0
- autobyteus/task_management/deliverable.py +16 -0
- autobyteus/task_management/deliverables/__init__.py +8 -0
- autobyteus/task_management/deliverables/file_deliverable.py +15 -0
- autobyteus/task_management/events.py +27 -0
- autobyteus/task_management/in_memory_task_board.py +126 -0
- autobyteus/task_management/schemas/__init__.py +15 -0
- autobyteus/task_management/schemas/deliverable_schema.py +13 -0
- autobyteus/task_management/schemas/plan_definition.py +35 -0
- autobyteus/task_management/schemas/task_status_report.py +27 -0
- autobyteus/task_management/task_plan.py +110 -0
- autobyteus/task_management/tools/__init__.py +14 -0
- autobyteus/task_management/tools/get_task_board_status.py +68 -0
- autobyteus/task_management/tools/publish_task_plan.py +113 -0
- autobyteus/task_management/tools/update_task_status.py +135 -0
- autobyteus/tools/__init__.py +2 -0
- autobyteus/tools/ask_user_input.py +2 -1
- autobyteus/tools/bash/bash_executor.py +61 -15
- autobyteus/tools/browser/session_aware/browser_session_aware_navigate_to.py +2 -0
- autobyteus/tools/browser/session_aware/browser_session_aware_web_element_trigger.py +3 -0
- autobyteus/tools/browser/session_aware/browser_session_aware_webpage_reader.py +3 -0
- autobyteus/tools/browser/session_aware/browser_session_aware_webpage_screenshot_taker.py +3 -0
- autobyteus/tools/browser/standalone/google_search_ui.py +2 -0
- autobyteus/tools/browser/standalone/navigate_to.py +2 -0
- autobyteus/tools/browser/standalone/web_page_pdf_generator.py +3 -0
- autobyteus/tools/browser/standalone/webpage_image_downloader.py +3 -0
- autobyteus/tools/browser/standalone/webpage_reader.py +2 -0
- autobyteus/tools/browser/standalone/webpage_screenshot_taker.py +3 -0
- autobyteus/tools/file/file_reader.py +36 -9
- autobyteus/tools/file/file_writer.py +37 -9
- autobyteus/tools/functional_tool.py +5 -4
- autobyteus/tools/image_downloader.py +2 -0
- autobyteus/tools/mcp/config_service.py +63 -58
- autobyteus/tools/mcp/server/http_managed_mcp_server.py +14 -2
- autobyteus/tools/mcp/server/stdio_managed_mcp_server.py +14 -2
- autobyteus/tools/mcp/server_instance_manager.py +30 -4
- autobyteus/tools/mcp/tool_registrar.py +106 -51
- autobyteus/tools/parameter_schema.py +17 -11
- autobyteus/tools/pdf_downloader.py +2 -1
- autobyteus/tools/registry/tool_definition.py +36 -37
- autobyteus/tools/registry/tool_registry.py +50 -2
- autobyteus/tools/timer.py +2 -0
- autobyteus/tools/tool_category.py +15 -4
- autobyteus/tools/tool_meta.py +6 -1
- autobyteus/tools/tool_origin.py +10 -0
- autobyteus/tools/usage/formatters/default_json_example_formatter.py +78 -3
- autobyteus/tools/usage/formatters/default_xml_example_formatter.py +23 -3
- autobyteus/tools/usage/formatters/gemini_json_example_formatter.py +6 -0
- autobyteus/tools/usage/formatters/google_json_example_formatter.py +7 -0
- autobyteus/tools/usage/formatters/openai_json_example_formatter.py +6 -4
- autobyteus/tools/usage/parsers/gemini_json_tool_usage_parser.py +23 -7
- autobyteus/tools/usage/parsers/provider_aware_tool_usage_parser.py +14 -25
- autobyteus/tools/usage/providers/__init__.py +2 -12
- autobyteus/tools/usage/providers/tool_manifest_provider.py +36 -29
- autobyteus/tools/usage/registries/__init__.py +7 -12
- autobyteus/tools/usage/registries/tool_formatter_pair.py +15 -0
- autobyteus/tools/usage/registries/tool_formatting_registry.py +58 -0
- autobyteus/tools/usage/registries/tool_usage_parser_registry.py +55 -0
- autobyteus/workflow/agentic_workflow.py +93 -0
- autobyteus/{agent/workflow → workflow}/base_agentic_workflow.py +19 -27
- autobyteus/workflow/bootstrap_steps/__init__.py +20 -0
- autobyteus/workflow/bootstrap_steps/agent_tool_injection_step.py +34 -0
- autobyteus/workflow/bootstrap_steps/base_workflow_bootstrap_step.py +23 -0
- autobyteus/workflow/bootstrap_steps/coordinator_initialization_step.py +41 -0
- autobyteus/workflow/bootstrap_steps/coordinator_prompt_preparation_step.py +108 -0
- autobyteus/workflow/bootstrap_steps/workflow_bootstrapper.py +50 -0
- autobyteus/workflow/bootstrap_steps/workflow_runtime_queue_initialization_step.py +25 -0
- autobyteus/workflow/context/__init__.py +17 -0
- autobyteus/workflow/context/team_manager.py +147 -0
- autobyteus/workflow/context/workflow_config.py +30 -0
- autobyteus/workflow/context/workflow_context.py +61 -0
- autobyteus/workflow/context/workflow_node_config.py +76 -0
- autobyteus/workflow/context/workflow_runtime_state.py +53 -0
- autobyteus/workflow/events/__init__.py +29 -0
- autobyteus/workflow/events/workflow_event_dispatcher.py +39 -0
- autobyteus/workflow/events/workflow_events.py +53 -0
- autobyteus/workflow/events/workflow_input_event_queue_manager.py +21 -0
- autobyteus/workflow/exceptions.py +8 -0
- autobyteus/workflow/factory/__init__.py +9 -0
- autobyteus/workflow/factory/workflow_factory.py +99 -0
- autobyteus/workflow/handlers/__init__.py +19 -0
- autobyteus/workflow/handlers/base_workflow_event_handler.py +16 -0
- autobyteus/workflow/handlers/inter_agent_message_request_event_handler.py +61 -0
- autobyteus/workflow/handlers/lifecycle_workflow_event_handler.py +27 -0
- autobyteus/workflow/handlers/process_user_message_event_handler.py +46 -0
- autobyteus/workflow/handlers/tool_approval_workflow_event_handler.py +39 -0
- autobyteus/workflow/handlers/workflow_event_handler_registry.py +23 -0
- autobyteus/workflow/phases/__init__.py +11 -0
- autobyteus/workflow/phases/workflow_operational_phase.py +19 -0
- autobyteus/workflow/phases/workflow_phase_manager.py +48 -0
- autobyteus/workflow/runtime/__init__.py +13 -0
- autobyteus/workflow/runtime/workflow_runtime.py +82 -0
- autobyteus/workflow/runtime/workflow_worker.py +117 -0
- autobyteus/workflow/shutdown_steps/__init__.py +17 -0
- autobyteus/workflow/shutdown_steps/agent_team_shutdown_step.py +42 -0
- autobyteus/workflow/shutdown_steps/base_workflow_shutdown_step.py +16 -0
- autobyteus/workflow/shutdown_steps/bridge_cleanup_step.py +28 -0
- autobyteus/workflow/shutdown_steps/sub_workflow_shutdown_step.py +41 -0
- autobyteus/workflow/shutdown_steps/workflow_shutdown_orchestrator.py +35 -0
- autobyteus/workflow/streaming/__init__.py +26 -0
- autobyteus/workflow/streaming/agent_event_bridge.py +48 -0
- autobyteus/workflow/streaming/agent_event_multiplexer.py +70 -0
- autobyteus/workflow/streaming/workflow_event_bridge.py +50 -0
- autobyteus/workflow/streaming/workflow_event_notifier.py +83 -0
- autobyteus/workflow/streaming/workflow_event_stream.py +33 -0
- autobyteus/workflow/streaming/workflow_stream_event_payloads.py +28 -0
- autobyteus/workflow/streaming/workflow_stream_events.py +45 -0
- autobyteus/workflow/utils/__init__.py +9 -0
- autobyteus/workflow/utils/wait_for_idle.py +46 -0
- autobyteus/workflow/workflow_builder.py +151 -0
- {autobyteus-1.1.3.dist-info → autobyteus-1.1.5.dist-info}/METADATA +16 -14
- autobyteus-1.1.5.dist-info/RECORD +455 -0
- {autobyteus-1.1.3.dist-info → autobyteus-1.1.5.dist-info}/top_level.txt +1 -0
- examples/__init__.py +1 -0
- examples/agent_team/__init__.py +1 -0
- examples/discover_phase_transitions.py +104 -0
- examples/run_browser_agent.py +262 -0
- examples/run_google_slides_agent.py +287 -0
- examples/run_mcp_browser_client.py +174 -0
- examples/run_mcp_google_slides_client.py +270 -0
- examples/run_mcp_list_tools.py +189 -0
- examples/run_poem_writer.py +284 -0
- examples/run_sqlite_agent.py +295 -0
- autobyteus/agent/context/agent_phase_manager.py +0 -264
- autobyteus/agent/context/phases.py +0 -49
- autobyteus/agent/group/__init__.py +0 -0
- autobyteus/agent/group/agent_group.py +0 -164
- autobyteus/agent/group/agent_group_context.py +0 -81
- autobyteus/agent/input_processor/content_prefixing_input_processor.py +0 -41
- autobyteus/agent/input_processor/metadata_appending_input_processor.py +0 -34
- autobyteus/agent/input_processor/passthrough_input_processor.py +0 -33
- autobyteus/agent/workflow/__init__.py +0 -11
- autobyteus/agent/workflow/agentic_workflow.py +0 -89
- autobyteus/tools/mcp/call_handlers/__init__.py +0 -16
- autobyteus/tools/mcp/call_handlers/base_handler.py +0 -40
- autobyteus/tools/mcp/call_handlers/stdio_handler.py +0 -76
- autobyteus/tools/mcp/call_handlers/streamable_http_handler.py +0 -55
- autobyteus/tools/mcp/registrar.py +0 -202
- autobyteus/tools/usage/providers/json_example_provider.py +0 -32
- autobyteus/tools/usage/providers/json_schema_provider.py +0 -35
- autobyteus/tools/usage/providers/json_tool_usage_parser_provider.py +0 -28
- autobyteus/tools/usage/providers/xml_example_provider.py +0 -28
- autobyteus/tools/usage/providers/xml_schema_provider.py +0 -29
- autobyteus/tools/usage/providers/xml_tool_usage_parser_provider.py +0 -26
- autobyteus/tools/usage/registries/json_example_formatter_registry.py +0 -51
- autobyteus/tools/usage/registries/json_schema_formatter_registry.py +0 -51
- autobyteus/tools/usage/registries/json_tool_usage_parser_registry.py +0 -42
- autobyteus/tools/usage/registries/xml_example_formatter_registry.py +0 -30
- autobyteus/tools/usage/registries/xml_schema_formatter_registry.py +0 -33
- autobyteus/tools/usage/registries/xml_tool_usage_parser_registry.py +0 -30
- autobyteus/workflow/simple_task.py +0 -98
- autobyteus/workflow/task.py +0 -147
- autobyteus/workflow/workflow.py +0 -49
- autobyteus-1.1.3.dist-info/RECORD +0 -312
- {autobyteus-1.1.3.dist-info → autobyteus-1.1.5.dist-info}/WHEEL +0 -0
- {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
|
-
|
|
24
|
-
|
|
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
|
-
|
|
51
|
+
|
|
52
|
+
with open(final_path, 'w', encoding='utf-8') as file:
|
|
27
53
|
file.write(content)
|
|
28
|
-
|
|
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 {
|
|
31
|
-
raise IOError(f"Could not write file at {
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
153
|
-
if not
|
|
154
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
except
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
82
|
+
async def _discover_and_register_from_config(self, server_config: BaseMcpConfig, schema_mapper: McpSchemaMapper) -> List[ToolDefinition]:
|
|
81
83
|
"""
|
|
82
|
-
|
|
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
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
|
179
|
+
logger.info("No MCP server configurations to process. Skipping reload.")
|
|
112
180
|
return []
|
|
113
181
|
|
|
114
182
|
schema_mapper = McpSchemaMapper()
|
|
115
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|