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.
- hanzo_mcp/__init__.py +1 -3
- hanzo_mcp/analytics/posthog_analytics.py +4 -17
- hanzo_mcp/bridge.py +9 -25
- hanzo_mcp/cli.py +8 -17
- hanzo_mcp/cli_enhanced.py +5 -14
- hanzo_mcp/cli_plugin.py +3 -9
- hanzo_mcp/config/settings.py +6 -20
- hanzo_mcp/config/tool_config.py +2 -4
- hanzo_mcp/core/base_agent.py +88 -88
- hanzo_mcp/core/model_registry.py +238 -210
- hanzo_mcp/dev_server.py +5 -15
- hanzo_mcp/prompts/__init__.py +2 -6
- hanzo_mcp/prompts/project_todo_reminder.py +3 -9
- hanzo_mcp/prompts/tool_explorer.py +1 -3
- hanzo_mcp/prompts/utils.py +7 -21
- hanzo_mcp/server.py +6 -7
- hanzo_mcp/tools/__init__.py +29 -32
- hanzo_mcp/tools/agent/__init__.py +2 -1
- hanzo_mcp/tools/agent/agent.py +10 -30
- hanzo_mcp/tools/agent/agent_tool.py +23 -17
- hanzo_mcp/tools/agent/claude_desktop_auth.py +3 -9
- hanzo_mcp/tools/agent/cli_agent_base.py +7 -24
- hanzo_mcp/tools/agent/cli_tools.py +76 -75
- hanzo_mcp/tools/agent/code_auth.py +1 -3
- hanzo_mcp/tools/agent/code_auth_tool.py +2 -6
- hanzo_mcp/tools/agent/critic_tool.py +8 -24
- hanzo_mcp/tools/agent/iching_tool.py +12 -36
- hanzo_mcp/tools/agent/network_tool.py +7 -18
- hanzo_mcp/tools/agent/prompt.py +1 -5
- hanzo_mcp/tools/agent/review_tool.py +10 -25
- hanzo_mcp/tools/agent/swarm_alias.py +1 -3
- hanzo_mcp/tools/agent/unified_cli_tools.py +38 -38
- hanzo_mcp/tools/common/batch_tool.py +15 -45
- hanzo_mcp/tools/common/config_tool.py +9 -28
- hanzo_mcp/tools/common/context.py +1 -3
- hanzo_mcp/tools/common/critic_tool.py +1 -3
- hanzo_mcp/tools/common/decorators.py +2 -6
- hanzo_mcp/tools/common/enhanced_base.py +2 -6
- hanzo_mcp/tools/common/fastmcp_pagination.py +4 -12
- hanzo_mcp/tools/common/forgiving_edit.py +9 -28
- hanzo_mcp/tools/common/mode.py +1 -5
- hanzo_mcp/tools/common/paginated_base.py +3 -11
- hanzo_mcp/tools/common/paginated_response.py +10 -30
- hanzo_mcp/tools/common/pagination.py +3 -9
- hanzo_mcp/tools/common/path_utils.py +34 -0
- hanzo_mcp/tools/common/permissions.py +14 -13
- hanzo_mcp/tools/common/personality.py +983 -701
- hanzo_mcp/tools/common/plugin_loader.py +3 -15
- hanzo_mcp/tools/common/stats.py +7 -19
- hanzo_mcp/tools/common/thinking_tool.py +1 -3
- hanzo_mcp/tools/common/tool_disable.py +2 -6
- hanzo_mcp/tools/common/tool_list.py +2 -6
- hanzo_mcp/tools/common/validation.py +1 -3
- hanzo_mcp/tools/compiler/__init__.py +8 -0
- hanzo_mcp/tools/compiler/sandboxed_compiler.py +681 -0
- hanzo_mcp/tools/config/config_tool.py +7 -13
- hanzo_mcp/tools/config/index_config.py +1 -3
- hanzo_mcp/tools/config/mode_tool.py +5 -15
- hanzo_mcp/tools/database/database_manager.py +3 -9
- hanzo_mcp/tools/database/graph.py +1 -3
- hanzo_mcp/tools/database/graph_add.py +3 -9
- hanzo_mcp/tools/database/graph_query.py +11 -34
- hanzo_mcp/tools/database/graph_remove.py +3 -9
- hanzo_mcp/tools/database/graph_search.py +6 -20
- hanzo_mcp/tools/database/graph_stats.py +11 -33
- hanzo_mcp/tools/database/sql.py +4 -12
- hanzo_mcp/tools/database/sql_query.py +6 -10
- hanzo_mcp/tools/database/sql_search.py +2 -6
- hanzo_mcp/tools/database/sql_stats.py +5 -15
- hanzo_mcp/tools/editor/neovim_command.py +1 -3
- hanzo_mcp/tools/editor/neovim_session.py +7 -13
- hanzo_mcp/tools/environment/__init__.py +8 -0
- hanzo_mcp/tools/environment/environment_detector.py +594 -0
- hanzo_mcp/tools/filesystem/__init__.py +28 -26
- hanzo_mcp/tools/filesystem/ast_multi_edit.py +14 -43
- hanzo_mcp/tools/filesystem/ast_tool.py +3 -0
- hanzo_mcp/tools/filesystem/base.py +20 -12
- hanzo_mcp/tools/filesystem/content_replace.py +7 -12
- hanzo_mcp/tools/filesystem/diff.py +2 -10
- hanzo_mcp/tools/filesystem/directory_tree.py +285 -51
- hanzo_mcp/tools/filesystem/edit.py +10 -18
- hanzo_mcp/tools/filesystem/find.py +312 -179
- hanzo_mcp/tools/filesystem/git_search.py +12 -24
- hanzo_mcp/tools/filesystem/multi_edit.py +10 -18
- hanzo_mcp/tools/filesystem/read.py +14 -30
- hanzo_mcp/tools/filesystem/rules_tool.py +9 -17
- hanzo_mcp/tools/filesystem/search.py +1160 -0
- hanzo_mcp/tools/filesystem/watch.py +2 -4
- hanzo_mcp/tools/filesystem/write.py +7 -10
- hanzo_mcp/tools/framework/__init__.py +8 -0
- hanzo_mcp/tools/framework/framework_modes.py +714 -0
- hanzo_mcp/tools/jupyter/base.py +6 -20
- hanzo_mcp/tools/jupyter/jupyter.py +4 -12
- hanzo_mcp/tools/llm/consensus_tool.py +8 -24
- hanzo_mcp/tools/llm/llm_manage.py +2 -6
- hanzo_mcp/tools/llm/llm_tool.py +17 -58
- hanzo_mcp/tools/llm/llm_unified.py +18 -59
- hanzo_mcp/tools/llm/provider_tools.py +1 -3
- hanzo_mcp/tools/lsp/lsp_tool.py +621 -481
- hanzo_mcp/tools/mcp/mcp_add.py +3 -5
- hanzo_mcp/tools/mcp/mcp_remove.py +1 -1
- hanzo_mcp/tools/mcp/mcp_stats.py +1 -3
- hanzo_mcp/tools/mcp/mcp_tool.py +9 -23
- hanzo_mcp/tools/memory/__init__.py +33 -40
- hanzo_mcp/tools/memory/conversation_memory.py +636 -0
- hanzo_mcp/tools/memory/knowledge_tools.py +7 -25
- hanzo_mcp/tools/memory/memory_tools.py +7 -19
- hanzo_mcp/tools/search/find_tool.py +12 -34
- hanzo_mcp/tools/search/unified_search.py +27 -81
- hanzo_mcp/tools/shell/__init__.py +16 -4
- hanzo_mcp/tools/shell/auto_background.py +2 -6
- hanzo_mcp/tools/shell/base.py +1 -5
- hanzo_mcp/tools/shell/base_process.py +5 -7
- hanzo_mcp/tools/shell/bash_session.py +7 -24
- hanzo_mcp/tools/shell/bash_session_executor.py +5 -15
- hanzo_mcp/tools/shell/bash_tool.py +3 -7
- hanzo_mcp/tools/shell/command_executor.py +26 -79
- hanzo_mcp/tools/shell/logs.py +4 -16
- hanzo_mcp/tools/shell/npx.py +2 -8
- hanzo_mcp/tools/shell/npx_tool.py +1 -3
- hanzo_mcp/tools/shell/pkill.py +4 -12
- hanzo_mcp/tools/shell/process_tool.py +2 -8
- hanzo_mcp/tools/shell/processes.py +5 -17
- hanzo_mcp/tools/shell/run_background.py +1 -3
- hanzo_mcp/tools/shell/run_command.py +1 -3
- hanzo_mcp/tools/shell/run_command_windows.py +1 -3
- hanzo_mcp/tools/shell/run_tool.py +56 -0
- hanzo_mcp/tools/shell/session_manager.py +2 -6
- hanzo_mcp/tools/shell/session_storage.py +2 -6
- hanzo_mcp/tools/shell/streaming_command.py +7 -23
- hanzo_mcp/tools/shell/uvx.py +4 -14
- hanzo_mcp/tools/shell/uvx_background.py +2 -6
- hanzo_mcp/tools/shell/uvx_tool.py +1 -3
- hanzo_mcp/tools/shell/zsh_tool.py +12 -20
- hanzo_mcp/tools/todo/todo.py +1 -3
- hanzo_mcp/tools/vector/__init__.py +97 -50
- hanzo_mcp/tools/vector/ast_analyzer.py +6 -20
- hanzo_mcp/tools/vector/git_ingester.py +10 -30
- hanzo_mcp/tools/vector/index_tool.py +3 -9
- hanzo_mcp/tools/vector/infinity_store.py +11 -30
- hanzo_mcp/tools/vector/mock_infinity.py +159 -0
- hanzo_mcp/tools/vector/node_tool.py +538 -0
- hanzo_mcp/tools/vector/project_manager.py +4 -12
- hanzo_mcp/tools/vector/unified_vector.py +384 -0
- hanzo_mcp/tools/vector/vector.py +2 -6
- hanzo_mcp/tools/vector/vector_index.py +8 -8
- hanzo_mcp/tools/vector/vector_search.py +7 -21
- {hanzo_mcp-0.8.8.dist-info → hanzo_mcp-0.9.0.dist-info}/METADATA +2 -2
- hanzo_mcp-0.9.0.dist-info/RECORD +191 -0
- hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +0 -645
- hanzo_mcp/tools/agent/swarm_tool.py +0 -723
- hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +0 -577
- hanzo_mcp/tools/filesystem/batch_search.py +0 -900
- hanzo_mcp/tools/filesystem/directory_tree_paginated.py +0 -350
- hanzo_mcp/tools/filesystem/find_files.py +0 -369
- hanzo_mcp/tools/filesystem/grep.py +0 -467
- hanzo_mcp/tools/filesystem/search_tool.py +0 -767
- hanzo_mcp/tools/filesystem/symbols_tool.py +0 -515
- hanzo_mcp/tools/filesystem/tree.py +0 -270
- hanzo_mcp/tools/jupyter/notebook_edit.py +0 -317
- hanzo_mcp/tools/jupyter/notebook_read.py +0 -147
- hanzo_mcp/tools/todo/todo_read.py +0 -143
- hanzo_mcp/tools/todo/todo_write.py +0 -374
- hanzo_mcp-0.8.8.dist-info/RECORD +0 -192
- {hanzo_mcp-0.8.8.dist-info → hanzo_mcp-0.9.0.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.8.8.dist-info → hanzo_mcp-0.9.0.dist-info}/entry_points.txt +0 -0
- {hanzo_mcp-0.8.8.dist-info → hanzo_mcp-0.9.0.dist-info}/top_level.txt +0 -0
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
"""Directory tree tool implementation.
|
|
2
2
|
|
|
3
|
-
This module provides the DirectoryTreeTool for viewing file and directory structures
|
|
3
|
+
This module provides the DirectoryTreeTool for viewing file and directory structures
|
|
4
|
+
with optional pagination and different display styles.
|
|
4
5
|
"""
|
|
5
6
|
|
|
6
|
-
from typing import Any, Unpack, Annotated, TypedDict, final, override
|
|
7
|
+
from typing import Any, Dict, List, Union, Unpack, Optional, Literal, Annotated, TypedDict, final, override
|
|
7
8
|
from pathlib import Path
|
|
9
|
+
import fnmatch
|
|
8
10
|
|
|
9
11
|
from pydantic import Field
|
|
10
12
|
from mcp.server import FastMCP
|
|
@@ -12,6 +14,10 @@ from mcp.server.fastmcp import Context as MCPContext
|
|
|
12
14
|
|
|
13
15
|
from hanzo_mcp.tools.common.truncate import truncate_response
|
|
14
16
|
from hanzo_mcp.tools.filesystem.base import FilesystemBaseTool
|
|
17
|
+
from hanzo_mcp.tools.common.pagination import (
|
|
18
|
+
CursorManager,
|
|
19
|
+
paginate_list,
|
|
20
|
+
)
|
|
15
21
|
|
|
16
22
|
DirectoryPath = Annotated[
|
|
17
23
|
str,
|
|
@@ -39,19 +45,52 @@ IncludeFiltered = Annotated[
|
|
|
39
45
|
),
|
|
40
46
|
]
|
|
41
47
|
|
|
48
|
+
PageSize = Annotated[
|
|
49
|
+
Optional[int],
|
|
50
|
+
Field(
|
|
51
|
+
default=None,
|
|
52
|
+
description="Number of entries per page (enables pagination when set)",
|
|
53
|
+
title="Page Size",
|
|
54
|
+
),
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
Page = Annotated[
|
|
58
|
+
int,
|
|
59
|
+
Field(
|
|
60
|
+
default=1,
|
|
61
|
+
description="Page number for pagination",
|
|
62
|
+
title="Page",
|
|
63
|
+
),
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
Style = Annotated[
|
|
67
|
+
Literal["compact", "detailed", "unix"],
|
|
68
|
+
Field(
|
|
69
|
+
default="compact",
|
|
70
|
+
description="Display style: compact (default), detailed (with sizes), or unix (tree-like)",
|
|
71
|
+
title="Style",
|
|
72
|
+
),
|
|
73
|
+
]
|
|
42
74
|
|
|
43
|
-
|
|
75
|
+
|
|
76
|
+
class DirectoryTreeToolParams(TypedDict, total=False):
|
|
44
77
|
"""Parameters for the DirectoryTreeTool.
|
|
45
78
|
|
|
46
79
|
Attributes:
|
|
47
80
|
path: The path to the directory to view
|
|
48
81
|
depth: The maximum depth to traverse (0 for unlimited)
|
|
49
82
|
include_filtered: Include directories that are normally filtered
|
|
83
|
+
page_size: Number of entries per page (enables pagination when set)
|
|
84
|
+
page: Page number for pagination
|
|
85
|
+
style: Display style (compact, detailed, unix)
|
|
50
86
|
"""
|
|
51
87
|
|
|
52
|
-
path:
|
|
53
|
-
depth:
|
|
54
|
-
include_filtered:
|
|
88
|
+
path: str
|
|
89
|
+
depth: int
|
|
90
|
+
include_filtered: bool
|
|
91
|
+
page_size: Optional[int]
|
|
92
|
+
page: int
|
|
93
|
+
style: Literal["compact", "detailed", "unix"]
|
|
55
94
|
|
|
56
95
|
|
|
57
96
|
@final
|
|
@@ -82,14 +121,21 @@ Returns a structured view of the directory tree with files and subdirectories.
|
|
|
82
121
|
Directories are marked with trailing slashes. The output is formatted as an
|
|
83
122
|
indented list for readability. By default, common development directories like
|
|
84
123
|
.git, node_modules, and venv are noted but not traversed unless explicitly
|
|
85
|
-
requested. Only works within allowed directories.
|
|
124
|
+
requested. Only works within allowed directories.
|
|
125
|
+
|
|
126
|
+
Supports multiple display styles:
|
|
127
|
+
- compact: Simple indented list (default)
|
|
128
|
+
- detailed: Includes file sizes and additional metadata
|
|
129
|
+
- unix: Traditional unix tree command style with ASCII art
|
|
130
|
+
|
|
131
|
+
Optional pagination is available by setting page_size parameter."""
|
|
86
132
|
|
|
87
133
|
@override
|
|
88
134
|
async def call(
|
|
89
135
|
self,
|
|
90
136
|
ctx: MCPContext,
|
|
91
137
|
**params: Unpack[DirectoryTreeToolParams],
|
|
92
|
-
) -> str:
|
|
138
|
+
) -> Union[str, Dict[str, Any]]:
|
|
93
139
|
"""Execute the tool with the given parameters.
|
|
94
140
|
|
|
95
141
|
Args:
|
|
@@ -102,9 +148,18 @@ requested. Only works within allowed directories."""
|
|
|
102
148
|
tool_ctx = self.create_tool_context(ctx)
|
|
103
149
|
|
|
104
150
|
# Extract parameters
|
|
105
|
-
path:
|
|
151
|
+
path: str = params["path"]
|
|
106
152
|
depth = params.get("depth", 3) # Default depth is 3
|
|
107
153
|
include_filtered = params.get("include_filtered", False) # Default to False
|
|
154
|
+
page_size = params.get("page_size") # Optional pagination
|
|
155
|
+
page = params.get("page", 1)
|
|
156
|
+
style = params.get("style", "compact")
|
|
157
|
+
|
|
158
|
+
# Expand path (handles ~, $HOME, etc.)
|
|
159
|
+
path = self.expand_path(path)
|
|
160
|
+
|
|
161
|
+
# For pagination, we need to use offset-based pagination
|
|
162
|
+
offset = (page - 1) * page_size if page_size else None
|
|
108
163
|
|
|
109
164
|
# Validate path parameter
|
|
110
165
|
path_validation = self.validate_path(path)
|
|
@@ -112,9 +167,8 @@ requested. Only works within allowed directories."""
|
|
|
112
167
|
await tool_ctx.error(path_validation.error_message)
|
|
113
168
|
return f"Error: {path_validation.error_message}"
|
|
114
169
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
)
|
|
170
|
+
pagination_info = f" (page {page}, size {page_size})" if page_size else ""
|
|
171
|
+
await tool_ctx.info(f"Getting directory tree: {path} (depth: {depth}, include_filtered: {include_filtered}, style: {style}){pagination_info}")
|
|
118
172
|
|
|
119
173
|
# Check if path is allowed
|
|
120
174
|
allowed, error_msg = await self.check_path_allowed(path, tool_ctx)
|
|
@@ -153,9 +207,7 @@ requested. Only works within allowed directories."""
|
|
|
153
207
|
}
|
|
154
208
|
|
|
155
209
|
# Log filtering settings
|
|
156
|
-
await tool_ctx.info(
|
|
157
|
-
f"Directory tree filtering: include_filtered={include_filtered}"
|
|
158
|
-
)
|
|
210
|
+
await tool_ctx.info(f"Directory tree filtering: include_filtered={include_filtered}")
|
|
159
211
|
|
|
160
212
|
# Check if a directory should be filtered
|
|
161
213
|
def should_filter(current_path: Path) -> bool:
|
|
@@ -165,9 +217,7 @@ requested. Only works within allowed directories."""
|
|
|
165
217
|
return False
|
|
166
218
|
|
|
167
219
|
# Filter based on directory name if filtering is enabled
|
|
168
|
-
return
|
|
169
|
-
current_path.name in FILTERED_DIRECTORIES and not include_filtered
|
|
170
|
-
)
|
|
220
|
+
return current_path.name in FILTERED_DIRECTORIES and not include_filtered
|
|
171
221
|
|
|
172
222
|
# Track stats for summary
|
|
173
223
|
stats = {
|
|
@@ -177,10 +227,116 @@ requested. Only works within allowed directories."""
|
|
|
177
227
|
"skipped_filtered": 0,
|
|
178
228
|
}
|
|
179
229
|
|
|
180
|
-
#
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
230
|
+
# If pagination is enabled, collect entries in a flat list
|
|
231
|
+
if page_size:
|
|
232
|
+
all_entries: List[Dict[str, Any]] = []
|
|
233
|
+
|
|
234
|
+
async def collect_entries(current_path: Path, current_depth: int = 0, parent_path: str = "") -> None:
|
|
235
|
+
"""Collect entries in a flat list for pagination."""
|
|
236
|
+
if not self.is_path_allowed(str(current_path)):
|
|
237
|
+
return
|
|
238
|
+
|
|
239
|
+
try:
|
|
240
|
+
# Sort entries: directories first, then files alphabetically
|
|
241
|
+
entries = sorted(current_path.iterdir(), key=lambda x: (not x.is_dir(), x.name))
|
|
242
|
+
|
|
243
|
+
for entry in entries:
|
|
244
|
+
if not self.is_path_allowed(str(entry)):
|
|
245
|
+
continue
|
|
246
|
+
|
|
247
|
+
# Calculate relative path for display
|
|
248
|
+
relative_path = f"{parent_path}/{entry.name}" if parent_path else entry.name
|
|
249
|
+
|
|
250
|
+
if entry.is_dir():
|
|
251
|
+
stats["directories"] += 1
|
|
252
|
+
entry_data: Dict[str, Any] = {
|
|
253
|
+
"path": relative_path,
|
|
254
|
+
"name": entry.name,
|
|
255
|
+
"type": "directory",
|
|
256
|
+
"depth": current_depth,
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
# Add size info for detailed style
|
|
260
|
+
if style == "detailed":
|
|
261
|
+
try:
|
|
262
|
+
entry_data["size"] = sum(f.stat().st_size for f in entry.rglob('*') if f.is_file())
|
|
263
|
+
except Exception:
|
|
264
|
+
entry_data["size"] = 0
|
|
265
|
+
|
|
266
|
+
# Check if we should filter this directory
|
|
267
|
+
if should_filter(entry):
|
|
268
|
+
entry_data["skipped"] = "filtered-directory"
|
|
269
|
+
stats["skipped_filtered"] += 1
|
|
270
|
+
all_entries.append(entry_data)
|
|
271
|
+
continue
|
|
272
|
+
|
|
273
|
+
# Check depth limit
|
|
274
|
+
if depth > 0 and current_depth >= depth:
|
|
275
|
+
entry_data["skipped"] = "depth-limit"
|
|
276
|
+
stats["skipped_depth"] += 1
|
|
277
|
+
all_entries.append(entry_data)
|
|
278
|
+
continue
|
|
279
|
+
|
|
280
|
+
# Add directory entry
|
|
281
|
+
all_entries.append(entry_data)
|
|
282
|
+
|
|
283
|
+
# Process children recursively
|
|
284
|
+
await collect_entries(entry, current_depth + 1, relative_path)
|
|
285
|
+
else:
|
|
286
|
+
# Add file entry
|
|
287
|
+
if depth <= 0 or current_depth < depth:
|
|
288
|
+
stats["files"] += 1
|
|
289
|
+
file_data = {
|
|
290
|
+
"path": relative_path,
|
|
291
|
+
"name": entry.name,
|
|
292
|
+
"type": "file",
|
|
293
|
+
"depth": current_depth,
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
# Add size info for detailed style
|
|
297
|
+
if style == "detailed":
|
|
298
|
+
try:
|
|
299
|
+
file_data["size"] = entry.stat().st_size
|
|
300
|
+
except Exception:
|
|
301
|
+
file_data["size"] = 0
|
|
302
|
+
|
|
303
|
+
all_entries.append(file_data)
|
|
304
|
+
|
|
305
|
+
except Exception as e:
|
|
306
|
+
await tool_ctx.warning(f"Error processing {current_path}: {str(e)}")
|
|
307
|
+
|
|
308
|
+
# Collect all entries
|
|
309
|
+
await tool_ctx.info("Collecting directory entries for pagination...")
|
|
310
|
+
await collect_entries(dir_path)
|
|
311
|
+
|
|
312
|
+
# Apply pagination using offset
|
|
313
|
+
start_idx = offset if offset else 0
|
|
314
|
+
end_idx = start_idx + page_size
|
|
315
|
+
paginated_entries = all_entries[start_idx:end_idx]
|
|
316
|
+
|
|
317
|
+
# Format entries based on style
|
|
318
|
+
formatted_entries = self._format_entries(paginated_entries, style)
|
|
319
|
+
|
|
320
|
+
# Build paginated response
|
|
321
|
+
response = {
|
|
322
|
+
"entries": formatted_entries,
|
|
323
|
+
"total_entries": len(all_entries),
|
|
324
|
+
"page": page,
|
|
325
|
+
"page_size": page_size,
|
|
326
|
+
"total_pages": (len(all_entries) + page_size - 1) // page_size,
|
|
327
|
+
"has_next": end_idx < len(all_entries),
|
|
328
|
+
"stats": {
|
|
329
|
+
"directories": stats["directories"],
|
|
330
|
+
"files": stats["files"],
|
|
331
|
+
"skipped_depth": stats["skipped_depth"],
|
|
332
|
+
"skipped_filtered": stats["skipped_filtered"],
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return response
|
|
337
|
+
|
|
338
|
+
# Non-paginated: Build the tree recursively
|
|
339
|
+
async def build_tree(current_path: Path, current_depth: int = 0) -> list[dict[str, Any]]:
|
|
184
340
|
result: list[dict[str, Any]] = []
|
|
185
341
|
|
|
186
342
|
# Skip processing if path isn't allowed
|
|
@@ -189,9 +345,7 @@ requested. Only works within allowed directories."""
|
|
|
189
345
|
|
|
190
346
|
try:
|
|
191
347
|
# Sort entries: directories first, then files alphabetically
|
|
192
|
-
entries = sorted(
|
|
193
|
-
current_path.iterdir(), key=lambda x: (not x.is_dir(), x.name)
|
|
194
|
-
)
|
|
348
|
+
entries = sorted(current_path.iterdir(), key=lambda x: (not x.is_dir(), x.name))
|
|
195
349
|
|
|
196
350
|
for entry in entries:
|
|
197
351
|
# Skip entries that aren't allowed
|
|
@@ -204,6 +358,13 @@ requested. Only works within allowed directories."""
|
|
|
204
358
|
"name": entry.name,
|
|
205
359
|
"type": "directory",
|
|
206
360
|
}
|
|
361
|
+
|
|
362
|
+
# Add size info for detailed style
|
|
363
|
+
if style == "detailed":
|
|
364
|
+
try:
|
|
365
|
+
entry_data["size"] = sum(f.stat().st_size for f in entry.rglob('*') if f.is_file())
|
|
366
|
+
except Exception:
|
|
367
|
+
entry_data["size"] = 0
|
|
207
368
|
|
|
208
369
|
# Check if we should filter this directory
|
|
209
370
|
if should_filter(entry):
|
|
@@ -220,54 +381,86 @@ requested. Only works within allowed directories."""
|
|
|
220
381
|
continue
|
|
221
382
|
|
|
222
383
|
# Process children recursively with depth increment
|
|
223
|
-
entry_data["children"] = await build_tree(
|
|
224
|
-
entry, current_depth + 1
|
|
225
|
-
)
|
|
384
|
+
entry_data["children"] = await build_tree(entry, current_depth + 1)
|
|
226
385
|
result.append(entry_data)
|
|
227
386
|
else:
|
|
228
387
|
# Files should be at the same level check as directories
|
|
229
388
|
if depth <= 0 or current_depth < depth:
|
|
230
389
|
stats["files"] += 1
|
|
231
|
-
|
|
232
|
-
|
|
390
|
+
file_data = {"name": entry.name, "type": "file"}
|
|
391
|
+
|
|
392
|
+
# Add size info for detailed style
|
|
393
|
+
if style == "detailed":
|
|
394
|
+
try:
|
|
395
|
+
file_data["size"] = entry.stat().st_size
|
|
396
|
+
except Exception:
|
|
397
|
+
file_data["size"] = 0
|
|
398
|
+
|
|
399
|
+
result.append(file_data)
|
|
233
400
|
|
|
234
401
|
except Exception as e:
|
|
235
402
|
await tool_ctx.warning(f"Error processing {current_path}: {str(e)}")
|
|
236
403
|
|
|
237
404
|
return result
|
|
238
405
|
|
|
239
|
-
# Format the tree
|
|
240
|
-
def format_tree(
|
|
241
|
-
tree_data: list[dict[str, Any]], level: int = 0
|
|
242
|
-
) -> list[str]:
|
|
406
|
+
# Format the tree based on style
|
|
407
|
+
def format_tree(tree_data: list[dict[str, Any]], level: int = 0, prefix: str = "", is_last: bool = True) -> list[str]:
|
|
243
408
|
lines = []
|
|
244
409
|
|
|
245
|
-
for item in tree_data:
|
|
246
|
-
|
|
247
|
-
|
|
410
|
+
for i, item in enumerate(tree_data):
|
|
411
|
+
is_last_item = i == len(tree_data) - 1
|
|
412
|
+
|
|
413
|
+
if style == "unix":
|
|
414
|
+
# Unix tree style with ASCII art
|
|
415
|
+
if level == 0:
|
|
416
|
+
current_prefix = ""
|
|
417
|
+
next_prefix = ""
|
|
418
|
+
else:
|
|
419
|
+
if is_last_item:
|
|
420
|
+
current_prefix = prefix + "└── "
|
|
421
|
+
next_prefix = prefix + " "
|
|
422
|
+
else:
|
|
423
|
+
current_prefix = prefix + "├── "
|
|
424
|
+
next_prefix = prefix + "│ "
|
|
425
|
+
else:
|
|
426
|
+
# Compact or detailed style with simple indentation
|
|
427
|
+
current_prefix = " " * level
|
|
428
|
+
next_prefix = " " * (level + 1)
|
|
248
429
|
|
|
249
430
|
# Format based on type
|
|
250
431
|
if item["type"] == "directory":
|
|
251
432
|
if "skipped" in item:
|
|
252
|
-
|
|
253
|
-
f"{indent}{item['name']}/ [skipped - {item['skipped']}]"
|
|
254
|
-
)
|
|
433
|
+
line = f"{current_prefix}{item['name']}/ [skipped - {item['skipped']}]"
|
|
255
434
|
else:
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
435
|
+
line = f"{current_prefix}{item['name']}/"
|
|
436
|
+
if style == "detailed" and "size" in item:
|
|
437
|
+
line += f" ({self._format_size(item['size'])})"
|
|
438
|
+
lines.append(line)
|
|
439
|
+
|
|
440
|
+
# Add children with increased indentation if present
|
|
441
|
+
if "children" in item and "skipped" not in item:
|
|
442
|
+
lines.extend(format_tree(item["children"], level + 1, next_prefix, is_last_item))
|
|
260
443
|
else:
|
|
261
444
|
# File
|
|
262
|
-
|
|
445
|
+
line = f"{current_prefix}{item['name']}"
|
|
446
|
+
if style == "detailed" and "size" in item:
|
|
447
|
+
line += f" ({self._format_size(item['size'])})"
|
|
448
|
+
lines.append(line)
|
|
263
449
|
|
|
264
450
|
return lines
|
|
265
451
|
|
|
266
452
|
# Build tree starting from the requested directory
|
|
267
453
|
tree_data = await build_tree(dir_path)
|
|
268
454
|
|
|
269
|
-
# Format
|
|
270
|
-
|
|
455
|
+
# Format based on style
|
|
456
|
+
if style == "unix":
|
|
457
|
+
# Start with the root directory name
|
|
458
|
+
formatted_lines = [str(dir_path)]
|
|
459
|
+
formatted_lines.extend(format_tree(tree_data))
|
|
460
|
+
else:
|
|
461
|
+
formatted_lines = format_tree(tree_data)
|
|
462
|
+
|
|
463
|
+
formatted_output = "\n".join(formatted_lines)
|
|
271
464
|
|
|
272
465
|
# Add stats summary
|
|
273
466
|
summary = (
|
|
@@ -277,7 +470,7 @@ requested. Only works within allowed directories."""
|
|
|
277
470
|
)
|
|
278
471
|
|
|
279
472
|
await tool_ctx.info(
|
|
280
|
-
f"Generated directory tree for {path} (depth: {depth}, include_filtered: {include_filtered})"
|
|
473
|
+
f"Generated directory tree for {path} (depth: {depth}, include_filtered: {include_filtered}, style: {style})"
|
|
281
474
|
)
|
|
282
475
|
|
|
283
476
|
# Truncate response to stay within token limits
|
|
@@ -285,11 +478,43 @@ requested. Only works within allowed directories."""
|
|
|
285
478
|
return truncate_response(
|
|
286
479
|
full_response,
|
|
287
480
|
max_tokens=25000,
|
|
288
|
-
truncation_message="\n\n[Response truncated due to token limit. Please use
|
|
481
|
+
truncation_message="\n\n[Response truncated due to token limit. Please use pagination (page_size parameter) or a smaller depth.]",
|
|
289
482
|
)
|
|
290
483
|
except Exception as e:
|
|
291
484
|
await tool_ctx.error(f"Error generating directory tree: {str(e)}")
|
|
485
|
+
if page_size:
|
|
486
|
+
return {"error": f"Error generating directory tree: {str(e)}"}
|
|
292
487
|
return f"Error generating directory tree: {str(e)}"
|
|
488
|
+
|
|
489
|
+
def _format_entries(self, entries: List[Dict[str, Any]], style: str) -> List[str]:
|
|
490
|
+
"""Format entries for paginated output."""
|
|
491
|
+
formatted = []
|
|
492
|
+
for entry in entries:
|
|
493
|
+
indent = " " * entry["depth"]
|
|
494
|
+
name = entry["name"]
|
|
495
|
+
|
|
496
|
+
if entry["type"] == "directory":
|
|
497
|
+
if "skipped" in entry:
|
|
498
|
+
line = f"{indent}{name}/ [skipped - {entry['skipped']}]"
|
|
499
|
+
else:
|
|
500
|
+
line = f"{indent}{name}/"
|
|
501
|
+
if style == "detailed" and "size" in entry:
|
|
502
|
+
line += f" ({self._format_size(entry['size'])})"
|
|
503
|
+
else:
|
|
504
|
+
line = f"{indent}{name}"
|
|
505
|
+
if style == "detailed" and "size" in entry:
|
|
506
|
+
line += f" ({self._format_size(entry['size'])})"
|
|
507
|
+
|
|
508
|
+
formatted.append(line)
|
|
509
|
+
return formatted
|
|
510
|
+
|
|
511
|
+
def _format_size(self, size: int) -> str:
|
|
512
|
+
"""Format file size in human-readable format."""
|
|
513
|
+
for unit in ["B", "KB", "MB", "GB", "TB"]:
|
|
514
|
+
if size < 1024.0:
|
|
515
|
+
return f"{size:.1f}{unit}"
|
|
516
|
+
size /= 1024.0
|
|
517
|
+
return f"{size:.1f}PB"
|
|
293
518
|
|
|
294
519
|
@override
|
|
295
520
|
def register(self, mcp_server: FastMCP) -> None:
|
|
@@ -309,7 +534,16 @@ requested. Only works within allowed directories."""
|
|
|
309
534
|
path: DirectoryPath,
|
|
310
535
|
depth: Depth = 3,
|
|
311
536
|
include_filtered: IncludeFiltered = False,
|
|
312
|
-
|
|
537
|
+
page_size: PageSize = None,
|
|
538
|
+
page: Page = 1,
|
|
539
|
+
style: Style = "compact",
|
|
540
|
+
) -> Union[str, Dict[str, Any]]:
|
|
313
541
|
return await tool_self.call(
|
|
314
|
-
ctx,
|
|
542
|
+
ctx,
|
|
543
|
+
path=path,
|
|
544
|
+
depth=depth,
|
|
545
|
+
include_filtered=include_filtered,
|
|
546
|
+
page_size=page_size,
|
|
547
|
+
page=page,
|
|
548
|
+
style=style
|
|
315
549
|
)
|
|
@@ -117,13 +117,15 @@ Usage:
|
|
|
117
117
|
await tool_ctx.error(path_validation.error_message)
|
|
118
118
|
return f"Error: {path_validation.error_message}"
|
|
119
119
|
|
|
120
|
+
# Expand path first (handles ~, $HOME, etc.)
|
|
121
|
+
expanded_path = self.expand_path(file_path)
|
|
122
|
+
file_path = expanded_path # Use expanded path for all operations
|
|
123
|
+
|
|
120
124
|
# Only validate old_string for non-empty if we're not creating a new file
|
|
121
125
|
# Empty old_string is valid when creating a new file
|
|
122
126
|
file_exists = Path(file_path).exists()
|
|
123
127
|
if file_exists and old_string.strip() == "":
|
|
124
|
-
await tool_ctx.error(
|
|
125
|
-
"Parameter 'old_string' cannot be empty for existing files"
|
|
126
|
-
)
|
|
128
|
+
await tool_ctx.error("Parameter 'old_string' cannot be empty for existing files")
|
|
127
129
|
return "Error: Parameter 'old_string' cannot be empty for existing files"
|
|
128
130
|
|
|
129
131
|
if (
|
|
@@ -131,12 +133,8 @@ Usage:
|
|
|
131
133
|
or not isinstance(expected_replacements, (int, float))
|
|
132
134
|
or expected_replacements < 0
|
|
133
135
|
):
|
|
134
|
-
await tool_ctx.error(
|
|
135
|
-
|
|
136
|
-
)
|
|
137
|
-
return (
|
|
138
|
-
"Error: Parameter 'expected_replacements' must be a non-negative number"
|
|
139
|
-
)
|
|
136
|
+
await tool_ctx.error("Parameter 'expected_replacements' must be a non-negative number")
|
|
137
|
+
return "Error: Parameter 'expected_replacements' must be a non-negative number"
|
|
140
138
|
|
|
141
139
|
await tool_ctx.info(f"Editing file: {file_path}")
|
|
142
140
|
|
|
@@ -164,9 +162,7 @@ Usage:
|
|
|
164
162
|
f.write(new_string)
|
|
165
163
|
|
|
166
164
|
await tool_ctx.info(f"Successfully created file: {file_path}")
|
|
167
|
-
return (
|
|
168
|
-
f"Successfully created file: {file_path} ({len(new_string)} bytes)"
|
|
169
|
-
)
|
|
165
|
+
return f"Successfully created file: {file_path} ({len(new_string)} bytes)"
|
|
170
166
|
|
|
171
167
|
# Check file exists for non-creation operations
|
|
172
168
|
exists, error_msg = await self.check_path_exists(file_path, tool_ctx)
|
|
@@ -199,9 +195,7 @@ Usage:
|
|
|
199
195
|
modified_content = original_content.replace(old_string, new_string)
|
|
200
196
|
else:
|
|
201
197
|
# If we can't find the exact string, report an error
|
|
202
|
-
await tool_ctx.error(
|
|
203
|
-
"The specified old_string was not found in the file content"
|
|
204
|
-
)
|
|
198
|
+
await tool_ctx.error("The specified old_string was not found in the file content")
|
|
205
199
|
return "Error: The specified old_string was not found in the file content. Please check that it matches exactly, including all whitespace and indentation."
|
|
206
200
|
|
|
207
201
|
# Generate diff
|
|
@@ -226,9 +220,7 @@ Usage:
|
|
|
226
220
|
num_backticks += 1
|
|
227
221
|
|
|
228
222
|
# Format diff with appropriate number of backticks
|
|
229
|
-
formatted_diff =
|
|
230
|
-
f"```{num_backticks}diff\n{diff_text}```{num_backticks}\n"
|
|
231
|
-
)
|
|
223
|
+
formatted_diff = f"```{num_backticks}diff\n{diff_text}```{num_backticks}\n"
|
|
232
224
|
|
|
233
225
|
# Write the file if there are changes
|
|
234
226
|
if diff_text:
|