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.
- hanzo_mcp/__init__.py +2 -2
- hanzo_mcp/cli.py +2 -2
- hanzo_mcp/cli_enhanced.py +4 -4
- hanzo_mcp/cli_plugin.py +91 -0
- hanzo_mcp/config/__init__.py +1 -1
- hanzo_mcp/config/settings.py +69 -6
- hanzo_mcp/config/tool_config.py +2 -2
- hanzo_mcp/dev_server.py +3 -3
- hanzo_mcp/prompts/project_system.py +1 -1
- hanzo_mcp/server.py +6 -2
- hanzo_mcp/server_enhanced.py +69 -0
- hanzo_mcp/tools/__init__.py +75 -29
- hanzo_mcp/tools/agent/__init__.py +1 -1
- hanzo_mcp/tools/agent/agent_tool.py +2 -2
- hanzo_mcp/tools/common/__init__.py +15 -1
- hanzo_mcp/tools/common/base.py +4 -4
- hanzo_mcp/tools/common/batch_tool.py +1 -1
- hanzo_mcp/tools/common/config_tool.py +2 -2
- hanzo_mcp/tools/common/context.py +2 -2
- hanzo_mcp/tools/common/context_fix.py +26 -0
- hanzo_mcp/tools/common/critic_tool.py +196 -0
- hanzo_mcp/tools/common/decorators.py +208 -0
- hanzo_mcp/tools/common/enhanced_base.py +106 -0
- hanzo_mcp/tools/common/mode.py +116 -0
- hanzo_mcp/tools/common/mode_loader.py +105 -0
- hanzo_mcp/tools/common/permissions.py +1 -1
- hanzo_mcp/tools/common/personality.py +936 -0
- hanzo_mcp/tools/common/plugin_loader.py +287 -0
- hanzo_mcp/tools/common/stats.py +4 -4
- hanzo_mcp/tools/common/tool_list.py +1 -1
- hanzo_mcp/tools/common/validation.py +1 -1
- hanzo_mcp/tools/config/__init__.py +3 -1
- hanzo_mcp/tools/config/config_tool.py +1 -1
- hanzo_mcp/tools/config/mode_tool.py +209 -0
- hanzo_mcp/tools/database/__init__.py +1 -1
- hanzo_mcp/tools/editor/__init__.py +1 -1
- hanzo_mcp/tools/filesystem/__init__.py +19 -14
- hanzo_mcp/tools/filesystem/batch_search.py +3 -3
- hanzo_mcp/tools/filesystem/diff.py +2 -2
- hanzo_mcp/tools/filesystem/rules_tool.py +235 -0
- hanzo_mcp/tools/filesystem/{unified_search.py → search_tool.py} +12 -12
- hanzo_mcp/tools/filesystem/{symbols_unified.py → symbols_tool.py} +104 -5
- hanzo_mcp/tools/filesystem/watch.py +3 -2
- hanzo_mcp/tools/jupyter/__init__.py +2 -2
- hanzo_mcp/tools/jupyter/jupyter.py +1 -1
- hanzo_mcp/tools/llm/__init__.py +3 -3
- hanzo_mcp/tools/llm/llm_tool.py +648 -143
- hanzo_mcp/tools/mcp/__init__.py +2 -2
- hanzo_mcp/tools/mcp/{mcp_unified.py → mcp_tool.py} +3 -3
- hanzo_mcp/tools/shell/__init__.py +6 -6
- hanzo_mcp/tools/shell/base_process.py +4 -2
- hanzo_mcp/tools/shell/bash_session_executor.py +1 -1
- hanzo_mcp/tools/shell/{bash_unified.py → bash_tool.py} +1 -1
- hanzo_mcp/tools/shell/command_executor.py +2 -2
- hanzo_mcp/tools/shell/{npx_unified.py → npx_tool.py} +1 -1
- hanzo_mcp/tools/shell/open.py +2 -2
- hanzo_mcp/tools/shell/{process_unified.py → process_tool.py} +1 -1
- hanzo_mcp/tools/shell/run_command_windows.py +1 -1
- hanzo_mcp/tools/shell/uvx.py +47 -2
- hanzo_mcp/tools/shell/uvx_background.py +47 -2
- hanzo_mcp/tools/shell/{uvx_unified.py → uvx_tool.py} +1 -1
- hanzo_mcp/tools/todo/__init__.py +14 -19
- hanzo_mcp/tools/todo/todo.py +22 -1
- hanzo_mcp/tools/vector/__init__.py +1 -1
- hanzo_mcp/tools/vector/infinity_store.py +2 -2
- hanzo_mcp/tools/vector/project_manager.py +1 -1
- hanzo_mcp-0.6.13.dist-info/METADATA +359 -0
- {hanzo_mcp-0.6.12.dist-info → hanzo_mcp-0.6.13.dist-info}/RECORD +72 -64
- {hanzo_mcp-0.6.12.dist-info → hanzo_mcp-0.6.13.dist-info}/entry_points.txt +1 -0
- hanzo_mcp/tools/common/palette.py +0 -344
- hanzo_mcp/tools/common/palette_loader.py +0 -108
- hanzo_mcp/tools/config/palette_tool.py +0 -179
- hanzo_mcp/tools/llm/llm_unified.py +0 -851
- hanzo_mcp-0.6.12.dist-info/METADATA +0 -339
- {hanzo_mcp-0.6.12.dist-info → hanzo_mcp-0.6.13.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.6.12.dist-info → hanzo_mcp-0.6.13.dist-info}/licenses/LICENSE +0 -0
- {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
|
|
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.
|
|
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
|
-
"
|
|
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
|
|
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
|
|
73
|
+
# Add search if project manager is available
|
|
71
74
|
if project_manager:
|
|
72
|
-
tools.append(
|
|
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
|
|
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
|
|
106
|
+
# Add search if project manager is available
|
|
103
107
|
if project_manager:
|
|
104
|
-
tools.append(
|
|
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
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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", "
|
|
156
|
-
# Batch search and
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
|
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
|
-
#
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
|
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
|
|
151
|
-
"""
|
|
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
|
|
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 "
|
|
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 """
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
"""
|
|
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
|
-
|
|
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
|
|
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 [] #
|
|
32
|
+
return [] # Tool handles both read and write
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
def get_jupyter_tools(permission_manager: PermissionManager) -> list[BaseTool]:
|