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.

@@ -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
- bash_tool,
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 shell interpreter."""
62
+ """Get the bash interpreter."""
63
63
  if platform.system() == "Windows":
64
- return "cmd.exe"
65
-
66
- # Check for user's preferred shell from environment
67
- shell = os.environ.get("SHELL", "/bin/bash")
68
-
69
- # Extract just the shell name from the path
70
- shell_name = os.path.basename(shell)
71
-
72
- # Check if it's a supported shell and the config file exists
73
- if shell_name == "zsh":
74
- # Check for .zshrc
75
- zshrc_path = Path.home() / ".zshrc"
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
- if platform.system() == "Windows":
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