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,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 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., '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 mcp.server.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,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()