hanzo-mcp 0.9.0__py3-none-any.whl → 0.9.2__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 (135) hide show
  1. hanzo_mcp/__init__.py +1 -1
  2. hanzo_mcp/analytics/posthog_analytics.py +14 -1
  3. hanzo_mcp/cli.py +108 -4
  4. hanzo_mcp/server.py +11 -0
  5. hanzo_mcp/tools/__init__.py +3 -16
  6. hanzo_mcp/tools/agent/__init__.py +5 -0
  7. hanzo_mcp/tools/agent/agent.py +5 -0
  8. hanzo_mcp/tools/agent/agent_tool.py +3 -17
  9. hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +623 -0
  10. hanzo_mcp/tools/agent/clarification_tool.py +7 -1
  11. hanzo_mcp/tools/agent/claude_desktop_auth.py +16 -6
  12. hanzo_mcp/tools/agent/cli_agent_base.py +5 -0
  13. hanzo_mcp/tools/agent/cli_tools.py +26 -0
  14. hanzo_mcp/tools/agent/code_auth_tool.py +5 -0
  15. hanzo_mcp/tools/agent/critic_tool.py +7 -1
  16. hanzo_mcp/tools/agent/iching_tool.py +5 -0
  17. hanzo_mcp/tools/agent/network_tool.py +5 -0
  18. hanzo_mcp/tools/agent/review_tool.py +7 -1
  19. hanzo_mcp/tools/agent/swarm_alias.py +5 -0
  20. hanzo_mcp/tools/agent/swarm_tool.py +701 -0
  21. hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +554 -0
  22. hanzo_mcp/tools/agent/unified_cli_tools.py +5 -0
  23. hanzo_mcp/tools/common/auto_timeout.py +254 -0
  24. hanzo_mcp/tools/common/base.py +4 -0
  25. hanzo_mcp/tools/common/batch_tool.py +5 -0
  26. hanzo_mcp/tools/common/config_tool.py +5 -0
  27. hanzo_mcp/tools/common/critic_tool.py +5 -0
  28. hanzo_mcp/tools/common/paginated_base.py +4 -0
  29. hanzo_mcp/tools/common/permissions.py +38 -12
  30. hanzo_mcp/tools/common/personality.py +673 -980
  31. hanzo_mcp/tools/common/stats.py +5 -0
  32. hanzo_mcp/tools/common/thinking_tool.py +5 -0
  33. hanzo_mcp/tools/common/timeout_parser.py +103 -0
  34. hanzo_mcp/tools/common/tool_disable.py +5 -0
  35. hanzo_mcp/tools/common/tool_enable.py +5 -0
  36. hanzo_mcp/tools/common/tool_list.py +5 -0
  37. hanzo_mcp/tools/config/config_tool.py +5 -0
  38. hanzo_mcp/tools/config/mode_tool.py +5 -0
  39. hanzo_mcp/tools/database/graph.py +5 -0
  40. hanzo_mcp/tools/database/graph_add.py +5 -0
  41. hanzo_mcp/tools/database/graph_query.py +5 -0
  42. hanzo_mcp/tools/database/graph_remove.py +5 -0
  43. hanzo_mcp/tools/database/graph_search.py +5 -0
  44. hanzo_mcp/tools/database/graph_stats.py +5 -0
  45. hanzo_mcp/tools/database/sql.py +5 -0
  46. hanzo_mcp/tools/database/sql_query.py +2 -0
  47. hanzo_mcp/tools/database/sql_search.py +5 -0
  48. hanzo_mcp/tools/database/sql_stats.py +5 -0
  49. hanzo_mcp/tools/editor/neovim_command.py +5 -0
  50. hanzo_mcp/tools/editor/neovim_edit.py +7 -2
  51. hanzo_mcp/tools/editor/neovim_session.py +5 -0
  52. hanzo_mcp/tools/filesystem/__init__.py +23 -26
  53. hanzo_mcp/tools/filesystem/ast_tool.py +3 -4
  54. hanzo_mcp/tools/filesystem/base.py +2 -18
  55. hanzo_mcp/tools/filesystem/batch_search.py +825 -0
  56. hanzo_mcp/tools/filesystem/content_replace.py +5 -3
  57. hanzo_mcp/tools/filesystem/diff.py +5 -0
  58. hanzo_mcp/tools/filesystem/directory_tree.py +34 -281
  59. hanzo_mcp/tools/filesystem/directory_tree_paginated.py +345 -0
  60. hanzo_mcp/tools/filesystem/edit.py +6 -5
  61. hanzo_mcp/tools/filesystem/find.py +177 -311
  62. hanzo_mcp/tools/filesystem/find_files.py +370 -0
  63. hanzo_mcp/tools/filesystem/git_search.py +5 -3
  64. hanzo_mcp/tools/filesystem/grep.py +454 -0
  65. hanzo_mcp/tools/filesystem/multi_edit.py +6 -5
  66. hanzo_mcp/tools/filesystem/read.py +10 -9
  67. hanzo_mcp/tools/filesystem/rules_tool.py +6 -4
  68. hanzo_mcp/tools/filesystem/search_tool.py +728 -0
  69. hanzo_mcp/tools/filesystem/symbols_tool.py +510 -0
  70. hanzo_mcp/tools/filesystem/tree.py +273 -0
  71. hanzo_mcp/tools/filesystem/watch.py +6 -1
  72. hanzo_mcp/tools/filesystem/write.py +13 -7
  73. hanzo_mcp/tools/jupyter/jupyter.py +30 -2
  74. hanzo_mcp/tools/jupyter/notebook_edit.py +298 -0
  75. hanzo_mcp/tools/jupyter/notebook_read.py +148 -0
  76. hanzo_mcp/tools/llm/consensus_tool.py +8 -6
  77. hanzo_mcp/tools/llm/llm_manage.py +5 -0
  78. hanzo_mcp/tools/llm/llm_tool.py +2 -0
  79. hanzo_mcp/tools/llm/llm_unified.py +5 -0
  80. hanzo_mcp/tools/llm/provider_tools.py +5 -0
  81. hanzo_mcp/tools/lsp/lsp_tool.py +475 -622
  82. hanzo_mcp/tools/mcp/mcp_add.py +7 -2
  83. hanzo_mcp/tools/mcp/mcp_remove.py +15 -2
  84. hanzo_mcp/tools/mcp/mcp_stats.py +5 -0
  85. hanzo_mcp/tools/mcp/mcp_tool.py +5 -0
  86. hanzo_mcp/tools/memory/knowledge_tools.py +14 -0
  87. hanzo_mcp/tools/memory/memory_tools.py +17 -0
  88. hanzo_mcp/tools/search/find_tool.py +5 -3
  89. hanzo_mcp/tools/search/unified_search.py +3 -1
  90. hanzo_mcp/tools/shell/__init__.py +2 -14
  91. hanzo_mcp/tools/shell/base_process.py +4 -2
  92. hanzo_mcp/tools/shell/bash_tool.py +2 -0
  93. hanzo_mcp/tools/shell/command_executor.py +7 -7
  94. hanzo_mcp/tools/shell/logs.py +5 -0
  95. hanzo_mcp/tools/shell/npx.py +5 -0
  96. hanzo_mcp/tools/shell/npx_background.py +5 -0
  97. hanzo_mcp/tools/shell/npx_tool.py +5 -0
  98. hanzo_mcp/tools/shell/open.py +5 -0
  99. hanzo_mcp/tools/shell/pkill.py +5 -0
  100. hanzo_mcp/tools/shell/process_tool.py +5 -0
  101. hanzo_mcp/tools/shell/processes.py +5 -0
  102. hanzo_mcp/tools/shell/run_background.py +5 -0
  103. hanzo_mcp/tools/shell/run_command.py +2 -0
  104. hanzo_mcp/tools/shell/run_command_windows.py +5 -0
  105. hanzo_mcp/tools/shell/streaming_command.py +5 -0
  106. hanzo_mcp/tools/shell/uvx.py +5 -0
  107. hanzo_mcp/tools/shell/uvx_background.py +5 -0
  108. hanzo_mcp/tools/shell/uvx_tool.py +5 -0
  109. hanzo_mcp/tools/shell/zsh_tool.py +3 -0
  110. hanzo_mcp/tools/todo/todo.py +5 -0
  111. hanzo_mcp/tools/todo/todo_read.py +142 -0
  112. hanzo_mcp/tools/todo/todo_write.py +367 -0
  113. hanzo_mcp/tools/vector/__init__.py +42 -95
  114. hanzo_mcp/tools/vector/index_tool.py +5 -0
  115. hanzo_mcp/tools/vector/vector.py +5 -0
  116. hanzo_mcp/tools/vector/vector_index.py +5 -0
  117. hanzo_mcp/tools/vector/vector_search.py +5 -0
  118. {hanzo_mcp-0.9.0.dist-info → hanzo_mcp-0.9.2.dist-info}/METADATA +1 -1
  119. hanzo_mcp-0.9.2.dist-info/RECORD +195 -0
  120. hanzo_mcp/tools/common/path_utils.py +0 -34
  121. hanzo_mcp/tools/compiler/__init__.py +0 -8
  122. hanzo_mcp/tools/compiler/sandboxed_compiler.py +0 -681
  123. hanzo_mcp/tools/environment/__init__.py +0 -8
  124. hanzo_mcp/tools/environment/environment_detector.py +0 -594
  125. hanzo_mcp/tools/filesystem/search.py +0 -1160
  126. hanzo_mcp/tools/framework/__init__.py +0 -8
  127. hanzo_mcp/tools/framework/framework_modes.py +0 -714
  128. hanzo_mcp/tools/memory/conversation_memory.py +0 -636
  129. hanzo_mcp/tools/shell/run_tool.py +0 -56
  130. hanzo_mcp/tools/vector/node_tool.py +0 -538
  131. hanzo_mcp/tools/vector/unified_vector.py +0 -384
  132. hanzo_mcp-0.9.0.dist-info/RECORD +0 -191
  133. {hanzo_mcp-0.9.0.dist-info → hanzo_mcp-0.9.2.dist-info}/WHEEL +0 -0
  134. {hanzo_mcp-0.9.0.dist-info → hanzo_mcp-0.9.2.dist-info}/entry_points.txt +0 -0
  135. {hanzo_mcp-0.9.0.dist-info → hanzo_mcp-0.9.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,454 @@
1
+ """Grep tool implementation.
2
+
3
+ This module provides the Grep tool for finding text patterns in files using ripgrep.
4
+ """
5
+
6
+ import re
7
+ import json
8
+ import shlex
9
+ import shutil
10
+ import asyncio
11
+ import fnmatch
12
+ from typing import Unpack, Annotated, TypedDict, final, override
13
+ from pathlib import Path
14
+
15
+ from pydantic import Field
16
+ from mcp.server import FastMCP
17
+
18
+ from hanzo_mcp.tools.common.auto_timeout import auto_timeout
19
+ from mcp.server.fastmcp import Context as MCPContext
20
+
21
+ from hanzo_mcp.tools.common.context import ToolContext
22
+ from hanzo_mcp.tools.common.truncate import truncate_response
23
+ from hanzo_mcp.tools.filesystem.base import FilesystemBaseTool
24
+
25
+ Pattern = Annotated[
26
+ str,
27
+ Field(
28
+ description="The regular expression pattern to search for in file contents",
29
+ min_length=1,
30
+ ),
31
+ ]
32
+
33
+ SearchPath = Annotated[
34
+ str,
35
+ Field(
36
+ description="The directory to search in. Defaults to the current working directory.",
37
+ default=".",
38
+ ),
39
+ ]
40
+
41
+ Include = Annotated[
42
+ str,
43
+ Field(
44
+ description='File pattern to include in the search (e.g. "*.js", "*.{ts,tsx}")',
45
+ default="*",
46
+ ),
47
+ ]
48
+
49
+
50
+ class GrepToolParams(TypedDict):
51
+ """Parameters for the Grep tool.
52
+
53
+ Attributes:
54
+ pattern: The regular expression pattern to search for in file contents
55
+ path: The directory to search in. Defaults to the current working directory.
56
+ include: File pattern to include in the search (e.g. "*.js", "*.{ts,tsx}")
57
+ """
58
+
59
+ pattern: Pattern
60
+ path: SearchPath
61
+ include: Include
62
+
63
+
64
+ @final
65
+ class Grep(FilesystemBaseTool):
66
+ """Fast content search tool that works with any codebase size."""
67
+
68
+ @property
69
+ @override
70
+ def name(self) -> str:
71
+ """Get the tool name.
72
+
73
+ Returns:
74
+ Tool name
75
+ """
76
+ return "grep"
77
+
78
+ @property
79
+ @override
80
+ def description(self) -> str:
81
+ """Get the tool description.
82
+
83
+ Returns:
84
+ Tool description
85
+ """
86
+ return """Fast content search tool that works with any codebase size.
87
+ Searches file contents using regular expressions.
88
+ Supports full regex syntax (eg. "log.*Error", "function\\s+\\w+", etc.).
89
+ Filter files by pattern with the include parameter (eg. "*.js", "*.{ts,tsx}").
90
+ Returns matching file paths sorted by modification time.
91
+ Use this tool when you need to find files containing specific patterns.
92
+ When you are doing an open ended search that may require multiple rounds of globbing and grepping, use the Agent tool instead."""
93
+
94
+ def is_ripgrep_installed(self) -> bool:
95
+ """Check if ripgrep (rg) is installed.
96
+
97
+ Returns:
98
+ True if ripgrep is installed, False otherwise
99
+ """
100
+ return shutil.which("rg") is not None
101
+
102
+ async def run_ripgrep(
103
+ self,
104
+ pattern: str,
105
+ path: str,
106
+ tool_ctx: ToolContext,
107
+ include_pattern: str | None = None,
108
+ ) -> str:
109
+ """Run ripgrep with the given parameters and return the results.
110
+
111
+ Args:
112
+ pattern: The regular expression pattern to search for
113
+ path: The directory or file to search in
114
+ include_pattern: Optional file pattern to include in the search
115
+ tool_ctx: Tool context for logging
116
+
117
+ Returns:
118
+ The search results as formatted string
119
+ """
120
+ # Special case for tests: direct file path with include pattern that doesn't match
121
+ if Path(path).is_file() and include_pattern and include_pattern != "*":
122
+ if not fnmatch.fnmatch(Path(path).name, include_pattern):
123
+ await tool_ctx.info(f"File does not match pattern '{include_pattern}': {path}")
124
+ return f"File does not match pattern '{include_pattern}': {path}"
125
+
126
+ cmd = ["rg", "--json", pattern]
127
+
128
+ # Add path
129
+ cmd.append(path)
130
+
131
+ # Add include pattern if provided
132
+ if include_pattern and include_pattern != "*":
133
+ cmd.extend(["-g", include_pattern])
134
+
135
+ await tool_ctx.info(f"Running ripgrep command: {shlex.join(cmd)}")
136
+
137
+ try:
138
+ # Execute ripgrep process
139
+ process = await asyncio.create_subprocess_exec(
140
+ *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
141
+ )
142
+
143
+ stdout, stderr = await process.communicate()
144
+
145
+ if process.returncode != 0 and process.returncode != 1:
146
+ # rg returns 1 when no matches are found, which is not an error
147
+ await tool_ctx.error(f"ripgrep failed with exit code {process.returncode}: {stderr.decode()}")
148
+ return f"Error executing ripgrep: {stderr.decode()}"
149
+
150
+ # Parse the JSON output
151
+ results = self.parse_ripgrep_json_output(stdout.decode())
152
+ return results
153
+
154
+ except Exception as e:
155
+ await tool_ctx.error(f"Error running ripgrep: {str(e)}")
156
+ return f"Error running ripgrep: {str(e)}"
157
+
158
+ def parse_ripgrep_json_output(self, output: str) -> str:
159
+ """Parse ripgrep JSON output and format it for human readability.
160
+
161
+ Args:
162
+ output: The JSON output from ripgrep
163
+
164
+ Returns:
165
+ Formatted string with search results
166
+ """
167
+ if not output.strip():
168
+ return "No matches found."
169
+
170
+ formatted_results = []
171
+ file_results = {}
172
+
173
+ for line in output.splitlines():
174
+ if not line.strip():
175
+ continue
176
+
177
+ try:
178
+ data = json.loads(line)
179
+
180
+ if data.get("type") == "match":
181
+ path = data.get("data", {}).get("path", {}).get("text", "")
182
+ line_number = data.get("data", {}).get("line_number", 0)
183
+ line_text = data.get("data", {}).get("lines", {}).get("text", "").rstrip()
184
+
185
+ if path not in file_results:
186
+ file_results[path] = []
187
+
188
+ file_results[path].append((line_number, line_text))
189
+
190
+ except json.JSONDecodeError as e:
191
+ formatted_results.append(f"Error parsing JSON: {str(e)}")
192
+
193
+ # Count total matches
194
+ total_matches = sum(len(matches) for matches in file_results.values())
195
+ total_files = len(file_results)
196
+
197
+ if total_matches == 0:
198
+ return "No matches found."
199
+
200
+ formatted_results.append(
201
+ f"Found {total_matches} matches in {total_files} file{'s' if total_files > 1 else ''}:"
202
+ )
203
+ formatted_results.append("") # Empty line for readability
204
+
205
+ # Format the results by file
206
+ for file_path, matches in file_results.items():
207
+ for line_number, line_text in matches:
208
+ formatted_results.append(f"{file_path}:{line_number}: {line_text}")
209
+
210
+ return "\n".join(formatted_results)
211
+
212
+ async def fallback_grep(
213
+ self,
214
+ pattern: str,
215
+ path: str,
216
+ tool_ctx: ToolContext,
217
+ include_pattern: str | None = None,
218
+ ) -> str:
219
+ """Fallback Python implementation when ripgrep is not available.
220
+
221
+ Args:
222
+ pattern: The regular expression pattern to search for
223
+ path: The directory or file to search in
224
+ include_pattern: Optional file pattern to include in the search
225
+ tool_ctx: Tool context for logging
226
+
227
+ Returns:
228
+ The search results as formatted string
229
+ """
230
+ await tool_ctx.info("Using fallback Python implementation for grep")
231
+
232
+ try:
233
+ input_path = Path(path)
234
+
235
+ # Find matching files
236
+ matching_files: list[Path] = []
237
+
238
+ # Process based on whether path is a file or directory
239
+ if input_path.is_file():
240
+ # Single file search - check file pattern match first
241
+ if (
242
+ include_pattern is None
243
+ or include_pattern == "*"
244
+ or fnmatch.fnmatch(input_path.name, include_pattern)
245
+ ):
246
+ matching_files.append(input_path)
247
+ await tool_ctx.info(f"Searching single file: {path}")
248
+ else:
249
+ # File doesn't match the pattern, return immediately
250
+ await tool_ctx.info(f"File does not match pattern '{include_pattern}': {path}")
251
+ return f"File does not match pattern '{include_pattern}': {path}"
252
+ elif input_path.is_dir():
253
+ # Directory search - find all files
254
+ await tool_ctx.info(f"Finding files in directory: {path}")
255
+
256
+ # Keep track of allowed paths for filtering
257
+ allowed_paths: set[str] = set()
258
+
259
+ # Collect all allowed paths first for faster filtering
260
+ for entry in input_path.rglob("*"):
261
+ entry_path = str(entry)
262
+ if self.is_path_allowed(entry_path):
263
+ allowed_paths.add(entry_path)
264
+
265
+ # Find matching files efficiently
266
+ for entry in input_path.rglob("*"):
267
+ entry_path = str(entry)
268
+ if entry_path in allowed_paths and entry.is_file():
269
+ if (
270
+ include_pattern is None
271
+ or include_pattern == "*"
272
+ or fnmatch.fnmatch(entry.name, include_pattern)
273
+ ):
274
+ matching_files.append(entry)
275
+
276
+ await tool_ctx.info(f"Found {len(matching_files)} matching files")
277
+ else:
278
+ # This shouldn't happen if path exists
279
+ await tool_ctx.error(f"Path is neither a file nor a directory: {path}")
280
+ return f"Error: Path is neither a file nor a directory: {path}"
281
+
282
+ # Report progress
283
+ total_files = len(matching_files)
284
+ if input_path.is_file():
285
+ await tool_ctx.info(f"Searching file: {path}")
286
+ else:
287
+ await tool_ctx.info(f"Searching through {total_files} files in directory")
288
+
289
+ # Set up for parallel processing
290
+ results: list[str] = []
291
+ files_processed = 0
292
+ matches_found = 0
293
+ batch_size = 20 # Process files in batches to avoid overwhelming the system
294
+
295
+ # Use a semaphore to limit concurrent file operations
296
+ semaphore = asyncio.Semaphore(10)
297
+
298
+ # Create an async function to search a single file
299
+ async def search_file(file_path: Path) -> list[str]:
300
+ nonlocal files_processed, matches_found
301
+ file_results: list[str] = []
302
+
303
+ try:
304
+ async with semaphore: # Limit concurrent operations
305
+ try:
306
+ with open(file_path, "r", encoding="utf-8") as f:
307
+ for line_num, line in enumerate(f, 1):
308
+ if re.search(pattern, line):
309
+ file_results.append(f"{file_path}:{line_num}: {line.rstrip()}")
310
+ matches_found += 1
311
+ files_processed += 1
312
+ except UnicodeDecodeError:
313
+ # Skip binary files
314
+ files_processed += 1
315
+ except Exception as e:
316
+ await tool_ctx.warning(f"Error reading {file_path}: {str(e)}")
317
+ except Exception as e:
318
+ await tool_ctx.warning(f"Error processing {file_path}: {str(e)}")
319
+
320
+ return file_results
321
+
322
+ # Process files in parallel batches
323
+ for i in range(0, len(matching_files), batch_size):
324
+ batch = matching_files[i : i + batch_size]
325
+ batch_tasks = [search_file(file_path) for file_path in batch]
326
+
327
+ # Report progress
328
+ await tool_ctx.report_progress(i, total_files)
329
+
330
+ # Wait for the batch to complete
331
+ batch_results = await asyncio.gather(*batch_tasks)
332
+
333
+ # Flatten and collect results
334
+ for file_result in batch_results:
335
+ results.extend(file_result)
336
+
337
+ # Final progress report
338
+ await tool_ctx.report_progress(total_files, total_files)
339
+
340
+ if not results:
341
+ if input_path.is_file():
342
+ return f"No matches found for pattern '{pattern}' in file: {path}"
343
+ else:
344
+ return f"No matches found for pattern '{pattern}' in files matching '{include_pattern or '*'}' in directory: {path}"
345
+
346
+ await tool_ctx.info(
347
+ f"Found {matches_found} matches in {files_processed} file{'s' if files_processed > 1 else ''}"
348
+ )
349
+ return (
350
+ f"Found {matches_found} matches in {files_processed} file{'s' if files_processed > 1 else ''}:\n\n"
351
+ + "\n".join(results)
352
+ )
353
+ except Exception as e:
354
+ await tool_ctx.error(f"Error searching file contents: {str(e)}")
355
+ return f"Error searching file contents: {str(e)}"
356
+
357
+ @override
358
+ @auto_timeout("grep")
359
+
360
+
361
+ async def call(
362
+ self,
363
+ ctx: MCPContext,
364
+ **params: Unpack[GrepToolParams],
365
+ ) -> str:
366
+ """Execute the grep tool with the given parameters.
367
+
368
+ Args:
369
+ ctx: MCP context
370
+ **params: Tool parameters
371
+
372
+ Returns:
373
+ Tool result
374
+ """
375
+ tool_ctx = self.create_tool_context(ctx)
376
+
377
+ # Extract parameters
378
+ pattern = params.get("pattern")
379
+ path: str = params.get("path", ".")
380
+ # Support both 'include' and legacy 'file_pattern' parameter for backward compatibility
381
+ include: str = params.get("include") or params.get("file_pattern")
382
+
383
+ # Validate required parameters for direct calls (not through MCP framework)
384
+ if pattern is None:
385
+ await tool_ctx.error("Parameter 'pattern' is required but was None")
386
+ return "Error: Parameter 'pattern' is required but was None"
387
+
388
+ # Validate path if provided
389
+ if path:
390
+ path_validation = self.validate_path(path)
391
+ if path_validation.is_error:
392
+ await tool_ctx.error(path_validation.error_message)
393
+ return f"Error: {path_validation.error_message}"
394
+
395
+ # Check if path is allowed
396
+ allowed, error_msg = await self.check_path_allowed(path, tool_ctx)
397
+ if not allowed:
398
+ return error_msg
399
+
400
+ # Check if path exists
401
+ exists, error_msg = await self.check_path_exists(path, tool_ctx)
402
+ if not exists:
403
+ return error_msg
404
+
405
+ # Log operation
406
+ search_info = f"Searching for pattern '{pattern}'"
407
+ if include:
408
+ search_info += f" in files matching '{include}'"
409
+ search_info += f" in path: {path}"
410
+ await tool_ctx.info(search_info)
411
+
412
+ # Check if ripgrep is installed and use it if available
413
+ try:
414
+ if self.is_ripgrep_installed():
415
+ await tool_ctx.info("ripgrep is installed, using ripgrep for search")
416
+ result = await self.run_ripgrep(pattern, path, tool_ctx, include)
417
+ return truncate_response(
418
+ result,
419
+ max_tokens=25000,
420
+ truncation_message="\n\n[Grep results truncated due to token limit. Use more specific patterns or paths to reduce output.]",
421
+ )
422
+ else:
423
+ await tool_ctx.info("ripgrep is not installed, using fallback implementation")
424
+ result = await self.fallback_grep(pattern, path, tool_ctx, include)
425
+ return truncate_response(
426
+ result,
427
+ max_tokens=25000,
428
+ truncation_message="\n\n[Grep results truncated due to token limit. Use more specific patterns or paths to reduce output.]",
429
+ )
430
+ except Exception as e:
431
+ await tool_ctx.error(f"Error in grep tool: {str(e)}")
432
+ return f"Error in grep tool: {str(e)}"
433
+
434
+ @override
435
+ def register(self, mcp_server: FastMCP) -> None:
436
+ """Register this grep tool with the MCP server.
437
+
438
+ Creates a wrapper function with explicitly defined parameters that match
439
+ the tool's parameter schema and registers it with the MCP server.
440
+
441
+ Args:
442
+ mcp_server: The FastMCP server instance
443
+ """
444
+ tool_self = self # Create a reference to self for use in the closure
445
+
446
+ @mcp_server.tool(name=self.name, description=self.description)
447
+ async def grep(
448
+ ctx: MCPContext,
449
+ pattern: Pattern,
450
+ path: SearchPath,
451
+ include: Include,
452
+ ) -> str:
453
+ # Use 'include' parameter if provided, otherwise fall back to 'file_pattern'
454
+ return await tool_self.call(ctx, pattern=pattern, path=path, include=include)
@@ -9,6 +9,8 @@ from pathlib import Path
9
9
 
10
10
  from pydantic import Field
11
11
  from mcp.server import FastMCP
12
+
13
+ from hanzo_mcp.tools.common.auto_timeout import auto_timeout
12
14
  from mcp.server.fastmcp import Context as MCPContext
13
15
 
14
16
  from hanzo_mcp.tools.filesystem.base import FilesystemBaseTool
@@ -132,6 +134,9 @@ If you want to create a new file, use:
132
134
  - Subsequent edits: normal edit operations on the created content"""
133
135
 
134
136
  @override
137
+ @auto_timeout("multi_edit")
138
+
139
+
135
140
  async def call(
136
141
  self,
137
142
  ctx: MCPContext,
@@ -147,7 +152,7 @@ If you want to create a new file, use:
147
152
  Tool result
148
153
  """
149
154
  tool_ctx = self.create_tool_context(ctx)
150
- self.set_tool_context_info(tool_ctx)
155
+ await self.set_tool_context_info(tool_ctx)
151
156
 
152
157
  # Extract parameters
153
158
  file_path: FilePath = params["file_path"]
@@ -159,10 +164,6 @@ If you want to create a new file, use:
159
164
  await tool_ctx.error(path_validation.error_message)
160
165
  return f"Error: {path_validation.error_message}"
161
166
 
162
- # Expand path first (handles ~, $HOME, etc.)
163
- expanded_path = self.expand_path(file_path)
164
- file_path = expanded_path # Use expanded path for all operations
165
-
166
167
  # Validate each edit
167
168
  for i, edit in enumerate(edits):
168
169
  if not isinstance(edit, dict):
@@ -8,6 +8,8 @@ from pathlib import Path
8
8
 
9
9
  from pydantic import Field
10
10
  from mcp.server import FastMCP
11
+
12
+ from hanzo_mcp.tools.common.auto_timeout import auto_timeout
11
13
  from mcp.server.fastmcp import Context as MCPContext
12
14
 
13
15
  from hanzo_mcp.tools.common.truncate import truncate_response
@@ -91,6 +93,7 @@ Usage:
91
93
  - When reading multiple files, you MUST use the batch tool to read them all at once"""
92
94
 
93
95
  @override
96
+ @auto_timeout("read")
94
97
  async def call(
95
98
  self,
96
99
  ctx: MCPContext,
@@ -106,7 +109,7 @@ Usage:
106
109
  Tool result
107
110
  """
108
111
  tool_ctx = self.create_tool_context(ctx)
109
- self.set_tool_context_info(tool_ctx)
112
+ await self.set_tool_context_info(tool_ctx)
110
113
 
111
114
  # Extract parameters
112
115
  file_path = params.get("file_path")
@@ -118,17 +121,15 @@ Usage:
118
121
  await tool_ctx.error("Parameter 'file_path' is required but was None")
119
122
  return "Error: Parameter 'file_path' is required but was None"
120
123
 
121
- # Expand path first (handles ~, $HOME, etc.)
122
- expanded_path = self.expand_path(file_path)
123
- await tool_ctx.info(f"Reading file: {expanded_path} (offset: {offset}, limit: {limit})")
124
+ await tool_ctx.info(f"Reading file: {file_path} (offset: {offset}, limit: {limit})")
124
125
 
125
- # Check if path is allowed (using expanded path)
126
- if not self.is_path_allowed(expanded_path):
127
- await tool_ctx.error(f"Access denied - path outside allowed directories: {expanded_path}")
128
- return f"Error: Access denied - path outside allowed directories: {expanded_path}"
126
+ # Check if path is allowed
127
+ if not self.is_path_allowed(file_path):
128
+ await tool_ctx.error(f"Access denied - path outside allowed directories: {file_path}")
129
+ return f"Error: Access denied - path outside allowed directories: {file_path}"
129
130
 
130
131
  try:
131
- file_path_obj = Path(expanded_path)
132
+ file_path_obj = Path(file_path)
132
133
 
133
134
  if not file_path_obj.exists():
134
135
  await tool_ctx.error(f"File does not exist: {file_path}")
@@ -9,6 +9,8 @@ from pathlib import Path
9
9
 
10
10
  from pydantic import Field
11
11
  from mcp.server import FastMCP
12
+
13
+ from hanzo_mcp.tools.common.auto_timeout import auto_timeout
12
14
  from mcp.server.fastmcp import Context as MCPContext
13
15
 
14
16
  from hanzo_mcp.tools.filesystem.base import FilesystemBaseTool
@@ -74,6 +76,9 @@ The tool returns the contents of all found configuration files to help
74
76
  understand project-specific requirements and preferences."""
75
77
 
76
78
  @override
79
+ @auto_timeout("rules")
80
+
81
+
77
82
  async def call(
78
83
  self,
79
84
  ctx: MCPContext,
@@ -89,13 +94,10 @@ understand project-specific requirements and preferences."""
89
94
  Tool result
90
95
  """
91
96
  tool_ctx = self.create_tool_context(ctx)
92
- self.set_tool_context_info(tool_ctx)
97
+ await self.set_tool_context_info(tool_ctx)
93
98
 
94
99
  # Extract parameters
95
100
  search_path = params.get("path", ".")
96
-
97
- # Expand path (handles ~, $HOME, etc.)
98
- search_path = self.expand_path(search_path)
99
101
 
100
102
  # Validate path
101
103
  path_validation = self.validate_path(search_path)