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
@@ -0,0 +1,33 @@
1
+ # file: autobyteus/autobyteus/tools/mcp/server/stdio_managed_mcp_server.py
2
+ import logging
3
+ from typing import cast
4
+
5
+ from mcp import ClientSession, StdioServerParameters
6
+ from mcp.client.stdio import stdio_client
7
+
8
+ from .base_managed_mcp_server import BaseManagedMcpServer
9
+ from ..types import StdioMcpServerConfig
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ class StdioManagedMcpServer(BaseManagedMcpServer):
14
+ """Manages the lifecycle of a stdio-based MCP server."""
15
+
16
+ def __init__(self, config: StdioMcpServerConfig):
17
+ super().__init__(config)
18
+
19
+ async def _create_client_session(self) -> ClientSession:
20
+ """Starts a subprocess and establishes a client session over its stdio."""
21
+ config = cast(StdioMcpServerConfig, self._config)
22
+ stdio_params = StdioServerParameters(
23
+ command=config.command,
24
+ args=config.args,
25
+ env=config.env,
26
+ cwd=config.cwd
27
+ )
28
+
29
+ logger.debug(f"Establishing stdio connection for server '{self.server_id}' with command: {config.command}")
30
+ read_stream, write_stream = await self._exit_stack.enter_async_context(stdio_client(stdio_params))
31
+ session = await self._exit_stack.enter_async_context(ClientSession(read_stream, write_stream))
32
+ logger.debug(f"ClientSession established for stdio server '{self.server_id}'.")
33
+ return session
@@ -0,0 +1,93 @@
1
+ # file: autobyteus/autobyteus/tools/mcp/server_instance_manager.py
2
+ import logging
3
+ from typing import Dict, List, AsyncIterator
4
+ from contextlib import asynccontextmanager
5
+
6
+ from autobyteus.utils.singleton import SingletonMeta
7
+ from .config_service import McpConfigService
8
+ from .server import BaseManagedMcpServer, StdioManagedMcpServer, HttpManagedMcpServer
9
+ from .types import McpTransportType, McpServerInstanceKey, BaseMcpConfig
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ class McpServerInstanceManager(metaclass=SingletonMeta):
14
+ """
15
+ Manages the lifecycle of BaseManagedMcpServer instances, providing
16
+ isolated server connections on a per-agent, per-server_id basis.
17
+ """
18
+ def __init__(self):
19
+ self._config_service = McpConfigService()
20
+ self._active_servers: Dict[McpServerInstanceKey, BaseManagedMcpServer] = {}
21
+ logger.info("McpServerInstanceManager initialized.")
22
+
23
+ def _create_server_instance(self, server_config: BaseMcpConfig) -> BaseManagedMcpServer:
24
+ """Factory method to create a server instance from a config."""
25
+ if server_config.transport_type == McpTransportType.STDIO:
26
+ return StdioManagedMcpServer(server_config)
27
+ elif server_config.transport_type == McpTransportType.STREAMABLE_HTTP:
28
+ return HttpManagedMcpServer(server_config)
29
+ else:
30
+ raise NotImplementedError(f"No ManagedMcpServer implementation for transport type '{server_config.transport_type}'.")
31
+
32
+ def get_server_instance(self, agent_id: str, server_id: str) -> BaseManagedMcpServer:
33
+ """
34
+ Retrieves or creates a dedicated, long-lived managed server instance
35
+ for a given agent and server ID.
36
+ """
37
+ instance_key = McpServerInstanceKey(agent_id=agent_id, server_id=server_id)
38
+
39
+ if instance_key in self._active_servers:
40
+ return self._active_servers[instance_key]
41
+
42
+ logger.info(f"Creating new persistent server instance for {instance_key}.")
43
+ config = self._config_service.get_config(server_id)
44
+ if not config:
45
+ raise ValueError(f"No configuration found for server_id '{server_id}'.")
46
+
47
+ server_instance = self._create_server_instance(config)
48
+ self._active_servers[instance_key] = server_instance
49
+ return server_instance
50
+
51
+ @asynccontextmanager
52
+ async def managed_discovery_session(self, server_config: BaseMcpConfig) -> AsyncIterator[BaseManagedMcpServer]:
53
+ """
54
+ Provides a temporary server instance for a one-shot operation like discovery.
55
+ Guarantees the instance is closed upon exiting the context.
56
+ This method uses the provided config object directly and does not look it up
57
+ in the config service, making it suitable for stateless previews.
58
+ """
59
+ if not server_config:
60
+ raise ValueError("A valid BaseMcpConfig object must be provided to managed_discovery_session.")
61
+
62
+ logger.debug(f"Creating temporary discovery instance for server '{server_config.server_id}'.")
63
+ temp_server_instance = self._create_server_instance(server_config)
64
+ try:
65
+ yield temp_server_instance
66
+ finally:
67
+ logger.debug(f"Closing temporary discovery instance for server '{server_config.server_id}'.")
68
+ await temp_server_instance.close()
69
+
70
+ async def cleanup_mcp_server_instances_for_agent(self, agent_id: str):
71
+ """
72
+ Closes all active MCP server instances and removes them from the cache for a specific agent.
73
+ """
74
+ logger.info(f"Cleaning up all MCP server instances for agent '{agent_id}'.")
75
+ keys_to_remove: List[McpServerInstanceKey] = []
76
+ for instance_key, server_instance in self._active_servers.items():
77
+ if instance_key.agent_id == agent_id:
78
+ try:
79
+ await server_instance.close()
80
+ except Exception as e:
81
+ logger.error(f"Error closing MCP server '{instance_key.server_id}' for agent '{instance_key.agent_id}': {e}", exc_info=True)
82
+ keys_to_remove.append(instance_key)
83
+
84
+ for key in keys_to_remove:
85
+ del self._active_servers[key]
86
+ logger.info(f"Finished cleaning up MCP server instances for agent '{agent_id}'.")
87
+
88
+ async def cleanup_all_mcp_server_instances(self):
89
+ """Closes all active MCP server instances for all agents and clears the cache."""
90
+ logger.info("Cleaning up all active MCP server instances.")
91
+ agent_ids = {key.agent_id for key in self._active_servers.keys()}
92
+ for agent_id in agent_ids:
93
+ await self.cleanup_mcp_server_instances_for_agent(agent_id)
@@ -4,98 +4,80 @@ from typing import Any, Optional, TYPE_CHECKING
4
4
 
