hanzo-mcp 0.8.11__py3-none-any.whl → 0.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of hanzo-mcp might be problematic. Click here for more details.

Files changed (166) hide show
  1. hanzo_mcp/__init__.py +1 -3
  2. hanzo_mcp/analytics/posthog_analytics.py +3 -9
  3. hanzo_mcp/bridge.py +9 -25
  4. hanzo_mcp/cli.py +6 -15
  5. hanzo_mcp/cli_enhanced.py +5 -14
  6. hanzo_mcp/cli_plugin.py +3 -9
  7. hanzo_mcp/config/settings.py +6 -20
  8. hanzo_mcp/config/tool_config.py +1 -3
  9. hanzo_mcp/core/base_agent.py +88 -88
  10. hanzo_mcp/core/model_registry.py +238 -210
  11. hanzo_mcp/dev_server.py +5 -15
  12. hanzo_mcp/prompts/__init__.py +2 -6
  13. hanzo_mcp/prompts/project_todo_reminder.py +3 -9
  14. hanzo_mcp/prompts/tool_explorer.py +1 -3
  15. hanzo_mcp/prompts/utils.py +7 -21
  16. hanzo_mcp/server.py +2 -6
  17. hanzo_mcp/tools/__init__.py +26 -27
  18. hanzo_mcp/tools/agent/__init__.py +2 -1
  19. hanzo_mcp/tools/agent/agent.py +10 -30
  20. hanzo_mcp/tools/agent/agent_tool.py +22 -15
  21. hanzo_mcp/tools/agent/claude_desktop_auth.py +3 -9
  22. hanzo_mcp/tools/agent/cli_agent_base.py +7 -24
  23. hanzo_mcp/tools/agent/cli_tools.py +75 -74
  24. hanzo_mcp/tools/agent/code_auth.py +1 -3
  25. hanzo_mcp/tools/agent/code_auth_tool.py +2 -6
  26. hanzo_mcp/tools/agent/critic_tool.py +8 -24
  27. hanzo_mcp/tools/agent/iching_tool.py +12 -36
  28. hanzo_mcp/tools/agent/network_tool.py +7 -18
  29. hanzo_mcp/tools/agent/prompt.py +1 -5
  30. hanzo_mcp/tools/agent/review_tool.py +10 -25
  31. hanzo_mcp/tools/agent/swarm_alias.py +1 -3
  32. hanzo_mcp/tools/agent/unified_cli_tools.py +38 -38
  33. hanzo_mcp/tools/common/batch_tool.py +15 -45
  34. hanzo_mcp/tools/common/config_tool.py +9 -28
  35. hanzo_mcp/tools/common/context.py +1 -3
  36. hanzo_mcp/tools/common/critic_tool.py +1 -3
  37. hanzo_mcp/tools/common/decorators.py +2 -6
  38. hanzo_mcp/tools/common/enhanced_base.py +2 -6
  39. hanzo_mcp/tools/common/fastmcp_pagination.py +4 -12
  40. hanzo_mcp/tools/common/forgiving_edit.py +9 -28
  41. hanzo_mcp/tools/common/mode.py +1 -5
  42. hanzo_mcp/tools/common/paginated_base.py +3 -11
  43. hanzo_mcp/tools/common/paginated_response.py +10 -30
  44. hanzo_mcp/tools/common/pagination.py +3 -9
  45. hanzo_mcp/tools/common/path_utils.py +34 -0
  46. hanzo_mcp/tools/common/permissions.py +14 -13
  47. hanzo_mcp/tools/common/personality.py +983 -701
  48. hanzo_mcp/tools/common/plugin_loader.py +3 -15
  49. hanzo_mcp/tools/common/stats.py +6 -18
  50. hanzo_mcp/tools/common/thinking_tool.py +1 -3
  51. hanzo_mcp/tools/common/tool_disable.py +2 -6
  52. hanzo_mcp/tools/common/tool_list.py +2 -6
  53. hanzo_mcp/tools/common/validation.py +1 -3
  54. hanzo_mcp/tools/compiler/__init__.py +8 -0
  55. hanzo_mcp/tools/compiler/sandboxed_compiler.py +681 -0
  56. hanzo_mcp/tools/config/config_tool.py +7 -13
  57. hanzo_mcp/tools/config/index_config.py +1 -3
  58. hanzo_mcp/tools/config/mode_tool.py +5 -15
  59. hanzo_mcp/tools/database/database_manager.py +3 -9
  60. hanzo_mcp/tools/database/graph.py +1 -3
  61. hanzo_mcp/tools/database/graph_add.py +3 -9
  62. hanzo_mcp/tools/database/graph_query.py +11 -34
  63. hanzo_mcp/tools/database/graph_remove.py +3 -9
  64. hanzo_mcp/tools/database/graph_search.py +6 -20
  65. hanzo_mcp/tools/database/graph_stats.py +11 -33
  66. hanzo_mcp/tools/database/sql.py +4 -12
  67. hanzo_mcp/tools/database/sql_query.py +6 -10
  68. hanzo_mcp/tools/database/sql_search.py +2 -6
  69. hanzo_mcp/tools/database/sql_stats.py +5 -15
  70. hanzo_mcp/tools/editor/neovim_command.py +1 -3
  71. hanzo_mcp/tools/editor/neovim_session.py +7 -13
  72. hanzo_mcp/tools/environment/__init__.py +8 -0
  73. hanzo_mcp/tools/environment/environment_detector.py +594 -0
  74. hanzo_mcp/tools/filesystem/__init__.py +28 -26
  75. hanzo_mcp/tools/filesystem/ast_multi_edit.py +14 -43
  76. hanzo_mcp/tools/filesystem/ast_tool.py +3 -0
  77. hanzo_mcp/tools/filesystem/base.py +20 -12
  78. hanzo_mcp/tools/filesystem/content_replace.py +7 -12
  79. hanzo_mcp/tools/filesystem/diff.py +2 -10
  80. hanzo_mcp/tools/filesystem/directory_tree.py +285 -51
  81. hanzo_mcp/tools/filesystem/edit.py +10 -18
  82. hanzo_mcp/tools/filesystem/find.py +312 -179
  83. hanzo_mcp/tools/filesystem/git_search.py +12 -24
  84. hanzo_mcp/tools/filesystem/multi_edit.py +10 -18
  85. hanzo_mcp/tools/filesystem/read.py +14 -30
  86. hanzo_mcp/tools/filesystem/rules_tool.py +9 -17
  87. hanzo_mcp/tools/filesystem/search.py +1160 -0
  88. hanzo_mcp/tools/filesystem/watch.py +2 -4
  89. hanzo_mcp/tools/filesystem/write.py +7 -10
  90. hanzo_mcp/tools/framework/__init__.py +8 -0
  91. hanzo_mcp/tools/framework/framework_modes.py +714 -0
  92. hanzo_mcp/tools/jupyter/base.py +6 -20
  93. hanzo_mcp/tools/jupyter/jupyter.py +4 -12
  94. hanzo_mcp/tools/llm/consensus_tool.py +8 -24
  95. hanzo_mcp/tools/llm/llm_manage.py +2 -6
  96. hanzo_mcp/tools/llm/llm_tool.py +17 -58
  97. hanzo_mcp/tools/llm/llm_unified.py +18 -59
  98. hanzo_mcp/tools/llm/provider_tools.py +1 -3
  99. hanzo_mcp/tools/lsp/lsp_tool.py +621 -481
  100. hanzo_mcp/tools/mcp/mcp_add.py +1 -3
  101. hanzo_mcp/tools/mcp/mcp_stats.py +1 -3
  102. hanzo_mcp/tools/mcp/mcp_tool.py +9 -23
  103. hanzo_mcp/tools/memory/__init__.py +10 -27
  104. hanzo_mcp/tools/memory/conversation_memory.py +636 -0
  105. hanzo_mcp/tools/memory/knowledge_tools.py +7 -25
  106. hanzo_mcp/tools/memory/memory_tools.py +6 -18
  107. hanzo_mcp/tools/search/find_tool.py +12 -34
  108. hanzo_mcp/tools/search/unified_search.py +24 -78
  109. hanzo_mcp/tools/shell/__init__.py +16 -4
  110. hanzo_mcp/tools/shell/auto_background.py +2 -6
  111. hanzo_mcp/tools/shell/base.py +1 -5
  112. hanzo_mcp/tools/shell/base_process.py +5 -7
  113. hanzo_mcp/tools/shell/bash_session.py +7 -24
  114. hanzo_mcp/tools/shell/bash_session_executor.py +5 -15
  115. hanzo_mcp/tools/shell/bash_tool.py +3 -7
  116. hanzo_mcp/tools/shell/command_executor.py +26 -79
  117. hanzo_mcp/tools/shell/logs.py +4 -16
  118. hanzo_mcp/tools/shell/npx.py +2 -8
  119. hanzo_mcp/tools/shell/npx_tool.py +1 -3
  120. hanzo_mcp/tools/shell/pkill.py +4 -12
  121. hanzo_mcp/tools/shell/process_tool.py +2 -8
  122. hanzo_mcp/tools/shell/processes.py +5 -17
  123. hanzo_mcp/tools/shell/run_background.py +1 -3
  124. hanzo_mcp/tools/shell/run_command.py +1 -3
  125. hanzo_mcp/tools/shell/run_command_windows.py +1 -3
  126. hanzo_mcp/tools/shell/run_tool.py +56 -0
  127. hanzo_mcp/tools/shell/session_manager.py +2 -6
  128. hanzo_mcp/tools/shell/session_storage.py +2 -6
  129. hanzo_mcp/tools/shell/streaming_command.py +7 -23
  130. hanzo_mcp/tools/shell/uvx.py +4 -14
  131. hanzo_mcp/tools/shell/uvx_background.py +2 -6
  132. hanzo_mcp/tools/shell/uvx_tool.py +1 -3
  133. hanzo_mcp/tools/shell/zsh_tool.py +12 -20
  134. hanzo_mcp/tools/todo/todo.py +1 -3
  135. hanzo_mcp/tools/vector/__init__.py +97 -50
  136. hanzo_mcp/tools/vector/ast_analyzer.py +6 -20
  137. hanzo_mcp/tools/vector/git_ingester.py +10 -30
  138. hanzo_mcp/tools/vector/index_tool.py +3 -9
  139. hanzo_mcp/tools/vector/infinity_store.py +7 -27
  140. hanzo_mcp/tools/vector/mock_infinity.py +1 -3
  141. hanzo_mcp/tools/vector/node_tool.py +538 -0
  142. hanzo_mcp/tools/vector/project_manager.py +4 -12
  143. hanzo_mcp/tools/vector/unified_vector.py +384 -0
  144. hanzo_mcp/tools/vector/vector.py +2 -6
  145. hanzo_mcp/tools/vector/vector_index.py +8 -8
  146. hanzo_mcp/tools/vector/vector_search.py +7 -21
  147. {hanzo_mcp-0.8.11.dist-info → hanzo_mcp-0.9.0.dist-info}/METADATA +2 -2
  148. hanzo_mcp-0.9.0.dist-info/RECORD +191 -0
  149. hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +0 -645
  150. hanzo_mcp/tools/agent/swarm_tool.py +0 -718
  151. hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +0 -577
  152. hanzo_mcp/tools/filesystem/batch_search.py +0 -900
  153. hanzo_mcp/tools/filesystem/directory_tree_paginated.py +0 -350
  154. hanzo_mcp/tools/filesystem/find_files.py +0 -369
  155. hanzo_mcp/tools/filesystem/grep.py +0 -467
  156. hanzo_mcp/tools/filesystem/search_tool.py +0 -767
  157. hanzo_mcp/tools/filesystem/symbols_tool.py +0 -515
  158. hanzo_mcp/tools/filesystem/tree.py +0 -270
  159. hanzo_mcp/tools/jupyter/notebook_edit.py +0 -317
  160. hanzo_mcp/tools/jupyter/notebook_read.py +0 -147
  161. hanzo_mcp/tools/todo/todo_read.py +0 -143
  162. hanzo_mcp/tools/todo/todo_write.py +0 -374
  163. hanzo_mcp-0.8.11.dist-info/RECORD +0 -193
  164. {hanzo_mcp-0.8.11.dist-info → hanzo_mcp-0.9.0.dist-info}/WHEEL +0 -0
  165. {hanzo_mcp-0.8.11.dist-info → hanzo_mcp-0.9.0.dist-info}/entry_points.txt +0 -0
  166. {hanzo_mcp-0.8.11.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
- class DirectoryTreeToolParams(TypedDict):
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: DirectoryPath
53
- depth: Depth
54
- include_filtered: IncludeFiltered
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: DirectoryPath = params["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
- await tool_ctx.info(
116
- f"Getting directory tree: {path} (depth: {depth}, include_filtered: {include_filtered})"
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
- # Build the tree recursively
181
- async def build_tree(
182
- current_path: Path, current_depth: int = 0
183
- ) -> list[dict[str, Any]]:
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
- # Add file entry
232
- result.append({"name": entry.name, "type": "file"})
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 as a simple indented structure
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
- # Indentation based on level
247
- indent = " " * level
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
- lines.append(
253
- f"{indent}{item['name']}/ [skipped - {item['skipped']}]"
254
- )
433
+ line = f"{current_prefix}{item['name']}/ [skipped - {item['skipped']}]"
255
434
  else:
256
- lines.append(f"{indent}{item['name']}/")
257
- # Add children with increased indentation if present
258
- if "children" in item:
259
- lines.extend(format_tree(item["children"], level + 1))
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
- lines.append(f"{indent}{item['name']}")
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 as simple text
270
- formatted_output = "\n".join(format_tree(tree_data))
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 a smaller depth, specific subdirectory, or the paginated version of this tool.]",
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
- ) -> str:
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, path=path, depth=depth, include_filtered=include_filtered
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
- "Parameter 'expected_replacements' must be a non-negative number"
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: