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.

Files changed (118) hide show
  1. hanzo_mcp/__init__.py +1 -1
  2. hanzo_mcp/cli.py +32 -0
  3. hanzo_mcp/dev_server.py +246 -0
  4. hanzo_mcp/prompts/__init__.py +1 -1
  5. hanzo_mcp/prompts/project_system.py +43 -7
  6. hanzo_mcp/server.py +5 -1
  7. hanzo_mcp/tools/__init__.py +168 -6
  8. hanzo_mcp/tools/agent/__init__.py +1 -1
  9. hanzo_mcp/tools/agent/agent.py +401 -0
  10. hanzo_mcp/tools/agent/agent_tool.py +3 -4
  11. hanzo_mcp/tools/common/__init__.py +1 -1
  12. hanzo_mcp/tools/common/base.py +9 -4
  13. hanzo_mcp/tools/common/batch_tool.py +3 -5
  14. hanzo_mcp/tools/common/config_tool.py +1 -1
  15. hanzo_mcp/tools/common/context.py +1 -1
  16. hanzo_mcp/tools/common/palette.py +344 -0
  17. hanzo_mcp/tools/common/palette_loader.py +108 -0
  18. hanzo_mcp/tools/common/stats.py +261 -0
  19. hanzo_mcp/tools/common/thinking_tool.py +3 -5
  20. hanzo_mcp/tools/common/tool_disable.py +144 -0
  21. hanzo_mcp/tools/common/tool_enable.py +182 -0
  22. hanzo_mcp/tools/common/tool_list.py +260 -0
  23. hanzo_mcp/tools/config/__init__.py +10 -0
  24. hanzo_mcp/tools/config/config_tool.py +212 -0
  25. hanzo_mcp/tools/config/index_config.py +176 -0
  26. hanzo_mcp/tools/config/palette_tool.py +166 -0
  27. hanzo_mcp/tools/database/__init__.py +71 -0
  28. hanzo_mcp/tools/database/database_manager.py +246 -0
  29. hanzo_mcp/tools/database/graph.py +482 -0
  30. hanzo_mcp/tools/database/graph_add.py +257 -0
  31. hanzo_mcp/tools/database/graph_query.py +536 -0
  32. hanzo_mcp/tools/database/graph_remove.py +267 -0
  33. hanzo_mcp/tools/database/graph_search.py +348 -0
  34. hanzo_mcp/tools/database/graph_stats.py +345 -0
  35. hanzo_mcp/tools/database/sql.py +411 -0
  36. hanzo_mcp/tools/database/sql_query.py +229 -0
  37. hanzo_mcp/tools/database/sql_search.py +296 -0
  38. hanzo_mcp/tools/database/sql_stats.py +254 -0
  39. hanzo_mcp/tools/editor/__init__.py +11 -0
  40. hanzo_mcp/tools/editor/neovim_command.py +272 -0
  41. hanzo_mcp/tools/editor/neovim_edit.py +290 -0
  42. hanzo_mcp/tools/editor/neovim_session.py +356 -0
  43. hanzo_mcp/tools/filesystem/__init__.py +52 -13
  44. hanzo_mcp/tools/filesystem/base.py +1 -1
  45. hanzo_mcp/tools/filesystem/batch_search.py +812 -0
  46. hanzo_mcp/tools/filesystem/content_replace.py +3 -5
  47. hanzo_mcp/tools/filesystem/diff.py +193 -0
  48. hanzo_mcp/tools/filesystem/directory_tree.py +3 -5
  49. hanzo_mcp/tools/filesystem/edit.py +3 -5
  50. hanzo_mcp/tools/filesystem/find.py +443 -0
  51. hanzo_mcp/tools/filesystem/find_files.py +348 -0
  52. hanzo_mcp/tools/filesystem/git_search.py +505 -0
  53. hanzo_mcp/tools/filesystem/grep.py +2 -2
  54. hanzo_mcp/tools/filesystem/multi_edit.py +3 -5
  55. hanzo_mcp/tools/filesystem/read.py +17 -5
  56. hanzo_mcp/tools/filesystem/{grep_ast_tool.py → symbols.py} +17 -27
  57. hanzo_mcp/tools/filesystem/symbols_unified.py +376 -0
  58. hanzo_mcp/tools/filesystem/tree.py +268 -0
  59. hanzo_mcp/tools/filesystem/unified_search.py +465 -443
  60. hanzo_mcp/tools/filesystem/unix_aliases.py +99 -0
  61. hanzo_mcp/tools/filesystem/watch.py +174 -0
  62. hanzo_mcp/tools/filesystem/write.py +3 -5
  63. hanzo_mcp/tools/jupyter/__init__.py +9 -12
  64. hanzo_mcp/tools/jupyter/base.py +1 -1
  65. hanzo_mcp/tools/jupyter/jupyter.py +326 -0
  66. hanzo_mcp/tools/jupyter/notebook_edit.py +3 -4
  67. hanzo_mcp/tools/jupyter/notebook_read.py +3 -5
  68. hanzo_mcp/tools/llm/__init__.py +31 -0
  69. hanzo_mcp/tools/llm/consensus_tool.py +351 -0
  70. hanzo_mcp/tools/llm/llm_manage.py +413 -0
  71. hanzo_mcp/tools/llm/llm_tool.py +346 -0
  72. hanzo_mcp/tools/llm/llm_unified.py +851 -0
  73. hanzo_mcp/tools/llm/provider_tools.py +412 -0
  74. hanzo_mcp/tools/mcp/__init__.py +15 -0
  75. hanzo_mcp/tools/mcp/mcp_add.py +263 -0
  76. hanzo_mcp/tools/mcp/mcp_remove.py +127 -0
  77. hanzo_mcp/tools/mcp/mcp_stats.py +165 -0
  78. hanzo_mcp/tools/mcp/mcp_unified.py +503 -0
  79. hanzo_mcp/tools/shell/__init__.py +21 -23
  80. hanzo_mcp/tools/shell/base.py +1 -1
  81. hanzo_mcp/tools/shell/base_process.py +303 -0
  82. hanzo_mcp/tools/shell/bash_unified.py +134 -0
  83. hanzo_mcp/tools/shell/logs.py +265 -0
  84. hanzo_mcp/tools/shell/npx.py +194 -0
  85. hanzo_mcp/tools/shell/npx_background.py +254 -0
  86. hanzo_mcp/tools/shell/npx_unified.py +101 -0
  87. hanzo_mcp/tools/shell/open.py +107 -0
  88. hanzo_mcp/tools/shell/pkill.py +262 -0
  89. hanzo_mcp/tools/shell/process_unified.py +131 -0
  90. hanzo_mcp/tools/shell/processes.py +279 -0
  91. hanzo_mcp/tools/shell/run_background.py +326 -0
  92. hanzo_mcp/tools/shell/run_command.py +3 -4
  93. hanzo_mcp/tools/shell/run_command_windows.py +3 -4
  94. hanzo_mcp/tools/shell/uvx.py +187 -0
  95. hanzo_mcp/tools/shell/uvx_background.py +249 -0
  96. hanzo_mcp/tools/shell/uvx_unified.py +101 -0
  97. hanzo_mcp/tools/todo/__init__.py +1 -1
  98. hanzo_mcp/tools/todo/base.py +1 -1
  99. hanzo_mcp/tools/todo/todo.py +265 -0
  100. hanzo_mcp/tools/todo/todo_read.py +3 -5
  101. hanzo_mcp/tools/todo/todo_write.py +3 -5
  102. hanzo_mcp/tools/vector/__init__.py +6 -1
  103. hanzo_mcp/tools/vector/git_ingester.py +3 -0
  104. hanzo_mcp/tools/vector/index_tool.py +358 -0
  105. hanzo_mcp/tools/vector/infinity_store.py +98 -0
  106. hanzo_mcp/tools/vector/project_manager.py +27 -5
  107. hanzo_mcp/tools/vector/vector.py +311 -0
  108. hanzo_mcp/tools/vector/vector_index.py +1 -1
  109. hanzo_mcp/tools/vector/vector_search.py +12 -7
  110. hanzo_mcp-0.6.1.dist-info/METADATA +336 -0
  111. hanzo_mcp-0.6.1.dist-info/RECORD +134 -0
  112. hanzo_mcp-0.6.1.dist-info/entry_points.txt +3 -0
  113. hanzo_mcp-0.5.1.dist-info/METADATA +0 -276
  114. hanzo_mcp-0.5.1.dist-info/RECORD +0 -68
  115. hanzo_mcp-0.5.1.dist-info/entry_points.txt +0 -2
  116. {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.6.1.dist-info}/WHEEL +0 -0
  117. {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.6.1.dist-info}/licenses/LICENSE +0 -0
  118. {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