autobyteus 1.1.2__py3-none-any.whl → 1.1.3__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 (33) hide show
  1. autobyteus/agent/bootstrap_steps/__init__.py +2 -0
  2. autobyteus/agent/bootstrap_steps/agent_bootstrapper.py +2 -0
  3. autobyteus/agent/bootstrap_steps/mcp_server_prewarming_step.py +71 -0
  4. autobyteus/agent/llm_response_processor/provider_aware_tool_usage_processor.py +41 -12
  5. autobyteus/agent/runtime/agent_worker.py +24 -34
  6. autobyteus/agent/shutdown_steps/__init__.py +17 -0
  7. autobyteus/agent/shutdown_steps/agent_shutdown_orchestrator.py +63 -0
  8. autobyteus/agent/shutdown_steps/base_shutdown_step.py +33 -0
  9. autobyteus/agent/shutdown_steps/llm_instance_cleanup_step.py +45 -0
  10. autobyteus/agent/shutdown_steps/mcp_server_cleanup_step.py +32 -0
  11. autobyteus/tools/base_tool.py +2 -0
  12. autobyteus/tools/mcp/__init__.py +10 -7
  13. autobyteus/tools/mcp/call_handlers/__init__.py +0 -2
  14. autobyteus/tools/mcp/config_service.py +1 -6
  15. autobyteus/tools/mcp/factory.py +12 -26
  16. autobyteus/tools/mcp/registrar.py +57 -178
  17. autobyteus/tools/mcp/server/__init__.py +16 -0
  18. autobyteus/tools/mcp/server/base_managed_mcp_server.py +139 -0
  19. autobyteus/tools/mcp/server/http_managed_mcp_server.py +29 -0
  20. autobyteus/tools/mcp/server/proxy.py +36 -0
  21. autobyteus/tools/mcp/server/stdio_managed_mcp_server.py +33 -0
  22. autobyteus/tools/mcp/server_instance_manager.py +93 -0
  23. autobyteus/tools/mcp/tool.py +28 -46
  24. autobyteus/tools/mcp/tool_registrar.py +177 -0
  25. autobyteus/tools/mcp/types.py +10 -21
  26. autobyteus/tools/registry/tool_definition.py +11 -2
  27. autobyteus/tools/registry/tool_registry.py +27 -28
  28. {autobyteus-1.1.2.dist-info → autobyteus-1.1.3.dist-info}/METADATA +2 -1
  29. {autobyteus-1.1.2.dist-info → autobyteus-1.1.3.dist-info}/RECORD +32 -20
  30. autobyteus/tools/mcp/call_handlers/sse_handler.py +0 -22
  31. {autobyteus-1.1.2.dist-info → autobyteus-1.1.3.dist-info}/WHEEL +0 -0
  32. {autobyteus-1.1.2.dist-info → autobyteus-1.1.3.dist-info}/licenses/LICENSE +0 -0
  33. {autobyteus-1.1.2.dist-info → autobyteus-1.1.3.dist-info}/top_level.txt +0 -0
@@ -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
@@ -2,22 +2,12 @@
2
2
  import logging
3
3
  from typing import Any, Dict, List, Optional, Union
4
4
 
5
- # Import the new handler architecture components
6
- from .call_handlers import (
7
- McpCallHandler,
8
- StdioMcpCallHandler,
9
- StreamableHttpMcpCallHandler,
10
- SseMcpCallHandler
11
- )
12
-
13
5
  # Consolidated imports from the autobyteus.autobyteus.mcp package public API
14
- from autobyteus.tools.mcp import (
15
- McpConfigService,
16
- McpSchemaMapper,
17
- McpToolFactory,
18
- McpTransportType,
19
- BaseMcpConfig
20
- )
6
+ from .config_service import McpConfigService
7
+ from .factory import McpToolFactory
8
+ from .schema_mapper import McpSchemaMapper
9
+ from .types import BaseMcpConfig, McpTransportType
10
+ from .server import StdioManagedMcpServer, HttpManagedMcpServer, BaseManagedMcpServer
21
11
 
22
12
  from autobyteus.tools.registry import ToolRegistry, ToolDefinition
23
13
  from autobyteus.tools.tool_category import ToolCategory
