autobyteus 1.1.0__py3-none-any.whl → 1.1.2__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/agent_bootstrapper.py +1 -1
- autobyteus/agent/bootstrap_steps/agent_runtime_queue_initialization_step.py +1 -1
- autobyteus/agent/bootstrap_steps/base_bootstrap_step.py +1 -1
- autobyteus/agent/bootstrap_steps/system_prompt_processing_step.py +1 -1
- autobyteus/agent/bootstrap_steps/workspace_context_initialization_step.py +1 -1
- autobyteus/agent/context/__init__.py +0 -5
- autobyteus/agent/context/agent_config.py +6 -2
- autobyteus/agent/context/agent_context.py +2 -5
- autobyteus/agent/context/agent_phase_manager.py +105 -5
- autobyteus/agent/context/agent_runtime_state.py +2 -2
- autobyteus/agent/context/phases.py +2 -0
- autobyteus/agent/events/__init__.py +0 -11
- autobyteus/agent/events/agent_events.py +0 -37
- autobyteus/agent/events/notifiers.py +25 -7
- autobyteus/agent/events/worker_event_dispatcher.py +1 -1
- autobyteus/agent/factory/agent_factory.py +6 -2
- autobyteus/agent/group/agent_group.py +16 -7
- autobyteus/agent/handlers/approved_tool_invocation_event_handler.py +28 -14
- autobyteus/agent/handlers/lifecycle_event_logger.py +1 -1
- autobyteus/agent/handlers/llm_complete_response_received_event_handler.py +4 -2
- autobyteus/agent/handlers/tool_invocation_request_event_handler.py +40 -15
- autobyteus/agent/handlers/tool_result_event_handler.py +12 -7
- autobyteus/agent/hooks/__init__.py +7 -0
- autobyteus/agent/hooks/base_phase_hook.py +11 -2
- autobyteus/agent/hooks/hook_definition.py +36 -0
- autobyteus/agent/hooks/hook_meta.py +37 -0
- autobyteus/agent/hooks/hook_registry.py +118 -0
- autobyteus/agent/input_processor/base_user_input_processor.py +6 -3
- autobyteus/agent/input_processor/passthrough_input_processor.py +2 -1
- autobyteus/agent/input_processor/processor_meta.py +1 -1
- autobyteus/agent/input_processor/processor_registry.py +19 -0
- autobyteus/agent/llm_response_processor/base_processor.py +6 -3
- autobyteus/agent/llm_response_processor/processor_meta.py +1 -1
- autobyteus/agent/llm_response_processor/processor_registry.py +19 -0
- autobyteus/agent/llm_response_processor/provider_aware_tool_usage_processor.py +2 -1
- autobyteus/agent/message/context_file_type.py +2 -3
- autobyteus/agent/phases/__init__.py +18 -0
- autobyteus/agent/phases/discover.py +52 -0
- autobyteus/agent/phases/manager.py +265 -0
- autobyteus/agent/phases/phase_enum.py +49 -0
- autobyteus/agent/phases/transition_decorator.py +40 -0
- autobyteus/agent/phases/transition_info.py +33 -0
- autobyteus/agent/remote_agent.py +1 -1
- autobyteus/agent/runtime/agent_runtime.py +5 -10
- autobyteus/agent/runtime/agent_worker.py +62 -19
- autobyteus/agent/streaming/agent_event_stream.py +58 -5
- autobyteus/agent/streaming/stream_event_payloads.py +24 -13
- autobyteus/agent/streaming/stream_events.py +14 -11
- autobyteus/agent/system_prompt_processor/base_processor.py +6 -3
- autobyteus/agent/system_prompt_processor/processor_meta.py +1 -1
- autobyteus/agent/system_prompt_processor/tool_manifest_injector_processor.py +45 -31
- autobyteus/agent/tool_invocation.py +29 -3
- autobyteus/agent/utils/wait_for_idle.py +1 -1
- autobyteus/agent/workspace/__init__.py +2 -0
- autobyteus/agent/workspace/base_workspace.py +33 -11
- autobyteus/agent/workspace/workspace_config.py +160 -0
- autobyteus/agent/workspace/workspace_definition.py +36 -0
- autobyteus/agent/workspace/workspace_meta.py +37 -0
- autobyteus/agent/workspace/workspace_registry.py +72 -0
- autobyteus/cli/__init__.py +4 -3
- autobyteus/cli/agent_cli.py +25 -207
- autobyteus/cli/cli_display.py +205 -0
- autobyteus/events/event_manager.py +2 -1
- autobyteus/events/event_types.py +3 -1
- autobyteus/llm/api/autobyteus_llm.py +2 -12
- autobyteus/llm/api/deepseek_llm.py +11 -173
- autobyteus/llm/api/grok_llm.py +11 -172
- autobyteus/llm/api/kimi_llm.py +24 -0
- autobyteus/llm/api/mistral_llm.py +4 -4
- autobyteus/llm/api/ollama_llm.py +2 -2
- autobyteus/llm/api/openai_compatible_llm.py +193 -0
- autobyteus/llm/api/openai_llm.py +11 -139
- autobyteus/llm/extensions/token_usage_tracking_extension.py +11 -1
- autobyteus/llm/llm_factory.py +168 -42
- autobyteus/llm/models.py +25 -29
- autobyteus/llm/ollama_provider.py +6 -2
- autobyteus/llm/ollama_provider_resolver.py +44 -0
- autobyteus/llm/providers.py +1 -0
- autobyteus/llm/token_counter/kimi_token_counter.py +24 -0
- autobyteus/llm/token_counter/token_counter_factory.py +3 -0
- autobyteus/llm/utils/messages.py +3 -3
- autobyteus/tools/__init__.py +2 -0
- autobyteus/tools/base_tool.py +7 -1
- autobyteus/tools/functional_tool.py +20 -5
- autobyteus/tools/mcp/call_handlers/stdio_handler.py +15 -1
- autobyteus/tools/mcp/config_service.py +106 -127
- autobyteus/tools/mcp/registrar.py +247 -59
- autobyteus/tools/mcp/types.py +5 -3
- autobyteus/tools/registry/tool_definition.py +8 -1
- autobyteus/tools/registry/tool_registry.py +18 -0
- autobyteus/tools/tool_category.py +11 -0
- autobyteus/tools/tool_meta.py +3 -1
- autobyteus/tools/tool_state.py +20 -0
- autobyteus/tools/usage/parsers/_json_extractor.py +99 -0
- autobyteus/tools/usage/parsers/default_json_tool_usage_parser.py +46 -77
- autobyteus/tools/usage/parsers/default_xml_tool_usage_parser.py +87 -96
- autobyteus/tools/usage/parsers/gemini_json_tool_usage_parser.py +37 -47
- autobyteus/tools/usage/parsers/openai_json_tool_usage_parser.py +112 -113
- {autobyteus-1.1.0.dist-info → autobyteus-1.1.2.dist-info}/METADATA +13 -12
- {autobyteus-1.1.0.dist-info → autobyteus-1.1.2.dist-info}/RECORD +103 -82
- {autobyteus-1.1.0.dist-info → autobyteus-1.1.2.dist-info}/WHEEL +0 -0
- {autobyteus-1.1.0.dist-info → autobyteus-1.1.2.dist-info}/licenses/LICENSE +0 -0
- {autobyteus-1.1.0.dist-info → autobyteus-1.1.2.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# file: autobyteus/autobyteus/tools/mcp/registrar.py
|
|
2
2
|
import logging
|
|
3
|
-
from typing import Dict
|
|
3
|
+
from typing import Any, Dict, List, Optional, Union
|
|
4
4
|
|
|
5
5
|
# Import the new handler architecture components
|
|
6
6
|
from .call_handlers import (
|
|
@@ -15,34 +15,32 @@ from autobyteus.tools.mcp import (
|
|
|
15
15
|
McpConfigService,
|
|
16
16
|
McpSchemaMapper,
|
|
17
17
|
McpToolFactory,
|
|
18
|
-
McpTransportType
|
|
18
|
+
McpTransportType,
|
|
19
|
+
BaseMcpConfig
|
|
19
20
|
)
|
|
20
21
|
|
|
21
22
|
from autobyteus.tools.registry import ToolRegistry, ToolDefinition
|
|
23
|
+
from autobyteus.tools.tool_category import ToolCategory
|
|
24
|
+
from autobyteus.utils.singleton import SingletonMeta
|
|
22
25
|
from mcp import types as mcp_types
|
|
23
26
|
|
|
24
27
|
|
|
25
28
|
logger = logging.getLogger(__name__)
|
|
26
29
|
|
|
27
|
-
class McpToolRegistrar:
|
|
30
|
+
class McpToolRegistrar(metaclass=SingletonMeta):
|
|
28
31
|
"""
|
|
29
32
|
Orchestrates the discovery of remote MCP tools and their registration
|
|
30
33
|
with the AutoByteUs ToolRegistry using a handler-based architecture.
|
|
34
|
+
This class is a singleton.
|
|
31
35
|
"""
|
|
32
|
-
def __init__(self
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if not isinstance(tool_registry, ToolRegistry):
|
|
41
|
-
raise TypeError("tool_registry must be a ToolRegistry instance.")
|
|
42
|
-
|
|
43
|
-
self._config_service = config_service
|
|
44
|
-
self._schema_mapper = schema_mapper
|
|
45
|
-
self._tool_registry = tool_registry
|
|
36
|
+
def __init__(self):
|
|
37
|
+
"""
|
|
38
|
+
Initializes the McpToolRegistrar singleton.
|
|
39
|
+
It retrieves singleton instances of its service dependencies and initializes
|
|
40
|
+
its internal state for tracking registered tools.
|
|
41
|
+
"""
|
|
42
|
+
self._config_service: McpConfigService = McpConfigService()
|
|
43
|
+
self._tool_registry: ToolRegistry = ToolRegistry()
|
|
46
44
|
|
|
47
45
|
# The handler registry maps a transport type to a reusable handler instance.
|
|
48
46
|
self._handler_registry: Dict[McpTransportType, McpCallHandler] = {
|
|
@@ -50,35 +48,119 @@ class McpToolRegistrar:
|
|
|
50
48
|
McpTransportType.STREAMABLE_HTTP: StreamableHttpMcpCallHandler(),
|
|
51
49
|
McpTransportType.SSE: SseMcpCallHandler(),
|
|
52
50
|
}
|
|
51
|
+
|
|
52
|
+
# Internal state to track which ToolDefinitions were registered from which server.
|
|
53
|
+
self._registered_tools_by_server: Dict[str, List[ToolDefinition]] = {}
|
|
53
54
|
|
|
54
55
|
logger.info(f"McpToolRegistrar initialized with {len(self._handler_registry)} call handlers.")
|
|
55
56
|
|
|
56
|
-
|
|
57
|
+
def _create_tool_definition_from_remote(
|
|
58
|
+
self,
|
|
59
|
+
remote_tool: mcp_types.Tool,
|
|
60
|
+
server_config: BaseMcpConfig,
|
|
61
|
+
handler: McpCallHandler,
|
|
62
|
+
schema_mapper: McpSchemaMapper
|
|
63
|
+
) -> ToolDefinition:
|
|
57
64
|
"""
|
|
58
|
-
|
|
65
|
+
Maps a single remote tool from an MCP server to an AutoByteUs ToolDefinition.
|
|
66
|
+
This is a helper method to centralize the mapping logic.
|
|
59
67
|
"""
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
68
|
+
if hasattr(remote_tool, 'model_dump_json'):
|
|
69
|
+
logger.debug(f"Processing remote tool from server '{server_config.server_id}':\n{remote_tool.model_dump_json(indent=2)}")
|
|
70
|
+
|
|
71
|
+
actual_arg_schema = schema_mapper.map_to_autobyteus_schema(remote_tool.inputSchema)
|
|
72
|
+
actual_desc = remote_tool.description
|
|
73
|
+
|
|
74
|
+
registered_name = remote_tool.name
|
|
75
|
+
if server_config.tool_name_prefix:
|
|
76
|
+
registered_name = f"{server_config.tool_name_prefix.rstrip('_')}_{remote_tool.name}"
|
|
77
|
+
|
|
78
|
+
tool_factory = McpToolFactory(
|
|
79
|
+
mcp_server_config=server_config,
|
|
80
|
+
mcp_remote_tool_name=remote_tool.name,
|
|
81
|
+
mcp_call_handler=handler,
|
|
82
|
+
registered_tool_name=registered_name,
|
|
83
|
+
tool_description=actual_desc,
|
|
84
|
+
tool_argument_schema=actual_arg_schema
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
tool_def = ToolDefinition(
|
|
88
|
+
name=registered_name,
|
|
89
|
+
description=actual_desc,
|
|
90
|
+
argument_schema=actual_arg_schema,
|
|
91
|
+
category=ToolCategory.MCP,
|
|
92
|
+
custom_factory=tool_factory.create_tool,
|
|
93
|
+
config_schema=None,
|
|
94
|
+
tool_class=None
|
|
95
|
+
)
|
|
96
|
+
return tool_def
|
|
65
97
|
|
|
66
|
-
|
|
67
|
-
|
|
98
|
+
async def discover_and_register_tools(self, mcp_config: Optional[Union[BaseMcpConfig, Dict[str, Any]]] = None) -> List[ToolDefinition]:
|
|
99
|
+
"""
|
|
100
|
+
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.
|
|
112
|
+
"""
|
|
113
|
+
configs_to_process: List[BaseMcpConfig]
|
|
114
|
+
|
|
115
|
+
if mcp_config:
|
|
116
|
+
validated_config: BaseMcpConfig
|
|
117
|
+
# If the user provided a raw dictionary, parse and add it via the service.
|
|
118
|
+
if isinstance(mcp_config, dict):
|
|
119
|
+
try:
|
|
120
|
+
validated_config = self._config_service.load_config(mcp_config)
|
|
121
|
+
except ValueError as e:
|
|
122
|
+
logger.error(f"Failed to parse provided MCP config dictionary: {e}")
|
|
123
|
+
raise
|
|
124
|
+
# If a validated object was passed, add it to the service to ensure it's known.
|
|
125
|
+
elif isinstance(mcp_config, BaseMcpConfig):
|
|
126
|
+
validated_config = self._config_service.add_config(mcp_config)
|
|
127
|
+
else:
|
|
128
|
+
raise TypeError(f"mcp_config must be a BaseMcpConfig object or a dictionary, not {type(mcp_config)}.")
|
|
129
|
+
|
|
130
|
+
logger.info(f"Starting targeted MCP tool discovery for server: {validated_config.server_id}")
|
|
131
|
+
# Unregister any existing tools from this server before re-discovering.
|
|
132
|
+
self.unregister_tools_from_server(validated_config.server_id)
|
|
133
|
+
configs_to_process = [validated_config]
|
|
134
|
+
else:
|
|
135
|
+
logger.info("Starting full MCP tool discovery and registration process. Unregistering all existing MCP tools first.")
|
|
136
|
+
# Get a copy of all server IDs that have tools registered.
|
|
137
|
+
all_server_ids = list(self._registered_tools_by_server.keys())
|
|
138
|
+
for server_id in all_server_ids:
|
|
139
|
+
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
|
+
self._registered_tools_by_server.clear()
|
|
143
|
+
|
|
144
|
+
configs_to_process = self._config_service.get_all_configs()
|
|
145
|
+
|
|
146
|
+
if not configs_to_process:
|
|
147
|
+
logger.info("No MCP server configurations to process. Skipping discovery.")
|
|
148
|
+
return []
|
|
149
|
+
|
|
150
|
+
schema_mapper = McpSchemaMapper()
|
|
151
|
+
registered_tool_definitions: List[ToolDefinition] = []
|
|
152
|
+
for server_config in configs_to_process:
|
|
68
153
|
if not server_config.enabled:
|
|
69
154
|
logger.info(f"MCP server '{server_config.server_id}' is disabled. Skipping.")
|
|
70
155
|
continue
|
|
71
156
|
|
|
72
157
|
logger.info(f"Discovering tools from MCP server: '{server_config.server_id}' ({server_config.transport_type.value})")
|
|
73
158
|
try:
|
|
74
|
-
# Get the correct handler for this server's transport type.
|
|
75
159
|
handler = self._handler_registry.get(server_config.transport_type)
|
|
76
160
|
if not handler:
|
|
77
161
|
logger.error(f"No MCP call handler found for transport type '{server_config.transport_type.value}' on server '{server_config.server_id}'.")
|
|
78
162
|
continue
|
|
79
163
|
|
|
80
|
-
# Use the handler to call the special 'list_tools' method.
|
|
81
|
-
# The registrar does not need to know how this is done; it's encapsulated in the handler.
|
|
82
164
|
remote_tools_result = await handler.handle_call(
|
|
83
165
|
config=server_config,
|
|
84
166
|
remote_tool_name="list_tools",
|
|
@@ -94,42 +176,148 @@ class McpToolRegistrar:
|
|
|
94
176
|
|
|
95
177
|
for remote_tool in actual_remote_tools:
|
|
96
178
|
try:
|
|
97
|
-
|
|
98
|
-
logger.debug(f"Processing remote tool from server '{server_config.server_id}':\n{remote_tool.model_dump_json(indent=2)}")
|
|
99
|
-
|
|
100
|
-
actual_arg_schema = self._schema_mapper.map_to_autobyteus_schema(remote_tool.inputSchema)
|
|
101
|
-
actual_desc = remote_tool.description
|
|
179
|
+
tool_def = self._create_tool_definition_from_remote(remote_tool, server_config, handler, schema_mapper)
|
|
102
180
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
# Create the tool factory, injecting the server config and the correct handler.
|
|
108
|
-
tool_factory = McpToolFactory(
|
|
109
|
-
mcp_server_config=server_config,
|
|
110
|
-
mcp_remote_tool_name=remote_tool.name,
|
|
111
|
-
mcp_call_handler=handler,
|
|
112
|
-
registered_tool_name=registered_name,
|
|
113
|
-
tool_description=actual_desc,
|
|
114
|
-
tool_argument_schema=actual_arg_schema
|
|
115
|
-
)
|
|
181
|
+
self._tool_registry.register_tool(tool_def)
|
|
182
|
+
self._registered_tools_by_server.setdefault(server_config.server_id, []).append(tool_def)
|
|
183
|
+
registered_tool_definitions.append(tool_def)
|
|
116
184
|
|
|
117
|
-
|
|
118
|
-
name=registered_name,
|
|
119
|
-
description=actual_desc,
|
|
120
|
-
argument_schema=actual_arg_schema,
|
|
121
|
-
custom_factory=tool_factory.create_tool,
|
|
122
|
-
config_schema=None,
|
|
123
|
-
tool_class=None
|
|
124
|
-
)
|
|
185
|
+
logger.info(f"Successfully registered MCP tool '{remote_tool.name}' from server '{server_config.server_id}' as '{tool_def.name}'.")
|
|
125
186
|
|
|
126
|
-
self._tool_registry.register_tool(tool_def)
|
|
127
|
-
logger.info(f"Successfully registered MCP tool '{remote_tool.name}' from server '{server_config.server_id}' as '{registered_name}'.")
|
|
128
|
-
registered_count +=1
|
|
129
187
|
except Exception as e_tool:
|
|
130
188
|
logger.error(f"Failed to process or register remote tool '{remote_tool.name}' from server '{server_config.server_id}': {e_tool}", exc_info=True)
|
|
131
189
|
|
|
132
190
|
except Exception as e_server:
|
|
133
191
|
logger.error(f"Failed to discover tools from MCP server '{server_config.server_id}': {e_server}", exc_info=True)
|
|
134
192
|
|
|
135
|
-
logger.info(f"MCP tool discovery and registration process completed. Total tools registered: {
|
|
193
|
+
logger.info(f"MCP tool discovery and registration process completed. Total tools registered: {len(registered_tool_definitions)}.")
|
|
194
|
+
return registered_tool_definitions
|
|
195
|
+
|
|
196
|
+
async def list_remote_tools(self, mcp_config: Union[BaseMcpConfig, Dict[str, Any]]) -> List[ToolDefinition]:
|
|
197
|
+
"""
|
|
198
|
+
Previews tools from a remote MCP server without registering them or storing the configuration.
|
|
199
|
+
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
|
+
"""
|
|
212
|
+
validated_config: BaseMcpConfig
|
|
213
|
+
if isinstance(mcp_config, dict):
|
|
214
|
+
# Use the static method to parse the config without storing it in the service.
|
|
215
|
+
validated_config = McpConfigService.parse_mcp_config_dict(mcp_config)
|
|
216
|
+
elif isinstance(mcp_config, BaseMcpConfig):
|
|
217
|
+
validated_config = mcp_config
|
|
218
|
+
else:
|
|
219
|
+
raise TypeError(f"mcp_config must be a BaseMcpConfig object or a dictionary, not {type(mcp_config)}.")
|
|
220
|
+
|
|
221
|
+
logger.info(f"Previewing tools from MCP server: '{validated_config.server_id}' ({validated_config.transport_type.value})")
|
|
222
|
+
|
|
223
|
+
schema_mapper = McpSchemaMapper()
|
|
224
|
+
tool_definitions: List[ToolDefinition] = []
|
|
225
|
+
|
|
226
|
+
try:
|
|
227
|
+
handler = self._handler_registry.get(validated_config.transport_type)
|
|
228
|
+
if not handler:
|
|
229
|
+
raise ValueError(f"No MCP call handler found for transport type '{validated_config.transport_type.value}'.")
|
|
230
|
+
|
|
231
|
+
remote_tools_result = await handler.handle_call(
|
|
232
|
+
config=validated_config,
|
|
233
|
+
remote_tool_name="list_tools",
|
|
234
|
+
arguments={}
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
if not isinstance(remote_tools_result, mcp_types.ListToolsResult):
|
|
238
|
+
error_msg = f"Expected ListToolsResult from handler, but got {type(remote_tools_result)}. Cannot preview tools from server '{validated_config.server_id}'."
|
|
239
|
+
logger.error(error_msg)
|
|
240
|
+
raise RuntimeError(error_msg)
|
|
241
|
+
|
|
242
|
+
actual_remote_tools: list[mcp_types.Tool] = remote_tools_result.tools
|
|
243
|
+
logger.info(f"Discovered {len(actual_remote_tools)} tools from server '{validated_config.server_id}' for preview.")
|
|
244
|
+
|
|
245
|
+
for remote_tool in actual_remote_tools:
|
|
246
|
+
try:
|
|
247
|
+
tool_def = self._create_tool_definition_from_remote(remote_tool, validated_config, handler, schema_mapper)
|
|
248
|
+
tool_definitions.append(tool_def)
|
|
249
|
+
except Exception as e_tool:
|
|
250
|
+
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
|
+
|
|
253
|
+
except Exception as e_server:
|
|
254
|
+
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
|
+
raise
|
|
257
|
+
|
|
258
|
+
logger.info(f"MCP tool preview completed. Found {len(tool_definitions)} tools.")
|
|
259
|
+
return tool_definitions
|
|
260
|
+
|
|
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
|
+
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
|
+
if not self.is_server_registered(server_id):
|
|
311
|
+
logger.info(f"No tools found for server ID '{server_id}'. Nothing to unregister.")
|
|
312
|
+
return False
|
|
313
|
+
|
|
314
|
+
tools_to_unregister = self._registered_tools_by_server.get(server_id, [])
|
|
315
|
+
logger.info(f"Unregistering {len(tools_to_unregister)} tools from server ID: '{server_id}'...")
|
|
316
|
+
|
|
317
|
+
for tool_def in tools_to_unregister:
|
|
318
|
+
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
|
+
logger.info(f"Successfully unregistered all tools and removed server '{server_id}' from registrar tracking.")
|
|
323
|
+
return True
|
autobyteus/tools/mcp/types.py
CHANGED
|
@@ -44,7 +44,11 @@ class StdioMcpServerConfig(BaseMcpConfig):
|
|
|
44
44
|
super().__post_init__()
|
|
45
45
|
self.transport_type = McpTransportType.STDIO
|
|
46
46
|
|
|
47
|
-
#
|
|
47
|
+
# BUG FIX: Normalize cwd. An empty string is invalid for subprocess creation
|
|
48
|
+
# and should be treated as None (use parent CWD).
|
|
49
|
+
if self.cwd == '':
|
|
50
|
+
self.cwd = None
|
|
51
|
+
|
|
48
52
|
if self.command is None or not isinstance(self.command, str) or not self.command.strip():
|
|
49
53
|
raise ValueError(f"StdioMcpServerConfig '{self.server_id}' 'command' must be a non-empty string.")
|
|
50
54
|
|
|
@@ -66,7 +70,6 @@ class SseMcpServerConfig(BaseMcpConfig):
|
|
|
66
70
|
super().__post_init__()
|
|
67
71
|
self.transport_type = McpTransportType.SSE
|
|
68
72
|
|
|
69
|
-
# Added: Validation for url
|
|
70
73
|
if self.url is None or not isinstance(self.url, str) or not self.url.strip():
|
|
71
74
|
raise ValueError(f"SseMcpServerConfig '{self.server_id}' 'url' must be a non-empty string.")
|
|
72
75
|
|
|
@@ -86,7 +89,6 @@ class StreamableHttpMcpServerConfig(BaseMcpConfig):
|
|
|
86
89
|
super().__post_init__()
|
|
87
90
|
self.transport_type = McpTransportType.STREAMABLE_HTTP
|
|
88
91
|
|
|
89
|
-
# Added: Validation for url
|
|
90
92
|
if self.url is None or not isinstance(self.url, str) or not self.url.strip():
|
|
91
93
|
raise ValueError(f"StreamableHttpMcpServerConfig '{self.server_id}' 'url' must be a non-empty string.")
|
|
92
94
|
|
|
@@ -6,6 +6,7 @@ from typing import Dict, Any, List as TypingList, Type, TYPE_CHECKING, Optional,
|
|
|
6
6
|
from autobyteus.llm.providers import LLMProvider
|
|
7
7
|
from autobyteus.tools.tool_config import ToolConfig
|
|
8
8
|
from autobyteus.tools.parameter_schema import ParameterSchema
|
|
9
|
+
from autobyteus.tools.tool_category import ToolCategory
|
|
9
10
|
from autobyteus.tools.usage.providers import (
|
|
10
11
|
XmlSchemaProvider,
|
|
11
12
|
JsonSchemaProvider,
|
|
@@ -27,6 +28,7 @@ class ToolDefinition:
|
|
|
27
28
|
name: str,
|
|
28
29
|
description: str,
|
|
29
30
|
argument_schema: Optional['ParameterSchema'],
|
|
31
|
+
category: ToolCategory,
|
|
30
32
|
config_schema: Optional['ParameterSchema'] = None,
|
|
31
33
|
tool_class: Optional[Type['BaseTool']] = None,
|
|
32
34
|
custom_factory: Optional[Callable[['ToolConfig'], 'BaseTool']] = None):
|
|
@@ -52,6 +54,8 @@ class ToolDefinition:
|
|
|
52
54
|
raise TypeError(f"ToolDefinition '{name}' received an invalid 'argument_schema'. Expected ParameterSchema or None.")
|
|
53
55
|
if config_schema is not None and not isinstance(config_schema, ParameterSchema):
|
|
54
56
|
raise TypeError(f"ToolDefinition '{name}' received an invalid 'config_schema'. Expected ParameterSchema or None.")
|
|
57
|
+
if not isinstance(category, ToolCategory):
|
|
58
|
+
raise TypeError(f"ToolDefinition '{name}' requires a ToolCategory for 'category'. Got {type(category)}")
|
|
55
59
|
|
|
56
60
|
self._name = name
|
|
57
61
|
self._description = description
|
|
@@ -59,6 +63,7 @@ class ToolDefinition:
|
|
|
59
63
|
self._config_schema: Optional['ParameterSchema'] = config_schema
|
|
60
64
|
self._tool_class = tool_class
|
|
61
65
|
self._custom_factory = custom_factory
|
|
66
|
+
self._category = category
|
|
62
67
|
|
|
63
68
|
logger.debug(f"ToolDefinition created for tool '{self.name}'.")
|
|
64
69
|
|
|
@@ -75,6 +80,8 @@ class ToolDefinition:
|
|
|
75
80
|
def argument_schema(self) -> Optional['ParameterSchema']: return self._argument_schema
|
|
76
81
|
@property
|
|
77
82
|
def config_schema(self) -> Optional['ParameterSchema']: return self._config_schema
|
|
83
|
+
@property
|
|
84
|
+
def category(self) -> ToolCategory: return self._category
|
|
78
85
|
|
|
79
86
|
# --- Schema Generation API ---
|
|
80
87
|
def get_usage_xml(self, provider: Optional[LLMProvider] = None) -> str:
|
|
@@ -129,4 +136,4 @@ class ToolDefinition:
|
|
|
129
136
|
|
|
130
137
|
def __repr__(self) -> str:
|
|
131
138
|
creator_repr = f"class='{self._tool_class.__name__}'" if self._tool_class else "factory=True"
|
|
132
|
-
return (f"ToolDefinition(name='{self.name}', {creator_repr})")
|
|
139
|
+
return (f"ToolDefinition(name='{self.name}', category='{self.category.value}', {creator_repr})")
|
|
@@ -43,6 +43,24 @@ class ToolRegistry(metaclass=SingletonMeta):
|
|
|
43
43
|
ToolRegistry._definitions[tool_name] = definition
|
|
44
44
|
logger.info(f"Successfully registered tool definition: '{tool_name}'")
|
|
45
45
|
|
|
46
|
+
def unregister_tool(self, name: str) -> bool:
|
|
47
|
+
"""
|
|
48
|
+
Unregisters a tool definition by its name.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
name: The unique name of the tool definition to unregister.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
True if the tool was found and unregistered, False otherwise.
|
|
55
|
+
"""
|
|
56
|
+
if name in self._definitions:
|
|
57
|
+
del self._definitions[name]
|
|
58
|
+
logger.info(f"Successfully unregistered tool definition: '{name}'")
|
|
59
|
+
return True
|
|
60
|
+
else:
|
|
61
|
+
logger.warning(f"Attempted to unregister tool '{name}', but it was not found in the registry.")
|
|
62
|
+
return False
|
|
63
|
+
|
|
46
64
|
def get_tool_definition(self, name: str) -> Optional[ToolDefinition]:
|
|
47
65
|
"""
|
|
48
66
|
Retrieves the definition for a specific tool name.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# file: autobyteus/autobyteus/tools/tool_category.py
|
|
2
|
+
from enum import Enum
|
|
3
|
+
|
|
4
|
+
class ToolCategory(str, Enum):
|
|
5
|
+
"""Enumeration of tool categories to identify their origin."""
|
|
6
|
+
LOCAL = "local"
|
|
7
|
+
MCP = "mcp"
|
|
8
|
+
# BUILT_IN, USER_DEFINED etc. could be added later.
|
|
9
|
+
|
|
10
|
+
def __str__(self) -> str:
|
|
11
|
+
return self.value
|
autobyteus/tools/tool_meta.py
CHANGED
|
@@ -5,6 +5,7 @@ from typing import Dict, Any
|
|
|
5
5
|
|
|
6
6
|
from autobyteus.tools.registry import default_tool_registry, ToolDefinition
|
|
7
7
|
from autobyteus.tools.parameter_schema import ParameterSchema
|
|
8
|
+
from autobyteus.tools.tool_category import ToolCategory
|
|
8
9
|
|
|
9
10
|
logger = logging.getLogger(__name__)
|
|
10
11
|
|
|
@@ -60,7 +61,8 @@ class ToolMeta(ABCMeta):
|
|
|
60
61
|
tool_class=cls,
|
|
61
62
|
custom_factory=None,
|
|
62
63
|
argument_schema=argument_schema,
|
|
63
|
-
config_schema=instantiation_config_schema
|
|
64
|
+
config_schema=instantiation_config_schema,
|
|
65
|
+
category=ToolCategory.LOCAL
|
|
64
66
|
)
|
|
65
67
|
default_tool_registry.register_tool(definition)
|
|
66
68
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# file: autobyteus/autobyteus/tools/tool_state.py
|
|
2
|
+
"""
|
|
3
|
+
Defines the ToolState class, an explicit container for a tool's internal state,
|
|
4
|
+
providing a dictionary-like interface for backward compatibility.
|
|
5
|
+
"""
|
|
6
|
+
from collections import UserDict
|
|
7
|
+
|
|
8
|
+
class ToolState(UserDict):
|
|
9
|
+
"""
|
|
10
|
+
A specialized container for a tool's state.
|
|
11
|
+
|
|
12
|
+
This class inherits from collections.UserDict to provide a dictionary-like
|
|
13
|
+
interface, ensuring that existing tools can interact with the state attribute
|
|
14
|
+
(tool.tool_state) just as they would with a regular dictionary.
|
|
15
|
+
|
|
16
|
+
The primary purpose of this class is to make the concept of a tool's
|
|
17
|
+
state explicit in the framework's type system, improving code clarity
|
|
18
|
+
and developer experience.
|
|
19
|
+
"""
|
|
20
|
+
pass
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import logging
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
def _find_json_blobs(text: str) -> List[str]:
|
|
8
|
+
"""
|
|
9
|
+
Robustly finds and extracts all top-level JSON objects or arrays from a string,
|
|
10
|
+
maintaining their original order of appearance. It handles JSON within
|
|
11
|
+
markdown-style code blocks (```json ... ```) and inline JSON.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
text: The string to search for JSON in.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
A list of strings, where each string is a valid-looking JSON blob,
|
|
18
|
+
ordered as they appeared in the input text.
|
|
19
|
+
"""
|
|
20
|
+
found_blobs = []
|
|
21
|
+
|
|
22
|
+
# 1. Find all markdown blobs first and store them with their start positions.
|
|
23
|
+
markdown_matches = list(re.finditer(r"```(?:json)?\s*([\s\S]+?)\s*```", text))
|
|
24
|
+
for match in markdown_matches:
|
|
25
|
+
content = match.group(1).strip()
|
|
26
|
+
found_blobs.append((match.start(), content))
|
|
27
|
+
|
|
28
|
+
# 2. Create a "masked" version of the text by replacing markdown blocks with spaces.
|
|
29
|
+
# This prevents the inline scanner from finding JSON inside them, while preserving indices.
|
|
30
|
+
masked_text_list = list(text)
|
|
31
|
+
for match in markdown_matches:
|
|
32
|
+
for i in range(match.start(), match.end()):
|
|
33
|
+
masked_text_list[i] = ' '
|
|
34
|
+
masked_text = "".join(masked_text_list)
|
|
35
|
+
|
|
36
|
+
# 3. Scan the masked text for any other JSON using a single pass brace-counter.
|
|
37
|
+
idx = 0
|
|
38
|
+
while idx < len(masked_text):
|
|
39
|
+
start_idx = -1
|
|
40
|
+
|
|
41
|
+
# Find the next opening brace or bracket
|
|
42
|
+
next_brace = masked_text.find('{', idx)
|
|
43
|
+
next_bracket = masked_text.find('[', idx)
|
|
44
|
+
|
|
45
|
+
if next_brace == -1 and next_bracket == -1:
|
|
46
|
+
break # No more JSON starts
|
|
47
|
+
|
|
48
|
+
if next_brace != -1 and (next_bracket == -1 or next_brace < next_bracket):
|
|
49
|
+
start_idx = next_brace
|
|
50
|
+
start_char, end_char = '{', '}'
|
|
51
|
+
else:
|
|
52
|
+
start_idx = next_bracket
|
|
53
|
+
start_char, end_char = '[', ']'
|
|
54
|
+
|
|
55
|
+
brace_count = 1
|
|
56
|
+
in_string = False
|
|
57
|
+
is_escaped = False
|
|
58
|
+
end_idx = -1
|
|
59
|
+
|
|
60
|
+
for i in range(start_idx + 1, len(masked_text)):
|
|
61
|
+
char = masked_text[i]
|
|
62
|
+
|
|
63
|
+
if in_string:
|
|
64
|
+
if is_escaped:
|
|
65
|
+
is_escaped = False
|
|
66
|
+
elif char == '\\':
|
|
67
|
+
is_escaped = True
|
|
68
|
+
elif char == '"':
|
|
69
|
+
in_string = False
|
|
70
|
+
else:
|
|
71
|
+
if char == '"':
|
|
72
|
+
in_string = True
|
|
73
|
+
is_escaped = False
|
|
74
|
+
elif char == '{' or char == '[':
|
|
75
|
+
brace_count += 1
|
|
76
|
+
elif char == '}' or char == ']':
|
|
77
|
+
brace_count -= 1
|
|
78
|
+
|
|
79
|
+
if brace_count == 0:
|
|
80
|
+
if (start_char == '{' and char == '}') or \
|
|
81
|
+
(start_char == '[' and char == ']'):
|
|
82
|
+
end_idx = i
|
|
83
|
+
break
|
|
84
|
+
|
|
85
|
+
if end_idx != -1:
|
|
86
|
+
# We found a blob in the masked text, so its indices are correct
|
|
87
|
+
# for the original text. Extract the blob from the original text.
|
|
88
|
+
blob = text[start_idx : end_idx + 1]
|
|
89
|
+
found_blobs.append((start_idx, blob))
|
|
90
|
+
idx = end_idx + 1
|
|
91
|
+
else:
|
|
92
|
+
# No matching end brace found, move on from the start character.
|
|
93
|
+
idx = start_idx + 1
|
|
94
|
+
|
|
95
|
+
# 4. Sort all found blobs by their start position to ensure correct order
|
|
96
|
+
found_blobs.sort(key=lambda item: item[0])
|
|
97
|
+
|
|
98
|
+
# 5. Return only the content of the blobs
|
|
99
|
+
return [content for _, content in found_blobs]
|