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.

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