hanzo-mcp 0.9.0__py3-none-any.whl → 0.9.1__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 +234 -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 +2 -3
  54. hanzo_mcp/tools/filesystem/base.py +0 -16
  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 +5 -4
  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 +5 -4
  66. hanzo_mcp/tools/filesystem/read.py +11 -8
  67. hanzo_mcp/tools/filesystem/rules_tool.py +5 -3
  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 +12 -6
  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.1.dist-info}/METADATA +1 -1
  119. hanzo_mcp-0.9.1.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.1.dist-info}/WHEEL +0 -0
  134. {hanzo_mcp-0.9.0.dist-info → hanzo_mcp-0.9.1.dist-info}/entry_points.txt +0 -0
  135. {hanzo_mcp-0.9.0.dist-info → hanzo_mcp-0.9.1.dist-info}/top_level.txt +0 -0
@@ -1,10 +1,9 @@
1
1
  """Unified find tool implementation.
2
2
 
3
- This module provides the FindTool for finding files by name or content using
3
+ This module provides the FindTool for finding text patterns in files using
4
4
  multiple search backends in order of preference: rg > ag > ack > grep.
5
5
  """
6
6
 
7
- import os
8
7
  import re
9
8
  import json
10
9
  import shutil
@@ -18,26 +17,21 @@ from typing import (
18
17
  TypedDict,
19
18
  final,
20
19
  override,
21
- Literal,
22
20
  )
23
21
  from pathlib import Path
24
22
 
25
23
  from pydantic import Field
26
24
  from mcp.server.fastmcp import Context as MCPContext
27
25
 
28
- from hanzo_mcp.tools.filesystem.base import FilesystemBaseTool
26
+ from hanzo_mcp.tools.common.auto_timeout import auto_timeout
29
27
 
30
- try:
31
- import ffind
32
- FFIND_AVAILABLE = True
33
- except ImportError:
34
- FFIND_AVAILABLE = False
28
+ from hanzo_mcp.tools.filesystem.base import FilesystemBaseTool
35
29
 
36
30
  # Parameter types
37
31
  Pattern = Annotated[
38
32
  str,
39
33
  Field(
40
- description="Pattern to search for (file name pattern or content regex/literal)",
34
+ description="Pattern to search for (regex or literal)",
41
35
  min_length=1,
42
36
  ),
43
37
  ]
@@ -50,18 +44,10 @@ SearchPath = Annotated[
50
44
  ),
51
45
  ]
52
46
 
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
-
61
47
  Include = Annotated[
62
48
  Optional[str],
63
49
  Field(
64
- description='File pattern to include (e.g. "*.js", "*.{ts,tsx}")',
50
+ description='File pattern to include (e.g. "*.js")',
65
51
  default=None,
66
52
  ),
67
53
  ]
@@ -78,22 +64,30 @@ CaseSensitive = Annotated[
78
64
  bool,
79
65
  Field(
80
66
  description="Case sensitive search",
81
- default=False,
67
+ default=True,
82
68
  ),
83
69
  ]
84
70
 
