hanzo-mcp 0.9.0__py3-none-any.whl → 0.9.2__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 (135) hide show
  1. hanzo_mcp/__init__.py +1 -1
  2. hanzo_mcp/analytics/posthog_analytics.py +14 -1
  3. hanzo_mcp/cli.py +108 -4
  4. hanzo_mcp/server.py +11 -0
  5. hanzo_mcp/tools/__init__.py +3 -16
  6. hanzo_mcp/tools/agent/__init__.py +5 -0
  7. hanzo_mcp/tools/agent/agent.py +5 -0
  8. hanzo_mcp/tools/agent/agent_tool.py +3 -17
  9. hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +623 -0
  10. hanzo_mcp/tools/agent/clarification_tool.py +7 -1
  11. hanzo_mcp/tools/agent/claude_desktop_auth.py +16 -6
  12. hanzo_mcp/tools/agent/cli_agent_base.py +5 -0
  13. hanzo_mcp/tools/agent/cli_tools.py +26 -0
  14. hanzo_mcp/tools/agent/code_auth_tool.py +5 -0
  15. hanzo_mcp/tools/agent/critic_tool.py +7 -1
  16. hanzo_mcp/tools/agent/iching_tool.py +5 -0
  17. hanzo_mcp/tools/agent/network_tool.py +5 -0
  18. hanzo_mcp/tools/agent/review_tool.py +7 -1
  19. hanzo_mcp/tools/agent/swarm_alias.py +5 -0
  20. hanzo_mcp/tools/agent/swarm_tool.py +701 -0
  21. hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +554 -0
  22. hanzo_mcp/tools/agent/unified_cli_tools.py +5 -0
  23. hanzo_mcp/tools/common/auto_timeout.py +254 -0
  24. hanzo_mcp/tools/common/base.py +4 -0
  25. hanzo_mcp/tools/common/batch_tool.py +5 -0
  26. hanzo_mcp/tools/common/config_tool.py +5 -0
  27. hanzo_mcp/tools/common/critic_tool.py +5 -0
  28. hanzo_mcp/tools/common/paginated_base.py +4 -0
  29. hanzo_mcp/tools/common/permissions.py +38 -12
  30. hanzo_mcp/tools/common/personality.py +673 -980
  31. hanzo_mcp/tools/common/stats.py +5 -0
  32. hanzo_mcp/tools/common/thinking_tool.py +5 -0
  33. hanzo_mcp/tools/common/timeout_parser.py +103 -0
  34. hanzo_mcp/tools/common/tool_disable.py +5 -0
  35. hanzo_mcp/tools/common/tool_enable.py +5 -0
  36. hanzo_mcp/tools/common/tool_list.py +5 -0
  37. hanzo_mcp/tools/config/config_tool.py +5 -0
  38. hanzo_mcp/tools/config/mode_tool.py +5 -0
  39. hanzo_mcp/tools/database/graph.py +5 -0
  40. hanzo_mcp/tools/database/graph_add.py +5 -0
  41. hanzo_mcp/tools/database/graph_query.py +5 -0
  42. hanzo_mcp/tools/database/graph_remove.py +5 -0
  43. hanzo_mcp/tools/database/graph_search.py +5 -0
  44. hanzo_mcp/tools/database/graph_stats.py +5 -0
  45. hanzo_mcp/tools/database/sql.py +5 -0
  46. hanzo_mcp/tools/database/sql_query.py +2 -0
  47. hanzo_mcp/tools/database/sql_search.py +5 -0
  48. hanzo_mcp/tools/database/sql_stats.py +5 -0
  49. hanzo_mcp/tools/editor/neovim_command.py +5 -0
  50. hanzo_mcp/tools/editor/neovim_edit.py +7 -2
  51. hanzo_mcp/tools/editor/neovim_session.py +5 -0
  52. hanzo_mcp/tools/filesystem/__init__.py +23 -26
  53. hanzo_mcp/tools/filesystem/ast_tool.py +3 -4
  54. hanzo_mcp/tools/filesystem/base.py +2 -18
  55. hanzo_mcp/tools/filesystem/batch_search.py +825 -0
  56. hanzo_mcp/tools/filesystem/content_replace.py +5 -3
  57. hanzo_mcp/tools/filesystem/diff.py +5 -0
  58. hanzo_mcp/tools/filesystem/directory_tree.py +34 -281
  59. hanzo_mcp/tools/filesystem/directory_tree_paginated.py +345 -0
  60. hanzo_mcp/tools/filesystem/edit.py +6 -5
  61. hanzo_mcp/tools/filesystem/find.py +177 -311
  62. hanzo_mcp/tools/filesystem/find_files.py +370 -0
  63. hanzo_mcp/tools/filesystem/git_search.py +5 -3
  64. hanzo_mcp/tools/filesystem/grep.py +454 -0
  65. hanzo_mcp/tools/filesystem/multi_edit.py +6 -5
  66. hanzo_mcp/tools/filesystem/read.py +10 -9
  67. hanzo_mcp/tools/filesystem/rules_tool.py +6 -4
  68. hanzo_mcp/tools/filesystem/search_tool.py +728 -0
  69. hanzo_mcp/tools/filesystem/symbols_tool.py +510 -0
  70. hanzo_mcp/tools/filesystem/tree.py +273 -0
  71. hanzo_mcp/tools/filesystem/watch.py +6 -1
  72. hanzo_mcp/tools/filesystem/write.py +13 -7
  73. hanzo_mcp/tools/jupyter/jupyter.py +30 -2
  74. hanzo_mcp/tools/jupyter/notebook_edit.py +298 -0
  75. hanzo_mcp/tools/jupyter/notebook_read.py +148 -0
  76. hanzo_mcp/tools/llm/consensus_tool.py +8 -6
  77. hanzo_mcp/tools/llm/llm_manage.py +5 -0
  78. hanzo_mcp/tools/llm/llm_tool.py +2 -0
  79. hanzo_mcp/tools/llm/llm_unified.py +5 -0
  80. hanzo_mcp/tools/llm/provider_tools.py +5 -0
  81. hanzo_mcp/tools/lsp/lsp_tool.py +475 -622
  82. hanzo_mcp/tools/mcp/mcp_add.py +7 -2
  83. hanzo_mcp/tools/mcp/mcp_remove.py +15 -2
  84. hanzo_mcp/tools/mcp/mcp_stats.py +5 -0
  85. hanzo_mcp/tools/mcp/mcp_tool.py +5 -0
  86. hanzo_mcp/tools/memory/knowledge_tools.py +14 -0
  87. hanzo_mcp/tools/memory/memory_tools.py +17 -0
  88. hanzo_mcp/tools/search/find_tool.py +5 -3
  89. hanzo_mcp/tools/search/unified_search.py +3 -1
  90. hanzo_mcp/tools/shell/__init__.py +2 -14
  91. hanzo_mcp/tools/shell/base_process.py +4 -2
  92. hanzo_mcp/tools/shell/bash_tool.py +2 -0
  93. hanzo_mcp/tools/shell/command_executor.py +7 -7
  94. hanzo_mcp/tools/shell/logs.py +5 -0
  95. hanzo_mcp/tools/shell/npx.py +5 -0
  96. hanzo_mcp/tools/shell/npx_background.py +5 -0
  97. hanzo_mcp/tools/shell/npx_tool.py +5 -0
  98. hanzo_mcp/tools/shell/open.py +5 -0
  99. hanzo_mcp/tools/shell/pkill.py +5 -0
  100. hanzo_mcp/tools/shell/process_tool.py +5 -0
  101. hanzo_mcp/tools/shell/processes.py +5 -0
  102. hanzo_mcp/tools/shell/run_background.py +5 -0
  103. hanzo_mcp/tools/shell/run_command.py +2 -0
  104. hanzo_mcp/tools/shell/run_command_windows.py +5 -0
  105. hanzo_mcp/tools/shell/streaming_command.py +5 -0
  106. hanzo_mcp/tools/shell/uvx.py +5 -0
  107. hanzo_mcp/tools/shell/uvx_background.py +5 -0
  108. hanzo_mcp/tools/shell/uvx_tool.py +5 -0
  109. hanzo_mcp/tools/shell/zsh_tool.py +3 -0
  110. hanzo_mcp/tools/todo/todo.py +5 -0
  111. hanzo_mcp/tools/todo/todo_read.py +142 -0
  112. hanzo_mcp/tools/todo/todo_write.py +367 -0
  113. hanzo_mcp/tools/vector/__init__.py +42 -95
  114. hanzo_mcp/tools/vector/index_tool.py +5 -0
  115. hanzo_mcp/tools/vector/vector.py +5 -0
  116. hanzo_mcp/tools/vector/vector_index.py +5 -0
  117. hanzo_mcp/tools/vector/vector_search.py +5 -0
  118. {hanzo_mcp-0.9.0.dist-info → hanzo_mcp-0.9.2.dist-info}/METADATA +1 -1
  119. hanzo_mcp-0.9.2.dist-info/RECORD +195 -0
  120. hanzo_mcp/tools/common/path_utils.py +0 -34
  121. hanzo_mcp/tools/compiler/__init__.py +0 -8
  122. hanzo_mcp/tools/compiler/sandboxed_compiler.py +0 -681
  123. hanzo_mcp/tools/environment/__init__.py +0 -8
  124. hanzo_mcp/tools/environment/environment_detector.py +0 -594
  125. hanzo_mcp/tools/filesystem/search.py +0 -1160
  126. hanzo_mcp/tools/framework/__init__.py +0 -8
  127. hanzo_mcp/tools/framework/framework_modes.py +0 -714
  128. hanzo_mcp/tools/memory/conversation_memory.py +0 -636
  129. hanzo_mcp/tools/shell/run_tool.py +0 -56
  130. hanzo_mcp/tools/vector/node_tool.py +0 -538
  131. hanzo_mcp/tools/vector/unified_vector.py +0 -384
  132. hanzo_mcp-0.9.0.dist-info/RECORD +0 -191
  133. {hanzo_mcp-0.9.0.dist-info → hanzo_mcp-0.9.2.dist-info}/WHEEL +0 -0
  134. {hanzo_mcp-0.9.0.dist-info → hanzo_mcp-0.9.2.dist-info}/entry_points.txt +0 -0
  135. {hanzo_mcp-0.9.0.dist-info → hanzo_mcp-0.9.2.dist-info}/top_level.txt +0 -0
