hanzo-mcp 0.5.1__py3-none-any.whl → 0.6.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 +32 -0
- hanzo_mcp/dev_server.py +246 -0
- hanzo_mcp/prompts/__init__.py +1 -1
- hanzo_mcp/prompts/project_system.py +43 -7
- hanzo_mcp/server.py +5 -1
- hanzo_mcp/tools/__init__.py +168 -6
- hanzo_mcp/tools/agent/__init__.py +1 -1
- hanzo_mcp/tools/agent/agent.py +401 -0
- hanzo_mcp/tools/agent/agent_tool.py +3 -4
- hanzo_mcp/tools/common/__init__.py +1 -1
- hanzo_mcp/tools/common/base.py +9 -4
- hanzo_mcp/tools/common/batch_tool.py +3 -5
- hanzo_mcp/tools/common/config_tool.py +1 -1
- hanzo_mcp/tools/common/context.py +1 -1
- hanzo_mcp/tools/common/palette.py +344 -0
- hanzo_mcp/tools/common/palette_loader.py +108 -0
- hanzo_mcp/tools/common/stats.py +261 -0
- hanzo_mcp/tools/common/thinking_tool.py +3 -5
- hanzo_mcp/tools/common/tool_disable.py +144 -0
- hanzo_mcp/tools/common/tool_enable.py +182 -0
- hanzo_mcp/tools/common/tool_list.py +260 -0
- hanzo_mcp/tools/config/__init__.py +10 -0
- hanzo_mcp/tools/config/config_tool.py +212 -0
- hanzo_mcp/tools/config/index_config.py +176 -0
- hanzo_mcp/tools/config/palette_tool.py +166 -0
- hanzo_mcp/tools/database/__init__.py +71 -0
- hanzo_mcp/tools/database/database_manager.py +246 -0
- hanzo_mcp/tools/database/graph.py +482 -0
- hanzo_mcp/tools/database/graph_add.py +257 -0
- hanzo_mcp/tools/database/graph_query.py +536 -0
- hanzo_mcp/tools/database/graph_remove.py +267 -0
- hanzo_mcp/tools/database/graph_search.py +348 -0
- hanzo_mcp/tools/database/graph_stats.py +345 -0
- hanzo_mcp/tools/database/sql.py +411 -0
- hanzo_mcp/tools/database/sql_query.py +229 -0
- hanzo_mcp/tools/database/sql_search.py +296 -0
- hanzo_mcp/tools/database/sql_stats.py +254 -0
- hanzo_mcp/tools/editor/__init__.py +11 -0
- hanzo_mcp/tools/editor/neovim_command.py +272 -0
- hanzo_mcp/tools/editor/neovim_edit.py +290 -0
- hanzo_mcp/tools/editor/neovim_session.py +356 -0
- hanzo_mcp/tools/filesystem/__init__.py +52 -13
- hanzo_mcp/tools/filesystem/base.py +1 -1
- hanzo_mcp/tools/filesystem/batch_search.py +812 -0
- hanzo_mcp/tools/filesystem/content_replace.py +3 -5
- hanzo_mcp/tools/filesystem/diff.py +193 -0
- hanzo_mcp/tools/filesystem/directory_tree.py +3 -5
- hanzo_mcp/tools/filesystem/edit.py +3 -5
- hanzo_mcp/tools/filesystem/find.py +443 -0
- hanzo_mcp/tools/filesystem/find_files.py +348 -0
- hanzo_mcp/tools/filesystem/git_search.py +505 -0
- hanzo_mcp/tools/filesystem/grep.py +2 -2
- hanzo_mcp/tools/filesystem/multi_edit.py +3 -5
- hanzo_mcp/tools/filesystem/read.py +17 -5
- hanzo_mcp/tools/filesystem/{grep_ast_tool.py → symbols.py} +17 -27
- hanzo_mcp/tools/filesystem/symbols_unified.py +376 -0
- hanzo_mcp/tools/filesystem/tree.py +268 -0
- hanzo_mcp/tools/filesystem/unified_search.py +465 -443
- hanzo_mcp/tools/filesystem/unix_aliases.py +99 -0
- hanzo_mcp/tools/filesystem/watch.py +174 -0
- hanzo_mcp/tools/filesystem/write.py +3 -5
- hanzo_mcp/tools/jupyter/__init__.py +9 -12
- hanzo_mcp/tools/jupyter/base.py +1 -1
- hanzo_mcp/tools/jupyter/jupyter.py +326 -0
- hanzo_mcp/tools/jupyter/notebook_edit.py +3 -4
- hanzo_mcp/tools/jupyter/notebook_read.py +3 -5
- hanzo_mcp/tools/llm/__init__.py +31 -0
- hanzo_mcp/tools/llm/consensus_tool.py +351 -0
- hanzo_mcp/tools/llm/llm_manage.py +413 -0
- hanzo_mcp/tools/llm/llm_tool.py +346 -0
- hanzo_mcp/tools/llm/llm_unified.py +851 -0
- hanzo_mcp/tools/llm/provider_tools.py +412 -0
- hanzo_mcp/tools/mcp/__init__.py +15 -0
- hanzo_mcp/tools/mcp/mcp_add.py +263 -0
- hanzo_mcp/tools/mcp/mcp_remove.py +127 -0
- hanzo_mcp/tools/mcp/mcp_stats.py +165 -0
- hanzo_mcp/tools/mcp/mcp_unified.py +503 -0
- hanzo_mcp/tools/shell/__init__.py +21 -23
- hanzo_mcp/tools/shell/base.py +1 -1
- hanzo_mcp/tools/shell/base_process.py +303 -0
- hanzo_mcp/tools/shell/bash_unified.py +134 -0
- hanzo_mcp/tools/shell/logs.py +265 -0
- hanzo_mcp/tools/shell/npx.py +194 -0
- hanzo_mcp/tools/shell/npx_background.py +254 -0
- hanzo_mcp/tools/shell/npx_unified.py +101 -0
- hanzo_mcp/tools/shell/open.py +107 -0
- hanzo_mcp/tools/shell/pkill.py +262 -0
- hanzo_mcp/tools/shell/process_unified.py +131 -0
- hanzo_mcp/tools/shell/processes.py +279 -0
- hanzo_mcp/tools/shell/run_background.py +326 -0
- hanzo_mcp/tools/shell/run_command.py +3 -4
- hanzo_mcp/tools/shell/run_command_windows.py +3 -4
- hanzo_mcp/tools/shell/uvx.py +187 -0
- hanzo_mcp/tools/shell/uvx_background.py +249 -0
- hanzo_mcp/tools/shell/uvx_unified.py +101 -0
- hanzo_mcp/tools/todo/__init__.py +1 -1
- hanzo_mcp/tools/todo/base.py +1 -1
- hanzo_mcp/tools/todo/todo.py +265 -0
- hanzo_mcp/tools/todo/todo_read.py +3 -5
- hanzo_mcp/tools/todo/todo_write.py +3 -5
- hanzo_mcp/tools/vector/__init__.py +6 -1
- hanzo_mcp/tools/vector/git_ingester.py +3 -0
- hanzo_mcp/tools/vector/index_tool.py +358 -0
- hanzo_mcp/tools/vector/infinity_store.py +98 -0
- hanzo_mcp/tools/vector/project_manager.py +27 -5
- hanzo_mcp/tools/vector/vector.py +311 -0
- hanzo_mcp/tools/vector/vector_index.py +1 -1
- hanzo_mcp/tools/vector/vector_search.py +12 -7
- hanzo_mcp-0.6.1.dist-info/METADATA +336 -0
- hanzo_mcp-0.6.1.dist-info/RECORD +134 -0
- hanzo_mcp-0.6.1.dist-info/entry_points.txt +3 -0
- hanzo_mcp-0.5.1.dist-info/METADATA +0 -276
- hanzo_mcp-0.5.1.dist-info/RECORD +0 -68
- hanzo_mcp-0.5.1.dist-info/entry_points.txt +0 -2
- {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.6.1.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.6.1.dist-info}/licenses/LICENSE +0 -0
- {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.6.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"""Tool for terminating background processes."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import signal
|
|
5
|
+
import psutil
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import Annotated, Optional, TypedDict, Unpack, final, override
|
|
8
|
+
|
|
9
|
+
from mcp.server.fastmcp import Context as MCPContext
|
|
10
|
+
from pydantic import Field
|
|
11
|
+
|
|
12
|
+
from hanzo_mcp.tools.common.base import BaseTool
|
|
13
|
+
from hanzo_mcp.tools.common.context import create_tool_context
|
|
14
|
+
from hanzo_mcp.tools.common.permissions import PermissionManager
|
|
15
|
+
from hanzo_mcp.tools.shell.run_background import RunBackgroundTool
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
ProcessId = Annotated[
|
|
19
|
+
Optional[str],
|
|
20
|
+
Field(
|
|
21
|
+
description="Process ID from run_background (use 'processes' to list)",
|
|
22
|
+
default=None,
|
|
23
|
+
),
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
Pid = Annotated[
|
|
27
|
+
Optional[int],
|
|
28
|
+
Field(
|
|
29
|
+
description="System process ID (PID)",
|
|
30
|
+
default=None,
|
|
31
|
+
),
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
Name = Annotated[
|
|
35
|
+
Optional[str],
|
|
36
|
+
Field(
|
|
37
|
+
description="Kill all processes matching this name",
|
|
38
|
+
default=None,
|
|
39
|
+
),
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
Force = Annotated[
|
|
43
|
+
bool,
|
|
44
|
+
Field(
|
|
45
|
+
description="Force kill (SIGKILL instead of SIGTERM)",
|
|
46
|
+
default=False,
|
|
47
|
+
),
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
All = Annotated[
|
|
51
|
+
bool,
|
|
52
|
+
Field(
|
|
53
|
+
description="Kill all background processes",
|
|
54
|
+
default=False,
|
|
55
|
+
),
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class PkillParams(TypedDict, total=False):
|
|
60
|
+
"""Parameters for killing processes."""
|
|
61
|
+
|
|
62
|
+
id: Optional[str]
|
|
63
|
+
pid: Optional[int]
|
|
64
|
+
name: Optional[str]
|
|
65
|
+
force: bool
|
|
66
|
+
all: bool
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@final
|
|
70
|
+
class PkillTool(BaseTool):
|
|
71
|
+
"""Tool for terminating processes."""
|
|
72
|
+
|
|
73
|
+
def __init__(self, permission_manager: PermissionManager):
|
|
74
|
+
"""Initialize the pkill tool.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
permission_manager: Permission manager for access control
|
|
78
|
+
"""
|
|
79
|
+
self.permission_manager = permission_manager
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
@override
|
|
83
|
+
def name(self) -> str:
|
|
84
|
+
"""Get the tool name."""
|
|
85
|
+
return "pkill"
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
@override
|
|
89
|
+
def description(self) -> str:
|
|
90
|
+
"""Get the tool description."""
|
|
91
|
+
return """Terminate running processes.
|
|
92
|
+
|
|
93
|
+
Can kill processes by:
|
|
94
|
+
- ID: Process ID from run_background (recommended)
|
|
95
|
+
- PID: System process ID
|
|
96
|
+
- Name: All processes matching name
|
|
97
|
+
- All: Terminate all background processes
|
|
98
|
+
|
|
99
|
+
Options:
|
|
100
|
+
- force: Use SIGKILL instead of SIGTERM
|
|
101
|
+
- all: Kill all background processes
|
|
102
|
+
|
|
103
|
+
Examples:
|
|
104
|
+
- pkill --id abc123 # Kill specific background process
|
|
105
|
+
- pkill --name "npm" # Kill all npm processes
|
|
106
|
+
- pkill --pid 12345 # Kill by system PID
|
|
107
|
+
- pkill --all # Kill all background processes
|
|
108
|
+
- pkill --id abc123 --force # Force kill
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
@override
|
|
112
|
+
async def call(
|
|
113
|
+
self,
|
|
114
|
+
ctx: MCPContext,
|
|
115
|
+
**params: Unpack[PkillParams],
|
|
116
|
+
) -> str:
|
|
117
|
+
"""Kill processes.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
ctx: MCP context
|
|
121
|
+
**params: Tool parameters
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Result of kill operation
|
|
125
|
+
"""
|
|
126
|
+
tool_ctx = create_tool_context(ctx)
|
|
127
|
+
await tool_ctx.set_tool_info(self.name)
|
|
128
|
+
|
|
129
|
+
# Extract parameters
|
|
130
|
+
process_id = params.get("id")
|
|
131
|
+
pid = params.get("pid")
|
|
132
|
+
name = params.get("name")
|
|
133
|
+
force = params.get("force", False)
|
|
134
|
+
kill_all = params.get("all", False)
|
|
135
|
+
|
|
136
|
+
# Validate that at least one target is specified
|
|
137
|
+
if not any([process_id, pid, name, kill_all]):
|
|
138
|
+
return "Error: Must specify --id, --pid, --name, or --all"
|
|
139
|
+
|
|
140
|
+
killed_count = 0
|
|
141
|
+
errors = []
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
# Kill all background processes
|
|
145
|
+
if kill_all:
|
|
146
|
+
await tool_ctx.info("Killing all background processes")
|
|
147
|
+
processes = RunBackgroundTool.get_processes()
|
|
148
|
+
|
|
149
|
+
for proc_id, process in list(processes.items()):
|
|
150
|
+
if process.is_running:
|
|
151
|
+
try:
|
|
152
|
+
if force:
|
|
153
|
+
process.kill()
|
|
154
|
+
else:
|
|
155
|
+
process.terminate()
|
|
156
|
+
killed_count += 1
|
|
157
|
+
await tool_ctx.info(f"Killed process {proc_id} ({process.name})")
|
|
158
|
+
except Exception as e:
|
|
159
|
+
errors.append(f"Failed to kill {proc_id}: {str(e)}")
|
|
160
|
+
|
|
161
|
+
if killed_count == 0:
|
|
162
|
+
return "No running background processes to kill."
|
|
163
|
+
|
|
164
|
+
# Kill by process ID
|
|
165
|
+
elif process_id:
|
|
166
|
+
await tool_ctx.info(f"Killing process with ID: {process_id}")
|
|
167
|
+
process = RunBackgroundTool.get_process(process_id)
|
|
168
|
+
|
|
169
|
+
if not process:
|
|
170
|
+
return f"Process with ID '{process_id}' not found."
|
|
171
|
+
|
|
172
|
+
if not process.is_running:
|
|
173
|
+
return f"Process '{process_id}' is not running (return code: {process.return_code})."
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
if force:
|
|
177
|
+
process.kill()
|
|
178
|
+
else:
|
|
179
|
+
process.terminate()
|
|
180
|
+
killed_count += 1
|
|
181
|
+
await tool_ctx.info(f"Successfully killed process {process_id}")
|
|
182
|
+
except Exception as e:
|
|
183
|
+
return f"Failed to kill process: {str(e)}"
|
|
184
|
+
|
|
185
|
+
# Kill by PID
|
|
186
|
+
elif pid:
|
|
187
|
+
await tool_ctx.info(f"Killing process with PID: {pid}")
|
|
188
|
+
try:
|
|
189
|
+
p = psutil.Process(pid)
|
|
190
|
+
|
|
191
|
+
if force:
|
|
192
|
+
p.kill()
|
|
193
|
+
else:
|
|
194
|
+
p.terminate()
|
|
195
|
+
|
|
196
|
+
killed_count += 1
|
|
197
|
+
await tool_ctx.info(f"Successfully killed PID {pid}")
|
|
198
|
+
|
|
199
|
+
# Check if this was a background process and update it
|
|
200
|
+
for proc_id, process in RunBackgroundTool.get_processes().items():
|
|
201
|
+
if process.pid == pid:
|
|
202
|
+
process.end_time = datetime.now()
|
|
203
|
+
break
|
|
204
|
+
|
|
205
|
+
except psutil.NoSuchProcess:
|
|
206
|
+
return f"Process with PID {pid} not found."
|
|
207
|
+
except psutil.AccessDenied:
|
|
208
|
+
return f"Permission denied to kill PID {pid}."
|
|
209
|
+
except Exception as e:
|
|
210
|
+
return f"Failed to kill PID {pid}: {str(e)}"
|
|
211
|
+
|
|
212
|
+
# Kill by name
|
|
213
|
+
elif name:
|
|
214
|
+
await tool_ctx.info(f"Killing all processes matching: {name}")
|
|
215
|
+
|
|
216
|
+
# First check background processes
|
|
217
|
+
bg_processes = RunBackgroundTool.get_processes()
|
|
218
|
+
for proc_id, process in list(bg_processes.items()):
|
|
219
|
+
if name.lower() in process.name.lower() and process.is_running:
|
|
220
|
+
try:
|
|
221
|
+
if force:
|
|
222
|
+
process.kill()
|
|
223
|
+
else:
|
|
224
|
+
process.terminate()
|
|
225
|
+
killed_count += 1
|
|
226
|
+
await tool_ctx.info(f"Killed background process {proc_id} ({process.name})")
|
|
227
|
+
except Exception as e:
|
|
228
|
+
errors.append(f"Failed to kill {proc_id}: {str(e)}")
|
|
229
|
+
|
|
230
|
+
# Also check system processes
|
|
231
|
+
for proc in psutil.process_iter(['pid', 'name']):
|
|
232
|
+
try:
|
|
233
|
+
if name.lower() in proc.info['name'].lower():
|
|
234
|
+
if force:
|
|
235
|
+
proc.kill()
|
|
236
|
+
else:
|
|
237
|
+
proc.terminate()
|
|
238
|
+
killed_count += 1
|
|
239
|
+
await tool_ctx.info(f"Killed {proc.info['name']} (PID: {proc.info['pid']})")
|
|
240
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
241
|
+
continue
|
|
242
|
+
except Exception as e:
|
|
243
|
+
errors.append(f"Failed to kill PID {proc.info['pid']}: {str(e)}")
|
|
244
|
+
|
|
245
|
+
# Build result message
|
|
246
|
+
if killed_count > 0:
|
|
247
|
+
result = f"Successfully killed {killed_count} process(es)."
|
|
248
|
+
else:
|
|
249
|
+
result = "No processes were killed."
|
|
250
|
+
|
|
251
|
+
if errors:
|
|
252
|
+
result += f"\n\nErrors:\n" + "\n".join(errors)
|
|
253
|
+
|
|
254
|
+
return result
|
|
255
|
+
|
|
256
|
+
except Exception as e:
|
|
257
|
+
await tool_ctx.error(f"Failed to kill processes: {str(e)}")
|
|
258
|
+
return f"Error killing processes: {str(e)}"
|
|
259
|
+
|
|
260
|
+
def register(self, mcp_server) -> None:
|
|
261
|
+
"""Register this tool with the MCP server."""
|
|
262
|
+
pass
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Process management tool."""
|
|
2
|
+
|
|
3
|
+
import signal
|
|
4
|
+
from typing import Optional, override
|
|
5
|
+
|
|
6
|
+
from mcp.server.fastmcp import Context as MCPContext
|
|
7
|
+
|
|
8
|
+
from hanzo_mcp.tools.common.base import BaseTool
|
|
9
|
+
from hanzo_mcp.tools.shell.base_process import ProcessManager
|
|
10
|
+
from mcp.server import FastMCP
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ProcessTool(BaseTool):
|
|
14
|
+
"""Tool for process management."""
|
|
15
|
+
|
|
16
|
+
name = "process"
|
|
17
|
+
|
|
18
|
+
def __init__(self):
|
|
19
|
+
"""Initialize the process tool."""
|
|
20
|
+
super().__init__()
|
|
21
|
+
self.process_manager = ProcessManager()
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
@override
|
|
25
|
+
def description(self) -> str:
|
|
26
|
+
"""Get the tool description."""
|
|
27
|
+
return """Manage background processes. Actions: list (default), kill, logs.
|
|
28
|
+
|
|
29
|
+
Usage:
|
|
30
|
+
process
|
|
31
|
+
process --action list
|
|
32
|
+
process --action kill --id npx_abc123
|
|
33
|
+
process --action logs --id uvx_def456
|
|
34
|
+
process --action logs --id bash_ghi789 --lines 50"""
|
|
35
|
+
|
|
36
|
+
@override
|
|
37
|
+
async def run(
|
|
38
|
+
self,
|
|
39
|
+
ctx: MCPContext,
|
|
40
|
+
action: str = "list",
|
|
41
|
+
id: Optional[str] = None,
|
|
42
|
+
signal_type: str = "TERM",
|
|
43
|
+
lines: int = 100,
|
|
44
|
+
) -> str:
|
|
45
|
+
"""Manage background processes.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
ctx: MCP context
|
|
49
|
+
action: Action to perform (list, kill, logs)
|
|
50
|
+
id: Process ID (for kill/logs actions)
|
|
51
|
+
signal_type: Signal type for kill (TERM, KILL, INT)
|
|
52
|
+
lines: Number of log lines to show
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Action result
|
|
56
|
+
"""
|
|
57
|
+
if action == "list":
|
|
58
|
+
processes = self.process_manager.list_processes()
|
|
59
|
+
if not processes:
|
|
60
|
+
return "No background processes running"
|
|
61
|
+
|
|
62
|
+
output = ["Background processes:"]
|
|
63
|
+
for proc_id, info in processes.items():
|
|
64
|
+
status = "running" if info["running"] else f"stopped (exit code: {info.get('return_code', 'unknown')})"
|
|
65
|
+
output.append(f"- {proc_id}: PID {info['pid']} - {status}")
|
|
66
|
+
if info.get("log_file"):
|
|
67
|
+
output.append(f" Log: {info['log_file']}")
|
|
68
|
+
|
|
69
|
+
return "\n".join(output)
|
|
70
|
+
|
|
71
|
+
elif action == "kill":
|
|
72
|
+
if not id:
|
|
73
|
+
return "Error: Process ID required for kill action"
|
|
74
|
+
|
|
75
|
+
process = self.process_manager.get_process(id)
|
|
76
|
+
if not process:
|
|
77
|
+
return f"Process {id} not found"
|
|
78
|
+
|
|
79
|
+
# Map signal names to signal numbers
|
|
80
|
+
signal_map = {
|
|
81
|
+
"TERM": signal.SIGTERM,
|
|
82
|
+
"KILL": signal.SIGKILL,
|
|
83
|
+
"INT": signal.SIGINT,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
sig = signal_map.get(signal_type.upper(), signal.SIGTERM)
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
process.send_signal(sig)
|
|
90
|
+
return f"Sent {signal_type} signal to process {id} (PID: {process.pid})"
|
|
91
|
+
except Exception as e:
|
|
92
|
+
return f"Failed to kill process {id}: {e}"
|
|
93
|
+
|
|
94
|
+
elif action == "logs":
|
|
95
|
+
if not id:
|
|
96
|
+
return "Error: Process ID required for logs action"
|
|
97
|
+
|
|
98
|
+
log_file = self.process_manager.get_log_file(id)
|
|
99
|
+
if not log_file or not log_file.exists():
|
|
100
|
+
return f"No log file found for process {id}"
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
with open(log_file, "r") as f:
|
|
104
|
+
log_lines = f.readlines()
|
|
105
|
+
|
|
106
|
+
# Get last N lines
|
|
107
|
+
if len(log_lines) > lines:
|
|
108
|
+
log_lines = log_lines[-lines:]
|
|
109
|
+
|
|
110
|
+
output = [f"Logs for process {id} (last {lines} lines):"]
|
|
111
|
+
output.append("-" * 50)
|
|
112
|
+
output.extend(line.rstrip() for line in log_lines)
|
|
113
|
+
|
|
114
|
+
return "\n".join(output)
|
|
115
|
+
except Exception as e:
|
|
116
|
+
return f"Error reading logs: {e}"
|
|
117
|
+
|
|
118
|
+
else:
|
|
119
|
+
return f"Unknown action: {action}. Use 'list', 'kill', or 'logs'"
|
|
120
|
+
|
|
121
|
+
def register(self, server: FastMCP) -> None:
|
|
122
|
+
"""Register the tool with the MCP server."""
|
|
123
|
+
server.tool(name=self.name, description=self.description)(self.call)
|
|
124
|
+
|
|
125
|
+
async def call(self, **kwargs) -> str:
|
|
126
|
+
"""Call the tool with arguments."""
|
|
127
|
+
return await self.run(None, **kwargs)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# Create tool instance
|
|
131
|
+
process_tool = ProcessTool()
|
|
@@ -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 mcp.server.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
|