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,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 mcp.server.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
|
|
@@ -5,9 +5,8 @@ This module provides the RunCommandTool for running shell commands.
|
|
|
5
5
|
|
|
6
6
|
from typing import Annotated, Any, TypedDict, Unpack, final, override
|
|
7
7
|
|
|
8
|
-
from fastmcp import Context as MCPContext
|
|
9
|
-
from
|
|
10
|
-
from fastmcp.server.dependencies import get_context
|
|
8
|
+
from mcp.server.fastmcp import Context as MCPContext
|
|
9
|
+
from mcp.server import FastMCP
|
|
11
10
|
from pydantic import Field
|
|
12
11
|
|
|
13
12
|
from hanzo_mcp.tools.common.base import handle_connection_errors
|
|
@@ -345,8 +344,8 @@ Important:
|
|
|
345
344
|
time_out: TimeOut,
|
|
346
345
|
is_input: IsInput,
|
|
347
346
|
blocking: Blocking,
|
|
347
|
+
ctx: MCPContext
|
|
348
348
|
) -> str:
|
|
349
|
-
ctx = get_context()
|
|
350
349
|
return await tool_self.call(
|
|
351
350
|
ctx,
|
|
352
351
|
command=command,
|
|
@@ -6,9 +6,8 @@ This module provides the RunCommandTool for running shell commands on Windows.
|
|
|
6
6
|
import os
|
|
7
7
|
from typing import Annotated, Any, final, override
|
|
8
8
|
|
|
9
|
-
from fastmcp import Context as MCPContext
|
|
10
|
-
from
|
|
11
|
-
from fastmcp.server.dependencies import get_context
|
|
9
|
+
from mcp.server.fastmcp import Context as MCPContext
|
|
10
|
+
from mcp.server import FastMCP
|
|
12
11
|
from pydantic import Field
|
|
13
12
|
|
|
14
13
|
from hanzo_mcp.tools.common.base import handle_connection_errors
|
|
@@ -317,8 +316,8 @@ Important:
|
|
|
317
316
|
default=True,
|
|
318
317
|
),
|
|
319
318
|
] = True,
|
|
319
|
+
ctx: MCPContext
|
|
320
320
|
) -> str:
|
|
321
|
-
ctx = get_context()
|
|
322
321
|
return await tool_self.call(
|
|
323
322
|
ctx,
|
|
324
323
|
command=command,
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""Run Python packages with uvx."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import shutil
|
|
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
|
+
|
|
14
|
+
|
|
15
|
+
Package = Annotated[
|
|
16
|
+
str,
|
|
17
|
+
Field(
|
|
18
|
+
description="Package name to run (e.g., 'ruff', 'black', 'pytest')",
|
|
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
|
+
PythonVersion = Annotated[
|
|
32
|
+
Optional[str],
|
|
33
|
+
Field(
|
|
34
|
+
description="Python version to use (e.g., '3.11', '3.12')",
|
|
35
|
+
default=None,
|
|
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 UvxParams(TypedDict, total=False):
|
|
49
|
+
"""Parameters for uvx tool."""
|
|
50
|
+
|
|
51
|
+
package: str
|
|
52
|
+
args: Optional[str]
|
|
53
|
+
python_version: Optional[str]
|
|
54
|
+
timeout: int
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@final
|
|
58
|
+
class UvxTool(BaseTool):
|
|
59
|
+
"""Tool for running Python packages with uvx."""
|
|
60
|
+
|
|
61
|
+
def __init__(self, permission_manager: PermissionManager):
|
|
62
|
+
"""Initialize the uvx 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 "uvx"
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
@override
|
|
77
|
+
def description(self) -> str:
|
|
78
|
+
"""Get the tool description."""
|
|
79
|
+
return """Run Python packages using uvx (Python package runner).
|
|
80
|
+
|
|
81
|
+
uvx allows running Python applications in isolated environments without
|
|
82
|
+
installing them globally. It automatically manages dependencies and Python versions.
|
|
83
|
+
|
|
84
|
+
Common packages:
|
|
85
|
+
- ruff: Fast Python linter and formatter
|
|
86
|
+
- black: Python code formatter
|
|
87
|
+
- pytest: Testing framework
|
|
88
|
+
- mypy: Static type checker
|
|
89
|
+
- pipx: Install Python apps
|
|
90
|
+
- httpie: HTTP client
|
|
91
|
+
- poetry: Dependency management
|
|
92
|
+
|
|
93
|
+
Examples:
|
|
94
|
+
- uvx --package ruff --args "check ."
|
|
95
|
+
- uvx --package black --args "--check src/"
|
|
96
|
+
- uvx --package pytest --args "-v tests/"
|
|
97
|
+
- uvx --package httpie --args "GET httpbin.org/get"
|
|
98
|
+
- uvx --package mypy --args "--strict src/"
|
|
99
|
+
|
|
100
|
+
For long-running servers, use uvx_background instead.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
@override
|
|
104
|
+
async def call(
|
|
105
|
+
self,
|
|
106
|
+
ctx: MCPContext,
|
|
107
|
+
**params: Unpack[UvxParams],
|
|
108
|
+
) -> str:
|
|
109
|
+
"""Execute uvx command.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
ctx: MCP context
|
|
113
|
+
**params: Tool parameters
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Command output
|
|
117
|
+
"""
|
|
118
|
+
tool_ctx = create_tool_context(ctx)
|
|
119
|
+
await tool_ctx.set_tool_info(self.name)
|
|
120
|
+
|
|
121
|
+
# Extract parameters
|
|
122
|
+
package = params.get("package")
|
|
123
|
+
if not package:
|
|
124
|
+
return "Error: package is required"
|
|
125
|
+
|
|
126
|
+
args = params.get("args", "")
|
|
127
|
+
python_version = params.get("python_version")
|
|
128
|
+
timeout = params.get("timeout", 120)
|
|
129
|
+
|
|
130
|
+
# Check if uvx is available
|
|
131
|
+
if not shutil.which("uvx"):
|
|
132
|
+
return """Error: uvx is not installed. Install it with:
|
|
133
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
134
|
+
|
|
135
|
+
Or on macOS:
|
|
136
|
+
brew install uv"""
|
|
137
|
+
|
|
138
|
+
# Build command
|
|
139
|
+
cmd = ["uvx"]
|
|
140
|
+
|
|
141
|
+
if python_version:
|
|
142
|
+
cmd.extend(["--python", python_version])
|
|
143
|
+
|
|
144
|
+
cmd.append(package)
|
|
145
|
+
|
|
146
|
+
# Add package arguments
|
|
147
|
+
if args:
|
|
148
|
+
# Split args properly (basic parsing)
|
|
149
|
+
import shlex
|
|
150
|
+
cmd.extend(shlex.split(args))
|
|
151
|
+
|
|
152
|
+
await tool_ctx.info(f"Running: {' '.join(cmd)}")
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
# Execute command
|
|
156
|
+
result = subprocess.run(
|
|
157
|
+
cmd,
|
|
158
|
+
capture_output=True,
|
|
159
|
+
text=True,
|
|
160
|
+
timeout=timeout,
|
|
161
|
+
check=True
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
output = []
|
|
165
|
+
if result.stdout:
|
|
166
|
+
output.append(result.stdout)
|
|
167
|
+
if result.stderr:
|
|
168
|
+
output.append(f"\nSTDERR:\n{result.stderr}")
|
|
169
|
+
|
|
170
|
+
return "\n".join(output) if output else "Command completed successfully with no output."
|
|
171
|
+
|
|
172
|
+
except subprocess.TimeoutExpired:
|
|
173
|
+
return f"Error: Command timed out after {timeout} seconds. Use uvx_background for long-running processes."
|
|
174
|
+
except subprocess.CalledProcessError as e:
|
|
175
|
+
error_msg = [f"Error: Command failed with exit code {e.returncode}"]
|
|
176
|
+
if e.stdout:
|
|
177
|
+
error_msg.append(f"\nSTDOUT:\n{e.stdout}")
|
|
178
|
+
if e.stderr:
|
|
179
|
+
error_msg.append(f"\nSTDERR:\n{e.stderr}")
|
|
180
|
+
return "\n".join(error_msg)
|
|
181
|
+
except Exception as e:
|
|
182
|
+
await tool_ctx.error(f"Unexpected error: {str(e)}")
|
|
183
|
+
return f"Error running uvx: {str(e)}"
|
|
184
|
+
|
|
185
|
+
def register(self, mcp_server) -> None:
|
|
186
|
+
"""Register this tool with the MCP server."""
|
|
187
|
+
pass
|