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.
- autobyteus/agent/bootstrap_steps/__init__.py +2 -0
- autobyteus/agent/bootstrap_steps/agent_bootstrapper.py +2 -0
- autobyteus/agent/bootstrap_steps/mcp_server_prewarming_step.py +71 -0
- autobyteus/agent/llm_response_processor/provider_aware_tool_usage_processor.py +41 -12
- autobyteus/agent/runtime/agent_worker.py +24 -34
- autobyteus/agent/shutdown_steps/__init__.py +17 -0
- autobyteus/agent/shutdown_steps/agent_shutdown_orchestrator.py +63 -0
- autobyteus/agent/shutdown_steps/base_shutdown_step.py +33 -0
- autobyteus/agent/shutdown_steps/llm_instance_cleanup_step.py +45 -0
- autobyteus/agent/shutdown_steps/mcp_server_cleanup_step.py +32 -0
- autobyteus/tools/base_tool.py +2 -0
- autobyteus/tools/mcp/__init__.py +10 -7
- autobyteus/tools/mcp/call_handlers/__init__.py +0 -2
- autobyteus/tools/mcp/config_service.py +1 -6
- autobyteus/tools/mcp/factory.py +12 -26
- autobyteus/tools/mcp/registrar.py +57 -178
- autobyteus/tools/mcp/server/__init__.py +16 -0
- autobyteus/tools/mcp/server/base_managed_mcp_server.py +139 -0
- autobyteus/tools/mcp/server/http_managed_mcp_server.py +29 -0
- autobyteus/tools/mcp/server/proxy.py +36 -0
- autobyteus/tools/mcp/server/stdio_managed_mcp_server.py +33 -0
- autobyteus/tools/mcp/server_instance_manager.py +93 -0
- autobyteus/tools/mcp/tool.py +28 -46
- autobyteus/tools/mcp/tool_registrar.py +177 -0
- autobyteus/tools/mcp/types.py +10 -21
- autobyteus/tools/registry/tool_definition.py +11 -2
- autobyteus/tools/registry/tool_registry.py +27 -28
- {autobyteus-1.1.2.dist-info → autobyteus-1.1.3.dist-info}/METADATA +2 -1
- {autobyteus-1.1.2.dist-info → autobyteus-1.1.3.dist-info}/RECORD +32 -20
- autobyteus/tools/mcp/call_handlers/sse_handler.py +0 -22
- {autobyteus-1.1.2.dist-info → autobyteus-1.1.3.dist-info}/WHEEL +0 -0
- {autobyteus-1.1.2.dist-info → autobyteus-1.1.3.dist-info}/licenses/LICENSE +0 -0
- {autobyteus-1.1.2.dist-info → autobyteus-1.1.3.dist-info}/top_level.txt +0 -0
autobyteus/tools/mcp/factory.py
CHANGED
|
@@ -2,12 +2,10 @@
|
|
|
2
2
|
import logging
|
|
3
3
|
from typing import Optional, TYPE_CHECKING
|
|
4
4
|
|
|
5
|
-
from
|
|
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
|
|
22
|
-
|
|
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
|
-
|
|
27
|
-
|
|
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
|
|
29
|
+
Initializes the factory with the identifiers and schema of a specific remote tool.
|
|
34
30
|
"""
|
|
35
|
-
self.
|
|
36
|
-
self.
|
|
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.
|
|
44
|
-
f"on server '{self.
|
|
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
|
-
|
|
65
|
-
|
|
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
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
|
137
|
+
for remote_tool in remote_tools:
|
|
178
138
|
try:
|
|
179
|
-
tool_def = self._create_tool_definition_from_remote(remote_tool, server_config,
|
|
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}'
|
|
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
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
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,
|
|
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)
|