hanzo-mcp 0.5.1__py3-none-any.whl → 0.5.2__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 (54) hide show
  1. hanzo_mcp/__init__.py +1 -1
  2. hanzo_mcp/tools/__init__.py +135 -4
  3. hanzo_mcp/tools/common/base.py +7 -2
  4. hanzo_mcp/tools/common/stats.py +261 -0
  5. hanzo_mcp/tools/common/tool_disable.py +144 -0
  6. hanzo_mcp/tools/common/tool_enable.py +182 -0
  7. hanzo_mcp/tools/common/tool_list.py +263 -0
  8. hanzo_mcp/tools/database/__init__.py +71 -0
  9. hanzo_mcp/tools/database/database_manager.py +246 -0
  10. hanzo_mcp/tools/database/graph_add.py +257 -0
  11. hanzo_mcp/tools/database/graph_query.py +536 -0
  12. hanzo_mcp/tools/database/graph_remove.py +267 -0
  13. hanzo_mcp/tools/database/graph_search.py +348 -0
  14. hanzo_mcp/tools/database/graph_stats.py +345 -0
  15. hanzo_mcp/tools/database/sql_query.py +229 -0
  16. hanzo_mcp/tools/database/sql_search.py +296 -0
  17. hanzo_mcp/tools/database/sql_stats.py +254 -0
  18. hanzo_mcp/tools/editor/__init__.py +11 -0
  19. hanzo_mcp/tools/editor/neovim_command.py +272 -0
  20. hanzo_mcp/tools/editor/neovim_edit.py +290 -0
  21. hanzo_mcp/tools/editor/neovim_session.py +356 -0
  22. hanzo_mcp/tools/filesystem/__init__.py +15 -5
  23. hanzo_mcp/tools/filesystem/{unified_search.py → batch_search.py} +254 -131
  24. hanzo_mcp/tools/filesystem/find_files.py +348 -0
  25. hanzo_mcp/tools/filesystem/git_search.py +505 -0
  26. hanzo_mcp/tools/llm/__init__.py +27 -0
  27. hanzo_mcp/tools/llm/consensus_tool.py +351 -0
  28. hanzo_mcp/tools/llm/llm_manage.py +413 -0
  29. hanzo_mcp/tools/llm/llm_tool.py +346 -0
  30. hanzo_mcp/tools/llm/provider_tools.py +412 -0
  31. hanzo_mcp/tools/mcp/__init__.py +11 -0
  32. hanzo_mcp/tools/mcp/mcp_add.py +263 -0
  33. hanzo_mcp/tools/mcp/mcp_remove.py +127 -0
  34. hanzo_mcp/tools/mcp/mcp_stats.py +165 -0
  35. hanzo_mcp/tools/shell/__init__.py +27 -7
  36. hanzo_mcp/tools/shell/logs.py +265 -0
  37. hanzo_mcp/tools/shell/npx.py +194 -0
  38. hanzo_mcp/tools/shell/npx_background.py +254 -0
  39. hanzo_mcp/tools/shell/pkill.py +262 -0
  40. hanzo_mcp/tools/shell/processes.py +279 -0
  41. hanzo_mcp/tools/shell/run_background.py +326 -0
  42. hanzo_mcp/tools/shell/uvx.py +187 -0
  43. hanzo_mcp/tools/shell/uvx_background.py +249 -0
  44. hanzo_mcp/tools/vector/__init__.py +5 -0
  45. hanzo_mcp/tools/vector/git_ingester.py +3 -0
  46. hanzo_mcp/tools/vector/index_tool.py +358 -0
  47. hanzo_mcp/tools/vector/infinity_store.py +98 -0
  48. hanzo_mcp/tools/vector/vector_search.py +11 -6
  49. {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.5.2.dist-info}/METADATA +1 -1
  50. {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.5.2.dist-info}/RECORD +54 -16
  51. {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.5.2.dist-info}/WHEEL +0 -0
  52. {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.5.2.dist-info}/entry_points.txt +0 -0
  53. {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.5.2.dist-info}/licenses/LICENSE +0 -0
  54. {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.5.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,279 @@
1
+ """Tool for listing running background processes."""
2
+
3
+ import psutil
4
+ from datetime import datetime
5
+ from typing import Annotated, Optional, TypedDict, Unpack, final, override
6
+
7
+ from fastmcp import Context as MCPContext
8
+ from pydantic import Field
9
+
10
+ from hanzo_mcp.tools.common.base import BaseTool
11
+ from hanzo_mcp.tools.common.context import create_tool_context
12
+ from hanzo_mcp.tools.common.permissions import PermissionManager
13
+ from hanzo_mcp.tools.shell.run_background import RunBackgroundTool
14
+
15
+
16
+ ShowAll = Annotated[
17
+ bool,
18
+ Field(
19
+ description="Show all system processes (not just background processes)",
20
+ default=False,
21
+ ),
22
+ ]
23
+
24
+ FilterName = Annotated[
25
+ Optional[str],
26
+ Field(
27
+ description="Filter processes by name",
28
+ default=None,
29
+ ),
30
+ ]
31
+
32
+ ShowDetails = Annotated[
33
+ bool,
34
+ Field(
35
+ description="Show detailed process information",
36
+ default=False,
37
+ ),
38
+ ]
39
+
40
+
41
+ class ProcessesParams(TypedDict, total=False):
42
+ """Parameters for listing processes."""
43
+
44
+ show_all: bool
45
+ filter_name: Optional[str]
46
+ show_details: bool
47
+
48
+
49
+ @final
50
+ class ProcessesTool(BaseTool):
51
+ """Tool for listing running processes."""
52
+
53
+ def __init__(self, permission_manager: PermissionManager):
54
+ """Initialize the processes tool.
55
+
56
+ Args:
57
+ permission_manager: Permission manager for access control
58
+ """
59
+ self.permission_manager = permission_manager
60
+
61
+ @property
62
+ @override
63
+ def name(self) -> str:
64
+ """Get the tool name."""
65
+ return "processes"
66
+
67
+ @property
68
+ @override
69
+ def description(self) -> str:
70
+ """Get the tool description."""
71
+ return """List running background processes started with run_background.
72
+
73
+ Shows:
74
+ - Process ID (for use with pkill)
75
+ - Process name
76
+ - Command
77
+ - PID (system process ID)
78
+ - Status (running/finished)
79
+ - Start time
80
+ - Log file location
81
+
82
+ Options:
83
+ - show_all: Show all system processes (requires permissions)
84
+ - filter_name: Filter by process name
85
+ - show_details: Show CPU, memory usage
86
+
87
+ Examples:
88
+ - processes # List background processes
89
+ - processes --show-details # Include resource usage
90
+ - processes --filter-name npm # Show only npm processes
91
+ """
92
+
93
+ @override
94
+ async def call(
95
+ self,
96
+ ctx: MCPContext,
97
+ **params: Unpack[ProcessesParams],
98
+ ) -> str:
99
+ """List running processes.
100
+
101
+ Args:
102
+ ctx: MCP context
103
+ **params: Tool parameters
104
+
105
+ Returns:
106
+ Process listing
107
+ """
108
+ tool_ctx = create_tool_context(ctx)
109
+ await tool_ctx.set_tool_info(self.name)
110
+
111
+ # Extract parameters
112
+ show_all = params.get("show_all", False)
113
+ filter_name = params.get("filter_name")
114
+ show_details = params.get("show_details", False)
115
+
116
+ try:
117
+ if show_all:
118
+ # Show all system processes
119
+ await tool_ctx.info("Listing all system processes")
120
+ return self._list_system_processes(filter_name, show_details)
121
+ else:
122
+ # Show only background processes
123
+ await tool_ctx.info("Listing background processes")
124
+ return self._list_background_processes(filter_name, show_details)
125
+
126
+ except Exception as e:
127
+ await tool_ctx.error(f"Failed to list processes: {str(e)}")
128
+ return f"Error listing processes: {str(e)}"
129
+
130
+ def _list_background_processes(
131
+ self, filter_name: Optional[str], show_details: bool
132
+ ) -> str:
133
+ """List background processes started with run_background."""
134
+ processes = RunBackgroundTool.get_processes()
135
+
136
+ if not processes:
137
+ return "No background processes are currently running."
138
+
139
+ # Filter if requested
140
+ filtered_processes = []
141
+ for proc_id, process in processes.items():
142
+ if filter_name:
143
+ if filter_name.lower() not in process.name.lower():
144
+ continue
145
+ filtered_processes.append((proc_id, process))
146
+
147
+ if not filtered_processes:
148
+ return f"No background processes found matching '{filter_name}'."
149
+
150
+ # Build output
151
+ output = []
152
+ output.append("=== Background Processes ===\n")
153
+
154
+ # Sort by start time (newest first)
155
+ filtered_processes.sort(key=lambda x: x[1].start_time, reverse=True)
156
+
157
+ for proc_id, process in filtered_processes:
158
+ status = "running" if process.is_running else f"finished (code: {process.return_code})"
159
+ runtime = datetime.now() - process.start_time
160
+ runtime_str = str(runtime).split('.')[0] # Remove microseconds
161
+
162
+ output.append(f"ID: {proc_id}")
163
+ output.append(f"Name: {process.name}")
164
+ output.append(f"Status: {status}")
165
+ output.append(f"PID: {process.pid}")
166
+ output.append(f"Runtime: {runtime_str}")
167
+ output.append(f"Command: {process.command}")
168
+ output.append(f"Working Dir: {process.working_dir}")
169
+
170
+ if process.log_file:
171
+ output.append(f"Log File: {process.log_file}")
172
+
173
+ if show_details and process.is_running:
174
+ try:
175
+ # Get process details using psutil
176
+ p = psutil.Process(process.pid)
177
+ output.append(f"CPU: {p.cpu_percent(interval=0.1):.1f}%")
178
+ output.append(f"Memory: {p.memory_info().rss / 1024 / 1024:.1f} MB")
179
+ output.append(f"Threads: {p.num_threads()}")
180
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
181
+ output.append("Process details unavailable")
182
+
183
+ output.append("-" * 40)
184
+
185
+ output.append(f"\nTotal: {len(filtered_processes)} process(es)")
186
+ output.append("\nUse 'pkill --id <ID>' to stop a process")
187
+ output.append("Use 'logs --id <ID>' to view process logs")
188
+
189
+ return "\n".join(output)
190
+
191
+ def _list_system_processes(
192
+ self, filter_name: Optional[str], show_details: bool
193
+ ) -> str:
194
+ """List all system processes."""
195
+ try:
196
+ processes = []
197
+
198
+ # Get all running processes
199
+ for proc in psutil.process_iter(['pid', 'name', 'cmdline', 'create_time']):
200
+ try:
201
+ info = proc.info
202
+ name = info['name']
203
+
204
+ # Filter if requested
205
+ if filter_name:
206
+ if filter_name.lower() not in name.lower():
207
+ continue
208
+
209
+ # Get command line
210
+ cmdline = info.get('cmdline')
211
+ if cmdline:
212
+ cmd = ' '.join(cmdline)
213
+ else:
214
+ cmd = name
215
+
216
+ # Truncate long commands
217
+ if len(cmd) > 80:
218
+ cmd = cmd[:77] + "..."
219
+
220
+ process_info = {
221
+ 'pid': info['pid'],
222
+ 'name': name,
223
+ 'cmd': cmd,
224
+ 'create_time': info['create_time'],
225
+ }
226
+
227
+ if show_details:
228
+ process_info['cpu'] = proc.cpu_percent(interval=0.1)
229
+ process_info['memory'] = proc.memory_info().rss / 1024 / 1024 # MB
230
+
231
+ processes.append(process_info)
232
+
233
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
234
+ continue
235
+
236
+ if not processes:
237
+ return f"No processes found matching '{filter_name}'."
238
+
239
+ # Sort by PID
240
+ processes.sort(key=lambda x: x['pid'])
241
+
242
+ # Build output
243
+ output = []
244
+ output.append("=== System Processes ===\n")
245
+
246
+ # Header
247
+ if show_details:
248
+ output.append(f"{'PID':>7} {'CPU%':>5} {'MEM(MB)':>8} {'NAME':<20} COMMAND")
249
+ output.append("-" * 80)
250
+
251
+ for proc in processes:
252
+ output.append(
253
+ f"{proc['pid']:>7} "
254
+ f"{proc['cpu']:>5.1f} "
255
+ f"{proc['memory']:>8.1f} "
256
+ f"{proc['name']:<20} "
257
+ f"{proc['cmd']}"
258
+ )
259
+ else:
260
+ output.append(f"{'PID':>7} {'NAME':<20} COMMAND")
261
+ output.append("-" * 80)
262
+
263
+ for proc in processes:
264
+ output.append(
265
+ f"{proc['pid']:>7} "
266
+ f"{proc['name']:<20} "
267
+ f"{proc['cmd']}"
268
+ )
269
+
270
+ output.append(f"\nTotal: {len(processes)} process(es)")
271
+
272
+ return "\n".join(output)
273
+
274
+ except Exception as e:
275
+ return f"Error listing system processes: {str(e)}\nYou may need elevated permissions."
276
+
277
+ def register(self, mcp_server) -> None:
278
+ """Register this tool with the MCP server."""
279
+ pass
@@ -0,0 +1,326 @@
1
+ """Background process execution tool."""
2
+
3
+ import asyncio
4
+ import os
5
+ import subprocess
6
+ import time
7
+ import uuid
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+ from typing import Annotated, Optional, TypedDict, Unpack, final, override
11
+
12
+ from fastmcp import Context as MCPContext
13
+ from pydantic import Field
14
+
15
+ from hanzo_mcp.tools.common.base import BaseTool
16
+ from hanzo_mcp.tools.common.context import create_tool_context
17
+ from hanzo_mcp.tools.common.permissions import PermissionManager
18
+
19
+
20
+ Command = Annotated[
21
+ str,
22
+ Field(
23
+ description="The command to execute in the background",
24
+ min_length=1,
25
+ ),
26
+ ]
27
+
28
+ WorkingDir = Annotated[
29
+ Optional[str],
30
+ Field(
31
+ description="Working directory for the command",
32
+ default=None,
33
+ ),
34
+ ]
35
+
36
+ Name = Annotated[
37
+ Optional[str],
38
+ Field(
39
+ description="Name for the background process (for identification)",
40
+ default=None,
41
+ ),
42
+ ]
43
+
44
+ LogToFile = Annotated[
45
+ bool,
46
+ Field(
47
+ description="Whether to log output to file",
48
+ default=True,
49
+ ),
50
+ ]
51
+
52
+ Env = Annotated[
53
+ Optional[dict[str, str]],
54
+ Field(
55
+ description="Environment variables to set",
56
+ default=None,
57
+ ),
58
+ ]
59
+
60
+
61
+ class RunBackgroundParams(TypedDict, total=False):
62
+ """Parameters for running background commands."""
63
+
64
+ command: str
65
+ working_dir: Optional[str]
66
+ name: Optional[str]
67
+ log_to_file: bool
68
+ env: Optional[dict[str, str]]
69
+
70
+
71
+ class BackgroundProcess:
72
+ """Represents a running background process."""
73
+
74
+ def __init__(
75
+ self,
76
+ process_id: str,
77
+ command: str,
78
+ name: str,
79
+ working_dir: str,
80
+ log_file: Optional[Path],
81
+ process: subprocess.Popen,
82
+ ):
83
+ self.process_id = process_id
84
+ self.command = command
85
+ self.name = name
86
+ self.working_dir = working_dir
87
+ self.log_file = log_file
88
+ self.process = process
89
+ self.start_time = datetime.now()
90
+ self.end_time: Optional[datetime] = None
91
+
92
+ @property
93
+ def is_running(self) -> bool:
94
+ """Check if process is still running."""
95
+ return self.process.poll() is None
96
+
97
+ @property
98
+ def pid(self) -> int:
99
+ """Get process ID."""
100
+ return self.process.pid
101
+
102
+ @property
103
+ def return_code(self) -> Optional[int]:
104
+ """Get return code if process has finished."""
105
+ return self.process.poll()
106
+
107
+ def terminate(self) -> None:
108
+ """Terminate the process."""
109
+ if self.is_running:
110
+ self.process.terminate()
111
+ self.end_time = datetime.now()
112
+
113
+ def kill(self) -> None:
114
+ """Kill the process forcefully."""
115
+ if self.is_running:
116
+ self.process.kill()
117
+ self.end_time = datetime.now()
118
+
119
+ def to_dict(self) -> dict:
120
+ """Convert to dictionary for display."""
121
+ return {
122
+ "id": self.process_id,
123
+ "name": self.name,
124
+ "command": self.command,
125
+ "pid": self.pid,
126
+ "working_dir": self.working_dir,
127
+ "log_file": str(self.log_file) if self.log_file else None,
128
+ "start_time": self.start_time.isoformat(),
129
+ "end_time": self.end_time.isoformat() if self.end_time else None,
130
+ "is_running": self.is_running,
131
+ "return_code": self.return_code,
132
+ }
133
+
134
+
135
+ @final
136
+ class RunBackgroundTool(BaseTool):
137
+ """Tool for running commands in the background."""
138
+
139
+ # Class variable to store running processes
140
+ _processes: dict[str, BackgroundProcess] = {}
141
+
142
+ def __init__(self, permission_manager: PermissionManager):
143
+ """Initialize the background runner tool.
144
+
145
+ Args:
146
+ permission_manager: Permission manager for access control
147
+ """
148
+ self.permission_manager = permission_manager
149
+ self.log_dir = Path.home() / ".hanzo" / "logs"
150
+ self.log_dir.mkdir(parents=True, exist_ok=True)
151
+
152
+ @property
153
+ @override
154
+ def name(self) -> str:
155
+ """Get the tool name."""
156
+ return "run_background"
157
+
158
+ @property
159
+ @override
160
+ def description(self) -> str:
161
+ """Get the tool description."""
162
+ return """Run a command in the background, allowing it to continue running.
163
+
164
+ Perfect for:
165
+ - Starting development servers (npm run dev, python -m http.server)
166
+ - Running long-running processes (file watchers, build processes)
167
+ - Starting services that need to keep running
168
+ - Running multiple processes concurrently
169
+
170
+ Features:
171
+ - Runs process in background, returns immediately
172
+ - Optional logging to ~/.hanzo/logs
173
+ - Process tracking with unique IDs
174
+ - Working directory support
175
+ - Environment variable support
176
+
177
+ Use 'processes' tool to list running processes
178
+ Use 'pkill' tool to terminate processes
179
+ Use 'logs' tool to view process output
180
+
181
+ Examples:
182
+ - run_background --command "npm run dev" --name "frontend-server"
183
+ - run_background --command "python app.py" --working-dir "/path/to/app"
184
+ """
185
+
186
+ @override
187
+ async def call(
188
+ self,
189
+ ctx: MCPContext,
190
+ **params: Unpack[RunBackgroundParams],
191
+ ) -> str:
192
+ """Execute a command in the background.
193
+
194
+ Args:
195
+ ctx: MCP context
196
+ **params: Tool parameters
197
+
198
+ Returns:
199
+ Information about the started process
200
+ """
201
+ tool_ctx = create_tool_context(ctx)
202
+ await tool_ctx.set_tool_info(self.name)
203
+
204
+ # Extract parameters
205
+ command = params.get("command")
206
+ if not command:
207
+ return "Error: command is required"
208
+
209
+ working_dir = params.get("working_dir", os.getcwd())
210
+ name = params.get("name", command.split()[0])
211
+ log_to_file = params.get("log_to_file", True)
212
+ env = params.get("env")
213
+
214
+ # Resolve absolute path for working directory
215
+ abs_working_dir = os.path.abspath(working_dir)
216
+
217
+ # Check permissions
218
+ if not self.permission_manager.has_permission(abs_working_dir):
219
+ return f"Permission denied: {abs_working_dir}"
220
+
221
+ # Check if working directory exists
222
+ if not os.path.exists(abs_working_dir):
223
+ return f"Working directory does not exist: {abs_working_dir}"
224
+
225
+ # Generate process ID
226
+ process_id = str(uuid.uuid4())[:8]
227
+
228
+ # Setup logging
229
+ log_file = None
230
+ if log_to_file:
231
+ log_filename = f"{process_id}_{name.replace(' ', '_')}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log"
232
+ log_file = self.log_dir / log_filename
233
+
234
+ await tool_ctx.info(f"Starting background process: {name}")
235
+
236
+ try:
237
+ # Prepare environment
238
+ process_env = os.environ.copy()
239
+ if env:
240
+ process_env.update(env)
241
+
242
+ # Open log file for writing
243
+ if log_file:
244
+ log_handle = open(log_file, 'w')
245
+ stdout = log_handle
246
+ stderr = subprocess.STDOUT
247
+ else:
248
+ stdout = subprocess.DEVNULL
249
+ stderr = subprocess.DEVNULL
250
+
251
+ # Start the process
252
+ process = subprocess.Popen(
253
+ command,
254
+ shell=True,
255
+ cwd=abs_working_dir,
256
+ env=process_env,
257
+ stdout=stdout,
258
+ stderr=stderr,
259
+ # Don't wait for process to complete
260
+ start_new_session=True, # Detach from parent process group
261
+ )
262
+
263
+ # Create process object
264
+ bg_process = BackgroundProcess(
265
+ process_id=process_id,
266
+ command=command,
267
+ name=name,
268
+ working_dir=abs_working_dir,
269
+ log_file=log_file,
270
+ process=process,
271
+ )
272
+
273
+ # Store in class variable
274
+ RunBackgroundTool._processes[process_id] = bg_process
275
+
276
+ # Clean up finished processes
277
+ self._cleanup_finished_processes()
278
+
279
+ await tool_ctx.info(f"Process started with ID: {process_id}, PID: {process.pid}")
280
+
281
+ # Return process information
282
+ return f"""Background process started successfully!
283
+
284
+ Process ID: {process_id}
285
+ Name: {name}
286
+ PID: {process.pid}
287
+ Command: {command}
288
+ Working Directory: {abs_working_dir}
289
+ Log File: {log_file if log_file else 'Not logging'}
290
+
291
+ Use 'processes' to list all running processes
292
+ Use 'pkill --id {process_id}' to stop this process
293
+ Use 'logs --id {process_id}' to view output (if logging enabled)
294
+ """
295
+
296
+ except Exception as e:
297
+ await tool_ctx.error(f"Failed to start background process: {str(e)}")
298
+ return f"Error starting background process: {str(e)}"
299
+
300
+ @classmethod
301
+ def get_processes(cls) -> dict[str, BackgroundProcess]:
302
+ """Get all tracked processes."""
303
+ return cls._processes
304
+
305
+ @classmethod
306
+ def get_process(cls, process_id: str) -> Optional[BackgroundProcess]:
307
+ """Get a specific process by ID."""
308
+ return cls._processes.get(process_id)
309
+
310
+ def _cleanup_finished_processes(self) -> None:
311
+ """Remove finished processes that have been terminated for a while."""
312
+ now = datetime.now()
313
+ to_remove = []
314
+
315
+ for process_id, process in RunBackgroundTool._processes.items():
316
+ if not process.is_running and process.end_time:
317
+ # Keep finished processes for 5 minutes for log access
318
+ if (now - process.end_time).total_seconds() > 300:
319
+ to_remove.append(process_id)
320
+
321
+ for process_id in to_remove:
322
+ del RunBackgroundTool._processes[process_id]
323
+
324
+ def register(self, mcp_server) -> None:
325
+ """Register this tool with the MCP server."""
326
+ pass