hanzo-mcp 0.7.6__py3-none-any.whl → 0.8.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 (178) hide show
  1. hanzo_mcp/__init__.py +7 -1
  2. hanzo_mcp/__main__.py +1 -1
  3. hanzo_mcp/analytics/__init__.py +2 -2
  4. hanzo_mcp/analytics/posthog_analytics.py +76 -82
  5. hanzo_mcp/cli.py +31 -36
  6. hanzo_mcp/cli_enhanced.py +94 -72
  7. hanzo_mcp/cli_plugin.py +27 -17
  8. hanzo_mcp/config/__init__.py +2 -2
  9. hanzo_mcp/config/settings.py +112 -88
  10. hanzo_mcp/config/tool_config.py +32 -34
  11. hanzo_mcp/dev_server.py +66 -67
  12. hanzo_mcp/prompts/__init__.py +94 -12
  13. hanzo_mcp/prompts/enhanced_prompts.py +809 -0
  14. hanzo_mcp/prompts/example_custom_prompt.py +6 -5
  15. hanzo_mcp/prompts/project_todo_reminder.py +0 -1
  16. hanzo_mcp/prompts/tool_explorer.py +10 -7
  17. hanzo_mcp/server.py +17 -21
  18. hanzo_mcp/server_enhanced.py +15 -22
  19. hanzo_mcp/tools/__init__.py +56 -28
  20. hanzo_mcp/tools/agent/__init__.py +16 -19
  21. hanzo_mcp/tools/agent/agent.py +82 -65
  22. hanzo_mcp/tools/agent/agent_tool.py +152 -122
  23. hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +66 -62
  24. hanzo_mcp/tools/agent/clarification_protocol.py +55 -50
  25. hanzo_mcp/tools/agent/clarification_tool.py +11 -10
  26. hanzo_mcp/tools/agent/claude_cli_tool.py +21 -20
  27. hanzo_mcp/tools/agent/claude_desktop_auth.py +130 -144
  28. hanzo_mcp/tools/agent/cli_agent_base.py +59 -53
  29. hanzo_mcp/tools/agent/code_auth.py +102 -107
  30. hanzo_mcp/tools/agent/code_auth_tool.py +28 -27
  31. hanzo_mcp/tools/agent/codex_cli_tool.py +20 -19
  32. hanzo_mcp/tools/agent/critic_tool.py +86 -73
  33. hanzo_mcp/tools/agent/gemini_cli_tool.py +21 -20
  34. hanzo_mcp/tools/agent/grok_cli_tool.py +21 -20
  35. hanzo_mcp/tools/agent/iching_tool.py +404 -139
  36. hanzo_mcp/tools/agent/network_tool.py +89 -73
  37. hanzo_mcp/tools/agent/prompt.py +2 -1
  38. hanzo_mcp/tools/agent/review_tool.py +101 -98
  39. hanzo_mcp/tools/agent/swarm_alias.py +87 -0
  40. hanzo_mcp/tools/agent/swarm_tool.py +246 -161
  41. hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +134 -92
  42. hanzo_mcp/tools/agent/tool_adapter.py +21 -11
  43. hanzo_mcp/tools/common/__init__.py +1 -1
  44. hanzo_mcp/tools/common/base.py +3 -5
  45. hanzo_mcp/tools/common/batch_tool.py +46 -39
  46. hanzo_mcp/tools/common/config_tool.py +120 -84
  47. hanzo_mcp/tools/common/context.py +1 -5
  48. hanzo_mcp/tools/common/context_fix.py +5 -3
  49. hanzo_mcp/tools/common/critic_tool.py +4 -8
  50. hanzo_mcp/tools/common/decorators.py +58 -56
  51. hanzo_mcp/tools/common/enhanced_base.py +29 -32
  52. hanzo_mcp/tools/common/fastmcp_pagination.py +91 -94
  53. hanzo_mcp/tools/common/forgiving_edit.py +91 -87
  54. hanzo_mcp/tools/common/mode.py +15 -17
  55. hanzo_mcp/tools/common/mode_loader.py +27 -24
  56. hanzo_mcp/tools/common/paginated_base.py +61 -53
  57. hanzo_mcp/tools/common/paginated_response.py +72 -79
  58. hanzo_mcp/tools/common/pagination.py +50 -53
  59. hanzo_mcp/tools/common/permissions.py +4 -4
  60. hanzo_mcp/tools/common/personality.py +186 -138
  61. hanzo_mcp/tools/common/plugin_loader.py +54 -54
  62. hanzo_mcp/tools/common/stats.py +65 -47
  63. hanzo_mcp/tools/common/test_helpers.py +31 -0
  64. hanzo_mcp/tools/common/thinking_tool.py +4 -8
  65. hanzo_mcp/tools/common/tool_disable.py +17 -12
  66. hanzo_mcp/tools/common/tool_enable.py +13 -14
  67. hanzo_mcp/tools/common/tool_list.py +36 -28
  68. hanzo_mcp/tools/common/truncate.py +23 -23
  69. hanzo_mcp/tools/config/__init__.py +4 -4
  70. hanzo_mcp/tools/config/config_tool.py +42 -29
  71. hanzo_mcp/tools/config/index_config.py +37 -34
  72. hanzo_mcp/tools/config/mode_tool.py +175 -55
  73. hanzo_mcp/tools/database/__init__.py +15 -12
  74. hanzo_mcp/tools/database/database_manager.py +77 -75
  75. hanzo_mcp/tools/database/graph.py +137 -91
  76. hanzo_mcp/tools/database/graph_add.py +30 -18
  77. hanzo_mcp/tools/database/graph_query.py +178 -102
  78. hanzo_mcp/tools/database/graph_remove.py +33 -28
  79. hanzo_mcp/tools/database/graph_search.py +97 -75
  80. hanzo_mcp/tools/database/graph_stats.py +91 -59
  81. hanzo_mcp/tools/database/sql.py +107 -79
  82. hanzo_mcp/tools/database/sql_query.py +30 -24
  83. hanzo_mcp/tools/database/sql_search.py +29 -25
  84. hanzo_mcp/tools/database/sql_stats.py +47 -35
  85. hanzo_mcp/tools/editor/neovim_command.py +25 -28
  86. hanzo_mcp/tools/editor/neovim_edit.py +21 -23
  87. hanzo_mcp/tools/editor/neovim_session.py +60 -54
  88. hanzo_mcp/tools/filesystem/__init__.py +31 -30
  89. hanzo_mcp/tools/filesystem/ast_multi_edit.py +329 -249
  90. hanzo_mcp/tools/filesystem/ast_tool.py +4 -4
  91. hanzo_mcp/tools/filesystem/base.py +1 -1
  92. hanzo_mcp/tools/filesystem/batch_search.py +316 -224
  93. hanzo_mcp/tools/filesystem/content_replace.py +4 -4
  94. hanzo_mcp/tools/filesystem/diff.py +71 -59
  95. hanzo_mcp/tools/filesystem/directory_tree.py +7 -7
  96. hanzo_mcp/tools/filesystem/directory_tree_paginated.py +49 -37
  97. hanzo_mcp/tools/filesystem/edit.py +4 -4
  98. hanzo_mcp/tools/filesystem/find.py +173 -80
  99. hanzo_mcp/tools/filesystem/find_files.py +73 -52
  100. hanzo_mcp/tools/filesystem/git_search.py +157 -104
  101. hanzo_mcp/tools/filesystem/grep.py +8 -8
  102. hanzo_mcp/tools/filesystem/multi_edit.py +4 -8
  103. hanzo_mcp/tools/filesystem/read.py +12 -10
  104. hanzo_mcp/tools/filesystem/rules_tool.py +59 -43
  105. hanzo_mcp/tools/filesystem/search_tool.py +263 -207
  106. hanzo_mcp/tools/filesystem/symbols_tool.py +94 -54
  107. hanzo_mcp/tools/filesystem/tree.py +35 -33
  108. hanzo_mcp/tools/filesystem/unix_aliases.py +13 -18
  109. hanzo_mcp/tools/filesystem/watch.py +37 -36
  110. hanzo_mcp/tools/filesystem/write.py +4 -8
  111. hanzo_mcp/tools/jupyter/__init__.py +4 -4
  112. hanzo_mcp/tools/jupyter/base.py +4 -5
  113. hanzo_mcp/tools/jupyter/jupyter.py +67 -47
  114. hanzo_mcp/tools/jupyter/notebook_edit.py +4 -4
  115. hanzo_mcp/tools/jupyter/notebook_read.py +4 -7
  116. hanzo_mcp/tools/llm/__init__.py +5 -7
  117. hanzo_mcp/tools/llm/consensus_tool.py +72 -52
  118. hanzo_mcp/tools/llm/llm_manage.py +101 -60
  119. hanzo_mcp/tools/llm/llm_tool.py +226 -166
  120. hanzo_mcp/tools/llm/provider_tools.py +25 -26
  121. hanzo_mcp/tools/lsp/__init__.py +1 -1
  122. hanzo_mcp/tools/lsp/lsp_tool.py +228 -143
  123. hanzo_mcp/tools/mcp/__init__.py +2 -3
  124. hanzo_mcp/tools/mcp/mcp_add.py +27 -25
  125. hanzo_mcp/tools/mcp/mcp_remove.py +7 -8
  126. hanzo_mcp/tools/mcp/mcp_stats.py +23 -22
  127. hanzo_mcp/tools/mcp/mcp_tool.py +129 -98
  128. hanzo_mcp/tools/memory/__init__.py +39 -21
  129. hanzo_mcp/tools/memory/knowledge_tools.py +124 -99
  130. hanzo_mcp/tools/memory/memory_tools.py +90 -108
  131. hanzo_mcp/tools/search/__init__.py +7 -2
  132. hanzo_mcp/tools/search/find_tool.py +297 -212
  133. hanzo_mcp/tools/search/unified_search.py +366 -314
  134. hanzo_mcp/tools/shell/__init__.py +8 -7
  135. hanzo_mcp/tools/shell/auto_background.py +56 -49
  136. hanzo_mcp/tools/shell/base.py +1 -1
  137. hanzo_mcp/tools/shell/base_process.py +75 -75
  138. hanzo_mcp/tools/shell/bash_session.py +2 -2
  139. hanzo_mcp/tools/shell/bash_session_executor.py +4 -4
  140. hanzo_mcp/tools/shell/bash_tool.py +24 -31
  141. hanzo_mcp/tools/shell/command_executor.py +12 -12
  142. hanzo_mcp/tools/shell/logs.py +43 -33
  143. hanzo_mcp/tools/shell/npx.py +13 -13
  144. hanzo_mcp/tools/shell/npx_background.py +24 -21
  145. hanzo_mcp/tools/shell/npx_tool.py +18 -22
  146. hanzo_mcp/tools/shell/open.py +19 -21
  147. hanzo_mcp/tools/shell/pkill.py +31 -26
  148. hanzo_mcp/tools/shell/process_tool.py +32 -32
  149. hanzo_mcp/tools/shell/processes.py +57 -58
  150. hanzo_mcp/tools/shell/run_background.py +24 -25
  151. hanzo_mcp/tools/shell/run_command.py +5 -5
  152. hanzo_mcp/tools/shell/run_command_windows.py +5 -5
  153. hanzo_mcp/tools/shell/session_storage.py +3 -3
  154. hanzo_mcp/tools/shell/streaming_command.py +141 -126
  155. hanzo_mcp/tools/shell/uvx.py +24 -25
  156. hanzo_mcp/tools/shell/uvx_background.py +35 -33
  157. hanzo_mcp/tools/shell/uvx_tool.py +18 -22
  158. hanzo_mcp/tools/todo/__init__.py +6 -2
  159. hanzo_mcp/tools/todo/todo.py +50 -37
  160. hanzo_mcp/tools/todo/todo_read.py +5 -8
  161. hanzo_mcp/tools/todo/todo_write.py +5 -7
  162. hanzo_mcp/tools/vector/__init__.py +40 -28
  163. hanzo_mcp/tools/vector/ast_analyzer.py +176 -143
  164. hanzo_mcp/tools/vector/git_ingester.py +170 -179
  165. hanzo_mcp/tools/vector/index_tool.py +96 -44
  166. hanzo_mcp/tools/vector/infinity_store.py +283 -228
  167. hanzo_mcp/tools/vector/mock_infinity.py +39 -40
  168. hanzo_mcp/tools/vector/project_manager.py +88 -78
  169. hanzo_mcp/tools/vector/vector.py +59 -42
  170. hanzo_mcp/tools/vector/vector_index.py +30 -27
  171. hanzo_mcp/tools/vector/vector_search.py +64 -45
  172. hanzo_mcp/types.py +6 -4
  173. {hanzo_mcp-0.7.6.dist-info → hanzo_mcp-0.8.0.dist-info}/METADATA +1 -1
  174. hanzo_mcp-0.8.0.dist-info/RECORD +185 -0
  175. hanzo_mcp-0.7.6.dist-info/RECORD +0 -182
  176. {hanzo_mcp-0.7.6.dist-info → hanzo_mcp-0.8.0.dist-info}/WHEEL +0 -0
  177. {hanzo_mcp-0.7.6.dist-info → hanzo_mcp-0.8.0.dist-info}/entry_points.txt +0 -0
  178. {hanzo_mcp-0.7.6.dist-info → hanzo_mcp-0.8.0.dist-info}/top_level.txt +0 -0
