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.
- hanzo_mcp/__init__.py +1 -1
- hanzo_mcp/cli.py +108 -4
- hanzo_mcp/tools/agent/__init__.py +5 -0
- hanzo_mcp/tools/agent/agent.py +5 -0
- hanzo_mcp/tools/agent/agent_tool.py +3 -0
- hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +5 -0
- hanzo_mcp/tools/agent/clarification_tool.py +2 -0
- hanzo_mcp/tools/agent/claude_desktop_auth.py +5 -0
- hanzo_mcp/tools/agent/cli_agent_base.py +5 -0
- hanzo_mcp/tools/agent/cli_tools.py +26 -0
- hanzo_mcp/tools/agent/code_auth_tool.py +5 -0
- hanzo_mcp/tools/agent/critic_tool.py +2 -0
- hanzo_mcp/tools/agent/iching_tool.py +5 -0
- hanzo_mcp/tools/agent/network_tool.py +5 -0
- hanzo_mcp/tools/agent/review_tool.py +2 -0
- hanzo_mcp/tools/agent/swarm_alias.py +5 -0
- hanzo_mcp/tools/agent/swarm_tool.py +3 -0
- hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +5 -0
- hanzo_mcp/tools/agent/unified_cli_tools.py +5 -0
- hanzo_mcp/tools/common/auto_timeout.py +234 -0
- hanzo_mcp/tools/common/base.py +4 -0
- hanzo_mcp/tools/common/batch_tool.py +5 -0
- hanzo_mcp/tools/common/config_tool.py +5 -0
- hanzo_mcp/tools/common/critic_tool.py +5 -0
- hanzo_mcp/tools/common/paginated_base.py +4 -0
- hanzo_mcp/tools/common/stats.py +5 -0
- hanzo_mcp/tools/common/thinking_tool.py +5 -0
- hanzo_mcp/tools/common/timeout_parser.py +103 -0
- hanzo_mcp/tools/common/tool_disable.py +5 -0
- hanzo_mcp/tools/common/tool_enable.py +5 -0
- hanzo_mcp/tools/common/tool_list.py +5 -0
- hanzo_mcp/tools/config/config_tool.py +5 -0
- hanzo_mcp/tools/config/mode_tool.py +5 -0
- hanzo_mcp/tools/database/graph.py +5 -0
- hanzo_mcp/tools/database/graph_add.py +5 -0
- hanzo_mcp/tools/database/graph_query.py +5 -0
- hanzo_mcp/tools/database/graph_remove.py +5 -0
- hanzo_mcp/tools/database/graph_search.py +5 -0
- hanzo_mcp/tools/database/graph_stats.py +5 -0
- hanzo_mcp/tools/database/sql.py +5 -0
- hanzo_mcp/tools/database/sql_query.py +2 -0
- hanzo_mcp/tools/database/sql_search.py +5 -0
- hanzo_mcp/tools/database/sql_stats.py +5 -0
- hanzo_mcp/tools/editor/neovim_command.py +5 -0
- hanzo_mcp/tools/editor/neovim_edit.py +5 -0
- hanzo_mcp/tools/editor/neovim_session.py +5 -0
- hanzo_mcp/tools/filesystem/ast_tool.py +2 -0
- hanzo_mcp/tools/filesystem/batch_search.py +5 -0
- hanzo_mcp/tools/filesystem/content_replace.py +5 -0
- hanzo_mcp/tools/filesystem/diff.py +5 -0
- hanzo_mcp/tools/filesystem/directory_tree.py +5 -0
- hanzo_mcp/tools/filesystem/directory_tree_paginated.py +5 -0
- hanzo_mcp/tools/filesystem/edit.py +5 -0
- hanzo_mcp/tools/filesystem/find.py +5 -0
- hanzo_mcp/tools/filesystem/find_files.py +5 -0
- hanzo_mcp/tools/filesystem/git_search.py +5 -0
- hanzo_mcp/tools/filesystem/grep.py +5 -0
- hanzo_mcp/tools/filesystem/multi_edit.py +5 -0
- hanzo_mcp/tools/filesystem/read.py +5 -0
- hanzo_mcp/tools/filesystem/rules_tool.py +5 -0
- hanzo_mcp/tools/filesystem/search_tool.py +5 -0
- hanzo_mcp/tools/filesystem/symbols_tool.py +5 -0
- hanzo_mcp/tools/filesystem/tree.py +5 -0
- hanzo_mcp/tools/filesystem/watch.py +5 -0
- hanzo_mcp/tools/filesystem/write.py +5 -0
- hanzo_mcp/tools/jupyter/jupyter.py +5 -0
- hanzo_mcp/tools/jupyter/notebook_edit.py +5 -0
- hanzo_mcp/tools/jupyter/notebook_read.py +5 -0
- hanzo_mcp/tools/llm/consensus_tool.py +2 -0
- hanzo_mcp/tools/llm/llm_manage.py +5 -0
- hanzo_mcp/tools/llm/llm_tool.py +2 -0
- hanzo_mcp/tools/llm/llm_unified.py +5 -0
- hanzo_mcp/tools/llm/provider_tools.py +5 -0
- hanzo_mcp/tools/lsp/lsp_tool.py +5 -0
- hanzo_mcp/tools/mcp/mcp_add.py +5 -0
- hanzo_mcp/tools/mcp/mcp_remove.py +5 -0
- hanzo_mcp/tools/mcp/mcp_stats.py +5 -0
- hanzo_mcp/tools/mcp/mcp_tool.py +5 -0
- hanzo_mcp/tools/memory/knowledge_tools.py +14 -0
- hanzo_mcp/tools/memory/memory_tools.py +17 -0
- hanzo_mcp/tools/search/find_tool.py +3 -1
- hanzo_mcp/tools/search/unified_search.py +3 -1
- hanzo_mcp/tools/shell/base_process.py +4 -2
- hanzo_mcp/tools/shell/bash_tool.py +2 -0
- hanzo_mcp/tools/shell/logs.py +5 -0
- hanzo_mcp/tools/shell/npx.py +5 -0
- hanzo_mcp/tools/shell/npx_background.py +5 -0
- hanzo_mcp/tools/shell/npx_tool.py +5 -0
- hanzo_mcp/tools/shell/open.py +5 -0
- hanzo_mcp/tools/shell/pkill.py +5 -0
- hanzo_mcp/tools/shell/process_tool.py +5 -0
- hanzo_mcp/tools/shell/processes.py +5 -0
- hanzo_mcp/tools/shell/run_background.py +5 -0
- hanzo_mcp/tools/shell/run_command.py +2 -0
- hanzo_mcp/tools/shell/run_command_windows.py +5 -0
- hanzo_mcp/tools/shell/streaming_command.py +5 -0
- hanzo_mcp/tools/shell/uvx.py +5 -0
- hanzo_mcp/tools/shell/uvx_background.py +5 -0
- hanzo_mcp/tools/shell/uvx_tool.py +5 -0
- hanzo_mcp/tools/shell/zsh_tool.py +3 -0
- hanzo_mcp/tools/todo/todo.py +5 -0
- hanzo_mcp/tools/todo/todo_read.py +5 -0
- hanzo_mcp/tools/todo/todo_write.py +5 -0
- hanzo_mcp/tools/vector/index_tool.py +5 -0
- hanzo_mcp/tools/vector/vector.py +5 -0
- hanzo_mcp/tools/vector/vector_index.py +5 -0
- hanzo_mcp/tools/vector/vector_search.py +5 -0
- {hanzo_mcp-0.8.15.dist-info → hanzo_mcp-0.9.1.dist-info}/METADATA +1 -1
- hanzo_mcp-0.9.1.dist-info/RECORD +195 -0
- hanzo_mcp-0.8.15.dist-info/RECORD +0 -193
- {hanzo_mcp-0.8.15.dist-info → hanzo_mcp-0.9.1.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.8.15.dist-info → hanzo_mcp-0.9.1.dist-info}/entry_points.txt +0 -0
- {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())
|
hanzo_mcp/tools/common/base.py
CHANGED
|
@@ -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
|
|
hanzo_mcp/tools/common/stats.py
CHANGED
|
@@ -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,
|