hanzo-mcp 0.5.1__py3-none-any.whl → 0.6.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of hanzo-mcp might be problematic. Click here for more details.
- hanzo_mcp/__init__.py +1 -1
- hanzo_mcp/cli.py +32 -0
- hanzo_mcp/dev_server.py +246 -0
- hanzo_mcp/prompts/__init__.py +1 -1
- hanzo_mcp/prompts/project_system.py +43 -7
- hanzo_mcp/server.py +5 -1
- hanzo_mcp/tools/__init__.py +168 -6
- hanzo_mcp/tools/agent/__init__.py +1 -1
- hanzo_mcp/tools/agent/agent.py +401 -0
- hanzo_mcp/tools/agent/agent_tool.py +3 -4
- hanzo_mcp/tools/common/__init__.py +1 -1
- hanzo_mcp/tools/common/base.py +9 -4
- hanzo_mcp/tools/common/batch_tool.py +3 -5
- hanzo_mcp/tools/common/config_tool.py +1 -1
- hanzo_mcp/tools/common/context.py +1 -1
- hanzo_mcp/tools/common/palette.py +344 -0
- hanzo_mcp/tools/common/palette_loader.py +108 -0
- hanzo_mcp/tools/common/stats.py +261 -0
- hanzo_mcp/tools/common/thinking_tool.py +3 -5
- hanzo_mcp/tools/common/tool_disable.py +144 -0
- hanzo_mcp/tools/common/tool_enable.py +182 -0
- hanzo_mcp/tools/common/tool_list.py +260 -0
- hanzo_mcp/tools/config/__init__.py +10 -0
- hanzo_mcp/tools/config/config_tool.py +212 -0
- hanzo_mcp/tools/config/index_config.py +176 -0
- hanzo_mcp/tools/config/palette_tool.py +166 -0
- hanzo_mcp/tools/database/__init__.py +71 -0
- hanzo_mcp/tools/database/database_manager.py +246 -0
- hanzo_mcp/tools/database/graph.py +482 -0
- hanzo_mcp/tools/database/graph_add.py +257 -0
- hanzo_mcp/tools/database/graph_query.py +536 -0
- hanzo_mcp/tools/database/graph_remove.py +267 -0
- hanzo_mcp/tools/database/graph_search.py +348 -0
- hanzo_mcp/tools/database/graph_stats.py +345 -0
- hanzo_mcp/tools/database/sql.py +411 -0
- hanzo_mcp/tools/database/sql_query.py +229 -0
- hanzo_mcp/tools/database/sql_search.py +296 -0
- hanzo_mcp/tools/database/sql_stats.py +254 -0
- hanzo_mcp/tools/editor/__init__.py +11 -0
- hanzo_mcp/tools/editor/neovim_command.py +272 -0
- hanzo_mcp/tools/editor/neovim_edit.py +290 -0
- hanzo_mcp/tools/editor/neovim_session.py +356 -0
- hanzo_mcp/tools/filesystem/__init__.py +52 -13
- hanzo_mcp/tools/filesystem/base.py +1 -1
- hanzo_mcp/tools/filesystem/batch_search.py +812 -0
- hanzo_mcp/tools/filesystem/content_replace.py +3 -5
- hanzo_mcp/tools/filesystem/diff.py +193 -0
- hanzo_mcp/tools/filesystem/directory_tree.py +3 -5
- hanzo_mcp/tools/filesystem/edit.py +3 -5
- hanzo_mcp/tools/filesystem/find.py +443 -0
- hanzo_mcp/tools/filesystem/find_files.py +348 -0
- hanzo_mcp/tools/filesystem/git_search.py +505 -0
- hanzo_mcp/tools/filesystem/grep.py +2 -2
- hanzo_mcp/tools/filesystem/multi_edit.py +3 -5
- hanzo_mcp/tools/filesystem/read.py +17 -5
- hanzo_mcp/tools/filesystem/{grep_ast_tool.py → symbols.py} +17 -27
- hanzo_mcp/tools/filesystem/symbols_unified.py +376 -0
- hanzo_mcp/tools/filesystem/tree.py +268 -0
- hanzo_mcp/tools/filesystem/unified_search.py +465 -443
- hanzo_mcp/tools/filesystem/unix_aliases.py +99 -0
- hanzo_mcp/tools/filesystem/watch.py +174 -0
- hanzo_mcp/tools/filesystem/write.py +3 -5
- hanzo_mcp/tools/jupyter/__init__.py +9 -12
- hanzo_mcp/tools/jupyter/base.py +1 -1
- hanzo_mcp/tools/jupyter/jupyter.py +326 -0
- hanzo_mcp/tools/jupyter/notebook_edit.py +3 -4
- hanzo_mcp/tools/jupyter/notebook_read.py +3 -5
- hanzo_mcp/tools/llm/__init__.py +31 -0
- hanzo_mcp/tools/llm/consensus_tool.py +351 -0
- hanzo_mcp/tools/llm/llm_manage.py +413 -0
- hanzo_mcp/tools/llm/llm_tool.py +346 -0
- hanzo_mcp/tools/llm/llm_unified.py +851 -0
- hanzo_mcp/tools/llm/provider_tools.py +412 -0
- hanzo_mcp/tools/mcp/__init__.py +15 -0
- hanzo_mcp/tools/mcp/mcp_add.py +263 -0
- hanzo_mcp/tools/mcp/mcp_remove.py +127 -0
- hanzo_mcp/tools/mcp/mcp_stats.py +165 -0
- hanzo_mcp/tools/mcp/mcp_unified.py +503 -0
- hanzo_mcp/tools/shell/__init__.py +21 -23
- hanzo_mcp/tools/shell/base.py +1 -1
- hanzo_mcp/tools/shell/base_process.py +303 -0
- hanzo_mcp/tools/shell/bash_unified.py +134 -0
- hanzo_mcp/tools/shell/logs.py +265 -0
- hanzo_mcp/tools/shell/npx.py +194 -0
- hanzo_mcp/tools/shell/npx_background.py +254 -0
- hanzo_mcp/tools/shell/npx_unified.py +101 -0
- hanzo_mcp/tools/shell/open.py +107 -0
- hanzo_mcp/tools/shell/pkill.py +262 -0
- hanzo_mcp/tools/shell/process_unified.py +131 -0
- hanzo_mcp/tools/shell/processes.py +279 -0
- hanzo_mcp/tools/shell/run_background.py +326 -0
- hanzo_mcp/tools/shell/run_command.py +3 -4
- hanzo_mcp/tools/shell/run_command_windows.py +3 -4
- hanzo_mcp/tools/shell/uvx.py +187 -0
- hanzo_mcp/tools/shell/uvx_background.py +249 -0
- hanzo_mcp/tools/shell/uvx_unified.py +101 -0
- hanzo_mcp/tools/todo/__init__.py +1 -1
- hanzo_mcp/tools/todo/base.py +1 -1
- hanzo_mcp/tools/todo/todo.py +265 -0
- hanzo_mcp/tools/todo/todo_read.py +3 -5
- hanzo_mcp/tools/todo/todo_write.py +3 -5
- hanzo_mcp/tools/vector/__init__.py +6 -1
- hanzo_mcp/tools/vector/git_ingester.py +3 -0
- hanzo_mcp/tools/vector/index_tool.py +358 -0
- hanzo_mcp/tools/vector/infinity_store.py +98 -0
- hanzo_mcp/tools/vector/project_manager.py +27 -5
- hanzo_mcp/tools/vector/vector.py +311 -0
- hanzo_mcp/tools/vector/vector_index.py +1 -1
- hanzo_mcp/tools/vector/vector_search.py +12 -7
- hanzo_mcp-0.6.1.dist-info/METADATA +336 -0
- hanzo_mcp-0.6.1.dist-info/RECORD +134 -0
- hanzo_mcp-0.6.1.dist-info/entry_points.txt +3 -0
- hanzo_mcp-0.5.1.dist-info/METADATA +0 -276
- hanzo_mcp-0.5.1.dist-info/RECORD +0 -68
- hanzo_mcp-0.5.1.dist-info/entry_points.txt +0 -2
- {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.6.1.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.6.1.dist-info}/licenses/LICENSE +0 -0
- {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.6.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
"""Unified symbols tool implementation.
|
|
2
|
+
|
|
3
|
+
This module provides the SymbolsTool for searching, indexing, and querying code symbols
|
|
4
|
+
using tree-sitter AST parsing. It can find function definitions, class declarations,
|
|
5
|
+
and other code structures with full context.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Annotated, TypedDict, Unpack, final, override, Optional, List, Dict, Any
|
|
11
|
+
import json
|
|
12
|
+
|
|
13
|
+
from mcp.server.fastmcp import Context as MCPContext
|
|
14
|
+
from grep_ast.grep_ast import TreeContext
|
|
15
|
+
from pydantic import Field
|
|
16
|
+
|
|
17
|
+
from hanzo_mcp.tools.filesystem.base import FilesystemBaseTool
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# Parameter types
|
|
21
|
+
Action = Annotated[
|
|
22
|
+
str,
|
|
23
|
+
Field(
|
|
24
|
+
description="Action: search (default), index, query, list",
|
|
25
|
+
default="search",
|
|
26
|
+
),
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
Pattern = Annotated[
|
|
30
|
+
Optional[str],
|
|
31
|
+
Field(
|
|
32
|
+
description="Pattern to search for in code",
|
|
33
|
+
default=None,
|
|
34
|
+
),
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
SearchPath = Annotated[
|
|
38
|
+
str,
|
|
39
|
+
Field(
|
|
40
|
+
description="Path to search/index (file or directory)",
|
|
41
|
+
default=".",
|
|
42
|
+
),
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
SymbolType = Annotated[
|
|
46
|
+
Optional[str],
|
|
47
|
+
Field(
|
|
48
|
+
description="Symbol type: function, class, method, variable",
|
|
49
|
+
default=None,
|
|
50
|
+
),
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
IgnoreCase = Annotated[
|
|
54
|
+
bool,
|
|
55
|
+
Field(
|
|
56
|
+
description="Ignore case when matching",
|
|
57
|
+
default=False,
|
|
58
|
+
),
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
ShowContext = Annotated[
|
|
62
|
+
bool,
|
|
63
|
+
Field(
|
|
64
|
+
description="Show AST context around matches",
|
|
65
|
+
default=True,
|
|
66
|
+
),
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
Limit = Annotated[
|
|
70
|
+
int,
|
|
71
|
+
Field(
|
|
72
|
+
description="Maximum results to return",
|
|
73
|
+
default=50,
|
|
74
|
+
),
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class SymbolsParams(TypedDict, total=False):
|
|
79
|
+
"""Parameters for symbols tool."""
|
|
80
|
+
action: str
|
|
81
|
+
pattern: Optional[str]
|
|
82
|
+
path: str
|
|
83
|
+
symbol_type: Optional[str]
|
|
84
|
+
ignore_case: bool
|
|
85
|
+
show_context: bool
|
|
86
|
+
limit: int
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@final
|
|
90
|
+
class SymbolsTool(FilesystemBaseTool):
|
|
91
|
+
"""Unified tool for code symbol operations using tree-sitter."""
|
|
92
|
+
|
|
93
|
+
def __init__(self, permission_manager):
|
|
94
|
+
"""Initialize the symbols tool."""
|
|
95
|
+
super().__init__(permission_manager)
|
|
96
|
+
self._symbol_cache = {} # Cache for indexed symbols
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
@override
|
|
100
|
+
def name(self) -> str:
|
|
101
|
+
"""Get the tool name."""
|
|
102
|
+
return "symbols"
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
@override
|
|
106
|
+
def description(self) -> str:
|
|
107
|
+
"""Get the tool description."""
|
|
108
|
+
return """Code symbols with tree-sitter. Actions: search (default), index, query, list.
|
|
109
|
+
|
|
110
|
+
Usage:
|
|
111
|
+
symbols "function_name"
|
|
112
|
+
symbols --action query --symbol-type function --path ./src
|
|
113
|
+
symbols --action index --path ./project
|
|
114
|
+
symbols --action list --path ./src --symbol-type class"""
|
|
115
|
+
|
|
116
|
+
@override
|
|
117
|
+
async def call(
|
|
118
|
+
self,
|
|
119
|
+
ctx: MCPContext,
|
|
120
|
+
**params: Unpack[SymbolsParams],
|
|
121
|
+
) -> str:
|
|
122
|
+
"""Execute symbols operation."""
|
|
123
|
+
tool_ctx = self.create_tool_context(ctx)
|
|
124
|
+
self.set_tool_context_info(tool_ctx)
|
|
125
|
+
|
|
126
|
+
# Extract action
|
|
127
|
+
action = params.get("action", "search")
|
|
128
|
+
|
|
129
|
+
# Route to appropriate handler
|
|
130
|
+
if action == "search":
|
|
131
|
+
return await self._handle_search(params, tool_ctx)
|
|
132
|
+
elif action == "index":
|
|
133
|
+
return await self._handle_index(params, tool_ctx)
|
|
134
|
+
elif action == "query":
|
|
135
|
+
return await self._handle_query(params, tool_ctx)
|
|
136
|
+
elif action == "list":
|
|
137
|
+
return await self._handle_list(params, tool_ctx)
|
|
138
|
+
else:
|
|
139
|
+
return f"Error: Unknown action '{action}'. Valid actions: search, index, query, list"
|
|
140
|
+
|
|
141
|
+
async def _handle_search(self, params: Dict[str, Any], tool_ctx) -> str:
|
|
142
|
+
"""Search for pattern in code with AST context."""
|
|
143
|
+
pattern = params.get("pattern")
|
|
144
|
+
if not pattern:
|
|
145
|
+
return "Error: pattern required for search action"
|
|
146
|
+
|
|
147
|
+
path = params.get("path", ".")
|
|
148
|
+
ignore_case = params.get("ignore_case", False)
|
|
149
|
+
show_context = params.get("show_context", True)
|
|
150
|
+
limit = params.get("limit", 50)
|
|
151
|
+
|
|
152
|
+
# Validate path
|
|
153
|
+
path_validation = self.validate_path(path)
|
|
154
|
+
if not path_validation.is_valid:
|
|
155
|
+
await tool_ctx.error(f"Invalid path: {path_validation.error_message}")
|
|
156
|
+
return f"Error: Invalid path: {path_validation.error_message}"
|
|
157
|
+
|
|
158
|
+
# Check permissions
|
|
159
|
+
is_allowed, error_message = await self.check_path_allowed(path, tool_ctx)
|
|
160
|
+
if not is_allowed:
|
|
161
|
+
return error_message
|
|
162
|
+
|
|
163
|
+
# Check existence
|
|
164
|
+
is_exists, error_message = await self.check_path_exists(path, tool_ctx)
|
|
165
|
+
if not is_exists:
|
|
166
|
+
return error_message
|
|
167
|
+
|
|
168
|
+
await tool_ctx.info(f"Searching for '{pattern}' in {path}")
|
|
169
|
+
|
|
170
|
+
# Get files to process
|
|
171
|
+
files_to_process = self._get_source_files(path)
|
|
172
|
+
if not files_to_process:
|
|
173
|
+
return f"No source code files found in {path}"
|
|
174
|
+
|
|
175
|
+
# Process files
|
|
176
|
+
results = []
|
|
177
|
+
match_count = 0
|
|
178
|
+
|
|
179
|
+
for file_path in files_to_process:
|
|
180
|
+
if match_count >= limit:
|
|
181
|
+
break
|
|
182
|
+
|
|
183
|
+
try:
|
|
184
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
185
|
+
code = f.read()
|
|
186
|
+
|
|
187
|
+
tc = TreeContext(
|
|
188
|
+
file_path,
|
|
189
|
+
code,
|
|
190
|
+
color=False,
|
|
191
|
+
verbose=False,
|
|
192
|
+
line_number=True,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# Find matches
|
|
196
|
+
loi = tc.grep(pattern, ignore_case)
|
|
197
|
+
|
|
198
|
+
if loi:
|
|
199
|
+
if show_context:
|
|
200
|
+
tc.add_lines_of_interest(loi)
|
|
201
|
+
tc.add_context()
|
|
202
|
+
output = tc.format()
|
|
203
|
+
else:
|
|
204
|
+
# Just show matching lines
|
|
205
|
+
output = "\n".join([f"{line}: {code.splitlines()[line-1]}" for line in loi])
|
|
206
|
+
|
|
207
|
+
results.append(f"\n{file_path}:\n{output}\n")
|
|
208
|
+
match_count += len(loi)
|
|
209
|
+
|
|
210
|
+
except Exception as e:
|
|
211
|
+
await tool_ctx.warning(f"Could not parse {file_path}: {str(e)}")
|
|
212
|
+
|
|
213
|
+
if not results:
|
|
214
|
+
return f"No matches found for '{pattern}' in {path}"
|
|
215
|
+
|
|
216
|
+
output = [f"=== Symbol Search Results for '{pattern}' ==="]
|
|
217
|
+
output.append(f"Found {match_count} matches in {len(results)} files\n")
|
|
218
|
+
output.extend(results)
|
|
219
|
+
|
|
220
|
+
if match_count >= limit:
|
|
221
|
+
output.append(f"\n(Results limited to {limit} matches)")
|
|
222
|
+
|
|
223
|
+
return "\n".join(output)
|
|
224
|
+
|
|
225
|
+
async def _handle_index(self, params: Dict[str, Any], tool_ctx) -> str:
|
|
226
|
+
"""Index symbols in a codebase."""
|
|
227
|
+
path = params.get("path", ".")
|
|
228
|
+
|
|
229
|
+
# Validate path
|
|
230
|
+
is_allowed, error_message = await self.check_path_allowed(path, tool_ctx)
|
|
231
|
+
if not is_allowed:
|
|
232
|
+
return error_message
|
|
233
|
+
|
|
234
|
+
await tool_ctx.info(f"Indexing symbols in {path}...")
|
|
235
|
+
|
|
236
|
+
files_to_process = self._get_source_files(path)
|
|
237
|
+
if not files_to_process:
|
|
238
|
+
return f"No source code files found in {path}"
|
|
239
|
+
|
|
240
|
+
# Clear cache for this path
|
|
241
|
+
self._symbol_cache[path] = {
|
|
242
|
+
"functions": [],
|
|
243
|
+
"classes": [],
|
|
244
|
+
"methods": [],
|
|
245
|
+
"variables": [],
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
indexed_count = 0
|
|
249
|
+
symbol_count = 0
|
|
250
|
+
|
|
251
|
+
for file_path in files_to_process:
|
|
252
|
+
try:
|
|
253
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
254
|
+
code = f.read()
|
|
255
|
+
|
|
256
|
+
tc = TreeContext(file_path, code, color=False, verbose=False)
|
|
257
|
+
|
|
258
|
+
# Extract symbols (simplified - would need proper tree-sitter queries)
|
|
259
|
+
# This is a placeholder for actual symbol extraction
|
|
260
|
+
symbols = self._extract_symbols(tc, file_path)
|
|
261
|
+
|
|
262
|
+
for symbol_type, syms in symbols.items():
|
|
263
|
+
self._symbol_cache[path][symbol_type].extend(syms)
|
|
264
|
+
symbol_count += len(syms)
|
|
265
|
+
|
|
266
|
+
indexed_count += 1
|
|
267
|
+
|
|
268
|
+
except Exception as e:
|
|
269
|
+
await tool_ctx.warning(f"Could not index {file_path}: {str(e)}")
|
|
270
|
+
|
|
271
|
+
output = [f"=== Symbol Indexing Complete ==="]
|
|
272
|
+
output.append(f"Indexed {indexed_count} files")
|
|
273
|
+
output.append(f"Found {symbol_count} total symbols:")
|
|
274
|
+
|
|
275
|
+
for symbol_type, symbols in self._symbol_cache[path].items():
|
|
276
|
+
if symbols:
|
|
277
|
+
output.append(f" {symbol_type}: {len(symbols)}")
|
|
278
|
+
|
|
279
|
+
return "\n".join(output)
|
|
280
|
+
|
|
281
|
+
async def _handle_query(self, params: Dict[str, Any], tool_ctx) -> str:
|
|
282
|
+
"""Query indexed symbols."""
|
|
283
|
+
path = params.get("path", ".")
|
|
284
|
+
symbol_type = params.get("symbol_type")
|
|
285
|
+
pattern = params.get("pattern")
|
|
286
|
+
limit = params.get("limit", 50)
|
|
287
|
+
|
|
288
|
+
# Check if we have indexed this path
|
|
289
|
+
if path not in self._symbol_cache:
|
|
290
|
+
return f"No symbols indexed for {path}. Run 'symbols --action index --path {path}' first."
|
|
291
|
+
|
|
292
|
+
symbols = self._symbol_cache[path]
|
|
293
|
+
results = []
|
|
294
|
+
|
|
295
|
+
# Filter by type if specified
|
|
296
|
+
if symbol_type:
|
|
297
|
+
if symbol_type in symbols:
|
|
298
|
+
candidates = symbols[symbol_type]
|
|
299
|
+
else:
|
|
300
|
+
return f"Unknown symbol type: {symbol_type}. Valid types: {', '.join(symbols.keys())}"
|
|
301
|
+
else:
|
|
302
|
+
# Combine all symbol types
|
|
303
|
+
candidates = []
|
|
304
|
+
for syms in symbols.values():
|
|
305
|
+
candidates.extend(syms)
|
|
306
|
+
|
|
307
|
+
# Filter by pattern if specified
|
|
308
|
+
if pattern:
|
|
309
|
+
filtered = []
|
|
310
|
+
for sym in candidates:
|
|
311
|
+
if pattern.lower() in sym["name"].lower():
|
|
312
|
+
filtered.append(sym)
|
|
313
|
+
candidates = filtered
|
|
314
|
+
|
|
315
|
+
# Limit results
|
|
316
|
+
candidates = candidates[:limit]
|
|
317
|
+
|
|
318
|
+
if not candidates:
|
|
319
|
+
return "No symbols found matching criteria"
|
|
320
|
+
|
|
321
|
+
output = [f"=== Symbol Query Results ==="]
|
|
322
|
+
output.append(f"Found {len(candidates)} symbols\n")
|
|
323
|
+
|
|
324
|
+
for sym in candidates:
|
|
325
|
+
output.append(f"{sym['type']}: {sym['name']}")
|
|
326
|
+
output.append(f" File: {sym['file']}:{sym['line']}")
|
|
327
|
+
if sym.get("signature"):
|
|
328
|
+
output.append(f" Signature: {sym['signature']}")
|
|
329
|
+
output.append("")
|
|
330
|
+
|
|
331
|
+
return "\n".join(output)
|
|
332
|
+
|
|
333
|
+
async def _handle_list(self, params: Dict[str, Any], tool_ctx) -> str:
|
|
334
|
+
"""List all symbols in a path."""
|
|
335
|
+
# Similar to query but shows all symbols
|
|
336
|
+
params["pattern"] = None
|
|
337
|
+
return await self._handle_query(params, tool_ctx)
|
|
338
|
+
|
|
339
|
+
def _get_source_files(self, path: str) -> List[str]:
|
|
340
|
+
"""Get all source code files in a path."""
|
|
341
|
+
path_obj = Path(path)
|
|
342
|
+
files_to_process = []
|
|
343
|
+
|
|
344
|
+
# Common source file extensions
|
|
345
|
+
extensions = {
|
|
346
|
+
".py", ".js", ".ts", ".jsx", ".tsx", ".java", ".cpp", ".c", ".h",
|
|
347
|
+
".hpp", ".cs", ".rb", ".go", ".rs", ".swift", ".kt", ".scala",
|
|
348
|
+
".php", ".lua", ".r", ".jl", ".ex", ".exs", ".clj", ".cljs"
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if path_obj.is_file():
|
|
352
|
+
if path_obj.suffix in extensions:
|
|
353
|
+
files_to_process.append(str(path_obj))
|
|
354
|
+
elif path_obj.is_dir():
|
|
355
|
+
for root, _, files in os.walk(path_obj):
|
|
356
|
+
for file in files:
|
|
357
|
+
file_path = Path(root) / file
|
|
358
|
+
if file_path.suffix in extensions and self.is_path_allowed(str(file_path)):
|
|
359
|
+
files_to_process.append(str(file_path))
|
|
360
|
+
|
|
361
|
+
return files_to_process
|
|
362
|
+
|
|
363
|
+
def _extract_symbols(self, tc: TreeContext, file_path: str) -> Dict[str, List[Dict[str, Any]]]:
|
|
364
|
+
"""Extract symbols from a TreeContext (placeholder implementation)."""
|
|
365
|
+
# This would need proper tree-sitter queries to extract symbols
|
|
366
|
+
# For now, return empty structure
|
|
367
|
+
return {
|
|
368
|
+
"functions": [],
|
|
369
|
+
"classes": [],
|
|
370
|
+
"methods": [],
|
|
371
|
+
"variables": [],
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
def register(self, mcp_server) -> None:
|
|
375
|
+
"""Register this tool with the MCP server."""
|
|
376
|
+
pass
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"""Tree tool implementation.
|
|
2
|
+
|
|
3
|
+
Unix-style tree command for directory visualization.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Annotated, TypedDict, Unpack, final, override, Optional, List
|
|
9
|
+
|
|
10
|
+
from mcp.server.fastmcp import Context as MCPContext
|
|
11
|
+
from pydantic import Field
|
|
12
|
+
|
|
13
|
+
from hanzo_mcp.tools.filesystem.base import FilesystemBaseTool
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Parameter types
|
|
17
|
+
TreePath = Annotated[
|
|
18
|
+
str,
|
|
19
|
+
Field(
|
|
20
|
+
description="Directory path to display",
|
|
21
|
+
default=".",
|
|
22
|
+
),
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
Depth = Annotated[
|
|
26
|
+
Optional[int],
|
|
27
|
+
Field(
|
|
28
|
+
description="Maximum depth to display",
|
|
29
|
+
default=None,
|
|
30
|
+
),
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
ShowHidden = Annotated[
|
|
34
|
+
bool,
|
|
35
|
+
Field(
|
|
36
|
+
description="Show hidden files (starting with .)",
|
|
37
|
+
default=False,
|
|
38
|
+
),
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
DirsOnly = Annotated[
|
|
42
|
+
bool,
|
|
43
|
+
Field(
|
|
44
|
+
description="Show only directories",
|
|
45
|
+
default=False,
|
|
46
|
+
),
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
ShowSize = Annotated[
|
|
50
|
+
bool,
|
|
51
|
+
Field(
|
|
52
|
+
description="Show file sizes",
|
|
53
|
+
default=False,
|
|
54
|
+
),
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
Pattern = Annotated[
|
|
58
|
+
Optional[str],
|
|
59
|
+
Field(
|
|
60
|
+
description="Only show files matching pattern",
|
|
61
|
+
default=None,
|
|
62
|
+
),
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class TreeParams(TypedDict, total=False):
|
|
67
|
+
"""Parameters for tree tool."""
|
|
68
|
+
path: str
|
|
69
|
+
depth: Optional[int]
|
|
70
|
+
show_hidden: bool
|
|
71
|
+
dirs_only: bool
|
|
72
|
+
show_size: bool
|
|
73
|
+
pattern: Optional[str]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@final
|
|
77
|
+
class TreeTool(FilesystemBaseTool):
|
|
78
|
+
"""Unix-style tree command for directory visualization."""
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
@override
|
|
82
|
+
def name(self) -> str:
|
|
83
|
+
"""Get the tool name."""
|
|
84
|
+
return "tree"
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
@override
|
|
88
|
+
def description(self) -> str:
|
|
89
|
+
"""Get the tool description."""
|
|
90
|
+
return """Directory tree visualization.
|
|
91
|
+
|
|
92
|
+
Usage:
|
|
93
|
+
tree
|
|
94
|
+
tree ./src --depth 2
|
|
95
|
+
tree --dirs-only
|
|
96
|
+
tree --pattern "*.py" --show-size"""
|
|
97
|
+
|
|
98
|
+
@override
|
|
99
|
+
async def call(
|
|
100
|
+
self,
|
|
101
|
+
ctx: MCPContext,
|
|
102
|
+
**params: Unpack[TreeParams],
|
|
103
|
+
) -> str:
|
|
104
|
+
"""Execute tree command."""
|
|
105
|
+
tool_ctx = self.create_tool_context(ctx)
|
|
106
|
+
|
|
107
|
+
# Extract parameters
|
|
108
|
+
path = params.get("path", ".")
|
|
109
|
+
max_depth = params.get("depth")
|
|
110
|
+
show_hidden = params.get("show_hidden", False)
|
|
111
|
+
dirs_only = params.get("dirs_only", False)
|
|
112
|
+
show_size = params.get("show_size", False)
|
|
113
|
+
pattern = params.get("pattern")
|
|
114
|
+
|
|
115
|
+
# Validate path
|
|
116
|
+
path_validation = self.validate_path(path)
|
|
117
|
+
if path_validation.is_error:
|
|
118
|
+
return f"Error: {path_validation.error_message}"
|
|
119
|
+
|
|
120
|
+
# Check permissions
|
|
121
|
+
allowed, error_msg = await self.check_path_allowed(path, tool_ctx)
|
|
122
|
+
if not allowed:
|
|
123
|
+
return error_msg
|
|
124
|
+
|
|
125
|
+
# Check existence
|
|
126
|
+
exists, error_msg = await self.check_path_exists(path, tool_ctx)
|
|
127
|
+
if not exists:
|
|
128
|
+
return error_msg
|
|
129
|
+
|
|
130
|
+
path_obj = Path(path)
|
|
131
|
+
if not path_obj.is_dir():
|
|
132
|
+
return f"Error: {path} is not a directory"
|
|
133
|
+
|
|
134
|
+
# Build tree
|
|
135
|
+
output = [str(path_obj)]
|
|
136
|
+
stats = {"dirs": 0, "files": 0}
|
|
137
|
+
|
|
138
|
+
self._build_tree(
|
|
139
|
+
path_obj,
|
|
140
|
+
output,
|
|
141
|
+
stats,
|
|
142
|
+
prefix="",
|
|
143
|
+
is_last=True,
|
|
144
|
+
current_depth=0,
|
|
145
|
+
max_depth=max_depth,
|
|
146
|
+
show_hidden=show_hidden,
|
|
147
|
+
dirs_only=dirs_only,
|
|
148
|
+
show_size=show_size,
|
|
149
|
+
pattern=pattern
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Add summary
|
|
153
|
+
output.append("")
|
|
154
|
+
if dirs_only:
|
|
155
|
+
output.append(f"{stats['dirs']} directories")
|
|
156
|
+
else:
|
|
157
|
+
output.append(f"{stats['dirs']} directories, {stats['files']} files")
|
|
158
|
+
|
|
159
|
+
return "\n".join(output)
|
|
160
|
+
|
|
161
|
+
def _build_tree(
|
|
162
|
+
self,
|
|
163
|
+
path: Path,
|
|
164
|
+
output: List[str],
|
|
165
|
+
stats: dict,
|
|
166
|
+
prefix: str,
|
|
167
|
+
is_last: bool,
|
|
168
|
+
current_depth: int,
|
|
169
|
+
max_depth: Optional[int],
|
|
170
|
+
show_hidden: bool,
|
|
171
|
+
dirs_only: bool,
|
|
172
|
+
show_size: bool,
|
|
173
|
+
pattern: Optional[str],
|
|
174
|
+
) -> None:
|
|
175
|
+
"""Recursively build tree structure."""
|
|
176
|
+
# Check depth limit
|
|
177
|
+
if max_depth is not None and current_depth >= max_depth:
|
|
178
|
+
return
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
# Get entries
|
|
182
|
+
entries = list(path.iterdir())
|
|
183
|
+
|
|
184
|
+
# Filter hidden files
|
|
185
|
+
if not show_hidden:
|
|
186
|
+
entries = [e for e in entries if not e.name.startswith(".")]
|
|
187
|
+
|
|
188
|
+
# Filter by pattern
|
|
189
|
+
if pattern:
|
|
190
|
+
import fnmatch
|
|
191
|
+
entries = [e for e in entries if fnmatch.fnmatch(e.name, pattern) or e.is_dir()]
|
|
192
|
+
|
|
193
|
+
# Filter dirs only
|
|
194
|
+
if dirs_only:
|
|
195
|
+
entries = [e for e in entries if e.is_dir()]
|
|
196
|
+
|
|
197
|
+
# Sort entries (dirs first, then alphabetically)
|
|
198
|
+
entries.sort(key=lambda e: (not e.is_dir(), e.name.lower()))
|
|
199
|
+
|
|
200
|
+
# Process each entry
|
|
201
|
+
for i, entry in enumerate(entries):
|
|
202
|
+
is_last_entry = i == len(entries) - 1
|
|
203
|
+
|
|
204
|
+
# Skip if not allowed
|
|
205
|
+
if not self.is_path_allowed(str(entry)):
|
|
206
|
+
continue
|
|
207
|
+
|
|
208
|
+
# Build the tree branch
|
|
209
|
+
if prefix:
|
|
210
|
+
if is_last_entry:
|
|
211
|
+
branch = prefix + "└── "
|
|
212
|
+
extension = prefix + " "
|
|
213
|
+
else:
|
|
214
|
+
branch = prefix + "├── "
|
|
215
|
+
extension = prefix + "│ "
|
|
216
|
+
else:
|
|
217
|
+
branch = ""
|
|
218
|
+
extension = ""
|
|
219
|
+
|
|
220
|
+
# Build entry line
|
|
221
|
+
line = branch + entry.name
|
|
222
|
+
|
|
223
|
+
# Add size if requested
|
|
224
|
+
if show_size and entry.is_file():
|
|
225
|
+
try:
|
|
226
|
+
size = entry.stat().st_size
|
|
227
|
+
line += f" ({self._format_size(size)})"
|
|
228
|
+
except:
|
|
229
|
+
pass
|
|
230
|
+
|
|
231
|
+
output.append(line)
|
|
232
|
+
|
|
233
|
+
# Update stats
|
|
234
|
+
if entry.is_dir():
|
|
235
|
+
stats["dirs"] += 1
|
|
236
|
+
# Recurse into directory
|
|
237
|
+
self._build_tree(
|
|
238
|
+
entry,
|
|
239
|
+
output,
|
|
240
|
+
stats,
|
|
241
|
+
prefix=extension,
|
|
242
|
+
is_last=is_last_entry,
|
|
243
|
+
current_depth=current_depth + 1,
|
|
244
|
+
max_depth=max_depth,
|
|
245
|
+
show_hidden=show_hidden,
|
|
246
|
+
dirs_only=dirs_only,
|
|
247
|
+
show_size=show_size,
|
|
248
|
+
pattern=pattern
|
|
249
|
+
)
|
|
250
|
+
else:
|
|
251
|
+
stats["files"] += 1
|
|
252
|
+
|
|
253
|
+
except PermissionError:
|
|
254
|
+
output.append(prefix + "[Permission Denied]")
|
|
255
|
+
except Exception as e:
|
|
256
|
+
output.append(prefix + f"[Error: {str(e)}]")
|
|
257
|
+
|
|
258
|
+
def _format_size(self, size: int) -> str:
|
|
259
|
+
"""Format file size in human-readable format."""
|
|
260
|
+
for unit in ["B", "KB", "MB", "GB", "TB"]:
|
|
261
|
+
if size < 1024.0:
|
|
262
|
+
return f"{size:.1f}{unit}"
|
|
263
|
+
size /= 1024.0
|
|
264
|
+
return f"{size:.1f}PB"
|
|
265
|
+
|
|
266
|
+
def register(self, mcp_server) -> None:
|
|
267
|
+
"""Register this tool with the MCP server."""
|
|
268
|
+
pass
|