hanzo-mcp 0.3.4__py3-none-any.whl → 0.5.0__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.

Files changed (87) hide show
  1. hanzo_mcp/__init__.py +1 -1
  2. hanzo_mcp/cli.py +123 -160
  3. hanzo_mcp/cli_enhanced.py +438 -0
  4. hanzo_mcp/config/__init__.py +19 -0
  5. hanzo_mcp/config/settings.py +388 -0
  6. hanzo_mcp/config/tool_config.py +197 -0
  7. hanzo_mcp/prompts/__init__.py +117 -0
  8. hanzo_mcp/prompts/compact_conversation.py +77 -0
  9. hanzo_mcp/prompts/create_release.py +38 -0
  10. hanzo_mcp/prompts/project_system.py +120 -0
  11. hanzo_mcp/prompts/project_todo_reminder.py +111 -0
  12. hanzo_mcp/prompts/utils.py +286 -0
  13. hanzo_mcp/server.py +120 -98
  14. hanzo_mcp/tools/__init__.py +107 -31
  15. hanzo_mcp/tools/agent/__init__.py +8 -11
  16. hanzo_mcp/tools/agent/agent_tool.py +290 -224
  17. hanzo_mcp/tools/agent/prompt.py +16 -13
  18. hanzo_mcp/tools/agent/tool_adapter.py +9 -9
  19. hanzo_mcp/tools/common/__init__.py +17 -16
  20. hanzo_mcp/tools/common/base.py +79 -110
  21. hanzo_mcp/tools/common/batch_tool.py +330 -0
  22. hanzo_mcp/tools/common/context.py +26 -292
  23. hanzo_mcp/tools/common/permissions.py +12 -12
  24. hanzo_mcp/tools/common/thinking_tool.py +153 -0
  25. hanzo_mcp/tools/common/validation.py +1 -63
  26. hanzo_mcp/tools/filesystem/__init__.py +88 -41
  27. hanzo_mcp/tools/filesystem/base.py +32 -24
  28. hanzo_mcp/tools/filesystem/content_replace.py +114 -107
  29. hanzo_mcp/tools/filesystem/directory_tree.py +129 -105
  30. hanzo_mcp/tools/filesystem/edit.py +279 -0
  31. hanzo_mcp/tools/filesystem/grep.py +458 -0
  32. hanzo_mcp/tools/filesystem/grep_ast_tool.py +250 -0
  33. hanzo_mcp/tools/filesystem/multi_edit.py +362 -0
  34. hanzo_mcp/tools/filesystem/read.py +255 -0
  35. hanzo_mcp/tools/filesystem/write.py +156 -0
  36. hanzo_mcp/tools/jupyter/__init__.py +41 -29
  37. hanzo_mcp/tools/jupyter/base.py +66 -57
  38. hanzo_mcp/tools/jupyter/{edit_notebook.py → notebook_edit.py} +162 -139
  39. hanzo_mcp/tools/jupyter/notebook_read.py +152 -0
  40. hanzo_mcp/tools/shell/__init__.py +29 -20
  41. hanzo_mcp/tools/shell/base.py +87 -45
  42. hanzo_mcp/tools/shell/bash_session.py +731 -0
  43. hanzo_mcp/tools/shell/bash_session_executor.py +295 -0
  44. hanzo_mcp/tools/shell/command_executor.py +435 -384
  45. hanzo_mcp/tools/shell/run_command.py +284 -131
  46. hanzo_mcp/tools/shell/run_command_windows.py +328 -0
  47. hanzo_mcp/tools/shell/session_manager.py +196 -0
  48. hanzo_mcp/tools/shell/session_storage.py +325 -0
  49. hanzo_mcp/tools/todo/__init__.py +66 -0
  50. hanzo_mcp/tools/todo/base.py +319 -0
  51. hanzo_mcp/tools/todo/todo_read.py +148 -0
  52. hanzo_mcp/tools/todo/todo_write.py +378 -0
  53. hanzo_mcp/tools/vector/__init__.py +95 -0
  54. hanzo_mcp/tools/vector/infinity_store.py +365 -0
  55. hanzo_mcp/tools/vector/project_manager.py +361 -0
  56. hanzo_mcp/tools/vector/vector_index.py +115 -0
  57. hanzo_mcp/tools/vector/vector_search.py +215 -0
  58. {hanzo_mcp-0.3.4.dist-info → hanzo_mcp-0.5.0.dist-info}/METADATA +35 -3
  59. hanzo_mcp-0.5.0.dist-info/RECORD +63 -0
  60. {hanzo_mcp-0.3.4.dist-info → hanzo_mcp-0.5.0.dist-info}/WHEEL +1 -1
  61. hanzo_mcp/tools/agent/base_provider.py +0 -73
  62. hanzo_mcp/tools/agent/litellm_provider.py +0 -45
  63. hanzo_mcp/tools/agent/lmstudio_agent.py +0 -385
  64. hanzo_mcp/tools/agent/lmstudio_provider.py +0 -219
  65. hanzo_mcp/tools/agent/provider_registry.py +0 -120
  66. hanzo_mcp/tools/common/error_handling.py +0 -86
  67. hanzo_mcp/tools/common/logging_config.py +0 -115
  68. hanzo_mcp/tools/common/session.py +0 -91
  69. hanzo_mcp/tools/common/think_tool.py +0 -123
  70. hanzo_mcp/tools/common/version_tool.py +0 -120
  71. hanzo_mcp/tools/filesystem/edit_file.py +0 -287
  72. hanzo_mcp/tools/filesystem/get_file_info.py +0 -170
  73. hanzo_mcp/tools/filesystem/read_files.py +0 -198
  74. hanzo_mcp/tools/filesystem/search_content.py +0 -275
  75. hanzo_mcp/tools/filesystem/write_file.py +0 -162
  76. hanzo_mcp/tools/jupyter/notebook_operations.py +0 -514
  77. hanzo_mcp/tools/jupyter/read_notebook.py +0 -165
  78. hanzo_mcp/tools/project/__init__.py +0 -64
  79. hanzo_mcp/tools/project/analysis.py +0 -882
  80. hanzo_mcp/tools/project/base.py +0 -66
  81. hanzo_mcp/tools/project/project_analyze.py +0 -173
  82. hanzo_mcp/tools/shell/run_script.py +0 -215
  83. hanzo_mcp/tools/shell/script_tool.py +0 -244
  84. hanzo_mcp-0.3.4.dist-info/RECORD +0 -53
  85. {hanzo_mcp-0.3.4.dist-info → hanzo_mcp-0.5.0.dist-info}/entry_points.txt +0 -0
  86. {hanzo_mcp-0.3.4.dist-info → hanzo_mcp-0.5.0.dist-info}/licenses/LICENSE +0 -0
  87. {hanzo_mcp-0.3.4.dist-info → hanzo_mcp-0.5.0.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
+ )