hanzo-mcp 0.5.2__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 (114) 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 +66 -35
  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 +2 -2
  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 +1 -1
  19. hanzo_mcp/tools/common/thinking_tool.py +3 -5
  20. hanzo_mcp/tools/common/tool_disable.py +1 -1
  21. hanzo_mcp/tools/common/tool_enable.py +1 -1
  22. hanzo_mcp/tools/common/tool_list.py +49 -52
  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 +1 -1
  28. hanzo_mcp/tools/database/graph.py +482 -0
  29. hanzo_mcp/tools/database/graph_add.py +1 -1
  30. hanzo_mcp/tools/database/graph_query.py +1 -1
  31. hanzo_mcp/tools/database/graph_remove.py +1 -1
  32. hanzo_mcp/tools/database/graph_search.py +1 -1
  33. hanzo_mcp/tools/database/graph_stats.py +1 -1
  34. hanzo_mcp/tools/database/sql.py +411 -0
  35. hanzo_mcp/tools/database/sql_query.py +1 -1
  36. hanzo_mcp/tools/database/sql_search.py +1 -1
  37. hanzo_mcp/tools/database/sql_stats.py +1 -1
  38. hanzo_mcp/tools/editor/neovim_command.py +1 -1
  39. hanzo_mcp/tools/editor/neovim_edit.py +1 -1
  40. hanzo_mcp/tools/editor/neovim_session.py +1 -1
  41. hanzo_mcp/tools/filesystem/__init__.py +42 -13
  42. hanzo_mcp/tools/filesystem/base.py +1 -1
  43. hanzo_mcp/tools/filesystem/batch_search.py +4 -4
  44. hanzo_mcp/tools/filesystem/content_replace.py +3 -5
  45. hanzo_mcp/tools/filesystem/diff.py +193 -0
  46. hanzo_mcp/tools/filesystem/directory_tree.py +3 -5
  47. hanzo_mcp/tools/filesystem/edit.py +3 -5
  48. hanzo_mcp/tools/filesystem/find.py +443 -0
  49. hanzo_mcp/tools/filesystem/find_files.py +1 -1
  50. hanzo_mcp/tools/filesystem/git_search.py +1 -1
  51. hanzo_mcp/tools/filesystem/grep.py +2 -2
  52. hanzo_mcp/tools/filesystem/multi_edit.py +3 -5
  53. hanzo_mcp/tools/filesystem/read.py +17 -5
  54. hanzo_mcp/tools/filesystem/{grep_ast_tool.py → symbols.py} +17 -27
  55. hanzo_mcp/tools/filesystem/symbols_unified.py +376 -0
  56. hanzo_mcp/tools/filesystem/tree.py +268 -0
  57. hanzo_mcp/tools/filesystem/unified_search.py +711 -0
  58. hanzo_mcp/tools/filesystem/unix_aliases.py +99 -0
  59. hanzo_mcp/tools/filesystem/watch.py +174 -0
  60. hanzo_mcp/tools/filesystem/write.py +3 -5
  61. hanzo_mcp/tools/jupyter/__init__.py +9 -12
  62. hanzo_mcp/tools/jupyter/base.py +1 -1
  63. hanzo_mcp/tools/jupyter/jupyter.py +326 -0
  64. hanzo_mcp/tools/jupyter/notebook_edit.py +3 -4
  65. hanzo_mcp/tools/jupyter/notebook_read.py +3 -5
  66. hanzo_mcp/tools/llm/__init__.py +4 -0
  67. hanzo_mcp/tools/llm/consensus_tool.py +1 -1
  68. hanzo_mcp/tools/llm/llm_manage.py +1 -1
  69. hanzo_mcp/tools/llm/llm_tool.py +1 -1
  70. hanzo_mcp/tools/llm/llm_unified.py +851 -0
  71. hanzo_mcp/tools/llm/provider_tools.py +1 -1
  72. hanzo_mcp/tools/mcp/__init__.py +4 -0
  73. hanzo_mcp/tools/mcp/mcp_add.py +1 -1
  74. hanzo_mcp/tools/mcp/mcp_remove.py +1 -1
  75. hanzo_mcp/tools/mcp/mcp_stats.py +1 -1
  76. hanzo_mcp/tools/mcp/mcp_unified.py +503 -0
  77. hanzo_mcp/tools/shell/__init__.py +20 -42
  78. hanzo_mcp/tools/shell/base.py +1 -1
  79. hanzo_mcp/tools/shell/base_process.py +303 -0
  80. hanzo_mcp/tools/shell/bash_unified.py +134 -0
  81. hanzo_mcp/tools/shell/logs.py +1 -1
  82. hanzo_mcp/tools/shell/npx.py +1 -1
  83. hanzo_mcp/tools/shell/npx_background.py +1 -1
  84. hanzo_mcp/tools/shell/npx_unified.py +101 -0
  85. hanzo_mcp/tools/shell/open.py +107 -0
  86. hanzo_mcp/tools/shell/pkill.py +1 -1
  87. hanzo_mcp/tools/shell/process_unified.py +131 -0
  88. hanzo_mcp/tools/shell/processes.py +1 -1
  89. hanzo_mcp/tools/shell/run_background.py +1 -1
  90. hanzo_mcp/tools/shell/run_command.py +3 -4
  91. hanzo_mcp/tools/shell/run_command_windows.py +3 -4
  92. hanzo_mcp/tools/shell/uvx.py +1 -1
  93. hanzo_mcp/tools/shell/uvx_background.py +1 -1
  94. hanzo_mcp/tools/shell/uvx_unified.py +101 -0
  95. hanzo_mcp/tools/todo/__init__.py +1 -1
  96. hanzo_mcp/tools/todo/base.py +1 -1
  97. hanzo_mcp/tools/todo/todo.py +265 -0
  98. hanzo_mcp/tools/todo/todo_read.py +3 -5
  99. hanzo_mcp/tools/todo/todo_write.py +3 -5
  100. hanzo_mcp/tools/vector/__init__.py +1 -1
  101. hanzo_mcp/tools/vector/index_tool.py +1 -1
  102. hanzo_mcp/tools/vector/project_manager.py +27 -5
  103. hanzo_mcp/tools/vector/vector.py +311 -0
  104. hanzo_mcp/tools/vector/vector_index.py +1 -1
  105. hanzo_mcp/tools/vector/vector_search.py +1 -1
  106. hanzo_mcp-0.6.1.dist-info/METADATA +336 -0
  107. hanzo_mcp-0.6.1.dist-info/RECORD +134 -0
  108. hanzo_mcp-0.6.1.dist-info/entry_points.txt +3 -0
  109. hanzo_mcp-0.5.2.dist-info/METADATA +0 -276
  110. hanzo_mcp-0.5.2.dist-info/RECORD +0 -106
  111. hanzo_mcp-0.5.2.dist-info/entry_points.txt +0 -2
  112. {hanzo_mcp-0.5.2.dist-info → hanzo_mcp-0.6.1.dist-info}/WHEEL +0 -0
  113. {hanzo_mcp-0.5.2.dist-info → hanzo_mcp-0.6.1.dist-info}/licenses/LICENSE +0 -0
  114. {hanzo_mcp-0.5.2.dist-info → hanzo_mcp-0.6.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,303 @@
1
+ """Base classes for process execution tools."""
2
+
3
+ import asyncio
4
+ import subprocess
5
+ import tempfile
6
+ import uuid
7
+ from abc import abstractmethod
8
+ from pathlib import Path
9
+ from typing import Any, Dict, List, Optional, override
10
+
11
+ from mcp.server.fastmcp import Context as MCPContext
12
+
13
+ from hanzo_mcp.tools.common.base import BaseTool
14
+ from hanzo_mcp.tools.common.permissions import PermissionManager
15
+
16
+
17
+ class ProcessManager:
18
+ """Singleton manager for background processes."""
19
+
20
+ _instance = None
21
+ _processes: Dict[str, subprocess.Popen] = {}
22
+ _logs: Dict[str, List[str]] = {}
23
+ _log_dir = Path(tempfile.gettempdir()) / "hanzo_mcp_logs"
24
+
25
+ def __new__(cls):
26
+ if cls._instance is None:
27
+ cls._instance = super().__new__(cls)
28
+ cls._instance._log_dir.mkdir(exist_ok=True)
29
+ return cls._instance
30
+
31
+ def add_process(self, process_id: str, process: subprocess.Popen, log_file: Path) -> None:
32
+ """Add a process to track."""
33
+ self._processes[process_id] = process
34
+ self._logs[process_id] = str(log_file)
35
+
36
+ def get_process(self, process_id: str) -> Optional[subprocess.Popen]:
37
+ """Get a tracked process."""
38
+ return self._processes.get(process_id)
39
+
40
+ def remove_process(self, process_id: str) -> None:
41
+ """Remove a process from tracking."""
42
+ self._processes.pop(process_id, None)
43
+ self._logs.pop(process_id, None)
44
+
45
+ def list_processes(self) -> Dict[str, Dict[str, Any]]:
46
+ """List all tracked processes."""
47
+ result = {}
48
+ for pid, proc in list(self._processes.items()):
49
+ if proc.poll() is None:
50
+ result[pid] = {
51
+ "pid": proc.pid,
52
+ "running": True,
53
+ "log_file": self._logs.get(pid)
54
+ }
55
+ else:
56
+ result[pid] = {
57
+ "pid": proc.pid,
58
+ "running": False,
59
+ "return_code": proc.returncode,
60
+ "log_file": self._logs.get(pid)
61
+ }
62
+ # Clean up finished processes
63
+ self.remove_process(pid)
64
+ return result
65
+
66
+ def get_log_file(self, process_id: str) -> Optional[Path]:
67
+ """Get log file path for a process."""
68
+ log_path = self._logs.get(process_id)
69
+ return Path(log_path) if log_path else None
70
+
71
+ @property
72
+ def log_dir(self) -> Path:
73
+ """Get the log directory."""
74
+ return self._log_dir
75
+
76
+
77
+ class BaseProcessTool(BaseTool):
78
+ """Base class for all process execution tools."""
79
+
80
+ def __init__(self, permission_manager: Optional[PermissionManager] = None):
81
+ """Initialize the process tool.
82
+
83
+ Args:
84
+ permission_manager: Optional permission manager for access control
85
+ """
86
+ super().__init__()
87
+ self.permission_manager = permission_manager
88
+ self.process_manager = ProcessManager()
89
+
90
+ @abstractmethod
91
+ def get_command_args(self, command: str, **kwargs) -> List[str]:
92
+ """Get the command arguments for subprocess.
93
+
94
+ Args:
95
+ command: The command or script to run
96
+ **kwargs: Additional arguments specific to the tool
97
+
98
+ Returns:
99
+ List of command arguments for subprocess
100
+ """
101
+ pass
102
+
103
+ @abstractmethod
104
+ def get_tool_name(self) -> str:
105
+ """Get the name of the tool being used (e.g., 'bash', 'uvx', 'npx')."""
106
+ pass
107
+
108
+ async def execute_sync(
109
+ self,
110
+ command: str,
111
+ cwd: Optional[Path] = None,
112
+ env: Optional[Dict[str, str]] = None,
113
+ timeout: Optional[int] = None,
114
+ **kwargs
115
+ ) -> str:
116
+ """Execute a command synchronously and return output.
117
+
118
+ Args:
119
+ command: Command to execute
120
+ cwd: Working directory
121
+ env: Environment variables
122
+ timeout: Timeout in seconds
123
+ **kwargs: Additional tool-specific arguments
124
+
125
+ Returns:
126
+ Command output
127
+
128
+ Raises:
129
+ RuntimeError: If command fails
130
+ """
131
+ # Check permissions if manager is available
132
+ if self.permission_manager and cwd:
133
+ self.permission_manager.check_permission(cwd)
134
+
135
+ # Get command arguments
136
+ cmd_args = self.get_command_args(command, **kwargs)
137
+
138
+ # Prepare environment
139
+ process_env = os.environ.copy()
140
+ if env:
141
+ process_env.update(env)
142
+
143
+ try:
144
+ result = subprocess.run(
145
+ cmd_args,
146
+ cwd=cwd,
147
+ env=process_env,
148
+ capture_output=True,
149
+ text=True,
150
+ timeout=timeout,
151
+ check=True
152
+ )
153
+ return result.stdout
154
+ except subprocess.TimeoutExpired:
155
+ raise RuntimeError(f"{self.get_tool_name()} command timed out after {timeout} seconds")
156
+ except subprocess.CalledProcessError as e:
157
+ error_msg = f"{self.get_tool_name()} command failed with exit code {e.returncode}"
158
+ if e.stderr:
159
+ error_msg += f"\nError: {e.stderr}"
160
+ raise RuntimeError(error_msg)
161
+
162
+ async def execute_background(
163
+ self,
164
+ command: str,
165
+ cwd: Optional[Path] = None,
166
+ env: Optional[Dict[str, str]] = None,
167
+ **kwargs
168
+ ) -> Dict[str, Any]:
169
+ """Execute a command in the background.
170
+
171
+ Args:
172
+ command: Command to execute
173
+ cwd: Working directory
174
+ env: Environment variables
175
+ **kwargs: Additional tool-specific arguments
176
+
177
+ Returns:
178
+ Dict with process_id and log_file
179
+ """
180
+ # Check permissions if manager is available
181
+ if self.permission_manager and cwd:
182
+ self.permission_manager.check_permission(cwd)
183
+
184
+ # Generate process ID and log file
185
+ process_id = f"{self.get_tool_name()}_{uuid.uuid4().hex[:8]}"
186
+ log_file = self.process_manager.log_dir / f"{process_id}.log"
187
+
188
+ # Get command arguments
189
+ cmd_args = self.get_command_args(command, **kwargs)
190
+
191
+ # Prepare environment
192
+ process_env = os.environ.copy()
193
+ if env:
194
+ process_env.update(env)
195
+
196
+ # Start process with output to log file
197
+ with open(log_file, "w") as f:
198
+ process = subprocess.Popen(
199
+ cmd_args,
200
+ cwd=cwd,
201
+ env=process_env,
202
+ stdout=f,
203
+ stderr=subprocess.STDOUT,
204
+ text=True
205
+ )
206
+
207
+ # Track the process
208
+ self.process_manager.add_process(process_id, process, log_file)
209
+
210
+ return {
211
+ "process_id": process_id,
212
+ "pid": process.pid,
213
+ "log_file": str(log_file),
214
+ "status": "started"
215
+ }
216
+
217
+
218
+ class BaseBinaryTool(BaseProcessTool):
219
+ """Base class for binary execution tools (like npx, uvx)."""
220
+
221
+ @abstractmethod
222
+ def get_binary_name(self) -> str:
223
+ """Get the name of the binary to execute."""
224
+ pass
225
+
226
+ @override
227
+ def get_command_args(self, command: str, **kwargs) -> List[str]:
228
+ """Get command arguments for binary execution.
229
+
230
+ Args:
231
+ command: The package or command to run
232
+ **kwargs: Additional arguments (args, flags, etc.)
233
+
234
+ Returns:
235
+ List of command arguments
236
+ """
237
+ cmd_args = [self.get_binary_name()]
238
+
239
+ # Add any binary-specific flags
240
+ if "flags" in kwargs:
241
+ cmd_args.extend(kwargs["flags"])
242
+
243
+ # Add the command/package
244
+ cmd_args.append(command)
245
+
246
+ # Add any additional arguments
247
+ if "args" in kwargs:
248
+ if isinstance(kwargs["args"], str):
249
+ cmd_args.extend(kwargs["args"].split())
250
+ else:
251
+ cmd_args.extend(kwargs["args"])
252
+
253
+ return cmd_args
254
+
255
+ @override
256
+ def get_tool_name(self) -> str:
257
+ """Get the tool name (same as binary name by default)."""
258
+ return self.get_binary_name()
259
+
260
+
261
+ class BaseScriptTool(BaseProcessTool):
262
+ """Base class for script execution tools (like bash, python)."""
263
+
264
+ @abstractmethod
265
+ def get_interpreter(self) -> str:
266
+ """Get the interpreter to use."""
267
+ pass
268
+
269
+ @abstractmethod
270
+ def get_script_flags(self) -> List[str]:
271
+ """Get default flags for the interpreter."""
272
+ pass
273
+
274
+ @override
275
+ def get_command_args(self, command: str, **kwargs) -> List[str]:
276
+ """Get command arguments for script execution.
277
+
278
+ Args:
279
+ command: The script content to execute
280
+ **kwargs: Additional arguments
281
+
282
+ Returns:
283
+ List of command arguments
284
+ """
285
+ cmd_args = [self.get_interpreter()]
286
+ cmd_args.extend(self.get_script_flags())
287
+
288
+ # For inline scripts, use -c flag
289
+ if not kwargs.get("is_file", False):
290
+ cmd_args.extend(["-c", command])
291
+ else:
292
+ cmd_args.append(command)
293
+
294
+ return cmd_args
295
+
296
+ @override
297
+ def get_tool_name(self) -> str:
298
+ """Get the tool name (interpreter name by default)."""
299
+ return self.get_interpreter()
300
+
301
+
302
+ # Import os at the top of the file
303
+ import os
@@ -0,0 +1,134 @@
1
+ """Bash/Shell tool for command execution."""
2
+
3
+ import os
4
+ import platform
5
+ from pathlib import Path
6
+ from typing import Optional, override
7
+
8
+ from mcp.server.fastmcp import Context as MCPContext
9
+
10
+ from hanzo_mcp.tools.shell.base_process import BaseScriptTool
11
+ from mcp.server import FastMCP
12
+
13
+
14
+ class BashTool(BaseScriptTool):
15
+ """Tool for running shell commands."""
16
+
17
+ name = "bash"
18
+
19
+ def register(self, server: FastMCP) -> None:
20
+ """Register the tool with the MCP server."""
21
+ server.tool(name=self.name, description=self.description)(self.call)
22
+
23
+ async def call(self, **kwargs) -> str:
24
+ """Call the tool with arguments."""
25
+ return await self.run(None, **kwargs)
26
+
27
+ @property
28
+ @override
29
+ def description(self) -> str:
30
+ """Get the tool description."""
31
+ return """Run shell commands. Actions: run (default), background.
32
+
33
+ Usage:
34
+ bash "ls -la"
35
+ bash --action background "python server.py"
36
+ bash "git status && git diff"
37
+ bash --action background "npm run dev" --cwd ./frontend"""
38
+
39
+ @override
40
+ def get_interpreter(self) -> str:
41
+ """Get the shell interpreter."""
42
+ if platform.system() == "Windows":
43
+ return "cmd.exe"
44
+
45
+ # Check for user's preferred shell from environment
46
+ shell = os.environ.get("SHELL", "/bin/bash")
47
+
48
+ # Extract just the shell name from the path
49
+ shell_name = os.path.basename(shell)
50
+
51
+ # Check if it's a supported shell and the config file exists
52
+ if shell_name == "zsh":
53
+ # Check for .zshrc
54
+ zshrc_path = Path.home() / ".zshrc"
55
+ if zshrc_path.exists():
56
+ return shell # Use full path to zsh
57
+ elif shell_name == "fish":
58
+ # Check for fish config
59
+ fish_config = Path.home() / ".config" / "fish" / "config.fish"
60
+ if fish_config.exists():
61
+ return shell # Use full path to fish
62
+
63
+ # Default to bash if no special shell config found
64
+ return "bash"
65
+
66
+ @override
67
+ def get_script_flags(self) -> list[str]:
68
+ """Get interpreter flags."""
69
+ if platform.system() == "Windows":
70
+ return ["/c"]
71
+ return ["-c"]
72
+
73
+ @override
74
+ def get_tool_name(self) -> str:
75
+ """Get the tool name."""
76
+ if platform.system() == "Windows":
77
+ return "shell"
78
+
79
+ # Return the actual shell being used
80
+ interpreter = self.get_interpreter()
81
+ return os.path.basename(interpreter)
82
+
83
+ @override
84
+ async def run(
85
+ self,
86
+ ctx: MCPContext,
87
+ command: str,
88
+ action: str = "run",
89
+ cwd: Optional[str] = None,
90
+ env: Optional[dict[str, str]] = None,
91
+ timeout: Optional[int] = None,
92
+ ) -> str:
93
+ """Run a shell command.
94
+
95
+ Args:
96
+ ctx: MCP context
97
+ command: Shell command to execute
98
+ action: Action to perform (run, background)
99
+ cwd: Working directory
100
+ env: Environment variables
101
+ timeout: Command timeout in seconds
102
+
103
+ Returns:
104
+ Command output or process info
105
+ """
106
+ # Prepare working directory
107
+ work_dir = Path(cwd).resolve() if cwd else Path.cwd()
108
+
109
+ if action == "background":
110
+ result = await self.execute_background(
111
+ command,
112
+ cwd=work_dir,
113
+ env=env
114
+ )
115
+ return (
116
+ f"Started command in background\n"
117
+ f"Process ID: {result['process_id']}\n"
118
+ f"PID: {result['pid']}\n"
119
+ f"Log file: {result['log_file']}\n"
120
+ f"Command: {command}"
121
+ )
122
+ else:
123
+ # Default to sync execution
124
+ output = await self.execute_sync(
125
+ command,
126
+ cwd=work_dir,
127
+ env=env,
128
+ timeout=timeout or 120 # Default 2 minute timeout
129
+ )
130
+ return output if output else "Command completed successfully (no output)"
131
+
132
+
133
+ # Create tool instance
134
+ bash_tool = BashTool()
@@ -4,7 +4,7 @@ import os
4
4
  from pathlib import Path
5
5
  from typing import Annotated, Optional, TypedDict, Unpack, final, override
6
6
 
7
- from fastmcp import Context as MCPContext
7
+ from mcp.server.fastmcp import Context as MCPContext
8
8
  from pydantic import Field
9
9
 
10
10
  from hanzo_mcp.tools.common.base import BaseTool
@@ -4,7 +4,7 @@ import subprocess
4
4
  import shutil
5
5
  from typing import Annotated, Optional, TypedDict, Unpack, final, override
6
6
 
7
- from fastmcp import Context as MCPContext
7
+ from mcp.server.fastmcp import Context as MCPContext
8
8
  from pydantic import Field
9
9
 
10
10
  from hanzo_mcp.tools.common.base import BaseTool
@@ -5,7 +5,7 @@ import shutil
5
5
  import uuid
6
6
  from typing import Annotated, Optional, TypedDict, Unpack, final, override
7
7
 
8
- from fastmcp import Context as MCPContext
8
+ from mcp.server.fastmcp import Context as MCPContext
9
9
  from pydantic import Field
10
10
 
11
11
  from hanzo_mcp.tools.common.base import BaseTool
@@ -0,0 +1,101 @@
1
+ """NPX tool for both sync and background execution."""
2
+
3
+ from pathlib import Path
4
+ from typing import Optional, override
5
+
6
+ from mcp.server.fastmcp import Context as MCPContext
7
+
8
+ from hanzo_mcp.tools.shell.base_process import BaseBinaryTool
9
+ from mcp.server import FastMCP
10
+
11
+
12
+ class NpxTool(BaseBinaryTool):
13
+ """Tool for running npx commands."""
14
+
15
+ name = "npx"
16
+
17
+ @property
18
+ @override
19
+ def description(self) -> str:
20
+ """Get the tool description."""
21
+ return """Run npx packages. Actions: run (default), background.
22
+
23
+ Usage:
24
+ npx create-react-app my-app
25
+ npx --action background http-server -p 8080
26
+ npx prettier --write "**/*.js"
27
+ npx --action background json-server db.json"""
28
+
29
+ @override
30
+ def get_binary_name(self) -> str:
31
+ """Get the binary name."""
32
+ return "npx"
33
+
34
+ @override
35
+ async def run(
36
+ self,
37
+ ctx: MCPContext,
38
+ package: str,
39
+ args: str = "",
40
+ action: str = "run",
41
+ cwd: Optional[str] = None,
42
+ yes: bool = True,
43
+ ) -> str:
44
+ """Run an npx command.
45
+
46
+ Args:
47
+ ctx: MCP context
48
+ package: NPX package to run
49
+ args: Additional arguments
50
+ action: Action to perform (run, background)
51
+ cwd: Working directory
52
+ yes: Auto-confirm package installation
53
+
54
+ Returns:
55
+ Command output or process info
56
+ """
57
+ # Prepare working directory
58
+ work_dir = Path(cwd).resolve() if cwd else Path.cwd()
59
+
60
+ # Prepare flags
61
+ flags = []
62
+ if yes:
63
+ flags.append("-y")
64
+
65
+ # Build full command
66
+ full_args = args.split() if args else []
67
+
68
+ if action == "background":
69
+ result = await self.execute_background(
70
+ package,
71
+ cwd=work_dir,
72
+ flags=flags,
73
+ args=full_args
74
+ )
75
+ return (
76
+ f"Started npx process in background\n"
77
+ f"Process ID: {result['process_id']}\n"
78
+ f"PID: {result['pid']}\n"
79
+ f"Log file: {result['log_file']}"
80
+ )
81
+ else:
82
+ # Default to sync execution
83
+ return await self.execute_sync(
84
+ package,
85
+ cwd=work_dir,
86
+ flags=flags,
87
+ args=full_args,
88
+ timeout=300 # 5 minute timeout for npx
89
+ )
90
+
91
+ def register(self, server: FastMCP) -> None:
92
+ """Register the tool with the MCP server."""
93
+ server.tool(name=self.name, description=self.description)(self.call)
94
+
95
+ async def call(self, **kwargs) -> str:
96
+ """Call the tool with arguments."""
97
+ return await self.run(None, **kwargs)
98
+
99
+
100
+ # Create tool instance
101
+ npx_tool = NpxTool()
@@ -0,0 +1,107 @@
1
+ """Open files or URLs in the default application."""
2
+
3
+ import platform
4
+ import subprocess
5
+ import webbrowser
6
+ from pathlib import Path
7
+ from typing import override
8
+ from urllib.parse import urlparse
9
+
10
+ from mcp.server.fastmcp import Context as MCPContext
11
+
12
+ from hanzo_mcp.tools.common.base import BaseTool
13
+ from mcp.server import FastMCP
14
+
15
+
16
+ class OpenTool(BaseTool):
17
+ """Tool for opening files or URLs in the default application."""
18
+
19
+ name = "open"
20
+
21
+ def register(self, server: FastMCP) -> None:
22
+ """Register the tool with the MCP server."""
23
+ server.tool(name=self.name, description=self.description)(self.call)
24
+
25
+ async def call(self, **kwargs) -> str:
26
+ """Call the tool with arguments."""
27
+ return await self.run(None, **kwargs)
28
+
29
+ @property
30
+ @override
31
+ def description(self) -> str:
32
+ """Get the tool description."""
33
+ return """Open files or URLs. Platform-aware.
34
+
35
+ Usage:
36
+ open https://example.com
37
+ open ./document.pdf
38
+ open /path/to/image.png"""
39
+
40
+ @override
41
+ async def run(self, ctx: MCPContext, path: str) -> str:
42
+ """Open a file or URL in the default application.
43
+
44
+ Args:
45
+ ctx: MCP context
46
+ path: File path or URL to open
47
+
48
+ Returns:
49
+ Success message
50
+
51
+ Raises:
52
+ RuntimeError: If opening fails
53
+ """
54
+ # Check if it's a URL
55
+ parsed = urlparse(path)
56
+ is_url = parsed.scheme in ('http', 'https', 'ftp', 'file')
57
+
58
+ if is_url:
59
+ # Open URL in default browser
60
+ try:
61
+ webbrowser.open(path)
62
+ return f"Opened URL in browser: {path}"
63
+ except Exception as e:
64
+ raise RuntimeError(f"Failed to open URL: {e}")
65
+
66
+ # It's a file path
67
+ file_path = Path(path).expanduser().resolve()
68
+
69
+ if not file_path.exists():
70
+ raise RuntimeError(f"File not found: {file_path}")
71
+
72
+ system = platform.system().lower()
73
+
74
+ try:
75
+ if system == "darwin": # macOS
76
+ subprocess.run(["open", str(file_path)], check=True)
77
+ elif system == "linux":
78
+ # Try xdg-open first (most common)
79
+ try:
80
+ subprocess.run(["xdg-open", str(file_path)], check=True)
81
+ except (subprocess.CalledProcessError, FileNotFoundError):
82
+ # Fallback to other common openers
83
+ for opener in ["gnome-open", "kde-open", "exo-open"]:
84
+ try:
85
+ subprocess.run([opener, str(file_path)], check=True)
86
+ break
87
+ except (subprocess.CalledProcessError, FileNotFoundError):
88
+ continue
89
+ else:
90
+ raise RuntimeError("No suitable file opener found on Linux")
91
+ elif system == "windows":
92
+ # Use os.startfile on Windows
93
+ import os
94
+ os.startfile(str(file_path))
95
+ else:
96
+ raise RuntimeError(f"Unsupported platform: {system}")
97
+
98
+ return f"Opened file: {file_path}"
99
+
100
+ except subprocess.CalledProcessError as e:
101
+ raise RuntimeError(f"Failed to open file: {e}")
102
+ except Exception as e:
103
+ raise RuntimeError(f"Error opening file: {e}")
104
+
105
+
106
+ # Create tool instance
107
+ open_tool = OpenTool()
@@ -6,7 +6,7 @@ import psutil
6
6
  from datetime import datetime
7
7
  from typing import Annotated, Optional, TypedDict, Unpack, final, override
8
8
 
9
- from fastmcp import Context as MCPContext
9
+ from mcp.server.fastmcp import Context as MCPContext
10
10
  from pydantic import Field
11
11
 
12
12
  from hanzo_mcp.tools.common.base import BaseTool