hanzo-mcp 0.6.12__py3-none-any.whl → 0.6.13__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 (77) hide show
  1. hanzo_mcp/__init__.py +2 -2
  2. hanzo_mcp/cli.py +2 -2
  3. hanzo_mcp/cli_enhanced.py +4 -4
  4. hanzo_mcp/cli_plugin.py +91 -0
  5. hanzo_mcp/config/__init__.py +1 -1
  6. hanzo_mcp/config/settings.py +69 -6
  7. hanzo_mcp/config/tool_config.py +2 -2
  8. hanzo_mcp/dev_server.py +3 -3
  9. hanzo_mcp/prompts/project_system.py +1 -1
  10. hanzo_mcp/server.py +6 -2
  11. hanzo_mcp/server_enhanced.py +69 -0
  12. hanzo_mcp/tools/__init__.py +75 -29
  13. hanzo_mcp/tools/agent/__init__.py +1 -1
  14. hanzo_mcp/tools/agent/agent_tool.py +2 -2
  15. hanzo_mcp/tools/common/__init__.py +15 -1
  16. hanzo_mcp/tools/common/base.py +4 -4
  17. hanzo_mcp/tools/common/batch_tool.py +1 -1
  18. hanzo_mcp/tools/common/config_tool.py +2 -2
  19. hanzo_mcp/tools/common/context.py +2 -2
  20. hanzo_mcp/tools/common/context_fix.py +26 -0
  21. hanzo_mcp/tools/common/critic_tool.py +196 -0
  22. hanzo_mcp/tools/common/decorators.py +208 -0
  23. hanzo_mcp/tools/common/enhanced_base.py +106 -0
  24. hanzo_mcp/tools/common/mode.py +116 -0
  25. hanzo_mcp/tools/common/mode_loader.py +105 -0
  26. hanzo_mcp/tools/common/permissions.py +1 -1
  27. hanzo_mcp/tools/common/personality.py +936 -0
  28. hanzo_mcp/tools/common/plugin_loader.py +287 -0
  29. hanzo_mcp/tools/common/stats.py +4 -4
  30. hanzo_mcp/tools/common/tool_list.py +1 -1
  31. hanzo_mcp/tools/common/validation.py +1 -1
  32. hanzo_mcp/tools/config/__init__.py +3 -1
  33. hanzo_mcp/tools/config/config_tool.py +1 -1
  34. hanzo_mcp/tools/config/mode_tool.py +209 -0
  35. hanzo_mcp/tools/database/__init__.py +1 -1
  36. hanzo_mcp/tools/editor/__init__.py +1 -1
  37. hanzo_mcp/tools/filesystem/__init__.py +19 -14
  38. hanzo_mcp/tools/filesystem/batch_search.py +3 -3
  39. hanzo_mcp/tools/filesystem/diff.py +2 -2
  40. hanzo_mcp/tools/filesystem/rules_tool.py +235 -0
  41. hanzo_mcp/tools/filesystem/{unified_search.py → search_tool.py} +12 -12
  42. hanzo_mcp/tools/filesystem/{symbols_unified.py → symbols_tool.py} +104 -5
  43. hanzo_mcp/tools/filesystem/watch.py +3 -2
  44. hanzo_mcp/tools/jupyter/__init__.py +2 -2
  45. hanzo_mcp/tools/jupyter/jupyter.py +1 -1
  46. hanzo_mcp/tools/llm/__init__.py +3 -3
  47. hanzo_mcp/tools/llm/llm_tool.py +648 -143
  48. hanzo_mcp/tools/mcp/__init__.py +2 -2
  49. hanzo_mcp/tools/mcp/{mcp_unified.py → mcp_tool.py} +3 -3
  50. hanzo_mcp/tools/shell/__init__.py +6 -6
  51. hanzo_mcp/tools/shell/base_process.py +4 -2
  52. hanzo_mcp/tools/shell/bash_session_executor.py +1 -1
  53. hanzo_mcp/tools/shell/{bash_unified.py → bash_tool.py} +1 -1
  54. hanzo_mcp/tools/shell/command_executor.py +2 -2
  55. hanzo_mcp/tools/shell/{npx_unified.py → npx_tool.py} +1 -1
  56. hanzo_mcp/tools/shell/open.py +2 -2
  57. hanzo_mcp/tools/shell/{process_unified.py → process_tool.py} +1 -1
  58. hanzo_mcp/tools/shell/run_command_windows.py +1 -1
  59. hanzo_mcp/tools/shell/uvx.py +47 -2
  60. hanzo_mcp/tools/shell/uvx_background.py +47 -2
  61. hanzo_mcp/tools/shell/{uvx_unified.py → uvx_tool.py} +1 -1
  62. hanzo_mcp/tools/todo/__init__.py +14 -19
  63. hanzo_mcp/tools/todo/todo.py +22 -1
  64. hanzo_mcp/tools/vector/__init__.py +1 -1
  65. hanzo_mcp/tools/vector/infinity_store.py +2 -2
  66. hanzo_mcp/tools/vector/project_manager.py +1 -1
  67. hanzo_mcp-0.6.13.dist-info/METADATA +359 -0
  68. {hanzo_mcp-0.6.12.dist-info → hanzo_mcp-0.6.13.dist-info}/RECORD +72 -64
  69. {hanzo_mcp-0.6.12.dist-info → hanzo_mcp-0.6.13.dist-info}/entry_points.txt +1 -0
  70. hanzo_mcp/tools/common/palette.py +0 -344
  71. hanzo_mcp/tools/common/palette_loader.py +0 -108
  72. hanzo_mcp/tools/config/palette_tool.py +0 -179
  73. hanzo_mcp/tools/llm/llm_unified.py +0 -851
  74. hanzo_mcp-0.6.12.dist-info/METADATA +0 -339
  75. {hanzo_mcp-0.6.12.dist-info → hanzo_mcp-0.6.13.dist-info}/WHEEL +0 -0
  76. {hanzo_mcp-0.6.12.dist-info → hanzo_mcp-0.6.13.dist-info}/licenses/LICENSE +0 -0
  77. {hanzo_mcp-0.6.12.dist-info → hanzo_mcp-0.6.13.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,4 @@
1
- """Filesystem tools package for Hanzo MCP.
1
+ """Filesystem tools package for Hanzo AI.
2
2
 
3
3
  This package provides tools for interacting with the filesystem, including reading, writing,
4
4
  and editing files, directory navigation, and content searching.
@@ -20,7 +20,8 @@ from hanzo_mcp.tools.filesystem.read import ReadTool
20
20
  from hanzo_mcp.tools.filesystem.write import Write
21
21
  from hanzo_mcp.tools.filesystem.batch_search import BatchSearchTool
22
22
  from hanzo_mcp.tools.filesystem.find_files import FindFilesTool
23
- from hanzo_mcp.tools.filesystem.unified_search import UnifiedSearchTool
23
+ from hanzo_mcp.tools.filesystem.rules_tool import RulesTool
24
+ from hanzo_mcp.tools.filesystem.search_tool import SearchTool
24
25
  from hanzo_mcp.tools.filesystem.watch import watch_tool
25
26
  from hanzo_mcp.tools.filesystem.diff import create_diff_tool
26
27
 
@@ -37,7 +38,8 @@ __all__ = [
37
38
  "GitSearchTool",
38
39
  "BatchSearchTool",
39
40
  "FindFilesTool",
40
- "UnifiedSearchTool",
41
+ "RulesTool",
42
+ "SearchTool",
41
43
  "get_filesystem_tools",
42
44
  "register_filesystem_tools",
43
45
  ]
@@ -51,7 +53,7 @@ def get_read_only_filesystem_tools(
51
53
 
52
54
  Args:
53
55
  permission_manager: Permission manager for access control
54
- project_manager: Optional project manager for unified search
56
+ project_manager: Optional project manager for search
55
57
 
56
58
  Returns:
57
59
  List of read-only filesystem tool instances
@@ -63,13 +65,14 @@ def get_read_only_filesystem_tools(
63
65
  SymbolsTool(permission_manager),
64
66
  GitSearchTool(permission_manager),
65
67
  FindFilesTool(permission_manager),
68
+ RulesTool(permission_manager),
66
69
  watch_tool,
67
70
  create_diff_tool(permission_manager),
68
71
  ]
69
72
 
70
- # Add unified search if project manager is available
73
+ # Add search if project manager is available
71
74
  if project_manager:
72
- tools.append(UnifiedSearchTool(permission_manager, project_manager))
75
+ tools.append(SearchTool(permission_manager, project_manager))
73
76
 
74
77
  return tools
75
78
 
@@ -79,7 +82,7 @@ def get_filesystem_tools(permission_manager: PermissionManager, project_manager=
79
82
 
80
83
  Args:
81
84
  permission_manager: Permission manager for access control
82
- project_manager: Optional project manager for unified search
85
+ project_manager: Optional project manager for search
83
86
 
84
87
  Returns:
85
88
  List of filesystem tool instances
@@ -95,13 +98,14 @@ def get_filesystem_tools(permission_manager: PermissionManager, project_manager=
95
98
  SymbolsTool(permission_manager),
96
99
  GitSearchTool(permission_manager),
97
100
  FindFilesTool(permission_manager),
101
+ RulesTool(permission_manager),
98
102
  watch_tool,
99
103
  create_diff_tool(permission_manager),
100
104
  ]
101
105
 
102
- # Add unified search if project manager is available
106
+ # Add search if project manager is available
103
107
  if project_manager:
104
- tools.append(UnifiedSearchTool(permission_manager, project_manager))
108
+ tools.append(SearchTool(permission_manager, project_manager))
105
109
 
106
110
  return tools
107
111
 
@@ -122,7 +126,7 @@ def register_filesystem_tools(
122
126
  disable_write_tools: Whether to disable write tools (default: False)
123
127
  disable_search_tools: Whether to disable search tools (default: False)
124
128
  enabled_tools: Dictionary of individual tool enable states (default: None)
125
- project_manager: Optional project manager for unified search (default: None)
129
+ project_manager: Optional project manager for search (default: None)
126
130
 
127
131
  Returns:
128
132
  List of registered tools
@@ -135,12 +139,13 @@ def register_filesystem_tools(
135
139
  "multi_edit": MultiEdit,
136
140
  "directory_tree": DirectoryTreeTool,
137
141
  "grep": Grep,
138
- "grep_ast": SymbolsTool, # Using correct import name
142
+ "symbols": SymbolsTool, # Unified symbols tool with grep_ast functionality
139
143
  "git_search": GitSearchTool,
140
144
  "content_replace": ContentReplaceTool,
141
145
  "batch_search": BatchSearchTool,
142
146
  "find_files": FindFilesTool,
143
- "unified_search": UnifiedSearchTool,
147
+ "rules": RulesTool,
148
+ "search": SearchTool,
144
149
  "watch": lambda pm: watch_tool, # Singleton instance
145
150
  "diff": create_diff_tool,
146
151
  }
@@ -152,8 +157,8 @@ def register_filesystem_tools(
152
157
  for tool_name, enabled in enabled_tools.items():
153
158
  if enabled and tool_name in tool_classes:
154
159
  tool_class = tool_classes[tool_name]
155
- if tool_name in ["batch_search", "unified_search"]:
156
- # Batch search and unified search require project_manager
160
+ if tool_name in ["batch_search", "search"]:
161
+ # Batch search and search require project_manager
157
162
  tools.append(tool_class(permission_manager, project_manager))
158
163
  elif tool_name == "watch":
159
164
  # Watch tool is a singleton
@@ -45,7 +45,7 @@ class SearchType(Enum):
45
45
 
46
46
  @dataclass
47
47
  class SearchResult:
48
- """Unified search result combining different search types."""
48
+ """Search result combining different search types."""
49
49
  file_path: str
50
50
  line_number: Optional[int]
51
51
  content: str
@@ -112,11 +112,11 @@ class BatchSearchParams(TypedDict):
112
112
 
113
113
  @final
114
114
  class BatchSearchTool(FilesystemBaseTool):
115
- """Unified search tool combining multiple search strategies."""
115
+ """Search tool combining multiple search strategies."""
116
116
 
117
117
  def __init__(self, permission_manager: PermissionManager,
118
118
  project_manager: Optional[ProjectVectorManager] = None):
119
- """Initialize the unified search tool."""
119
+ """Initialize the search tool."""
120
120
  super().__init__(permission_manager)
121
121
  self.project_manager = project_manager
122
122
 
@@ -98,7 +98,7 @@ diff a.json b.json --ignore-whitespace"""
98
98
 
99
99
  # Generate diff
100
100
  if unified:
101
- # Unified diff format
101
+ # diff format
102
102
  diff_lines = list(difflib.unified_diff(
103
103
  lines1,
104
104
  lines2,
@@ -183,7 +183,7 @@ diff a.json b.json --ignore-whitespace"""
183
183
  unified: bool = True,
184
184
  context: int = 3,
185
185
  ignore_whitespace: bool = False,
186
- show_line_numbers: bool = True,
186
+ show_line_numbers: bool = True
187
187
  ) -> str:
188
188
  """Handle diff tool calls."""
189
189
  return await tool_self.run(
@@ -0,0 +1,235 @@
1
+ """Rules tool implementation.
2
+
3
+ This module provides the RulesTool for reading local preferences from .cursor rules
4
+ or .claude code configuration files.
5
+ """
6
+
7
+ import json
8
+ import os
9
+ from pathlib import Path
10
+ from typing import Annotated, TypedDict, Unpack, final, override, Optional
11
+
12
+ from mcp.server.fastmcp import Context as MCPContext
13
+ from mcp.server import FastMCP
14
+ from pydantic import Field
15
+
16
+ from hanzo_mcp.tools.filesystem.base import FilesystemBaseTool
17
+
18
+
19
+ SearchPath = Annotated[
20
+ str,
21
+ Field(
22
+ description="Directory path to search for configuration files (defaults to current directory)",
23
+ default=".",
24
+ ),
25
+ ]
26
+
27
+
28
+ class RulesToolParams(TypedDict, total=False):
29
+ """Parameters for the RulesTool.
30
+
31
+ Attributes:
32
+ path: Directory path to search for configuration files
33
+ """
34
+
35
+ path: str
36
+
37
+
38
+ @final
39
+ class RulesTool(FilesystemBaseTool):
40
+ """Tool for reading local preferences from configuration files."""
41
+
42
+ @property
43
+ @override
44
+ def name(self) -> str:
45
+ """Get the tool name.
46
+
47
+ Returns:
48
+ Tool name
49
+ """
50
+ return "rules"
51
+
52
+ @property
53
+ @override
54
+ def description(self) -> str:
55
+ """Get the tool description.
56
+
57
+ Returns:
58
+ Tool description
59
+ """
60
+ return """Read local preferences and rules from .cursor/rules or .claude/code configuration files.
61
+
62
+ This tool searches for and reads configuration files that contain project-specific
63
+ preferences, coding standards, and rules for AI assistants.
64
+
65
+ Searches for (in order of priority):
66
+ 1. .cursorrules in current directory
67
+ 2. .cursor/rules in current directory
68
+ 3. .claude/code.md in current directory
69
+ 4. .claude/rules.md in current directory
70
+ 5. Recursively searches parent directories up to project root
71
+
72
+ Usage:
73
+ rules # Search from current directory
74
+ rules --path /project # Search from specific directory
75
+
76
+ The tool returns the contents of all found configuration files to help
77
+ understand project-specific requirements and preferences."""
78
+
79
+ @override
80
+ async def call(
81
+ self,
82
+ ctx: MCPContext,
83
+ **params: Unpack[RulesToolParams],
84
+ ) -> str:
85
+ """Execute the tool with the given parameters.
86
+
87
+ Args:
88
+ ctx: MCP context
89
+ **params: Tool parameters
90
+
91
+ Returns:
92
+ Tool result
93
+ """
94
+ tool_ctx = self.create_tool_context(ctx)
95
+ self.set_tool_context_info(tool_ctx)
96
+
97
+ # Extract parameters
98
+ search_path = params.get("path", ".")
99
+
100
+ # Validate path
101
+ path_validation = self.validate_path(search_path)
102
+ if not path_validation.is_valid:
103
+ await tool_ctx.error(f"Invalid path: {path_validation.error_message}")
104
+ return f"Error: Invalid path: {path_validation.error_message}"
105
+
106
+ # Check permissions
107
+ is_allowed, error_message = await self.check_path_allowed(search_path, tool_ctx)
108
+ if not is_allowed:
109
+ return error_message
110
+
111
+ # Check existence
112
+ is_exists, error_message = await self.check_path_exists(search_path, tool_ctx)
113
+ if not is_exists:
114
+ return error_message
115
+
116
+ # Convert to Path object
117
+ start_path = Path(search_path).resolve()
118
+
119
+ # Configuration files to search for
120
+ config_files = [
121
+ ".cursorrules",
122
+ ".cursor/rules",
123
+ ".cursor/rules.md",
124
+ ".claude/code.md",
125
+ ".claude/rules.md",
126
+ ".claude/config.md",
127
+ ]
128
+
129
+ found_configs = []
130
+
131
+ # Search in current directory and parent directories
132
+ current_path = start_path
133
+ while True:
134
+ for config_file in config_files:
135
+ config_path = current_path / config_file
136
+
137
+ # Check if file exists and we have permission
138
+ if config_path.exists() and config_path.is_file():
139
+ try:
140
+ # Check permissions for this specific file
141
+ if self.is_path_allowed(str(config_path)):
142
+ with open(config_path, "r", encoding="utf-8") as f:
143
+ content = f.read()
144
+
145
+ found_configs.append({
146
+ "path": str(config_path),
147
+ "relative_path": str(config_path.relative_to(start_path)),
148
+ "content": content,
149
+ "size": len(content)
150
+ })
151
+
152
+ await tool_ctx.info(f"Found configuration: {config_path}")
153
+ except Exception as e:
154
+ await tool_ctx.warning(f"Could not read {config_path}: {str(e)}")
155
+
156
+ # Check if we've reached the root or a git repository root
157
+ if current_path.parent == current_path:
158
+ break
159
+
160
+ # Check if this is a git repository root
161
+ if (current_path / ".git").exists():
162
+ # Search one more time in the git root before stopping
163
+ if current_path != start_path:
164
+ for config_file in config_files:
165
+ config_path = current_path / config_file
166
+ if config_path.exists() and config_path.is_file() and str(config_path) not in [c["path"] for c in found_configs]:
167
+ try:
168
+ if self.is_path_allowed(str(config_path)):
169
+ with open(config_path, "r", encoding="utf-8") as f:
170
+ content = f.read()
171
+
172
+ found_configs.append({
173
+ "path": str(config_path),
174
+ "relative_path": str(config_path.relative_to(start_path)),
175
+ "content": content,
176
+ "size": len(content)
177
+ })
178
+
179
+ await tool_ctx.info(f"Found configuration: {config_path}")
180
+ except Exception as e:
181
+ await tool_ctx.warning(f"Could not read {config_path}: {str(e)}")
182
+ break
183
+
184
+ # Move to parent directory
185
+ parent = current_path.parent
186
+
187
+ # Check if parent is still within allowed paths
188
+ if not self.is_path_allowed(str(parent)):
189
+ await tool_ctx.info(f"Stopped at directory boundary: {parent}")
190
+ break
191
+
192
+ current_path = parent
193
+
194
+ # Format results
195
+ if not found_configs:
196
+ return f"""No configuration files found.
197
+
198
+ Searched for:
199
+ {chr(10).join('- ' + cf for cf in config_files)}
200
+
201
+ Starting from: {start_path}
202
+
203
+ To create project rules, create one of these files with your preferences:
204
+ - .cursorrules: For Cursor IDE rules
205
+ - .cursor/rules: Alternative Cursor location
206
+ - .claude/code.md: For Claude-specific coding preferences
207
+ - .claude/rules.md: For general Claude interaction rules"""
208
+
209
+ # Build output
210
+ output = [f"=== Found {len(found_configs)} Configuration File(s) ===\n"]
211
+
212
+ for i, config in enumerate(found_configs, 1):
213
+ output.append(f"--- [{i}] {config['path']} ({config['size']} bytes) ---")
214
+ output.append(config['content'])
215
+ output.append("") # Empty line between configs
216
+
217
+ output.append(f"\nSearched from: {start_path}")
218
+
219
+ return "\n".join(output)
220
+
221
+ @override
222
+ def register(self, mcp_server: FastMCP) -> None:
223
+ """Register this rules tool with the MCP server.
224
+
225
+ Args:
226
+ mcp_server: The FastMCP server instance
227
+ """
228
+ tool_self = self # Create a reference to self for use in the closure
229
+
230
+ @mcp_server.tool(name=self.name, description=self.description)
231
+ async def rules(
232
+ path: SearchPath = ".",
233
+ ctx: MCPContext = None,
234
+ ) -> str:
235
+ return await tool_self.call(ctx, path=path)
@@ -1,4 +1,4 @@
1
- """Unified search tool that runs multiple search types in parallel.
1
+ """Search tool that runs multiple search types in parallel.
2
2
 
3
3
  This tool consolidates all search capabilities and runs them concurrently:
4
4
  - grep: Fast pattern/regex search using ripgrep
@@ -41,7 +41,7 @@ class SearchType(Enum):
41
41
 
42
42
  @dataclass
43
43
  class SearchResult:
44
- """Unified search result from any search type."""
44
+ """Search result from any search type."""
45
45
  file_path: str
46
46
  line_number: Optional[int]
47
47
  content: str
@@ -133,7 +133,7 @@ IncludeContext = Annotated[
133
133
 
134
134
 
135
135
  class UnifiedSearchParams(TypedDict):
136
- """Parameters for unified search."""
136
+ """Parameters for search."""
137
137
  pattern: Pattern
138
138
  path: SearchPath
139
139
  include: Include
@@ -147,12 +147,12 @@ class UnifiedSearchParams(TypedDict):
147
147
 
148
148
 
149
149
  @final
150
- class UnifiedSearchTool(FilesystemBaseTool):
151
- """Unified search tool that runs multiple search types in parallel."""
150
+ class SearchTool(FilesystemBaseTool):
151
+ """Search tool that runs multiple search types in parallel."""
152
152
 
153
153
  def __init__(self, permission_manager: PermissionManager,
154
154
  project_manager: Optional[ProjectVectorManager] = None):
155
- """Initialize the unified search tool.
155
+ """Initialize the search tool.
156
156
 
157
157
  Args:
158
158
  permission_manager: Permission manager for access control
@@ -175,13 +175,13 @@ class UnifiedSearchTool(FilesystemBaseTool):
175
175
  @override
176
176
  def name(self) -> str:
177
177
  """Get the tool name."""
178
- return "unified_search"
178
+ return "search"
179
179
 
180
180
  @property
181
181
  @override
182
182
  def description(self) -> str:
183
183
  """Get the tool description."""
184
- return """Unified search that runs multiple search strategies in parallel.
184
+ return """Search that runs multiple search strategies in parallel.
185
185
 
186
186
  Automatically runs the most appropriate search types based on your pattern:
187
187
  - Pattern matching (grep) for exact text/regex
@@ -527,7 +527,7 @@ This is the recommended search tool for comprehensive results."""
527
527
  ctx: MCPContext,
528
528
  **params: Unpack[UnifiedSearchParams],
529
529
  ) -> str:
530
- """Execute unified search across all enabled search types."""
530
+ """Execute search across all enabled search types."""
531
531
  import time
532
532
  start_time = time.time()
533
533
 
@@ -559,7 +559,7 @@ This is the recommended search tool for comprehensive results."""
559
559
  # Analyze pattern to determine best search strategies
560
560
  pattern_analysis = self._analyze_pattern(pattern)
561
561
 
562
- await tool_ctx.info(f"Starting unified search for '{pattern}' in {path}")
562
+ await tool_ctx.info(f"Starting search for '{pattern}' in {path}")
563
563
 
564
564
  # Build list of search tasks based on enabled types and pattern analysis
565
565
  search_tasks = []
@@ -679,11 +679,11 @@ This is the recommended search tool for comprehensive results."""
679
679
 
680
680
  @override
681
681
  def register(self, mcp_server: FastMCP) -> None:
682
- """Register the unified search tool with the MCP server."""
682
+ """Register the search tool with the MCP server."""
683
683
  tool_self = self
684
684
 
685
685
  @mcp_server.tool(name=self.name, description=self.description)
686
- async def unified_search(
686
+ async def search(
687
687
  ctx: MCPContext,
688
688
  pattern: Pattern,
689
689
  path: SearchPath = ".",
@@ -21,7 +21,7 @@ from hanzo_mcp.tools.filesystem.base import FilesystemBaseTool
21
21
  Action = Annotated[
22
22
  str,
23
23
  Field(
24
- description="Action: search (default), index, query, list",
24
+ description="Action: search (default), ast, index, query, list",
25
25
  default="search",
26
26
  ),
27
27
  ]
@@ -88,7 +88,7 @@ class SymbolsParams(TypedDict, total=False):
88
88
 
89
89
  @final
90
90
  class SymbolsTool(FilesystemBaseTool):
91
- """Unified tool for code symbol operations using tree-sitter."""
91
+ """Tool for code symbol operations using tree-sitter."""
92
92
 
93
93
  def __init__(self, permission_manager):
94
94
  """Initialize the symbols tool."""
@@ -105,13 +105,16 @@ class SymbolsTool(FilesystemBaseTool):
105
105
  @override
106
106
  def description(self) -> str:
107
107
  """Get the tool description."""
108
- return """Code symbols with tree-sitter. Actions: search (default), index, query, list.
108
+ return """Code symbols search with tree-sitter AST. Actions: search (default), ast, index, query, list.
109
109
 
110
110
  Usage:
111
111
  symbols "function_name"
112
+ symbols --action ast --pattern "TODO" --path ./src
112
113
  symbols --action query --symbol-type function --path ./src
113
114
  symbols --action index --path ./project
114
- symbols --action list --path ./src --symbol-type class"""
115
+ symbols --action list --path ./src --symbol-type class
116
+
117
+ Finds code structures (functions, classes, methods) with full context."""
115
118
 
116
119
  @override
117
120
  async def call(
@@ -129,6 +132,8 @@ symbols --action list --path ./src --symbol-type class"""
129
132
  # Route to appropriate handler
130
133
  if action == "search":
131
134
  return await self._handle_search(params, tool_ctx)
135
+ elif action == "ast" or action == "grep_ast": # Support both for backward compatibility
136
+ return await self._handle_ast(params, tool_ctx)
132
137
  elif action == "index":
133
138
  return await self._handle_index(params, tool_ctx)
134
139
  elif action == "query":
@@ -136,7 +141,7 @@ symbols --action list --path ./src --symbol-type class"""
136
141
  elif action == "list":
137
142
  return await self._handle_list(params, tool_ctx)
138
143
  else:
139
- return f"Error: Unknown action '{action}'. Valid actions: search, index, query, list"
144
+ return f"Error: Unknown action '{action}'. Valid actions: search, ast, index, query, list"
140
145
 
141
146
  async def _handle_search(self, params: Dict[str, Any], tool_ctx) -> str:
142
147
  """Search for pattern in code with AST context."""
@@ -222,6 +227,100 @@ symbols --action list --path ./src --symbol-type class"""
222
227
 
223
228
  return "\n".join(output)
224
229
 
230
+ async def _handle_ast(self, params: Dict[str, Any], tool_ctx) -> str:
231
+ """AST-aware grep - shows code structure context around matches."""
232
+ pattern = params.get("pattern")
233
+ if not pattern:
234
+ return "Error: pattern required for ast action"
235
+
236
+ path = params.get("path", ".")
237
+ ignore_case = params.get("ignore_case", False)
238
+ show_context = params.get("show_context", True)
239
+ limit = params.get("limit", 50)
240
+
241
+ # Validate path
242
+ path_validation = self.validate_path(path)
243
+ if not path_validation.is_valid:
244
+ await tool_ctx.error(f"Invalid path: {path_validation.error_message}")
245
+ return f"Error: Invalid path: {path_validation.error_message}"
246
+
247
+ # Check permissions
248
+ is_allowed, error_message = await self.check_path_allowed(path, tool_ctx)
249
+ if not is_allowed:
250
+ return error_message
251
+
252
+ # Check existence
253
+ is_exists, error_message = await self.check_path_exists(path, tool_ctx)
254
+ if not is_exists:
255
+ return error_message
256
+
257
+ await tool_ctx.info(f"Running AST-aware grep for '{pattern}' in {path}")
258
+
259
+ # Get files to process
260
+ files_to_process = self._get_source_files(path)
261
+ if not files_to_process:
262
+ return f"No source code files found in {path}"
263
+
264
+ # Process files
265
+ results = []
266
+ match_count = 0
267
+
268
+ for file_path in files_to_process:
269
+ if match_count >= limit:
270
+ break
271
+
272
+ try:
273
+ with open(file_path, "r", encoding="utf-8") as f:
274
+ code = f.read()
275
+
276
+ # Create TreeContext for AST parsing
277
+ tc = TreeContext(
278
+ file_path,
279
+ code,
280
+ color=False,
281
+ verbose=False,
282
+ line_number=True,
283
+ )
284
+
285
+ # Find matches with case sensitivity option
286
+ if ignore_case:
287
+ import re
288
+ loi = tc.grep(pattern, ignore_case=True)
289
+ else:
290
+ loi = tc.grep(pattern, ignore_case=False)
291
+
292
+ if loi:
293
+ # Always show AST context for grep_ast
294
+ tc.add_lines_of_interest(loi)
295
+ tc.add_context()
296
+
297
+ # Get the formatted output with structure
298
+ output = tc.format()
299
+
300
+ # Add section separator and file info
301
+ results.append(f"\n{'='*60}")
302
+ results.append(f"File: {file_path}")
303
+ results.append(f"Matches: {len(loi)}")
304
+ results.append(f"{'='*60}\n")
305
+ results.append(output)
306
+
307
+ match_count += len(loi)
308
+
309
+ except Exception as e:
310
+ await tool_ctx.warning(f"Could not parse {file_path}: {str(e)}")
311
+
312
+ if not results:
313
+ return f"No matches found for '{pattern}' in {path}"
314
+
315
+ output = [f"=== AST-aware Grep Results for '{pattern}' ==="]
316
+ output.append(f"Total matches: {match_count} in {len([r for r in results if '===' in str(r)])//4} files\n")
317
+ output.extend(results)
318
+
319
+ if match_count >= limit:
320
+ output.append(f"\n(Results limited to {limit} matches)")
321
+
322
+ return "\n".join(output)
323
+
225
324
  async def _handle_index(self, params: Dict[str, Any], tool_ctx) -> str:
226
325
  """Index symbols in a codebase."""
227
326
  path = params.get("path", ".")
@@ -23,16 +23,17 @@ class WatchTool(BaseTool):
23
23
 
24
24
  @server.tool(name=self.name, description=self.description)
25
25
  async def watch_handler(
26
+ ctx: MCPContext,
26
27
  path: str,
27
28
  pattern: str = "*",
28
29
  interval: int = 1,
29
30
  recursive: bool = True,
30
31
  exclude: str = "",
31
- duration: int = 30,
32
+ duration: int = 30
32
33
  ) -> str:
33
34
  """Handle watch tool calls."""
34
35
  return await self.run(
35
- None,
36
+ ctx,
36
37
  path=path,
37
38
  pattern=pattern,
38
39
  interval=interval,
@@ -1,4 +1,4 @@
1
- """Jupyter notebook tools package for Hanzo MCP.
1
+ """Jupyter notebook tools package for Hanzo AI.
2
2
 
3
3
  This package provides tools for working with Jupyter notebooks (.ipynb files),
4
4
  including reading and editing notebook cells.
@@ -29,7 +29,7 @@ def get_read_only_jupyter_tools(
29
29
  Returns:
30
30
  List of Jupyter notebook tool instances
31
31
  """
32
- return [] # Unified tool handles both read and write
32
+ return [] # Tool handles both read and write
33
33
 
34
34
 
35
35
  def get_jupyter_tools(permission_manager: PermissionManager) -> list[BaseTool]:
@@ -81,7 +81,7 @@ class NotebookParams(TypedDict, total=False):
81
81
 
82
82
  @final
83
83
  class JupyterTool(JupyterBaseTool):
84
- """Unified tool for Jupyter notebook operations."""
84
+ """Tool for Jupyter notebook operations."""
85
85
 
86
86
  @property
87
87
  @override