hanzo-mcp 0.5.0__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 (60) hide show
  1. hanzo_mcp/__init__.py +1 -1
  2. hanzo_mcp/config/settings.py +61 -0
  3. hanzo_mcp/tools/__init__.py +158 -12
  4. hanzo_mcp/tools/common/base.py +7 -2
  5. hanzo_mcp/tools/common/config_tool.py +396 -0
  6. hanzo_mcp/tools/common/stats.py +261 -0
  7. hanzo_mcp/tools/common/tool_disable.py +144 -0
  8. hanzo_mcp/tools/common/tool_enable.py +182 -0
  9. hanzo_mcp/tools/common/tool_list.py +263 -0
  10. hanzo_mcp/tools/database/__init__.py +71 -0
  11. hanzo_mcp/tools/database/database_manager.py +246 -0
  12. hanzo_mcp/tools/database/graph_add.py +257 -0
  13. hanzo_mcp/tools/database/graph_query.py +536 -0
  14. hanzo_mcp/tools/database/graph_remove.py +267 -0
  15. hanzo_mcp/tools/database/graph_search.py +348 -0
  16. hanzo_mcp/tools/database/graph_stats.py +345 -0
  17. hanzo_mcp/tools/database/sql_query.py +229 -0
  18. hanzo_mcp/tools/database/sql_search.py +296 -0
  19. hanzo_mcp/tools/database/sql_stats.py +254 -0
  20. hanzo_mcp/tools/editor/__init__.py +11 -0
  21. hanzo_mcp/tools/editor/neovim_command.py +272 -0
  22. hanzo_mcp/tools/editor/neovim_edit.py +290 -0
  23. hanzo_mcp/tools/editor/neovim_session.py +356 -0
  24. hanzo_mcp/tools/filesystem/__init__.py +20 -1
  25. hanzo_mcp/tools/filesystem/batch_search.py +812 -0
  26. hanzo_mcp/tools/filesystem/find_files.py +348 -0
  27. hanzo_mcp/tools/filesystem/git_search.py +505 -0
  28. hanzo_mcp/tools/llm/__init__.py +27 -0
  29. hanzo_mcp/tools/llm/consensus_tool.py +351 -0
  30. hanzo_mcp/tools/llm/llm_manage.py +413 -0
  31. hanzo_mcp/tools/llm/llm_tool.py +346 -0
  32. hanzo_mcp/tools/llm/provider_tools.py +412 -0
  33. hanzo_mcp/tools/mcp/__init__.py +11 -0
  34. hanzo_mcp/tools/mcp/mcp_add.py +263 -0
  35. hanzo_mcp/tools/mcp/mcp_remove.py +127 -0
  36. hanzo_mcp/tools/mcp/mcp_stats.py +165 -0
  37. hanzo_mcp/tools/shell/__init__.py +27 -7
  38. hanzo_mcp/tools/shell/logs.py +265 -0
  39. hanzo_mcp/tools/shell/npx.py +194 -0
  40. hanzo_mcp/tools/shell/npx_background.py +254 -0
  41. hanzo_mcp/tools/shell/pkill.py +262 -0
  42. hanzo_mcp/tools/shell/processes.py +279 -0
  43. hanzo_mcp/tools/shell/run_background.py +326 -0
  44. hanzo_mcp/tools/shell/uvx.py +187 -0
  45. hanzo_mcp/tools/shell/uvx_background.py +249 -0
  46. hanzo_mcp/tools/vector/__init__.py +21 -12
  47. hanzo_mcp/tools/vector/ast_analyzer.py +459 -0
  48. hanzo_mcp/tools/vector/git_ingester.py +485 -0
  49. hanzo_mcp/tools/vector/index_tool.py +358 -0
  50. hanzo_mcp/tools/vector/infinity_store.py +465 -1
  51. hanzo_mcp/tools/vector/mock_infinity.py +162 -0
  52. hanzo_mcp/tools/vector/vector_index.py +7 -6
  53. hanzo_mcp/tools/vector/vector_search.py +22 -7
  54. {hanzo_mcp-0.5.0.dist-info → hanzo_mcp-0.5.2.dist-info}/METADATA +68 -20
  55. hanzo_mcp-0.5.2.dist-info/RECORD +106 -0
  56. hanzo_mcp-0.5.0.dist-info/RECORD +0 -63
  57. {hanzo_mcp-0.5.0.dist-info → hanzo_mcp-0.5.2.dist-info}/WHEEL +0 -0
  58. {hanzo_mcp-0.5.0.dist-info → hanzo_mcp-0.5.2.dist-info}/entry_points.txt +0 -0
  59. {hanzo_mcp-0.5.0.dist-info → hanzo_mcp-0.5.2.dist-info}/licenses/LICENSE +0 -0
  60. {hanzo_mcp-0.5.0.dist-info → hanzo_mcp-0.5.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,194 @@
1
+ """Run Node.js packages with npx."""
2
+
3
+ import subprocess
4
+ import shutil
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
+
14
+
15
+ Package = Annotated[
16
+ str,
17
+ Field(
18
+ description="Package name to run (e.g., 'eslint', 'prettier', 'create-react-app')",
19
+ min_length=1,
20
+ ),
21
+ ]
22
+
23
+ Args = Annotated[
24
+ Optional[str],
25
+ Field(
26
+ description="Arguments to pass to the package",
27
+ default=None,
28
+ ),
29
+ ]
30
+
31
+ Yes = Annotated[
32
+ bool,
33
+ Field(
34
+ description="Auto-confirm package installation",
35
+ default=True,
36
+ ),
37
+ ]
38
+
39
+ Timeout = Annotated[
40
+ int,
41
+ Field(
42
+ description="Timeout in seconds (default 120)",
43
+ default=120,
44
+ ),
45
+ ]
46
+
47
+
48
+ class NpxParams(TypedDict, total=False):
49
+ """Parameters for npx tool."""
50
+
51
+ package: str
52
+ args: Optional[str]
53
+ yes: bool
54
+ timeout: int
55
+
56
+
57
+ @final
58
+ class NpxTool(BaseTool):
59
+ """Tool for running Node.js packages with npx."""
60
+
61
+ def __init__(self, permission_manager: PermissionManager):
62
+ """Initialize the npx tool.
63
+
64
+ Args:
65
+ permission_manager: Permission manager for access control
66
+ """
67
+ self.permission_manager = permission_manager
68
+
69
+ @property
70
+ @override
71
+ def name(self) -> str:
72
+ """Get the tool name."""
73
+ return "npx"
74
+
75
+ @property
76
+ @override
77
+ def description(self) -> str:
78
+ """Get the tool description."""
79
+ return """Run Node.js packages using npx (Node package runner).
80
+
81
+ npx allows running Node.js packages without installing them globally.
82
+ It automatically downloads and executes packages on demand.
83
+
84
+ Common packages:
85
+ - eslint: JavaScript linter
86
+ - prettier: Code formatter
87
+ - typescript: TypeScript compiler
88
+ - create-react-app: Create React apps
89
+ - create-next-app: Create Next.js apps
90
+ - jest: Testing framework
91
+ - webpack: Module bundler
92
+ - vite: Build tool
93
+ - vercel: Deploy to Vercel
94
+ - netlify-cli: Deploy to Netlify
95
+
96
+ Examples:
97
+ - npx --package eslint --args "--init"
98
+ - npx --package prettier --args "--write src/**/*.js"
99
+ - npx --package "create-react-app" --args "my-app"
100
+ - npx --package typescript --args "--init"
101
+ - npx --package jest --args "--coverage"
102
+
103
+ For long-running servers, use npx_background instead.
104
+ """
105
+
106
+ @override
107
+ async def call(
108
+ self,
109
+ ctx: MCPContext,
110
+ **params: Unpack[NpxParams],
111
+ ) -> str:
112
+ """Execute npx command.
113
+
114
+ Args:
115
+ ctx: MCP context
116
+ **params: Tool parameters
117
+
118
+ Returns:
119
+ Command output
120
+ """
121
+ tool_ctx = create_tool_context(ctx)
122
+ await tool_ctx.set_tool_info(self.name)
123
+
124
+ # Extract parameters
125
+ package = params.get("package")
126
+ if not package:
127
+ return "Error: package is required"
128
+
129
+ args = params.get("args", "")
130
+ yes = params.get("yes", True)
131
+ timeout = params.get("timeout", 120)
132
+
133
+ # Check if npx is available
134
+ if not shutil.which("npx"):
135
+ return """Error: npx is not installed. Install Node.js and npm:
136
+
137
+ On macOS:
138
+ brew install node
139
+
140
+ On Ubuntu/Debian:
141
+ sudo apt update && sudo apt install nodejs npm
142
+
143
+ Or download from: https://nodejs.org/"""
144
+
145
+ # Build command
146
+ cmd = ["npx"]
147
+
148
+ if yes:
149
+ cmd.append("--yes")
150
+
151
+ cmd.append(package)
152
+
153
+ # Add package arguments
154
+ if args:
155
+ # Split args properly (basic parsing)
156
+ import shlex
157
+ cmd.extend(shlex.split(args))
158
+
159
+ await tool_ctx.info(f"Running: {' '.join(cmd)}")
160
+
161
+ try:
162
+ # Execute command
163
+ result = subprocess.run(
164
+ cmd,
165
+ capture_output=True,
166
+ text=True,
167
+ timeout=timeout,
168
+ check=True
169
+ )
170
+
171
+ output = []
172
+ if result.stdout:
173
+ output.append(result.stdout)
174
+ if result.stderr:
175
+ output.append(f"\nSTDERR:\n{result.stderr}")
176
+
177
+ return "\n".join(output) if output else "Command completed successfully with no output."
178
+
179
+ except subprocess.TimeoutExpired:
180
+ return f"Error: Command timed out after {timeout} seconds. Use npx_background for long-running processes."
181
+ except subprocess.CalledProcessError as e:
182
+ error_msg = [f"Error: Command failed with exit code {e.returncode}"]
183
+ if e.stdout:
184
+ error_msg.append(f"\nSTDOUT:\n{e.stdout}")
185
+ if e.stderr:
186
+ error_msg.append(f"\nSTDERR:\n{e.stderr}")
187
+ return "\n".join(error_msg)
188
+ except Exception as e:
189
+ await tool_ctx.error(f"Unexpected error: {str(e)}")
190
+ return f"Error running npx: {str(e)}"
191
+
192
+ def register(self, mcp_server) -> None:
193
+ """Register this tool with the MCP server."""
194
+ pass
@@ -0,0 +1,254 @@
1
+ """Run Node.js packages in background with npx."""
2
+
3
+ import subprocess
4
+ import shutil
5
+ import uuid
6
+ from typing import Annotated, Optional, TypedDict, Unpack, final, override
7
+
8
+ from fastmcp import Context as MCPContext
9
+ from pydantic import Field
10
+
11
+ from hanzo_mcp.tools.common.base import BaseTool
12
+ from hanzo_mcp.tools.common.context import create_tool_context
13
+ from hanzo_mcp.tools.common.permissions import PermissionManager
14
+ from hanzo_mcp.tools.shell.run_background import BackgroundProcess, RunBackgroundTool
15
+
16
+
17
+ Package = Annotated[
18
+ str,
19
+ Field(
20
+ description="Package name to run (e.g., 'http-server', 'json-server', 'serve')",
21
+ min_length=1,
22
+ ),
23
+ ]
24
+
25
+ Args = Annotated[
26
+ Optional[str],
27
+ Field(
28
+ description="Arguments to pass to the package",
29
+ default=None,
30
+ ),
31
+ ]
32
+
33
+ Name = Annotated[
34
+ Optional[str],
35
+ Field(
36
+ description="Process name for identification",
37
+ default=None,
38
+ ),
39
+ ]
40
+
41
+ Yes = Annotated[
42
+ bool,
43
+ Field(
44
+ description="Auto-confirm package installation",
45
+ default=True,
46
+ ),
47
+ ]
48
+
49
+ LogOutput = Annotated[
50
+ bool,
51
+ Field(
52
+ description="Log output to file in ~/.hanzo/logs",
53
+ default=True,
54
+ ),
55
+ ]
56
+
57
+ WorkingDir = Annotated[
58
+ Optional[str],
59
+ Field(
60
+ description="Working directory for the process",
61
+ default=None,
62
+ ),
63
+ ]
64
+
65
+
66
+ class NpxBackgroundParams(TypedDict, total=False):
67
+ """Parameters for npx background tool."""
68
+
69
+ package: str
70
+ args: Optional[str]
71
+ name: Optional[str]
72
+ yes: bool
73
+ log_output: bool
74
+ working_dir: Optional[str]
75
+
76
+
77
+ @final
78
+ class NpxBackgroundTool(BaseTool):
79
+ """Tool for running Node.js packages in background with npx."""
80
+
81
+ def __init__(self, permission_manager: PermissionManager):
82
+ """Initialize the npx background tool.
83
+
84
+ Args:
85
+ permission_manager: Permission manager for access control
86
+ """
87
+ self.permission_manager = permission_manager
88
+
89
+ @property
90
+ @override
91
+ def name(self) -> str:
92
+ """Get the tool name."""
93
+ return "npx_background"
94
+
95
+ @property
96
+ @override
97
+ def description(self) -> str:
98
+ """Get the tool description."""
99
+ return """Run Node.js packages in the background using npx.
100
+
101
+ Perfect for running servers and long-running Node.js applications.
102
+ The process continues running even after the command returns.
103
+
104
+ Common server packages:
105
+ - http-server: Simple HTTP server
106
+ - json-server: REST API mock server
107
+ - serve: Static file server
108
+ - live-server: Dev server with reload
109
+ - webpack-dev-server: Webpack dev server
110
+ - nodemon: Auto-restart on changes
111
+ - localtunnel: Expose local server
112
+ - ngrok: Secure tunnels
113
+
114
+ Examples:
115
+ - npx_background --package http-server --args "-p 8080" --name web-server
116
+ - npx_background --package json-server --args "db.json --port 3001" --name api
117
+ - npx_background --package serve --args "-s build -p 5000" --name static
118
+ - npx_background --package live-server --args "--port=8081" --name dev-server
119
+
120
+ Use 'processes' to list running processes and 'pkill' to stop them.
121
+ """
122
+
123
+ @override
124
+ async def call(
125
+ self,
126
+ ctx: MCPContext,
127
+ **params: Unpack[NpxBackgroundParams],
128
+ ) -> str:
129
+ """Execute npx command in background.
130
+
131
+ Args:
132
+ ctx: MCP context
133
+ **params: Tool parameters
134
+
135
+ Returns:
136
+ Process information
137
+ """
138
+ tool_ctx = create_tool_context(ctx)
139
+ await tool_ctx.set_tool_info(self.name)
140
+
141
+ # Extract parameters
142
+ package = params.get("package")
143
+ if not package:
144
+ return "Error: package is required"
145
+
146
+ args = params.get("args", "")
147
+ name = params.get("name", f"npx-{package}")
148
+ yes = params.get("yes", True)
149
+ log_output = params.get("log_output", True)
150
+ working_dir = params.get("working_dir")
151
+
152
+ # Check if npx is available
153
+ if not shutil.which("npx"):
154
+ return """Error: npx is not installed. Install Node.js and npm:
155
+
156
+ On macOS:
157
+ brew install node
158
+
159
+ On Ubuntu/Debian:
160
+ sudo apt update && sudo apt install nodejs npm
161
+
162
+ Or download from: https://nodejs.org/"""
163
+
164
+ # Build command
165
+ cmd = ["npx"]
166
+
167
+ if yes:
168
+ cmd.append("--yes")
169
+
170
+ cmd.append(package)
171
+
172
+ # Add package arguments
173
+ if args:
174
+ # Split args properly (basic parsing)
175
+ import shlex
176
+ cmd.extend(shlex.split(args))
177
+
178
+ # Generate process ID
179
+ process_id = str(uuid.uuid4())[:8]
180
+
181
+ # Prepare log file if needed
182
+ log_file = None
183
+ if log_output:
184
+ from pathlib import Path
185
+ log_dir = Path.home() / ".hanzo" / "logs"
186
+ log_dir.mkdir(parents=True, exist_ok=True)
187
+ log_file = log_dir / f"{name}_{process_id}.log"
188
+
189
+ await tool_ctx.info(f"Starting background process: {' '.join(cmd)}")
190
+
191
+ try:
192
+ # Start process
193
+ if log_output and log_file:
194
+ with open(log_file, "w") as f:
195
+ process = subprocess.Popen(
196
+ cmd,
197
+ stdout=f,
198
+ stderr=subprocess.STDOUT,
199
+ cwd=working_dir,
200
+ start_new_session=True
201
+ )
202
+ else:
203
+ process = subprocess.Popen(
204
+ cmd,
205
+ stdout=subprocess.DEVNULL,
206
+ stderr=subprocess.DEVNULL,
207
+ cwd=working_dir,
208
+ start_new_session=True
209
+ )
210
+
211
+ # Create background process object
212
+ bg_process = BackgroundProcess(
213
+ process_id=process_id,
214
+ command=" ".join(cmd),
215
+ name=name,
216
+ process=process,
217
+ log_file=str(log_file) if log_file else None,
218
+ working_dir=working_dir
219
+ )
220
+
221
+ # Register with RunBackgroundTool
222
+ RunBackgroundTool._add_process(bg_process)
223
+
224
+ output = [
225
+ f"Started npx background process:",
226
+ f" ID: {process_id}",
227
+ f" Name: {name}",
228
+ f" Package: {package}",
229
+ f" PID: {process.pid}",
230
+ f" Command: {' '.join(cmd)}",
231
+ ]
232
+
233
+ if working_dir:
234
+ output.append(f" Working Dir: {working_dir}")
235
+
236
+ if log_file:
237
+ output.append(f" Log: {log_file}")
238
+
239
+ output.extend([
240
+ "",
241
+ "Use 'processes' to list running processes.",
242
+ f"Use 'logs --process-id {process_id}' to view output.",
243
+ f"Use 'pkill --process-id {process_id}' to stop."
244
+ ])
245
+
246
+ return "\n".join(output)
247
+
248
+ except Exception as e:
249
+ await tool_ctx.error(f"Failed to start process: {str(e)}")
250
+ return f"Error starting npx background process: {str(e)}"
251
+
252
+ def register(self, mcp_server) -> None:
253
+ """Register this tool with the MCP server."""
254
+ pass
@@ -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 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