autobyteus 1.1.2__py3-none-any.whl → 1.1.4__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 (171) hide show
  1. autobyteus/agent/agent.py +1 -1
  2. autobyteus/agent/bootstrap_steps/__init__.py +2 -0
  3. autobyteus/agent/bootstrap_steps/agent_bootstrapper.py +2 -0
  4. autobyteus/agent/bootstrap_steps/mcp_server_prewarming_step.py +71 -0
  5. autobyteus/agent/bootstrap_steps/system_prompt_processing_step.py +4 -2
  6. autobyteus/agent/context/agent_config.py +36 -5
  7. autobyteus/agent/events/worker_event_dispatcher.py +1 -2
  8. autobyteus/agent/handlers/inter_agent_message_event_handler.py +1 -1
  9. autobyteus/agent/handlers/llm_user_message_ready_event_handler.py +2 -2
  10. autobyteus/agent/handlers/tool_result_event_handler.py +48 -20
  11. autobyteus/agent/handlers/user_input_message_event_handler.py +1 -1
  12. autobyteus/agent/input_processor/__init__.py +1 -7
  13. autobyteus/agent/llm_response_processor/provider_aware_tool_usage_processor.py +41 -12
  14. autobyteus/agent/message/context_file_type.py +6 -0
  15. autobyteus/agent/message/send_message_to.py +68 -99
  16. autobyteus/agent/phases/discover.py +2 -1
  17. autobyteus/agent/runtime/agent_worker.py +25 -34
  18. autobyteus/agent/shutdown_steps/__init__.py +17 -0
  19. autobyteus/agent/shutdown_steps/agent_shutdown_orchestrator.py +63 -0
  20. autobyteus/agent/shutdown_steps/base_shutdown_step.py +33 -0
  21. autobyteus/agent/shutdown_steps/llm_instance_cleanup_step.py +45 -0
  22. autobyteus/agent/shutdown_steps/mcp_server_cleanup_step.py +32 -0
  23. autobyteus/agent/tool_execution_result_processor/__init__.py +9 -0
  24. autobyteus/agent/tool_execution_result_processor/base_processor.py +46 -0
  25. autobyteus/agent/tool_execution_result_processor/processor_definition.py +36 -0
  26. autobyteus/agent/tool_execution_result_processor/processor_meta.py +36 -0
  27. autobyteus/agent/tool_execution_result_processor/processor_registry.py +70 -0
  28. autobyteus/agent/workspace/base_workspace.py +17 -2
  29. autobyteus/cli/__init__.py +1 -1
  30. autobyteus/cli/cli_display.py +1 -1
  31. autobyteus/cli/workflow_tui/__init__.py +4 -0
  32. autobyteus/cli/workflow_tui/app.py +210 -0
  33. autobyteus/cli/workflow_tui/state.py +189 -0
  34. autobyteus/cli/workflow_tui/widgets/__init__.py +6 -0
  35. autobyteus/cli/workflow_tui/widgets/agent_list_sidebar.py +149 -0
  36. autobyteus/cli/workflow_tui/widgets/focus_pane.py +335 -0
  37. autobyteus/cli/workflow_tui/widgets/logo.py +27 -0
  38. autobyteus/cli/workflow_tui/widgets/renderables.py +70 -0
  39. autobyteus/cli/workflow_tui/widgets/shared.py +51 -0
  40. autobyteus/cli/workflow_tui/widgets/status_bar.py +14 -0
  41. autobyteus/events/event_types.py +3 -0
  42. autobyteus/llm/api/lmstudio_llm.py +37 -0
  43. autobyteus/llm/api/openai_compatible_llm.py +20 -3
  44. autobyteus/llm/llm_factory.py +2 -0
  45. autobyteus/llm/lmstudio_provider.py +89 -0
  46. autobyteus/llm/providers.py +1 -0
  47. autobyteus/llm/token_counter/token_counter_factory.py +2 -0
  48. autobyteus/tools/__init__.py +2 -0
  49. autobyteus/tools/ask_user_input.py +2 -1
  50. autobyteus/tools/base_tool.py +2 -0
  51. autobyteus/tools/bash/bash_executor.py +2 -1
  52. autobyteus/tools/browser/session_aware/browser_session_aware_navigate_to.py +2 -0
  53. autobyteus/tools/browser/session_aware/browser_session_aware_web_element_trigger.py +3 -0
  54. autobyteus/tools/browser/session_aware/browser_session_aware_webpage_reader.py +3 -0
  55. autobyteus/tools/browser/session_aware/browser_session_aware_webpage_screenshot_taker.py +3 -0
  56. autobyteus/tools/browser/standalone/google_search_ui.py +2 -0
  57. autobyteus/tools/browser/standalone/navigate_to.py +2 -0
  58. autobyteus/tools/browser/standalone/web_page_pdf_generator.py +3 -0
  59. autobyteus/tools/browser/standalone/webpage_image_downloader.py +3 -0
  60. autobyteus/tools/browser/standalone/webpage_reader.py +2 -0
  61. autobyteus/tools/browser/standalone/webpage_screenshot_taker.py +3 -0
  62. autobyteus/tools/file/file_reader.py +36 -9
  63. autobyteus/tools/file/file_writer.py +37 -9
  64. autobyteus/tools/functional_tool.py +5 -4
  65. autobyteus/tools/image_downloader.py +2 -0
  66. autobyteus/tools/mcp/__init__.py +10 -7
  67. autobyteus/tools/mcp/call_handlers/__init__.py +0 -2
  68. autobyteus/tools/mcp/config_service.py +1 -6
  69. autobyteus/tools/mcp/factory.py +12 -26
  70. autobyteus/tools/mcp/server/__init__.py +16 -0
  71. autobyteus/tools/mcp/server/base_managed_mcp_server.py +139 -0
  72. autobyteus/tools/mcp/server/http_managed_mcp_server.py +29 -0
  73. autobyteus/tools/mcp/server/proxy.py +36 -0
  74. autobyteus/tools/mcp/server/stdio_managed_mcp_server.py +33 -0
  75. autobyteus/tools/mcp/server_instance_manager.py +93 -0
  76. autobyteus/tools/mcp/tool.py +28 -46
  77. autobyteus/tools/mcp/tool_registrar.py +179 -0
  78. autobyteus/tools/mcp/types.py +10 -21
  79. autobyteus/tools/pdf_downloader.py +2 -1
  80. autobyteus/tools/registry/tool_definition.py +20 -7
  81. autobyteus/tools/registry/tool_registry.py +75 -28
  82. autobyteus/tools/timer.py +2 -0
  83. autobyteus/tools/tool_category.py +14 -4
  84. autobyteus/tools/tool_meta.py +6 -1
  85. autobyteus/tools/tool_origin.py +10 -0
  86. autobyteus/workflow/agentic_workflow.py +93 -0
  87. autobyteus/{agent/workflow → workflow}/base_agentic_workflow.py +19 -27
  88. autobyteus/workflow/bootstrap_steps/__init__.py +20 -0
  89. autobyteus/workflow/bootstrap_steps/agent_tool_injection_step.py +34 -0
  90. autobyteus/workflow/bootstrap_steps/base_workflow_bootstrap_step.py +23 -0
  91. autobyteus/workflow/bootstrap_steps/coordinator_initialization_step.py +41 -0
  92. autobyteus/workflow/bootstrap_steps/coordinator_prompt_preparation_step.py +108 -0
  93. autobyteus/workflow/bootstrap_steps/workflow_bootstrapper.py +50 -0
  94. autobyteus/workflow/bootstrap_steps/workflow_runtime_queue_initialization_step.py +25 -0
  95. autobyteus/workflow/context/__init__.py +17 -0
  96. autobyteus/workflow/context/team_manager.py +147 -0
  97. autobyteus/workflow/context/workflow_config.py +30 -0
  98. autobyteus/workflow/context/workflow_context.py +61 -0
  99. autobyteus/workflow/context/workflow_node_config.py +76 -0
  100. autobyteus/workflow/context/workflow_runtime_state.py +53 -0
  101. autobyteus/workflow/events/__init__.py +29 -0
  102. autobyteus/workflow/events/workflow_event_dispatcher.py +39 -0
  103. autobyteus/workflow/events/workflow_events.py +53 -0
  104. autobyteus/workflow/events/workflow_input_event_queue_manager.py +21 -0
  105. autobyteus/workflow/exceptions.py +8 -0
  106. autobyteus/workflow/factory/__init__.py +9 -0
  107. autobyteus/workflow/factory/workflow_factory.py +99 -0
  108. autobyteus/workflow/handlers/__init__.py +19 -0
  109. autobyteus/workflow/handlers/base_workflow_event_handler.py +16 -0
  110. autobyteus/workflow/handlers/inter_agent_message_request_event_handler.py +61 -0
  111. autobyteus/workflow/handlers/lifecycle_workflow_event_handler.py +27 -0
  112. autobyteus/workflow/handlers/process_user_message_event_handler.py +46 -0
  113. autobyteus/workflow/handlers/tool_approval_workflow_event_handler.py +39 -0
  114. autobyteus/workflow/handlers/workflow_event_handler_registry.py +23 -0
  115. autobyteus/workflow/phases/__init__.py +11 -0
  116. autobyteus/workflow/phases/workflow_operational_phase.py +19 -0
  117. autobyteus/workflow/phases/workflow_phase_manager.py +48 -0
  118. autobyteus/workflow/runtime/__init__.py +13 -0
  119. autobyteus/workflow/runtime/workflow_runtime.py +82 -0
  120. autobyteus/workflow/runtime/workflow_worker.py +117 -0
  121. autobyteus/workflow/shutdown_steps/__init__.py +17 -0
  122. autobyteus/workflow/shutdown_steps/agent_team_shutdown_step.py +42 -0
  123. autobyteus/workflow/shutdown_steps/base_workflow_shutdown_step.py +16 -0
  124. autobyteus/workflow/shutdown_steps/bridge_cleanup_step.py +28 -0
  125. autobyteus/workflow/shutdown_steps/sub_workflow_shutdown_step.py +41 -0
  126. autobyteus/workflow/shutdown_steps/workflow_shutdown_orchestrator.py +35 -0
  127. autobyteus/workflow/streaming/__init__.py +26 -0
  128. autobyteus/workflow/streaming/agent_event_bridge.py +48 -0
  129. autobyteus/workflow/streaming/agent_event_multiplexer.py +70 -0
  130. autobyteus/workflow/streaming/workflow_event_bridge.py +50 -0
  131. autobyteus/workflow/streaming/workflow_event_notifier.py +83 -0
  132. autobyteus/workflow/streaming/workflow_event_stream.py +33 -0
  133. autobyteus/workflow/streaming/workflow_stream_event_payloads.py +28 -0
  134. autobyteus/workflow/streaming/workflow_stream_events.py +45 -0
  135. autobyteus/workflow/utils/__init__.py +9 -0
  136. autobyteus/workflow/utils/wait_for_idle.py +46 -0
  137. autobyteus/workflow/workflow_builder.py +151 -0
  138. {autobyteus-1.1.2.dist-info → autobyteus-1.1.4.dist-info}/METADATA +16 -13
  139. {autobyteus-1.1.2.dist-info → autobyteus-1.1.4.dist-info}/RECORD +156 -75
  140. {autobyteus-1.1.2.dist-info → autobyteus-1.1.4.dist-info}/top_level.txt +1 -0
  141. examples/__init__.py +1 -0
  142. examples/discover_phase_transitions.py +104 -0
  143. examples/run_browser_agent.py +260 -0
  144. examples/run_google_slides_agent.py +286 -0
  145. examples/run_mcp_browser_client.py +174 -0
  146. examples/run_mcp_google_slides_client.py +270 -0
  147. examples/run_mcp_list_tools.py +189 -0
  148. examples/run_poem_writer.py +274 -0
  149. examples/run_sqlite_agent.py +293 -0
  150. examples/workflow/__init__.py +1 -0
  151. examples/workflow/run_basic_research_workflow.py +189 -0
  152. examples/workflow/run_code_review_workflow.py +269 -0
  153. examples/workflow/run_debate_workflow.py +212 -0
  154. examples/workflow/run_workflow_with_tui.py +153 -0
  155. autobyteus/agent/context/agent_phase_manager.py +0 -264
  156. autobyteus/agent/context/phases.py +0 -49
  157. autobyteus/agent/group/__init__.py +0 -0
  158. autobyteus/agent/group/agent_group.py +0 -164
  159. autobyteus/agent/group/agent_group_context.py +0 -81
  160. autobyteus/agent/input_processor/content_prefixing_input_processor.py +0 -41
  161. autobyteus/agent/input_processor/metadata_appending_input_processor.py +0 -34
  162. autobyteus/agent/input_processor/passthrough_input_processor.py +0 -33
  163. autobyteus/agent/workflow/__init__.py +0 -11
  164. autobyteus/agent/workflow/agentic_workflow.py +0 -89
  165. autobyteus/tools/mcp/call_handlers/sse_handler.py +0 -22
  166. autobyteus/tools/mcp/registrar.py +0 -323
  167. autobyteus/workflow/simple_task.py +0 -98
  168. autobyteus/workflow/task.py +0 -147
  169. autobyteus/workflow/workflow.py +0 -49
  170. {autobyteus-1.1.2.dist-info → autobyteus-1.1.4.dist-info}/WHEEL +0 -0
  171. {autobyteus-1.1.2.dist-info → autobyteus-1.1.4.dist-info}/licenses/LICENSE +0 -0
