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.
- hanzo_mcp/__init__.py +1 -1
- hanzo_mcp/analytics/posthog_analytics.py +14 -1
- hanzo_mcp/cli.py +108 -4
- hanzo_mcp/server.py +11 -0
- hanzo_mcp/tools/__init__.py +3 -16
- hanzo_mcp/tools/agent/__init__.py +5 -0
- hanzo_mcp/tools/agent/agent.py +5 -0
- hanzo_mcp/tools/agent/agent_tool.py +3 -17
- hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +623 -0
- hanzo_mcp/tools/agent/clarification_tool.py +7 -1
- hanzo_mcp/tools/agent/claude_desktop_auth.py +16 -6
- 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 +7 -1
- 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 +7 -1
- hanzo_mcp/tools/agent/swarm_alias.py +5 -0
- hanzo_mcp/tools/agent/swarm_tool.py +701 -0
- hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +554 -0
- hanzo_mcp/tools/agent/unified_cli_tools.py +5 -0
- hanzo_mcp/tools/common/auto_timeout.py +254 -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/permissions.py +38 -12
- hanzo_mcp/tools/common/personality.py +673 -980
- 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 +7 -2
- hanzo_mcp/tools/editor/neovim_session.py +5 -0
- hanzo_mcp/tools/filesystem/__init__.py +23 -26
- hanzo_mcp/tools/filesystem/ast_tool.py +3 -4
- hanzo_mcp/tools/filesystem/base.py +2 -18
- hanzo_mcp/tools/filesystem/batch_search.py +825 -0
- hanzo_mcp/tools/filesystem/content_replace.py +5 -3
- hanzo_mcp/tools/filesystem/diff.py +5 -0
- hanzo_mcp/tools/filesystem/directory_tree.py +34 -281
- hanzo_mcp/tools/filesystem/directory_tree_paginated.py +345 -0
- hanzo_mcp/tools/filesystem/edit.py +6 -5
- hanzo_mcp/tools/filesystem/find.py +177 -311
- hanzo_mcp/tools/filesystem/find_files.py +370 -0
- hanzo_mcp/tools/filesystem/git_search.py +5 -3
- hanzo_mcp/tools/filesystem/grep.py +454 -0
- hanzo_mcp/tools/filesystem/multi_edit.py +6 -5
- hanzo_mcp/tools/filesystem/read.py +10 -9
- hanzo_mcp/tools/filesystem/rules_tool.py +6 -4
- hanzo_mcp/tools/filesystem/search_tool.py +728 -0
- hanzo_mcp/tools/filesystem/symbols_tool.py +510 -0
- hanzo_mcp/tools/filesystem/tree.py +273 -0
- hanzo_mcp/tools/filesystem/watch.py +6 -1
- hanzo_mcp/tools/filesystem/write.py +13 -7
- hanzo_mcp/tools/jupyter/jupyter.py +30 -2
- hanzo_mcp/tools/jupyter/notebook_edit.py +298 -0
- hanzo_mcp/tools/jupyter/notebook_read.py +148 -0
- hanzo_mcp/tools/llm/consensus_tool.py +8 -6
- 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 +475 -622
- hanzo_mcp/tools/mcp/mcp_add.py +7 -2
- hanzo_mcp/tools/mcp/mcp_remove.py +15 -2
- 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 +5 -3
- hanzo_mcp/tools/search/unified_search.py +3 -1
- hanzo_mcp/tools/shell/__init__.py +2 -14
- hanzo_mcp/tools/shell/base_process.py +4 -2
- hanzo_mcp/tools/shell/bash_tool.py +2 -0
- hanzo_mcp/tools/shell/command_executor.py +7 -7
- 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 +142 -0
- hanzo_mcp/tools/todo/todo_write.py +367 -0
- hanzo_mcp/tools/vector/__init__.py +42 -95
- 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.9.0.dist-info → hanzo_mcp-0.9.2.dist-info}/METADATA +1 -1
- hanzo_mcp-0.9.2.dist-info/RECORD +195 -0
- hanzo_mcp/tools/common/path_utils.py +0 -34
- hanzo_mcp/tools/compiler/__init__.py +0 -8
- hanzo_mcp/tools/compiler/sandboxed_compiler.py +0 -681
- hanzo_mcp/tools/environment/__init__.py +0 -8
- hanzo_mcp/tools/environment/environment_detector.py +0 -594
- hanzo_mcp/tools/filesystem/search.py +0 -1160
- hanzo_mcp/tools/framework/__init__.py +0 -8
- hanzo_mcp/tools/framework/framework_modes.py +0 -714
- hanzo_mcp/tools/memory/conversation_memory.py +0 -636
- hanzo_mcp/tools/shell/run_tool.py +0 -56
- hanzo_mcp/tools/vector/node_tool.py +0 -538
- hanzo_mcp/tools/vector/unified_vector.py +0 -384
- hanzo_mcp-0.9.0.dist-info/RECORD +0 -191
- {hanzo_mcp-0.9.0.dist-info → hanzo_mcp-0.9.2.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.9.0.dist-info → hanzo_mcp-0.9.2.dist-info}/entry_points.txt +0 -0
- {hanzo_mcp-0.9.0.dist-info → hanzo_mcp-0.9.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,254 @@
|
|
|
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(*args: Any, **params: Any) -> Any:
|
|
145
|
+
# Handle both method calls (with self) and function calls
|
|
146
|
+
# For methods: args = (self, ctx), For functions: args = (ctx,)
|
|
147
|
+
if len(args) >= 2:
|
|
148
|
+
# Method call: self, ctx, **params
|
|
149
|
+
self_or_ctx = args[0]
|
|
150
|
+
ctx = args[1]
|
|
151
|
+
call_func = lambda: func(self_or_ctx, ctx, **params)
|
|
152
|
+
elif len(args) == 1:
|
|
153
|
+
# Function call: ctx, **params
|
|
154
|
+
ctx = args[0]
|
|
155
|
+
call_func = lambda: func(ctx, **params)
|
|
156
|
+
else:
|
|
157
|
+
raise TypeError(f"Expected at least 1 argument (ctx), got {len(args)}")
|
|
158
|
+
|
|
159
|
+
# Fast path for tests - skip timeout logic
|
|
160
|
+
if os.getenv("HANZO_MCP_FAST_TESTS") == "1":
|
|
161
|
+
return await call_func()
|
|
162
|
+
|
|
163
|
+
# Get tool-specific timeout
|
|
164
|
+
tool_timeout = timeout_manager._get_timeout_for_tool(tool_name)
|
|
165
|
+
|
|
166
|
+
# Create task for the tool execution
|
|
167
|
+
tool_task = asyncio.create_task(call_func())
|
|
168
|
+
|
|
169
|
+
try:
|
|
170
|
+
# Wait for completion with timeout
|
|
171
|
+
result = await asyncio.wait_for(tool_task, timeout=tool_timeout)
|
|
172
|
+
return result
|
|
173
|
+
|
|
174
|
+
except asyncio.TimeoutError:
|
|
175
|
+
# Tool timed out - background it
|
|
176
|
+
process_id = f"{tool_name}_{uuid.uuid4().hex[:8]}"
|
|
177
|
+
log_file = timeout_manager.process_manager.create_log_file(process_id)
|
|
178
|
+
|
|
179
|
+
# Start background execution (need to reconstruct the call)
|
|
180
|
+
async def background_call():
|
|
181
|
+
if len(args) >= 2:
|
|
182
|
+
return await func(args[0], ctx, **params)
|
|
183
|
+
else:
|
|
184
|
+
return await func(ctx, **params)
|
|
185
|
+
|
|
186
|
+
asyncio.create_task(
|
|
187
|
+
timeout_manager._background_tool_execution(
|
|
188
|
+
background_call, tool_name, ctx, process_id, log_file, **params
|
|
189
|
+
)
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# Return backgrounding message
|
|
193
|
+
timeout_formatted = format_timeout(tool_timeout)
|
|
194
|
+
return (
|
|
195
|
+
f"Operation automatically backgrounded after {timeout_formatted}\\n"
|
|
196
|
+
f"Process ID: {process_id}\\n"
|
|
197
|
+
f"Log file: {log_file}\\n\\n"
|
|
198
|
+
f"Use 'process --action logs --id {process_id}' to view results\\n"
|
|
199
|
+
f"Use 'process --action kill --id {process_id}' to cancel\\n\\n"
|
|
200
|
+
f"The {tool_name} operation is continuing in the background..."
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
return wrapper
|
|
204
|
+
return decorator
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
# Global timeout manager instance
|
|
208
|
+
_global_timeout_manager = None
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def get_global_timeout_manager() -> MCPToolTimeoutManager:
|
|
212
|
+
"""Get the global timeout manager instance.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
Global timeout manager
|
|
216
|
+
"""
|
|
217
|
+
global _global_timeout_manager
|
|
218
|
+
if _global_timeout_manager is None:
|
|
219
|
+
_global_timeout_manager = MCPToolTimeoutManager()
|
|
220
|
+
return _global_timeout_manager
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def set_global_timeout(timeout_seconds: float) -> None:
|
|
224
|
+
"""Set the global timeout for all MCP tools.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
timeout_seconds: Timeout in seconds
|
|
228
|
+
"""
|
|
229
|
+
manager = get_global_timeout_manager()
|
|
230
|
+
manager.timeout = timeout_seconds
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def set_tool_timeout(tool_name: str, timeout_seconds: float) -> None:
|
|
234
|
+
"""Set timeout for a specific tool via environment variable.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
tool_name: Name of the tool
|
|
238
|
+
timeout_seconds: Timeout in seconds
|
|
239
|
+
"""
|
|
240
|
+
env_var = f"HANZO_MCP_{tool_name.upper()}_TIMEOUT"
|
|
241
|
+
os.environ[env_var] = str(timeout_seconds)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
# Convenience decorator using global manager
|
|
245
|
+
def auto_timeout(tool_name: str):
|
|
246
|
+
"""Convenience decorator using the global timeout manager.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
tool_name: Name of the tool
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
Decorator function
|
|
253
|
+
"""
|
|
254
|
+
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
|
|
|
@@ -8,9 +8,6 @@ from typing import Any, TypeVar, final
|
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
from collections.abc import Callable, Awaitable
|
|
10
10
|
|
|
11
|
-
# Import centralized path resolution
|
|
12
|
-
from hanzo_mcp.tools.common.path_utils import resolve_path
|
|
13
|
-
|
|
14
11
|
# Define type variables for better type annotations
|
|
15
12
|
T = TypeVar("T")
|
|
16
13
|
P = TypeVar("P")
|
|
@@ -84,8 +81,7 @@ class PermissionManager:
|
|
|
84
81
|
Args:
|
|
85
82
|
path: The path to allow
|
|
86
83
|
"""
|
|
87
|
-
|
|
88
|
-
resolved_path = Path(resolve_path(path))
|
|
84
|
+
resolved_path: Path = Path(path).resolve()
|
|
89
85
|
self.allowed_paths.add(resolved_path)
|
|
90
86
|
|
|
91
87
|
def remove_allowed_path(self, path: str) -> None:
|
|
@@ -94,8 +90,7 @@ class PermissionManager:
|
|
|
94
90
|
Args:
|
|
95
91
|
path: The path to remove
|
|
96
92
|
"""
|
|
97
|
-
|
|
98
|
-
resolved_path = Path(resolve_path(path))
|
|
93
|
+
resolved_path: Path = Path(path).resolve()
|
|
99
94
|
if resolved_path in self.allowed_paths:
|
|
100
95
|
self.allowed_paths.remove(resolved_path)
|
|
101
96
|
|
|
@@ -105,8 +100,7 @@ class PermissionManager:
|
|
|
105
100
|
Args:
|
|
106
101
|
path: The path to exclude
|
|
107
102
|
"""
|
|
108
|
-
|
|
109
|
-
resolved_path = Path(resolve_path(path))
|
|
103
|
+
resolved_path: Path = Path(path).resolve()
|
|
110
104
|
self.excluded_paths.add(resolved_path)
|
|
111
105
|
|
|
112
106
|
def add_exclusion_pattern(self, pattern: str) -> None:
|
|
@@ -118,7 +112,7 @@ class PermissionManager:
|
|
|
118
112
|
self.excluded_patterns.append(pattern)
|
|
119
113
|
|
|
120
114
|
def is_path_allowed(self, path: str) -> bool:
|
|
121
|
-
"""Check if a path is allowed.
|
|
115
|
+
"""Check if a path is allowed with security validation.
|
|
122
116
|
|
|
123
117
|
Args:
|
|
124
118
|
path: The path to check
|
|
@@ -126,8 +120,24 @@ class PermissionManager:
|
|
|
126
120
|
Returns:
|
|
127
121
|
True if the path is allowed, False otherwise
|
|
128
122
|
"""
|
|
129
|
-
#
|
|
130
|
-
|
|
123
|
+
# Security check: Reject paths with traversal attempts
|
|
124
|
+
if ".." in str(path) or "~" in str(path):
|
|
125
|
+
return False
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
# Resolve the path (follows symlinks and makes absolute)
|
|
129
|
+
resolved_path: Path = Path(path).resolve(strict=False)
|
|
130
|
+
|
|
131
|
+
# Security check: Ensure resolved path doesn't escape allowed directories
|
|
132
|
+
# by checking if it's actually under an allowed path after resolution
|
|
133
|
+
original_path = Path(path)
|
|
134
|
+
if original_path.is_absolute() and str(resolved_path) != str(original_path.resolve(strict=False)):
|
|
135
|
+
# Path resolution changed the path significantly, might be symlink attack
|
|
136
|
+
# Additional check: is the resolved path still under allowed paths?
|
|
137
|
+
pass # Continue to normal checks
|
|
138
|
+
except (OSError, RuntimeError) as e:
|
|
139
|
+
# Path resolution failed, deny access
|
|
140
|
+
return False
|
|
131
141
|
|
|
132
142
|
# Check exclusions first
|
|
133
143
|
if self._is_path_excluded(resolved_path):
|
|
@@ -136,12 +146,28 @@ class PermissionManager:
|
|
|
136
146
|
# Check if the path is within any allowed path
|
|
137
147
|
for allowed_path in self.allowed_paths:
|
|
138
148
|
try:
|
|
149
|
+
# This will raise ValueError if resolved_path is not under allowed_path
|
|
139
150
|
resolved_path.relative_to(allowed_path)
|
|
151
|
+
# Additional check: ensure no symlinks are escaping the allowed directory
|
|
152
|
+
if resolved_path.exists() and resolved_path.is_symlink():
|
|
153
|
+
link_target = Path(os.readlink(resolved_path))
|
|
154
|
+
if link_target.is_absolute():
|
|
155
|
+
# Absolute symlink - check if it points within allowed paths
|
|
156
|
+
if not any(self._is_subpath(link_target, ap) for ap in self.allowed_paths):
|
|
157
|
+
return False
|
|
140
158
|
return True
|
|
141
159
|
except ValueError:
|
|
142
160
|
continue
|
|
143
161
|
|
|
144
162
|
return False
|
|
163
|
+
|
|
164
|
+
def _is_subpath(self, child: Path, parent: Path) -> bool:
|
|
165
|
+
"""Check if child is a subpath of parent."""
|
|
166
|
+
try:
|
|
167
|
+
child.resolve().relative_to(parent.resolve())
|
|
168
|
+
return True
|
|
169
|
+
except ValueError:
|
|
170
|
+
return False
|
|
145
171
|
|
|
146
172
|
def _is_path_excluded(self, path: Path) -> bool:
|
|
147
173
|
"""Check if a path is excluded.
|