hanzo-mcp 0.8.11__py3-none-any.whl → 0.9.0__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 -3
- hanzo_mcp/analytics/posthog_analytics.py +3 -9
- hanzo_mcp/bridge.py +9 -25
- hanzo_mcp/cli.py +6 -15
- hanzo_mcp/cli_enhanced.py +5 -14
- hanzo_mcp/cli_plugin.py +3 -9
- hanzo_mcp/config/settings.py +6 -20
- hanzo_mcp/config/tool_config.py +1 -3
- hanzo_mcp/core/base_agent.py +88 -88
- hanzo_mcp/core/model_registry.py +238 -210
- hanzo_mcp/dev_server.py +5 -15
- hanzo_mcp/prompts/__init__.py +2 -6
- hanzo_mcp/prompts/project_todo_reminder.py +3 -9
- hanzo_mcp/prompts/tool_explorer.py +1 -3
- hanzo_mcp/prompts/utils.py +7 -21
- hanzo_mcp/server.py +2 -6
- hanzo_mcp/tools/__init__.py +26 -27
- hanzo_mcp/tools/agent/__init__.py +2 -1
- hanzo_mcp/tools/agent/agent.py +10 -30
- hanzo_mcp/tools/agent/agent_tool.py +22 -15
- hanzo_mcp/tools/agent/claude_desktop_auth.py +3 -9
- hanzo_mcp/tools/agent/cli_agent_base.py +7 -24
- hanzo_mcp/tools/agent/cli_tools.py +75 -74
- hanzo_mcp/tools/agent/code_auth.py +1 -3
- hanzo_mcp/tools/agent/code_auth_tool.py +2 -6
- hanzo_mcp/tools/agent/critic_tool.py +8 -24
- hanzo_mcp/tools/agent/iching_tool.py +12 -36
- hanzo_mcp/tools/agent/network_tool.py +7 -18
- hanzo_mcp/tools/agent/prompt.py +1 -5
- hanzo_mcp/tools/agent/review_tool.py +10 -25
- hanzo_mcp/tools/agent/swarm_alias.py +1 -3
- hanzo_mcp/tools/agent/unified_cli_tools.py +38 -38
- hanzo_mcp/tools/common/batch_tool.py +15 -45
- hanzo_mcp/tools/common/config_tool.py +9 -28
- hanzo_mcp/tools/common/context.py +1 -3
- hanzo_mcp/tools/common/critic_tool.py +1 -3
- hanzo_mcp/tools/common/decorators.py +2 -6
- hanzo_mcp/tools/common/enhanced_base.py +2 -6
- hanzo_mcp/tools/common/fastmcp_pagination.py +4 -12
- hanzo_mcp/tools/common/forgiving_edit.py +9 -28
- hanzo_mcp/tools/common/mode.py +1 -5
- hanzo_mcp/tools/common/paginated_base.py +3 -11
- hanzo_mcp/tools/common/paginated_response.py +10 -30
- hanzo_mcp/tools/common/pagination.py +3 -9
- hanzo_mcp/tools/common/path_utils.py +34 -0
- hanzo_mcp/tools/common/permissions.py +14 -13
- hanzo_mcp/tools/common/personality.py +983 -701
- hanzo_mcp/tools/common/plugin_loader.py +3 -15
- hanzo_mcp/tools/common/stats.py +6 -18
- hanzo_mcp/tools/common/thinking_tool.py +1 -3
- hanzo_mcp/tools/common/tool_disable.py +2 -6
- hanzo_mcp/tools/common/tool_list.py +2 -6
- hanzo_mcp/tools/common/validation.py +1 -3
- hanzo_mcp/tools/compiler/__init__.py +8 -0
- hanzo_mcp/tools/compiler/sandboxed_compiler.py +681 -0
- hanzo_mcp/tools/config/config_tool.py +7 -13
- hanzo_mcp/tools/config/index_config.py +1 -3
- hanzo_mcp/tools/config/mode_tool.py +5 -15
- hanzo_mcp/tools/database/database_manager.py +3 -9
- hanzo_mcp/tools/database/graph.py +1 -3
- hanzo_mcp/tools/database/graph_add.py +3 -9
- hanzo_mcp/tools/database/graph_query.py +11 -34
- hanzo_mcp/tools/database/graph_remove.py +3 -9
- hanzo_mcp/tools/database/graph_search.py +6 -20
- hanzo_mcp/tools/database/graph_stats.py +11 -33
- hanzo_mcp/tools/database/sql.py +4 -12
- hanzo_mcp/tools/database/sql_query.py +6 -10
- hanzo_mcp/tools/database/sql_search.py +2 -6
- hanzo_mcp/tools/database/sql_stats.py +5 -15
- hanzo_mcp/tools/editor/neovim_command.py +1 -3
- hanzo_mcp/tools/editor/neovim_session.py +7 -13
- hanzo_mcp/tools/environment/__init__.py +8 -0
- hanzo_mcp/tools/environment/environment_detector.py +594 -0
- hanzo_mcp/tools/filesystem/__init__.py +28 -26
- hanzo_mcp/tools/filesystem/ast_multi_edit.py +14 -43
- hanzo_mcp/tools/filesystem/ast_tool.py +3 -0
- hanzo_mcp/tools/filesystem/base.py +20 -12
- hanzo_mcp/tools/filesystem/content_replace.py +7 -12
- hanzo_mcp/tools/filesystem/diff.py +2 -10
- hanzo_mcp/tools/filesystem/directory_tree.py +285 -51
- hanzo_mcp/tools/filesystem/edit.py +10 -18
- hanzo_mcp/tools/filesystem/find.py +312 -179
- hanzo_mcp/tools/filesystem/git_search.py +12 -24
- hanzo_mcp/tools/filesystem/multi_edit.py +10 -18
- hanzo_mcp/tools/filesystem/read.py +14 -30
- hanzo_mcp/tools/filesystem/rules_tool.py +9 -17
- hanzo_mcp/tools/filesystem/search.py +1160 -0
- hanzo_mcp/tools/filesystem/watch.py +2 -4
- hanzo_mcp/tools/filesystem/write.py +7 -10
- hanzo_mcp/tools/framework/__init__.py +8 -0
- hanzo_mcp/tools/framework/framework_modes.py +714 -0
- hanzo_mcp/tools/jupyter/base.py +6 -20
- hanzo_mcp/tools/jupyter/jupyter.py +4 -12
- hanzo_mcp/tools/llm/consensus_tool.py +8 -24
- hanzo_mcp/tools/llm/llm_manage.py +2 -6
- hanzo_mcp/tools/llm/llm_tool.py +17 -58
- hanzo_mcp/tools/llm/llm_unified.py +18 -59
- hanzo_mcp/tools/llm/provider_tools.py +1 -3
- hanzo_mcp/tools/lsp/lsp_tool.py +621 -481
- hanzo_mcp/tools/mcp/mcp_add.py +1 -3
- hanzo_mcp/tools/mcp/mcp_stats.py +1 -3
- hanzo_mcp/tools/mcp/mcp_tool.py +9 -23
- hanzo_mcp/tools/memory/__init__.py +10 -27
- hanzo_mcp/tools/memory/conversation_memory.py +636 -0
- hanzo_mcp/tools/memory/knowledge_tools.py +7 -25
- hanzo_mcp/tools/memory/memory_tools.py +6 -18
- hanzo_mcp/tools/search/find_tool.py +12 -34
- hanzo_mcp/tools/search/unified_search.py +24 -78
- hanzo_mcp/tools/shell/__init__.py +16 -4
- hanzo_mcp/tools/shell/auto_background.py +2 -6
- hanzo_mcp/tools/shell/base.py +1 -5
- hanzo_mcp/tools/shell/base_process.py +5 -7
- hanzo_mcp/tools/shell/bash_session.py +7 -24
- hanzo_mcp/tools/shell/bash_session_executor.py +5 -15
- hanzo_mcp/tools/shell/bash_tool.py +3 -7
- hanzo_mcp/tools/shell/command_executor.py +26 -79
- hanzo_mcp/tools/shell/logs.py +4 -16
- hanzo_mcp/tools/shell/npx.py +2 -8
- hanzo_mcp/tools/shell/npx_tool.py +1 -3
- hanzo_mcp/tools/shell/pkill.py +4 -12
- hanzo_mcp/tools/shell/process_tool.py +2 -8
- hanzo_mcp/tools/shell/processes.py +5 -17
- hanzo_mcp/tools/shell/run_background.py +1 -3
- hanzo_mcp/tools/shell/run_command.py +1 -3
- hanzo_mcp/tools/shell/run_command_windows.py +1 -3
- hanzo_mcp/tools/shell/run_tool.py +56 -0
- hanzo_mcp/tools/shell/session_manager.py +2 -6
- hanzo_mcp/tools/shell/session_storage.py +2 -6
- hanzo_mcp/tools/shell/streaming_command.py +7 -23
- hanzo_mcp/tools/shell/uvx.py +4 -14
- hanzo_mcp/tools/shell/uvx_background.py +2 -6
- hanzo_mcp/tools/shell/uvx_tool.py +1 -3
- hanzo_mcp/tools/shell/zsh_tool.py +12 -20
- hanzo_mcp/tools/todo/todo.py +1 -3
- hanzo_mcp/tools/vector/__init__.py +97 -50
- hanzo_mcp/tools/vector/ast_analyzer.py +6 -20
- hanzo_mcp/tools/vector/git_ingester.py +10 -30
- hanzo_mcp/tools/vector/index_tool.py +3 -9
- hanzo_mcp/tools/vector/infinity_store.py +7 -27
- hanzo_mcp/tools/vector/mock_infinity.py +1 -3
- hanzo_mcp/tools/vector/node_tool.py +538 -0
- hanzo_mcp/tools/vector/project_manager.py +4 -12
- hanzo_mcp/tools/vector/unified_vector.py +384 -0
- hanzo_mcp/tools/vector/vector.py +2 -6
- hanzo_mcp/tools/vector/vector_index.py +8 -8
- hanzo_mcp/tools/vector/vector_search.py +7 -21
- {hanzo_mcp-0.8.11.dist-info → hanzo_mcp-0.9.0.dist-info}/METADATA +2 -2
- hanzo_mcp-0.9.0.dist-info/RECORD +191 -0
- hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +0 -645
- hanzo_mcp/tools/agent/swarm_tool.py +0 -718
- hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +0 -577
- hanzo_mcp/tools/filesystem/batch_search.py +0 -900
- hanzo_mcp/tools/filesystem/directory_tree_paginated.py +0 -350
- hanzo_mcp/tools/filesystem/find_files.py +0 -369
- hanzo_mcp/tools/filesystem/grep.py +0 -467
- hanzo_mcp/tools/filesystem/search_tool.py +0 -767
- hanzo_mcp/tools/filesystem/symbols_tool.py +0 -515
- hanzo_mcp/tools/filesystem/tree.py +0 -270
- hanzo_mcp/tools/jupyter/notebook_edit.py +0 -317
- hanzo_mcp/tools/jupyter/notebook_read.py +0 -147
- hanzo_mcp/tools/todo/todo_read.py +0 -143
- hanzo_mcp/tools/todo/todo_write.py +0 -374
- hanzo_mcp-0.8.11.dist-info/RECORD +0 -193
- {hanzo_mcp-0.8.11.dist-info → hanzo_mcp-0.9.0.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.8.11.dist-info → hanzo_mcp-0.9.0.dist-info}/entry_points.txt +0 -0
- {hanzo_mcp-0.8.11.dist-info → hanzo_mcp-0.9.0.dist-info}/top_level.txt +0 -0
|
@@ -9,25 +9,30 @@ from mcp.server import FastMCP
|
|
|
9
9
|
from hanzo_mcp.tools.common.base import BaseTool, ToolRegistry
|
|
10
10
|
from hanzo_mcp.tools.filesystem.diff import create_diff_tool
|
|
11
11
|
from hanzo_mcp.tools.filesystem.edit import Edit
|
|
12
|
-
from hanzo_mcp.tools.filesystem.grep import Grep
|
|
13
12
|
from hanzo_mcp.tools.filesystem.read import ReadTool
|
|
14
13
|
from hanzo_mcp.tools.filesystem.watch import watch_tool
|
|
15
14
|
from hanzo_mcp.tools.filesystem.write import Write
|
|
16
15
|
from hanzo_mcp.tools.common.permissions import PermissionManager
|
|
17
16
|
from hanzo_mcp.tools.filesystem.ast_tool import ASTTool
|
|
18
|
-
from hanzo_mcp.tools.filesystem.
|
|
17
|
+
from hanzo_mcp.tools.filesystem.find import FindTool
|
|
19
18
|
from hanzo_mcp.tools.filesystem.git_search import GitSearchTool
|
|
20
19
|
from hanzo_mcp.tools.filesystem.multi_edit import MultiEdit
|
|
21
20
|
from hanzo_mcp.tools.filesystem.rules_tool import RulesTool
|
|
22
|
-
from hanzo_mcp.tools.filesystem.search_tool import SearchTool
|
|
23
|
-
from hanzo_mcp.tools.filesystem.batch_search import BatchSearchTool
|
|
24
21
|
from hanzo_mcp.tools.filesystem.directory_tree import DirectoryTreeTool
|
|
25
22
|
from hanzo_mcp.tools.filesystem.content_replace import ContentReplaceTool
|
|
26
23
|
|
|
24
|
+
# Import unified search tool (which includes legacy Grep as alias)
|
|
25
|
+
from hanzo_mcp.tools.filesystem.search import (
|
|
26
|
+
UnifiedSearchTool,
|
|
27
|
+
Grep, # Legacy alias for backward compatibility
|
|
28
|
+
create_unified_search_tool,
|
|
29
|
+
create_grep_tool,
|
|
30
|
+
)
|
|
31
|
+
|
|
27
32
|
# Import new search tools
|
|
28
33
|
try:
|
|
29
34
|
from hanzo_mcp.tools.search import (
|
|
30
|
-
FindTool,
|
|
35
|
+
FindTool as SearchFindTool, # Rename to avoid conflict with filesystem FindTool
|
|
31
36
|
UnifiedSearch,
|
|
32
37
|
create_find_tool,
|
|
33
38
|
create_unified_search_tool,
|
|
@@ -44,14 +49,15 @@ __all__ = [
|
|
|
44
49
|
"Edit",
|
|
45
50
|
"MultiEdit",
|
|
46
51
|
"DirectoryTreeTool",
|
|
47
|
-
"Grep",
|
|
52
|
+
"Grep", # Legacy alias for UnifiedSearchTool
|
|
53
|
+
"UnifiedSearchTool", # New unified search tool
|
|
48
54
|
"ContentReplaceTool",
|
|
49
55
|
"ASTTool",
|
|
50
56
|
"GitSearchTool",
|
|
51
|
-
"
|
|
52
|
-
"FindFilesTool",
|
|
57
|
+
"FindTool",
|
|
53
58
|
"RulesTool",
|
|
54
|
-
"
|
|
59
|
+
"create_unified_search_tool",
|
|
60
|
+
"create_grep_tool",
|
|
55
61
|
"get_filesystem_tools",
|
|
56
62
|
"register_filesystem_tools",
|
|
57
63
|
]
|
|
@@ -76,15 +82,14 @@ def get_read_only_filesystem_tools(
|
|
|
76
82
|
Grep(permission_manager),
|
|
77
83
|
ASTTool(permission_manager),
|
|
78
84
|
GitSearchTool(permission_manager),
|
|
79
|
-
|
|
85
|
+
FindTool(permission_manager),
|
|
80
86
|
RulesTool(permission_manager),
|
|
81
87
|
watch_tool,
|
|
82
88
|
create_diff_tool(permission_manager),
|
|
83
89
|
]
|
|
84
90
|
|
|
85
|
-
# Add search
|
|
86
|
-
|
|
87
|
-
tools.append(SearchTool(permission_manager, project_manager))
|
|
91
|
+
# Add unified search tool (replaces old SearchTool and BatchSearchTool)
|
|
92
|
+
tools.append(UnifiedSearchTool(permission_manager, project_manager))
|
|
88
93
|
|
|
89
94
|
# Add new search tools if available
|
|
90
95
|
if UNIFIED_SEARCH_AVAILABLE:
|
|
@@ -93,9 +98,7 @@ def get_read_only_filesystem_tools(
|
|
|
93
98
|
return tools
|
|
94
99
|
|
|
95
100
|
|
|
96
|
-
def get_filesystem_tools(
|
|
97
|
-
permission_manager: PermissionManager, project_manager=None
|
|
98
|
-
) -> list[BaseTool]:
|
|
101
|
+
def get_filesystem_tools(permission_manager: PermissionManager, project_manager=None) -> list[BaseTool]:
|
|
99
102
|
"""Create instances of all filesystem tools.
|
|
100
103
|
|
|
101
104
|
Args:
|
|
@@ -115,15 +118,14 @@ def get_filesystem_tools(
|
|
|
115
118
|
ContentReplaceTool(permission_manager),
|
|
116
119
|
ASTTool(permission_manager),
|
|
117
120
|
GitSearchTool(permission_manager),
|
|
118
|
-
|
|
121
|
+
FindTool(permission_manager),
|
|
119
122
|
RulesTool(permission_manager),
|
|
120
123
|
watch_tool,
|
|
121
124
|
create_diff_tool(permission_manager),
|
|
122
125
|
]
|
|
123
126
|
|
|
124
|
-
# Add search
|
|
125
|
-
|
|
126
|
-
tools.append(SearchTool(permission_manager, project_manager))
|
|
127
|
+
# Add unified search tool (replaces old SearchTool and BatchSearchTool)
|
|
128
|
+
tools.append(UnifiedSearchTool(permission_manager, project_manager))
|
|
127
129
|
|
|
128
130
|
# Add new search tools if available
|
|
129
131
|
if UNIFIED_SEARCH_AVAILABLE:
|
|
@@ -160,14 +162,13 @@ def register_filesystem_tools(
|
|
|
160
162
|
"edit": Edit,
|
|
161
163
|
"multi_edit": MultiEdit,
|
|
162
164
|
"directory_tree": DirectoryTreeTool,
|
|
163
|
-
"grep": Grep,
|
|
165
|
+
"grep": Grep, # Legacy alias for UnifiedSearchTool
|
|
166
|
+
"search": UnifiedSearchTool, # New unified search tool (replaces SearchTool and BatchSearchTool)
|
|
164
167
|
"ast": ASTTool, # AST-based code structure search with tree-sitter
|
|
165
168
|
"git_search": GitSearchTool,
|
|
166
169
|
"content_replace": ContentReplaceTool,
|
|
167
|
-
"
|
|
168
|
-
"find_files": FindFilesTool,
|
|
170
|
+
"find": FindTool,
|
|
169
171
|
"rules": RulesTool,
|
|
170
|
-
"search": SearchTool,
|
|
171
172
|
"watch": lambda pm: watch_tool, # Singleton instance
|
|
172
173
|
"diff": create_diff_tool,
|
|
173
174
|
}
|
|
@@ -184,8 +185,8 @@ def register_filesystem_tools(
|
|
|
184
185
|
for tool_name, enabled in enabled_tools.items():
|
|
185
186
|
if enabled and tool_name in tool_classes:
|
|
186
187
|
tool_class = tool_classes[tool_name]
|
|
187
|
-
if tool_name
|
|
188
|
-
#
|
|
188
|
+
if tool_name == "search":
|
|
189
|
+
# Unified search tool requires project_manager
|
|
189
190
|
tools.append(tool_class(permission_manager, project_manager))
|
|
190
191
|
elif tool_name == "watch":
|
|
191
192
|
# Watch tool is a singleton
|
|
@@ -226,6 +227,7 @@ def register_filesystem_tools(
|
|
|
226
227
|
try:
|
|
227
228
|
ast_tool = next((t for t in tools if getattr(t, "name", "") == "ast"), None)
|
|
228
229
|
if ast_tool is not None:
|
|
230
|
+
|
|
229
231
|
class _SymbolsAlias(ASTTool): # type: ignore[misc]
|
|
230
232
|
@property
|
|
231
233
|
def name(self) -> str: # type: ignore[override]
|
|
@@ -129,9 +129,7 @@ class ASTMultiEdit(BaseTool):
|
|
|
129
129
|
|
|
130
130
|
return parser.parse(bytes(content, "utf-8"))
|
|
131
131
|
|
|
132
|
-
def _find_references(
|
|
133
|
-
self, symbol: str, file_path: str, project_root: Optional[str] = None
|
|
134
|
-
) -> List[ASTMatch]:
|
|
132
|
+
def _find_references(self, symbol: str, file_path: str, project_root: Optional[str] = None) -> List[ASTMatch]:
|
|
135
133
|
"""Find all references to a symbol across the project."""
|
|
136
134
|
matches = []
|
|
137
135
|
|
|
@@ -149,9 +147,7 @@ class ASTMultiEdit(BaseTool):
|
|
|
149
147
|
|
|
150
148
|
return matches
|
|
151
149
|
|
|
152
|
-
def _get_reference_patterns(
|
|
153
|
-
self, symbol: str, file_path: str
|
|
154
|
-
) -> List[Dict[str, Any]]:
|
|
150
|
+
def _get_reference_patterns(self, symbol: str, file_path: str) -> List[Dict[str, Any]]:
|
|
155
151
|
"""Get language-specific patterns for finding references."""
|
|
156
152
|
ext = Path(file_path).suffix.lower()
|
|
157
153
|
lang = self.languages.get(ext, "generic")
|
|
@@ -262,9 +258,7 @@ class ASTMultiEdit(BaseTool):
|
|
|
262
258
|
matches.extend(self._query_ast(tree, pattern, file_path, content))
|
|
263
259
|
else:
|
|
264
260
|
# Fallback to text search
|
|
265
|
-
matches.extend(
|
|
266
|
-
self._text_search(content, pattern["query"], file_path)
|
|
267
|
-
)
|
|
261
|
+
matches.extend(self._text_search(content, pattern["query"], file_path))
|
|
268
262
|
|
|
269
263
|
except Exception:
|
|
270
264
|
continue
|
|
@@ -313,9 +307,7 @@ class ASTMultiEdit(BaseTool):
|
|
|
313
307
|
|
|
314
308
|
return matches
|
|
315
309
|
|
|
316
|
-
def _get_parent_context(
|
|
317
|
-
self, node: tree_sitter.Node, content: str
|
|
318
|
-
) -> Optional[str]:
|
|
310
|
+
def _get_parent_context(self, node: tree_sitter.Node, content: str) -> Optional[str]:
|
|
319
311
|
"""Get parent context for better understanding."""
|
|
320
312
|
parent = node.parent
|
|
321
313
|
if parent:
|
|
@@ -335,9 +327,7 @@ class ASTMultiEdit(BaseTool):
|
|
|
335
327
|
|
|
336
328
|
return None
|
|
337
329
|
|
|
338
|
-
def _text_search(
|
|
339
|
-
self, content: str, pattern: str, file_path: str
|
|
340
|
-
) -> List[ASTMatch]:
|
|
330
|
+
def _text_search(self, content: str, pattern: str, file_path: str) -> List[ASTMatch]:
|
|
341
331
|
"""Fallback text search."""
|
|
342
332
|
matches = []
|
|
343
333
|
lines = content.split("\n")
|
|
@@ -412,18 +402,14 @@ class ASTMultiEdit(BaseTool):
|
|
|
412
402
|
|
|
413
403
|
return str(path.parent)
|
|
414
404
|
|
|
415
|
-
def _group_matches_by_file(
|
|
416
|
-
self, matches: List[ASTMatch]
|
|
417
|
-
) -> Dict[str, List[ASTMatch]]:
|
|
405
|
+
def _group_matches_by_file(self, matches: List[ASTMatch]) -> Dict[str, List[ASTMatch]]:
|
|
418
406
|
"""Group matches by file for efficient editing."""
|
|
419
407
|
grouped = defaultdict(list)
|
|
420
408
|
for match in matches:
|
|
421
409
|
grouped[match.file_path].append(match)
|
|
422
410
|
return grouped
|
|
423
411
|
|
|
424
|
-
def _create_unique_context(
|
|
425
|
-
self, content: str, match: ASTMatch, context_lines: int
|
|
426
|
-
) -> str:
|
|
412
|
+
def _create_unique_context(self, content: str, match: ASTMatch, context_lines: int) -> str:
|
|
427
413
|
"""Create unique context for edit identification."""
|
|
428
414
|
lines = content.split("\n")
|
|
429
415
|
|
|
@@ -499,27 +485,20 @@ class ASTMultiEdit(BaseTool):
|
|
|
499
485
|
pattern = {"query": edit_op.old_string, "type": "text"}
|
|
500
486
|
matches = self._query_ast(tree, pattern, file_path, content)
|
|
501
487
|
else:
|
|
502
|
-
matches = self._text_search(
|
|
503
|
-
content, edit_op.old_string, file_path
|
|
504
|
-
)
|
|
488
|
+
matches = self._text_search(content, edit_op.old_string, file_path)
|
|
505
489
|
|
|
506
490
|
# Filter by node types if specified
|
|
507
491
|
if edit_op.node_types:
|
|
508
492
|
matches = [m for m in matches if m.node_type in edit_op.node_types]
|
|
509
493
|
|
|
510
494
|
# Check expected count
|
|
511
|
-
if (
|
|
512
|
-
edit_op.expect_count is not None
|
|
513
|
-
and len(matches) != edit_op.expect_count
|
|
514
|
-
):
|
|
495
|
+
if edit_op.expect_count is not None and len(matches) != edit_op.expect_count:
|
|
515
496
|
results["errors"].append(
|
|
516
497
|
{
|
|
517
498
|
"edit": edit_op.old_string,
|
|
518
499
|
"expected": edit_op.expect_count,
|
|
519
500
|
"found": len(matches),
|
|
520
|
-
"locations": [
|
|
521
|
-
f"{m.file_path}:{m.line_start}" for m in matches[:5]
|
|
522
|
-
],
|
|
501
|
+
"locations": [f"{m.file_path}:{m.line_start}" for m in matches[:5]],
|
|
523
502
|
}
|
|
524
503
|
)
|
|
525
504
|
continue
|
|
@@ -547,9 +526,7 @@ class ASTMultiEdit(BaseTool):
|
|
|
547
526
|
success = await self._apply_file_changes(file_path, changes)
|
|
548
527
|
if success:
|
|
549
528
|
results["edits_applied"] += len(changes)
|
|
550
|
-
results["changes"].append(
|
|
551
|
-
{"file": file_path, "edits": len(changes)}
|
|
552
|
-
)
|
|
529
|
+
results["changes"].append({"file": file_path, "edits": len(changes)})
|
|
553
530
|
except Exception as e:
|
|
554
531
|
results["errors"].append({"file": file_path, "error": str(e)})
|
|
555
532
|
|
|
@@ -564,9 +541,7 @@ class ASTMultiEdit(BaseTool):
|
|
|
564
541
|
grouped[match.file_path].append((edit_op, match))
|
|
565
542
|
return grouped
|
|
566
543
|
|
|
567
|
-
async def _apply_file_changes(
|
|
568
|
-
self, file_path: str, changes: List[Tuple[EditOperation, ASTMatch]]
|
|
569
|
-
) -> bool:
|
|
544
|
+
async def _apply_file_changes(self, file_path: str, changes: List[Tuple[EditOperation, ASTMatch]]) -> bool:
|
|
570
545
|
"""Apply changes to a single file."""
|
|
571
546
|
with open(file_path, "r", encoding="utf-8") as f:
|
|
572
547
|
content = f.read()
|
|
@@ -600,9 +575,7 @@ class ASTMultiEdit(BaseTool):
|
|
|
600
575
|
|
|
601
576
|
return True
|
|
602
577
|
|
|
603
|
-
def _generate_preview(
|
|
604
|
-
self, matches: List[Tuple[EditOperation, ASTMatch]], page_size: int
|
|
605
|
-
) -> List[Dict[str, Any]]:
|
|
578
|
+
def _generate_preview(self, matches: List[Tuple[EditOperation, ASTMatch]], page_size: int) -> List[Dict[str, Any]]:
|
|
606
579
|
"""Generate preview of changes."""
|
|
607
580
|
preview = []
|
|
608
581
|
|
|
@@ -625,9 +598,7 @@ class ASTMultiEdit(BaseTool):
|
|
|
625
598
|
|
|
626
599
|
return preview
|
|
627
600
|
|
|
628
|
-
def _fallback_to_basic_edit(
|
|
629
|
-
self, file_path: str, edits: List[Dict[str, Any]]
|
|
630
|
-
) -> MCPResourceDocument:
|
|
601
|
+
def _fallback_to_basic_edit(self, file_path: str, edits: List[Dict[str, Any]]) -> MCPResourceDocument:
|
|
631
602
|
"""Fallback to basic multi-edit when treesitter not available."""
|
|
632
603
|
# Delegate to existing multi_edit tool
|
|
633
604
|
from hanzo_mcp.tools.filesystem.multi_edit import MultiEdit
|
|
@@ -117,6 +117,9 @@ Searches code structure intelligently, understanding syntax and providing semant
|
|
|
117
117
|
# Extract parameters
|
|
118
118
|
pattern: Pattern = params["pattern"]
|
|
119
119
|
path: SearchPath = params["path"]
|
|
120
|
+
|
|
121
|
+
# Expand path (handles ~, $HOME, etc.)
|
|
122
|
+
path = self.expand_path(path)
|
|
120
123
|
ignore_case = params.get("ignore_case", False)
|
|
121
124
|
line_number = params.get("line_number", False)
|
|
122
125
|
|
|
@@ -11,6 +11,7 @@ from pathlib import Path
|
|
|
11
11
|
from mcp.server.fastmcp import Context as MCPContext
|
|
12
12
|
|
|
13
13
|
from hanzo_mcp.tools.common.base import FileSystemTool
|
|
14
|
+
from hanzo_mcp.tools.common.path_utils import resolve_path
|
|
14
15
|
from hanzo_mcp.tools.common.context import ToolContext, create_tool_context
|
|
15
16
|
|
|
16
17
|
|
|
@@ -21,9 +22,22 @@ class FilesystemBaseTool(FileSystemTool, ABC):
|
|
|
21
22
|
the base functionality in FileSystemTool.
|
|
22
23
|
"""
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
@staticmethod
|
|
26
|
+
def expand_path(path: str) -> str:
|
|
27
|
+
"""Expand user home directory (~) and environment variables in a path.
|
|
28
|
+
|
|
29
|
+
This method uses the centralized resolve_path function to ensure
|
|
30
|
+
consistent path resolution across all tools.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
path: The path to expand
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
The fully resolved absolute path
|
|
37
|
+
"""
|
|
38
|
+
return resolve_path(path)
|
|
39
|
+
|
|
40
|
+
async def check_path_allowed(self, path: str, tool_ctx: Any, error_prefix: str = "Error") -> tuple[bool, str]:
|
|
27
41
|
"""Check if a path is allowed and log an error if not.
|
|
28
42
|
|
|
29
43
|
Args:
|
|
@@ -40,9 +54,7 @@ class FilesystemBaseTool(FileSystemTool, ABC):
|
|
|
40
54
|
return False, f"{error_prefix}: {message}"
|
|
41
55
|
return True, ""
|
|
42
56
|
|
|
43
|
-
async def check_path_exists(
|
|
44
|
-
self, path: str, tool_ctx: Any, error_prefix: str = "Error"
|
|
45
|
-
) -> tuple[bool, str]:
|
|
57
|
+
async def check_path_exists(self, path: str, tool_ctx: Any, error_prefix: str = "Error") -> tuple[bool, str]:
|
|
46
58
|
"""Check if a path exists and log an error if not.
|
|
47
59
|
|
|
48
60
|
Args:
|
|
@@ -60,9 +72,7 @@ class FilesystemBaseTool(FileSystemTool, ABC):
|
|
|
60
72
|
return False, f"{error_prefix}: {message}"
|
|
61
73
|
return True, ""
|
|
62
74
|
|
|
63
|
-
async def check_is_file(
|
|
64
|
-
self, path: str, tool_ctx: Any, error_prefix: str = "Error"
|
|
65
|
-
) -> tuple[bool, str]:
|
|
75
|
+
async def check_is_file(self, path: str, tool_ctx: Any, error_prefix: str = "Error") -> tuple[bool, str]:
|
|
66
76
|
"""Check if a path is a file and log an error if not.
|
|
67
77
|
|
|
68
78
|
Args:
|
|
@@ -80,9 +90,7 @@ class FilesystemBaseTool(FileSystemTool, ABC):
|
|
|
80
90
|
return False, f"{error_prefix}: {message}"
|
|
81
91
|
return True, ""
|
|
82
92
|
|
|
83
|
-
async def check_is_directory(
|
|
84
|
-
self, path: str, tool_ctx: Any, error_prefix: str = "Error"
|
|
85
|
-
) -> tuple[bool, str]:
|
|
93
|
+
async def check_is_directory(self, path: str, tool_ctx: Any, error_prefix: str = "Error") -> tuple[bool, str]:
|
|
86
94
|
"""Check if a path is a directory and log an error if not.
|
|
87
95
|
|
|
88
96
|
Args:
|
|
@@ -121,6 +121,9 @@ Only works within allowed directories."""
|
|
|
121
121
|
pattern: Pattern = params["pattern"]
|
|
122
122
|
replacement: Replacement = params["replacement"]
|
|
123
123
|
path: SearchPath = params["path"]
|
|
124
|
+
|
|
125
|
+
# Expand path (handles ~, $HOME, etc.)
|
|
126
|
+
path = self.expand_path(path)
|
|
124
127
|
file_pattern = params.get("file_pattern", "*") # Default to all files
|
|
125
128
|
dry_run = params.get("dry_run", False) # Default to False
|
|
126
129
|
|
|
@@ -159,15 +162,11 @@ Only works within allowed directories."""
|
|
|
159
162
|
# Process based on whether path is a file or directory
|
|
160
163
|
if input_path.is_file():
|
|
161
164
|
# Single file search
|
|
162
|
-
if file_pattern == "*" or fnmatch.fnmatch(
|
|
163
|
-
input_path.name, file_pattern
|
|
164
|
-
):
|
|
165
|
+
if file_pattern == "*" or fnmatch.fnmatch(input_path.name, file_pattern):
|
|
165
166
|
matching_files.append(input_path)
|
|
166
167
|
await tool_ctx.info(f"Searching single file: {path}")
|
|
167
168
|
else:
|
|
168
|
-
await tool_ctx.info(
|
|
169
|
-
f"File does not match pattern '{file_pattern}': {path}"
|
|
170
|
-
)
|
|
169
|
+
await tool_ctx.info(f"File does not match pattern '{file_pattern}': {path}")
|
|
171
170
|
return f"File does not match pattern '{file_pattern}': {path}"
|
|
172
171
|
elif input_path.is_dir():
|
|
173
172
|
# Directory search - optimized file finding
|
|
@@ -186,9 +185,7 @@ Only works within allowed directories."""
|
|
|
186
185
|
for entry in input_path.rglob("*"):
|
|
187
186
|
entry_path = str(entry)
|
|
188
187
|
if entry_path in allowed_paths and entry.is_file():
|
|
189
|
-
if file_pattern == "*" or fnmatch.fnmatch(
|
|
190
|
-
entry.name, file_pattern
|
|
191
|
-
):
|
|
188
|
+
if file_pattern == "*" or fnmatch.fnmatch(entry.name, file_pattern):
|
|
192
189
|
matching_files.append(entry)
|
|
193
190
|
|
|
194
191
|
await tool_ctx.info(f"Found {len(matching_files)} matching files")
|
|
@@ -251,9 +248,7 @@ Only works within allowed directories."""
|
|
|
251
248
|
)
|
|
252
249
|
message = f"Dry run: {replacements_made} replacements of '{pattern}' with '{replacement}' would be made in {files_modified} files:"
|
|
253
250
|
else:
|
|
254
|
-
await tool_ctx.info(
|
|
255
|
-
f"Made {replacements_made} replacements in {files_modified} files"
|
|
256
|
-
)
|
|
251
|
+
await tool_ctx.info(f"Made {replacements_made} replacements in {files_modified} files")
|
|
257
252
|
message = f"Made {replacements_made} replacements of '{pattern}' with '{replacement}' in {files_modified} files:"
|
|
258
253
|
|
|
259
254
|
return message + "\n\n" + "\n".join(results)
|
|
@@ -166,16 +166,8 @@ diff a.json b.json --ignore-whitespace"""
|
|
|
166
166
|
output.extend(diff_lines)
|
|
167
167
|
|
|
168
168
|
# Add summary
|
|
169
|
-
additions = sum(
|
|
170
|
-
|
|
171
|
-
for line in diff_lines
|
|
172
|
-
if line.startswith("+") and not line.startswith("+++")
|
|
173
|
-
)
|
|
174
|
-
deletions = sum(
|
|
175
|
-
1
|
|
176
|
-
for line in diff_lines
|
|
177
|
-
if line.startswith("-") and not line.startswith("---")
|
|
178
|
-
)
|
|
169
|
+
additions = sum(1 for line in diff_lines if line.startswith("+") and not line.startswith("+++"))
|
|
170
|
+
deletions = sum(1 for line in diff_lines if line.startswith("-") and not line.startswith("---"))
|
|
179
171
|
|
|
180
172
|
output.append("")
|
|
181
173
|
output.append("=" * 60)
|