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,9 +1,10 @@
1
1
  """Unified find tool implementation.
2
2
 
3
- This module provides the FindTool for finding text patterns in files using
3
+ This module provides the FindTool for finding files by name or content using
4
4
  multiple search backends in order of preference: rg > ag > ack > grep.
5
5
  """
6
6
 
7
+ import os
7
8
  import re
8
9
  import json
9
10
  import shutil
@@ -17,6 +18,7 @@ from typing import (
17
18
  TypedDict,
18
19
  final,
19
20
  override,
21
+ Literal,
20
22
  )
21
23
  from pathlib import Path
22
24
 
@@ -25,11 +27,17 @@ from mcp.server.fastmcp import Context as MCPContext
25
27
 
26
28
  from hanzo_mcp.tools.filesystem.base import FilesystemBaseTool
27
29
 
30
+ try:
31
+ import ffind
32
+ FFIND_AVAILABLE = True
33
+ except ImportError:
34
+ FFIND_AVAILABLE = False
35
+
28
36
  # Parameter types
29
37
  Pattern = Annotated[
30
38
  str,
31
39
  Field(
32
- description="Pattern to search for (regex or literal)",
40
+ description="Pattern to search for (file name pattern or content regex/literal)",
33
41
  min_length=1,
34
42
  ),
35
43
  ]
@@ -42,10 +50,18 @@ SearchPath = Annotated[
42
50
  ),
43
51
  ]
44
52
 
53
+ Mode = Annotated[
54
+ Literal["name", "content", "both"],
55
+ Field(
56
+ description="Search mode: 'name' for file names, 'content' for file contents, 'both' for both",
57
+ default="name",
58
+ ),
59
+ ]
60
+
45
61
  Include = Annotated[
46
62
  Optional[str],
47
63
  Field(
48
- description='File pattern to include (e.g. "*.js")',
64
+ description='File pattern to include (e.g. "*.js", "*.{ts,tsx}")',
49
65
  default=None,
50
66
  ),
51
67
  ]
@@ -62,30 +78,22 @@ CaseSensitive = Annotated[
62
78
  bool,
63
79
  Field(
64
80
  description="Case sensitive search",
65
- default=True,
66
- ),
67
- ]
68
-
69
- FixedStrings = Annotated[
70
- bool,
71
- Field(
72
- description="Treat pattern as literal string, not regex",
73
81
  default=False,
74
82
  ),
75
83
  ]
76
84
 
