hanzo-mcp 0.8.8__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.

Files changed (167) hide show
  1. hanzo_mcp/__init__.py +1 -3
  2. hanzo_mcp/analytics/posthog_analytics.py +4 -17
  3. hanzo_mcp/bridge.py +9 -25
  4. hanzo_mcp/cli.py +8 -17
  5. hanzo_mcp/cli_enhanced.py +5 -14
  6. hanzo_mcp/cli_plugin.py +3 -9
  7. hanzo_mcp/config/settings.py +6 -20
  8. hanzo_mcp/config/tool_config.py +2 -4
  9. hanzo_mcp/core/base_agent.py +88 -88
  10. hanzo_mcp/core/model_registry.py +238 -210
  11. hanzo_mcp/dev_server.py +5 -15
  12. hanzo_mcp/prompts/__init__.py +2 -6
  13. hanzo_mcp/prompts/project_todo_reminder.py +3 -9
  14. hanzo_mcp/prompts/tool_explorer.py +1 -3
  15. hanzo_mcp/prompts/utils.py +7 -21
  16. hanzo_mcp/server.py +6 -7
  17. hanzo_mcp/tools/__init__.py +29 -32
  18. hanzo_mcp/tools/agent/__init__.py +2 -1
  19. hanzo_mcp/tools/agent/agent.py +10 -30
  20. hanzo_mcp/tools/agent/agent_tool.py +23 -17
  21. hanzo_mcp/tools/agent/claude_desktop_auth.py +3 -9
  22. hanzo_mcp/tools/agent/cli_agent_base.py +7 -24
  23. hanzo_mcp/tools/agent/cli_tools.py +76 -75
  24. hanzo_mcp/tools/agent/code_auth.py +1 -3
  25. hanzo_mcp/tools/agent/code_auth_tool.py +2 -6
  26. hanzo_mcp/tools/agent/critic_tool.py +8 -24
  27. hanzo_mcp/tools/agent/iching_tool.py +12 -36
  28. hanzo_mcp/tools/agent/network_tool.py +7 -18
  29. hanzo_mcp/tools/agent/prompt.py +1 -5
  30. hanzo_mcp/tools/agent/review_tool.py +10 -25
  31. hanzo_mcp/tools/agent/swarm_alias.py +1 -3
  32. hanzo_mcp/tools/agent/unified_cli_tools.py +38 -38
  33. hanzo_mcp/tools/common/batch_tool.py +15 -45
  34. hanzo_mcp/tools/common/config_tool.py +9 -28
  35. hanzo_mcp/tools/common/context.py +1 -3
  36. hanzo_mcp/tools/common/critic_tool.py +1 -3
  37. hanzo_mcp/tools/common/decorators.py +2 -6
  38. hanzo_mcp/tools/common/enhanced_base.py +2 -6
  39. hanzo_mcp/tools/common/fastmcp_pagination.py +4 -12
  40. hanzo_mcp/tools/common/forgiving_edit.py +9 -28
  41. hanzo_mcp/tools/common/mode.py +1 -5
  42. hanzo_mcp/tools/common/paginated_base.py +3 -11
  43. hanzo_mcp/tools/common/paginated_response.py +10 -30
  44. hanzo_mcp/tools/common/pagination.py +3 -9
  45. hanzo_mcp/tools/common/path_utils.py +34 -0
  46. hanzo_mcp/tools/common/permissions.py +14 -13
  47. hanzo_mcp/tools/common/personality.py +983 -701
  48. hanzo_mcp/tools/common/plugin_loader.py +3 -15
  49. hanzo_mcp/tools/common/stats.py +7 -19
  50. hanzo_mcp/tools/common/thinking_tool.py +1 -3
  51. hanzo_mcp/tools/common/tool_disable.py +2 -6
  52. hanzo_mcp/tools/common/tool_list.py +2 -6
  53. hanzo_mcp/tools/common/validation.py +1 -3
  54. hanzo_mcp/tools/compiler/__init__.py +8 -0
  55. hanzo_mcp/tools/compiler/sandboxed_compiler.py +681 -0
  56. hanzo_mcp/tools/config/config_tool.py +7 -13
  57. hanzo_mcp/tools/config/index_config.py +1 -3
  58. hanzo_mcp/tools/config/mode_tool.py +5 -15
  59. hanzo_mcp/tools/database/database_manager.py +3 -9
  60. hanzo_mcp/tools/database/graph.py +1 -3
  61. hanzo_mcp/tools/database/graph_add.py +3 -9
  62. hanzo_mcp/tools/database/graph_query.py +11 -34
  63. hanzo_mcp/tools/database/graph_remove.py +3 -9
  64. hanzo_mcp/tools/database/graph_search.py +6 -20
  65. hanzo_mcp/tools/database/graph_stats.py +11 -33
  66. hanzo_mcp/tools/database/sql.py +4 -12
  67. hanzo_mcp/tools/database/sql_query.py +6 -10
  68. hanzo_mcp/tools/database/sql_search.py +2 -6
  69. hanzo_mcp/tools/database/sql_stats.py +5 -15
  70. hanzo_mcp/tools/editor/neovim_command.py +1 -3
  71. hanzo_mcp/tools/editor/neovim_session.py +7 -13
  72. hanzo_mcp/tools/environment/__init__.py +8 -0
  73. hanzo_mcp/tools/environment/environment_detector.py +594 -0
  74. hanzo_mcp/tools/filesystem/__init__.py +28 -26
  75. hanzo_mcp/tools/filesystem/ast_multi_edit.py +14 -43
  76. hanzo_mcp/tools/filesystem/ast_tool.py +3 -0
  77. hanzo_mcp/tools/filesystem/base.py +20 -12
  78. hanzo_mcp/tools/filesystem/content_replace.py +7 -12
  79. hanzo_mcp/tools/filesystem/diff.py +2 -10
  80. hanzo_mcp/tools/filesystem/directory_tree.py +285 -51
  81. hanzo_mcp/tools/filesystem/edit.py +10 -18
  82. hanzo_mcp/tools/filesystem/find.py +312 -179
  83. hanzo_mcp/tools/filesystem/git_search.py +12 -24
  84. hanzo_mcp/tools/filesystem/multi_edit.py +10 -18
  85. hanzo_mcp/tools/filesystem/read.py +14 -30
  86. hanzo_mcp/tools/filesystem/rules_tool.py +9 -17
  87. hanzo_mcp/tools/filesystem/search.py +1160 -0
  88. hanzo_mcp/tools/filesystem/watch.py +2 -4
  89. hanzo_mcp/tools/filesystem/write.py +7 -10
  90. hanzo_mcp/tools/framework/__init__.py +8 -0
  91. hanzo_mcp/tools/framework/framework_modes.py +714 -0
  92. hanzo_mcp/tools/jupyter/base.py +6 -20
  93. hanzo_mcp/tools/jupyter/jupyter.py +4 -12
  94. hanzo_mcp/tools/llm/consensus_tool.py +8 -24
  95. hanzo_mcp/tools/llm/llm_manage.py +2 -6
  96. hanzo_mcp/tools/llm/llm_tool.py +17 -58
  97. hanzo_mcp/tools/llm/llm_unified.py +18 -59
  98. hanzo_mcp/tools/llm/provider_tools.py +1 -3
  99. hanzo_mcp/tools/lsp/lsp_tool.py +621 -481
  100. hanzo_mcp/tools/mcp/mcp_add.py +3 -5
  101. hanzo_mcp/tools/mcp/mcp_remove.py +1 -1
  102. hanzo_mcp/tools/mcp/mcp_stats.py +1 -3
  103. hanzo_mcp/tools/mcp/mcp_tool.py +9 -23
  104. hanzo_mcp/tools/memory/__init__.py +33 -40
  105. hanzo_mcp/tools/memory/conversation_memory.py +636 -0
  106. hanzo_mcp/tools/memory/knowledge_tools.py +7 -25
  107. hanzo_mcp/tools/memory/memory_tools.py +7 -19
  108. hanzo_mcp/tools/search/find_tool.py +12 -34
  109. hanzo_mcp/tools/search/unified_search.py +27 -81
  110. hanzo_mcp/tools/shell/__init__.py +16 -4
  111. hanzo_mcp/tools/shell/auto_background.py +2 -6
  112. hanzo_mcp/tools/shell/base.py +1 -5
  113. hanzo_mcp/tools/shell/base_process.py +5 -7
  114. hanzo_mcp/tools/shell/bash_session.py +7 -24
  115. hanzo_mcp/tools/shell/bash_session_executor.py +5 -15
  116. hanzo_mcp/tools/shell/bash_tool.py +3 -7
  117. hanzo_mcp/tools/shell/command_executor.py +26 -79
  118. hanzo_mcp/tools/shell/logs.py +4 -16
  119. hanzo_mcp/tools/shell/npx.py +2 -8
  120. hanzo_mcp/tools/shell/npx_tool.py +1 -3
  121. hanzo_mcp/tools/shell/pkill.py +4 -12
  122. hanzo_mcp/tools/shell/process_tool.py +2 -8
  123. hanzo_mcp/tools/shell/processes.py +5 -17
  124. hanzo_mcp/tools/shell/run_background.py +1 -3
  125. hanzo_mcp/tools/shell/run_command.py +1 -3
  126. hanzo_mcp/tools/shell/run_command_windows.py +1 -3
  127. hanzo_mcp/tools/shell/run_tool.py +56 -0
  128. hanzo_mcp/tools/shell/session_manager.py +2 -6
  129. hanzo_mcp/tools/shell/session_storage.py +2 -6
  130. hanzo_mcp/tools/shell/streaming_command.py +7 -23
  131. hanzo_mcp/tools/shell/uvx.py +4 -14
  132. hanzo_mcp/tools/shell/uvx_background.py +2 -6
  133. hanzo_mcp/tools/shell/uvx_tool.py +1 -3
  134. hanzo_mcp/tools/shell/zsh_tool.py +12 -20
  135. hanzo_mcp/tools/todo/todo.py +1 -3
  136. hanzo_mcp/tools/vector/__init__.py +97 -50
  137. hanzo_mcp/tools/vector/ast_analyzer.py +6 -20
  138. hanzo_mcp/tools/vector/git_ingester.py +10 -30
  139. hanzo_mcp/tools/vector/index_tool.py +3 -9
  140. hanzo_mcp/tools/vector/infinity_store.py +11 -30
  141. hanzo_mcp/tools/vector/mock_infinity.py +159 -0
  142. hanzo_mcp/tools/vector/node_tool.py +538 -0
  143. hanzo_mcp/tools/vector/project_manager.py +4 -12
  144. hanzo_mcp/tools/vector/unified_vector.py +384 -0
  145. hanzo_mcp/tools/vector/vector.py +2 -6
  146. hanzo_mcp/tools/vector/vector_index.py +8 -8
  147. hanzo_mcp/tools/vector/vector_search.py +7 -21
  148. {hanzo_mcp-0.8.8.dist-info → hanzo_mcp-0.9.0.dist-info}/METADATA +2 -2
  149. hanzo_mcp-0.9.0.dist-info/RECORD +191 -0
  150. hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +0 -645
  151. hanzo_mcp/tools/agent/swarm_tool.py +0 -723
  152. hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +0 -577
  153. hanzo_mcp/tools/filesystem/batch_search.py +0 -900
  154. hanzo_mcp/tools/filesystem/directory_tree_paginated.py +0 -350
  155. hanzo_mcp/tools/filesystem/find_files.py +0 -369
  156. hanzo_mcp/tools/filesystem/grep.py +0 -467
  157. hanzo_mcp/tools/filesystem/search_tool.py +0 -767
  158. hanzo_mcp/tools/filesystem/symbols_tool.py +0 -515
  159. hanzo_mcp/tools/filesystem/tree.py +0 -270
  160. hanzo_mcp/tools/jupyter/notebook_edit.py +0 -317
  161. hanzo_mcp/tools/jupyter/notebook_read.py +0 -147
  162. hanzo_mcp/tools/todo/todo_read.py +0 -143
  163. hanzo_mcp/tools/todo/todo_write.py +0 -374
  164. hanzo_mcp-0.8.8.dist-info/RECORD +0 -192
  165. {hanzo_mcp-0.8.8.dist-info → hanzo_mcp-0.9.0.dist-info}/WHEEL +0 -0
  166. {hanzo_mcp-0.8.8.dist-info → hanzo_mcp-0.9.0.dist-info}/entry_points.txt +0 -0
  167. {hanzo_mcp-0.8.8.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.find_files import FindFilesTool
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
- "BatchSearchTool",
52
- "FindFilesTool",
57
+ "FindTool",
53
58
  "RulesTool",
54
- "SearchTool",
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
- FindFilesTool(permission_manager),
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 if project manager is available
86
- if project_manager:
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
- FindFilesTool(permission_manager),
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 if project manager is available
125
- if project_manager:
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
- "batch_search": BatchSearchTool,
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 in ["batch_search", "search"]:
188
- # Batch search and search require project_manager
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
- async def check_path_allowed(
25
- self, path: str, tool_ctx: Any, error_prefix: str = "Error"
26
- ) -> tuple[bool, str]:
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
- 1
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)