@@ -30,40 +20,48 @@ logger = logging.getLogger(__name__)
30
20
  class McpToolRegistrar(metaclass=SingletonMeta):
31
21
  """
32
22
  Orchestrates the discovery of remote MCP tools and their registration
33
- with the AutoByteUs ToolRegistry using a handler-based architecture.
34
- This class is a singleton.
23
+ with the AutoByteUs ToolRegistry using a stateful, server-centric architecture.
35
24
  """
36
25
  def __init__(self):
37
26
  """
38
27
  Initializes the McpToolRegistrar singleton.
39
- It retrieves singleton instances of its service dependencies and initializes
40
- its internal state for tracking registered tools.
41
28
  """
42
29
  self._config_service: McpConfigService = McpConfigService()
43
30
  self._tool_registry: ToolRegistry = ToolRegistry()
44
-
45
- # The handler registry maps a transport type to a reusable handler instance.
46
- self._handler_registry: Dict[McpTransportType, McpCallHandler] = {
47
- McpTransportType.STDIO: StdioMcpCallHandler(),
48
- McpTransportType.STREAMABLE_HTTP: StreamableHttpMcpCallHandler(),
49
- McpTransportType.SSE: SseMcpCallHandler(),
50
- }
51
-
52
- # Internal state to track which ToolDefinitions were registered from which server.
53
31
  self._registered_tools_by_server: Dict[str, List[ToolDefinition]] = {}
54
-
55
- logger.info(f"McpToolRegistrar initialized with {len(self._handler_registry)} call handlers.")
32
+ logger.info("McpToolRegistrar initialized.")
33
+
34
+ def _create_server_instance_for_discovery(self, server_config: BaseMcpConfig) -> BaseManagedMcpServer:
35
+ """Creates a server instance based on transport type."""
36
+ if server_config.transport_type == McpTransportType.STDIO:
37
+ return StdioManagedMcpServer(server_config)
38
+ elif server_config.transport_type == McpTransportType.STREAMABLE_HTTP:
39
+ return HttpManagedMcpServer(server_config)
40
+ else:
41
+ raise NotImplementedError(f"Discovery not implemented for transport type: {server_config.transport_type}")
42
+
43
+ async def _fetch_tools_from_server(self, server_config: BaseMcpConfig) -> List[mcp_types.Tool]:
44
+ """
45
+ Creates a temporary server instance to perform a single, one-shot
46
+ tool discovery, ensuring resources are properly closed.
47
+ """
48
+ discovery_server = self._create_server_instance_for_discovery(server_config)
49
+ try:
50
+ # The list_remote_tools method implicitly handles the connection.
51
+ remote_tools = await discovery_server.list_remote_tools()
52
+ return remote_tools
53
+ finally:
54
+ # The finally block guarantees the temporary server connection is closed.
55
+ await discovery_server.close()
56
56
 
57
57
  def _create_tool_definition_from_remote(
58
58
  self,
59
59
  remote_tool: mcp_types.Tool,
60
60
  server_config: BaseMcpConfig,
61
- handler: McpCallHandler,
62
61
  schema_mapper: McpSchemaMapper
63
62
  ) -> ToolDefinition:
64
63
  """
65
64
  Maps a single remote tool from an MCP server to an AutoByteUs ToolDefinition.
66
- This is a helper method to centralize the mapping logic.
67
65
  """
68
66
  if hasattr(remote_tool, 'model_dump_json'):
69
67
  logger.debug(f"Processing remote tool from server '{server_config.server_id}':\n{remote_tool.model_dump_json(indent=2)}")
@@ -75,16 +73,16 @@ class McpToolRegistrar(metaclass=SingletonMeta):
75
73
  if server_config.tool_name_prefix:
76
74
  registered_name = f"{server_config.tool_name_prefix.rstrip('_')}_{remote_tool.name}"
77
75
 
76
+ # The factory now only needs key identifiers, not live objects.
78
77
  tool_factory = McpToolFactory(
79
- mcp_server_config=server_config,
80
- mcp_remote_tool_name=remote_tool.name,
81
- mcp_call_handler=handler,
78
+ server_id=server_config.server_id,
79
+ remote_tool_name=remote_tool.name,
82
80
  registered_tool_name=registered_name,
83
81
  tool_description=actual_desc,
84
82
  tool_argument_schema=actual_arg_schema
85
83
  )
