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
|
@@ -9,6 +9,7 @@ from hanzo_mcp.tools.shell.open import open_tool
|
|
|
9
9
|
from hanzo_mcp.tools.common.base import BaseTool, ToolRegistry
|
|
10
10
|
from hanzo_mcp.tools.shell.npx_tool import npx_tool
|
|
11
11
|
from hanzo_mcp.tools.shell.uvx_tool import uvx_tool
|
|
12
|
+
from hanzo_mcp.tools.shell.zsh_tool import zsh_tool, shell_tool
|
|
12
13
|
|
|
13
14
|
# Import tools
|
|
14
15
|
from hanzo_mcp.tools.shell.bash_tool import bash_tool
|
|
@@ -37,14 +38,19 @@ def get_shell_tools(
|
|
|
37
38
|
"""
|
|
38
39
|
# Set permission manager for tools that need it
|
|
39
40
|
bash_tool.permission_manager = permission_manager
|
|
41
|
+
zsh_tool.permission_manager = permission_manager
|
|
42
|
+
shell_tool.permission_manager = permission_manager
|
|
40
43
|
npx_tool.permission_manager = permission_manager
|
|
41
44
|
uvx_tool.permission_manager = permission_manager
|
|
42
45
|
|
|
43
46
|
# Note: StreamingCommandTool is abstract and shouldn't be instantiated directly
|
|
44
47
|
# It's used as a base class for other streaming tools
|
|
45
48
|
|
|
49
|
+
# Return shell_tool first (smart default), then specific shells
|
|
46
50
|
return [
|
|
47
|
-
|
|
51
|
+
shell_tool, # Smart shell (prefers zsh if available)
|
|
52
|
+
zsh_tool, # Explicit zsh
|
|
53
|
+
bash_tool, # Explicit bash
|
|
48
54
|
npx_tool,
|
|
49
55
|
uvx_tool,
|
|
50
56
|
process_tool,
|
|
@@ -49,6 +49,30 @@ class AutoBackgroundExecutor:
|
|
|
49
49
|
Returns:
|
|
50
50
|
Tuple of (output/status, was_backgrounded, process_id)
|
|
51
51
|
"""
|
|
52
|
+
# Fast path for tests/offline: run synchronously
|
|
53
|
+
import os
|
|
54
|
+
|
|
55
|
+
if os.getenv("HANZO_MCP_FAST_TESTS") == "1":
|
|
56
|
+
import subprocess
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
proc = subprocess.run(
|
|
60
|
+
cmd_args,
|
|
61
|
+
cwd=str(cwd) if cwd else None,
|
|
62
|
+
env=env,
|
|
63
|
+
capture_output=True,
|
|
64
|
+
text=True,
|
|
65
|
+
)
|
|
66
|
+
if proc.returncode != 0:
|
|
67
|
+
return (
|
|
68
|
+
f"Command failed with exit code {proc.returncode}:\n{proc.stdout}{proc.stderr}",
|
|
69
|
+
False,
|
|
70
|
+
None,
|
|
71
|
+
)
|
|
72
|
+
return proc.stdout, False, None
|
|
73
|
+
except Exception as e: # pragma: no cover
|
|
74
|
+
return f"Error executing command: {e}", False, None
|
|
75
|
+
|
|
52
76
|
# Generate process ID
|
|
53
77
|
process_id = f"{tool_name}_{uuid.uuid4().hex[:8]}"
|
|
54
78
|
|
|
@@ -59,29 +59,20 @@ bash "npm run dev" --cwd ./frontend # Auto-backgrounds if needed"""
|
|
|
59
59
|
|
|
60
60
|
@override
|
|
61
61
|
def get_interpreter(self) -> str:
|
|
62
|
-
"""Get the
|
|
62
|
+
"""Get the bash interpreter."""
|
|
63
63
|
if platform.system() == "Windows":
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if zshrc_path.exists():
|
|
77
|
-
return shell # Use full path to zsh
|
|
78
|
-
elif shell_name == "fish":
|
|
79
|
-
# Check for fish config
|
|
80
|
-
fish_config = Path.home() / ".config" / "fish" / "config.fish"
|
|
81
|
-
if fish_config.exists():
|
|
82
|
-
return shell # Use full path to fish
|
|
83
|
-
|
|
84
|
-
# Default to bash if no special shell config found
|
|
64
|
+
# Try to find bash on Windows (Git Bash, WSL, etc.)
|
|
65
|
+
bash_paths = [
|
|
66
|
+
"C:\\Program Files\\Git\\bin\\bash.exe",
|
|
67
|
+
"C:\\cygwin64\\bin\\bash.exe",
|
|
68
|
+
"C:\\msys64\\usr\\bin\\bash.exe",
|
|
69
|
+
]
|
|
70
|
+
for path in bash_paths:
|
|
71
|
+
if Path(path).exists():
|
|
72
|
+
return path
|
|
73
|
+
return "cmd.exe" # Fall back to cmd if no bash found
|
|
74
|
+
|
|
75
|
+
# On Unix-like systems, always use bash
|
|
85
76
|
return "bash"
|
|
86
77
|
|
|
87
78
|
@override
|
|
@@ -94,12 +85,7 @@ bash "npm run dev" --cwd ./frontend # Auto-backgrounds if needed"""
|
|
|
94
85
|
@override
|
|
95
86
|
def get_tool_name(self) -> str:
|
|
96
87
|
"""Get the tool name."""
|
|
97
|
-
|
|
98
|
-
return "shell"
|
|
99
|
-
|
|
100
|
-
# Return the actual shell being used
|
|
101
|
-
interpreter = self.get_interpreter()
|
|
102
|
-
return os.path.basename(interpreter)
|
|
88
|
+
return "bash"
|
|
103
89
|
|
|
104
90
|
@override
|
|
105
91
|
async def run(
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""Zsh shell tool for command execution with enhanced features."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import platform
|
|
6
|
+
from typing import Optional, override
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from mcp.server import FastMCP
|
|
10
|
+
from mcp.server.fastmcp import Context as MCPContext
|
|
11
|
+
|
|
12
|
+
from hanzo_mcp.tools.shell.base_process import BaseScriptTool
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ZshTool(BaseScriptTool):
|
|
16
|
+
"""Tool for running commands in Zsh shell with enhanced features."""
|
|
17
|
+
|
|
18
|
+
name = "zsh"
|
|
19
|
+
|
|
20
|
+
def register(self, server: FastMCP) -> None:
|
|
21
|
+
"""Register the tool with the MCP server."""
|
|
22
|
+
tool_self = self
|
|
23
|
+
|
|
24
|
+
@server.tool(name=self.name, description=self.description)
|
|
25
|
+
async def zsh(
|
|
26
|
+
ctx: MCPContext,
|
|
27
|
+
command: str,
|
|
28
|
+
cwd: Optional[str] = None,
|
|
29
|
+
env: Optional[dict[str, str]] = None,
|
|
30
|
+
timeout: Optional[int] = None,
|
|
31
|
+
) -> str:
|
|
32
|
+
return await tool_self.run(
|
|
33
|
+
ctx, command=command, cwd=cwd, env=env, timeout=timeout
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
async def call(self, ctx: MCPContext, **params) -> str:
|
|
37
|
+
"""Call the tool with arguments."""
|
|
38
|
+
return await self.run(
|
|
39
|
+
ctx,
|
|
40
|
+
command=params["command"],
|
|
41
|
+
cwd=params.get("cwd"),
|
|
42
|
+
env=params.get("env"),
|
|
43
|
+
timeout=params.get("timeout"),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
@override
|
|
48
|
+
def description(self) -> str:
|
|
49
|
+
"""Get the tool description."""
|
|
50
|
+
return """Run commands in Zsh shell with enhanced features like better globbing and completion.
|
|
51
|
+
|
|
52
|
+
Zsh provides advanced features over bash:
|
|
53
|
+
- Extended globbing patterns
|
|
54
|
+
- Better tab completion
|
|
55
|
+
- Array and associative array support
|
|
56
|
+
- Powerful command line editing
|
|
57
|
+
- Plugin ecosystem (oh-my-zsh, etc.)
|
|
58
|
+
|
|
59
|
+
Commands that run for more than 2 minutes will automatically continue in the background.
|
|
60
|
+
|
|
61
|
+
Usage:
|
|
62
|
+
zsh "ls -la"
|
|
63
|
+
zsh "echo $ZSH_VERSION"
|
|
64
|
+
zsh "git status && git diff"
|
|
65
|
+
zsh "npm run dev" --cwd ./frontend # Auto-backgrounds if needed"""
|
|
66
|
+
|
|
67
|
+
@override
|
|
68
|
+
def get_interpreter(self) -> str:
|
|
69
|
+
"""Get the zsh interpreter path."""
|
|
70
|
+
if platform.system() == "Windows":
|
|
71
|
+
# Try to find zsh on Windows (WSL, Git Bash, etc.)
|
|
72
|
+
zsh_paths = [
|
|
73
|
+
"C:\\Program Files\\Git\\usr\\bin\\zsh.exe",
|
|
74
|
+
"C:\\cygwin64\\bin\\zsh.exe",
|
|
75
|
+
"C:\\msys64\\usr\\bin\\zsh.exe",
|
|
76
|
+
]
|
|
77
|
+
for path in zsh_paths:
|
|
78
|
+
if Path(path).exists():
|
|
79
|
+
return path
|
|
80
|
+
# Fall back to bash if no zsh found
|
|
81
|
+
return "bash"
|
|
82
|
+
|
|
83
|
+
# On Unix-like systems, check for zsh
|
|
84
|
+
zsh_path = shutil.which("zsh")
|
|
85
|
+
if zsh_path:
|
|
86
|
+
return zsh_path
|
|
87
|
+
|
|
88
|
+
# Fall back to bash if zsh not found
|
|
89
|
+
return "bash"
|
|
90
|
+
|
|
91
|
+
@override
|
|
92
|
+
def get_script_flags(self) -> list[str]:
|
|
93
|
+
"""Get interpreter flags."""
|
|
94
|
+
if platform.system() == "Windows" and self.get_interpreter().endswith(".exe"):
|
|
95
|
+
return ["-c"]
|
|
96
|
+
return ["-c"]
|
|
97
|
+
|
|
98
|
+
@override
|
|
99
|
+
def get_tool_name(self) -> str:
|
|
100
|
+
"""Get the tool name."""
|
|
101
|
+
return "zsh"
|
|
102
|
+
|
|
103
|
+
@override
|
|
104
|
+
async def run(
|
|
105
|
+
self,
|
|
106
|
+
ctx: MCPContext,
|
|
107
|
+
command: str,
|
|
108
|
+
cwd: Optional[str] = None,
|
|
109
|
+
env: Optional[dict[str, str]] = None,
|
|
110
|
+
timeout: Optional[int] = None,
|
|
111
|
+
) -> str:
|
|
112
|
+
"""Run a zsh command with auto-backgrounding.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
ctx: MCP context
|
|
116
|
+
command: Zsh command to execute
|
|
117
|
+
cwd: Working directory
|
|
118
|
+
env: Environment variables
|
|
119
|
+
timeout: Command timeout in seconds (ignored - auto-backgrounds after 2 minutes)
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Command output or background status
|
|
123
|
+
"""
|
|
124
|
+
# Check if zsh is available
|
|
125
|
+
if not shutil.which("zsh") and platform.system() != "Windows":
|
|
126
|
+
return "Error: Zsh is not installed. Please install zsh first."
|
|
127
|
+
|
|
128
|
+
# Prepare working directory
|
|
129
|
+
work_dir = Path(cwd).resolve() if cwd else Path.cwd()
|
|
130
|
+
|
|
131
|
+
# Use execute_sync which has auto-backgrounding
|
|
132
|
+
output = await self.execute_sync(
|
|
133
|
+
command, cwd=work_dir, env=env, timeout=timeout
|
|
134
|
+
)
|
|
135
|
+
return output if output else "Command completed successfully (no output)"
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class ShellTool(BaseScriptTool):
|
|
139
|
+
"""Smart shell tool that uses the best available shell (zsh > bash)."""
|
|
140
|
+
|
|
141
|
+
name = "shell"
|
|
142
|
+
|
|
143
|
+
def __init__(self):
|
|
144
|
+
"""Initialize and detect the best shell."""
|
|
145
|
+
super().__init__()
|
|
146
|
+
self._best_shell = self._detect_best_shell()
|
|
147
|
+
|
|
148
|
+
def _detect_best_shell(self) -> str:
|
|
149
|
+
"""Detect the best available shell."""
|
|
150
|
+
# Check for zsh first
|
|
151
|
+
if shutil.which("zsh"):
|
|
152
|
+
# Also check if .zshrc exists
|
|
153
|
+
if (Path.home() / ".zshrc").exists():
|
|
154
|
+
return "zsh"
|
|
155
|
+
|
|
156
|
+
# Check for user's preferred shell
|
|
157
|
+
user_shell = os.environ.get("SHELL", "")
|
|
158
|
+
if user_shell and Path(user_shell).exists():
|
|
159
|
+
return user_shell
|
|
160
|
+
|
|
161
|
+
# Default to bash
|
|
162
|
+
return "bash"
|
|
163
|
+
|
|
164
|
+
def register(self, server: FastMCP) -> None:
|
|
165
|
+
"""Register the tool with the MCP server."""
|
|
166
|
+
tool_self = self
|
|
167
|
+
|
|
168
|
+
@server.tool(name=self.name, description=self.description)
|
|
169
|
+
async def shell(
|
|
170
|
+
ctx: MCPContext,
|
|
171
|
+
command: str,
|
|
172
|
+
cwd: Optional[str] = None,
|
|
173
|
+
env: Optional[dict[str, str]] = None,
|
|
174
|
+
timeout: Optional[int] = None,
|
|
175
|
+
) -> str:
|
|
176
|
+
return await tool_self.run(
|
|
177
|
+
ctx, command=command, cwd=cwd, env=env, timeout=timeout
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
async def call(self, ctx: MCPContext, **params) -> str:
|
|
181
|
+
"""Call the tool with arguments."""
|
|
182
|
+
return await self.run(
|
|
183
|
+
ctx,
|
|
184
|
+
command=params["command"],
|
|
185
|
+
cwd=params.get("cwd"),
|
|
186
|
+
env=params.get("env"),
|
|
187
|
+
timeout=params.get("timeout"),
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
@property
|
|
191
|
+
@override
|
|
192
|
+
def description(self) -> str:
|
|
193
|
+
"""Get the tool description."""
|
|
194
|
+
return f"""Run shell commands using the best available shell (currently: {os.path.basename(self._best_shell)}).
|
|
195
|
+
|
|
196
|
+
Automatically selects:
|
|
197
|
+
- Zsh if available (with .zshrc)
|
|
198
|
+
- User's preferred shell ($SHELL)
|
|
199
|
+
- Bash as fallback
|
|
200
|
+
|
|
201
|
+
Commands that run for more than 2 minutes will automatically continue in the background.
|
|
202
|
+
|
|
203
|
+
Usage:
|
|
204
|
+
shell "ls -la"
|
|
205
|
+
shell "echo $SHELL" # Shows which shell is being used
|
|
206
|
+
shell "git status && git diff"
|
|
207
|
+
shell "npm run dev" --cwd ./frontend # Auto-backgrounds if needed"""
|
|
208
|
+
|
|
209
|
+
@override
|
|
210
|
+
def get_interpreter(self) -> str:
|
|
211
|
+
"""Get the best shell interpreter."""
|
|
212
|
+
return self._best_shell
|
|
213
|
+
|
|
214
|
+
@override
|
|
215
|
+
def get_script_flags(self) -> list[str]:
|
|
216
|
+
"""Get interpreter flags."""
|
|
217
|
+
if platform.system() == "Windows":
|
|
218
|
+
return ["/c"] if self._best_shell == "cmd.exe" else ["-c"]
|
|
219
|
+
return ["-c"]
|
|
220
|
+
|
|
221
|
+
@override
|
|
222
|
+
def get_tool_name(self) -> str:
|
|
223
|
+
"""Get the tool name."""
|
|
224
|
+
return "shell"
|
|
225
|
+
|
|
226
|
+
@override
|
|
227
|
+
async def run(
|
|
228
|
+
self,
|
|
229
|
+
ctx: MCPContext,
|
|
230
|
+
command: str,
|
|
231
|
+
cwd: Optional[str] = None,
|
|
232
|
+
env: Optional[dict[str, str]] = None,
|
|
233
|
+
timeout: Optional[int] = None,
|
|
234
|
+
) -> str:
|
|
235
|
+
"""Run a shell command with auto-backgrounding.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
ctx: MCP context
|
|
239
|
+
command: Shell command to execute
|
|
240
|
+
cwd: Working directory
|
|
241
|
+
env: Environment variables
|
|
242
|
+
timeout: Command timeout in seconds (ignored - auto-backgrounds after 2 minutes)
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
Command output or background status
|
|
246
|
+
"""
|
|
247
|
+
# Prepare working directory
|
|
248
|
+
work_dir = Path(cwd).resolve() if cwd else Path.cwd()
|
|
249
|
+
|
|
250
|
+
# Add shell info to output if verbose
|
|
251
|
+
shell_name = os.path.basename(self._best_shell)
|
|
252
|
+
|
|
253
|
+
# Use execute_sync which has auto-backgrounding
|
|
254
|
+
output = await self.execute_sync(
|
|
255
|
+
command, cwd=work_dir, env=env, timeout=timeout
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
if output:
|
|
259
|
+
return output
|
|
260
|
+
else:
|
|
261
|
+
return f"Command completed successfully in {shell_name} (no output)"
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
# Create tool instances
|
|
265
|
+
zsh_tool = ZshTool()
|
|
266
|
+
shell_tool = ShellTool() # Smart shell that prefers zsh
|