hanzo-mcp 0.7.7__py3-none-any.whl → 0.8.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 (178) hide show
  1. hanzo_mcp/__init__.py +6 -0
  2. hanzo_mcp/__main__.py +1 -1
  3. hanzo_mcp/analytics/__init__.py +2 -2
  4. hanzo_mcp/analytics/posthog_analytics.py +76 -82
  5. hanzo_mcp/cli.py +31 -36
  6. hanzo_mcp/cli_enhanced.py +94 -72
  7. hanzo_mcp/cli_plugin.py +27 -17
  8. hanzo_mcp/config/__init__.py +2 -2
  9. hanzo_mcp/config/settings.py +112 -88
  10. hanzo_mcp/config/tool_config.py +32 -34
  11. hanzo_mcp/dev_server.py +66 -67
  12. hanzo_mcp/prompts/__init__.py +94 -12
  13. hanzo_mcp/prompts/enhanced_prompts.py +809 -0
  14. hanzo_mcp/prompts/example_custom_prompt.py +6 -5
  15. hanzo_mcp/prompts/project_todo_reminder.py +0 -1
  16. hanzo_mcp/prompts/tool_explorer.py +10 -7
  17. hanzo_mcp/server.py +17 -21
  18. hanzo_mcp/server_enhanced.py +15 -22
  19. hanzo_mcp/tools/__init__.py +56 -28
  20. hanzo_mcp/tools/agent/__init__.py +16 -19
  21. hanzo_mcp/tools/agent/agent.py +82 -65
  22. hanzo_mcp/tools/agent/agent_tool.py +152 -122
  23. hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +66 -62
  24. hanzo_mcp/tools/agent/clarification_protocol.py +55 -50
  25. hanzo_mcp/tools/agent/clarification_tool.py +11 -10
  26. hanzo_mcp/tools/agent/claude_cli_tool.py +21 -20
  27. hanzo_mcp/tools/agent/claude_desktop_auth.py +130 -144
  28. hanzo_mcp/tools/agent/cli_agent_base.py +59 -53
  29. hanzo_mcp/tools/agent/code_auth.py +102 -107
  30. hanzo_mcp/tools/agent/code_auth_tool.py +28 -27
  31. hanzo_mcp/tools/agent/codex_cli_tool.py +20 -19
  32. hanzo_mcp/tools/agent/critic_tool.py +86 -73
  33. hanzo_mcp/tools/agent/gemini_cli_tool.py +21 -20
  34. hanzo_mcp/tools/agent/grok_cli_tool.py +21 -20
  35. hanzo_mcp/tools/agent/iching_tool.py +404 -139
  36. hanzo_mcp/tools/agent/network_tool.py +89 -73
  37. hanzo_mcp/tools/agent/prompt.py +2 -1
  38. hanzo_mcp/tools/agent/review_tool.py +101 -98
  39. hanzo_mcp/tools/agent/swarm_alias.py +87 -0
  40. hanzo_mcp/tools/agent/swarm_tool.py +246 -161
  41. hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +134 -92
  42. hanzo_mcp/tools/agent/tool_adapter.py +21 -11
  43. hanzo_mcp/tools/common/__init__.py +1 -1
  44. hanzo_mcp/tools/common/base.py +3 -5
  45. hanzo_mcp/tools/common/batch_tool.py +46 -39
  46. hanzo_mcp/tools/common/config_tool.py +120 -84
  47. hanzo_mcp/tools/common/context.py +1 -5
  48. hanzo_mcp/tools/common/context_fix.py +5 -3
  49. hanzo_mcp/tools/common/critic_tool.py +4 -8
  50. hanzo_mcp/tools/common/decorators.py +58 -56
  51. hanzo_mcp/tools/common/enhanced_base.py +29 -32
  52. hanzo_mcp/tools/common/fastmcp_pagination.py +91 -94
  53. hanzo_mcp/tools/common/forgiving_edit.py +91 -87
  54. hanzo_mcp/tools/common/mode.py +15 -17
  55. hanzo_mcp/tools/common/mode_loader.py +27 -24
  56. hanzo_mcp/tools/common/paginated_base.py +61 -53
  57. hanzo_mcp/tools/common/paginated_response.py +72 -79
  58. hanzo_mcp/tools/common/pagination.py +50 -53
  59. hanzo_mcp/tools/common/permissions.py +4 -4
  60. hanzo_mcp/tools/common/personality.py +186 -138
  61. hanzo_mcp/tools/common/plugin_loader.py +54 -54
  62. hanzo_mcp/tools/common/stats.py +65 -47
  63. hanzo_mcp/tools/common/test_helpers.py +31 -0
  64. hanzo_mcp/tools/common/thinking_tool.py +4 -8
  65. hanzo_mcp/tools/common/tool_disable.py +17 -12
  66. hanzo_mcp/tools/common/tool_enable.py +13 -14
  67. hanzo_mcp/tools/common/tool_list.py +36 -28
  68. hanzo_mcp/tools/common/truncate.py +23 -23
  69. hanzo_mcp/tools/config/__init__.py +4 -4
  70. hanzo_mcp/tools/config/config_tool.py +42 -29
  71. hanzo_mcp/tools/config/index_config.py +37 -34
  72. hanzo_mcp/tools/config/mode_tool.py +175 -55
  73. hanzo_mcp/tools/database/__init__.py +15 -12
  74. hanzo_mcp/tools/database/database_manager.py +77 -75
  75. hanzo_mcp/tools/database/graph.py +137 -91
  76. hanzo_mcp/tools/database/graph_add.py +30 -18
  77. hanzo_mcp/tools/database/graph_query.py +178 -102
  78. hanzo_mcp/tools/database/graph_remove.py +33 -28
  79. hanzo_mcp/tools/database/graph_search.py +97 -75
  80. hanzo_mcp/tools/database/graph_stats.py +91 -59
  81. hanzo_mcp/tools/database/sql.py +107 -79
  82. hanzo_mcp/tools/database/sql_query.py +30 -24
  83. hanzo_mcp/tools/database/sql_search.py +29 -25
  84. hanzo_mcp/tools/database/sql_stats.py +47 -35
  85. hanzo_mcp/tools/editor/neovim_command.py +25 -28
  86. hanzo_mcp/tools/editor/neovim_edit.py +21 -23
  87. hanzo_mcp/tools/editor/neovim_session.py +60 -54
  88. hanzo_mcp/tools/filesystem/__init__.py +31 -30
  89. hanzo_mcp/tools/filesystem/ast_multi_edit.py +329 -249
  90. hanzo_mcp/tools/filesystem/ast_tool.py +4 -4
  91. hanzo_mcp/tools/filesystem/base.py +1 -1
  92. hanzo_mcp/tools/filesystem/batch_search.py +316 -224
  93. hanzo_mcp/tools/filesystem/content_replace.py +4 -4
  94. hanzo_mcp/tools/filesystem/diff.py +71 -59
  95. hanzo_mcp/tools/filesystem/directory_tree.py +7 -7
  96. hanzo_mcp/tools/filesystem/directory_tree_paginated.py +49 -37
  97. hanzo_mcp/tools/filesystem/edit.py +4 -4
  98. hanzo_mcp/tools/filesystem/find.py +173 -80
  99. hanzo_mcp/tools/filesystem/find_files.py +73 -52
  100. hanzo_mcp/tools/filesystem/git_search.py +157 -104
  101. hanzo_mcp/tools/filesystem/grep.py +8 -8
  102. hanzo_mcp/tools/filesystem/multi_edit.py +4 -8
  103. hanzo_mcp/tools/filesystem/read.py +12 -10
  104. hanzo_mcp/tools/filesystem/rules_tool.py +59 -43
  105. hanzo_mcp/tools/filesystem/search_tool.py +263 -207
  106. hanzo_mcp/tools/filesystem/symbols_tool.py +94 -54
  107. hanzo_mcp/tools/filesystem/tree.py +35 -33
  108. hanzo_mcp/tools/filesystem/unix_aliases.py +13 -18
  109. hanzo_mcp/tools/filesystem/watch.py +37 -36
  110. hanzo_mcp/tools/filesystem/write.py +4 -8
  111. hanzo_mcp/tools/jupyter/__init__.py +4 -4
  112. hanzo_mcp/tools/jupyter/base.py +4 -5
  113. hanzo_mcp/tools/jupyter/jupyter.py +67 -47
  114. hanzo_mcp/tools/jupyter/notebook_edit.py +4 -4
  115. hanzo_mcp/tools/jupyter/notebook_read.py +4 -7
  116. hanzo_mcp/tools/llm/__init__.py +5 -7
  117. hanzo_mcp/tools/llm/consensus_tool.py +72 -52
  118. hanzo_mcp/tools/llm/llm_manage.py +101 -60
  119. hanzo_mcp/tools/llm/llm_tool.py +226 -166
  120. hanzo_mcp/tools/llm/provider_tools.py +25 -26
  121. hanzo_mcp/tools/lsp/__init__.py +1 -1
  122. hanzo_mcp/tools/lsp/lsp_tool.py +228 -143
  123. hanzo_mcp/tools/mcp/__init__.py +2 -3
  124. hanzo_mcp/tools/mcp/mcp_add.py +27 -25
  125. hanzo_mcp/tools/mcp/mcp_remove.py +7 -8
  126. hanzo_mcp/tools/mcp/mcp_stats.py +23 -22
  127. hanzo_mcp/tools/mcp/mcp_tool.py +129 -98
  128. hanzo_mcp/tools/memory/__init__.py +39 -21
  129. hanzo_mcp/tools/memory/knowledge_tools.py +124 -99
  130. hanzo_mcp/tools/memory/memory_tools.py +90 -108
  131. hanzo_mcp/tools/search/__init__.py +7 -2
  132. hanzo_mcp/tools/search/find_tool.py +297 -212
  133. hanzo_mcp/tools/search/unified_search.py +366 -314
  134. hanzo_mcp/tools/shell/__init__.py +8 -7
  135. hanzo_mcp/tools/shell/auto_background.py +56 -49
  136. hanzo_mcp/tools/shell/base.py +1 -1
  137. hanzo_mcp/tools/shell/base_process.py +75 -75
  138. hanzo_mcp/tools/shell/bash_session.py +2 -2
  139. hanzo_mcp/tools/shell/bash_session_executor.py +4 -4
  140. hanzo_mcp/tools/shell/bash_tool.py +24 -31
  141. hanzo_mcp/tools/shell/command_executor.py +12 -12
  142. hanzo_mcp/tools/shell/logs.py +43 -33
  143. hanzo_mcp/tools/shell/npx.py +13 -13
  144. hanzo_mcp/tools/shell/npx_background.py +24 -21
  145. hanzo_mcp/tools/shell/npx_tool.py +18 -22
  146. hanzo_mcp/tools/shell/open.py +19 -21
  147. hanzo_mcp/tools/shell/pkill.py +31 -26
  148. hanzo_mcp/tools/shell/process_tool.py +32 -32
  149. hanzo_mcp/tools/shell/processes.py +57 -58
  150. hanzo_mcp/tools/shell/run_background.py +24 -25
  151. hanzo_mcp/tools/shell/run_command.py +5 -5
  152. hanzo_mcp/tools/shell/run_command_windows.py +5 -5
  153. hanzo_mcp/tools/shell/session_storage.py +3 -3
  154. hanzo_mcp/tools/shell/streaming_command.py +141 -126
  155. hanzo_mcp/tools/shell/uvx.py +24 -25
  156. hanzo_mcp/tools/shell/uvx_background.py +35 -33
  157. hanzo_mcp/tools/shell/uvx_tool.py +18 -22
  158. hanzo_mcp/tools/todo/__init__.py +6 -2
  159. hanzo_mcp/tools/todo/todo.py +50 -37
  160. hanzo_mcp/tools/todo/todo_read.py +5 -8
  161. hanzo_mcp/tools/todo/todo_write.py +5 -7
  162. hanzo_mcp/tools/vector/__init__.py +40 -28
  163. hanzo_mcp/tools/vector/ast_analyzer.py +176 -143
  164. hanzo_mcp/tools/vector/git_ingester.py +170 -179
  165. hanzo_mcp/tools/vector/index_tool.py +96 -44
  166. hanzo_mcp/tools/vector/infinity_store.py +283 -228
  167. hanzo_mcp/tools/vector/mock_infinity.py +39 -40
  168. hanzo_mcp/tools/vector/project_manager.py +88 -78
  169. hanzo_mcp/tools/vector/vector.py +59 -42
  170. hanzo_mcp/tools/vector/vector_index.py +30 -27
  171. hanzo_mcp/tools/vector/vector_search.py +64 -45
  172. hanzo_mcp/types.py +6 -4
  173. {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.1.dist-info}/METADATA +1 -1
  174. hanzo_mcp-0.8.1.dist-info/RECORD +185 -0
  175. hanzo_mcp-0.7.7.dist-info/RECORD +0 -182
  176. {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.1.dist-info}/WHEEL +0 -0
  177. {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.1.dist-info}/entry_points.txt +0 -0
  178. {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.1.dist-info}/top_level.txt +0 -0