@@ -4,23 +4,27 @@ 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 asyncio
8
- import fnmatch
9
- import json
10
7
  import re
11
- import shlex
8
+ import json
12
9
  import shutil
10
+ import asyncio
11
+ import fnmatch
12
+ from typing import (
13
+ List,
14
+ Unpack,
15
+ Optional,
16
+ Annotated,
17
+ TypedDict,
18
+ final,
19
+ override,
20
+ )
13
21
  from pathlib import Path
14
- from typing import Annotated, TypedDict, Unpack, final, override, Optional, List, Dict, Any
15
22
 
16
- from mcp.server.fastmcp import Context as MCPContext
17
23
  from pydantic import Field
24
+ from mcp.server.fastmcp import Context as MCPContext
18
25
 
19
- from hanzo_mcp.tools.common.context import ToolContext
20
- from hanzo_mcp.tools.common.truncate import truncate_response
21
26
  from hanzo_mcp.tools.filesystem.base import FilesystemBaseTool
22
27
 
23
-
24
28
  # Parameter types
25
29
  Pattern = Annotated[
26
30
  str,
@@ -49,7 +53,7 @@ Include = Annotated[
49
53
  Exclude = Annotated[
50
54
  Optional[str],
51
55
  Field(
52
- description='File pattern to exclude',
56
+ description="File pattern to exclude",
53
57
  default=None,
54
58
  ),
55
59
  ]
@@ -89,6 +93,7 @@ Backend = Annotated[
89
93
 
90
94
  class FindParams(TypedDict, total=False):
91
95
  """Parameters for find tool."""
96
+
92
97
  pattern: str
93
98
  path: str
94
99
  include: Optional[str]
@@ -102,7 +107,7 @@ class FindParams(TypedDict, total=False):
102
107
  @final
103
108
  class FindTool(FilesystemBaseTool):
104
109
  """Unified find tool with multiple backend support."""
105
-
110
+
106
111
  def __init__(self, permission_manager):
107
112
  """Initialize the find tool."""
108
113
  super().__init__(permission_manager)
@@ -116,12 +121,12 @@ class FindTool(FilesystemBaseTool):
116
121
  return "find"
117
122
 
118
123
  @property
119
- @override
124
+ @override
120
125
  def description(self) -> str:
121
126
  """Get the tool description."""
122
127
  backends = self._get_available_backends()
123
128
  backend_str = ", ".join(backends) if backends else "fallback grep"
124
-
129
+
125
130
  return f"""Find pattern in files (like ffind). Available: {backend_str}.
126
131
 
127
132
  Usage:
@@ -154,7 +159,7 @@ Fast, intuitive file content search."""
154
159
  pattern = params.get("pattern")
155
160
  if not pattern:
156
161
  return "Error: pattern is required"
157
-
162
+
158
163
  path = params.get("path", ".")
159
164
  include = params.get("include")
160
165
  exclude = params.get("exclude")
@@ -162,7 +167,7 @@ Fast, intuitive file content search."""
162
167
  fixed_strings = params.get("fixed_strings", False)
163
168
  show_context = params.get("show_context", 0)
164
169
  backend = params.get("backend")
165
-
170
+
166
171
  # Validate path
167
172
  path_validation = self.validate_path(path)
168
173
  if path_validation.is_error:
@@ -181,7 +186,7 @@ Fast, intuitive file content search."""
181
186
 
182
187
  # Select backend
183
188
  available = self._get_available_backends()
184
-
189
+
185
190
  if backend:
186
191
  # User specified backend
187
192
  if backend not in available and backend != "grep":
@@ -193,23 +198,71 @@ Fast, intuitive file content search."""
193
198
  else:
194
199
  # Fallback
195
200
  selected_backend = "grep"
196
-
197
- await tool_ctx.info(f"Using {selected_backend} to search for '{pattern}' in {path}")
198
-
201
+
202
+ await tool_ctx.info(
203
+ f"Using {selected_backend} to search for '{pattern}' in {path}"
204
+ )
205
+
199
206
  # Execute search
200
207
  if selected_backend == "rg":
201
- return await self._run_ripgrep(pattern, path, include, exclude, case_sensitive, fixed_strings, show_context, tool_ctx)
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,
217
+ )
202
218
  elif selected_backend == "ag":
203
- return await self._run_silver_searcher(pattern, path, include, exclude, case_sensitive, fixed_strings, show_context, tool_ctx)
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,
228
+ )
204
229
  elif selected_backend == "ack":
205
- return await self._run_ack(pattern, path, include, exclude, case_sensitive, fixed_strings, show_context, tool_ctx)
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,
239
+ )
206
240
  else:
207
- return await self._run_fallback_grep(pattern, path, include, exclude, case_sensitive, fixed_strings, show_context, tool_ctx)
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,
250
+ )
208
251
 
209
- async def _run_ripgrep(self, pattern, path, include, exclude, case_sensitive, fixed_strings, show_context, tool_ctx) -> str:
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,
262
+ ) -> str:
210
263
  """Run ripgrep backend."""
211
264
  cmd = ["rg", "--json"]
212
-
265
+
213
266
  if not case_sensitive:
214
267
  cmd.append("-i")
215
268
  if fixed_strings:
@@ -220,30 +273,40 @@ Fast, intuitive file content search."""
220
273
  cmd.extend(["-g", include])
221
274
  if exclude:
222
275
  cmd.extend(["-g", f"!{exclude}"])
223
-
276
+
224
277
  cmd.extend([pattern, path])
225
-
278
+
226
279
  try:
227
280
  process = await asyncio.create_subprocess_exec(
228
281
  *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
229
282
  )
230
-
283
+
231
284
  stdout, stderr = await process.communicate()
232
-
285
+
233
286
  if process.returncode not in [0, 1]: # 1 = no matches
234
287
  await tool_ctx.error(f"ripgrep failed: {stderr.decode()}")
235
288
  return f"Error: {stderr.decode()}"
236
-
289
+
237
290
  return self._parse_ripgrep_output(stdout.decode())
238
-
291
+
239
292
  except Exception as e:
240
293
  await tool_ctx.error(f"Error running ripgrep: {str(e)}")
241
294
  return f"Error running ripgrep: {str(e)}"
242
295
 
243
- async def _run_silver_searcher(self, pattern, path, include, exclude, case_sensitive, fixed_strings, show_context, tool_ctx) -> str:
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,
306
+ ) -> str:
244
307
  """Run silver searcher (ag) backend."""
245
308
  cmd = ["ag", "--nocolor", "--nogroup"]
246
-
309
+
247
310
  if not case_sensitive:
248
311
  cmd.append("-i")
249
312
  if fixed_strings:
@@ -254,35 +317,45 @@ Fast, intuitive file content search."""
254
317
  cmd.extend(["-G", include])
255
318
  if exclude:
256
319
  cmd.extend(["--ignore", exclude])
257
-
320
+
258
321
  cmd.extend([pattern, path])
259
-
322
+
260
323
  try:
261
324
  process = await asyncio.create_subprocess_exec(
262
325
  *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
263
326
  )
264
-
327
+
265
328
  stdout, stderr = await process.communicate()
266
-
329
+
267
330
  if process.returncode not in [0, 1]:
268
331
  await tool_ctx.error(f"ag failed: {stderr.decode()}")
269
332
  return f"Error: {stderr.decode()}"
270
-
333
+
271
334
  output = stdout.decode()
272
335
  if not output.strip():
273
336
  return "No matches found."
274
-
275
- lines = output.strip().split('\n')
337
+
338
+ lines = output.strip().split("\n")
276
339
  return f"Found {len(lines)} matches:\n\n" + output
277
-
340
+
278
341
  except Exception as e:
279
342
  await tool_ctx.error(f"Error running ag: {str(e)}")
280
343
  return f"Error running ag: {str(e)}"
281
344
 
282
- async def _run_ack(self, pattern, path, include, exclude, case_sensitive, fixed_strings, show_context, tool_ctx) -> str:
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,
355
+ ) -> str:
283
356
  """Run ack backend."""
284
357
  cmd = ["ack", "--nocolor", "--nogroup"]
285
-
358
+
286
359
  if not case_sensitive:
287
360
  cmd.append("-i")
288
361
  if fixed_strings:
@@ -291,40 +364,56 @@ Fast, intuitive file content search."""
291
364
  cmd.extend(["-C", str(show_context)])
292
365
  if include:
293
366
  # ack uses different syntax for file patterns
294
- cmd.extend(["--type-add", f"custom:ext:{include.replace('*.', '')}", "--type=custom"])
295
-
367
+ cmd.extend(
368
+ [
369
+ "--type-add",
370
+ f"custom:ext:{include.replace('*.', '')}",
371
+ "--type=custom",
372
+ ]
373
+ )
374
+
296
375
  cmd.extend([pattern, path])
297
-
376
+
298
377
  try:
299
378
  process = await asyncio.create_subprocess_exec(
300
379
  *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
301
380
  )
302
-
381
+
303
382
  stdout, stderr = await process.communicate()
304
-
383
+
305
384
  if process.returncode not in [0, 1]:
306
385
  await tool_ctx.error(f"ack failed: {stderr.decode()}")
307
386
  return f"Error: {stderr.decode()}"
308
-
387
+
309
388
  output = stdout.decode()
310
389
  if not output.strip():
311
390
  return "No matches found."
312
-
313
- lines = output.strip().split('\n')
391
+
392
+ lines = output.strip().split("\n")
314
393
  return f"Found {len(lines)} matches:\n\n" + output
315
-
394
+
316
395
  except Exception as e:
317
396
  await tool_ctx.error(f"Error running ack: {str(e)}")
318
397
  return f"Error running ack: {str(e)}"
319
398
 
320
- async def _run_fallback_grep(self, pattern, path, include, exclude, case_sensitive, fixed_strings, show_context, tool_ctx) -> str:
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,
409
+ ) -> str:
321
410
  """Fallback Python implementation."""
322
411
  await tool_ctx.info("Using fallback Python grep implementation")
323
-
412
+
324
413
  try:
325
414
  input_path = Path(path)
326
415
  matching_files = []
327
-
416
+
328
417
  # Get files to search
329
418
  if input_path.is_file():
330
419
  if self._match_file_pattern(input_path.name, include, exclude):
@@ -334,64 +423,68 @@ Fast, intuitive file content search."""
334
423
  if entry.is_file() and self.is_path_allowed(str(entry)):
335
424
  if self._match_file_pattern(entry.name, include, exclude):
336
425
  matching_files.append(entry)
337
-
426
+
338
427
  if not matching_files:
339
428
  return "No matching files found."
340
-
429
+
341
430
  # Compile pattern
342
431
  if fixed_strings:
343
432
  pattern_re = re.escape(pattern)
344
433
  else:
345
434
  pattern_re = pattern
346
-
435
+
347
436
  if not case_sensitive:
348
437
  flags = re.IGNORECASE
349
438
  else:
350
439
  flags = 0
351
-
440
+
352
441
  regex = re.compile(pattern_re, flags)
353
-
442
+
354
443
  # Search files
355
444
  results = []
356
445
  total_matches = 0
357
-
446
+
358
447
  for file_path in matching_files:
359
448
  try:
360
449
  with open(file_path, "r", encoding="utf-8") as f:
361
450
  lines = f.readlines()
362
-
451
+
363
452
  for i, line in enumerate(lines, 1):
364
453
  if regex.search(line):
365
454
  # Format result with context if requested
366
455
  if show_context > 0:
367
456
  start = max(0, i - show_context - 1)
368
457
  end = min(len(lines), i + show_context)
369
-
458
+
370
459
  context_lines = []
371
460
  for j in range(start, end):
372
461
  prefix = ":" if j + 1 == i else "-"
373
- context_lines.append(f"{file_path}:{j+1}{prefix}{lines[j].rstrip()}")
462
+ context_lines.append(
463
+ f"{file_path}:{j + 1}{prefix}{lines[j].rstrip()}"
464
+ )
374
465
  results.extend(context_lines)
375
466
  results.append("") # Separator
376
467
  else:
377
468
  results.append(f"{file_path}:{i}:{line.rstrip()}")
378
469
  total_matches += 1
379
-
470
+
380
471
  except UnicodeDecodeError:
381
472
  pass # Skip binary files
382
473
  except Exception as e:
383
474
  await tool_ctx.warning(f"Error reading {file_path}: {str(e)}")
384
-
475
+
385
476
  if not results:
386
477
  return "No matches found."
387
-
478
+
388
479
  return f"Found {total_matches} matches:\n\n" + "\n".join(results)
389
-
480
+
390
481
  except Exception as e:
391
482
  await tool_ctx.error(f"Error in fallback grep: {str(e)}")
392
483
  return f"Error in fallback grep: {str(e)}"
393
484
 
394
- def _match_file_pattern(self, filename: str, include: Optional[str], exclude: Optional[str]) -> bool:
485
+ def _match_file_pattern(
486
+ self, filename: str, include: Optional[str], exclude: Optional[str]
487
+ ) -> bool:
395
488
  """Check if filename matches include/exclude patterns."""
396
489
  if include and not fnmatch.fnmatch(filename, include):
397
490
  return False
@@ -403,42 +496,42 @@ Fast, intuitive file content search."""
403
496
  """Parse ripgrep JSON output."""
404
497
  if not output.strip():
405
498
  return "No matches found."
406
-
499
+
407
500
  results = []
408
501
  total_matches = 0
409
-
502
+
410
503
  for line in output.splitlines():
411
504
  if not line.strip():
412
505
  continue
413
-
506
+
414
507
  try:
415
508
  data = json.loads(line)
416
-
509
+
417
510
  if data.get("type") == "match":
418
511
  match_data = data.get("data", {})
419
512
  path = match_data.get("path", {}).get("text", "")
420
513
  line_number = match_data.get("line_number", 0)
421
514
  line_text = match_data.get("lines", {}).get("text", "").rstrip()
422
-
515
+
423
516
  results.append(f"{path}:{line_number}:{line_text}")
424
517
  total_matches += 1
425
-
518
+
426
519
  elif data.get("type") == "context":
427
520
  context_data = data.get("data", {})
428
521
  path = context_data.get("path", {}).get("text", "")
429
522
  line_number = context_data.get("line_number", 0)
430
523
  line_text = context_data.get("lines", {}).get("text", "").rstrip()
431
-
524
+
432
525
  results.append(f"{path}:{line_number}-{line_text}")
433
-
526
+
434
527
  except json.JSONDecodeError:
435
528
  pass
436
-
529
+
437
530
  if not results:
438
531
  return "No matches found."
439
-
532
+
440
533
  return f"Found {total_matches} matches:\n\n" + "\n".join(results)
441
534
 
442
535
  def register(self, mcp_server) -> None:
443
536
  """Register this tool with the MCP server."""
444
- pass
537
+ pass