@@ -8,6 +8,8 @@ from pathlib import Path
8
8
  from pydantic import Field
9
9
  from mcp.server.fastmcp import Context as MCPContext
10
10
 
11
+ from hanzo_mcp.tools.common.auto_timeout import auto_timeout
12
+
11
13
  from hanzo_mcp.tools.common.base import BaseTool
12
14
  from hanzo_mcp.tools.common.context import create_tool_context
13
15
 
@@ -131,6 +133,9 @@ Use 'mcp_stats' to see all added servers and their status.
131
133
  """
132
134
 
133
135
  @override
136
+ @auto_timeout("mcp_add")
137
+
138
+
134
139
  async def call(
135
140
  self,
136
141
  ctx: MCPContext,
@@ -219,8 +224,8 @@ Use 'mcp_stats' to see all added servers and their status.
219
224
  if not shutil.which("uvx"):
220
225
  return "Error: uvx not found. Install uv first."
221
226
 
222
- # TODO: Actually start and connect to the MCP server
223
- # For now, we just store the configuration
227
+ # Server is validated and ready to be used
228
+ # The actual connection happens when tools are invoked
224
229
  server_config["status"] = "ready"
225
230
 
226
231
  except Exception as e:
@@ -5,6 +5,8 @@ from typing import Unpack, Annotated, TypedDict, final, override
5
5
  from pydantic import Field
6
6
  from mcp.server.fastmcp import Context as MCPContext
7
7
 
8
+ from hanzo_mcp.tools.common.auto_timeout import auto_timeout
9
+
8
10
  from hanzo_mcp.tools.common.base import BaseTool
9
11
  from hanzo_mcp.tools.mcp.mcp_add import McpAddTool
10
12
  from hanzo_mcp.tools.common.context import create_tool_context
@@ -64,6 +66,9 @@ Use 'mcp_stats' to see all servers before removing.
64
66
  """
