hanzo-mcp 0.7.7__py3-none-any.whl → 0.8.1__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 +6 -0
- hanzo_mcp/__main__.py +1 -1
- hanzo_mcp/analytics/__init__.py +2 -2
- hanzo_mcp/analytics/posthog_analytics.py +76 -82
- hanzo_mcp/cli.py +31 -36
- hanzo_mcp/cli_enhanced.py +94 -72
- hanzo_mcp/cli_plugin.py +27 -17
- hanzo_mcp/config/__init__.py +2 -2
- hanzo_mcp/config/settings.py +112 -88
- hanzo_mcp/config/tool_config.py +32 -34
- hanzo_mcp/dev_server.py +66 -67
- hanzo_mcp/prompts/__init__.py +94 -12
- hanzo_mcp/prompts/enhanced_prompts.py +809 -0
- hanzo_mcp/prompts/example_custom_prompt.py +6 -5
- hanzo_mcp/prompts/project_todo_reminder.py +0 -1
- hanzo_mcp/prompts/tool_explorer.py +10 -7
- hanzo_mcp/server.py +17 -21
- hanzo_mcp/server_enhanced.py +15 -22
- hanzo_mcp/tools/__init__.py +56 -28
- hanzo_mcp/tools/agent/__init__.py +16 -19
- hanzo_mcp/tools/agent/agent.py +82 -65
- hanzo_mcp/tools/agent/agent_tool.py +152 -122
- hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +66 -62
- hanzo_mcp/tools/agent/clarification_protocol.py +55 -50
- hanzo_mcp/tools/agent/clarification_tool.py +11 -10
- hanzo_mcp/tools/agent/claude_cli_tool.py +21 -20
- hanzo_mcp/tools/agent/claude_desktop_auth.py +130 -144
- hanzo_mcp/tools/agent/cli_agent_base.py +59 -53
- hanzo_mcp/tools/agent/code_auth.py +102 -107
- hanzo_mcp/tools/agent/code_auth_tool.py +28 -27
- hanzo_mcp/tools/agent/codex_cli_tool.py +20 -19
- hanzo_mcp/tools/agent/critic_tool.py +86 -73
- hanzo_mcp/tools/agent/gemini_cli_tool.py +21 -20
- hanzo_mcp/tools/agent/grok_cli_tool.py +21 -20
- hanzo_mcp/tools/agent/iching_tool.py +404 -139
- hanzo_mcp/tools/agent/network_tool.py +89 -73
- hanzo_mcp/tools/agent/prompt.py +2 -1
- hanzo_mcp/tools/agent/review_tool.py +101 -98
- hanzo_mcp/tools/agent/swarm_alias.py +87 -0
- hanzo_mcp/tools/agent/swarm_tool.py +246 -161
- hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +134 -92
- hanzo_mcp/tools/agent/tool_adapter.py +21 -11
- hanzo_mcp/tools/common/__init__.py +1 -1
- hanzo_mcp/tools/common/base.py +3 -5
- hanzo_mcp/tools/common/batch_tool.py +46 -39
- hanzo_mcp/tools/common/config_tool.py +120 -84
- hanzo_mcp/tools/common/context.py +1 -5
- hanzo_mcp/tools/common/context_fix.py +5 -3
- hanzo_mcp/tools/common/critic_tool.py +4 -8
- hanzo_mcp/tools/common/decorators.py +58 -56
- hanzo_mcp/tools/common/enhanced_base.py +29 -32
- hanzo_mcp/tools/common/fastmcp_pagination.py +91 -94
- hanzo_mcp/tools/common/forgiving_edit.py +91 -87
- hanzo_mcp/tools/common/mode.py +15 -17
- hanzo_mcp/tools/common/mode_loader.py +27 -24
- hanzo_mcp/tools/common/paginated_base.py +61 -53
- hanzo_mcp/tools/common/paginated_response.py +72 -79
- hanzo_mcp/tools/common/pagination.py +50 -53
- hanzo_mcp/tools/common/permissions.py +4 -4
- hanzo_mcp/tools/common/personality.py +186 -138
- hanzo_mcp/tools/common/plugin_loader.py +54 -54
- hanzo_mcp/tools/common/stats.py +65 -47
- hanzo_mcp/tools/common/test_helpers.py +31 -0
- hanzo_mcp/tools/common/thinking_tool.py +4 -8
- hanzo_mcp/tools/common/tool_disable.py +17 -12
- hanzo_mcp/tools/common/tool_enable.py +13 -14
- hanzo_mcp/tools/common/tool_list.py +36 -28
- hanzo_mcp/tools/common/truncate.py +23 -23
- hanzo_mcp/tools/config/__init__.py +4 -4
- hanzo_mcp/tools/config/config_tool.py +42 -29
- hanzo_mcp/tools/config/index_config.py +37 -34
- hanzo_mcp/tools/config/mode_tool.py +175 -55
- hanzo_mcp/tools/database/__init__.py +15 -12
- hanzo_mcp/tools/database/database_manager.py +77 -75
- hanzo_mcp/tools/database/graph.py +137 -91
- hanzo_mcp/tools/database/graph_add.py +30 -18
- hanzo_mcp/tools/database/graph_query.py +178 -102
- hanzo_mcp/tools/database/graph_remove.py +33 -28
- hanzo_mcp/tools/database/graph_search.py +97 -75
- hanzo_mcp/tools/database/graph_stats.py +91 -59
- hanzo_mcp/tools/database/sql.py +107 -79
- hanzo_mcp/tools/database/sql_query.py +30 -24
- hanzo_mcp/tools/database/sql_search.py +29 -25
- hanzo_mcp/tools/database/sql_stats.py +47 -35
- hanzo_mcp/tools/editor/neovim_command.py +25 -28
- hanzo_mcp/tools/editor/neovim_edit.py +21 -23
- hanzo_mcp/tools/editor/neovim_session.py +60 -54
- hanzo_mcp/tools/filesystem/__init__.py +31 -30
- hanzo_mcp/tools/filesystem/ast_multi_edit.py +329 -249
- hanzo_mcp/tools/filesystem/ast_tool.py +4 -4
- hanzo_mcp/tools/filesystem/base.py +1 -1
- hanzo_mcp/tools/filesystem/batch_search.py +316 -224
- hanzo_mcp/tools/filesystem/content_replace.py +4 -4
- hanzo_mcp/tools/filesystem/diff.py +71 -59
- hanzo_mcp/tools/filesystem/directory_tree.py +7 -7
- hanzo_mcp/tools/filesystem/directory_tree_paginated.py +49 -37
- hanzo_mcp/tools/filesystem/edit.py +4 -4
- hanzo_mcp/tools/filesystem/find.py +173 -80
- hanzo_mcp/tools/filesystem/find_files.py +73 -52
- hanzo_mcp/tools/filesystem/git_search.py +157 -104
- hanzo_mcp/tools/filesystem/grep.py +8 -8
- hanzo_mcp/tools/filesystem/multi_edit.py +4 -8
- hanzo_mcp/tools/filesystem/read.py +12 -10
- hanzo_mcp/tools/filesystem/rules_tool.py +59 -43
- hanzo_mcp/tools/filesystem/search_tool.py +263 -207
- hanzo_mcp/tools/filesystem/symbols_tool.py +94 -54
- hanzo_mcp/tools/filesystem/tree.py +35 -33
- hanzo_mcp/tools/filesystem/unix_aliases.py +13 -18
- hanzo_mcp/tools/filesystem/watch.py +37 -36
- hanzo_mcp/tools/filesystem/write.py +4 -8
- hanzo_mcp/tools/jupyter/__init__.py +4 -4
- hanzo_mcp/tools/jupyter/base.py +4 -5
- hanzo_mcp/tools/jupyter/jupyter.py +67 -47
- hanzo_mcp/tools/jupyter/notebook_edit.py +4 -4
- hanzo_mcp/tools/jupyter/notebook_read.py +4 -7
- hanzo_mcp/tools/llm/__init__.py +5 -7
- hanzo_mcp/tools/llm/consensus_tool.py +72 -52
- hanzo_mcp/tools/llm/llm_manage.py +101 -60
- hanzo_mcp/tools/llm/llm_tool.py +226 -166
- hanzo_mcp/tools/llm/provider_tools.py +25 -26
- hanzo_mcp/tools/lsp/__init__.py +1 -1
- hanzo_mcp/tools/lsp/lsp_tool.py +228 -143
- hanzo_mcp/tools/mcp/__init__.py +2 -3
- hanzo_mcp/tools/mcp/mcp_add.py +27 -25
- hanzo_mcp/tools/mcp/mcp_remove.py +7 -8
- hanzo_mcp/tools/mcp/mcp_stats.py +23 -22
- hanzo_mcp/tools/mcp/mcp_tool.py +129 -98
- hanzo_mcp/tools/memory/__init__.py +39 -21
- hanzo_mcp/tools/memory/knowledge_tools.py +124 -99
- hanzo_mcp/tools/memory/memory_tools.py +90 -108
- hanzo_mcp/tools/search/__init__.py +7 -2
- hanzo_mcp/tools/search/find_tool.py +297 -212
- hanzo_mcp/tools/search/unified_search.py +366 -314
- hanzo_mcp/tools/shell/__init__.py +8 -7
- hanzo_mcp/tools/shell/auto_background.py +56 -49
- hanzo_mcp/tools/shell/base.py +1 -1
- hanzo_mcp/tools/shell/base_process.py +75 -75
- hanzo_mcp/tools/shell/bash_session.py +2 -2
- hanzo_mcp/tools/shell/bash_session_executor.py +4 -4
- hanzo_mcp/tools/shell/bash_tool.py +24 -31
- hanzo_mcp/tools/shell/command_executor.py +12 -12
- hanzo_mcp/tools/shell/logs.py +43 -33
- hanzo_mcp/tools/shell/npx.py +13 -13
- hanzo_mcp/tools/shell/npx_background.py +24 -21
- hanzo_mcp/tools/shell/npx_tool.py +18 -22
- hanzo_mcp/tools/shell/open.py +19 -21
- hanzo_mcp/tools/shell/pkill.py +31 -26
- hanzo_mcp/tools/shell/process_tool.py +32 -32
- hanzo_mcp/tools/shell/processes.py +57 -58
- hanzo_mcp/tools/shell/run_background.py +24 -25
- hanzo_mcp/tools/shell/run_command.py +5 -5
- hanzo_mcp/tools/shell/run_command_windows.py +5 -5
- hanzo_mcp/tools/shell/session_storage.py +3 -3
- hanzo_mcp/tools/shell/streaming_command.py +141 -126
- hanzo_mcp/tools/shell/uvx.py +24 -25
- hanzo_mcp/tools/shell/uvx_background.py +35 -33
- hanzo_mcp/tools/shell/uvx_tool.py +18 -22
- hanzo_mcp/tools/todo/__init__.py +6 -2
- hanzo_mcp/tools/todo/todo.py +50 -37
- hanzo_mcp/tools/todo/todo_read.py +5 -8
- hanzo_mcp/tools/todo/todo_write.py +5 -7
- hanzo_mcp/tools/vector/__init__.py +40 -28
- hanzo_mcp/tools/vector/ast_analyzer.py +176 -143
- hanzo_mcp/tools/vector/git_ingester.py +170 -179
- hanzo_mcp/tools/vector/index_tool.py +96 -44
- hanzo_mcp/tools/vector/infinity_store.py +283 -228
- hanzo_mcp/tools/vector/mock_infinity.py +39 -40
- hanzo_mcp/tools/vector/project_manager.py +88 -78
- hanzo_mcp/tools/vector/vector.py +59 -42
- hanzo_mcp/tools/vector/vector_index.py +30 -27
- hanzo_mcp/tools/vector/vector_search.py +64 -45
- hanzo_mcp/types.py +6 -4
- {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.1.dist-info}/METADATA +1 -1
- hanzo_mcp-0.8.1.dist-info/RECORD +185 -0
- hanzo_mcp-0.7.7.dist-info/RECORD +0 -182
- {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.1.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.1.dist-info}/entry_points.txt +0 -0
- {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.1.dist-info}/top_level.txt +0 -0
|
@@ -1,51 +1,48 @@
|
|
|
1
1
|
"""Base classes for process execution tools."""
|
|
2
2
|
|
|
3
|
-
import asyncio
|
|
4
3
|
import os
|
|
5
|
-
import subprocess
|
|
6
|
-
import tempfile
|
|
7
|
-
import time
|
|
8
4
|
import uuid
|
|
5
|
+
import tempfile
|
|
6
|
+
import subprocess
|
|
9
7
|
from abc import abstractmethod
|
|
8
|
+
from typing import Any, Dict, List, Optional, override
|
|
10
9
|
from pathlib import Path
|
|
11
|
-
from typing import Any, Dict, List, Optional, Tuple, override
|
|
12
|
-
|
|
13
|
-
from mcp.server.fastmcp import Context as MCPContext
|
|
14
10
|
|
|
15
11
|
from hanzo_mcp.tools.common.base import BaseTool
|
|
16
|
-
from hanzo_mcp.tools.common.permissions import PermissionManager
|
|
17
12
|
from hanzo_mcp.tools.common.truncate import truncate_response
|
|
13
|
+
from hanzo_mcp.tools.common.permissions import PermissionManager
|
|
14
|
+
|
|
18
15
|
# Import moved to __init__ to avoid circular import
|
|
19
16
|
|
|
20
17
|
|
|
21
18
|
class ProcessManager:
|
|
22
19
|
"""Singleton manager for background processes."""
|
|
23
|
-
|
|
20
|
+
|
|
24
21
|
_instance = None
|
|
25
22
|
_processes: Dict[str, Any] = {}
|
|
26
23
|
_logs: Dict[str, str] = {}
|
|
27
24
|
_log_dir = Path(tempfile.gettempdir()) / "hanzo_mcp_logs"
|
|
28
|
-
|
|
25
|
+
|
|
29
26
|
def __new__(cls):
|
|
30
27
|
if cls._instance is None:
|
|
31
28
|
cls._instance = super().__new__(cls)
|
|
32
29
|
cls._instance._log_dir.mkdir(exist_ok=True)
|
|
33
30
|
return cls._instance
|
|
34
|
-
|
|
31
|
+
|
|
35
32
|
def add_process(self, process_id: str, process: Any, log_file: str) -> None:
|
|
36
33
|
"""Add a process to track."""
|
|
37
34
|
self._processes[process_id] = process
|
|
38
35
|
self._logs[process_id] = log_file
|
|
39
|
-
|
|
36
|
+
|
|
40
37
|
def get_process(self, process_id: str) -> Optional[Any]:
|
|
41
38
|
"""Get a tracked process."""
|
|
42
39
|
return self._processes.get(process_id)
|
|
43
|
-
|
|
40
|
+
|
|
44
41
|
def remove_process(self, process_id: str) -> None:
|
|
45
42
|
"""Remove a process from tracking."""
|
|
46
43
|
self._processes.pop(process_id, None)
|
|
47
44
|
self._logs.pop(process_id, None)
|
|
48
|
-
|
|
45
|
+
|
|
49
46
|
def list_processes(self) -> Dict[str, Dict[str, Any]]:
|
|
50
47
|
"""List all tracked processes."""
|
|
51
48
|
result = {}
|
|
@@ -54,45 +51,45 @@ class ProcessManager:
|
|
|
54
51
|
result[pid] = {
|
|
55
52
|
"pid": proc.pid,
|
|
56
53
|
"running": True,
|
|
57
|
-
"log_file": self._logs.get(pid)
|
|
54
|
+
"log_file": self._logs.get(pid),
|
|
58
55
|
}
|
|
59
56
|
else:
|
|
60
57
|
result[pid] = {
|
|
61
58
|
"pid": proc.pid,
|
|
62
59
|
"running": False,
|
|
63
60
|
"return_code": proc.returncode,
|
|
64
|
-
"log_file": self._logs.get(pid)
|
|
61
|
+
"log_file": self._logs.get(pid),
|
|
65
62
|
}
|
|
66
63
|
# Clean up finished processes
|
|
67
64
|
self.remove_process(pid)
|
|
68
65
|
return result
|
|
69
|
-
|
|
66
|
+
|
|
70
67
|
def get_log_file(self, process_id: str) -> Optional[Path]:
|
|
71
68
|
"""Get log file path for a process."""
|
|
72
69
|
log_path = self._logs.get(process_id)
|
|
73
70
|
return Path(log_path) if log_path else None
|
|
74
|
-
|
|
71
|
+
|
|
75
72
|
@property
|
|
76
73
|
def log_dir(self) -> Path:
|
|
77
74
|
"""Get the log directory."""
|
|
78
75
|
return self._log_dir
|
|
79
|
-
|
|
76
|
+
|
|
80
77
|
def create_log_file(self, process_id: str) -> Path:
|
|
81
78
|
"""Create a log file for a process.
|
|
82
|
-
|
|
79
|
+
|
|
83
80
|
Args:
|
|
84
81
|
process_id: Process identifier
|
|
85
|
-
|
|
82
|
+
|
|
86
83
|
Returns:
|
|
87
84
|
Path to the created log file
|
|
88
85
|
"""
|
|
89
86
|
log_file = self._log_dir / f"{process_id}.log"
|
|
90
87
|
log_file.touch()
|
|
91
88
|
return log_file
|
|
92
|
-
|
|
89
|
+
|
|
93
90
|
def mark_completed(self, process_id: str, return_code: int) -> None:
|
|
94
91
|
"""Mark a process as completed with the given return code.
|
|
95
|
-
|
|
92
|
+
|
|
96
93
|
Args:
|
|
97
94
|
process_id: Process identifier
|
|
98
95
|
return_code: Process exit code
|
|
@@ -104,10 +101,10 @@ class ProcessManager:
|
|
|
104
101
|
|
|
105
102
|
class BaseProcessTool(BaseTool):
|
|
106
103
|
"""Base class for all process execution tools."""
|
|
107
|
-
|
|
104
|
+
|
|
108
105
|
def __init__(self, permission_manager: Optional[PermissionManager] = None):
|
|
109
106
|
"""Initialize the process tool.
|
|
110
|
-
|
|
107
|
+
|
|
111
108
|
Args:
|
|
112
109
|
permission_manager: Optional permission manager for access control
|
|
113
110
|
"""
|
|
@@ -116,46 +113,47 @@ class BaseProcessTool(BaseTool):
|
|
|
116
113
|
self.process_manager = ProcessManager()
|
|
117
114
|
# Import here to avoid circular import
|
|
118
115
|
from hanzo_mcp.tools.shell.auto_background import AutoBackgroundExecutor
|
|
116
|
+
|
|
119
117
|
self.auto_background_executor = AutoBackgroundExecutor(self.process_manager)
|
|
120
|
-
|
|
118
|
+
|
|
121
119
|
@abstractmethod
|
|
122
120
|
def get_command_args(self, command: str, **kwargs) -> List[str]:
|
|
123
121
|
"""Get the command arguments for subprocess.
|
|
124
|
-
|
|
122
|
+
|
|
125
123
|
Args:
|
|
126
124
|
command: The command or script to run
|
|
127
125
|
**kwargs: Additional arguments specific to the tool
|
|
128
|
-
|
|
126
|
+
|
|
129
127
|
Returns:
|
|
130
128
|
List of command arguments for subprocess
|
|
131
129
|
"""
|
|
132
130
|
pass
|
|
133
|
-
|
|
131
|
+
|
|
134
132
|
@abstractmethod
|
|
135
133
|
def get_tool_name(self) -> str:
|
|
136
134
|
"""Get the name of the tool being used (e.g., 'bash', 'uvx', 'npx')."""
|
|
137
135
|
pass
|
|
138
|
-
|
|
136
|
+
|
|
139
137
|
async def execute_sync(
|
|
140
138
|
self,
|
|
141
139
|
command: str,
|
|
142
140
|
cwd: Optional[Path] = None,
|
|
143
141
|
env: Optional[Dict[str, str]] = None,
|
|
144
142
|
timeout: Optional[int] = None,
|
|
145
|
-
**kwargs
|
|
143
|
+
**kwargs,
|
|
146
144
|
) -> str:
|
|
147
145
|
"""Execute a command with auto-backgrounding after 2 minutes.
|
|
148
|
-
|
|
146
|
+
|
|
149
147
|
Args:
|
|
150
148
|
command: Command to execute
|
|
151
149
|
cwd: Working directory
|
|
152
150
|
env: Environment variables
|
|
153
151
|
timeout: Timeout in seconds (ignored - auto-backgrounds after 2 minutes)
|
|
154
152
|
**kwargs: Additional tool-specific arguments
|
|
155
|
-
|
|
153
|
+
|
|
156
154
|
Returns:
|
|
157
155
|
Command output or background status
|
|
158
|
-
|
|
156
|
+
|
|
159
157
|
Raises:
|
|
160
158
|
RuntimeError: If command fails
|
|
161
159
|
"""
|
|
@@ -163,23 +161,25 @@ class BaseProcessTool(BaseTool):
|
|
|
163
161
|
if self.permission_manager and cwd:
|
|
164
162
|
if not self.permission_manager.is_path_allowed(str(cwd)):
|
|
165
163
|
raise PermissionError(f"Access denied to path: {cwd}")
|
|
166
|
-
|
|
164
|
+
|
|
167
165
|
# Get command arguments
|
|
168
166
|
cmd_args = self.get_command_args(command, **kwargs)
|
|
169
|
-
|
|
167
|
+
|
|
170
168
|
# Prepare environment
|
|
171
169
|
process_env = os.environ.copy()
|
|
172
170
|
if env:
|
|
173
171
|
process_env.update(env)
|
|
174
|
-
|
|
172
|
+
|
|
175
173
|
# Execute with auto-backgrounding
|
|
176
|
-
output, was_backgrounded, process_id =
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
174
|
+
output, was_backgrounded, process_id = (
|
|
175
|
+
await self.auto_background_executor.execute_with_auto_background(
|
|
176
|
+
cmd_args=cmd_args,
|
|
177
|
+
tool_name=self.get_tool_name(),
|
|
178
|
+
cwd=cwd,
|
|
179
|
+
env=process_env,
|
|
180
|
+
)
|
|
181
181
|
)
|
|
182
|
-
|
|
182
|
+
|
|
183
183
|
if was_backgrounded:
|
|
184
184
|
return output
|
|
185
185
|
else:
|
|
@@ -189,24 +189,24 @@ class BaseProcessTool(BaseTool):
|
|
|
189
189
|
return truncate_response(
|
|
190
190
|
output,
|
|
191
191
|
max_tokens=25000,
|
|
192
|
-
truncation_message="\n\n[Command output truncated due to token limit. Output may be available in logs or files.]"
|
|
192
|
+
truncation_message="\n\n[Command output truncated due to token limit. Output may be available in logs or files.]",
|
|
193
193
|
)
|
|
194
|
-
|
|
194
|
+
|
|
195
195
|
async def execute_background(
|
|
196
196
|
self,
|
|
197
197
|
command: str,
|
|
198
198
|
cwd: Optional[Path] = None,
|
|
199
199
|
env: Optional[Dict[str, str]] = None,
|
|
200
|
-
**kwargs
|
|
200
|
+
**kwargs,
|
|
201
201
|
) -> Dict[str, Any]:
|
|
202
202
|
"""Execute a command in the background.
|
|
203
|
-
|
|
203
|
+
|
|
204
204
|
Args:
|
|
205
205
|
command: Command to execute
|
|
206
206
|
cwd: Working directory
|
|
207
207
|
env: Environment variables
|
|
208
208
|
**kwargs: Additional tool-specific arguments
|
|
209
|
-
|
|
209
|
+
|
|
210
210
|
Returns:
|
|
211
211
|
Dict with process_id and log_file
|
|
212
212
|
"""
|
|
@@ -214,19 +214,19 @@ class BaseProcessTool(BaseTool):
|
|
|
214
214
|
if self.permission_manager and cwd:
|
|
215
215
|
if not self.permission_manager.is_path_allowed(str(cwd)):
|
|
216
216
|
raise PermissionError(f"Access denied to path: {cwd}")
|
|
217
|
-
|
|
217
|
+
|
|
218
218
|
# Generate process ID and log file
|
|
219
219
|
process_id = f"{self.get_tool_name()}_{uuid.uuid4().hex[:8]}"
|
|
220
220
|
log_file = self.process_manager.log_dir / f"{process_id}.log"
|
|
221
|
-
|
|
221
|
+
|
|
222
222
|
# Get command arguments
|
|
223
223
|
cmd_args = self.get_command_args(command, **kwargs)
|
|
224
|
-
|
|
224
|
+
|
|
225
225
|
# Prepare environment
|
|
226
226
|
process_env = os.environ.copy()
|
|
227
227
|
if env:
|
|
228
228
|
process_env.update(env)
|
|
229
|
-
|
|
229
|
+
|
|
230
230
|
# Start process with output to log file
|
|
231
231
|
with open(log_file, "w") as f:
|
|
232
232
|
process = subprocess.Popen(
|
|
@@ -235,57 +235,57 @@ class BaseProcessTool(BaseTool):
|
|
|
235
235
|
env=process_env,
|
|
236
236
|
stdout=f,
|
|
237
237
|
stderr=subprocess.STDOUT,
|
|
238
|
-
text=True
|
|
238
|
+
text=True,
|
|
239
239
|
)
|
|
240
|
-
|
|
240
|
+
|
|
241
241
|
# Track the process
|
|
242
242
|
self.process_manager.add_process(process_id, process, log_file)
|
|
243
|
-
|
|
243
|
+
|
|
244
244
|
return {
|
|
245
245
|
"process_id": process_id,
|
|
246
246
|
"pid": process.pid,
|
|
247
247
|
"log_file": str(log_file),
|
|
248
|
-
"status": "started"
|
|
248
|
+
"status": "started",
|
|
249
249
|
}
|
|
250
250
|
|
|
251
251
|
|
|
252
252
|
class BaseBinaryTool(BaseProcessTool):
|
|
253
253
|
"""Base class for binary execution tools (like npx, uvx)."""
|
|
254
|
-
|
|
254
|
+
|
|
255
255
|
@abstractmethod
|
|
256
256
|
def get_binary_name(self) -> str:
|
|
257
257
|
"""Get the name of the binary to execute."""
|
|
258
258
|
pass
|
|
259
|
-
|
|
259
|
+
|
|
260
260
|
@override
|
|
261
261
|
def get_command_args(self, command: str, **kwargs) -> List[str]:
|
|
262
262
|
"""Get command arguments for binary execution.
|
|
263
|
-
|
|
263
|
+
|
|
264
264
|
Args:
|
|
265
265
|
command: The package or command to run
|
|
266
266
|
**kwargs: Additional arguments (args, flags, etc.)
|
|
267
|
-
|
|
267
|
+
|
|
268
268
|
Returns:
|
|
269
269
|
List of command arguments
|
|
270
270
|
"""
|
|
271
271
|
cmd_args = [self.get_binary_name()]
|
|
272
|
-
|
|
272
|
+
|
|
273
273
|
# Add any binary-specific flags
|
|
274
274
|
if "flags" in kwargs:
|
|
275
275
|
cmd_args.extend(kwargs["flags"])
|
|
276
|
-
|
|
276
|
+
|
|
277
277
|
# Add the command/package
|
|
278
278
|
cmd_args.append(command)
|
|
279
|
-
|
|
279
|
+
|
|
280
280
|
# Add any additional arguments
|
|
281
281
|
if "args" in kwargs:
|
|
282
282
|
if isinstance(kwargs["args"], str):
|
|
283
283
|
cmd_args.extend(kwargs["args"].split())
|
|
284
284
|
else:
|
|
285
285
|
cmd_args.extend(kwargs["args"])
|
|
286
|
-
|
|
286
|
+
|
|
287
287
|
return cmd_args
|
|
288
|
-
|
|
288
|
+
|
|
289
289
|
@override
|
|
290
290
|
def get_tool_name(self) -> str:
|
|
291
291
|
"""Get the tool name (same as binary name by default)."""
|
|
@@ -294,44 +294,44 @@ class BaseBinaryTool(BaseProcessTool):
|
|
|
294
294
|
|
|
295
295
|
class BaseScriptTool(BaseProcessTool):
|
|
296
296
|
"""Base class for script execution tools (like bash, python)."""
|
|
297
|
-
|
|
297
|
+
|
|
298
298
|
@abstractmethod
|
|
299
299
|
def get_interpreter(self) -> str:
|
|
300
300
|
"""Get the interpreter to use."""
|
|
301
301
|
pass
|
|
302
|
-
|
|
302
|
+
|
|
303
303
|
@abstractmethod
|
|
304
304
|
def get_script_flags(self) -> List[str]:
|
|
305
305
|
"""Get default flags for the interpreter."""
|
|
306
306
|
pass
|
|
307
|
-
|
|
307
|
+
|
|
308
308
|
@override
|
|
309
309
|
def get_command_args(self, command: str, **kwargs) -> List[str]:
|
|
310
310
|
"""Get command arguments for script execution.
|
|
311
|
-
|
|
311
|
+
|
|
312
312
|
Args:
|
|
313
313
|
command: The script content to execute
|
|
314
314
|
**kwargs: Additional arguments
|
|
315
|
-
|
|
315
|
+
|
|
316
316
|
Returns:
|
|
317
317
|
List of command arguments
|
|
318
318
|
"""
|
|
319
319
|
cmd_args = [self.get_interpreter()]
|
|
320
320
|
cmd_args.extend(self.get_script_flags())
|
|
321
|
-
|
|
321
|
+
|
|
322
322
|
# For inline scripts, use -c flag
|
|
323
323
|
if not kwargs.get("is_file", False):
|
|
324
324
|
cmd_args.extend(["-c", command])
|
|
325
325
|
else:
|
|
326
326
|
cmd_args.append(command)
|
|
327
|
-
|
|
327
|
+
|
|
328
328
|
return cmd_args
|
|
329
|
-
|
|
330
|
-
@override
|
|
329
|
+
|
|
330
|
+
@override
|
|
331
331
|
def get_tool_name(self) -> str:
|
|
332
332
|
"""Get the tool name (interpreter name by default)."""
|
|
333
333
|
return self.get_interpreter()
|
|
334
334
|
|
|
335
335
|
|
|
336
336
|
# Import os at the top of the file
|
|
337
|
-
import os
|
|
337
|
+
import os
|
|
@@ -14,8 +14,8 @@ import bashlex # type: ignore
|
|
|
14
14
|
import libtmux
|
|
15
15
|
|
|
16
16
|
from hanzo_mcp.tools.shell.base import (
|
|
17
|
-
BashCommandStatus,
|
|
18
17
|
CommandResult,
|
|
18
|
+
BashCommandStatus,
|
|
19
19
|
)
|
|
20
20
|
|
|
21
21
|
|
|
@@ -355,7 +355,7 @@ class BashSession:
|
|
|
355
355
|
error_message=(
|
|
356
356
|
f"ERROR: Cannot execute multiple commands at once.\n"
|
|
357
357
|
f"Please run each command separately OR chain them into a single command via && or ;\n"
|
|
358
|
-
f"Provided commands:\n{
|
|
358
|
+
f"Provided commands:\n{chr(10).join(f'({i + 1}) {cmd}' for i, cmd in enumerate(splited_commands))}"
|
|
359
359
|
),
|
|
360
360
|
command=command,
|
|
361
361
|
status=BashCommandStatus.COMPLETED,
|
|
@@ -4,15 +4,14 @@ This module provides a BashSessionExecutor class that replaces the old CommandEx
|
|
|
4
4
|
implementation with the new BashSession-based approach for better persistent execution.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
import asyncio
|
|
8
|
-
import logging
|
|
9
7
|
import os
|
|
10
8
|
import shlex
|
|
11
|
-
import
|
|
9
|
+
import asyncio
|
|
10
|
+
import logging
|
|
12
11
|
from typing import final
|
|
13
12
|
|
|
13
|
+
from hanzo_mcp.tools.shell.base import CommandResult, BashCommandStatus
|
|
14
14
|
from hanzo_mcp.tools.common.permissions import PermissionManager
|
|
15
|
-
from hanzo_mcp.tools.shell.base import BashCommandStatus, CommandResult
|
|
16
15
|
from hanzo_mcp.tools.shell.session_manager import SessionManager
|
|
17
16
|
|
|
18
17
|
|
|
@@ -65,6 +64,7 @@ class BashSessionExecutor:
|
|
|
65
64
|
if data is not None:
|
|
66
65
|
try:
|
|
67
66
|
import json
|
|
67
|
+
|
|
68
68
|
logger = logging.getLogger(__name__)
|
|
69
69
|
if isinstance(data, (dict, list)):
|
|
70
70
|
data_str = json.dumps(data)
|
|
@@ -2,40 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
4
|
import platform
|
|
5
|
-
from pathlib import Path
|
|
6
5
|
from typing import Optional, override
|
|
6
|
+
from pathlib import Path
|
|
7
7
|
|
|
8
|
+
from mcp.server import FastMCP
|
|
8
9
|
from mcp.server.fastmcp import Context as MCPContext
|
|
9
10
|
|
|
10
11
|
from hanzo_mcp.tools.shell.base_process import BaseScriptTool
|
|
11
|
-
from mcp.server import FastMCP
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class BashTool(BaseScriptTool):
|
|
15
15
|
"""Tool for running shell commands."""
|
|
16
|
-
|
|
16
|
+
|
|
17
17
|
name = "bash"
|
|
18
|
-
|
|
18
|
+
|
|
19
19
|
def register(self, server: FastMCP) -> None:
|
|
20
20
|
"""Register the tool with the MCP server."""
|
|
21
21
|
tool_self = self
|
|
22
|
-
|
|
22
|
+
|
|
23
23
|
@server.tool(name=self.name, description=self.description)
|
|
24
24
|
async def bash(
|
|
25
25
|
ctx: MCPContext,
|
|
26
26
|
command: str,
|
|
27
27
|
cwd: Optional[str] = None,
|
|
28
28
|
env: Optional[dict[str, str]] = None,
|
|
29
|
-
timeout: Optional[int] = None
|
|
29
|
+
timeout: Optional[int] = None,
|
|
30
30
|
) -> str:
|
|
31
31
|
return await tool_self.run(
|
|
32
|
-
ctx,
|
|
33
|
-
command=command,
|
|
34
|
-
cwd=cwd,
|
|
35
|
-
env=env,
|
|
36
|
-
timeout=timeout
|
|
32
|
+
ctx, command=command, cwd=cwd, env=env, timeout=timeout
|
|
37
33
|
)
|
|
38
|
-
|
|
34
|
+
|
|
39
35
|
async def call(self, ctx: MCPContext, **params) -> str:
|
|
40
36
|
"""Call the tool with arguments."""
|
|
41
37
|
return await self.run(
|
|
@@ -43,9 +39,9 @@ class BashTool(BaseScriptTool):
|
|
|
43
39
|
command=params["command"],
|
|
44
40
|
cwd=params.get("cwd"),
|
|
45
41
|
env=params.get("env"),
|
|
46
|
-
timeout=params.get("timeout")
|
|
42
|
+
timeout=params.get("timeout"),
|
|
47
43
|
)
|
|
48
|
-
|
|
44
|
+
|
|
49
45
|
@property
|
|
50
46
|
@override
|
|
51
47
|
def description(self) -> str:
|
|
@@ -60,19 +56,19 @@ bash "ls -la"
|
|
|
60
56
|
bash "python server.py" # Auto-backgrounds after 2 minutes
|
|
61
57
|
bash "git status && git diff"
|
|
62
58
|
bash "npm run dev" --cwd ./frontend # Auto-backgrounds if needed"""
|
|
63
|
-
|
|
59
|
+
|
|
64
60
|
@override
|
|
65
61
|
def get_interpreter(self) -> str:
|
|
66
62
|
"""Get the shell interpreter."""
|
|
67
63
|
if platform.system() == "Windows":
|
|
68
64
|
return "cmd.exe"
|
|
69
|
-
|
|
65
|
+
|
|
70
66
|
# Check for user's preferred shell from environment
|
|
71
67
|
shell = os.environ.get("SHELL", "/bin/bash")
|
|
72
|
-
|
|
68
|
+
|
|
73
69
|
# Extract just the shell name from the path
|
|
74
70
|
shell_name = os.path.basename(shell)
|
|
75
|
-
|
|
71
|
+
|
|
76
72
|
# Check if it's a supported shell and the config file exists
|
|
77
73
|
if shell_name == "zsh":
|
|
78
74
|
# Check for .zshrc
|
|
@@ -84,27 +80,27 @@ bash "npm run dev" --cwd ./frontend # Auto-backgrounds if needed"""
|
|
|
84
80
|
fish_config = Path.home() / ".config" / "fish" / "config.fish"
|
|
85
81
|
if fish_config.exists():
|
|
86
82
|
return shell # Use full path to fish
|
|
87
|
-
|
|
83
|
+
|
|
88
84
|
# Default to bash if no special shell config found
|
|
89
85
|
return "bash"
|
|
90
|
-
|
|
86
|
+
|
|
91
87
|
@override
|
|
92
88
|
def get_script_flags(self) -> list[str]:
|
|
93
89
|
"""Get interpreter flags."""
|
|
94
90
|
if platform.system() == "Windows":
|
|
95
91
|
return ["/c"]
|
|
96
92
|
return ["-c"]
|
|
97
|
-
|
|
93
|
+
|
|
98
94
|
@override
|
|
99
95
|
def get_tool_name(self) -> str:
|
|
100
96
|
"""Get the tool name."""
|
|
101
97
|
if platform.system() == "Windows":
|
|
102
98
|
return "shell"
|
|
103
|
-
|
|
99
|
+
|
|
104
100
|
# Return the actual shell being used
|
|
105
101
|
interpreter = self.get_interpreter()
|
|
106
102
|
return os.path.basename(interpreter)
|
|
107
|
-
|
|
103
|
+
|
|
108
104
|
@override
|
|
109
105
|
async def run(
|
|
110
106
|
self,
|
|
@@ -115,29 +111,26 @@ bash "npm run dev" --cwd ./frontend # Auto-backgrounds if needed"""
|
|
|
115
111
|
timeout: Optional[int] = None,
|
|
116
112
|
) -> str:
|
|
117
113
|
"""Run a shell command with auto-backgrounding.
|
|
118
|
-
|
|
114
|
+
|
|
119
115
|
Args:
|
|
120
116
|
ctx: MCP context
|
|
121
117
|
command: Shell command to execute
|
|
122
118
|
cwd: Working directory
|
|
123
119
|
env: Environment variables
|
|
124
120
|
timeout: Command timeout in seconds (ignored - auto-backgrounds after 2 minutes)
|
|
125
|
-
|
|
121
|
+
|
|
126
122
|
Returns:
|
|
127
123
|
Command output or background status
|
|
128
124
|
"""
|
|
129
125
|
# Prepare working directory
|
|
130
126
|
work_dir = Path(cwd).resolve() if cwd else Path.cwd()
|
|
131
|
-
|
|
127
|
+
|
|
132
128
|
# Always use execute_sync which now has auto-backgrounding
|
|
133
129
|
output = await self.execute_sync(
|
|
134
|
-
command,
|
|
135
|
-
cwd=work_dir,
|
|
136
|
-
env=env,
|
|
137
|
-
timeout=timeout
|
|
130
|
+
command, cwd=work_dir, env=env, timeout=timeout
|
|
138
131
|
)
|
|
139
132
|
return output if output else "Command completed successfully (no output)"
|
|
140
133
|
|
|
141
134
|
|
|
142
135
|
# Create tool instance
|
|
143
|
-
bash_tool = BashTool()
|
|
136
|
+
bash_tool = BashTool()
|
|
@@ -4,20 +4,19 @@ This module provides tools for executing shell commands and scripts with
|
|
|
4
4
|
comprehensive error handling, permissions checking, and progress tracking.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
import asyncio
|
|
8
|
-
import base64
|
|
9
7
|
import os
|
|
10
8
|
import re
|
|
9
|
+
import sys
|
|
11
10
|
import shlex
|
|
11
|
+
import base64
|
|
12
12
|
import shutil
|
|
13
|
-
import
|
|
13
|
+
import asyncio
|
|
14
14
|
import tempfile
|
|
15
|
-
from collections.abc import Awaitable, Callable
|
|
16
15
|
from typing import final
|
|
16
|
+
from collections.abc import Callable, Awaitable
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
from hanzo_mcp.tools.common.permissions import PermissionManager
|
|
20
18
|
from hanzo_mcp.tools.shell.base import CommandResult
|
|
19
|
+
from hanzo_mcp.tools.common.permissions import PermissionManager
|
|
21
20
|
|
|
22
21
|
|
|
23
22
|
@final
|
|
@@ -183,6 +182,7 @@ class CommandExecutor:
|
|
|
183
182
|
if data is not None:
|
|
184
183
|
try:
|
|
185
184
|
import json
|
|
185
|
+
|
|
186
186
|
logger = logging.getLogger(__name__)
|
|
187
187
|
if isinstance(data, (dict, list)):
|
|
188
188
|
data_str = json.dumps(data)
|
|
@@ -312,11 +312,11 @@ class CommandExecutor:
|
|
|
312
312
|
self._log(f"Escaped command: {escaped_command}")
|
|
313
313
|
|
|
314
314
|
# Wrap command with appropriate shell invocation
|
|
315
|
-
if
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
315
|
+
if (
|
|
316
|
+
shell_basename == "zsh"
|
|
317
|
+
or shell_basename == "bash"
|
|
318
|
+
or shell_basename == "fish"
|
|
319
|
+
):
|
|
320
320
|
shell_cmd = f"{user_shell} -l -c '{escaped_command}'"
|
|
321
321
|
else:
|
|
322
322
|
# Default fallback
|
|
@@ -691,7 +691,7 @@ class CommandExecutor:
|
|
|
691
691
|
match = re.match(r"([a-zA-Z]):\\(.*)", temp_path)
|
|
692
692
|
if match:
|
|
693
693
|
drive, path = match.groups()
|
|
694
|
-
wsl_path = f"/mnt/{drive.lower()}/{path.replace(
|
|
694
|
+
wsl_path = f"/mnt/{drive.lower()}/{path.replace(chr(92), '/')}"
|
|
695
695
|
else:
|
|
696
696
|
wsl_path = temp_path.replace("\\", "/")
|
|
697
697
|
self._log(f"WSL path conversion may be incomplete: {wsl_path}")
|