85
- Recursive = Annotated[
71
+ FixedStrings = Annotated[
86
72
  bool,
87
73
  Field(
88
- description="Search recursively in subdirectories",
89
- default=True,
74
+ description="Treat pattern as literal string, not regex",
75
+ default=False,
90
76
  ),
91
77
  ]
92
78
 
93
- MaxResults = Annotated[
94
- Optional[int],
79
+ ShowContext = Annotated[
80
+ int,
95
81
  Field(
96
- description="Maximum number of results to return",
82
+ description="Lines of context to show around matches",
83
+ default=0,
84
+ ),
85
+ ]
86
+
87
+ Backend = Annotated[
88
+ Optional[str],
89
+ Field(
90
+ description="Force specific backend: rg, ag, ack, grep",
97
91
  default=None,
98
92
  ),
99
93
  ]
@@ -104,17 +98,17 @@ class FindParams(TypedDict, total=False):
104
98
 
105
99
  pattern: str
106
100
  path: str
107
- mode: Literal["name", "content", "both"]
108
101
  include: Optional[str]
109
102
  exclude: Optional[str]
110
103
  case_sensitive: bool
111
- recursive: bool
112
- max_results: Optional[int]
104
+ fixed_strings: bool
105
+ show_context: int
106
+ backend: Optional[str]
113
107
 
114
108
 
115
109
  @final
116
110
  class FindTool(FilesystemBaseTool):
117
- """Unified find tool for searching by file name or content."""
111
+ """Unified find tool with multiple backend support."""
118
112
 
119
113
  def __init__(self, permission_manager):
120
114
  """Initialize the find tool."""
@@ -133,24 +127,17 @@ class FindTool(FilesystemBaseTool):
133
127
  def description(self) -> str:
134
128
  """Get the tool description."""
135
129
  backends = self._get_available_backends()
136
- backend_str = ", ".join(backends) if backends else "fallback search"
130
+ backend_str = ", ".join(backends) if backends else "fallback grep"
137
131
 
138
- return f"""Find files by name, content, or both. Available backends: {backend_str}.
132
+ return f"""Find pattern in files (like ffind). Available: {backend_str}.
139
133
 
140
- Examples:
141
- # Find by file name (default mode)
142
- find "*.py"
143
- find "test_*" ./src
144
- find "README.*" --case-sensitive
134
+ Usage:
135
+ find "TODO"
136
+ find "error.*fatal" ./src
137
+ find "config" --include "*.json"
138
+ find "password" --exclude "*.log"
145
139
 
146
- # Find by content
147
- find "TODO" --mode content
148
- find "error.*fatal" ./src --mode content
149
-
150
- # Find both name and content
151
- find "config" --mode both --include "*.json"
152
-
153
- Supports wildcards for names, regex for content."""
140
+ Fast, intuitive file content search."""
154
141
 
155
142
  def _get_available_backends(self) -> List[str]:
156
143
  """Get list of available search backends."""
@@ -162,6 +149,9 @@ Supports wildcards for names, regex for content."""
162
149
  return self._available_backends
163
150
 
164
151
  @override
152
+ @auto_timeout("find")
153
+
154
+
165
155
  async def call(
166
156
  self,
167
157
  ctx: MCPContext,
@@ -176,15 +166,12 @@ Supports wildcards for names, regex for content."""
176
166
  return "Error: pattern is required"
177
167
 
178
168
  path = params.get("path", ".")
179
- mode = params.get("mode", "name")
180
169
  include = params.get("include")
181
170
  exclude = params.get("exclude")
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)
171
+ case_sensitive = params.get("case_sensitive", True)
172
+ fixed_strings = params.get("fixed_strings", False)
173
+ show_context = params.get("show_context", 0)
174
+ backend = params.get("backend")
188
175
 
189
176
  # Validate path
190
177
  path_validation = self.validate_path(path)
@@ -202,154 +189,93 @@ Supports wildcards for names, regex for content."""
202
189
  if not exists:
203
190
  return error_msg
204
191
 
205
- await tool_ctx.info(f"Searching for '{pattern}' in {path} (mode: {mode})")
192
+ # Select backend
193
+ available = self._get_available_backends()
206
194
 
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
- )
195
+ if backend:
196
+ # User specified backend
197
+ if backend not in available and backend != "grep":
198
+ return f"Error: Backend '{backend}' not available. Available: {', '.join(available + ['grep'])}"
199
+ selected_backend = backend
200
+ elif available:
201
+ # Use first available
202
+ selected_backend = available[0]
220
203
  else:
221
- return f"Error: Invalid mode '{mode}'. Use 'name', 'content', or 'both'."
222
-
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}'"
204
+ # Fallback
205
+ selected_backend = "grep"
270
206
 
271
- # Format output
272
- output = [f"Found {len(results)} file(s) matching '{pattern}':"]
273
- output.append("")
207
+ await tool_ctx.info(f"Using {selected_backend} to search for '{pattern}' in {path}")
274
208
 
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")
299
-
300
- # Execute content search
209
+ # Execute search
301
210
  if selected_backend == "rg":
302
- return await self._run_ripgrep_content(
303
- pattern, path, include, exclude, case_sensitive, max_results, tool_ctx
211
+ return await self._run_ripgrep(
212
+ pattern,
213
+ path,
214
+ include,
215
+ exclude,
216
+ case_sensitive,
217
+ fixed_strings,
218
+ show_context,
219
+ tool_ctx,
304
220
  )
305
221
  elif selected_backend == "ag":
306
- return await self._run_silver_searcher_content(
307
- pattern, path, include, exclude, case_sensitive, max_results, tool_ctx
222
+ return await self._run_silver_searcher(
223
+ pattern,
224
+ path,
225
+ include,
226
+ exclude,
227
+ case_sensitive,
228
+ fixed_strings,
229
+ show_context,
230
+ tool_ctx,
308
231
  )
309
232
  elif selected_backend == "ack":
310
- return await self._run_ack_content(
311
- pattern, path, include, exclude, case_sensitive, max_results, tool_ctx
233
+ return await self._run_ack(
234
+ pattern,
235
+ path,
236
+ include,
237
+ exclude,
238
+ case_sensitive,
239
+ fixed_strings,
240
+ show_context,
241
+ tool_ctx,
312
242
  )
313
243
  else:
314
- return await self._run_fallback_grep_content(
315
- pattern, path, include, exclude, case_sensitive, max_results, tool_ctx
244
+ return await self._run_fallback_grep(
245
+ pattern,
246
+ path,
247
+ include,
248
+ exclude,
249
+ case_sensitive,
250
+ fixed_strings,
251
+ show_context,
252
+ tool_ctx,
316
253
  )
317
254
 
318
- async def _find_both(
319
- self, pattern, path, include, exclude, case_sensitive, recursive, max_results, tool_ctx
320
- ) -> str:
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
255
+ async def _run_ripgrep(
256
+ self,
257
+ pattern,
258
+ path,
259
+ include,
260
+ exclude,
261
+ case_sensitive,
262
+ fixed_strings,
263
+ show_context,
264
+ tool_ctx,
341
265
  ) -> str:
342
- """Run ripgrep backend for content search."""
266
+ """Run ripgrep backend."""
343
267
  cmd = ["rg", "--json"]
344
268
 
345
269
  if not case_sensitive:
346
270
  cmd.append("-i")
271
+ if fixed_strings:
272
+ cmd.append("-F")
273
+ if show_context > 0:
274
+ cmd.extend(["-C", str(show_context)])
347
275
  if include:
348
276
  cmd.extend(["-g", include])
349
277
  if exclude:
350
278
  cmd.extend(["-g", f"!{exclude}"])
351
- if max_results:
352
- cmd.extend(["-m", str(max_results)])
353
279
 
354
280
  cmd.extend([pattern, path])
355
281
 
@@ -370,20 +296,30 @@ Supports wildcards for names, regex for content."""
370
296
  await tool_ctx.error(f"Error running ripgrep: {str(e)}")
371
297
  return f"Error running ripgrep: {str(e)}"
372
298
 
373
- async def _run_silver_searcher_content(
374
- self, pattern, path, include, exclude, case_sensitive, max_results, tool_ctx
299
+ async def _run_silver_searcher(
300
+ self,
301
+ pattern,
302
+ path,
303
+ include,
304
+ exclude,
305
+ case_sensitive,
306
+ fixed_strings,
307
+ show_context,
308
+ tool_ctx,
375
309
  ) -> str:
376
- """Run silver searcher (ag) backend for content search."""
310
+ """Run silver searcher (ag) backend."""
377
311
  cmd = ["ag", "--nocolor", "--nogroup"]
378
312
 
379
313
  if not case_sensitive:
380
314
  cmd.append("-i")
315
+ if fixed_strings:
316
+ cmd.append("-F")
317
+ if show_context > 0:
318
+ cmd.extend(["-C", str(show_context)])
381
319
  if include:
382
320
  cmd.extend(["-G", include])
383
321
  if exclude:
384
322
  cmd.extend(["--ignore", exclude])
385
- if max_results:
386
- cmd.extend(["-m", str(max_results)])
387
323
 
388
324
  cmd.extend([pattern, path])
389
325
 
@@ -409,23 +345,35 @@ Supports wildcards for names, regex for content."""
409
345
  await tool_ctx.error(f"Error running ag: {str(e)}")
410
346
  return f"Error running ag: {str(e)}"
411
347
 
412
- async def _run_ack_content(
413
- self, pattern, path, include, exclude, case_sensitive, max_results, tool_ctx
348
+ async def _run_ack(
349
+ self,
350
+ pattern,
351
+ path,
352
+ include,
353
+ exclude,
354
+ case_sensitive,
355
+ fixed_strings,
356
+ show_context,
357
+ tool_ctx,
414
358
  ) -> str:
415
- """Run ack backend for content search."""
359
+ """Run ack backend."""
416
360
  cmd = ["ack", "--nocolor", "--nogroup"]
417
361
 
418
362
  if not case_sensitive:
419
363
  cmd.append("-i")
364
+ if fixed_strings:
365
+ cmd.append("-Q")
366
+ if show_context > 0:
367
+ cmd.extend(["-C", str(show_context)])
420
368
  if include:
421
369
  # ack uses different syntax for file patterns
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)])
370
+ cmd.extend(
371
+ [
372
+ "--type-add",
373
+ f"custom:ext:{include.replace('*.', '')}",
374
+ "--type=custom",
375
+ ]
376
+ )
429
377
 
430
378
  cmd.extend([pattern, path])
431
379
 
@@ -451,10 +399,18 @@ Supports wildcards for names, regex for content."""
451
399
  await tool_ctx.error(f"Error running ack: {str(e)}")
452
400
  return f"Error running ack: {str(e)}"
453
401
 
454
- async def _run_fallback_grep_content(
455
- self, pattern, path, include, exclude, case_sensitive, max_results, tool_ctx
402
+ async def _run_fallback_grep(
403
+ self,
404
+ pattern,
405
+ path,
406
+ include,
407
+ exclude,
408
+ case_sensitive,
409
+ fixed_strings,
410
+ show_context,
411
+ tool_ctx,
456
412
  ) -> str:
457
- """Fallback Python implementation for content search."""
413
+ """Fallback Python implementation."""
458
414
  await tool_ctx.info("Using fallback Python grep implementation")
459
415
 
460
416
  try:
@@ -475,11 +431,17 @@ Supports wildcards for names, regex for content."""
475
431
  return "No matching files found."
476
432
 
477
433
  # Compile pattern
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}"
434
+ if fixed_strings:
435
+ pattern_re = re.escape(pattern)
436
+ else:
437
+ pattern_re = pattern
438
+
439
+ if not case_sensitive:
440
+ flags = re.IGNORECASE
441
+ else:
442
+ flags = 0
443
+
444
+ regex = re.compile(pattern_re, flags)
483
445
 
484
446
  # Search files
485
447
  results = []
@@ -492,20 +454,26 @@ Supports wildcards for names, regex for content."""
492
454
 
493
455
  for i, line in enumerate(lines, 1):
494
456
  if regex.search(line):
495
- results.append(f"{file_path}:{i}:{line.rstrip()}")
457
+ # Format result with context if requested
458
+ if show_context > 0:
459
+ start = max(0, i - show_context - 1)
460
+ end = min(len(lines), i + show_context)
461
+
462
+ context_lines = []
463
+ for j in range(start, end):
464
+ prefix = ":" if j + 1 == i else "-"
465
+ context_lines.append(f"{file_path}:{j + 1}{prefix}{lines[j].rstrip()}")
466
+ results.extend(context_lines)
467
+ results.append("") # Separator
468
+ else:
469
+ results.append(f"{file_path}:{i}:{line.rstrip()}")
496
470
  total_matches += 1
497
-
498
- if max_results and total_matches >= max_results:
499
- break
500
471
 
501
472
  except UnicodeDecodeError:
502
473
  pass # Skip binary files
503
474
  except Exception as e:
504
475
  await tool_ctx.warning(f"Error reading {file_path}: {str(e)}")
505
476
 
506
- if max_results and total_matches >= max_results:
507
- break
508
-
509
477
  if not results:
510
478
  return "No matches found."
511
479
 
@@ -515,108 +483,6 @@ Supports wildcards for names, regex for content."""
515
483
  await tool_ctx.error(f"Error in fallback grep: {str(e)}")
516
484
  return f"Error in fallback grep: {str(e)}"
517
485
 
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
486
  def _match_file_pattern(self, filename: str, include: Optional[str], exclude: Optional[str]) -> bool:
621
487
  """Check if filename matches include/exclude patterns."""
622
488
  if include and not fnmatch.fnmatch(filename, include):
@@ -667,4 +533,4 @@ Supports wildcards for names, regex for content."""
667
533
 
668
534
  def register(self, mcp_server) -> None:
669
535
  """Register this tool with the MCP server."""
670
- pass
536
+ pass