@@ -1,18 +1,16 @@
1
1
  """Tool for viewing process logs."""
2
2
 
3
- import os
3
+ from typing import Unpack, Optional, Annotated, TypedDict, final, override
4
4
  from pathlib import Path
5
- from typing import Annotated, Optional, TypedDict, Unpack, final, override
6
5
 
7
- from mcp.server.fastmcp import Context as MCPContext
8
6
  from pydantic import Field
7
+ from mcp.server.fastmcp import Context as MCPContext
9
8
 
10
9
  from hanzo_mcp.tools.common.base import BaseTool
11
10
  from hanzo_mcp.tools.common.context import create_tool_context
12
11
  from hanzo_mcp.tools.common.permissions import PermissionManager
13
12
  from hanzo_mcp.tools.shell.run_background import RunBackgroundTool
14
13
 
15
-
16
14
  ProcessId = Annotated[
17
15
  Optional[str],
18
16
  Field(
@@ -142,20 +140,20 @@ Use run_command with 'tail -f' for continuous monitoring.
142
140
  process = RunBackgroundTool.get_process(process_id)
143
141
  if not process:
144
142
  return f"Process with ID '{process_id}' not found."
145
-
143
+
146
144
  if not process.log_file:
147
145
  return f"Process '{process_id}' does not have logging enabled."
148
-
146
+
149
147
  log_path = process.log_file
150
-
148
+
151
149
  elif log_file:
152
150
  # Use specified log file
153
151
  log_path = Path(log_file)
154
-
152
+
155
153
  # Check if it's in the logs directory
156
154
  if not log_path.is_absolute():
157
155
  log_path = self.log_dir / log_path
158
-
156
+
159
157
  else:
160
158
  return "Error: Must specify --id or --file"
161
159
 
@@ -169,13 +167,15 @@ Use run_command with 'tail -f' for continuous monitoring.
169
167
 
170
168
  # Note about follow mode
171
169
  if follow:
172
- await tool_ctx.warning("Follow mode not supported in MCP. Showing latest lines instead.")
170
+ await tool_ctx.warning(
171
+ "Follow mode not supported in MCP. Showing latest lines instead."
172
+ )
173
173
 
174
174
  # Read log file
175
175
  await tool_ctx.info(f"Reading log file: {log_path}")
176
-
176
+
177
177
  try:
178
- with open(log_path, 'r') as f:
178
+ with open(log_path, "r") as f:
179
179
  if lines == -1:
180
180
  # Read entire file
181
181
  content = f.read()
@@ -183,13 +183,13 @@ Use run_command with 'tail -f' for continuous monitoring.
183
183
  # Read last N lines
184
184
  all_lines = f.readlines()
185
185
  if len(all_lines) <= lines:
186
- content = ''.join(all_lines)
186
+ content = "".join(all_lines)
187
187
  else:
188
- content = ''.join(all_lines[-lines:])
189
-
188
+ content = "".join(all_lines[-lines:])
189
+
190
190
  if not content:
191
191
  return f"Log file is empty: {log_path}"
192
-
192
+
193
193
  # Add header
194
194
  header = f"=== Log: {log_path.name} ===\n"
195
195
  if process_id:
@@ -197,12 +197,16 @@ Use run_command with 'tail -f' for continuous monitoring.
197
197
  if process:
198
198
  header += f"Process: {process.name} (ID: {process_id})\n"
199
199
  header += f"Command: {process.command}\n"
200
- status = "running" if process.is_running else f"finished (code: {process.return_code})"
200
+ status = (
201
+ "running"
202
+ if process.is_running
203
+ else f"finished (code: {process.return_code})"
204
+ )
201
205
  header += f"Status: {status}\n"
202
206
  header += f"{'=' * 50}\n"
203
-
207
+
204
208
  return header + content
205
-
209
+
206
210
  except Exception as e:
207
211
  return f"Error reading log file: {str(e)}"
208
212
 
@@ -213,48 +217,54 @@ Use run_command with 'tail -f' for continuous monitoring.
213
217
  async def _list_logs(self, tool_ctx) -> str:
214
218
  """List all available log files."""
215
219
  await tool_ctx.info("Listing available log files")
216
-
220
+
217
221
  if not self.log_dir.exists():
218
222
  return "No logs directory found."
219
-
223
+
220
224
  # Get all log files
221
225
  log_files = list(self.log_dir.glob("*.log"))
222
-
226
+
223
227
  if not log_files:
224
228
  return "No log files found."
225
-
229
+
226
230
  # Sort by modification time (newest first)
227
231
  log_files.sort(key=lambda x: x.stat().st_mtime, reverse=True)
228
-
232
+
229
233
  # Check which logs belong to active processes
230
234
  active_processes = RunBackgroundTool.get_processes()
231
- active_log_files = {str(p.log_file): (pid, p) for pid, p in active_processes.items() if p.log_file}
232
-
235
+ active_log_files = {
236
+ str(p.log_file): (pid, p)
237
+ for pid, p in active_processes.items()
238
+ if p.log_file
239
+ }
240
+
233
241
  # Build output
234
242
  output = []
235
243
  output.append("=== Available Log Files ===\n")
236
-
244
+
237
245
  for log_file in log_files[:50]: # Limit to 50 most recent
238
246
  size = log_file.stat().st_size
239
247
  size_str = self._format_size(size)
240
-
248
+
241
249
  # Check if this belongs to an active process
242
250
  if str(log_file) in active_log_files:
243
251
  pid, process = active_log_files[str(log_file)]
244
252
  status = "active" if process.is_running else "finished"
245
- output.append(f"{log_file.name:<50} {size_str:>10} [{status}] (ID: {pid})")
253
+ output.append(
254
+ f"{log_file.name:<50} {size_str:>10} [{status}] (ID: {pid})"
255
+ )
246
256
  else:
247
257
  output.append(f"{log_file.name:<50} {size_str:>10}")
248
-
258
+
249
259
  output.append(f"\nTotal: {len(log_files)} log file(s)")
250
260
  output.append("\nUse 'logs --file <filename>' to view a specific log")
251
261
  output.append("Use 'logs --id <process-id>' to view logs for a running process")
252
-
262
+
253
263
  return "\n".join(output)
254
264
 
255
265
  def _format_size(self, size: int) -> str:
256
266
  """Format file size in human-readable format."""
257
- for unit in ['B', 'KB', 'MB', 'GB']:
267
+ for unit in ["B", "KB", "MB", "GB"]:
258
268
  if size < 1024.0:
259
269
  return f"{size:.1f} {unit}"
260
270
  size /= 1024.0
@@ -262,4 +272,4 @@ Use run_command with 'tail -f' for continuous monitoring.
262
272
 
263
273
  def register(self, mcp_server) -> None:
264
274
  """Register this tool with the MCP server."""
265
- pass
275
+ pass
@@ -1,17 +1,16 @@
1
1
  """Run Node.js packages with npx."""
2
2
 
3
- import subprocess
4
3
  import shutil
5
- from typing import Annotated, Optional, TypedDict, Unpack, final, override
4
+ import subprocess
5
+ from typing import Unpack, Optional, Annotated, TypedDict, final, override
6
6
 
7
- from mcp.server.fastmcp import Context as MCPContext
8
7
  from pydantic import Field
8
+ from mcp.server.fastmcp import Context as MCPContext
9
9
 
10
10
  from hanzo_mcp.tools.common.base import BaseTool
11
11
  from hanzo_mcp.tools.common.context import create_tool_context
12
12
  from hanzo_mcp.tools.common.permissions import PermissionManager
13
13
 
14
-
15
14
  Package = Annotated[
16
15
  str,
17
16
  Field(
@@ -144,16 +143,17 @@ Or download from: https://nodejs.org/"""
144
143
 
145
144
  # Build command
146
145
  cmd = ["npx"]
147
-
146
+
148
147
  if yes:
149
148
  cmd.append("--yes")
150
-
149
+
151
150
  cmd.append(package)
152
-
151
+
153
152
  # Add package arguments
154
153
  if args:
155
154
  # Split args properly (basic parsing)
156
155
  import shlex
156
+
157
157
  cmd.extend(shlex.split(args))
158
158
 
159
159
  await tool_ctx.info(f"Running: {' '.join(cmd)}")
@@ -161,11 +161,7 @@ Or download from: https://nodejs.org/"""
161
161
  try:
162
162
  # Execute command
163
163
  result = subprocess.run(
164
- cmd,
165
- capture_output=True,
166
- text=True,
167
- timeout=timeout,
168
- check=True
164
+ cmd, capture_output=True, text=True, timeout=timeout, check=True
169
165
  )
170
166
 
171
167
  output = []
@@ -174,7 +170,11 @@ Or download from: https://nodejs.org/"""
174
170
  if result.stderr:
175
171
  output.append(f"\nSTDERR:\n{result.stderr}")
176
172
 
177
- return "\n".join(output) if output else "Command completed successfully with no output."
173
+ return (
174
+ "\n".join(output)
175
+ if output
176
+ else "Command completed successfully with no output."
177
+ )
178
178
 
179
179
  except subprocess.TimeoutExpired:
180
180
  return f"Error: Command timed out after {timeout} seconds. Use npx_background for long-running processes."
@@ -1,19 +1,18 @@
1
1
  """Run Node.js packages in background with npx."""
2
2
 
3
- import subprocess
4
- import shutil
5
3
  import uuid
6
- from typing import Annotated, Optional, TypedDict, Unpack, final, override
4
+ import shutil
5
+ import subprocess
6
+ from typing import Unpack, Optional, Annotated, TypedDict, final, override
7
7
 
8
- from mcp.server.fastmcp import Context as MCPContext
9
8
  from pydantic import Field
9
+ from mcp.server.fastmcp import Context as MCPContext
10
10
 
11
11
  from hanzo_mcp.tools.common.base import BaseTool
12
12
  from hanzo_mcp.tools.common.context import create_tool_context
13
13
  from hanzo_mcp.tools.common.permissions import PermissionManager
14
14
  from hanzo_mcp.tools.shell.run_background import BackgroundProcess, RunBackgroundTool
15
15
 
16
-
17
16
  Package = Annotated[
18
17
  str,
19
18
  Field(
@@ -163,25 +162,27 @@ Or download from: https://nodejs.org/"""
163
162
 
164
163
  # Build command
165
164
  cmd = ["npx"]
166
-
165
+
167
166
  if yes:
168
167
  cmd.append("--yes")
169
-
168
+
170
169
  cmd.append(package)
171
-
170
+
172
171
  # Add package arguments
173
172
  if args:
174
173
  # Split args properly (basic parsing)
175
174
  import shlex
175
+
176
176
  cmd.extend(shlex.split(args))
177
177
 
178
178
  # Generate process ID
179
179
  process_id = str(uuid.uuid4())[:8]
180
-
180
+
181
181
  # Prepare log file if needed
182
182
  log_file = None
183
183
  if log_output:
184
184
  from pathlib import Path
185
+
185
186
  log_dir = Path.home() / ".hanzo" / "logs"
186
187
  log_dir.mkdir(parents=True, exist_ok=True)
187
188
  log_file = log_dir / f"{name}_{process_id}.log"
@@ -197,7 +198,7 @@ Or download from: https://nodejs.org/"""
197
198
  stdout=f,
198
199
  stderr=subprocess.STDOUT,
199
200
  cwd=working_dir,
200
- start_new_session=True
201
+ start_new_session=True,
201
202
  )
202
203
  else:
203
204
  process = subprocess.Popen(
@@ -205,7 +206,7 @@ Or download from: https://nodejs.org/"""
205
206
  stdout=subprocess.DEVNULL,
206
207
  stderr=subprocess.DEVNULL,
207
208
  cwd=working_dir,
208
- start_new_session=True
209
+ start_new_session=True,
209
210
  )
210
211
 
211
212
  # Create background process object
@@ -215,7 +216,7 @@ Or download from: https://nodejs.org/"""
215
216
  name=name,
216
217
  process=process,
217
218
  log_file=str(log_file) if log_file else None,
218
- working_dir=working_dir
219
+ working_dir=working_dir,
219
220
  )
220
221
 
221
222
  # Register with RunBackgroundTool
@@ -229,19 +230,21 @@ Or download from: https://nodejs.org/"""
229
230
  f" PID: {process.pid}",
230
231
  f" Command: {' '.join(cmd)}",
231
232
  ]
232
-
233
+
233
234
  if working_dir:
234
235
  output.append(f" Working Dir: {working_dir}")
235
-
236
+
236
237
  if log_file:
237
238
  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
- ])
239
+
240
+ output.extend(
241
+ [
242
+ "",
243
+ "Use 'processes' to list running processes.",
244
+ f"Use 'logs --process-id {process_id}' to view output.",
245
+ f"Use 'pkill --process-id {process_id}' to stop.",
246
+ ]
247
+ )
245
248
 
246
249
  return "\n".join(output)
247
250
 
@@ -1,19 +1,19 @@
1
1
  """NPX tool for both sync and background execution."""
2
2
 
3
- from pathlib import Path
4
3
  from typing import Optional, override
4
+ from pathlib import Path
5
5
 
6
+ from mcp.server import FastMCP
6
7
  from mcp.server.fastmcp import Context as MCPContext
7
8
 
8
9
  from hanzo_mcp.tools.shell.base_process import BaseBinaryTool
9
- from mcp.server import FastMCP
10
10
 
11
11
 
12
12
  class NpxTool(BaseBinaryTool):
13
13
  """Tool for running npx commands."""
14
-
14
+
15
15
  name = "npx"
16
-
16
+
17
17
  @property
18
18
  @override
19
19
  def description(self) -> str:
@@ -27,12 +27,12 @@ npx create-react-app my-app
27
27
  npx http-server -p 8080 # Auto-backgrounds after 2 minutes
28
28
  npx prettier --write "**/*.js"
29
29
  npx json-server db.json # Auto-backgrounds if needed"""
30
-
30
+
31
31
  @override
32
32
  def get_binary_name(self) -> str:
33
33
  """Get the binary name."""
34
34
  return "npx"
35
-
35
+
36
36
  @override
37
37
  async def run(
38
38
  self,
@@ -43,57 +43,53 @@ npx json-server db.json # Auto-backgrounds if needed"""
43
43
  yes: bool = True,
44
44
  ) -> str:
45
45
  """Run an npx command with auto-backgrounding.
46
-
46
+
47
47
  Args:
48
48
  ctx: MCP context
49
49
  package: NPX package to run
50
50
  args: Additional arguments
51
51
  cwd: Working directory
52
52
  yes: Auto-confirm package installation
53
-
53
+
54
54
  Returns:
55
55
  Command output or background status
56
56
  """
57
57
  # Prepare working directory
58
58
  work_dir = Path(cwd).resolve() if cwd else Path.cwd()
59
-
59
+
60
60
  # Prepare flags
61
61
  flags = []
62
62
  if yes:
63
63
  flags.append("-y")
64
-
64
+
65
65
  # Build full command
66
66
  full_args = args.split() if args else []
67
-
67
+
68
68
  # Always use execute_sync which now has auto-backgrounding
69
69
  return await self.execute_sync(
70
70
  package,
71
71
  cwd=work_dir,
72
72
  flags=flags,
73
73
  args=full_args,
74
- timeout=None # Let auto-backgrounding handle timeout
74
+ timeout=None, # Let auto-backgrounding handle timeout
75
75
  )
76
76
 
77
77
  def register(self, server: FastMCP) -> None:
78
78
  """Register the tool with the MCP server."""
79
79
  tool_self = self
80
-
80
+
81
81
  @server.tool(name=self.name, description=self.description)
82
82
  async def npx(
83
83
  ctx: MCPContext,
84
84
  package: str,
85
85
  args: str = "",
86
86
  cwd: Optional[str] = None,
87
- yes: bool = True
87
+ yes: bool = True,
88
88
  ) -> str:
89
89
  return await tool_self.run(
90
- ctx,
91
- package=package,
92
- args=args,
93
- cwd=cwd,
94
- yes=yes
90
+ ctx, package=package, args=args, cwd=cwd, yes=yes
95
91
  )
96
-
92
+
97
93
  async def call(self, ctx: MCPContext, **params) -> str:
98
94
  """Call the tool with arguments."""
99
95
  return await self.run(
@@ -101,9 +97,9 @@ npx json-server db.json # Auto-backgrounds if needed"""
101
97
  package=params["package"],
102
98
  args=params.get("args", ""),
103
99
  cwd=params.get("cwd"),
104
- yes=params.get("yes", True)
100
+ yes=params.get("yes", True),
105
101
  )
106
102
 
107
103
 
108
104
  # Create tool instance
109
- npx_tool = NpxTool()
105
+ npx_tool = NpxTool()
@@ -3,32 +3,29 @@
3
3
  import platform
4
4
  import subprocess
5
5
  import webbrowser
6
- from pathlib import Path
7
6
  from typing import override
7
+ from pathlib import Path
8
8
  from urllib.parse import urlparse
9
9
 
10
+ from mcp.server import FastMCP
10
11
  from mcp.server.fastmcp import Context as MCPContext
11
12
 
12
13
  from hanzo_mcp.tools.common.base import BaseTool
13
- from mcp.server import FastMCP
14
14
 
15
15
 
16
16
  class OpenTool(BaseTool):
17
17
  """Tool for opening files or URLs in the default application."""
18
18
 
19
19
  name = "open"
20
-
20
+
21
21
  def register(self, server: FastMCP) -> None:
22
22
  """Register the tool with the MCP server."""
23
23
  tool_self = self
24
-
24
+
25
25
  @server.tool(name=self.name, description=self.description)
26
- async def open(
27
- path: str,
28
- ctx: MCPContext
29
- ) -> str:
26
+ async def open(path: str, ctx: MCPContext) -> str:
30
27
  return await tool_self.run(ctx, path)
31
-
28
+
32
29
  async def call(self, ctx: MCPContext, **params) -> str:
33
30
  """Call the tool with arguments."""
34
31
  return await self.run(ctx, params["path"])
@@ -47,21 +44,21 @@ open /path/to/image.png"""
47
44
  @override
48
45
  async def run(self, ctx: MCPContext, path: str) -> str:
49
46
  """Open a file or URL in the default application.
50
-
47
+
51
48
  Args:
52
49
  ctx: MCP context
53
50
  path: File path or URL to open
54
-
51
+
55
52
  Returns:
56
53
  Success message
57
-
54
+
58
55
  Raises:
59
56
  RuntimeError: If opening fails
60
57
  """
61
58
  # Check if it's a URL
62
59
  parsed = urlparse(path)
63
- is_url = parsed.scheme in ('http', 'https', 'ftp', 'file')
64
-
60
+ is_url = parsed.scheme in ("http", "https", "ftp", "file")
61
+
65
62
  if is_url:
66
63
  # Open URL in default browser
67
64
  try:
@@ -69,15 +66,15 @@ open /path/to/image.png"""
69
66
  return f"Opened URL in browser: {path}"
70
67
  except Exception as e:
71
68
  raise RuntimeError(f"Failed to open URL: {e}")
72
-
69
+
73
70
  # It's a file path
74
71
  file_path = Path(path).expanduser().resolve()
75
-
72
+
76
73
  if not file_path.exists():
77
74
  raise RuntimeError(f"File not found: {file_path}")
78
-
75
+
79
76
  system = platform.system().lower()
80
-
77
+
81
78
  try:
82
79
  if system == "darwin": # macOS
83
80
  subprocess.run(["open", str(file_path)], check=True)
@@ -98,12 +95,13 @@ open /path/to/image.png"""
98
95
  elif system == "windows":
99
96
  # Use os.startfile on Windows
100
97
  import os
98
+
101
99
  os.startfile(str(file_path))
102
100
  else:
103
101
  raise RuntimeError(f"Unsupported platform: {system}")
104
-
102
+
105
103
  return f"Opened file: {file_path}"
106
-
104
+
107
105
  except subprocess.CalledProcessError as e:
108
106
  raise RuntimeError(f"Failed to open file: {e}")
109
107
  except Exception as e:
@@ -111,4 +109,4 @@ open /path/to/image.png"""
111
109
 
112
110
 
113
111
  # Create tool instance
114
- open_tool = OpenTool()
112
+ open_tool = OpenTool()