65
67
 
66
68
  @override
69
+ @auto_timeout("mcp_remove")
70
+
71
+
67
72
  async def call(
68
73
  self,
69
74
  ctx: MCPContext,
@@ -103,8 +108,16 @@ Use 'mcp_stats' to see all servers before removing.
103
108
  if not force:
104
109
  return f"Error: Server '{name}' is currently running. Use --force to remove anyway."
105
110
  else:
106
- # TODO: Stop the server process
107
- await tool_ctx.info(f"Stopping running server '{name}'")
111
+ # Stop the server process if it's running
112
+ process_id = server.get("process_id")
113
+ if process_id:
114
+ try:
115
+ import signal
116
+ import os
117
+ os.kill(process_id, signal.SIGTERM)
118
+ await tool_ctx.info(f"Stopped running server '{name}' (PID: {process_id})")
119
+ except ProcessLookupError:
120
+ await tool_ctx.info(f"Server '{name}' process not found (already stopped)")
108
121
 
109
122
  # Remove from registry
110
123
  del McpAddTool._mcp_servers[name]
@@ -4,6 +4,8 @@ from typing import Unpack, TypedDict, final, override
4
4
 
5
5
  from mcp.server.fastmcp import Context as MCPContext
6
6
 
7
+ from hanzo_mcp.tools.common.auto_timeout import auto_timeout
8
+
7
9
  from hanzo_mcp.tools.common.base import BaseTool
8
10
  from hanzo_mcp.tools.mcp.mcp_add import McpAddTool
9
11
  from hanzo_mcp.tools.common.context import create_tool_context
@@ -47,6 +49,9 @@ Example:
47
49
  """
48
50
 
49
51
  @override
52
+ @auto_timeout("mcp_stats")
53
+
54
+
50
55
  async def call(
51
56
  self,
52
57
  ctx: MCPContext,
@@ -20,6 +20,8 @@ from pathlib import Path
20
20
  from pydantic import Field
21
21
  from mcp.server.fastmcp import Context as MCPContext
22
22
 
23
+ from hanzo_mcp.tools.common.auto_timeout import auto_timeout
24
+
23
25
  from hanzo_mcp.tools.common.base import BaseTool
24
26
  from hanzo_mcp.tools.common.context import create_tool_context
25
27
 
@@ -270,6 +272,9 @@ mcp --action enable --name github
270
272
  Status: {enabled} enabled, {running} running"""
271
273
 
272
274
  @override
275
+ @auto_timeout("mcp")
276
+
277
+
273
278
  async def call(
274
279
  self,
275
280
  ctx: MCPContext,
@@ -7,6 +7,8 @@ supporting hierarchical organization (session, project, global).
7
7
  from typing import Any, Dict, List, Optional, final, override
8
8
 
9
9
  from mcp.server import FastMCP
10
+
11
+ from hanzo_mcp.tools.common.auto_timeout import auto_timeout
10
12
  from mcp.server.fastmcp import Context as MCPContext
11
13
 
12
14
  from hanzo_mcp.tools.common.base import BaseTool
@@ -69,6 +71,9 @@ recall_facts(queries=["company policies"], scope="global", limit=5)
69
71
  """
70
72
 
71
73
  @override
74
+ @auto_timeout("knowledge_tools")
75
+
76
+
72
77
  async def call(
73
78
  self,
74
79
  ctx: MCPContext,
@@ -190,6 +195,9 @@ store_facts(facts=["Company founded in 2020"], scope="global", kb_name="company_
190
195
  """
191
196
 
192
197
  @override
198
+ @auto_timeout("knowledge_tools")
199
+
200
+
193
201
  async def call(
194
202
  self,
195
203
  ctx: MCPContext,
@@ -286,6 +294,9 @@ summarize_to_memory(content="Company guidelines...", topic="Guidelines", scope="
286
294
  """
287
295
 
288
296
  @override
297
+ @auto_timeout("knowledge_tools")
298
+
299
+
289
300
  async def call(
290
301
  self,
291
302
  ctx: MCPContext,
@@ -390,6 +401,9 @@ manage_knowledge_bases(action="delete", kb_name="old_docs")
390
401
  """
391
402
 
392
403
  @override
404
+ @auto_timeout("knowledge_tools")
405
+
406
+
393
407
  async def call(
394
408
  self,
395
409
  ctx: MCPContext,
@@ -7,6 +7,8 @@ The hanzo-memory package provides embedded database and vector search capabiliti
7
7
  from typing import Dict, List, Optional, final, override
8
8
 
9
9
  from mcp.server import FastMCP
10
+
11
+ from hanzo_mcp.tools.common.auto_timeout import auto_timeout
10
12
  from mcp.server.fastmcp import Context as MCPContext
11
13
 
12
14
  from hanzo_mcp.tools.common.base import BaseTool
@@ -69,6 +71,9 @@ recall_memories(queries=["coding standards"], scope="global")
69
71
  """
70
72
 
71
73
  @override
74
+ @auto_timeout("memory_tools")
75
+
76
+
72
77
  async def call(
73
78
  self,
74
79
  ctx: MCPContext,
@@ -155,6 +160,9 @@ create_memories(statements=["User prefers dark mode", "User works in Python"])
155
160
  """
156
161
 
157
162
  @override
163
+ @auto_timeout("memory_tools")
164
+
165
+
158
166
  async def call(self, ctx: MCPContext, statements: List[str]) -> str:
159
167
  """Create new memories.
160
168
 
@@ -219,6 +227,9 @@ update_memories(updates=[
219
227
  """
220
228
 
221
229
  @override
230
+ @auto_timeout("memory_tools")
231
+
232
+
222
233
  async def call(self, ctx: MCPContext, updates: List[Dict[str, str]]) -> str:
223
234
  """Update memories.
224
235
 
@@ -283,6 +294,9 @@ delete_memories(ids=["mem_1", "mem_2"])
283
294
  """
284
295
 
285
296
  @override
297
+ @auto_timeout("memory_tools")
298
+
299
+
286
300
  async def call(self, ctx: MCPContext, ids: List[str]) -> str:
287
301
  """Delete memories.
288
302
 
@@ -345,6 +359,9 @@ manage_memories(
345
359
  """
346
360
 
347
361
  @override
362
+ @auto_timeout("memory_tools")
363
+
364
+
348
365
  async def call(
349
366
  self,
350
367
  ctx: MCPContext,
@@ -11,6 +11,7 @@ from dataclasses import dataclass
11
11
 
12
12
  from hanzo_mcp.types import MCPResourceDocument
13
13
  from hanzo_mcp.tools.common.base import BaseTool
14
+ from hanzo_mcp.tools.common.auto_timeout import auto_timeout
14
15
 
15
16
  # Check if ffind command is available
16
17
  try:
@@ -75,8 +76,8 @@ class FindTool(BaseTool):
75
76
  reading or searching within files.
76
77
  """
77
78
 
78
- def __init__(self, permission_manager=None):
79
- super().__init__(permission_manager=permission_manager)
79
+ def __init__(self):
80
+ super().__init__()
80
81
  self._cache = {}
81
82
  self._gitignore_cache = {}
82
83
 
@@ -375,7 +376,8 @@ class FindTool(BaseTool):
375
376
  }
376
377
  )
377
378
 
378
- async def call(self, **kwargs) -> str:
379
+ @auto_timeout("find")
380
+ async def call(self, ctx=None, **kwargs) -> str:
379
381
  """Tool interface for MCP - converts result to JSON string."""
380
382
  result = await self.run(**kwargs)
381
383
  return result.to_json_string()
@@ -14,6 +14,7 @@ from dataclasses import dataclass
14
14
 
15
15
  from hanzo_mcp.types import MCPResourceDocument
16
16
  from hanzo_mcp.tools.common.base import BaseTool
17
+ from hanzo_mcp.tools.common.auto_timeout import auto_timeout
17
18
 
18
19
  # Import memory tools if available
19
20
  try:
@@ -349,7 +350,8 @@ class UnifiedSearch(BaseTool):
349
350
 
350
351
  return MCPResourceDocument(data=response_data)
351
352
 
352
- async def call(self, **kwargs) -> str:
353
+ @auto_timeout("search")
354
+ async def call(self, ctx=None, **kwargs) -> str:
353
355
  """Tool interface for MCP - converts result to JSON string."""
354
356
  result = await self.run(**kwargs)
355
357
  return result.to_json_string()
@@ -13,7 +13,6 @@ from hanzo_mcp.tools.shell.zsh_tool import zsh_tool, shell_tool
13
13
 
14
14
  # Import tools
15
15
  from hanzo_mcp.tools.shell.bash_tool import bash_tool
16
- from hanzo_mcp.tools.shell.run_tool import run_tool
17
16
  from hanzo_mcp.tools.common.permissions import PermissionManager
18
17
  from hanzo_mcp.tools.shell.process_tool import process_tool
19
18
 
@@ -41,16 +40,14 @@ def get_shell_tools(
41
40
  bash_tool.permission_manager = permission_manager
42
41
  zsh_tool.permission_manager = permission_manager
43
42
  shell_tool.permission_manager = permission_manager
44
- run_tool.permission_manager = permission_manager
45
43
  npx_tool.permission_manager = permission_manager
46
44
  uvx_tool.permission_manager = permission_manager
47
45
 
48
46
  # Note: StreamingCommandTool is abstract and shouldn't be instantiated directly
49
47
  # It's used as a base class for other streaming tools
50
48
 
51
- # Return run_tool first (simplified command execution), then shell_tool (smart default), then specific shells
49
+ # Return shell_tool first (smart default), then specific shells
52
50
  return [
53
- run_tool, # Simplified run command with auto-backgrounding
54
51
  shell_tool, # Smart shell (prefers zsh if available)
55
52
  zsh_tool, # Explicit zsh
56
53
  bash_tool, # Explicit bash
@@ -65,25 +62,16 @@ def get_shell_tools(
65
62
  def register_shell_tools(
66
63
  mcp_server: FastMCP,
67
64
  permission_manager: PermissionManager,
68
- enabled_tools: dict[str, bool] | None = None,
69
65
  ) -> list[BaseTool]:
70
66
  """Register all shell tools with the MCP server.
71
67
 
72
68
  Args:
73
69
  mcp_server: The FastMCP server instance
74
70
  permission_manager: Permission manager for access control
75
- enabled_tools: Optional dict of tool names to enable/disable
76
71
 
77
72
  Returns:
78
73
  List of registered tools
79
74
  """
80
- all_tools = get_shell_tools(permission_manager)
81
-
82
- # Filter tools based on enabled_tools if provided
83
- if enabled_tools is not None:
84
- tools = [tool for tool in all_tools if enabled_tools.get(tool.name, True)]
85
- else:
86
- tools = all_tools
87
-
75
+ tools = get_shell_tools(permission_manager)
88
76
  ToolRegistry.register_tools(mcp_server, tools)
89
77
  return tools
@@ -183,11 +183,13 @@ class BaseProcessTool(BaseTool):
183
183
  else:
184
184
  if output.startswith("Command failed"):
185
185
  raise RuntimeError(output)
186
+ # Get configurable max tokens from environment, default to 25000
187
+ max_tokens = int(os.environ.get("HANZO_MCP_MAX_RESPONSE_TOKENS", "25000"))
186
188
  # Truncate output to prevent token limit issues
187
189
  return truncate_response(
188
190
  output,
189
- max_tokens=25000,
190
- truncation_message="\n\n[Command output truncated due to token limit. Output may be available in logs or files.]",
191
+ max_tokens=max_tokens,
192
+ truncation_message=f"\n\n[Command output truncated due to {max_tokens} token limit. Output may be available in logs or files. Set HANZO_MCP_MAX_RESPONSE_TOKENS env var to adjust limit.]",
191
193
  )
192
194
 
193
195
  async def execute_background(
@@ -8,6 +8,7 @@ from pathlib import Path
8
8
  from mcp.server import FastMCP
9
9
  from mcp.server.fastmcp import Context as MCPContext
10
10
 
11
+ from hanzo_mcp.tools.common.auto_timeout import auto_timeout
11
12
  from hanzo_mcp.tools.shell.base_process import BaseScriptTool
12
13
 
13
14
 
@@ -30,6 +31,7 @@ class BashTool(BaseScriptTool):
30
31
  ) -> str:
31
32
  return await tool_self.run(ctx, command=command, cwd=cwd, env=env, timeout=timeout)
32
33
 
34
+ @auto_timeout("bash")
33
35
  async def call(self, ctx: MCPContext, **params) -> str:
34
36
  """Call the tool with arguments."""
35
37
  return await self.run(
@@ -109,13 +109,12 @@ class CommandExecutor:
109
109
  if shell_basename in ["wsl", "wsl.exe"]:
110
110
  # For WSL, handle commands with shell operators differently
111
111
  if any(char in command for char in ";&|<>(){}[]$\"'`"):
112
- # For commands with special characters, use a more reliable approach
113
- # with bash -c and double quotes around the entire command
114
- escaped_command = command.replace('"', '\\"')
112
+ # Use shlex.quote for proper escaping to prevent command injection
113
+ escaped_command = shlex.quote(command)
115
114
  if use_login_shell:
116
- formatted_command = f'{user_shell} bash -l -c "{escaped_command}"'
115
+ formatted_command = f'{user_shell} bash -l -c {escaped_command}'
117
116
  else:
118
- formatted_command = f'{user_shell} bash -c "{escaped_command}"'
117
+ formatted_command = f'{user_shell} bash -c {escaped_command}'
119
118
  else:
120
119
  # # For simple commands without special characters
121
120
  # # Still respect login shell preference
@@ -125,8 +124,9 @@ class CommandExecutor:
125
124
  formatted_command = f"{user_shell} {command}"
126
125
 
127
126
  elif shell_basename in ["powershell", "powershell.exe", "pwsh", "pwsh.exe"]:
128
- # For PowerShell, escape double quotes with backslash most robust
129
- escaped_command = command.replace('"', '\\"')
127
+ # Use proper escaping for PowerShell to prevent injection
128
+ # PowerShell requires different escaping than POSIX shells
129
+ escaped_command = command.replace('"', '`"').replace("'", "``'").replace('$', '`$')
130
130
  formatted_command = f'"{user_shell}" -Command "{escaped_command}"'
131
131
 
132
132
  else:
@@ -6,6 +6,8 @@ from pathlib import Path
6
6
  from pydantic import Field
7
7
  from mcp.server.fastmcp import Context as MCPContext
8
8
 
9
+ from hanzo_mcp.tools.common.auto_timeout import auto_timeout
10
+
9
11
  from hanzo_mcp.tools.common.base import BaseTool
10
12
  from hanzo_mcp.tools.common.context import create_tool_context
11
13
  from hanzo_mcp.tools.common.permissions import PermissionManager
@@ -105,6 +107,9 @@ Use run_command with 'tail -f' for continuous monitoring.
105
107
  """
106
108
 
107
109
  @override
110
+ @auto_timeout("logs")
111
+
112
+
108
113
  async def call(
109
114
  self,
110
115
  ctx: MCPContext,
@@ -7,6 +7,8 @@ from typing import Unpack, Optional, Annotated, TypedDict, final, override
7
7
  from pydantic import Field
8
8
  from mcp.server.fastmcp import Context as MCPContext
9
9
 
10
+ from hanzo_mcp.tools.common.auto_timeout import auto_timeout
11
+
10
12
  from hanzo_mcp.tools.common.base import BaseTool
11
13
  from hanzo_mcp.tools.common.context import create_tool_context
12
14
  from hanzo_mcp.tools.common.permissions import PermissionManager
@@ -103,6 +105,9 @@ For long-running servers, use npx_background instead.
103
105
  """
104
106
 
105
107
  @override
108
+ @auto_timeout("npx")
109
+
110
+
106
111
  async def call(
107
112
  self,
108
113
  ctx: MCPContext,
@@ -8,6 +8,8 @@ from typing import Unpack, Optional, Annotated, TypedDict, final, override
8
8
  from pydantic import Field
9
9
  from mcp.server.fastmcp import Context as MCPContext
10
10
 
11
+ from hanzo_mcp.tools.common.auto_timeout import auto_timeout
12
+
11
13
  from hanzo_mcp.tools.common.base import BaseTool
12
14
  from hanzo_mcp.tools.common.context import create_tool_context
13
15
  from hanzo_mcp.tools.common.permissions import PermissionManager
@@ -120,6 +122,9 @@ Use 'processes' to list running processes and 'pkill' to stop them.
120
122
  """
121
123
 
122
124
  @override
125
+ @auto_timeout("npx")
126
+
127
+
123
128
  async def call(
124
129
  self,
125
130
  ctx: MCPContext,
@@ -4,6 +4,8 @@ from typing import Optional, override
4
4
  from pathlib import Path
5
5
 
6
6
  from mcp.server import FastMCP
7
+
8
+ from hanzo_mcp.tools.common.auto_timeout import auto_timeout
7
9
  from mcp.server.fastmcp import Context as MCPContext
8
10
 
9
11
  from hanzo_mcp.tools.shell.base_process import BaseBinaryTool
@@ -88,6 +90,9 @@ npx json-server db.json # Auto-backgrounds if needed"""
88
90
  ) -> str:
89
91
  return await tool_self.run(ctx, package=package, args=args, cwd=cwd, yes=yes)
90
92
 
93
+ @auto_timeout("npx")
94
+
95
+
91
96
  async def call(self, ctx: MCPContext, **params) -> str:
92
97
  """Call the tool with arguments."""
93
98
  return await self.run(
@@ -8,6 +8,8 @@ from pathlib import Path
8
8
  from urllib.parse import urlparse
9
9
 
10
10
  from mcp.server import FastMCP
11
+
12
+ from hanzo_mcp.tools.common.auto_timeout import auto_timeout
11
13
  from mcp.server.fastmcp import Context as MCPContext
12
14
 
13
15
  from hanzo_mcp.tools.common.base import BaseTool
@@ -26,6 +28,9 @@ class OpenTool(BaseTool):
26
28
  async def open(path: str, ctx: MCPContext) -> str:
27
29
  return await tool_self.run(ctx, path)
28
30
 
31
+ @auto_timeout("open")
32
+
33
+
29
34
  async def call(self, ctx: MCPContext, **params) -> str:
30
35
  """Call the tool with arguments."""
31
36
  return await self.run(ctx, params["path"])
@@ -7,6 +7,8 @@ import psutil
7
7
  from pydantic import Field
8
8
  from mcp.server.fastmcp import Context as MCPContext
9
9
 
10
+ from hanzo_mcp.tools.common.auto_timeout import auto_timeout
11
+
10
12
  from hanzo_mcp.tools.common.base import BaseTool
11
13
  from hanzo_mcp.tools.common.context import create_tool_context
12
14
  from hanzo_mcp.tools.common.permissions import PermissionManager
@@ -106,6 +108,9 @@ Examples:
106
108
  """
107
109
 
108
110
  @override
111
+ @auto_timeout("pkill")
112
+
113
+
109
114
  async def call(
110
115
  self,
111
116
  ctx: MCPContext,
@@ -4,6 +4,8 @@ import signal
4
4
  from typing import Optional, override
5
5
 
6
6
  from mcp.server import FastMCP
7
+
8
+ from hanzo_mcp.tools.common.auto_timeout import auto_timeout
7
9
  from mcp.server.fastmcp import Context as MCPContext
8
10
 
9
11
  from hanzo_mcp.tools.common.base import BaseTool
@@ -132,6 +134,9 @@ process --action logs --id bash_ghi789 --lines 50"""
132
134
  ) -> str:
133
135
  return await tool_self.run(ctx, action=action, id=id, signal_type=signal_type, lines=lines)
134
136
 
137
+ @auto_timeout("process")
138
+
139
+
135
140
  async def call(self, ctx: MCPContext, **params) -> str:
136
141
  """Call the tool with arguments."""
137
142
  return await self.run(
@@ -7,6 +7,8 @@ import psutil
7
7
  from pydantic import Field
8
8
  from mcp.server.fastmcp import Context as MCPContext
9
9
 
10
+ from hanzo_mcp.tools.common.auto_timeout import auto_timeout
11
+
10
12
  from hanzo_mcp.tools.common.base import BaseTool
11
13
  from hanzo_mcp.tools.common.context import create_tool_context
12
14
  from hanzo_mcp.tools.common.permissions import PermissionManager
@@ -90,6 +92,9 @@ Examples:
90
92
  """
91
93
 
92
94
  @override
95
+ @auto_timeout("processes")
96
+
97
+
93
98
  async def call(
94
99
  self,
95
100
  ctx: MCPContext,
@@ -10,6 +10,8 @@ from datetime import datetime
10
10
  from pydantic import Field
11
11
  from mcp.server.fastmcp import Context as MCPContext
12
12
 
13
+ from hanzo_mcp.tools.common.auto_timeout import auto_timeout
14
+
13
15
  from hanzo_mcp.tools.common.base import BaseTool
14
16
  from hanzo_mcp.tools.common.context import create_tool_context
15
17
  from hanzo_mcp.tools.common.permissions import PermissionManager
@@ -181,6 +183,9 @@ Examples:
181
183
  """
182
184
 
183
185
  @override
186
+ @auto_timeout("run")
187
+
188
+
184
189
  async def call(
185
190
  self,
186
191
  ctx: MCPContext,
@@ -9,6 +9,7 @@ from pydantic import Field
9
9
  from mcp.server import FastMCP
10
10
  from mcp.server.fastmcp import Context as MCPContext
11
11
 
12
+ from hanzo_mcp.tools.common.auto_timeout import auto_timeout
12
13
  from hanzo_mcp.tools.shell.base import ShellBaseTool
13
14
  from hanzo_mcp.tools.common.base import handle_connection_errors
14
15
  from hanzo_mcp.tools.common.context import create_tool_context
@@ -269,6 +270,7 @@ Important:
269
270
  return tool_ctx
270
271
 
271
272
  @override
273
+ @auto_timeout("run_command")
272
274
  async def call(
273
275
  self,
274
276
  ctx: MCPContext,
@@ -8,6 +8,8 @@ from typing import Any, Annotated, final, override
8
8
 
9
9
  from pydantic import Field
10
10
  from mcp.server import FastMCP
11
+
12
+ from hanzo_mcp.tools.common.auto_timeout import auto_timeout
11
13
  from mcp.server.fastmcp import Context as MCPContext
12
14
 
13
15
  from hanzo_mcp.tools.shell.base import ShellBaseTool
@@ -194,6 +196,9 @@ Important:
194
196
  return tool_ctx
195
197
 
196
198
  @override
199
+ @auto_timeout("run_command_windows")
200
+
201
+
197
202
  async def call(self, ctx: MCPContext, **params: Any) -> str:
198
203
  """Execute the tool with the given parameters.
199
204
 
@@ -1,3 +1,5 @@
1
+
2
+ from hanzo_mcp.tools.common.auto_timeout import auto_timeout
1
3
  """Streaming command execution with disk-based logging and session management."""
2
4
 
3
5
  import os
@@ -173,6 +175,9 @@ class StreamingCommandTool(BaseProcessTool):
173
175
 
174
176
  return None
175
177
 
178
+ @auto_timeout("streaming_command")
179
+
180
+
176
181
  async def call(self, ctx: Any, **kwargs) -> Dict[str, Any]:
177
182
  """MCP tool entry point.
178
183
 
@@ -7,6 +7,8 @@ from typing import Unpack, Optional, Annotated, TypedDict, final, override
7
7
  from pydantic import Field
8
8
  from mcp.server.fastmcp import Context as MCPContext
9
9
 
10
+ from hanzo_mcp.tools.common.auto_timeout import auto_timeout
11
+
10
12
  from hanzo_mcp.tools.common.base import BaseTool
11
13
  from hanzo_mcp.tools.common.context import create_tool_context
12
14
  from hanzo_mcp.tools.common.permissions import PermissionManager
@@ -100,6 +102,9 @@ For long-running servers, use uvx_background instead.
100
102
  """
101
103
 
102
104
  @override
105
+ @auto_timeout("uvx")
106
+
107
+
103
108
  async def call(
104
109
  self,
105
110
  ctx: MCPContext,
@@ -8,6 +8,8 @@ from typing import Unpack, Optional, Annotated, TypedDict, final, override
8
8
  from pydantic import Field
9
9
  from mcp.server.fastmcp import Context as MCPContext
10
10
 
11
+ from hanzo_mcp.tools.common.auto_timeout import auto_timeout
12
+
11
13
  from hanzo_mcp.tools.common.base import BaseTool
12
14
  from hanzo_mcp.tools.common.context import create_tool_context
13
15
  from hanzo_mcp.tools.common.permissions import PermissionManager
@@ -119,6 +121,9 @@ Use 'processes' to list running processes and 'pkill' to stop them.
119
121
  """
120
122
 
121
123
  @override
124
+ @auto_timeout("uvx")
125
+
126
+
122
127
  async def call(
123
128
  self,
124
129
  ctx: MCPContext,
@@ -4,6 +4,8 @@ from typing import Optional, override
4
4
  from pathlib import Path
5
5
 
6
6
  from mcp.server import FastMCP
7
+
8
+ from hanzo_mcp.tools.common.auto_timeout import auto_timeout
7
9
  from mcp.server.fastmcp import Context as MCPContext
8
10
 
9
11
  from hanzo_mcp.tools.shell.base_process import BaseBinaryTool
@@ -88,6 +90,9 @@ uvx jupyter lab --port 8888 # Auto-backgrounds if needed"""
88
90
  ) -> str:
89
91
  return await tool_self.run(ctx, package=package, args=args, cwd=cwd, python=python)
90
92
 
93
+ @auto_timeout("uvx")
94
+
95
+
91
96
  async def call(self, ctx: MCPContext, **params) -> str:
92
97
  """Call the tool with arguments."""
93
98
  return await self.run(