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,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 fastmcp import FastMCP
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 fastmcp import FastMCP
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