5
5
  from autobyteus.tools.base_tool import BaseTool
6
6
  from autobyteus.tools.parameter_schema import ParameterSchema
7
+ from .server.proxy import McpServerProxy
7
8
 
8
9
  if TYPE_CHECKING:
9
10
  from autobyteus.agent.context import AgentContext
10
- from .call_handlers.base_handler import McpCallHandler
11
- from .types import BaseMcpConfig
12
11
 
13
12
  logger = logging.getLogger(__name__)
14
13
 
15
14
  class GenericMcpTool(BaseTool):
16
15
  """
17
16
  A generic tool wrapper for executing tools on a remote MCP server.
18
- This tool delegates the entire remote call, including connection and
19
- protocol specifics, to an MCP call handler.
17
+ This tool is a lightweight blueprint. At execution time, it uses a proxy
18
+ to interact with the server, completely hiding the connection management logic.
20
19
  """
21
20
 
22
21
  def __init__(self,
23
- mcp_server_config: 'BaseMcpConfig',
24
- mcp_remote_tool_name: str,
25
- mcp_call_handler: 'McpCallHandler',
22
+ server_id: str,
23
+ remote_tool_name: str,
26
24
  name: str,
27
25
  description: str,
28
26
  argument_schema: ParameterSchema):
29
27
  """
30
- Initializes the GenericMcpTool instance.
28
+ Initializes the GenericMcpTool instance with identifiers.
31
29
  """
32
30
  super().__init__()
33
31
 
34
- self._mcp_server_config = mcp_server_config
35
- self._mcp_remote_tool_name = mcp_remote_tool_name
36
- self._mcp_call_handler = mcp_call_handler
32
+ self._server_id = server_id
33
+ self._remote_tool_name = remote_tool_name
37
34
 
35
+ # Instance-specific properties for schema generation
38
36
  self._instance_name = name
39
37
  self._instance_description = description
40
38
  self._instance_argument_schema = argument_schema
41
39
 
42
- # Override the base class's schema-related methods with instance-specific
43
- # versions for correct validation and usage generation.
44
40
  self.get_name = self.get_instance_name