86
84
 
87
- tool_def = ToolDefinition(
85
+ return ToolDefinition(
88
86
  name=registered_name,
89
87
  description=actual_desc,
90
88
  argument_schema=actual_arg_schema,
@@ -93,54 +91,29 @@ class McpToolRegistrar(metaclass=SingletonMeta):
93
91
  config_schema=None,
94
92
  tool_class=None
95
93
  )
96
- return tool_def
97
94
 
98
95
  async def discover_and_register_tools(self, mcp_config: Optional[Union[BaseMcpConfig, Dict[str, Any]]] = None) -> List[ToolDefinition]:
99
96
  """
100
97
  Discovers tools from MCP servers and registers them.
101
-
102
- If `mcp_config` is provided (as a validated object or a raw dictionary),
103
- it discovers tools only from that specific server, unregistering any
104
- of its old tools first.
105
-
106
- If `mcp_config` is None, it unregisters all existing MCP tools and then
107
- discovers tools from all enabled servers found in the McpConfigService.
108
-
109
- Returns:
110
- A list of ToolDefinition objects for all tools that were successfully
111
- discovered and registered during this call.
98
+ This process uses a helper to manage short-lived server instances for discovery.
112
99
  """
113
100
  configs_to_process: List[BaseMcpConfig]
114
101
 
115
102
  if mcp_config:
116
- validated_config: BaseMcpConfig
117
- # If the user provided a raw dictionary, parse and add it via the service.
118
103
  if isinstance(mcp_config, dict):
119
- try:
120
- validated_config = self._config_service.load_config(mcp_config)
121
- except ValueError as e:
122
- logger.error(f"Failed to parse provided MCP config dictionary: {e}")
123
- raise
124
- # If a validated object was passed, add it to the service to ensure it's known.
104
+ configs_to_process = [self._config_service.load_config(mcp_config)]
125
105
  elif isinstance(mcp_config, BaseMcpConfig):
126
- validated_config = self._config_service.add_config(mcp_config)
106
+ configs_to_process = [self._config_service.add_config(mcp_config)]
127
107
  else:
128
108
  raise TypeError(f"mcp_config must be a BaseMcpConfig object or a dictionary, not {type(mcp_config)}.")
129
-
130
- logger.info(f"Starting targeted MCP tool discovery for server: {validated_config.server_id}")
131
- # Unregister any existing tools from this server before re-discovering.
132
- self.unregister_tools_from_server(validated_config.server_id)
133
- configs_to_process = [validated_config]
109
+ logger.info(f"Starting targeted MCP tool discovery for server: {configs_to_process[0].server_id}")
110
+ self.unregister_tools_from_server(configs_to_process[0].server_id)
134
111
  else:
135
- logger.info("Starting full MCP tool discovery and registration process. Unregistering all existing MCP tools first.")
136
- # Get a copy of all server IDs that have tools registered.
112
+ logger.info("Starting full MCP tool discovery. Unregistering all existing MCP tools first.")
137
113
  all_server_ids = list(self._registered_tools_by_server.keys())
138
114
  for server_id in all_server_ids:
139
115
  self.unregister_tools_from_server(server_id)
140
-
141
- # The registrar's internal state should now be empty, but a clear() is a good safeguard.
142
116
  self._registered_tools_by_server.clear()
143
-
144
117
  configs_to_process = self._config_service.get_all_configs()
145
118
 
146
119
  if not configs_to_process:
@@ -155,37 +128,21 @@ class McpToolRegistrar(metaclass=SingletonMeta):
155
128
  continue
156
129
 
157
130
  logger.info(f"Discovering tools from MCP server: '{server_config.server_id}' ({server_config.transport_type.value})")
131
+
158
132
  try:
159
- handler = self._handler_registry.get(server_config.transport_type)
160
- if not handler:
161
- logger.error(f"No MCP call handler found for transport type '{server_config.transport_type.value}' on server '{server_config.server_id}'.")
162
- continue
163
-
164
- remote_tools_result = await handler.handle_call(
165
- config=server_config,
166
- remote_tool_name="list_tools",
167
- arguments={}
168
- )
169
-
170
- if not isinstance(remote_tools_result, mcp_types.ListToolsResult):
171
- logger.error(f"Expected ListToolsResult from handler for 'list_tools', but got {type(remote_tools_result)}. Skipping server '{server_config.server_id}'.")
172
- continue
173
-
174
- actual_remote_tools: list[mcp_types.Tool] = remote_tools_result.tools
175
- logger.info(f"Discovered {len(actual_remote_tools)} tools from server '{server_config.server_id}'.")
133
+ # The helper abstracts away the connect/close lifecycle for discovery.
134
+ remote_tools = await self._fetch_tools_from_server(server_config)
135
+ logger.info(f"Discovered {len(remote_tools)} tools from server '{server_config.server_id}'.")
176
136
 
177
- for remote_tool in actual_remote_tools:
137
+ for remote_tool in remote_tools:
178
138
  try:
179
- tool_def = self._create_tool_definition_from_remote(remote_tool, server_config, handler, schema_mapper)
180
-
139
+ tool_def = self._create_tool_definition_from_remote(remote_tool, server_config, schema_mapper)
181
140
  self._tool_registry.register_tool(tool_def)
182
141
  self._registered_tools_by_server.setdefault(server_config.server_id, []).append(tool_def)
183
142
  registered_tool_definitions.append(tool_def)
184
-
185
143
  logger.info(f"Successfully registered MCP tool '{remote_tool.name}' from server '{server_config.server_id}' as '{tool_def.name}'.")
186
-
187
144
  except Exception as e_tool:
188
- logger.error(f"Failed to process or register remote tool '{remote_tool.name}' from server '{server_config.server_id}': {e_tool}", exc_info=True)
145
+ logger.error(f"Failed to process or register remote tool '{remote_tool.name}': {e_tool}", exc_info=True)
189
146
 
190
147
  except Exception as e_server:
191
148
  logger.error(f"Failed to discover tools from MCP server '{server_config.server_id}': {e_server}", exc_info=True)
@@ -195,23 +152,11 @@ class McpToolRegistrar(metaclass=SingletonMeta):
195
152
 
196
153
  async def list_remote_tools(self, mcp_config: Union[BaseMcpConfig, Dict[str, Any]]) -> List[ToolDefinition]:
197
154
  """
198
- Previews tools from a remote MCP server without registering them or storing the configuration.
155
+ Previews tools from a remote MCP server without registering them.
199
156
  This is a stateless "dry-run" or "preview" operation.
200
-
201
- Args:
202
- mcp_config: A single MCP server configuration, as a validated object or a raw dictionary.
203
-
204
- Returns:
205
- A list of ToolDefinition objects for all tools discovered on the server.
206
-
207
- Raises:
208
- TypeError: If mcp_config is not a supported type.
209
- ValueError: If the provided configuration is invalid.
210
- RuntimeError: If the connection or tool call to the remote server fails.
211
157
  """
212
158
  validated_config: BaseMcpConfig
213
159
  if isinstance(mcp_config, dict):
214
- # Use the static method to parse the config without storing it in the service.
215
160
  validated_config = McpConfigService.parse_mcp_config_dict(mcp_config)
216
161
  elif isinstance(mcp_config, BaseMcpConfig):
217
162
  validated_config = mcp_config
@@ -224,100 +169,34 @@ class McpToolRegistrar(metaclass=SingletonMeta):
224
169
  tool_definitions: List[ToolDefinition] = []
225
170
 
226
171
  try:
227
- handler = self._handler_registry.get(validated_config.transport_type)
228
- if not handler:
229
- raise ValueError(f"No MCP call handler found for transport type '{validated_config.transport_type.value}'.")
230
-
231
- remote_tools_result = await handler.handle_call(
232
- config=validated_config,
233
- remote_tool_name="list_tools",
234
- arguments={}
235
- )
236
-
237
- if not isinstance(remote_tools_result, mcp_types.ListToolsResult):
238
- error_msg = f"Expected ListToolsResult from handler, but got {type(remote_tools_result)}. Cannot preview tools from server '{validated_config.server_id}'."
239
- logger.error(error_msg)
240
- raise RuntimeError(error_msg)
172
+ # Use the same helper to fetch tools, abstracting away the connection lifecycle.
173
+ remote_tools = await self._fetch_tools_from_server(validated_config)
174
+ logger.info(f"Discovered {len(remote_tools)} tools from server '{validated_config.server_id}' for preview.")
241
175
 
242
- actual_remote_tools: list[mcp_types.Tool] = remote_tools_result.tools
243
- logger.info(f"Discovered {len(actual_remote_tools)} tools from server '{validated_config.server_id}' for preview.")
244
-
245
- for remote_tool in actual_remote_tools:
176
+ for remote_tool in remote_tools:
246
177
  try:
247
- tool_def = self._create_tool_definition_from_remote(remote_tool, validated_config, handler, schema_mapper)
178
+ tool_def = self._create_tool_definition_from_remote(remote_tool, validated_config, schema_mapper)
248
179
  tool_definitions.append(tool_def)
249
180
  except Exception as e_tool:
250
181
  logger.error(f"Failed to map remote tool '{remote_tool.name}' from server '{validated_config.server_id}' during preview: {e_tool}", exc_info=True)
251
- # For a preview, we continue to show other valid tools.
252
182
 
253
183
  except Exception as e_server:
254
184
  logger.error(f"Failed to discover tools for preview from MCP server '{validated_config.server_id}': {e_server}", exc_info=True)
255
- # Re-raise server-level exceptions to the caller as per AC5.
256
185
  raise
257
186
 
258
187
  logger.info(f"MCP tool preview completed. Found {len(tool_definitions)} tools.")
259
188
  return tool_definitions
260
189
 
261
- def get_registered_tools_for_server(self, server_id: str) -> List[ToolDefinition]:
262
- """
263
- Returns a list of ToolDefinition objects that were successfully registered
264
- from a specific MCP server during the most recent discovery process.
265
-
266
- Args:
267
- server_id: The unique ID of the MCP server to query.
268
-
269
- Returns:
270
- A list of ToolDefinition objects. Returns an empty list if the server ID
271
- is not found or registered no tools.
272
- """
273
- return self._registered_tools_by_server.get(server_id, [])
274
-
275
- def get_all_registered_mcp_tools(self) -> List[ToolDefinition]:
276
- """
277
- Returns a flat list of all ToolDefinitions registered from any MCP server.
278
- """
279
- all_tools = []
280
- for server_tools in self._registered_tools_by_server.values():
281
- all_tools.extend(server_tools)
282
- return all_tools
283
-
284
- def is_server_registered(self, server_id: str) -> bool:
285
- """
286
- Checks if any tools from a specific MCP server are currently registered.
287
-
288
- Args:
289
- server_id: The unique ID of the MCP server.
290
-
291
- Returns:
292
- True if the server has tools registered, False otherwise.
293
- """
294
- is_registered = server_id in self._registered_tools_by_server
295
- logger.debug(f"Checking if server '{server_id}' is registered: {is_registered}")
296
- return is_registered
297
-
298
190
  def unregister_tools_from_server(self, server_id: str) -> bool:
299
- """
300
- Unregisters all tools associated with a given MCP server ID from the
301
- main ToolRegistry and removes the server from internal tracking.
302
-
303
- Args:
304
- server_id: The unique ID of the MCP server whose tools should be unregistered.
305
-
306
- Returns:
307
- True if the server was found and its tools were processed for unregistration,
308
- False if the server was not found in the registrar's state.
309
- """
310
191
  if not self.is_server_registered(server_id):
311
192
  logger.info(f"No tools found for server ID '{server_id}'. Nothing to unregister.")
312
193
  return False
313
-
314
- tools_to_unregister = self._registered_tools_by_server.get(server_id, [])
194
+ tools_to_unregister = self._registered_tools_by_server.pop(server_id, [])
315
195
  logger.info(f"Unregistering {len(tools_to_unregister)} tools from server ID: '{server_id}'...")
316
-
317
196
  for tool_def in tools_to_unregister:
318
197
  self._tool_registry.unregister_tool(tool_def.name)
319
-
320
- # Remove the server from the tracking dictionary
321
- del self._registered_tools_by_server[server_id]
322
198
  logger.info(f"Successfully unregistered all tools and removed server '{server_id}' from registrar tracking.")
323
199
  return True
200
+
201
+ def is_server_registered(self, server_id: str) -> bool:
202
+ return server_id in self._registered_tools_by_server
@@ -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)