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
|
@@ -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)
|
autobyteus/tools/mcp/tool.py
CHANGED
|
@@ -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
|
|
19
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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.
|
|
35
|
-
self.
|
|
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 '{
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
85
|
-
|
|
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
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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"
|
|
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
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# file: autobyteus/autobyteus/tools/mcp/tool_registrar.py
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Any, Dict, List, Optional, Union
|
|
4
|
+
|
|
5
|
+
# Consolidated imports from the autobyteus.autobyteus.mcp package public API
|
|
6
|
+
from .config_service import McpConfigService
|
|
7
|
+
from .factory import McpToolFactory
|
|
8
|
+
from .schema_mapper import McpSchemaMapper
|
|
9
|
+
from .server_instance_manager import McpServerInstanceManager
|
|
10
|
+
from .types import BaseMcpConfig
|
|
11
|
+
from .server import BaseManagedMcpServer
|
|
12
|
+
|
|
13
|
+
from autobyteus.tools.registry import ToolRegistry, ToolDefinition
|
|
14
|
+
from autobyteus.tools.tool_category import ToolCategory
|
|
15
|
+
from autobyteus.utils.singleton import SingletonMeta
|
|
16
|
+
from mcp import types as mcp_types
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
class McpToolRegistrar(metaclass=SingletonMeta):
|
|
22
|
+
"""
|
|
23
|
+
Orchestrates the discovery of remote MCP tools and their registration
|
|
24
|
+
with the AutoByteUs ToolRegistry.
|
|
25
|
+
"""
|
|
26
|
+
def __init__(self):
|
|
27
|
+
"""
|
|
28
|
+
Initializes the McpToolRegistrar singleton.
|
|
29
|
+
"""
|
|
30
|
+
self._config_service: McpConfigService = McpConfigService()
|
|
31
|
+
self._tool_registry: ToolRegistry = ToolRegistry()
|
|
32
|
+
self._instance_manager: McpServerInstanceManager = McpServerInstanceManager()
|
|
33
|
+
self._registered_tools_by_server: Dict[str, List[ToolDefinition]] = {}
|
|
34
|
+
logger.info("McpToolRegistrar initialized.")
|
|
35
|
+
|
|
36
|
+
async def _fetch_tools_from_server(self, server_config: BaseMcpConfig) -> List[mcp_types.Tool]:
|
|
37
|
+
"""
|
|
38
|
+
Uses the instance manager to get a temporary, managed session for discovery.
|
|
39
|
+
"""
|
|
40
|
+
async with self._instance_manager.managed_discovery_session(server_config) as discovery_server:
|
|
41
|
+
# The context manager guarantees the server is connected and will be closed.
|
|
42
|
+
remote_tools = await discovery_server.list_remote_tools()
|
|
43
|
+
return remote_tools
|
|
44
|
+
|
|
45
|
+
def _create_tool_definition_from_remote(
|
|
46
|
+
self,
|
|
47
|
+
remote_tool: mcp_types.Tool,
|
|
48
|
+
server_config: BaseMcpConfig,
|
|
49
|
+
schema_mapper: McpSchemaMapper
|
|
50
|
+
) -> ToolDefinition:
|
|
51
|
+
"""
|
|
52
|
+
Maps a single remote tool from an MCP server to an AutoByteUs ToolDefinition.
|
|
53
|
+
"""
|
|
54
|
+
actual_arg_schema = schema_mapper.map_to_autobyteus_schema(remote_tool.inputSchema)
|
|
55
|
+
actual_desc = remote_tool.description
|
|
56
|
+
|
|
57
|
+
registered_name = remote_tool.name
|
|
58
|
+
if server_config.tool_name_prefix:
|
|
59
|
+
registered_name = f"{server_config.tool_name_prefix.rstrip('_')}_{remote_tool.name}"
|
|
60
|
+
|
|
61
|
+
tool_factory = McpToolFactory(
|
|
62
|
+
server_id=server_config.server_id,
|
|
63
|
+
remote_tool_name=remote_tool.name,
|
|
64
|
+
registered_tool_name=registered_name,
|
|
65
|
+
tool_description=actual_desc,
|
|
66
|
+
tool_argument_schema=actual_arg_schema
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return ToolDefinition(
|
|
70
|
+
name=registered_name,
|
|
71
|
+
description=actual_desc,
|
|
72
|
+
argument_schema=actual_arg_schema,
|
|
73
|
+
category=ToolCategory.MCP,
|
|
74
|
+
metadata={"mcp_server_id": server_config.server_id}, # Store origin in generic metadata
|
|
75
|
+
custom_factory=tool_factory.create_tool,
|
|
76
|
+
config_schema=None,
|
|
77
|
+
tool_class=None
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
async def discover_and_register_tools(self, mcp_config: Optional[Union[BaseMcpConfig, Dict[str, Any]]] = None) -> List[ToolDefinition]:
|
|
81
|
+
"""
|
|
82
|
+
Discovers tools from MCP servers and registers them.
|
|
83
|
+
"""
|
|
84
|
+
configs_to_process: List[BaseMcpConfig]
|
|
85
|
+
|
|
86
|
+
if mcp_config:
|
|
87
|
+
validated_config: BaseMcpConfig
|
|
88
|
+
if isinstance(mcp_config, dict):
|
|
89
|
+
try:
|
|
90
|
+
validated_config = self._config_service.load_config(mcp_config)
|
|
91
|
+
except ValueError as e:
|
|
92
|
+
logger.error(f"Failed to parse provided MCP config dictionary: {e}")
|
|
93
|
+
raise
|
|
94
|
+
elif isinstance(mcp_config, BaseMcpConfig):
|
|
95
|
+
validated_config = self._config_service.add_config(mcp_config)
|
|
96
|
+
else:
|
|
97
|
+
raise TypeError(f"mcp_config must be a BaseMcpConfig object or a dictionary, not {type(mcp_config)}.")
|
|
98
|
+
|
|
99
|
+
logger.info(f"Starting targeted MCP tool discovery for server: {validated_config.server_id}")
|
|
100
|
+
self.unregister_tools_from_server(validated_config.server_id)
|
|
101
|
+
configs_to_process = [validated_config]
|
|
102
|
+
else:
|
|
103
|
+
logger.info("Starting full MCP tool discovery. Unregistering all existing MCP tools first.")
|
|
104
|
+
all_server_ids = list(self._registered_tools_by_server.keys())
|
|
105
|
+
for server_id in all_server_ids:
|
|
106
|
+
self.unregister_tools_from_server(server_id)
|
|
107
|
+
self._registered_tools_by_server.clear()
|
|
108
|
+
configs_to_process = self._config_service.get_all_configs()
|
|
109
|
+
|
|
110
|
+
if not configs_to_process:
|
|
111
|
+
logger.info("No MCP server configurations to process. Skipping discovery.")
|
|
112
|
+
return []
|
|
113
|
+
|
|
114
|
+
schema_mapper = McpSchemaMapper()
|
|
115
|
+
registered_tool_definitions: List[ToolDefinition] = []
|
|
116
|
+
for server_config in configs_to_process:
|
|
117
|
+
if not server_config.enabled:
|
|
118
|
+
logger.info(f"MCP server '{server_config.server_id}' is disabled. Skipping.")
|
|
119
|
+
continue
|
|
120
|
+
|
|
121
|
+
logger.info(f"Discovering tools from MCP server: '{server_config.server_id}'")
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
remote_tools = await self._fetch_tools_from_server(server_config)
|
|
125
|
+
logger.info(f"Discovered {len(remote_tools)} tools from server '{server_config.server_id}'.")
|
|
126
|
+
|
|
127
|
+
for remote_tool in remote_tools:
|
|
128
|
+
try:
|
|
129
|
+
tool_def = self._create_tool_definition_from_remote(remote_tool, server_config, schema_mapper)
|
|
130
|
+
self._tool_registry.register_tool(tool_def)
|
|
131
|
+
self._registered_tools_by_server.setdefault(server_config.server_id, []).append(tool_def)
|
|
132
|
+
registered_tool_definitions.append(tool_def)
|
|
133
|
+
except Exception as e_tool:
|
|
134
|
+
logger.error(f"Failed to process or register remote tool '{remote_tool.name}': {e_tool}", exc_info=True)
|
|
135
|
+
|
|
136
|
+
except Exception as e_server:
|
|
137
|
+
logger.error(f"Failed to discover tools from MCP server '{server_config.server_id}': {e_server}", exc_info=True)
|
|
138
|
+
|
|
139
|
+
logger.info(f"MCP tool discovery and registration process completed. Total tools registered: {len(registered_tool_definitions)}.")
|
|
140
|
+
return registered_tool_definitions
|
|
141
|
+
|
|
142
|
+
async def list_remote_tools(self, mcp_config: Union[BaseMcpConfig, Dict[str, Any]]) -> List[ToolDefinition]:
|
|
143
|
+
validated_config: BaseMcpConfig
|
|
144
|
+
if isinstance(mcp_config, dict):
|
|
145
|
+
validated_config = McpConfigService.parse_mcp_config_dict(mcp_config)
|
|
146
|
+
elif isinstance(mcp_config, BaseMcpConfig):
|
|
147
|
+
validated_config = mcp_config
|
|
148
|
+
else:
|
|
149
|
+
raise TypeError(f"mcp_config must be a BaseMcpConfig object or a dictionary, not {type(mcp_config)}.")
|
|
150
|
+
|
|
151
|
+
logger.info(f"Previewing tools from MCP server: '{validated_config.server_id}'")
|
|
152
|
+
schema_mapper = McpSchemaMapper()
|
|
153
|
+
tool_definitions: List[ToolDefinition] = []
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
remote_tools = await self._fetch_tools_from_server(validated_config)
|
|
157
|
+
logger.info(f"Discovered {len(remote_tools)} tools from server '{validated_config.server_id}' for preview.")
|
|
158
|
+
for remote_tool in remote_tools:
|
|
159
|
+
tool_def = self._create_tool_definition_from_remote(remote_tool, validated_config, schema_mapper)
|
|
160
|
+
tool_definitions.append(tool_def)
|
|
161
|
+
except Exception as e_server:
|
|
162
|
+
logger.error(f"Failed to discover tools for preview from MCP server '{validated_config.server_id}': {e_server}", exc_info=True)
|
|
163
|
+
raise
|
|
164
|
+
|
|
165
|
+
logger.info(f"MCP tool preview completed. Found {len(tool_definitions)} tools.")
|
|
166
|
+
return tool_definitions
|
|
167
|
+
|
|
168
|
+
def unregister_tools_from_server(self, server_id: str) -> bool:
|
|
169
|
+
if not self.is_server_registered(server_id):
|
|
170
|
+
return False
|
|
171
|
+
tools_to_unregister = self._registered_tools_by_server.pop(server_id, [])
|
|
172
|
+
for tool_def in tools_to_unregister:
|
|
173
|
+
self._tool_registry.unregister_tool(tool_def.name)
|
|
174
|
+
return True
|
|
175
|
+
|
|
176
|
+
def is_server_registered(self, server_id: str) -> bool:
|
|
177
|
+
return server_id in self._registered_tools_by_server
|
autobyteus/tools/mcp/types.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# file: autobyteus/autobyteus/mcp/types.py
|
|
1
|
+
# file: autobyteus/autobyteus/tools/mcp/types.py
|
|
2
2
|
import logging
|
|
3
3
|
from typing import List, Dict, Any, Optional, Type
|
|
4
4
|
from dataclasses import dataclass, field, InitVar
|
|
@@ -9,9 +9,17 @@ logger = logging.getLogger(__name__)
|
|
|
9
9
|
class McpTransportType(str, Enum):
|
|
10
10
|
"""Enumeration of supported MCP transport types."""
|
|
11
11
|
STDIO = "stdio"
|
|
12
|
-
SSE = "sse"
|
|
13
12
|
STREAMABLE_HTTP = "streamable_http"
|
|
14
13
|
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class McpServerInstanceKey:
|
|
16
|
+
"""
|
|
17
|
+
A dedicated, hashable key for identifying a unique server instance.
|
|
18
|
+
An instance is unique for a given agent and a specific server configuration.
|
|
19
|
+
"""
|
|
20
|
+
agent_id: str
|
|
21
|
+
server_id: str
|
|
22
|
+
|
|
15
23
|
@dataclass
|
|
16
24
|
class BaseMcpConfig:
|
|
17
25
|
"""
|
|
@@ -59,25 +67,6 @@ class StdioMcpServerConfig(BaseMcpConfig):
|
|
|
59
67
|
if self.cwd is not None and not isinstance(self.cwd, str):
|
|
60
68
|
raise ValueError(f"StdioMcpServerConfig '{self.server_id}' 'cwd' must be a string if provided.")
|
|
61
69
|
|
|
62
|
-
@dataclass
|
|
63
|
-
class SseMcpServerConfig(BaseMcpConfig):
|
|
64
|
-
"""Configuration parameters for an MCP server using SSE transport."""
|
|
65
|
-
url: Optional[str] = None # Changed: Added default None
|
|
66
|
-
token: Optional[str] = None
|
|
67
|
-
headers: Dict[str, str] = field(default_factory=dict)
|
|
68
|
-
|
|
69
|
-
def __post_init__(self):
|
|
70
|
-
super().__post_init__()
|
|
71
|
-
self.transport_type = McpTransportType.SSE
|
|
72
|
-
|
|
73
|
-
if self.url is None or not isinstance(self.url, str) or not self.url.strip():
|
|
74
|
-
raise ValueError(f"SseMcpServerConfig '{self.server_id}' 'url' must be a non-empty string.")
|
|
75
|
-
|
|
76
|
-
if self.token is not None and not isinstance(self.token, str):
|
|
77
|
-
raise ValueError(f"SseMcpServerConfig '{self.server_id}' 'token' must be a string if provided.")
|
|
78
|
-
if not isinstance(self.headers, dict) or not all(isinstance(k, str) and isinstance(v, str) for k, v in self.headers.items()):
|
|
79
|
-
raise ValueError(f"SseMcpServerConfig '{self.server_id}' 'headers' must be a Dict[str, str].")
|
|
80
|
-
|
|
81
70
|
@dataclass
|
|
82
71
|
class StreamableHttpMcpServerConfig(BaseMcpConfig):
|
|
83
72
|
"""Configuration parameters for an MCP server using Streamable HTTP transport."""
|
|
@@ -31,7 +31,8 @@ class ToolDefinition:
|
|
|
31
31
|
category: ToolCategory,
|
|
32
32
|
config_schema: Optional['ParameterSchema'] = None,
|
|
33
33
|
tool_class: Optional[Type['BaseTool']] = None,
|
|
34
|
-
custom_factory: Optional[Callable[['ToolConfig'], 'BaseTool']] = None
|
|
34
|
+
custom_factory: Optional[Callable[['ToolConfig'], 'BaseTool']] = None,
|
|
35
|
+
metadata: Optional[Dict[str, Any]] = None):
|
|
35
36
|
"""
|
|
36
37
|
Initializes the ToolDefinition.
|
|
37
38
|
"""
|
|
@@ -56,6 +57,10 @@ class ToolDefinition:
|
|
|
56
57
|
raise TypeError(f"ToolDefinition '{name}' received an invalid 'config_schema'. Expected ParameterSchema or None.")
|
|
57
58
|
if not isinstance(category, ToolCategory):
|
|
58
59
|
raise TypeError(f"ToolDefinition '{name}' requires a ToolCategory for 'category'. Got {type(category)}")
|
|
60
|
+
|
|
61
|
+
# Validation for MCP-specific metadata
|
|
62
|
+
if category == ToolCategory.MCP and not (metadata and metadata.get("mcp_server_id")):
|
|
63
|
+
raise ValueError(f"ToolDefinition '{name}' with category MCP must provide a 'mcp_server_id' in its metadata.")
|
|
59
64
|
|
|
60
65
|
self._name = name
|
|
61
66
|
self._description = description
|
|
@@ -64,6 +69,7 @@ class ToolDefinition:
|
|
|
64
69
|
self._tool_class = tool_class
|
|
65
70
|
self._custom_factory = custom_factory
|
|
66
71
|
self._category = category
|
|
72
|
+
self._metadata = metadata or {}
|
|
67
73
|
|
|
68
74
|
logger.debug(f"ToolDefinition created for tool '{self.name}'.")
|
|
69
75
|
|
|
@@ -82,6 +88,8 @@ class ToolDefinition:
|
|
|
82
88
|
def config_schema(self) -> Optional['ParameterSchema']: return self._config_schema
|
|
83
89
|
@property
|
|
84
90
|
def category(self) -> ToolCategory: return self._category
|
|
91
|
+
@property
|
|
92
|
+
def metadata(self) -> Dict[str, Any]: return self._metadata
|
|
85
93
|
|
|
86
94
|
# --- Schema Generation API ---
|
|
87
95
|
def get_usage_xml(self, provider: Optional[LLMProvider] = None) -> str:
|
|
@@ -136,4 +144,5 @@ class ToolDefinition:
|
|
|
136
144
|
|
|
137
145
|
def __repr__(self) -> str:
|
|
138
146
|
creator_repr = f"class='{self._tool_class.__name__}'" if self._tool_class else "factory=True"
|
|
139
|
-
|
|
147
|
+
metadata_repr = f", metadata={self.metadata}" if self.metadata else ""
|
|
148
|
+
return (f"ToolDefinition(name='{self.name}', category='{self.category.value}'{metadata_repr}, {creator_repr})")
|
|
@@ -5,6 +5,7 @@ from typing import Dict, List, Optional, Type, TYPE_CHECKING
|
|
|
5
5
|
from autobyteus.tools.registry.tool_definition import ToolDefinition
|
|
6
6
|
from autobyteus.utils.singleton import SingletonMeta
|
|
7
7
|
from autobyteus.tools.tool_config import ToolConfig
|
|
8
|
+
from autobyteus.tools.tool_category import ToolCategory
|
|
8
9
|
|
|
9
10
|
if TYPE_CHECKING:
|
|
10
11
|
from autobyteus.tools.base_tool import BaseTool
|
|
@@ -78,19 +79,8 @@ class ToolRegistry(metaclass=SingletonMeta):
|
|
|
78
79
|
|
|
79
80
|
def create_tool(self, name: str, config: Optional[ToolConfig] = None) -> 'BaseTool':
|
|
80
81
|
"""
|
|
81
|
-
Creates a tool instance using its definition
|
|
82
|
-
|
|
83
|
-
Args:
|
|
84
|
-
name: The name of the tool to create.
|
|
85
|
-
config: Optional ToolConfig with constructor parameters for class-based tools
|
|
86
|
-
or to be passed to a custom factory.
|
|
87
|
-
|
|
88
|
-
Returns:
|
|
89
|
-
The tool instance if the definition exists.
|
|
90
|
-
|
|
91
|
-
Raises:
|
|
92
|
-
ValueError: If the tool definition is not found or is invalid.
|
|
93
|
-
TypeError: If tool instantiation fails.
|
|
82
|
+
Creates a tool instance using its definition and injects the definition
|
|
83
|
+
back into the instance.
|
|
94
84
|
"""
|
|
95
85
|
definition = self.get_tool_definition(name)
|
|
96
86
|
if not definition:
|
|
@@ -98,24 +88,20 @@ class ToolRegistry(metaclass=SingletonMeta):
|
|
|
98
88
|
raise ValueError(f"No tool definition found for name '{name}'")
|
|
99
89
|
|
|
100
90
|
try:
|
|
101
|
-
|
|
91
|
+
tool_instance: 'BaseTool'
|
|
102
92
|
if definition.custom_factory:
|
|
103
93
|
logger.info(f"Creating tool instance for '{name}' using its custom factory.")
|
|
104
|
-
# Pass the config to the factory. The factory can choose to use it or not.
|
|
105
94
|
tool_instance = definition.custom_factory(config)
|
|
106
|
-
|
|
107
|
-
# Fall back to instantiating the tool_class
|
|
108
95
|
elif definition.tool_class:
|
|
109
|
-
# For class-based tools, the convention is to pass the ToolConfig object
|
|
110
|
-
# itself to the constructor under the 'config' keyword argument.
|
|
111
96
|
logger.info(f"Creating tool instance for '{name}' using class '{definition.tool_class.__name__}' and passing ToolConfig.")
|
|
112
97
|
tool_instance = definition.tool_class(config=config)
|
|
113
|
-
|
|
114
98
|
else:
|
|
115
|
-
# This case should be prevented by ToolDefinition's validation
|
|
116
99
|
raise ValueError(f"ToolDefinition for '{name}' is invalid: missing both tool_class and custom_factory.")
|
|
117
100
|
|
|
118
|
-
|
|
101
|
+
# Inject the definition into the newly created instance.
|
|
102
|
+
tool_instance.definition = definition
|
|
103
|
+
logger.debug(f"Injected ToolDefinition into instance of '{name}'.")
|
|
104
|
+
|
|
119
105
|
return tool_instance
|
|
120
106
|
|
|
121
107
|
except Exception as e:
|
|
@@ -126,23 +112,36 @@ class ToolRegistry(metaclass=SingletonMeta):
|
|
|
126
112
|
def list_tools(self) -> List[ToolDefinition]:
|
|
127
113
|
"""
|
|
128
114
|
Returns a list of all registered tool definitions.
|
|
129
|
-
|
|
130
|
-
Returns:
|
|
131
|
-
A list of ToolDefinition objects.
|
|
132
115
|
"""
|
|
133
116
|
return list(self._definitions.values())
|
|
134
117
|
|
|
135
118
|
def list_tool_names(self) -> List[str]:
|
|
136
119
|
"""
|
|
137
120
|
Returns a list of the names of all registered tools.
|
|
138
|
-
|
|
139
|
-
Returns:
|
|
140
|
-
A list of tool name strings.
|
|
141
121
|
"""
|
|
142
122
|
return list(self._definitions.keys())
|
|
143
123
|
|
|
144
124
|
def get_all_definitions(self) -> Dict[str, ToolDefinition]:
|
|
145
125
|
"""Returns the internal dictionary of definitions."""
|
|
146
126
|
return dict(ToolRegistry._definitions)
|
|
127
|
+
|
|
128
|
+
def get_tools_by_mcp_server(self, server_id: str) -> List[ToolDefinition]:
|
|
129
|
+
"""
|
|
130
|
+
Returns a list of all registered tool definitions that originated
|
|
131
|
+
from a specific MCP server.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
server_id: The unique ID of the MCP server to query for.
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
A list of matching ToolDefinition objects.
|
|
138
|
+
"""
|
|
139
|
+
if not server_id:
|
|
140
|
+
return []
|
|
141
|
+
|
|
142
|
+
return [
|
|
143
|
+
td for td in self._definitions.values()
|
|
144
|
+
if td.category == ToolCategory.MCP and td.metadata.get("mcp_server_id") == server_id
|
|
145
|
+
]
|
|
147
146
|
|
|
148
147
|
default_tool_registry = ToolRegistry()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: autobyteus
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.3
|
|
4
4
|
Summary: Multi-Agent framework
|
|
5
5
|
Home-page: https://github.com/AutoByteus/autobyteus
|
|
6
6
|
Author: Ryan Zheng
|
|
@@ -33,6 +33,7 @@ Requires-Dist: numpy==2.2.5
|
|
|
33
33
|
Requires-Dist: aiohttp
|
|
34
34
|
Requires-Dist: autobyteus-llm-client==1.1.1
|
|
35
35
|
Requires-Dist: brui-core==1.0.8
|
|
36
|
+
Requires-Dist: mcp[cli]==1.9.1
|
|
36
37
|
Provides-Extra: dev
|
|
37
38
|
Requires-Dist: coverage; extra == "dev"
|
|
38
39
|
Requires-Dist: flake8; extra == "dev"
|