hanzo-mcp 0.3.8__py3-none-any.whl → 0.5.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 +1 -1
- hanzo_mcp/cli.py +118 -170
- hanzo_mcp/cli_enhanced.py +438 -0
- hanzo_mcp/config/__init__.py +19 -0
- hanzo_mcp/config/settings.py +449 -0
- hanzo_mcp/config/tool_config.py +197 -0
- hanzo_mcp/prompts/__init__.py +117 -0
- hanzo_mcp/prompts/compact_conversation.py +77 -0
- hanzo_mcp/prompts/create_release.py +38 -0
- hanzo_mcp/prompts/project_system.py +120 -0
- hanzo_mcp/prompts/project_todo_reminder.py +111 -0
- hanzo_mcp/prompts/utils.py +286 -0
- hanzo_mcp/server.py +117 -99
- hanzo_mcp/tools/__init__.py +121 -33
- hanzo_mcp/tools/agent/__init__.py +8 -11
- hanzo_mcp/tools/agent/agent_tool.py +290 -224
- hanzo_mcp/tools/agent/prompt.py +16 -13
- hanzo_mcp/tools/agent/tool_adapter.py +9 -9
- hanzo_mcp/tools/common/__init__.py +17 -16
- hanzo_mcp/tools/common/base.py +79 -110
- hanzo_mcp/tools/common/batch_tool.py +330 -0
- hanzo_mcp/tools/common/config_tool.py +396 -0
- hanzo_mcp/tools/common/context.py +26 -292
- hanzo_mcp/tools/common/permissions.py +12 -12
- hanzo_mcp/tools/common/thinking_tool.py +153 -0
- hanzo_mcp/tools/common/validation.py +1 -63
- hanzo_mcp/tools/filesystem/__init__.py +97 -57
- hanzo_mcp/tools/filesystem/base.py +32 -24
- hanzo_mcp/tools/filesystem/content_replace.py +114 -107
- hanzo_mcp/tools/filesystem/directory_tree.py +129 -105
- hanzo_mcp/tools/filesystem/edit.py +279 -0
- hanzo_mcp/tools/filesystem/grep.py +458 -0
- hanzo_mcp/tools/filesystem/grep_ast_tool.py +250 -0
- hanzo_mcp/tools/filesystem/multi_edit.py +362 -0
- hanzo_mcp/tools/filesystem/read.py +255 -0
- hanzo_mcp/tools/filesystem/unified_search.py +689 -0
- hanzo_mcp/tools/filesystem/write.py +156 -0
- hanzo_mcp/tools/jupyter/__init__.py +41 -29
- hanzo_mcp/tools/jupyter/base.py +66 -57
- hanzo_mcp/tools/jupyter/{edit_notebook.py → notebook_edit.py} +162 -139
- hanzo_mcp/tools/jupyter/notebook_read.py +152 -0
- hanzo_mcp/tools/shell/__init__.py +29 -20
- hanzo_mcp/tools/shell/base.py +87 -45
- hanzo_mcp/tools/shell/bash_session.py +731 -0
- hanzo_mcp/tools/shell/bash_session_executor.py +295 -0
- hanzo_mcp/tools/shell/command_executor.py +435 -384
- hanzo_mcp/tools/shell/run_command.py +284 -131
- hanzo_mcp/tools/shell/run_command_windows.py +328 -0
- hanzo_mcp/tools/shell/session_manager.py +196 -0
- hanzo_mcp/tools/shell/session_storage.py +325 -0
- hanzo_mcp/tools/todo/__init__.py +66 -0
- hanzo_mcp/tools/todo/base.py +319 -0
- hanzo_mcp/tools/todo/todo_read.py +148 -0
- hanzo_mcp/tools/todo/todo_write.py +378 -0
- hanzo_mcp/tools/vector/__init__.py +99 -0
- hanzo_mcp/tools/vector/ast_analyzer.py +459 -0
- hanzo_mcp/tools/vector/git_ingester.py +482 -0
- hanzo_mcp/tools/vector/infinity_store.py +731 -0
- hanzo_mcp/tools/vector/mock_infinity.py +162 -0
- hanzo_mcp/tools/vector/project_manager.py +361 -0
- hanzo_mcp/tools/vector/vector_index.py +116 -0
- hanzo_mcp/tools/vector/vector_search.py +225 -0
- hanzo_mcp-0.5.1.dist-info/METADATA +276 -0
- hanzo_mcp-0.5.1.dist-info/RECORD +68 -0
- {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.1.dist-info}/WHEEL +1 -1
- hanzo_mcp/tools/agent/base_provider.py +0 -73
- hanzo_mcp/tools/agent/litellm_provider.py +0 -45
- hanzo_mcp/tools/agent/lmstudio_agent.py +0 -385
- hanzo_mcp/tools/agent/lmstudio_provider.py +0 -219
- hanzo_mcp/tools/agent/provider_registry.py +0 -120
- hanzo_mcp/tools/common/error_handling.py +0 -86
- hanzo_mcp/tools/common/logging_config.py +0 -115
- hanzo_mcp/tools/common/session.py +0 -91
- hanzo_mcp/tools/common/think_tool.py +0 -123
- hanzo_mcp/tools/common/version_tool.py +0 -120
- hanzo_mcp/tools/filesystem/edit_file.py +0 -287
- hanzo_mcp/tools/filesystem/get_file_info.py +0 -170
- hanzo_mcp/tools/filesystem/read_files.py +0 -199
- hanzo_mcp/tools/filesystem/search_content.py +0 -275
- hanzo_mcp/tools/filesystem/write_file.py +0 -162
- hanzo_mcp/tools/jupyter/notebook_operations.py +0 -514
- hanzo_mcp/tools/jupyter/read_notebook.py +0 -165
- hanzo_mcp/tools/project/__init__.py +0 -64
- hanzo_mcp/tools/project/analysis.py +0 -886
- hanzo_mcp/tools/project/base.py +0 -66
- hanzo_mcp/tools/project/project_analyze.py +0 -173
- hanzo_mcp/tools/shell/run_script.py +0 -215
- hanzo_mcp/tools/shell/script_tool.py +0 -244
- hanzo_mcp-0.3.8.dist-info/METADATA +0 -196
- hanzo_mcp-0.3.8.dist-info/RECORD +0 -53
- {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.1.dist-info}/entry_points.txt +0 -0
- {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.1.dist-info}/licenses/LICENSE +0 -0
- {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""Bash session executor for Hanzo MCP.
|
|
2
|
+
|
|
3
|
+
This module provides a BashSessionExecutor class that replaces the old CommandExecutor
|
|
4
|
+
implementation with the new BashSession-based approach for better persistent execution.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import os
|
|
9
|
+
import shlex
|
|
10
|
+
import subprocess
|
|
11
|
+
from typing import final
|
|
12
|
+
|
|
13
|
+
from hanzo_mcp.tools.common.permissions import PermissionManager
|
|
14
|
+
from hanzo_mcp.tools.shell.base import BashCommandStatus, CommandResult
|
|
15
|
+
from hanzo_mcp.tools.shell.session_manager import SessionManager
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@final
|
|
19
|
+
class BashSessionExecutor:
|
|
20
|
+
"""Command executor using BashSession for persistent execution.
|
|
21
|
+
|
|
22
|
+
This class provides the same interface as the old CommandExecutor but uses
|
|
23
|
+
the new BashSession implementation for better persistent command execution.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
permission_manager: PermissionManager,
|
|
29
|
+
verbose: bool = False,
|
|
30
|
+
session_manager: SessionManager | None = None,
|
|
31
|
+
fast_test_mode: bool = False,
|
|
32
|
+
) -> None:
|
|
33
|
+
"""Initialize bash session executor.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
permission_manager: Permission manager for access control
|
|
37
|
+
verbose: Enable verbose logging
|
|
38
|
+
session_manager: Optional session manager for dependency injection.
|
|
39
|
+
If None, creates a new SessionManager instance.
|
|
40
|
+
fast_test_mode: Enable fast test mode with reduced timeouts and polling intervals
|
|
41
|
+
"""
|
|
42
|
+
self.permission_manager: PermissionManager = permission_manager
|
|
43
|
+
self.verbose: bool = verbose
|
|
44
|
+
self.fast_test_mode: bool = fast_test_mode
|
|
45
|
+
|
|
46
|
+
# If no session manager is provided, create a non-singleton instance to avoid shared state
|
|
47
|
+
self.session_manager: SessionManager = session_manager or SessionManager(
|
|
48
|
+
use_singleton=False
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Excluded commands or patterns (for compatibility)
|
|
52
|
+
self.excluded_commands: list[str] = ["rm"]
|
|
53
|
+
|
|
54
|
+
def _log(self, message: str, data: object | None = None) -> None:
|
|
55
|
+
"""Log a message if verbose logging is enabled.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
message: The message to log
|
|
59
|
+
data: Optional data to include with the message
|
|
60
|
+
"""
|
|
61
|
+
if not self.verbose:
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
if data is not None:
|
|
65
|
+
try:
|
|
66
|
+
import json
|
|
67
|
+
|
|
68
|
+
if isinstance(data, (dict, list)):
|
|
69
|
+
data_str = json.dumps(data)
|
|
70
|
+
else:
|
|
71
|
+
data_str = str(data)
|
|
72
|
+
print(f"DEBUG: {message}: {data_str}")
|
|
73
|
+
except Exception:
|
|
74
|
+
print(f"DEBUG: {message}: {data}")
|
|
75
|
+
else:
|
|
76
|
+
print(f"DEBUG: {message}")
|
|
77
|
+
|
|
78
|
+
def allow_command(self, command: str) -> None:
|
|
79
|
+
"""Allow a specific command that might otherwise be excluded.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
command: The command to allow
|
|
83
|
+
"""
|
|
84
|
+
if command in self.excluded_commands:
|
|
85
|
+
self.excluded_commands.remove(command)
|
|
86
|
+
|
|
87
|
+
def deny_command(self, command: str) -> None:
|
|
88
|
+
"""Deny a specific command, adding it to the excluded list.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
command: The command to deny
|
|
92
|
+
"""
|
|
93
|
+
if command not in self.excluded_commands:
|
|
94
|
+
self.excluded_commands.append(command)
|
|
95
|
+
|
|
96
|
+
def is_command_allowed(self, command: str) -> bool:
|
|
97
|
+
"""Check if a command is allowed based on exclusion lists.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
command: The command to check
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
True if the command is allowed, False otherwise
|
|
104
|
+
"""
|
|
105
|
+
# Check for empty commands
|
|
106
|
+
try:
|
|
107
|
+
args: list[str] = shlex.split(command)
|
|
108
|
+
except ValueError as e:
|
|
109
|
+
self._log(f"Command parsing error: {e}")
|
|
110
|
+
return False
|
|
111
|
+
|
|
112
|
+
if not args:
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
base_command: str = args[0]
|
|
116
|
+
|
|
117
|
+
# Check if base command is in exclusion list
|
|
118
|
+
if base_command in self.excluded_commands:
|
|
119
|
+
self._log(f"Command rejected (in exclusion list): {base_command}")
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
return True
|
|
123
|
+
|
|
124
|
+
async def execute_command(
|
|
125
|
+
self,
|
|
126
|
+
command: str,
|
|
127
|
+
env: dict[str, str] | None = None,
|
|
128
|
+
timeout: float | None = 60.0,
|
|
129
|
+
session_id: str = "",
|
|
130
|
+
is_input: bool = False,
|
|
131
|
+
blocking: bool = False,
|
|
132
|
+
) -> CommandResult:
|
|
133
|
+
"""Execute a shell command with safety checks.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
command: The command to execute
|
|
137
|
+
env: Optional environment variables
|
|
138
|
+
timeout: Optional timeout in seconds
|
|
139
|
+
session_id: Optional session ID for persistent execution
|
|
140
|
+
is_input: Whether this is input to a running process
|
|
141
|
+
blocking: Whether to run in blocking mode
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
CommandResult containing execution results
|
|
145
|
+
"""
|
|
146
|
+
self._log(
|
|
147
|
+
f"Executing command: {command} (is_input={is_input}, blocking={blocking})"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Check if the command is allowed (skip for input to running processes)
|
|
151
|
+
if not is_input and not self.is_command_allowed(command):
|
|
152
|
+
return CommandResult(
|
|
153
|
+
return_code=1,
|
|
154
|
+
error_message=f"Command not allowed: {command}",
|
|
155
|
+
session_id=session_id,
|
|
156
|
+
command=command,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# Handle subprocess mode when session_id is explicitly None
|
|
160
|
+
if not session_id:
|
|
161
|
+
return await self._execute_subprocess_mode(command, env, timeout)
|
|
162
|
+
|
|
163
|
+
# Default working directory for new sessions only
|
|
164
|
+
# Existing sessions maintain their current working directory
|
|
165
|
+
# Users should use 'cd' commands to navigate within sessions
|
|
166
|
+
default_work_dir = os.path.expanduser("~")
|
|
167
|
+
|
|
168
|
+
# Generate default session ID if none provided
|
|
169
|
+
effective_session_id = session_id
|
|
170
|
+
if effective_session_id == "":
|
|
171
|
+
import uuid
|
|
172
|
+
|
|
173
|
+
effective_session_id = f"default_{uuid.uuid4().hex[:8]}"
|
|
174
|
+
self._log(f"Generated default session ID: {effective_session_id}")
|
|
175
|
+
|
|
176
|
+
# Use session-based execution
|
|
177
|
+
try:
|
|
178
|
+
# Get existing session or create new one
|
|
179
|
+
session = self.session_manager.get_session(effective_session_id)
|
|
180
|
+
if session is None:
|
|
181
|
+
# Use faster timeouts and polling for tests
|
|
182
|
+
if self.fast_test_mode:
|
|
183
|
+
timeout_seconds = (
|
|
184
|
+
10 # Faster timeout for tests but not too aggressive
|
|
185
|
+
)
|
|
186
|
+
poll_interval = 0.2 # Faster polling for tests but still reasonable
|
|
187
|
+
else:
|
|
188
|
+
timeout_seconds = 30 # Default timeout
|
|
189
|
+
poll_interval = 0.5 # Default polling
|
|
190
|
+
|
|
191
|
+
session = self.session_manager.get_or_create_session(
|
|
192
|
+
session_id=effective_session_id,
|
|
193
|
+
work_dir=default_work_dir,
|
|
194
|
+
no_change_timeout_seconds=timeout_seconds,
|
|
195
|
+
poll_interval=poll_interval,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# Set environment variables if provided (only for new commands, not input)
|
|
199
|
+
if env and not is_input:
|
|
200
|
+
for key, value in env.items():
|
|
201
|
+
env_result = session.execute(f'export {key}="{value}"')
|
|
202
|
+
if env_result.return_code != 0:
|
|
203
|
+
self._log(f"Failed to set environment variable {key}")
|
|
204
|
+
|
|
205
|
+
# Execute the command with enhanced parameters
|
|
206
|
+
result = session.execute(
|
|
207
|
+
command=command, is_input=is_input, blocking=blocking, timeout=timeout
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# Add session_id to the result
|
|
211
|
+
result.session_id = effective_session_id
|
|
212
|
+
return result
|
|
213
|
+
except Exception as e:
|
|
214
|
+
self._log(f"Session execution error: {str(e)}")
|
|
215
|
+
return CommandResult(
|
|
216
|
+
return_code=1,
|
|
217
|
+
error_message=f"Error executing command in session: {str(e)}",
|
|
218
|
+
session_id=effective_session_id,
|
|
219
|
+
command=command,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
async def _execute_subprocess_mode(
|
|
223
|
+
self,
|
|
224
|
+
command: str,
|
|
225
|
+
env: dict[str, str] | None = None,
|
|
226
|
+
timeout: float | None = 60.0,
|
|
227
|
+
) -> CommandResult:
|
|
228
|
+
"""Execute command in true subprocess mode with no persistence.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
command: The command to execute
|
|
232
|
+
env: Optional environment variables
|
|
233
|
+
timeout: Optional timeout in seconds
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
CommandResult containing execution results
|
|
237
|
+
"""
|
|
238
|
+
self._log(f"Executing command in subprocess mode: {command}")
|
|
239
|
+
|
|
240
|
+
# Prepare environment - start with current env and add any custom vars
|
|
241
|
+
subprocess_env = os.environ.copy()
|
|
242
|
+
if env:
|
|
243
|
+
subprocess_env.update(env)
|
|
244
|
+
|
|
245
|
+
try:
|
|
246
|
+
# Use asyncio.create_subprocess_shell for async execution
|
|
247
|
+
process = await asyncio.create_subprocess_shell(
|
|
248
|
+
command,
|
|
249
|
+
stdout=asyncio.subprocess.PIPE,
|
|
250
|
+
stderr=asyncio.subprocess.PIPE,
|
|
251
|
+
env=subprocess_env,
|
|
252
|
+
cwd=os.path.expanduser("~"), # Start in home directory
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
# Wait for completion with timeout
|
|
256
|
+
try:
|
|
257
|
+
stdout, stderr = await asyncio.wait_for(
|
|
258
|
+
process.communicate(), timeout=timeout
|
|
259
|
+
)
|
|
260
|
+
except asyncio.TimeoutError:
|
|
261
|
+
# Kill the process if it times out
|
|
262
|
+
process.kill()
|
|
263
|
+
await process.wait()
|
|
264
|
+
return CommandResult(
|
|
265
|
+
return_code=-1,
|
|
266
|
+
stdout="",
|
|
267
|
+
stderr="",
|
|
268
|
+
error_message=f"Command timed out after {timeout} seconds",
|
|
269
|
+
session_id=None,
|
|
270
|
+
command=command,
|
|
271
|
+
status=BashCommandStatus.HARD_TIMEOUT,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
# Decode output
|
|
275
|
+
stdout_str = stdout.decode("utf-8", errors="replace") if stdout else ""
|
|
276
|
+
stderr_str = stderr.decode("utf-8", errors="replace") if stderr else ""
|
|
277
|
+
|
|
278
|
+
return CommandResult(
|
|
279
|
+
return_code=process.returncode or 0,
|
|
280
|
+
stdout=stdout_str,
|
|
281
|
+
stderr=stderr_str,
|
|
282
|
+
session_id=None,
|
|
283
|
+
command=command,
|
|
284
|
+
status=BashCommandStatus.COMPLETED,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
except Exception as e:
|
|
288
|
+
self._log(f"Subprocess execution error: {str(e)}")
|
|
289
|
+
return CommandResult(
|
|
290
|
+
return_code=1,
|
|
291
|
+
error_message=f"Error executing command in subprocess: {str(e)}",
|
|
292
|
+
session_id=None,
|
|
293
|
+
command=command,
|
|
294
|
+
status=BashCommandStatus.COMPLETED,
|
|
295
|
+
)
|