hanzo-mcp 0.8.2__py3-none-any.whl → 0.8.4__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.
Potentially problematic release.
This version of hanzo-mcp might be problematic. Click here for more details.
- hanzo_mcp/__init__.py +15 -2
- hanzo_mcp/bridge.py +133 -127
- hanzo_mcp/cli.py +45 -21
- hanzo_mcp/compute_nodes.py +68 -55
- hanzo_mcp/config/settings.py +11 -0
- hanzo_mcp/core/base_agent.py +520 -0
- hanzo_mcp/core/model_registry.py +436 -0
- hanzo_mcp/dev_server.py +3 -2
- hanzo_mcp/server.py +4 -1
- hanzo_mcp/tools/__init__.py +61 -46
- hanzo_mcp/tools/agent/__init__.py +63 -52
- hanzo_mcp/tools/agent/agent_tool.py +12 -1
- hanzo_mcp/tools/agent/cli_tools.py +543 -0
- hanzo_mcp/tools/agent/network_tool.py +11 -55
- hanzo_mcp/tools/agent/unified_cli_tools.py +259 -0
- hanzo_mcp/tools/common/batch_tool.py +2 -0
- hanzo_mcp/tools/common/context.py +3 -1
- hanzo_mcp/tools/config/config_tool.py +121 -9
- hanzo_mcp/tools/filesystem/__init__.py +18 -0
- hanzo_mcp/tools/llm/__init__.py +44 -16
- hanzo_mcp/tools/llm/llm_tool.py +13 -0
- hanzo_mcp/tools/llm/llm_unified.py +911 -0
- hanzo_mcp/tools/shell/__init__.py +7 -1
- hanzo_mcp/tools/shell/auto_background.py +24 -0
- hanzo_mcp/tools/shell/bash_tool.py +14 -28
- hanzo_mcp/tools/shell/zsh_tool.py +266 -0
- hanzo_mcp-0.8.4.dist-info/METADATA +411 -0
- {hanzo_mcp-0.8.2.dist-info → hanzo_mcp-0.8.4.dist-info}/RECORD +31 -25
- hanzo_mcp-0.8.2.dist-info/METADATA +0 -526
- {hanzo_mcp-0.8.2.dist-info → hanzo_mcp-0.8.4.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.8.2.dist-info → hanzo_mcp-0.8.4.dist-info}/entry_points.txt +0 -0
- {hanzo_mcp-0.8.2.dist-info → hanzo_mcp-0.8.4.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"""Unified CLI Tools - DRY implementation using base agent classes.
|
|
2
|
+
|
|
3
|
+
This module provides the single, clean implementation of all CLI tools
|
|
4
|
+
following Python best practices and eliminating all duplication.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from typing import Any, Dict, List, Optional
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from mcp.server import FastMCP
|
|
14
|
+
from mcp.server.fastmcp import Context
|
|
15
|
+
|
|
16
|
+
from ..common.base import BaseTool
|
|
17
|
+
from ...core.base_agent import CLIAgent, AgentConfig
|
|
18
|
+
from ..common.permissions import PermissionManager
|
|
19
|
+
from ...core.model_registry import registry
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class UnifiedCLITool(BaseTool, CLIAgent):
|
|
23
|
+
"""Unified CLI tool that combines BaseTool and CLIAgent functionality.
|
|
24
|
+
|
|
25
|
+
MRO: BaseTool first for proper method resolution order.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
name: str,
|
|
31
|
+
description: str,
|
|
32
|
+
cli_command: str,
|
|
33
|
+
default_model: str,
|
|
34
|
+
permission_manager: Optional[PermissionManager] = None,
|
|
35
|
+
):
|
|
36
|
+
"""Initialize unified CLI tool.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
name: Tool name
|
|
40
|
+
description: Tool description
|
|
41
|
+
cli_command: CLI command to execute
|
|
42
|
+
default_model: Default model to use
|
|
43
|
+
permission_manager: Permission manager for access control
|
|
44
|
+
"""
|
|
45
|
+
# Initialize CLIAgent with config
|
|
46
|
+
config = AgentConfig(model=default_model)
|
|
47
|
+
CLIAgent.__init__(self, config)
|
|
48
|
+
|
|
49
|
+
# Store tool metadata
|
|
50
|
+
self._name = name
|
|
51
|
+
self._description = description
|
|
52
|
+
self._cli_command = cli_command
|
|
53
|
+
self.permission_manager = permission_manager
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def name(self) -> str:
|
|
57
|
+
return self._name
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def description(self) -> str:
|
|
61
|
+
return self._description
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def cli_command(self) -> str:
|
|
65
|
+
return self._cli_command
|
|
66
|
+
|
|
67
|
+
def build_command(self, prompt: str, **kwargs: Any) -> List[str]:
|
|
68
|
+
"""Build the CLI command with model-specific formatting.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
prompt: The prompt
|
|
72
|
+
**kwargs: Additional parameters
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Command arguments list
|
|
76
|
+
"""
|
|
77
|
+
command = [self.cli_command]
|
|
78
|
+
|
|
79
|
+
# Get model config from registry
|
|
80
|
+
model_config = registry.get(self.config.model)
|
|
81
|
+
|
|
82
|
+
# Handle different CLI tool formats
|
|
83
|
+
if self.cli_command == "claude":
|
|
84
|
+
if model_config:
|
|
85
|
+
command.extend(["--model", model_config.full_name])
|
|
86
|
+
# Claude takes prompt via stdin
|
|
87
|
+
return command
|
|
88
|
+
|
|
89
|
+
elif self.cli_command == "openai":
|
|
90
|
+
# OpenAI CLI format
|
|
91
|
+
command.extend(["api", "chat.completions.create"])
|
|
92
|
+
if model_config:
|
|
93
|
+
command.extend(["-m", model_config.full_name])
|
|
94
|
+
command.extend(["-g", "user", prompt])
|
|
95
|
+
return command
|
|
96
|
+
|
|
97
|
+
elif self.cli_command in ["gemini", "grok"]:
|
|
98
|
+
# Simple format: command --model MODEL prompt
|
|
99
|
+
if model_config:
|
|
100
|
+
command.extend(["--model", model_config.full_name])
|
|
101
|
+
command.append(prompt)
|
|
102
|
+
return command
|
|
103
|
+
|
|
104
|
+
elif self.cli_command == "openhands":
|
|
105
|
+
# OpenHands format
|
|
106
|
+
command.extend(["run", prompt])
|
|
107
|
+
if model_config:
|
|
108
|
+
command.extend(["--model", model_config.full_name])
|
|
109
|
+
if self.config.working_dir:
|
|
110
|
+
command.extend(["--workspace", str(self.config.working_dir)])
|
|
111
|
+
return command
|
|
112
|
+
|
|
113
|
+
elif self.cli_command == "hanzo":
|
|
114
|
+
# Hanzo dev format
|
|
115
|
+
command.append("dev")
|
|
116
|
+
if model_config:
|
|
117
|
+
command.extend(["--model", model_config.full_name])
|
|
118
|
+
command.extend(["--prompt", prompt])
|
|
119
|
+
return command
|
|
120
|
+
|
|
121
|
+
elif self.cli_command == "cline":
|
|
122
|
+
# Cline format
|
|
123
|
+
command.append(prompt)
|
|
124
|
+
command.append("--no-interactive")
|
|
125
|
+
return command
|
|
126
|
+
|
|
127
|
+
elif self.cli_command == "aider":
|
|
128
|
+
# Aider format
|
|
129
|
+
if model_config:
|
|
130
|
+
command.extend(["--model", model_config.full_name])
|
|
131
|
+
command.extend(["--message", prompt])
|
|
132
|
+
command.extend(["--yes", "--no-stream"])
|
|
133
|
+
return command
|
|
134
|
+
|
|
135
|
+
elif self.cli_command == "ollama":
|
|
136
|
+
# Ollama format for local models
|
|
137
|
+
command.extend(["run", self.config.model.replace("ollama/", "")])
|
|
138
|
+
command.append(prompt)
|
|
139
|
+
return command
|
|
140
|
+
|
|
141
|
+
# Default format
|
|
142
|
+
command.append(prompt)
|
|
143
|
+
return command
|
|
144
|
+
|
|
145
|
+
async def call(self, ctx: Context[Any, Any, Any], **params: Any) -> str:
|
|
146
|
+
"""Execute the CLI tool via MCP interface.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
ctx: MCP context
|
|
150
|
+
**params: Tool parameters
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Execution result
|
|
154
|
+
"""
|
|
155
|
+
# Update config from params
|
|
156
|
+
if params.get("model"):
|
|
157
|
+
self.config.model = registry.resolve(params["model"])
|
|
158
|
+
if params.get("working_dir"):
|
|
159
|
+
self.config.working_dir = Path(params["working_dir"])
|
|
160
|
+
if params.get("timeout"):
|
|
161
|
+
self.config.timeout = params["timeout"]
|
|
162
|
+
|
|
163
|
+
# Execute using base agent
|
|
164
|
+
result = await self.execute(
|
|
165
|
+
params.get("prompt", ""),
|
|
166
|
+
context=ctx,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
return result.content
|
|
170
|
+
|
|
171
|
+
def register(self, mcp_server: FastMCP) -> None:
|
|
172
|
+
"""Register this tool with the MCP server.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
mcp_server: The FastMCP server instance
|
|
176
|
+
"""
|
|
177
|
+
tool_self = self
|
|
178
|
+
|
|
179
|
+
@mcp_server.tool(name=self.name, description=self.description)
|
|
180
|
+
async def tool_wrapper(
|
|
181
|
+
prompt: str,
|
|
182
|
+
ctx: Context[Any, Any, Any],
|
|
183
|
+
model: Optional[str] = None,
|
|
184
|
+
working_dir: Optional[str] = None,
|
|
185
|
+
timeout: int = 300,
|
|
186
|
+
) -> str:
|
|
187
|
+
return await tool_self.call(
|
|
188
|
+
ctx,
|
|
189
|
+
prompt=prompt,
|
|
190
|
+
model=model,
|
|
191
|
+
working_dir=working_dir,
|
|
192
|
+
timeout=timeout,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def create_cli_tools(permission_manager: Optional[PermissionManager] = None) -> Dict[str, UnifiedCLITool]:
|
|
197
|
+
"""Create all CLI tools with unified implementation.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
permission_manager: Permission manager for access control
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
Dictionary of tool name to tool instance
|
|
204
|
+
"""
|
|
205
|
+
tools = {}
|
|
206
|
+
|
|
207
|
+
# Define all tools with their configurations
|
|
208
|
+
tool_configs = [
|
|
209
|
+
("claude", "Execute Claude CLI for AI assistance", "claude", "claude"),
|
|
210
|
+
("cc", "Claude Code CLI (alias for claude)", "claude", "claude"),
|
|
211
|
+
("codex", "Execute OpenAI Codex/GPT-4 CLI", "openai", "gpt-4-turbo"),
|
|
212
|
+
("gemini", "Execute Google Gemini CLI", "gemini", "gemini"),
|
|
213
|
+
("grok", "Execute xAI Grok CLI", "grok", "grok"),
|
|
214
|
+
("openhands", "Execute OpenHands for autonomous coding", "openhands", "claude"),
|
|
215
|
+
("oh", "OpenHands CLI (alias)", "openhands", "claude"),
|
|
216
|
+
("hanzo_dev", "Execute Hanzo Dev AI assistant", "hanzo", "claude"),
|
|
217
|
+
("cline", "Execute Cline for autonomous coding", "cline", "claude"),
|
|
218
|
+
("aider", "Execute Aider for AI pair programming", "aider", "gpt-4-turbo"),
|
|
219
|
+
]
|
|
220
|
+
|
|
221
|
+
for name, description, cli_command, default_model in tool_configs:
|
|
222
|
+
tools[name] = UnifiedCLITool(
|
|
223
|
+
name=name,
|
|
224
|
+
description=description,
|
|
225
|
+
cli_command=cli_command,
|
|
226
|
+
default_model=default_model,
|
|
227
|
+
permission_manager=permission_manager,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
return tools
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def register_cli_tools(
|
|
234
|
+
mcp_server: FastMCP,
|
|
235
|
+
permission_manager: Optional[PermissionManager] = None,
|
|
236
|
+
) -> List[BaseTool]:
|
|
237
|
+
"""Register all CLI tools with the MCP server.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
mcp_server: The FastMCP server instance
|
|
241
|
+
permission_manager: Permission manager for access control
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
List of registered CLI tools
|
|
245
|
+
"""
|
|
246
|
+
tools = create_cli_tools(permission_manager)
|
|
247
|
+
|
|
248
|
+
# Register each tool
|
|
249
|
+
for tool in tools.values():
|
|
250
|
+
tool.register(mcp_server)
|
|
251
|
+
|
|
252
|
+
return list(tools.values())
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
__all__ = [
|
|
256
|
+
"UnifiedCLITool",
|
|
257
|
+
"create_cli_tools",
|
|
258
|
+
"register_cli_tools",
|
|
259
|
+
]
|
|
@@ -140,6 +140,8 @@ To make a batch call, provide the following:
|
|
|
140
140
|
|
|
141
141
|
Available tools in batch call:
|
|
142
142
|
Tool: dispatch_agent,read,directory_tree,grep,grep_ast,run_command,notebook_read
|
|
143
|
+
CLI Tools: claude,cc,codex,gemini,grok,openhands,oh,hanzo_dev,cline,aider
|
|
144
|
+
AST/Symbols: ast,symbols (tree-sitter based code analysis)
|
|
143
145
|
Not available: think,write,edit,multi_edit,notebook_edit
|
|
144
146
|
"""
|
|
145
147
|
|
|
@@ -67,7 +67,9 @@ class ToolContext:
|
|
|
67
67
|
"""
|
|
68
68
|
return self._mcp_context.client_id
|
|
69
69
|
|
|
70
|
-
def set_tool_info(
|
|
70
|
+
async def set_tool_info(
|
|
71
|
+
self, tool_name: str, execution_id: str | None = None
|
|
72
|
+
) -> None:
|
|
71
73
|
"""Set information about the currently executing tool.
|
|
72
74
|
|
|
73
75
|
Args:
|
|
@@ -4,10 +4,12 @@ Git-style config tool for managing settings.
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
from typing import Unpack, Optional, Annotated, TypedDict, final, override
|
|
7
|
+
from pathlib import Path
|
|
7
8
|
|
|
8
9
|
from pydantic import Field
|
|
9
10
|
from mcp.server.fastmcp import Context as MCPContext
|
|
10
11
|
|
|
12
|
+
from hanzo_mcp.config import load_settings, save_settings
|
|
11
13
|
from hanzo_mcp.tools.common.base import BaseTool
|
|
12
14
|
from hanzo_mcp.tools.common.permissions import PermissionManager
|
|
13
15
|
from hanzo_mcp.tools.config.index_config import IndexScope, IndexConfig
|
|
@@ -24,7 +26,7 @@ Action = Annotated[
|
|
|
24
26
|
Key = Annotated[
|
|
25
27
|
Optional[str],
|
|
26
28
|
Field(
|
|
27
|
-
description="Configuration key (e.g.,
|
|
29
|
+
description="Configuration key (e.g., tools.write.enabled, enabled_tools.write, index.scope)",
|
|
28
30
|
default=None,
|
|
29
31
|
),
|
|
30
32
|
]
|
|
@@ -64,6 +66,15 @@ class ConfigParams(TypedDict, total=False):
|
|
|
64
66
|
path: Optional[str]
|
|
65
67
|
|
|
66
68
|
|
|
69
|
+
def _parse_bool(value: str) -> Optional[bool]:
|
|
70
|
+
v = value.strip().lower()
|
|
71
|
+
if v in {"true", "1", "yes", "on"}:
|
|
72
|
+
return True
|
|
73
|
+
if v in {"false", "0", "no", "off"}:
|
|
74
|
+
return False
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
|
|
67
78
|
@final
|
|
68
79
|
class ConfigTool(BaseTool):
|
|
69
80
|
"""Git-style configuration management tool."""
|
|
@@ -88,6 +99,7 @@ class ConfigTool(BaseTool):
|
|
|
88
99
|
Usage:
|
|
89
100
|
config index.scope
|
|
90
101
|
config --action set index.scope project
|
|
102
|
+
config --action set tools.write.enabled false
|
|
91
103
|
config --action list
|
|
92
104
|
config --action toggle index.scope --path ./project"""
|
|
93
105
|
|
|
@@ -131,7 +143,22 @@ config --action toggle index.scope --path ./project"""
|
|
|
131
143
|
current_scope = self.index_config.get_scope(path)
|
|
132
144
|
return f"index.scope={current_scope.value}"
|
|
133
145
|
|
|
134
|
-
#
|
|
146
|
+
# tools.<name>.enabled → enabled_tools lookup
|
|
147
|
+
if key.startswith("tools.") and key.endswith(".enabled"):
|
|
148
|
+
parts = key.split(".")
|
|
149
|
+
if len(parts) == 3:
|
|
150
|
+
tool_name = parts[1]
|
|
151
|
+
settings = load_settings(project_dir=path if scope == "local" else None)
|
|
152
|
+
return f"{key}={settings.is_tool_enabled(tool_name)}"
|
|
153
|
+
|
|
154
|
+
# enabled_tools.<name>
|
|
155
|
+
if key.startswith("enabled_tools."):
|
|
156
|
+
tool_name = key.split(".", 1)[1]
|
|
157
|
+
settings = load_settings(project_dir=path if scope == "local" else None)
|
|
158
|
+
val = settings.enabled_tools.get(tool_name)
|
|
159
|
+
return f"{key}={val if val is not None else 'unset'}"
|
|
160
|
+
|
|
161
|
+
# Indexing (legacy) per-tool setting: <tool>.enabled
|
|
135
162
|
if "." in key:
|
|
136
163
|
tool, setting = key.split(".", 1)
|
|
137
164
|
if setting == "enabled":
|
|
@@ -140,6 +167,17 @@ config --action toggle index.scope --path ./project"""
|
|
|
140
167
|
|
|
141
168
|
return f"Unknown key: {key}"
|
|
142
169
|
|
|
170
|
+
def _save_project_settings(self, settings, project_dir: Optional[str]) -> Path:
|
|
171
|
+
"""Save to project config if path provided; else global."""
|
|
172
|
+
if project_dir:
|
|
173
|
+
project_path = Path(project_dir)
|
|
174
|
+
project_path.mkdir(parents=True, exist_ok=True)
|
|
175
|
+
cfg = project_path / ".hanzo-mcp.json"
|
|
176
|
+
cfg.write_text(__import__("json").dumps(settings.__dict__ if hasattr(settings, "__dict__") else {}, indent=2))
|
|
177
|
+
return cfg
|
|
178
|
+
# Fallback to global handler
|
|
179
|
+
return save_settings(settings, global_config=True)
|
|
180
|
+
|
|
143
181
|
async def _handle_set(
|
|
144
182
|
self,
|
|
145
183
|
key: Optional[str],
|
|
@@ -151,7 +189,7 @@ config --action toggle index.scope --path ./project"""
|
|
|
151
189
|
"""Set configuration value."""
|
|
152
190
|
if not key:
|
|
153
191
|
return "Error: key required for set action"
|
|
154
|
-
if
|
|
192
|
+
if value is None:
|
|
155
193
|
return "Error: value required for set action"
|
|
156
194
|
|
|
157
195
|
# Handle index scope
|
|
@@ -165,13 +203,54 @@ config --action toggle index.scope --path ./project"""
|
|
|
165
203
|
except ValueError:
|
|
166
204
|
return f"Error: Invalid scope value '{value}'. Valid: project, global, auto"
|
|
167
205
|
|
|
168
|
-
#
|
|
206
|
+
# tools.<name>.enabled → enabled_tools mapping
|
|
207
|
+
if key.startswith("tools.") and key.endswith(".enabled"):
|
|
208
|
+
parts = key.split(".")
|
|
209
|
+
if len(parts) == 3:
|
|
210
|
+
tool_name = parts[1]
|
|
211
|
+
parsed = _parse_bool(value)
|
|
212
|
+
if parsed is None:
|
|
213
|
+
return "Error: value must be boolean (true/false)"
|
|
214
|
+
settings = load_settings(project_dir=path if scope == "local" else None)
|
|
215
|
+
et = dict(settings.enabled_tools)
|
|
216
|
+
et[tool_name] = parsed
|
|
217
|
+
settings.enabled_tools = et
|
|
218
|
+
# Save
|
|
219
|
+
if scope == "local" and path:
|
|
220
|
+
# Write a project .hanzo-mcp.json adjacent to the path
|
|
221
|
+
# Note: save_settings(local) saves to CWD; we target specific path here
|
|
222
|
+
out = self._save_project_settings(settings, path)
|
|
223
|
+
return f"Set {key}={parsed} (project: {out})"
|
|
224
|
+
else:
|
|
225
|
+
out = save_settings(settings, global_config=True)
|
|
226
|
+
return f"Set {key}={parsed} (global: {out})"
|
|
227
|
+
|
|
228
|
+
# enabled_tools.<name>
|
|
229
|
+
if key.startswith("enabled_tools."):
|
|
230
|
+
tool_name = key.split(".", 1)[1]
|
|
231
|
+
parsed = _parse_bool(value)
|
|
232
|
+
if parsed is None:
|
|
233
|
+
return "Error: value must be boolean (true/false)"
|
|
234
|
+
settings = load_settings(project_dir=path if scope == "local" else None)
|
|
235
|
+
et = dict(settings.enabled_tools)
|
|
236
|
+
et[tool_name] = parsed
|
|
237
|
+
settings.enabled_tools = et
|
|
238
|
+
if scope == "local" and path:
|
|
239
|
+
out = self._save_project_settings(settings, path)
|
|
240
|
+
return f"Set {key}={parsed} (project: {out})"
|
|
241
|
+
else:
|
|
242
|
+
out = save_settings(settings, global_config=True)
|
|
243
|
+
return f"Set {key}={parsed} (global: {out})"
|
|
244
|
+
|
|
245
|
+
# Indexing (legacy) per-tool setting: <tool>.enabled (search indexers)
|
|
169
246
|
if "." in key:
|
|
170
247
|
tool, setting = key.split(".", 1)
|
|
171
248
|
if setting == "enabled":
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
249
|
+
parsed = _parse_bool(value)
|
|
250
|
+
if parsed is None:
|
|
251
|
+
return "Error: value must be boolean (true/false)"
|
|
252
|
+
self.index_config.set_indexing_enabled(tool, parsed)
|
|
253
|
+
return f"Set {key}={parsed}"
|
|
175
254
|
|
|
176
255
|
return f"Unknown key: {key}"
|
|
177
256
|
|
|
@@ -188,12 +267,18 @@ config --action toggle index.scope --path ./project"""
|
|
|
188
267
|
|
|
189
268
|
output.append(f"\nProjects with custom config: {status['project_count']}")
|
|
190
269
|
|
|
191
|
-
output.append("\nTool settings:")
|
|
270
|
+
output.append("\nTool settings (indexing):")
|
|
192
271
|
for tool, settings in status["tools"].items():
|
|
193
272
|
output.append(f" {tool}:")
|
|
194
273
|
output.append(f" enabled: {settings['enabled']}")
|
|
195
274
|
output.append(f" per_project: {settings['per_project']}")
|
|
196
275
|
|
|
276
|
+
# Also show enabled_tools snapshot
|
|
277
|
+
settings_snapshot = load_settings(project_dir=path if scope == "local" else None)
|
|
278
|
+
output.append("\nEnabled tools (execution):")
|
|
279
|
+
for tool_name, enabled in sorted(settings_snapshot.enabled_tools.items()):
|
|
280
|
+
output.append(f" {tool_name}: {enabled}")
|
|
281
|
+
|
|
197
282
|
return "\n".join(output)
|
|
198
283
|
|
|
199
284
|
async def _handle_toggle(
|
|
@@ -210,7 +295,34 @@ config --action toggle index.scope --path ./project"""
|
|
|
210
295
|
)
|
|
211
296
|
return f"Toggled index.scope to {new_scope.value}"
|
|
212
297
|
|
|
213
|
-
# Handle tool enable/disable
|
|
298
|
+
# Handle execution tool enable/disable: tools.<name>.enabled or enabled_tools.<name>
|
|
299
|
+
if key.startswith("tools.") and key.endswith(".enabled"):
|
|
300
|
+
parts = key.split(".")
|
|
301
|
+
if len(parts) == 3:
|
|
302
|
+
tool_name = parts[1]
|
|
303
|
+
settings = load_settings(project_dir=path if scope == "local" else None)
|
|
304
|
+
current = bool(settings.enabled_tools.get(tool_name, True))
|
|
305
|
+
settings.enabled_tools[tool_name] = not current
|
|
306
|
+
if scope == "local" and path:
|
|
307
|
+
out = self._save_project_settings(settings, path)
|
|
308
|
+
return f"Toggled {key} to {not current} (project: {out})"
|
|
309
|
+
else:
|
|
310
|
+
out = save_settings(settings, global_config=True)
|
|
311
|
+
return f"Toggled {key} to {not current} (global: {out})"
|
|
312
|
+
|
|
313
|
+
if key.startswith("enabled_tools."):
|
|
314
|
+
tool_name = key.split(".", 1)[1]
|
|
315
|
+
settings = load_settings(project_dir=path if scope == "local" else None)
|
|
316
|
+
current = bool(settings.enabled_tools.get(tool_name, True))
|
|
317
|
+
settings.enabled_tools[tool_name] = not current
|
|
318
|
+
if scope == "local" and path:
|
|
319
|
+
out = self._save_project_settings(settings, path)
|
|
320
|
+
return f"Toggled {key} to {not current} (project: {out})"
|
|
321
|
+
else:
|
|
322
|
+
out = save_settings(settings, global_config=True)
|
|
323
|
+
return f"Toggled {key} to {not current} (global: {out})"
|
|
324
|
+
|
|
325
|
+
# Handle indexing toggles (legacy)
|
|
214
326
|
if "." in key:
|
|
215
327
|
tool, setting = key.split(".", 1)
|
|
216
328
|
if setting == "enabled":
|
|
@@ -221,4 +221,22 @@ def register_filesystem_tools(
|
|
|
221
221
|
tools = get_filesystem_tools(permission_manager, project_manager)
|
|
222
222
|
|
|
223
223
|
ToolRegistry.register_tools(mcp_server, tools)
|
|
224
|
+
|
|
225
|
+
# Register 'symbols' as an alias for 'ast' to match common terminology
|
|
226
|
+
try:
|
|
227
|
+
ast_tool = next((t for t in tools if getattr(t, "name", "") == "ast"), None)
|
|
228
|
+
if ast_tool is not None:
|
|
229
|
+
class _SymbolsAlias(ASTTool): # type: ignore[misc]
|
|
230
|
+
@property
|
|
231
|
+
def name(self) -> str: # type: ignore[override]
|
|
232
|
+
return "symbols"
|
|
233
|
+
|
|
234
|
+
@property
|
|
235
|
+
def description(self) -> str: # type: ignore[override]
|
|
236
|
+
return f"Alias of 'ast' tool.\n\n{ast_tool.description}"
|
|
237
|
+
|
|
238
|
+
ToolRegistry.register_tool(mcp_server, _SymbolsAlias(permission_manager))
|
|
239
|
+
except Exception:
|
|
240
|
+
# Alias is best-effort; don't fail tool registration if aliasing fails
|
|
241
|
+
pass
|
|
224
242
|
return tools
|
hanzo_mcp/tools/llm/__init__.py
CHANGED
|
@@ -1,22 +1,50 @@
|
|
|
1
|
-
"""LLM tools for Hanzo AI.
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
1
|
+
"""LLM tools for Hanzo AI.
|
|
2
|
+
|
|
3
|
+
This package exposes LLM-related tools. Imports are guarded to keep the
|
|
4
|
+
package importable on environments lacking optional dependencies or newer
|
|
5
|
+
typing features.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
try: # pragma: no cover - guard heavy imports
|
|
9
|
+
from hanzo_mcp.tools.llm.llm_tool import LLMTool
|
|
10
|
+
except Exception: # pragma: no cover
|
|
11
|
+
LLMTool = None # type: ignore
|
|
12
|
+
|
|
13
|
+
try: # pragma: no cover
|
|
14
|
+
from hanzo_mcp.tools.llm.llm_manage import LLMManageTool
|
|
15
|
+
except Exception:
|
|
16
|
+
LLMManageTool = None # type: ignore
|
|
17
|
+
|
|
18
|
+
try: # pragma: no cover
|
|
19
|
+
from hanzo_mcp.tools.llm.consensus_tool import ConsensusTool
|
|
20
|
+
except Exception:
|
|
21
|
+
ConsensusTool = None # type: ignore
|
|
22
|
+
|
|
23
|
+
try: # pragma: no cover
|
|
24
|
+
from hanzo_mcp.tools.llm.llm_unified import UnifiedLLMTool
|
|
25
|
+
except Exception:
|
|
26
|
+
UnifiedLLMTool = None # type: ignore
|
|
27
|
+
|
|
28
|
+
try: # pragma: no cover
|
|
29
|
+
from hanzo_mcp.tools.llm.provider_tools import (
|
|
30
|
+
GroqTool,
|
|
31
|
+
GeminiTool,
|
|
32
|
+
OpenAITool,
|
|
33
|
+
MistralTool,
|
|
34
|
+
AnthropicTool,
|
|
35
|
+
PerplexityTool,
|
|
36
|
+
create_provider_tools,
|
|
37
|
+
)
|
|
38
|
+
except Exception: # pragma: no cover
|
|
39
|
+
GroqTool = GeminiTool = OpenAITool = MistralTool = AnthropicTool = PerplexityTool = None # type: ignore
|
|
40
|
+
|
|
41
|
+
def create_provider_tools(*args, **kwargs): # type: ignore
|
|
42
|
+
return []
|
|
43
|
+
|
|
16
44
|
|
|
17
45
|
__all__ = [
|
|
18
46
|
"LLMTool",
|
|
19
|
-
"
|
|
47
|
+
"UnifiedLLMTool",
|
|
20
48
|
"ConsensusTool",
|
|
21
49
|
"LLMManageTool",
|
|
22
50
|
"create_provider_tools",
|
hanzo_mcp/tools/llm/llm_tool.py
CHANGED
|
@@ -280,6 +280,19 @@ Available: {", ".join(available) if available else "None"}"""
|
|
|
280
280
|
# Running in test mode without MCP context
|
|
281
281
|
pass
|
|
282
282
|
|
|
283
|
+
# Fast test path: allow offline deterministic responses
|
|
284
|
+
if os.getenv("HANZO_MCP_FAST_TESTS") == "1":
|
|
285
|
+
action = params.get("action", "query")
|
|
286
|
+
if action == "query":
|
|
287
|
+
prompt = params.get("prompt", "") or ""
|
|
288
|
+
# Simple keyword routing for tests
|
|
289
|
+
if "generate tasks" in prompt.lower():
|
|
290
|
+
return '{"project": "Demo", "tasks": [{"title": "Init repo", "slug": "init-repo"}, {"title": "Add CI", "slug": "add-ci"}]}'
|
|
291
|
+
return "Architecture Proposal: minimal service layers and tests"
|
|
292
|
+
elif action == "consensus":
|
|
293
|
+
return "Consensus: aligned"
|
|
294
|
+
# Other actions can fall through to regular path if available
|
|
295
|
+
|
|
283
296
|
if not LITELLM_AVAILABLE:
|
|
284
297
|
return (
|
|
285
298
|
"Error: LiteLLM is not installed. Install it with: pip install litellm"
|