@@ -2,14 +2,15 @@
2
2
  """
3
3
  This package implements the Model Context Protocol (MCP) integration for AutoByteUs.
4
4
  It allows AutoByteUs to connect to external MCP servers, discover tools,
5
- and register them as standard AutoByteUs tools using a handler-based architecture.
5
+ and register them as standard AutoByteUs tools using a stateful, server-centric
6
+ architecture with per-agent isolation.
6
7
  """
7
8
  import logging
8
9
 
9
10
  logger = logging.getLogger(__name__)
10
11
 
11
12
  # The actual 'mcp' library and its components are expected to be installed
12
- # in the environment and are used by the internal handlers.
13
+ # in the environment and are used by the internal components.
13
14
 
14
15
  logger.info("AutoByteUs MCP integration package initialized. Expects 'mcp' library to be available.")
15
16
 
@@ -17,9 +18,9 @@ logger.info("AutoByteUs MCP integration package initialized. Expects 'mcp' libra
17
18
  from .types import (
18
19
  BaseMcpConfig,
19
20
  StdioMcpServerConfig,
20
- SseMcpServerConfig,
21
21
  StreamableHttpMcpServerConfig,
22
- McpTransportType
22
+ McpTransportType,
23
+ McpServerInstanceKey,
23
24
  )
24
25
  # Import McpConfigService from config_service.py
25
26
  from .config_service import McpConfigService
@@ -28,17 +29,19 @@ from .config_service import McpConfigService
28
29
  from .schema_mapper import McpSchemaMapper
29
30
  from .tool import GenericMcpTool
30
31
  from .factory import McpToolFactory
31
- from .registrar import McpToolRegistrar
32
+ from .tool_registrar import McpToolRegistrar
33
+ from .server_instance_manager import McpServerInstanceManager
32
34
 
33
35
  __all__ = [
34
36
  # Types from types.py
35
37
  "BaseMcpConfig",
36
38
  "StdioMcpServerConfig",
37
- "SseMcpServerConfig",
38
39
  "StreamableHttpMcpServerConfig",
39
40
  "McpTransportType",
40
- # Service from config_service.py
41
+ "McpServerInstanceKey",
42
+ # Services and Managers
41
43
  "McpConfigService",
44
+ "McpServerInstanceManager",
42
45
  # Other public components
43
46
  "McpSchemaMapper",
44
47
  "GenericMcpTool",
@@ -8,11 +8,9 @@ for a specific transport protocol (e.g., STDIO, Streamable HTTP).
8
8
  from .base_handler import McpCallHandler
9
9
  from .stdio_handler import StdioMcpCallHandler
10
10
  from .streamable_http_handler import StreamableHttpMcpCallHandler
11
- from .sse_handler import SseMcpCallHandler
12
11
 
13
12
  __all__ = [
14
13
  "McpCallHandler",
15
14
  "StdioMcpCallHandler",
16
15
  "StreamableHttpMcpCallHandler",
17
- "SseMcpCallHandler",
18
16
  ]
@@ -8,7 +8,6 @@ from typing import List, Dict, Any, Optional, Union, Type
8
8
  from .types import (
9
9
  BaseMcpConfig,
10
10
  StdioMcpServerConfig,
11
- SseMcpServerConfig,
12
11
  StreamableHttpMcpServerConfig,
13
12
  McpTransportType
14
13
  )
@@ -38,7 +37,7 @@ class McpConfigService(metaclass=SingletonMeta):
38
37
  @staticmethod
39
38
  def _create_specific_config(server_id: str, transport_type: McpTransportType, config_data: Dict[str, Any]) -> BaseMcpConfig:
40
39
  """
41
- Creates a specific McpServerConfig (Stdio, Sse, StreamableHttp) based on transport_type.
40
+ Creates a specific McpServerConfig (Stdio, StreamableHttp) based on transport_type.
42
41
  The 'server_id' is injected.
43
42
  Parameters from nested structures like 'stdio_params' are un-nested.
44
43
  """
@@ -50,7 +49,6 @@ class McpConfigService(metaclass=SingletonMeta):
50
49
 
51
50
  transport_specific_params_key_map = {
52
51
  McpTransportType.STDIO: "stdio_params",
53
- McpTransportType.SSE: "sse_params",
54
52
  McpTransportType.STREAMABLE_HTTP: "streamable_http_params"
55
53
  }
56
54
 
@@ -62,7 +60,6 @@ class McpConfigService(metaclass=SingletonMeta):
62
60
  constructor_params.update(specific_params_dict)
63
61
 
64
62
  constructor_params.pop(transport_specific_params_key_map.get(McpTransportType.STDIO), None)
65
- constructor_params.pop(transport_specific_params_key_map.get(McpTransportType.SSE), None)
66
63
  constructor_params.pop(transport_specific_params_key_map.get(McpTransportType.STREAMABLE_HTTP), None)
67
64
  constructor_params.pop('transport_type', None)
68
65
 
@@ -75,8 +72,6 @@ class McpConfigService(metaclass=SingletonMeta):
75
72
  try:
76
73
  if transport_type == McpTransportType.STDIO:
77
74
  return StdioMcpServerConfig(**constructor_params)
78
- elif transport_type == McpTransportType.SSE:
79
- return SseMcpServerConfig(**constructor_params)
80
75
  elif transport_type == McpTransportType.STREAMABLE_HTTP:
81
76
  return StreamableHttpMcpServerConfig(**constructor_params)
82
77
  else:
@@ -2,12 +2,10 @@
2
2
  import logging
3
3
  from typing import Optional, TYPE_CHECKING
4
4
 
5
- from autobyteus.tools.mcp.tool import GenericMcpTool
5
+ from .tool import GenericMcpTool
6
6
  from autobyteus.tools.factory.tool_factory import ToolFactory
7
7
 
8
8
  if TYPE_CHECKING:
9
- from autobyteus.tools.mcp.call_handlers.base_handler import McpCallHandler
10
- from autobyteus.tools.mcp.types import BaseMcpConfig
11
9
  from autobyteus.tools.parameter_schema import ParameterSchema
12
10
  from autobyteus.tools.tool_config import ToolConfig
13
11
  from autobyteus.tools.base_tool import BaseTool
@@ -18,52 +16,40 @@ class McpToolFactory(ToolFactory):
18
16
  """
19
17
  A dedicated factory for creating configured instances of GenericMcpTool.
20
18
 
21
- This factory captures the necessary context at the time of tool discovery
22
- (e.g., server config, remote tool name, call handler) and uses it to
23
- instantiate a GenericMcpTool when requested by the ToolRegistry.
19
+ This factory captures the key identifiers of a remote tool (server_id,
20
+ remote_tool_name) and its schema information at the time of discovery.
24
21
  """
25
22
  def __init__(self,
26
- mcp_server_config: 'BaseMcpConfig',
27
- mcp_remote_tool_name: str,
28
- mcp_call_handler: 'McpCallHandler',
23
+ server_id: str,
24
+ remote_tool_name: str,
29
25
  registered_tool_name: str,
30
26
  tool_description: str,
31
27
  tool_argument_schema: 'ParameterSchema'):
32
28
  """
33
- Initializes the factory with the context of a specific remote tool.
29
+ Initializes the factory with the identifiers and schema of a specific remote tool.
34
30
  """
35
- self._mcp_server_config = mcp_server_config
36
- self._mcp_remote_tool_name = mcp_remote_tool_name
37
- self._mcp_call_handler = mcp_call_handler
31
+ self._server_id = server_id
32
+ self._remote_tool_name = remote_tool_name
38
33
  self._registered_tool_name = registered_tool_name
39
34
  self._tool_description = tool_description
40
35
  self._tool_argument_schema = tool_argument_schema
41
36
 
42
37
  logger.debug(
43
- f"McpToolFactory created for remote tool '{self._mcp_remote_tool_name}' "
44
- f"on server '{self._mcp_server_config.server_id}' (to be registered as '{self._registered_tool_name}')."
38
+ f"McpToolFactory created for remote tool '{self._remote_tool_name}' "
39
+ f"on server '{self._server_id}' (to be registered as '{self._registered_tool_name}')."
45
40
  )
46
41
 
47
42
  def create_tool(self, config: Optional['ToolConfig'] = None) -> 'BaseTool':
48
43
  """
49
44
  Creates and returns a new instance of GenericMcpTool using the
50
45
  configuration captured by this factory.
51
-
52
- Args:
53
- config: An optional ToolConfig. This is part of the standard factory
54
- interface but is not used by this specific factory, as all
55
- configuration is provided during initialization.
56
-
57
- Returns:
58
- A configured instance of GenericMcpTool.
59
46
  """
60
47
  if config:
61
48
  logger.debug(f"McpToolFactory for '{self._registered_tool_name}' received a ToolConfig, which will be ignored.")
62
49
 
63
50
  return GenericMcpTool(
64
- mcp_server_config=self._mcp_server_config,
65
- mcp_remote_tool_name=self._mcp_remote_tool_name,
66
- mcp_call_handler=self._mcp_call_handler,
51
+ server_id=self._server_id,
52
+ remote_tool_name=self._remote_tool_name,
67
53
  name=self._registered_tool_name,
68
54
  description=self._tool_description,
69
55
  argument_schema=self._tool_argument_schema
@@ -0,0 +1,16 @@
1
+ # file: autobyteus/autobyteus/tools/mcp/server/__init__.py
2
+ """
3
+ This package contains the core abstractions for managing connections to remote MCP servers.
4
+ """
5
+ from .base_managed_mcp_server import BaseManagedMcpServer, ServerState
6
+ from .stdio_managed_mcp_server import StdioManagedMcpServer
7
+ from .http_managed_mcp_server import HttpManagedMcpServer
8
+ from .proxy import McpServerProxy
9
+
10
+ __all__ = [
11
+ "BaseManagedMcpServer",
12
+ "ServerState",
13
+ "StdioManagedMcpServer",
14
+ "HttpManagedMcpServer",
15
+ "McpServerProxy",
16
+ ]
@@ -0,0 +1,139 @@
1
+ # file: autobyteus/autobyteus/tools/mcp/server/base_managed_mcp_server.py
2
+ import logging
3
+ import asyncio
4
+ from abc import ABC, abstractmethod
5
+ from enum import Enum
6
+ from typing import Dict, Any, List, Optional
7
+ from contextlib import AsyncExitStack
8
+
9
+ from mcp import ClientSession, types as mcp_types
10
+
11
+ from ..types import BaseMcpConfig
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ class ServerState(str, Enum):
16
+ """Enumerates the possible connection states of a BaseManagedMcpServer."""
17
+ DISCONNECTED = "disconnected"
18
+ CONNECTING = "connecting"
19
+ CONNECTED = "connected"
20
+ FAILED = "failed"
21
+ CLOSED = "closed"
22
+
23
+ class BaseManagedMcpServer(ABC):
24
+ """
25
+ Abstract base class representing a connection to a remote MCP server.
26
+ It manages the entire lifecycle and state of a single server connection.
27
+ """
28
+
29
+ # --- Attributes ---
30
+ _config: BaseMcpConfig
31
+ _state: ServerState
32
+ _connection_lock: asyncio.Lock
33
+ _client_session: Optional[ClientSession]
34
+ _exit_stack: AsyncExitStack
35
+
36
+ # --- Initialization ---
37
+ def __init__(self, config: BaseMcpConfig):
38
+ self._config = config
39
+ self._state = ServerState.DISCONNECTED
40
+ self._connection_lock = asyncio.Lock()
41
+ self._client_session = None
42
+ self._exit_stack = AsyncExitStack()
43
+
44
+ # --- Public Properties ---
45
+ @property
46
+ def server_id(self) -> str:
47
+ return self._config.server_id
48
+
49
+ @property
50
+ def config(self) -> BaseMcpConfig:
51
+ return self._config
52
+
53
+ @property
54
+ def state(self) -> ServerState:
55
+ return self._state
56
+
57
+ # --- Abstract Methods ---
58
+ @abstractmethod
59
+ async def _create_client_session(self) -> ClientSession:
60
+ """
61
+ Transport-specific logic to establish a connection and return a ClientSession.
62
+ This method should leverage self._exit_stack to manage resources.
63
+ """
64
+ pass
65
+
66
+ # --- Public API Methods ---
67
+ async def connect(self) -> None:
68
+ """Public method to establish a connection to the server. Idempotent."""
69
+ if self._state == ServerState.CONNECTED:
70
+ return
71
+
72
+ async with self._connection_lock:
73
+ # Re-check state after acquiring lock
74
+ if self._state == ServerState.CONNECTED:
75
+ return
76
+ if self._state == ServerState.CONNECTING:
77
+ logger.debug(f"Connection already in progress for '{self.server_id}'. Awaiting completion.")
78
+ # A more advanced implementation might use an asyncio.Event to wait here.
79
+ # For now, serializing via the lock is sufficient.
80
+ return
81
+
82
+ logger.info(f"Connecting to MCP server '{self.server_id}'...")
83
+ self._state = ServerState.CONNECTING
84
+ try:
85
+ # The exit stack must be fresh for each connection attempt.
86
+ self._exit_stack = AsyncExitStack()
87
+ self._client_session = await self._create_client_session()
88
+ self._state = ServerState.CONNECTED
89
+ logger.info(f"Successfully connected to MCP server '{self.server_id}'.")
90
+ except Exception as e:
91
+ self._state = ServerState.FAILED
92
+ logger.error(f"Failed to connect to MCP server '{self.server_id}': {e}", exc_info=True)
93
+ # Clean up any partially established resources on failure
94
+ if self._exit_stack:
95
+ await self._exit_stack.aclose()
96
+ self._client_session = None
97
+ raise
98
+
99
+ async def close(self) -> None:
100
+ """Public method to gracefully close the connection to the server."""
101
+ async with self._connection_lock:
102
+ if self._state in [ServerState.DISCONNECTED, ServerState.CLOSED]:
103
+ logger.debug(f"Server '{self.server_id}' is already closed or disconnected. No action taken.")
104
+ return
105
+
106
+ logger.info(f"Closing connection to MCP server '{self.server_id}'...")
107
+ self._state = ServerState.CLOSED
108
+
109
+ try:
110
+ if self._exit_stack:
111
+ await self._exit_stack.aclose()
112
+ except Exception as e:
113
+ logger.error(f"Error during resource cleanup for server '{self.server_id}': {e}", exc_info=True)
114
+
115
+ self._client_session = None
116
+ logger.info(f"Connection to MCP server '{self.server_id}' closed.")
117
+
118
+ async def list_remote_tools(self) -> List[mcp_types.Tool]:
119
+ """Connects if needed and fetches the list of raw tool objects."""
120
+ if self._state != ServerState.CONNECTED:
121
+ await self.connect()
122
+
123
+ if not self._client_session:
124
+ raise RuntimeError(f"Cannot list tools: client session not available for server '{self.server_id}'.")
125
+
126
+ logger.debug(f"Listing remote tools on server '{self.server_id}'.")
127
+ result = await self._client_session.list_tools()
128
+ return result.tools
129
+
130
+ async def call_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Any:
131
+ """Connects if needed and executes a tool call on the remote server."""
132
+ if self._state != ServerState.CONNECTED:
133
+ await self.connect()
134
+
135
+ if not self._client_session:
136
+ raise RuntimeError(f"Cannot call tool: client session not available for server '{self.server_id}'.")
137
+
138
+ logger.debug(f"Calling remote tool '{tool_name}' on server '{self.server_id}'.")
139
+ return await self._client_session.call_tool(tool_name, arguments)
@@ -0,0 +1,29 @@
1
+ # file: autobyteus/autobyteus/tools/mcp/server/http_managed_mcp_server.py
2
+ import logging
3
+ from typing import cast
4
+
5
+ from mcp import ClientSession
6
+ from mcp.client.streamable_http import streamablehttp_client
7
+
8
+ from .base_managed_mcp_server import BaseManagedMcpServer
9
+ from ..types import StreamableHttpMcpServerConfig
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ class HttpManagedMcpServer(BaseManagedMcpServer):
14
+ """Manages the lifecycle of a streamable_http-based MCP server."""
15
+
16
+ def __init__(self, config: StreamableHttpMcpServerConfig):
17
+ super().__init__(config)
18
+
19
+ async def _create_client_session(self) -> ClientSession:
20
+ """Connects to a remote HTTP endpoint and establishes a client session."""
21
+ config = cast(StreamableHttpMcpServerConfig, self._config)
22
+
23
+ logger.debug(f"Establishing HTTP connection for server '{self.server_id}' to URL: {config.url}")
24
+ read_stream, write_stream = await self._exit_stack.enter_async_context(
25
+ streamablehttp_client(config.url, headers=config.headers)
26
+ )
27
+ 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}'.")
29
+ return session
@@ -0,0 +1,36 @@
1
+ # file: autobyteus/autobyteus/tools/mcp/server/proxy.py
2
+ import logging
3
+ from typing import Any, Dict
4
+
5
+ from ..server_instance_manager import McpServerInstanceManager
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ class McpServerProxy:
10
+ """
11
+ A proxy object that provides the interface of a BaseManagedMcpServer
12
+ but delegates the actual work to a real server instance retrieved from the
13
+ McpServerInstanceManager. This decouples the tool from the manager.
14
+ """
15
+ def __init__(self, agent_id: str, server_id: str):
16
+ if not agent_id or not server_id:
17
+ raise ValueError("McpServerProxy requires both agent_id and server_id.")
18
+ self._agent_id = agent_id
19
+ self._server_id = server_id
20
+ self._instance_manager = McpServerInstanceManager()
21
+ logger.debug(f"McpServerProxy created for agent '{agent_id}' and server '{server_id}'.")
22
+
23
+ async def call_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Any:
24
+ """
25
+ Gets the real server instance from the manager and delegates the tool call.
26
+ """
27
+ logger.debug(f"Proxy: Getting server instance for agent '{self._agent_id}', server '{self._server_id}'.")
28
+ # The manager handles the logic of creating or returning a cached instance.
29
+ real_server_instance = self._instance_manager.get_server_instance(
30
+ agent_id=self._agent_id,
31
+ server_id=self._server_id
32
+ )
33
+
34
+ logger.debug(f"Proxy: Delegating 'call_tool({tool_name})' to real server instance.")
35
+ # The real instance handles its own connection state.
36
+ return await real_server_instance.call_tool(tool_name, arguments)
@@ -0,0 +1,33 @@
1
+ # file: autobyteus/autobyteus/tools/mcp/server/stdio_managed_mcp_server.py
2
+ import logging
3
+ from typing import cast
4
+
5
+ from mcp import ClientSession, StdioServerParameters
6
+ from mcp.client.stdio import stdio_client
7
+
8
+ from .base_managed_mcp_server import BaseManagedMcpServer
9
+ from ..types import StdioMcpServerConfig
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ class StdioManagedMcpServer(BaseManagedMcpServer):
14
+ """Manages the lifecycle of a stdio-based MCP server."""
15
+
16
+ def __init__(self, config: StdioMcpServerConfig):
17
+ super().__init__(config)
18
+
19
+ async def _create_client_session(self) -> ClientSession:
20
+ """Starts a subprocess and establishes a client session over its stdio."""
21
+ config = cast(StdioMcpServerConfig, self._config)
22
+ stdio_params = StdioServerParameters(
23
+ command=config.command,
24
+ args=config.args,
25
+ env=config.env,
26
+ cwd=config.cwd
27
+ )
28
+
29
+ logger.debug(f"Establishing stdio connection for server '{self.server_id}' with command: {config.command}")
30
+ read_stream, write_stream = await self._exit_stack.enter_async_context(stdio_client(stdio_params))
31
+ 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}'.")
33
+ return session
@@ -0,0 +1,93 @@
1
+ # file: autobyteus/autobyteus/tools/mcp/server_instance_manager.py
2
+ import logging
3
+ from typing import Dict, List, AsyncIterator
4
+ from contextlib import asynccontextmanager
5
+
6
+ from autobyteus.utils.singleton import SingletonMeta
7
+ from .config_service import McpConfigService
8
+ from .server import BaseManagedMcpServer, StdioManagedMcpServer, HttpManagedMcpServer
9
+ from .types import McpTransportType, McpServerInstanceKey, BaseMcpConfig
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ class McpServerInstanceManager(metaclass=SingletonMeta):
14
+ """
15
+ Manages the lifecycle of BaseManagedMcpServer instances, providing
16
+ isolated server connections on a per-agent, per-server_id basis.
17
+ """
18
+ def __init__(self):
19
+ self._config_service = McpConfigService()
20
+ self._active_servers: Dict[McpServerInstanceKey, BaseManagedMcpServer] = {}
21
+ logger.info("McpServerInstanceManager initialized.")
22
+
23
+ def _create_server_instance(self, server_config: BaseMcpConfig) -> BaseManagedMcpServer:
24
+ """Factory method to create a server instance from a config."""
25
+ if server_config.transport_type == McpTransportType.STDIO:
26
+ return StdioManagedMcpServer(server_config)
27
+ elif server_config.transport_type == McpTransportType.STREAMABLE_HTTP:
28
+ return HttpManagedMcpServer(server_config)
29
+ else:
30
+ raise NotImplementedError(f"No ManagedMcpServer implementation for transport type '{server_config.transport_type}'.")
31
+
32
+ def get_server_instance(self, agent_id: str, server_id: str) -> BaseManagedMcpServer:
33
+ """
34
+ Retrieves or creates a dedicated, long-lived managed server instance
35
+ for a given agent and server ID.
36
+ """
37
+ instance_key = McpServerInstanceKey(agent_id=agent_id, server_id=server_id)
38
+
39
+ if instance_key in self._active_servers:
40
+ return self._active_servers[instance_key]
41
+
42
+ logger.info(f"Creating new persistent server instance for {instance_key}.")
43
+ config = self._config_service.get_config(server_id)
44
+ if not config:
45
+ raise ValueError(f"No configuration found for server_id '{server_id}'.")
46
+
47
+ server_instance = self._create_server_instance(config)
48
+ self._active_servers[instance_key] = server_instance
49
+ return server_instance
50
+
51
+ @asynccontextmanager
52
+ async def managed_discovery_session(self, server_config: BaseMcpConfig) -> AsyncIterator[BaseManagedMcpServer]:
53
+ """
54
+ Provides a temporary server instance for a one-shot operation like discovery.
55
+ Guarantees the instance is closed upon exiting the context.
56
+ This method uses the provided config object directly and does not look it up
57
+ in the config service, making it suitable for stateless previews.
58
+ """
59
+ if not server_config:
60
+ raise ValueError("A valid BaseMcpConfig object must be provided to managed_discovery_session.")
61
+
62
+ logger.debug(f"Creating temporary discovery instance for server '{server_config.server_id}'.")
63
+ temp_server_instance = self._create_server_instance(server_config)
64
+ try:
65
+ yield temp_server_instance
66
+ finally:
67
+ logger.debug(f"Closing temporary discovery instance for server '{server_config.server_id}'.")
68
+ await temp_server_instance.close()
69
+
70
+ async def cleanup_mcp_server_instances_for_agent(self, agent_id: str):
71
+ """
72
+ Closes all active MCP server instances and removes them from the cache for a specific agent.
73
+ """
74
+ logger.info(f"Cleaning up all MCP server instances for agent '{agent_id}'.")
75
+ keys_to_remove: List[McpServerInstanceKey] = []
76
+ for instance_key, server_instance in self._active_servers.items():
77
+ if instance_key.agent_id == agent_id:
78
+ try:
79
+ await server_instance.close()
80
+ except Exception as e:
81
+ logger.error(f"Error closing MCP server '{instance_key.server_id}' for agent '{instance_key.agent_id}': {e}", exc_info=True)
82
+ keys_to_remove.append(instance_key)
83
+
84
+ for key in keys_to_remove:
85
+ del self._active_servers[key]
86
+ logger.info(f"Finished cleaning up MCP server instances for agent '{agent_id}'.")
87
+
88
+ async def cleanup_all_mcp_server_instances(self):
89
+ """Closes all active MCP server instances for all agents and clears the cache."""
90
+ logger.info("Cleaning up all active MCP server instances.")
91
+ agent_ids = {key.agent_id for key in self._active_servers.keys()}
92
+ for agent_id in agent_ids:
93
+ await self.cleanup_mcp_server_instances_for_agent(agent_id)
@@ -4,98 +4,80 @@ from typing import Any, Optional, TYPE_CHECKING
4
4
 
5
5
  from autobyteus.tools.base_tool import BaseTool
6
6
  from autobyteus.tools.parameter_schema import ParameterSchema
7
+ from .server.proxy import McpServerProxy
7
8
 
8
9
  if TYPE_CHECKING:
9
10
  from autobyteus.agent.context import AgentContext
10
- from .call_handlers.base_handler import McpCallHandler
11
- from .types import BaseMcpConfig
12
11
 
13
12
  logger = logging.getLogger(__name__)
14
13
 
15
14
  class GenericMcpTool(BaseTool):
16
15
  """
17
16
  A generic tool wrapper for executing tools on a remote MCP server.
18
- This tool delegates the entire remote call, including connection and
19
- protocol specifics, to an MCP call handler.
17
+ This tool is a lightweight blueprint. At execution time, it uses a proxy
18
+ to interact with the server, completely hiding the connection management logic.
20
19
  """
21
20
 
22
21
  def __init__(self,
23
- mcp_server_config: 'BaseMcpConfig',
24
- mcp_remote_tool_name: str,
25
- mcp_call_handler: 'McpCallHandler',
22
+ server_id: str,
23
+ remote_tool_name: str,
26
24
  name: str,
27
25
  description: str,
28
26
  argument_schema: ParameterSchema):
29
27
  """
30
- Initializes the GenericMcpTool instance.
28
+ Initializes the GenericMcpTool instance with identifiers.
31
29
  """
32
30
  super().__init__()
33
31
 
34
- self._mcp_server_config = mcp_server_config
35
- self._mcp_remote_tool_name = mcp_remote_tool_name
36
- self._mcp_call_handler = mcp_call_handler
32
+ self._server_id = server_id
33
+ self._remote_tool_name = remote_tool_name
37
34
 
35
+ # Instance-specific properties for schema generation
38
36
  self._instance_name = name
39
37
  self._instance_description = description
40
38
  self._instance_argument_schema = argument_schema
41
39
 
42
- # Override the base class's schema-related methods with instance-specific
43
- # versions for correct validation and usage generation.
44
40
  self.get_name = self.get_instance_name
45
41
  self.get_description = self.get_instance_description
46
42
  self.get_argument_schema = self.get_instance_argument_schema
47
43
 
48
- logger.info(f"GenericMcpTool instance created for remote tool '{mcp_remote_tool_name}' on server '{self._mcp_server_config.server_id}'. "
44
+ logger.info(f"GenericMcpTool instance created for remote tool '{remote_tool_name}' on server '{self._server_id}'. "
49
45
  f"Registered in AutoByteUs as '{self._instance_name}'.")
50
46
 
51
47
  # --- Getters for instance-specific data ---
48
+ def get_instance_name(self) -> str: return self._instance_name
49
+ def get_instance_description(self) -> str: return self._instance_description
50
+ def get_instance_argument_schema(self) -> ParameterSchema: return self._instance_argument_schema
52
51
 
53
- def get_instance_name(self) -> str:
54
- return self._instance_name
55
-
56
- def get_instance_description(self) -> str:
57
- return self._instance_description
58
-
59
- def get_instance_argument_schema(self) -> ParameterSchema:
60
- return self._instance_argument_schema
61
-
62
- # --- Base class methods that are NOT overridden at instance level ---
63
-
52
+ # --- Base class methods (class-level, not instance-level) ---
64
53
  @classmethod
65
- def get_name(cls) -> str:
66
- return "GenericMcpTool"
67
-
54
+ def get_name(cls) -> str: return "GenericMcpTool"
68
55
  @classmethod
69
- def get_description(cls) -> str:
70
- return "A generic wrapper for executing tools on a remote MCP server. Specifics are instance-based."
71
-
56
+ def get_description(cls) -> str: return "A generic wrapper for executing remote MCP tools."
72
57
  @classmethod
73
- def get_argument_schema(cls) -> Optional[ParameterSchema]:
74
- return None
58
+ def get_argument_schema(cls) -> Optional[ParameterSchema]: return None
75
59
 
76
60
  async def _execute(self, context: 'AgentContext', **kwargs: Any) -> Any:
77
61
  """
78
- Executes the remote MCP tool by delegating to the injected call handler.
62
+ Creates a proxy for the remote server and executes the tool call.
63
+ The proxy handles all interaction with the McpServerInstanceManager.
79
64
  """
65
+ agent_id = context.agent_id
80
66
  tool_name_for_log = self.get_instance_name()
81
- logger.info(f"GenericMcpTool '{tool_name_for_log}': Delegating call for remote tool '{self._mcp_remote_tool_name}' "
82
- f"on server '{self._mcp_server_config.server_id}' to handler.")
83
67
 
84
- if not self._mcp_call_handler:
85
- logger.error(f"GenericMcpTool '{tool_name_for_log}': McpCallHandler is not set. Cannot execute.")
86
- raise RuntimeError("McpCallHandler not available in GenericMcpTool instance.")
87
-
68
+ logger.info(f"GenericMcpTool '{tool_name_for_log}': Creating proxy for agent '{agent_id}' and server '{self._server_id}'.")
69
+
88
70
  try:
89
- # The handler is responsible for the entire end-to-end call.
90
- return await self._mcp_call_handler.handle_call(
91
- config=self._mcp_server_config,
92
- remote_tool_name=self._mcp_remote_tool_name,
71
+ # The proxy is created on-demand for each execution.
72
+ proxy = McpServerProxy(agent_id=agent_id, server_id=self._server_id)
73
+
74
+ return await proxy.call_tool(
75
+ tool_name=self._remote_tool_name,
93
76
  arguments=kwargs
94
77
  )
95
78
  except Exception as e:
96
79
  logger.error(
97
- f"The MCP call handler for tool '{tool_name_for_log}' raised an exception: {e}",
80
+ f"Execution failed for tool '{tool_name_for_log}' on server '{self._server_id}' for agent '{agent_id}': {e}",
98
81
  exc_info=True
99
82
  )
100
- # Re-raise to ensure the agent knows the tool call failed.
101
83
  raise