77
- ShowContext = Annotated[
78
- int,
85
+ Recursive = Annotated[
86
+ bool,
79
87
  Field(
80
- description="Lines of context to show around matches",
81
- default=0,
88
+ description="Search recursively in subdirectories",
89
+ default=True,
82
90
  ),
83
91
  ]
84
92
 
85
- Backend = Annotated[
86
- Optional[str],
93
+ MaxResults = Annotated[
94
+ Optional[int],
87
95
  Field(
88
- description="Force specific backend: rg, ag, ack, grep",
96
+ description="Maximum number of results to return",
89
97
  default=None,
90
98
  ),
91
99
  ]
@@ -96,17 +104,17 @@ class FindParams(TypedDict, total=False):
96
104
 
97
105
  pattern: str
98
106
  path: str
107
+ mode: Literal["name", "content", "both"]
99
108
  include: Optional[str]
100
109
  exclude: Optional[str]
101
110
  case_sensitive: bool
102
- fixed_strings: bool
103
- show_context: int
104
- backend: Optional[str]
111
+ recursive: bool
112
+ max_results: Optional[int]
105
113
 
106
114
 
107
115
  @final
108
116
  class FindTool(FilesystemBaseTool):
109
- """Unified find tool with multiple backend support."""
117
+ """Unified find tool for searching by file name or content."""
110
118
 
111
119
  def __init__(self, permission_manager):
112
120
  """Initialize the find tool."""
@@ -125,17 +133,24 @@ class FindTool(FilesystemBaseTool):
125
133
  def description(self) -> str:
126
134
  """Get the tool description."""
127
135
  backends = self._get_available_backends()
128
- backend_str = ", ".join(backends) if backends else "fallback grep"
136
+ backend_str = ", ".join(backends) if backends else "fallback search"
137
+
138
+ return f"""Find files by name, content, or both. Available backends: {backend_str}.
139
+
140
+ Examples:
141
+ # Find by file name (default mode)
142
+ find "*.py"
143
+ find "test_*" ./src
144
+ find "README.*" --case-sensitive
129
145
 
130
- return f"""Find pattern in files (like ffind). Available: {backend_str}.
146
+ # Find by content
147
+ find "TODO" --mode content
148
+ find "error.*fatal" ./src --mode content
131
149
 
132
- Usage:
133
- find "TODO"
134
- find "error.*fatal" ./src
135
- find "config" --include "*.json"
136
- find "password" --exclude "*.log"
150
+ # Find both name and content
151
+ find "config" --mode both --include "*.json"
137
152
 
138
- Fast, intuitive file content search."""
153
+ Supports wildcards for names, regex for content."""
139
154
 
140
155
  def _get_available_backends(self) -> List[str]:
141
156
  """Get list of available search backends."""
@@ -161,12 +176,15 @@ Fast, intuitive file content search."""
161
176
  return "Error: pattern is required"
162
177
 
163
178
  path = params.get("path", ".")
179
+ mode = params.get("mode", "name")
164
180
  include = params.get("include")
165
181
  exclude = params.get("exclude")
166
- case_sensitive = params.get("case_sensitive", True)
167
- fixed_strings = params.get("fixed_strings", False)
168
- show_context = params.get("show_context", 0)
169
- backend = params.get("backend")
182
+ case_sensitive = params.get("case_sensitive", False)
183
+ recursive = params.get("recursive", True)
184
+ max_results = params.get("max_results")
185
+
186
+ # Expand path (handles ~, $HOME, etc.)
187
+ path = self.expand_path(path)
170
188
 
171
189
  # Validate path
172
190
  path_validation = self.validate_path(path)
@@ -184,95 +202,154 @@ Fast, intuitive file content search."""
184
202
  if not exists:
185
203
  return error_msg
186
204
 
187
- # Select backend
188
- available = self._get_available_backends()
205
+ await tool_ctx.info(f"Searching for '{pattern}' in {path} (mode: {mode})")
189
206
 
190
- if backend:
191
- # User specified backend
192
- if backend not in available and backend != "grep":
193
- return f"Error: Backend '{backend}' not available. Available: {', '.join(available + ['grep'])}"
194
- selected_backend = backend
195
- elif available:
196
- # Use first available
197
- selected_backend = available[0]
207
+ # Route to appropriate search method
208
+ if mode == "name":
209
+ return await self._find_by_name(
210
+ pattern, path, include, exclude, case_sensitive, recursive, max_results, tool_ctx
211
+ )
212
+ elif mode == "content":
213
+ return await self._find_by_content(
214
+ pattern, path, include, exclude, case_sensitive, max_results, tool_ctx
215
+ )
216
+ elif mode == "both":
217
+ return await self._find_both(
218
+ pattern, path, include, exclude, case_sensitive, recursive, max_results, tool_ctx
219
+ )
198
220
  else:
199
- # Fallback
200
- selected_backend = "grep"
221
+ return f"Error: Invalid mode '{mode}'. Use 'name', 'content', or 'both'."
201
222
 
202
- await tool_ctx.info(
203
- f"Using {selected_backend} to search for '{pattern}' in {path}"
204
- )
223
+ async def _find_by_name(
224
+ self, pattern, path, include, exclude, case_sensitive, recursive, max_results, tool_ctx
225
+ ) -> str:
226
+ """Find files by name pattern."""
227
+ search_path = path or os.getcwd()
228
+
229
+ # If ffind is not available, fall back to basic implementation
230
+ if not FFIND_AVAILABLE:
231
+ return await self._find_files_fallback(
232
+ pattern, search_path, recursive, not case_sensitive, False, False, True, max_results or 100
233
+ )
234
+
235
+ try:
236
+ # Use ffind for efficient searching
237
+ results = []
238
+ count = 0
239
+
240
+ # Configure ffind options
241
+ options = {
242
+ "pattern": pattern,
243
+ "path": search_path,
244
+ "recursive": recursive,
245
+ "ignore_case": not case_sensitive,
246
+ "hidden": False,
247
+ }
248
+
249
+ # Search with ffind
250
+ for filepath in ffind.find(**options):
251
+ # Check if it matches our include/exclude criteria
252
+ filename = os.path.basename(filepath)
253
+ if not self._match_file_pattern(filename, include, exclude):
254
+ continue
255
+
256
+ # Make path relative for cleaner output
257
+ try:
258
+ rel_path = os.path.relpath(filepath, search_path)
259
+ except ValueError:
260
+ rel_path = filepath
261
+
262
+ results.append(rel_path)
263
+ count += 1
264
+
265
+ if max_results and count >= max_results:
266
+ break
267
+
268
+ if not results:
269
+ return f"No files found matching '{pattern}'"
270
+
271
+ # Format output
272
+ output = [f"Found {len(results)} file(s) matching '{pattern}':"]
273
+ output.append("")
274
+
275
+ for filepath in sorted(results):
276
+ output.append(filepath)
277
+
278
+ if max_results and count >= max_results:
279
+ output.append(f"\n... (showing first {max_results} results)")
280
+
281
+ return "\n".join(output)
282
+
283
+ except Exception as e:
284
+ await tool_ctx.error(f"Error during name search: {str(e)}")
285
+ # Fall back to basic implementation
286
+ return await self._find_files_fallback(
287
+ pattern, search_path, recursive, not case_sensitive, False, False, True, max_results or 100
288
+ )
289
+
290
+ async def _find_by_content(
291
+ self, pattern, path, include, exclude, case_sensitive, max_results, tool_ctx
292
+ ) -> str:
293
+ """Find files by content pattern."""
294
+ # Select backend for content search
295
+ available = self._get_available_backends()
296
+ selected_backend = available[0] if available else "grep"
297
+
298
+ await tool_ctx.info(f"Using {selected_backend} for content search")
205
299
 
206
- # Execute search
300
+ # Execute content search
207
301
  if selected_backend == "rg":
208
- return await self._run_ripgrep(
209
- pattern,
210
- path,
211
- include,
212
- exclude,
213
- case_sensitive,
214
- fixed_strings,
215
- show_context,
216
- tool_ctx,
302
+ return await self._run_ripgrep_content(
303
+ pattern, path, include, exclude, case_sensitive, max_results, tool_ctx
217
304
  )
218
305
  elif selected_backend == "ag":
219
- return await self._run_silver_searcher(
220
- pattern,
221
- path,
222
- include,
223
- exclude,
224
- case_sensitive,
225
- fixed_strings,
226
- show_context,
227
- tool_ctx,
306
+ return await self._run_silver_searcher_content(
307
+ pattern, path, include, exclude, case_sensitive, max_results, tool_ctx
228
308
  )
229
309
  elif selected_backend == "ack":
230
- return await self._run_ack(
231
- pattern,
232
- path,
233
- include,
234
- exclude,
235
- case_sensitive,
236
- fixed_strings,
237
- show_context,
238
- tool_ctx,
310
+ return await self._run_ack_content(
311
+ pattern, path, include, exclude, case_sensitive, max_results, tool_ctx
239
312
  )
240
313
  else:
241
- return await self._run_fallback_grep(
242
- pattern,
243
- path,
244
- include,
245
- exclude,
246
- case_sensitive,
247
- fixed_strings,
248
- show_context,
249
- tool_ctx,
314
+ return await self._run_fallback_grep_content(
315
+ pattern, path, include, exclude, case_sensitive, max_results, tool_ctx
250
316
  )
251
317
 
252
- async def _run_ripgrep(
253
- self,
254
- pattern,
255
- path,
256
- include,
257
- exclude,
258
- case_sensitive,
259
- fixed_strings,
260
- show_context,
261
- tool_ctx,
318
+ async def _find_both(
319
+ self, pattern, path, include, exclude, case_sensitive, recursive, max_results, tool_ctx
262
320
  ) -> str:
263
- """Run ripgrep backend."""
321
+ """Find files by both name and content."""
322
+ # Run both searches
323
+ name_results = await self._find_by_name(
324
+ pattern, path, include, exclude, case_sensitive, recursive, max_results, tool_ctx
325
+ )
326
+ content_results = await self._find_by_content(
327
+ pattern, path, include, exclude, case_sensitive, max_results, tool_ctx
328
+ )
329
+
330
+ # Combine results
331
+ output = ["=== NAME MATCHES ==="]
332
+ output.append(name_results)
333
+ output.append("")
334
+ output.append("=== CONTENT MATCHES ===")
335
+ output.append(content_results)
336
+
337
+ return "\n".join(output)
338
+
339
+ async def _run_ripgrep_content(
340
+ self, pattern, path, include, exclude, case_sensitive, max_results, tool_ctx
341
+ ) -> str:
342
+ """Run ripgrep backend for content search."""
264
343
  cmd = ["rg", "--json"]
265
344
 
266
345
  if not case_sensitive:
267
346
  cmd.append("-i")
268
- if fixed_strings:
269
- cmd.append("-F")
270
- if show_context > 0:
271
- cmd.extend(["-C", str(show_context)])
272
347
  if include:
273
348
  cmd.extend(["-g", include])
274
349
  if exclude:
275
350
  cmd.extend(["-g", f"!{exclude}"])
351
+ if max_results:
352
+ cmd.extend(["-m", str(max_results)])
276
353
 
277
354
  cmd.extend([pattern, path])
278
355
 
@@ -293,30 +370,20 @@ Fast, intuitive file content search."""
293
370
  await tool_ctx.error(f"Error running ripgrep: {str(e)}")
294
371
  return f"Error running ripgrep: {str(e)}"
295
372
 
296
- async def _run_silver_searcher(
297
- self,
298
- pattern,
299
- path,
300
- include,
301
- exclude,
302
- case_sensitive,
303
- fixed_strings,
304
- show_context,
305
- tool_ctx,
373
+ async def _run_silver_searcher_content(
374
+ self, pattern, path, include, exclude, case_sensitive, max_results, tool_ctx
306
375
  ) -> str:
307
- """Run silver searcher (ag) backend."""
376
+ """Run silver searcher (ag) backend for content search."""
308
377
  cmd = ["ag", "--nocolor", "--nogroup"]
309
378
 
310
379
  if not case_sensitive:
311
380
  cmd.append("-i")
312
- if fixed_strings:
313
- cmd.append("-F")
314
- if show_context > 0:
315
- cmd.extend(["-C", str(show_context)])
316
381
  if include:
317
382
  cmd.extend(["-G", include])
318
383
  if exclude:
319
384
  cmd.extend(["--ignore", exclude])
385
+ if max_results:
386
+ cmd.extend(["-m", str(max_results)])
320
387
 
321
388
  cmd.extend([pattern, path])
322
389
 
@@ -342,35 +409,23 @@ Fast, intuitive file content search."""
342
409
  await tool_ctx.error(f"Error running ag: {str(e)}")
343
410
  return f"Error running ag: {str(e)}"
344
411
 
345
- async def _run_ack(
346
- self,
347
- pattern,
348
- path,
349
- include,
350
- exclude,
351
- case_sensitive,
352
- fixed_strings,
353
- show_context,
354
- tool_ctx,
412
+ async def _run_ack_content(
413
+ self, pattern, path, include, exclude, case_sensitive, max_results, tool_ctx
355
414
  ) -> str:
356
- """Run ack backend."""
415
+ """Run ack backend for content search."""
357
416
  cmd = ["ack", "--nocolor", "--nogroup"]
358
417
 
359
418
  if not case_sensitive:
360
419
  cmd.append("-i")
361
- if fixed_strings:
362
- cmd.append("-Q")
363
- if show_context > 0:
364
- cmd.extend(["-C", str(show_context)])
365
420
  if include:
366
421
  # ack uses different syntax for file patterns
367
- cmd.extend(
368
- [
369
- "--type-add",
370
- f"custom:ext:{include.replace('*.', '')}",
371
- "--type=custom",
372
- ]
373
- )
422
+ cmd.extend([
423
+ "--type-add",
424
+ f"custom:ext:{include.replace('*.', '')}",
425
+ "--type=custom",
426
+ ])
427
+ if max_results:
428
+ cmd.extend(["-m", str(max_results)])
374
429
 
375
430
  cmd.extend([pattern, path])
376
431
 
@@ -396,18 +451,10 @@ Fast, intuitive file content search."""
396
451
  await tool_ctx.error(f"Error running ack: {str(e)}")
397
452
  return f"Error running ack: {str(e)}"
398
453
 
399
- async def _run_fallback_grep(
400
- self,
401
- pattern,
402
- path,
403
- include,
404
- exclude,
405
- case_sensitive,
406
- fixed_strings,
407
- show_context,
408
- tool_ctx,
454
+ async def _run_fallback_grep_content(
455
+ self, pattern, path, include, exclude, case_sensitive, max_results, tool_ctx
409
456
  ) -> str:
410
- """Fallback Python implementation."""
457
+ """Fallback Python implementation for content search."""
411
458
  await tool_ctx.info("Using fallback Python grep implementation")
412
459
 
413
460
  try:
@@ -428,17 +475,11 @@ Fast, intuitive file content search."""
428
475
  return "No matching files found."
429
476
 
430
477
  # Compile pattern
431
- if fixed_strings:
432
- pattern_re = re.escape(pattern)
433
- else:
434
- pattern_re = pattern
435
-
436
- if not case_sensitive:
437
- flags = re.IGNORECASE
438
- else:
439
- flags = 0
440
-
441
- regex = re.compile(pattern_re, flags)
478
+ flags = 0 if case_sensitive else re.IGNORECASE
479
+ try:
480
+ regex = re.compile(pattern, flags)
481
+ except re.error as e:
482
+ return f"Error: Invalid regex pattern: {e}"
442
483
 
443
484
  # Search files
444
485
  results = []
@@ -451,28 +492,20 @@ Fast, intuitive file content search."""
451
492
 
452
493
  for i, line in enumerate(lines, 1):
453
494
  if regex.search(line):
454
- # Format result with context if requested
455
- if show_context > 0:
456
- start = max(0, i - show_context - 1)
457
- end = min(len(lines), i + show_context)
458
-
459
- context_lines = []
460
- for j in range(start, end):
461
- prefix = ":" if j + 1 == i else "-"
462
- context_lines.append(
463
- f"{file_path}:{j + 1}{prefix}{lines[j].rstrip()}"
464
- )
465
- results.extend(context_lines)
466
- results.append("") # Separator
467
- else:
468
- results.append(f"{file_path}:{i}:{line.rstrip()}")
495
+ results.append(f"{file_path}:{i}:{line.rstrip()}")
469
496
  total_matches += 1
497
+
498
+ if max_results and total_matches >= max_results:
499
+ break
470
500
 
471
501
  except UnicodeDecodeError:
472
502
  pass # Skip binary files
473
503
  except Exception as e:
474
504
  await tool_ctx.warning(f"Error reading {file_path}: {str(e)}")
475
505
 
506
+ if max_results and total_matches >= max_results:
507
+ break
508
+
476
509
  if not results:
477
510
  return "No matches found."
478
511
 
@@ -482,9 +515,109 @@ Fast, intuitive file content search."""
482
515
  await tool_ctx.error(f"Error in fallback grep: {str(e)}")
483
516
  return f"Error in fallback grep: {str(e)}"
484
517
 
485
- def _match_file_pattern(
486
- self, filename: str, include: Optional[str], exclude: Optional[str]
487
- ) -> bool:
518
+ async def _find_files_fallback(
519
+ self,
520
+ pattern: str,
521
+ search_path: str,
522
+ recursive: bool,
523
+ ignore_case: bool,
524
+ hidden: bool,
525
+ dirs_only: bool,
526
+ files_only: bool,
527
+ max_results: int,
528
+ ) -> str:
529
+ """Fallback implementation for file name search when ffind is not available."""
530
+ results = []
531
+ count = 0
532
+
533
+ # Convert pattern for case-insensitive matching
534
+ if ignore_case:
535
+ pattern = pattern.lower()
536
+
537
+ try:
538
+ if recursive:
539
+ # Walk directory tree
540
+ for root, dirs, files in os.walk(search_path):
541
+ # Skip hidden directories if not requested
542
+ if not hidden:
543
+ dirs[:] = [d for d in dirs if not d.startswith(".")]
544
+
545
+ # Check directories
546
+ if not files_only:
547
+ for dirname in dirs:
548
+ if self._match_pattern(dirname, pattern, ignore_case):
549
+ filepath = os.path.join(root, dirname)
550
+ rel_path = os.path.relpath(filepath, search_path)
551
+ results.append(rel_path + "/")
552
+ count += 1
553
+ if count >= max_results:
554
+ break
555
+
556
+ # Check files
557
+ if not dirs_only:
558
+ for filename in files:
559
+ if not hidden and filename.startswith("."):
560
+ continue
561
+
562
+ if self._match_pattern(filename, pattern, ignore_case):
563
+ filepath = os.path.join(root, filename)
564
+ rel_path = os.path.relpath(filepath, search_path)
565
+ results.append(rel_path)
566
+ count += 1
567
+ if count >= max_results:
568
+ break
569
+
570
+ if count >= max_results:
571
+ break
572
+ else:
573
+ # Only search in the specified directory
574
+ for entry in os.listdir(search_path):
575
+ if not hidden and entry.startswith("."):
576
+ continue
577
+
578
+ filepath = os.path.join(search_path, entry)
579
+ is_dir = os.path.isdir(filepath)
580
+
581
+ if dirs_only and not is_dir:
582
+ continue
583
+ if files_only and is_dir:
584
+ continue
585
+
586
+ if self._match_pattern(entry, pattern, ignore_case):
587
+ results.append(entry + "/" if is_dir else entry)
588
+ count += 1
589
+ if count >= max_results:
590
+ break
591
+
592
+ if not results:
593
+ return f"No files found matching '{pattern}' (using fallback search)"
594
+
595
+ # Format output
596
+ output = [f"Found {len(results)} file(s) matching '{pattern}' (using fallback search):"]
597
+ output.append("")
598
+
599
+ for filepath in sorted(results):
600
+ output.append(filepath)
601
+
602
+ if count >= max_results:
603
+ output.append(f"\n... (showing first {max_results} results)")
604
+
605
+ if not FFIND_AVAILABLE:
606
+ output.append("\nNote: Install 'ffind' for faster searching: pip install ffind")
607
+
608
+ return "\n".join(output)
609
+
610
+ except Exception as e:
611
+ return f"Error searching for files: {str(e)}"
612
+
613
+ def _match_pattern(self, filename: str, pattern: str, ignore_case: bool) -> bool:
614
+ """Check if filename matches pattern."""
615
+ if ignore_case:
616
+ return fnmatch.fnmatch(filename.lower(), pattern)
617
+ else:
618
+ return fnmatch.fnmatch(filename, pattern)
619
+
620
+ def _match_file_pattern(self, filename: str, include: Optional[str], exclude: Optional[str]) -> bool:
488
621
  """Check if filename matches include/exclude patterns."""
489
622
  if include and not fnmatch.fnmatch(filename, include):
490
623
  return False
@@ -534,4 +667,4 @@ Fast, intuitive file content search."""
534
667
 
535
668
  def register(self, mcp_server) -> None:
536
669
  """Register this tool with the MCP server."""
537
- pass
670
+ pass