hanzo-mcp 0.8.15__py3-none-any.whl → 0.9.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 (113) hide show
  1. hanzo_mcp/__init__.py +1 -1
  2. hanzo_mcp/cli.py +108 -4
  3. hanzo_mcp/tools/agent/__init__.py +5 -0
  4. hanzo_mcp/tools/agent/agent.py +5 -0
  5. hanzo_mcp/tools/agent/agent_tool.py +3 -0
  6. hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +5 -0
  7. hanzo_mcp/tools/agent/clarification_tool.py +2 -0
  8. hanzo_mcp/tools/agent/claude_desktop_auth.py +5 -0
  9. hanzo_mcp/tools/agent/cli_agent_base.py +5 -0
  10. hanzo_mcp/tools/agent/cli_tools.py +26 -0
  11. hanzo_mcp/tools/agent/code_auth_tool.py +5 -0
  12. hanzo_mcp/tools/agent/critic_tool.py +2 -0
  13. hanzo_mcp/tools/agent/iching_tool.py +5 -0
  14. hanzo_mcp/tools/agent/network_tool.py +5 -0
  15. hanzo_mcp/tools/agent/review_tool.py +2 -0
  16. hanzo_mcp/tools/agent/swarm_alias.py +5 -0
  17. hanzo_mcp/tools/agent/swarm_tool.py +3 -0
  18. hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +5 -0
  19. hanzo_mcp/tools/agent/unified_cli_tools.py +5 -0
  20. hanzo_mcp/tools/common/auto_timeout.py +234 -0
  21. hanzo_mcp/tools/common/base.py +4 -0
  22. hanzo_mcp/tools/common/batch_tool.py +5 -0
  23. hanzo_mcp/tools/common/config_tool.py +5 -0
  24. hanzo_mcp/tools/common/critic_tool.py +5 -0
  25. hanzo_mcp/tools/common/paginated_base.py +4 -0
  26. hanzo_mcp/tools/common/stats.py +5 -0
  27. hanzo_mcp/tools/common/thinking_tool.py +5 -0
  28. hanzo_mcp/tools/common/timeout_parser.py +103 -0
  29. hanzo_mcp/tools/common/tool_disable.py +5 -0
  30. hanzo_mcp/tools/common/tool_enable.py +5 -0
  31. hanzo_mcp/tools/common/tool_list.py +5 -0
  32. hanzo_mcp/tools/config/config_tool.py +5 -0
  33. hanzo_mcp/tools/config/mode_tool.py +5 -0
  34. hanzo_mcp/tools/database/graph.py +5 -0
  35. hanzo_mcp/tools/database/graph_add.py +5 -0
  36. hanzo_mcp/tools/database/graph_query.py +5 -0
  37. hanzo_mcp/tools/database/graph_remove.py +5 -0
  38. hanzo_mcp/tools/database/graph_search.py +5 -0
  39. hanzo_mcp/tools/database/graph_stats.py +5 -0
  40. hanzo_mcp/tools/database/sql.py +5 -0
  41. hanzo_mcp/tools/database/sql_query.py +2 -0
  42. hanzo_mcp/tools/database/sql_search.py +5 -0
  43. hanzo_mcp/tools/database/sql_stats.py +5 -0
  44. hanzo_mcp/tools/editor/neovim_command.py +5 -0
  45. hanzo_mcp/tools/editor/neovim_edit.py +5 -0
  46. hanzo_mcp/tools/editor/neovim_session.py +5 -0
  47. hanzo_mcp/tools/filesystem/ast_tool.py +2 -0
  48. hanzo_mcp/tools/filesystem/batch_search.py +5 -0
  49. hanzo_mcp/tools/filesystem/content_replace.py +5 -0
  50. hanzo_mcp/tools/filesystem/diff.py +5 -0
  51. hanzo_mcp/tools/filesystem/directory_tree.py +5 -0
  52. hanzo_mcp/tools/filesystem/directory_tree_paginated.py +5 -0
  53. hanzo_mcp/tools/filesystem/edit.py +5 -0
  54. hanzo_mcp/tools/filesystem/find.py +5 -0
  55. hanzo_mcp/tools/filesystem/find_files.py +5 -0
  56. hanzo_mcp/tools/filesystem/git_search.py +5 -0
  57. hanzo_mcp/tools/filesystem/grep.py +5 -0
  58. hanzo_mcp/tools/filesystem/multi_edit.py +5 -0
  59. hanzo_mcp/tools/filesystem/read.py +5 -0
  60. hanzo_mcp/tools/filesystem/rules_tool.py +5 -0
  61. hanzo_mcp/tools/filesystem/search_tool.py +5 -0
  62. hanzo_mcp/tools/filesystem/symbols_tool.py +5 -0
  63. hanzo_mcp/tools/filesystem/tree.py +5 -0
  64. hanzo_mcp/tools/filesystem/watch.py +5 -0
  65. hanzo_mcp/tools/filesystem/write.py +5 -0
  66. hanzo_mcp/tools/jupyter/jupyter.py +5 -0
  67. hanzo_mcp/tools/jupyter/notebook_edit.py +5 -0
  68. hanzo_mcp/tools/jupyter/notebook_read.py +5 -0
  69. hanzo_mcp/tools/llm/consensus_tool.py +2 -0
  70. hanzo_mcp/tools/llm/llm_manage.py +5 -0
  71. hanzo_mcp/tools/llm/llm_tool.py +2 -0
  72. hanzo_mcp/tools/llm/llm_unified.py +5 -0
  73. hanzo_mcp/tools/llm/provider_tools.py +5 -0
  74. hanzo_mcp/tools/lsp/lsp_tool.py +5 -0
  75. hanzo_mcp/tools/mcp/mcp_add.py +5 -0
  76. hanzo_mcp/tools/mcp/mcp_remove.py +5 -0
  77. hanzo_mcp/tools/mcp/mcp_stats.py +5 -0
  78. hanzo_mcp/tools/mcp/mcp_tool.py +5 -0
  79. hanzo_mcp/tools/memory/knowledge_tools.py +14 -0
  80. hanzo_mcp/tools/memory/memory_tools.py +17 -0
  81. hanzo_mcp/tools/search/find_tool.py +3 -1
  82. hanzo_mcp/tools/search/unified_search.py +3 -1
  83. hanzo_mcp/tools/shell/base_process.py +4 -2
  84. hanzo_mcp/tools/shell/bash_tool.py +2 -0
  85. hanzo_mcp/tools/shell/logs.py +5 -0
  86. hanzo_mcp/tools/shell/npx.py +5 -0
  87. hanzo_mcp/tools/shell/npx_background.py +5 -0
  88. hanzo_mcp/tools/shell/npx_tool.py +5 -0
  89. hanzo_mcp/tools/shell/open.py +5 -0
  90. hanzo_mcp/tools/shell/pkill.py +5 -0
  91. hanzo_mcp/tools/shell/process_tool.py +5 -0
  92. hanzo_mcp/tools/shell/processes.py +5 -0
  93. hanzo_mcp/tools/shell/run_background.py +5 -0
  94. hanzo_mcp/tools/shell/run_command.py +2 -0
  95. hanzo_mcp/tools/shell/run_command_windows.py +5 -0
  96. hanzo_mcp/tools/shell/streaming_command.py +5 -0
  97. hanzo_mcp/tools/shell/uvx.py +5 -0
  98. hanzo_mcp/tools/shell/uvx_background.py +5 -0
  99. hanzo_mcp/tools/shell/uvx_tool.py +5 -0
  100. hanzo_mcp/tools/shell/zsh_tool.py +3 -0
  101. hanzo_mcp/tools/todo/todo.py +5 -0
  102. hanzo_mcp/tools/todo/todo_read.py +5 -0
  103. hanzo_mcp/tools/todo/todo_write.py +5 -0
  104. hanzo_mcp/tools/vector/index_tool.py +5 -0
  105. hanzo_mcp/tools/vector/vector.py +5 -0
  106. hanzo_mcp/tools/vector/vector_index.py +5 -0
  107. hanzo_mcp/tools/vector/vector_search.py +5 -0
  108. {hanzo_mcp-0.8.15.dist-info → hanzo_mcp-0.9.1.dist-info}/METADATA +1 -1
  109. hanzo_mcp-0.9.1.dist-info/RECORD +195 -0
  110. hanzo_mcp-0.8.15.dist-info/RECORD +0 -193
  111. {hanzo_mcp-0.8.15.dist-info → hanzo_mcp-0.9.1.dist-info}/WHEEL +0 -0
  112. {hanzo_mcp-0.8.15.dist-info → hanzo_mcp-0.9.1.dist-info}/entry_points.txt +0 -0
  113. {hanzo_mcp-0.8.15.dist-info → hanzo_mcp-0.9.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,234 @@
1
+ """Universal auto-timeout and backgrounding for all MCP tools.
2
+
3
+ This module provides automatic timeout and backgrounding for any MCP tool operation
4
+ that takes longer than the configured threshold (default: 2 minutes).
5
+ """
6
+
7
+ import asyncio
8
+ import functools
9
+ import json
10
+ import time
11
+ import uuid
12
+ import os
13
+ from pathlib import Path
14
+ from typing import Any, Callable, Optional, Tuple
15
+ from collections.abc import Awaitable
16
+
17
+ from mcp.server.fastmcp import Context as MCPContext
18
+ from .timeout_parser import parse_timeout, format_timeout
19
+
20
+
21
+ class MCPToolTimeoutManager:
22
+ """Manager for MCP tool timeouts and backgrounding."""
23
+
24
+ # Default timeout before auto-backgrounding (2 minutes)
25
+ DEFAULT_TIMEOUT = 120.0
26
+
27
+ # Environment variable to configure timeout
28
+ TIMEOUT_ENV_VAR = "HANZO_MCP_TOOL_TIMEOUT"
29
+
30
+ def __init__(self, process_manager: Optional[Any] = None):
31
+ """Initialize the timeout manager.
32
+
33
+ Args:
34
+ process_manager: Process manager for tracking background operations
35
+ """
36
+ if process_manager is None:
37
+ # Lazy import to avoid circular imports
38
+ try:
39
+ from hanzo_mcp.tools.shell.base_process import ProcessManager
40
+ self.process_manager = ProcessManager()
41
+ except ImportError:
42
+ # If ProcessManager is not available, disable backgrounding
43
+ self.process_manager = None
44
+ else:
45
+ self.process_manager = process_manager
46
+
47
+ # Get timeout from environment or use default
48
+ env_timeout = os.getenv(self.TIMEOUT_ENV_VAR)
49
+ if env_timeout:
50
+ try:
51
+ self.timeout = parse_timeout(env_timeout)
52
+ except ValueError:
53
+ self.timeout = self.DEFAULT_TIMEOUT
54
+ else:
55
+ self.timeout = self.DEFAULT_TIMEOUT
56
+
57
+ def _get_timeout_for_tool(self, tool_name: str) -> float:
58
+ """Get timeout setting for a specific tool.
59
+
60
+ Args:
61
+ tool_name: Name of the tool
62
+
63
+ Returns:
64
+ Timeout in seconds
65
+ """
66
+ # Check for tool-specific timeout
67
+ env_var = f"HANZO_MCP_{tool_name.upper()}_TIMEOUT"
68
+ tool_timeout = os.getenv(env_var)
69
+ if tool_timeout:
70
+ try:
71
+ return parse_timeout(tool_timeout)
72
+ except ValueError:
73
+ pass
74
+
75
+ return self.timeout
76
+
77
+ async def _background_tool_execution(
78
+ self,
79
+ tool_func: Callable,
80
+ tool_name: str,
81
+ ctx: MCPContext,
82
+ process_id: str,
83
+ log_file: Path,
84
+ **params: Any
85
+ ) -> None:
86
+ """Execute tool in background and log results.
87
+
88
+ Args:
89
+ tool_func: The tool function to execute
90
+ tool_name: Name of the tool
91
+ ctx: MCP context
92
+ process_id: Process identifier
93
+ log_file: Log file path
94
+ **params: Tool parameters
95
+ """
96
+ try:
97
+ # Log start
98
+ with open(log_file, "a") as f:
99
+ f.write(f"=== Background execution started for {tool_name} ===\\n")
100
+ f.write(f"Parameters: {json.dumps(params, indent=2, default=str)}\\n")
101
+ f.write(f"Started at: {time.strftime('%Y-%m-%d %H:%M:%S')}\\n\\n")
102
+
103
+ # Execute the tool
104
+ result = await tool_func(ctx, **params)
105
+
106
+ # Log completion
107
+ with open(log_file, "a") as f:
108
+ f.write(f"\\n\\n=== Tool execution completed ===\\n")
109
+ f.write(f"Completed at: {time.strftime('%Y-%m-%d %H:%M:%S')}\\n")
110
+ f.write(f"Result length: {len(str(result))} characters\\n")
111
+ f.write("\\n=== RESULT ===\\n")
112
+ f.write(str(result))
113
+ f.write("\\n=== END RESULT ===\\n")
114
+
115
+ # Mark as completed
116
+ self.process_manager.mark_completed(process_id, 0)
117
+
118
+ except Exception as e:
119
+ # Log error
120
+ with open(log_file, "a") as f:
121
+ f.write(f"\\n\\n=== Tool execution failed ===\\n")
122
+ f.write(f"Failed at: {time.strftime('%Y-%m-%d %H:%M:%S')}\\n")
123
+ f.write(f"Error: {str(e)}\\n")
124
+ f.write(f"Error type: {type(e).__name__}\\n")
125
+
126
+ self.process_manager.mark_completed(process_id, 1)
127
+
128
+
129
+ def with_auto_timeout(tool_name: str, timeout_manager: Optional[MCPToolTimeoutManager] = None):
130
+ """Decorator to add automatic timeout and backgrounding to MCP tools.
131
+
132
+ Args:
133
+ tool_name: Name of the tool (for logging and process tracking)
134
+ timeout_manager: Optional timeout manager instance
135
+
136
+ Returns:
137
+ Decorator function
138
+ """
139
+ if timeout_manager is None:
140
+ timeout_manager = MCPToolTimeoutManager()
141
+
142
+ def decorator(func: Callable[..., Awaitable[Any]]) -> Callable[..., Awaitable[Any]]:
143
+ @functools.wraps(func)
144
+ async def wrapper(ctx: MCPContext, **params: Any) -> Any:
145
+ # Fast path for tests - skip timeout logic
146
+ if os.getenv("HANZO_MCP_FAST_TESTS") == "1":
147
+ return await func(ctx, **params)
148
+
149
+ # Get tool-specific timeout
150
+ tool_timeout = timeout_manager._get_timeout_for_tool(tool_name)
151
+
152
+ # Create task for the tool execution
153
+ tool_task = asyncio.create_task(func(ctx, **params))
154
+
155
+ try:
156
+ # Wait for completion with timeout
157
+ result = await asyncio.wait_for(tool_task, timeout=tool_timeout)
158
+ return result
159
+
160
+ except asyncio.TimeoutError:
161
+ # Tool timed out - background it
162
+ process_id = f"{tool_name}_{uuid.uuid4().hex[:8]}"
163
+ log_file = timeout_manager.process_manager.create_log_file(process_id)
164
+
165
+ # Start background execution
166
+ asyncio.create_task(
167
+ timeout_manager._background_tool_execution(
168
+ func, tool_name, ctx, process_id, log_file, **params
169
+ )
170
+ )
171
+
172
+ # Return backgrounding message
173
+ timeout_formatted = format_timeout(tool_timeout)
174
+ return (
175
+ f"Operation automatically backgrounded after {timeout_formatted}\\n"
176
+ f"Process ID: {process_id}\\n"
177
+ f"Log file: {log_file}\\n\\n"
178
+ f"Use 'process --action logs --id {process_id}' to view results\\n"
179
+ f"Use 'process --action kill --id {process_id}' to cancel\\n\\n"
180
+ f"The {tool_name} operation is continuing in the background..."
181
+ )
182
+
183
+ return wrapper
184
+ return decorator
185
+
186
+
187
+ # Global timeout manager instance
188
+ _global_timeout_manager = None
189
+
190
+
191
+ def get_global_timeout_manager() -> MCPToolTimeoutManager:
192
+ """Get the global timeout manager instance.
193
+
194
+ Returns:
195
+ Global timeout manager
196
+ """
197
+ global _global_timeout_manager
198
+ if _global_timeout_manager is None:
199
+ _global_timeout_manager = MCPToolTimeoutManager()
200
+ return _global_timeout_manager
201
+
202
+
203
+ def set_global_timeout(timeout_seconds: float) -> None:
204
+ """Set the global timeout for all MCP tools.
205
+
206
+ Args:
207
+ timeout_seconds: Timeout in seconds
208
+ """
209
+ manager = get_global_timeout_manager()
210
+ manager.timeout = timeout_seconds
211
+
212
+
213
+ def set_tool_timeout(tool_name: str, timeout_seconds: float) -> None:
214
+ """Set timeout for a specific tool via environment variable.
215
+
216
+ Args:
217
+ tool_name: Name of the tool
218
+ timeout_seconds: Timeout in seconds
219
+ """
220
+ env_var = f"HANZO_MCP_{tool_name.upper()}_TIMEOUT"
221
+ os.environ[env_var] = str(timeout_seconds)
222
+
223
+
224
+ # Convenience decorator using global manager
225
+ def auto_timeout(tool_name: str):
226
+ """Convenience decorator using the global timeout manager.
227
+
228
+ Args:
229
+ tool_name: Name of the tool
230
+
231
+ Returns:
232
+ Decorator function
233
+ """
234
+ return with_auto_timeout(tool_name, get_global_timeout_manager())
@@ -11,6 +11,8 @@ from typing import Any, Callable, final
11
11
  from collections.abc import Awaitable
12
12
 
13
13
  from mcp.server import FastMCP
14
+
15
+ from hanzo_mcp.tools.common.auto_timeout import auto_timeout
14
16
  from mcp.server.fastmcp import Context as MCPContext
15
17
 
16
18
  from hanzo_mcp.tools.common.validation import (
@@ -88,6 +90,8 @@ class BaseTool(ABC):
88
90
  pass
89
91
 
90
92
  @abstractmethod
93
+ @auto_timeout("base")
94
+
91
95
  async def call(self, ctx: MCPContext, **params: Any) -> Any:
92
96
  """Execute the tool with the given parameters.
93
97
 
@@ -9,6 +9,8 @@ from typing import Any, Unpack, Annotated, TypedDict, final, override
9
9
 
10
10
  from pydantic import Field
11
11
  from mcp.server import FastMCP
12
+
13
+ from hanzo_mcp.tools.common.auto_timeout import auto_timeout
12
14
  from mcp.server.fastmcp import Context as MCPContext
13
15
 
14
16
  from hanzo_mcp.tools.common.base import BaseTool
@@ -154,6 +156,9 @@ Not available: think,write,edit,multi_edit,notebook_edit
154
156
  self.tools = tools
155
157
 
156
158
  @override
159
+ @auto_timeout("batch")
160
+
161
+
157
162
  async def call(
158
163
  self,
159
164
  ctx: MCPContext,
@@ -5,6 +5,8 @@ from typing import Any, Dict, Unpack, Optional, TypedDict, final
5
5
 
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.config.settings import (
9
11
  ProjectConfig,
10
12
  MCPServerConfig,
@@ -62,6 +64,9 @@ Perfect for AI-driven configuration where users can say things like:
62
64
 
63
65
  Automatically detects projects based on LLM.md files and manages .hanzo/ directories."""
64
66
 
67
+ @auto_timeout("config")
68
+
69
+
65
70
  async def call(
66
71
  self,
67
72
  ctx: MCPContext,
@@ -7,6 +7,8 @@ from typing import Unpack, Annotated, TypedDict, final, override
7
7
 
8
8
  from pydantic import Field
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
@@ -131,6 +133,9 @@ Recommendations:
131
133
  pass
132
134
 
133
135
  @override
136
+ @auto_timeout("critic")
137
+
138
+
134
139
  async def call(
135
140
  self,
136
141
  ctx: MCPContext,
@@ -9,6 +9,8 @@ from typing import Any, Dict, Union
9
9
 
10
10
  from mcp.server.fastmcp import Context as MCPContext
11
11
 
12
+ from hanzo_mcp.tools.common.auto_timeout import auto_timeout
13
+
12
14
  from hanzo_mcp.tools.common.base import BaseTool, handle_connection_errors
13
15
  from hanzo_mcp.tools.common.pagination import CursorManager
14
16
  from hanzo_mcp.tools.common.paginated_response import paginate_if_needed
@@ -44,6 +46,8 @@ class PaginatedBaseTool(BaseTool):
44
46
  pass
45
47
 
46
48
  @handle_connection_errors
49
+ @auto_timeout("paginated_base")
50
+
47
51
  async def call(self, ctx: MCPContext, **params: Any) -> Union[str, Dict[str, Any]]:
48
52
  """Execute the tool with automatic pagination support.
49
53
 
@@ -7,6 +7,8 @@ from datetime import datetime
7
7
  import psutil
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.mcp.mcp_add import McpAddTool
12
14
  from hanzo_mcp.tools.common.context import create_tool_context
@@ -57,6 +59,9 @@ Example:
57
59
  """
58
60
 
59
61
  @override
62
+ @auto_timeout("stats")
63
+
64
+
60
65
  async def call(
61
66
  self,
62
67
  ctx: MCPContext,
@@ -7,6 +7,8 @@ from typing import Unpack, Annotated, TypedDict, final, override
7
7
 
8
8
  from pydantic import Field
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
@@ -93,6 +95,9 @@ Feature Implementation Planning
93
95
  pass
94
96
 
95
97
  @override
98
+ @auto_timeout("thinking")
99
+
100
+
96
101
  async def call(
97
102
  self,
98
103
  ctx: MCPContext,
@@ -0,0 +1,103 @@
1
+ """Human-readable timeout parsing utilities."""
2
+
3
+ import re
4
+ from typing import Union
5
+
6
+
7
+ def parse_timeout(timeout_str: Union[str, int, float]) -> float:
8
+ """Parse timeout from human-readable string or numeric value.
9
+
10
+ Supports formats like:
11
+ - "2min", "5m", "120s", "30sec", "1.5h", "0.5hr"
12
+ - 120 (seconds as number)
13
+ - "120" (seconds as string)
14
+
15
+ Args:
16
+ timeout_str: Timeout value as string or number
17
+
18
+ Returns:
19
+ Timeout in seconds as float
20
+
21
+ Raises:
22
+ ValueError: If format is not recognized
23
+ """
24
+ if isinstance(timeout_str, (int, float)):
25
+ return float(timeout_str)
26
+
27
+ if isinstance(timeout_str, str):
28
+ # Handle pure numeric strings
29
+ try:
30
+ return float(timeout_str)
31
+ except ValueError:
32
+ pass
33
+
34
+ # Handle human-readable formats
35
+ timeout_str = timeout_str.lower().strip()
36
+
37
+ # Regex patterns for different time units
38
+ patterns = [
39
+ # Hours: 1h, 1.5hr, 2hour, 3hours
40
+ (r'^(\d*\.?\d+)\s*h(?:r|our|ours)?$', 3600),
41
+ # Minutes: 2m, 5min, 10mins, 1.5minute
42
+ (r'^(\d*\.?\d+)\s*m(?:in|ins|inute|inutes)?$', 60),
43
+ # Seconds: 30s, 120sec, 45secs, 60second, 90seconds
44
+ (r'^(\d*\.?\d+)\s*s(?:ec|ecs|econd|econds)?$', 1),
45
+ ]
46
+
47
+ for pattern, multiplier in patterns:
48
+ match = re.match(pattern, timeout_str)
49
+ if match:
50
+ value = float(match.group(1))
51
+ return value * multiplier
52
+
53
+ # If no pattern matches, raise error
54
+ raise ValueError(
55
+ f"Invalid timeout format: '{timeout_str}'. "
56
+ f"Supported formats: 2min, 5m, 120s, 30sec, 1.5h, 0.5hr, or numeric seconds."
57
+ )
58
+
59
+ raise ValueError(f"Unsupported timeout type: {type(timeout_str)}")
60
+
61
+
62
+ def format_timeout(seconds: float) -> str:
63
+ """Format timeout seconds into human-readable string.
64
+
65
+ Args:
66
+ seconds: Timeout in seconds
67
+
68
+ Returns:
69
+ Human-readable string like "2m", "90s", "1.5h"
70
+ """
71
+ if seconds >= 3600: # >= 1 hour
72
+ hours = seconds / 3600
73
+ if hours.is_integer():
74
+ return f"{int(hours)}h"
75
+ else:
76
+ return f"{hours:.1f}h"
77
+ elif seconds >= 60: # >= 1 minute
78
+ minutes = seconds / 60
79
+ if minutes.is_integer():
80
+ return f"{int(minutes)}m"
81
+ else:
82
+ return f"{minutes:.1f}m"
83
+ else: # < 1 minute
84
+ if seconds.is_integer():
85
+ return f"{int(seconds)}s"
86
+ else:
87
+ return f"{seconds:.1f}s"
88
+
89
+
90
+ # Test the parser
91
+ if __name__ == "__main__":
92
+ test_cases = [
93
+ "2min", "5m", "120s", "30sec", "1.5h", "0.5hr",
94
+ "90", 120, 3600.0, "1hour", "2hours", "30seconds"
95
+ ]
96
+
97
+ for case in test_cases:
98
+ try:
99
+ result = parse_timeout(case)
100
+ formatted = format_timeout(result)
101
+ print(f"{case} -> {result}s ({formatted})")
102
+ except ValueError as e:
103
+ print(f"{case} -> ERROR: {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.common.context import create_tool_context
10
12
  from hanzo_mcp.tools.common.tool_enable import ToolEnableTool
@@ -72,6 +74,9 @@ Use 'tool_enable' to re-enable disabled tools.
72
74
  """
73
75
 
74
76
  @override
77
+ @auto_timeout("tool_disable")
78
+
79
+
75
80
  async def call(
76
81
  self,
77
82
  ctx: MCPContext,
@@ -7,6 +7,8 @@ from pathlib import Path
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
 
@@ -119,6 +121,9 @@ Use 'tool_list' to see all available tools and their status.
119
121
  """
120
122
 
121
123
  @override
124
+ @auto_timeout("tool_enable")
125
+
126
+
122
127
  async def call(
123
128
  self,
124
129
  ctx: MCPContext,
@@ -5,6 +5,8 @@ from typing import Unpack, Optional, 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.common.context import create_tool_context
10
12
  from hanzo_mcp.tools.common.tool_enable import ToolEnableTool
@@ -158,6 +160,9 @@ Use 'tool_enable' and 'tool_disable' to change tool status.
158
160
  """
159
161
 
160
162
  @override
163
+ @auto_timeout("tool_list")
164
+
165
+
161
166
  async def call(
162
167
  self,
163
168
  ctx: MCPContext,
@@ -9,6 +9,8 @@ from pathlib import Path
9
9
  from pydantic import Field
10
10
  from mcp.server.fastmcp import Context as MCPContext
11
11
 
12
+ from hanzo_mcp.tools.common.auto_timeout import auto_timeout
13
+
12
14
  from hanzo_mcp.config import load_settings, save_settings
13
15
  from hanzo_mcp.tools.common.base import BaseTool
14
16
  from hanzo_mcp.tools.common.permissions import PermissionManager
@@ -104,6 +106,9 @@ config --action list
104
106
  config --action toggle index.scope --path ./project"""
105
107
 
106
108
  @override
109
+ @auto_timeout("config")
110
+
111
+
107
112
  async def call(
108
113
  self,
109
114
  ctx: MCPContext,
@@ -3,6 +3,8 @@
3
3
  from typing import Optional, override
4
4
 
5
5
  from mcp.server import FastMCP
6
+
7
+ from hanzo_mcp.tools.common.auto_timeout import auto_timeout
6
8
  from mcp.server.fastmcp import Context as MCPContext
7
9
 
8
10
  from hanzo_mcp.tools.common.base import BaseTool
@@ -310,6 +312,9 @@ mode --action current"""
310
312
  """Handle mode tool calls."""
311
313
  return await tool_self.run(ctx, action=action, name=name)
312
314
 
315
+ @auto_timeout("mode")
316
+
317
+
313
318
  async def call(self, ctx: MCPContext, **params) -> str:
314
319
  """Call the tool with arguments."""
315
320
  return await self.run(ctx, action=params.get("action", "list"), name=params.get("name"))
@@ -15,6 +15,8 @@ from typing import (
15
15
  from pydantic import Field
16
16
  from mcp.server.fastmcp import Context as MCPContext
17
17
 
18
+ from hanzo_mcp.tools.common.auto_timeout import auto_timeout
19
+
18
20
  from hanzo_mcp.tools.common.base import BaseTool
19
21
  from hanzo_mcp.tools.common.permissions import PermissionManager
20
22
  from hanzo_mcp.tools.database.database_manager import DatabaseManager
@@ -146,6 +148,9 @@ graph --action search --pattern "John" --node-type User
146
148
  """
147
149
 
148
150
  @override
151
+ @auto_timeout("graph")
152
+
153
+
149
154
  async def call(
150
155
  self,
151
156
  ctx: MCPContext,
@@ -6,6 +6,8 @@ from typing import Unpack, Optional, Annotated, TypedDict, final, override
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
@@ -137,6 +139,9 @@ Examples:
137
139
  """
138
140
 
139
141
  @override
142
+ @auto_timeout("graph_add")
143
+
144
+
140
145
  async def call(
141
146
  self,
142
147
  ctx: MCPContext,
@@ -15,6 +15,8 @@ from collections import deque
15
15
  from pydantic import Field
16
16
  from mcp.server.fastmcp import Context as MCPContext
17
17
 
18
+ from hanzo_mcp.tools.common.auto_timeout import auto_timeout
19
+
18
20
  from hanzo_mcp.tools.common.base import BaseTool
19
21
  from hanzo_mcp.tools.common.context import create_tool_context
20
22
  from hanzo_mcp.tools.common.permissions import PermissionManager
@@ -147,6 +149,9 @@ Examples:
147
149
  """
148
150
 
149
151
  @override
152
+ @auto_timeout("graph_query")
153
+
154
+
150
155
  async def call(
151
156
  self,
152
157
  ctx: MCPContext,
@@ -5,6 +5,8 @@ from typing import Unpack, Optional, 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.common.context import create_tool_context
10
12
  from hanzo_mcp.tools.common.permissions import PermissionManager
@@ -116,6 +118,9 @@ Examples:
116
118
  """
117
119
 
118
120
  @override
121
+ @auto_timeout("graph_remove")
122
+
123
+
119
124
  async def call(
120
125
  self,
121
126
  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
@@ -117,6 +119,9 @@ Examples:
117
119
  """
118
120
 
119
121
  @override
122
+ @auto_timeout("graph_search")
123
+
124
+
120
125
  async def call(
121
126
  self,
122
127
  ctx: MCPContext,
@@ -7,6 +7,8 @@ from collections import defaultdict
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
@@ -97,6 +99,9 @@ Examples:
97
99
  """
98
100
 
99
101
  @override
102
+ @auto_timeout("graph_stats")
103
+
104
+
100
105
  async def call(
101
106
  self,
102
107
  ctx: MCPContext,