45
41
  self.get_description = self.get_instance_description
46
42
  self.get_argument_schema = self.get_instance_argument_schema
47
43
 
48
- logger.info(f"GenericMcpTool instance created for remote tool '{mcp_remote_tool_name}' on server '{self._mcp_server_config.server_id}'. "
44
+ logger.info(f"GenericMcpTool instance created for remote tool '{remote_tool_name}' on server '{self._server_id}'. "
49
45
  f"Registered in AutoByteUs as '{self._instance_name}'.")
50
46
 
51
47
  # --- Getters for instance-specific data ---
48
+ def get_instance_name(self) -> str: return self._instance_name
49
+ def get_instance_description(self) -> str: return self._instance_description
50
+ def get_instance_argument_schema(self) -> ParameterSchema: return self._instance_argument_schema
52
51
 
53
- def get_instance_name(self) -> str:
54
- return self._instance_name
55
-
56
- def get_instance_description(self) -> str:
57
- return self._instance_description
58
-
59
- def get_instance_argument_schema(self) -> ParameterSchema:
60
- return self._instance_argument_schema
61
-
62
- # --- Base class methods that are NOT overridden at instance level ---
63
-
52
+ # --- Base class methods (class-level, not instance-level) ---
64
53
  @classmethod
65
- def get_name(cls) -> str:
66
- return "GenericMcpTool"
67
-
54
+ def get_name(cls) -> str: return "GenericMcpTool"
68
55
  @classmethod
69
- def get_description(cls) -> str:
70
- return "A generic wrapper for executing tools on a remote MCP server. Specifics are instance-based."
71
-
56
+ def get_description(cls) -> str: return "A generic wrapper for executing remote MCP tools."
72
57
  @classmethod
73
- def get_argument_schema(cls) -> Optional[ParameterSchema]:
74
- return None
58
+ def get_argument_schema(cls) -> Optional[ParameterSchema]: return None
75
59
 
76
60
  async def _execute(self, context: 'AgentContext', **kwargs: Any) -> Any:
77
61
  """
78
- Executes the remote MCP tool by delegating to the injected call handler.
62
+ Creates a proxy for the remote server and executes the tool call.
63
+ The proxy handles all interaction with the McpServerInstanceManager.
79
64
  """
65
+ agent_id = context.agent_id
80
66
  tool_name_for_log = self.get_instance_name()
81
- logger.info(f"GenericMcpTool '{tool_name_for_log}': Delegating call for remote tool '{self._mcp_remote_tool_name}' "
82
- f"on server '{self._mcp_server_config.server_id}' to handler.")
83
67
 
84
- if not self._mcp_call_handler:
85
- logger.error(f"GenericMcpTool '{tool_name_for_log}': McpCallHandler is not set. Cannot execute.")
86
- raise RuntimeError("McpCallHandler not available in GenericMcpTool instance.")
87
-
68
+ logger.info(f"GenericMcpTool '{tool_name_for_log}': Creating proxy for agent '{agent_id}' and server '{self._server_id}'.")
69
+
88
70
  try:
89
- # The handler is responsible for the entire end-to-end call.
90
- return await self._mcp_call_handler.handle_call(
91
- config=self._mcp_server_config,
92
- remote_tool_name=self._mcp_remote_tool_name,
71
+ # The proxy is created on-demand for each execution.
72
+ proxy = McpServerProxy(agent_id=agent_id, server_id=self._server_id)
73
+
74
+ return await proxy.call_tool(
75
+ tool_name=self._remote_tool_name,
93
76
  arguments=kwargs
94
77
  )
95
78
  except Exception as e:
96
79
  logger.error(
97
- f"The MCP call handler for tool '{tool_name_for_log}' raised an exception: {e}",
80
+ f"Execution failed for tool '{tool_name_for_log}' on server '{self._server_id}' for agent '{agent_id}': {e}",
98
81
  exc_info=True
99
82
  )
100
- # Re-raise to ensure the agent knows the tool call failed.
101
83
  raise
@@ -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
@@ -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
- return (f"ToolDefinition(name='{self.name}', category='{self.category.value}', {creator_repr})")
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, either from a factory or a class.
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
- # Prefer the custom factory if it exists
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
- logger.debug(f"Successfully created tool instance for '{name}'")
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.2
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"