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
@@ -1,18 +1,17 @@
1
1
  """Git search tool for searching through git history."""
2
2
 
3
3
  import os
4
- import subprocess
5
4
  import re
6
- from typing import Annotated, TypedDict, Unpack, final, override
5
+ import subprocess
6
+ from typing import Unpack, Annotated, TypedDict, final, override
7
7
 
8
- from mcp.server.fastmcp import Context as MCPContext
9
8
  from pydantic import Field
9
+ from mcp.server.fastmcp import Context as MCPContext
10
10
 
11
11
  from hanzo_mcp.tools.common.base import BaseTool
12
12
  from hanzo_mcp.tools.common.context import create_tool_context
13
13
  from hanzo_mcp.tools.common.permissions import PermissionManager
14
14
 
15
-
16
15
  Pattern = Annotated[
17
16
  str,
18
17
  Field(
@@ -188,7 +187,7 @@ Examples:
188
187
 
189
188
  # Resolve absolute path
190
189
  abs_path = os.path.abspath(path)
191
-
190
+
192
191
  # Check permissions
193
192
  if not self.permission_manager.has_permission(abs_path):
194
193
  return f"Permission denied: {abs_path}"
@@ -210,23 +209,51 @@ Examples:
210
209
  try:
211
210
  if search_type == "content":
212
211
  return await self._search_content(
213
- abs_path, pattern, case_sensitive, max_count,
214
- branch, file_pattern, tool_ctx
212
+ abs_path,
213
+ pattern,
214
+ case_sensitive,
215
+ max_count,
216
+ branch,
217
+ file_pattern,
218
+ tool_ctx,
215
219
  )
216
220
  elif search_type == "commits":
217
221
  return await self._search_commits(
218
- abs_path, pattern, case_sensitive, max_count,
219
- branch, author, since, until, file_pattern, tool_ctx
222
+ abs_path,
223
+ pattern,
224
+ case_sensitive,
225
+ max_count,
226
+ branch,
227
+ author,
228
+ since,
229
+ until,
230
+ file_pattern,
231
+ tool_ctx,
220
232
  )
221
233
  elif search_type == "diff":
222
234
  return await self._search_diff(
223
- abs_path, pattern, case_sensitive, max_count,
224
- branch, author, since, until, file_pattern, tool_ctx
235
+ abs_path,
236
+ pattern,
237
+ case_sensitive,
238
+ max_count,
239
+ branch,
240
+ author,
241
+ since,
242
+ until,
243
+ file_pattern,
244
+ tool_ctx,
225
245
  )
226
246
  elif search_type == "log":
227
247
  return await self._search_log(
228
- abs_path, pattern, max_count, branch,
229
- author, since, until, file_pattern, tool_ctx
248
+ abs_path,
249
+ pattern,
250
+ max_count,
251
+ branch,
252
+ author,
253
+ since,
254
+ until,
255
+ file_pattern,
256
+ tool_ctx,
230
257
  )
231
258
  elif search_type == "blame":
232
259
  return await self._search_blame(
@@ -243,32 +270,35 @@ Examples:
243
270
  return f"Error: {str(e)}"
244
271
 
245
272
  async def _search_content(
246
- self, repo_path: str, pattern: str, case_sensitive: bool,
247
- max_count: int, branch: str | None, file_pattern: str | None,
248
- tool_ctx
273
+ self,
274
+ repo_path: str,
275
+ pattern: str,
276
+ case_sensitive: bool,
277
+ max_count: int,
278
+ branch: str | None,
279
+ file_pattern: str | None,
280
+ tool_ctx,
249
281
  ) -> str:
250
282
  """Search file contents in git history."""
251
283
  cmd = ["git", "grep", "-n", f"--max-count={max_count}"]
252
-
284
+
253
285
  if not case_sensitive:
254
286
  cmd.append("-i")
255
-
287
+
256
288
  if branch:
257
289
  cmd.append(branch)
258
290
  else:
259
291
  cmd.append("--all") # Search all branches
260
-
292
+
261
293
  cmd.append(pattern)
262
-
294
+
263
295
  if file_pattern:
264
296
  cmd.extend(["--", file_pattern])
265
-
266
- result = subprocess.run(
267
- cmd, cwd=repo_path, capture_output=True, text=True
268
- )
269
-
297
+
298
+ result = subprocess.run(cmd, cwd=repo_path, capture_output=True, text=True)
299
+
270
300
  if result.returncode == 0:
271
- lines = result.stdout.strip().split('\n')
301
+ lines = result.stdout.strip().split("\n")
272
302
  if lines and lines[0]:
273
303
  await tool_ctx.info(f"Found {len(lines)} matches")
274
304
  return self._format_grep_results(lines, pattern)
@@ -282,43 +312,51 @@ Examples:
282
312
  )
283
313
 
284
314
  async def _search_commits(
285
- self, repo_path: str, pattern: str, case_sensitive: bool,
286
- max_count: int, branch: str | None, author: str | None,
287
- since: str | None, until: str | None, file_pattern: str | None,
288
- tool_ctx
315
+ self,
316
+ repo_path: str,
317
+ pattern: str,
318
+ case_sensitive: bool,
319
+ max_count: int,
320
+ branch: str | None,
321
+ author: str | None,
322
+ since: str | None,
323
+ until: str | None,
324
+ file_pattern: str | None,
325
+ tool_ctx,
289
326
  ) -> str:
290
327
  """Search commit messages."""
291
328
  cmd = ["git", "log", f"--max-count={max_count}", "--oneline"]
292
-
329
+
293
330
  grep_flag = "--grep" if case_sensitive else "--grep-ignore-case"
294
331
  cmd.extend([grep_flag, pattern])
295
-
332
+
296
333
  if branch:
297
334
  cmd.append(branch)
298
335
  else:
299
336
  cmd.append("--all")
300
-
337
+
301
338
  if author:
302
339
  cmd.extend(["--author", author])
303
-
340
+
304
341
  if since:
305
342
  cmd.extend(["--since", since])
306
-
343
+
307
344
  if until:
308
345
  cmd.extend(["--until", until])
309
-
346
+
310
347
  if file_pattern:
311
348
  cmd.extend(["--", file_pattern])
312
-
313
- result = subprocess.run(
314
- cmd, cwd=repo_path, capture_output=True, text=True
315
- )
316
-
349
+
350
+ result = subprocess.run(cmd, cwd=repo_path, capture_output=True, text=True)
351
+
317
352
  if result.returncode == 0:
318
- lines = result.stdout.strip().split('\n')
353
+ lines = result.stdout.strip().split("\n")
319
354
  if lines and lines[0]:
320
355
  await tool_ctx.info(f"Found {len(lines)} commits")
321
- return f"Found {len(lines)} commits matching '{pattern}':\n\n" + result.stdout
356
+ return (
357
+ f"Found {len(lines)} commits matching '{pattern}':\n\n"
358
+ + result.stdout
359
+ )
322
360
  else:
323
361
  return f"No commits found matching: {pattern}"
324
362
  else:
@@ -327,51 +365,59 @@ Examples:
327
365
  )
328
366
 
329
367
  async def _search_diff(
330
- self, repo_path: str, pattern: str, case_sensitive: bool,
331
- max_count: int, branch: str | None, author: str | None,
332
- since: str | None, until: str | None, file_pattern: str | None,
333
- tool_ctx
368
+ self,
369
+ repo_path: str,
370
+ pattern: str,
371
+ case_sensitive: bool,
372
+ max_count: int,
373
+ branch: str | None,
374
+ author: str | None,
375
+ since: str | None,
376
+ until: str | None,
377
+ file_pattern: str | None,
378
+ tool_ctx,
334
379
  ) -> str:
335
380
  """Search for pattern in diffs (when code was added/removed)."""
336
381
  cmd = ["git", "log", f"--max-count={max_count}", "-p"]
337
-
382
+
338
383
  # Use -G for diff search (shows commits that added/removed pattern)
339
384
  search_flag = f"-G{pattern}"
340
385
  if not case_sensitive:
341
386
  # For case-insensitive, we need to use -G with regex
342
387
  import re
388
+
343
389
  case_insensitive_pattern = "".join(
344
390
  f"[{c.upper()}{c.lower()}]" if c.isalpha() else re.escape(c)
345
391
  for c in pattern
346
392
  )
347
393
  search_flag = f"-G{case_insensitive_pattern}"
348
-
394
+
349
395
  cmd.append(search_flag)
350
-
396
+
351
397
  if branch:
352
398
  cmd.append(branch)
353
399
  else:
354
400
  cmd.append("--all")
355
-
401
+
356
402
  if author:
357
403
  cmd.extend(["--author", author])
358
-
404
+
359
405
  if since:
360
406
  cmd.extend(["--since", since])
361
-
407
+
362
408
  if until:
363
409
  cmd.extend(["--until", until])
364
-
410
+
365
411
  if file_pattern:
366
412
  cmd.extend(["--", file_pattern])
367
-
368
- result = subprocess.run(
369
- cmd, cwd=repo_path, capture_output=True, text=True
370
- )
371
-
413
+
414
+ result = subprocess.run(cmd, cwd=repo_path, capture_output=True, text=True)
415
+
372
416
  if result.returncode == 0 and result.stdout.strip():
373
417
  # Parse and highlight matching lines
374
- output = self._highlight_diff_matches(result.stdout, pattern, case_sensitive)
418
+ output = self._highlight_diff_matches(
419
+ result.stdout, pattern, case_sensitive
420
+ )
375
421
  matches = output.count("commit ")
376
422
  await tool_ctx.info(f"Found {matches} commits with changes")
377
423
  return f"Found {matches} commits with changes matching '{pattern}':\n\n{output}"
@@ -379,83 +425,90 @@ Examples:
379
425
  return f"No changes found matching: {pattern}"
380
426
 
381
427
  async def _search_log(
382
- self, repo_path: str, pattern: str | None, max_count: int,
383
- branch: str | None, author: str | None, since: str | None,
384
- until: str | None, file_pattern: str | None, tool_ctx
428
+ self,
429
+ repo_path: str,
430
+ pattern: str | None,
431
+ max_count: int,
432
+ branch: str | None,
433
+ author: str | None,
434
+ since: str | None,
435
+ until: str | None,
436
+ file_pattern: str | None,
437
+ tool_ctx,
385
438
  ) -> str:
386
439
  """Search git log with filters."""
387
440
  cmd = ["git", "log", f"--max-count={max_count}", "--oneline"]
388
-
441
+
389
442
  if pattern:
390
443
  # Search in commit message and changes
391
444
  cmd.extend(["--grep", pattern, f"-G{pattern}"])
392
-
445
+
393
446
  if branch:
394
447
  cmd.append(branch)
395
448
  else:
396
449
  cmd.append("--all")
397
-
450
+
398
451
  if author:
399
452
  cmd.extend(["--author", author])
400
-
453
+
401
454
  if since:
402
455
  cmd.extend(["--since", since])
403
-
456
+
404
457
  if until:
405
458
  cmd.extend(["--until", until])
406
-
459
+
407
460
  if file_pattern:
408
461
  cmd.extend(["--", file_pattern])
409
-
410
- result = subprocess.run(
411
- cmd, cwd=repo_path, capture_output=True, text=True
412
- )
413
-
462
+
463
+ result = subprocess.run(cmd, cwd=repo_path, capture_output=True, text=True)
464
+
414
465
  if result.returncode == 0 and result.stdout.strip():
415
- lines = result.stdout.strip().split('\n')
466
+ lines = result.stdout.strip().split("\n")
416
467
  await tool_ctx.info(f"Found {len(lines)} commits")
417
468
  return f"Found {len(lines)} commits:\n\n" + result.stdout
418
469
  else:
419
470
  return "No commits found matching criteria"
420
471
 
421
472
  async def _search_blame(
422
- self, repo_path: str, pattern: str, case_sensitive: bool,
423
- file_pattern: str | None, tool_ctx
473
+ self,
474
+ repo_path: str,
475
+ pattern: str,
476
+ case_sensitive: bool,
477
+ file_pattern: str | None,
478
+ tool_ctx,
424
479
  ) -> str:
425
480
  """Search using git blame to find who changed lines."""
426
481
  if not file_pattern:
427
482
  return "Error: file_pattern is required for blame search"
428
-
483
+
429
484
  # First, find files matching the pattern
430
485
  cmd = ["git", "ls-files", file_pattern]
431
- result = subprocess.run(
432
- cmd, cwd=repo_path, capture_output=True, text=True
433
- )
434
-
486
+ result = subprocess.run(cmd, cwd=repo_path, capture_output=True, text=True)
487
+
435
488
  if result.returncode != 0 or not result.stdout.strip():
436
489
  return f"No files found matching: {file_pattern}"
437
-
438
- files = result.stdout.strip().split('\n')
490
+
491
+ files = result.stdout.strip().split("\n")
439
492
  all_matches = []
440
-
493
+
441
494
  for file_path in files[:10]: # Limit to 10 files
442
495
  # Get blame for the file
443
496
  cmd = ["git", "blame", "-l", file_path]
444
- result = subprocess.run(
445
- cmd, cwd=repo_path, capture_output=True, text=True
446
- )
447
-
497
+ result = subprocess.run(cmd, cwd=repo_path, capture_output=True, text=True)
498
+
448
499
  if result.returncode == 0:
449
500
  # Search for pattern in blame output
450
501
  flags = 0 if case_sensitive else re.IGNORECASE
451
- for line in result.stdout.split('\n'):
502
+ for line in result.stdout.split("\n"):
452
503
  if re.search(pattern, line, flags):
453
504
  all_matches.append(f"{file_path}: {line}")
454
-
505
+
455
506
  if all_matches:
456
507
  await tool_ctx.info(f"Found {len(all_matches)} matching lines")
457
- return f"Found {len(all_matches)} lines matching '{pattern}':\n\n" + \
458
- "\n".join(all_matches[:50]) # Limit output
508
+ return (
509
+ f"Found {len(all_matches)} lines matching '{pattern}':\n\n"
510
+ + "\n".join(all_matches[:50])
511
+ ) # Limit output
459
512
  else:
460
513
  return f"No lines found matching: {pattern}"
461
514
 
@@ -463,43 +516,43 @@ Examples:
463
516
  """Format git grep results nicely."""
464
517
  output = []
465
518
  current_ref = None
466
-
519
+
467
520
  for line in lines:
468
- if ':' in line:
469
- parts = line.split(':', 3)
521
+ if ":" in line:
522
+ parts = line.split(":", 3)
470
523
  if len(parts) >= 3:
471
524
  ref = parts[0]
472
525
  file_path = parts[1]
473
526
  line_num = parts[2]
474
527
  content = parts[3] if len(parts) > 3 else ""
475
-
528
+
476
529
  if ref != current_ref:
477
530
  current_ref = ref
478
531
  output.append(f"\n=== {ref} ===")
479
-
532
+
480
533
  output.append(f"{file_path}:{line_num}: {content}")
481
-
534
+
482
535
  return f"Found matches for '{pattern}':\n" + "\n".join(output)
483
536
 
484
537
  def _highlight_diff_matches(
485
538
  self, diff_output: str, pattern: str, case_sensitive: bool
486
539
  ) -> str:
487
540
  """Highlight matching lines in diff output."""
488
- lines = diff_output.split('\n')
541
+ lines = diff_output.split("\n")
489
542
  output = []
490
543
  flags = 0 if case_sensitive else re.IGNORECASE
491
-
544
+
492
545
  for line in lines:
493
- if line.startswith(('+', '-')) and not line.startswith(('+++', '---')):
546
+ if line.startswith(("+", "-")) and not line.startswith(("+++", "---")):
494
547
  if re.search(pattern, line[1:], flags):
495
548
  output.append(f">>> {line}") # Highlight matching lines
496
549
  else:
497
550
  output.append(line)
498
551
  else:
499
552
  output.append(line)
500
-
553
+
501
554
  return "\n".join(output)
502
555
 
503
556
  def register(self, mcp_server) -> None:
504
557
  """Register this tool with the MCP server."""
505
- pass
558
+ pass
@@ -3,18 +3,18 @@
3
3
  This module provides the Grep tool for finding text patterns in files using ripgrep.
4
4
  """
5
5
 
6
- import asyncio
7
- import fnmatch
8
- import json
9
6
  import re
7
+ import json
10
8
  import shlex
11
9
  import shutil
10
+ import asyncio
11
+ import fnmatch
12
+ from typing import Unpack, Annotated, TypedDict, final, override
12
13
  from pathlib import Path
13
- from typing import Annotated, TypedDict, Unpack, final, override
14
14
 
15
- from mcp.server.fastmcp import Context as MCPContext
16
- from mcp.server import FastMCP
17
15
  from pydantic import Field
16
+ from mcp.server import FastMCP
17
+ from mcp.server.fastmcp import Context as MCPContext
18
18
 
19
19
  from hanzo_mcp.tools.common.context import ToolContext
20
20
  from hanzo_mcp.tools.common.truncate import truncate_response
@@ -426,7 +426,7 @@ When you are doing an open ended search that may require multiple rounds of glob
426
426
  return truncate_response(
427
427
  result,
428
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.]"
429
+ truncation_message="\n\n[Grep results truncated due to token limit. Use more specific patterns or paths to reduce output.]",
430
430
  )
431
431
  else:
432
432
  await tool_ctx.info(
@@ -436,7 +436,7 @@ When you are doing an open ended search that may require multiple rounds of glob
436
436
  return truncate_response(
437
437
  result,
438
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.]"
439
+ truncation_message="\n\n[Grep results truncated due to token limit. Use more specific patterns or paths to reduce output.]",
440
440
  )
441
441
  except Exception as e:
442
442
  await tool_ctx.error(f"Error in grep tool: {str(e)}")
@@ -3,13 +3,13 @@
3
3
  This module provides the MultiEdit tool for making multiple precise text replacements in files.
4
4
  """
5
5
 
6
+ from typing import Unpack, Annotated, TypedDict, final, override
6
7
  from difflib import unified_diff
7
8
  from pathlib import Path
8
- from typing import Annotated, TypedDict, Unpack, final, override
9
9
 
10
- from mcp.server.fastmcp import Context as MCPContext
11
- from mcp.server import FastMCP
12
10
  from pydantic import Field
11
+ from mcp.server import FastMCP
12
+ from mcp.server.fastmcp import Context as MCPContext
13
13
 
14
14
  from hanzo_mcp.tools.filesystem.base import FilesystemBaseTool
15
15
 
@@ -348,11 +348,7 @@ If you want to create a new file, use:
348
348
  tool_self = self # Create a reference to self for use in the closure
349
349
 
350
350
  @mcp_server.tool(name=self.name, description=self.description)
351
- async def multi_edit(
352
- file_path: FilePath,
353
- edits: Edits,
354
- ctx: MCPContext
355
- ) -> str:
351
+ async def multi_edit(file_path: FilePath, edits: Edits, ctx: MCPContext) -> str:
356
352
  return await tool_self.call(
357
353
  ctx,
358
354
  file_path=file_path,
@@ -3,15 +3,15 @@
3
3
  This module provides the ReadTool for reading the contents of files.
4
4
  """
5
5
 
6
+ from typing import Unpack, Annotated, TypedDict, final, override
6
7
  from pathlib import Path
7
- from typing import Annotated, TypedDict, Unpack, final, override
8
8
 
9
- from mcp.server.fastmcp import Context as MCPContext
10
- from mcp.server import FastMCP
11
9
  from pydantic import Field
10
+ from mcp.server import FastMCP
11
+ from mcp.server.fastmcp import Context as MCPContext
12
12
 
13
- from hanzo_mcp.tools.filesystem.base import FilesystemBaseTool
14
13
  from hanzo_mcp.tools.common.truncate import truncate_response
14
+ from hanzo_mcp.tools.filesystem.base import FilesystemBaseTool
15
15
 
16
16
  FilePath = Annotated[
17
17
  str,
@@ -220,12 +220,12 @@ Usage:
220
220
  result += f"\n... (output truncated, showing {limit} of {limit + truncated_lines}+ lines)"
221
221
 
222
222
  await tool_ctx.info(f"Successfully read file: {file_path}")
223
-
223
+
224
224
  # Apply token limit to prevent excessive output
225
225
  return truncate_response(
226
226
  result,
227
227
  max_tokens=25000,
228
- truncation_message="\n\n[File content truncated due to token limit. Use offset/limit parameters to read specific sections.]"
228
+ truncation_message="\n\n[File content truncated due to token limit. Use offset/limit parameters to read specific sections.]",
229
229
  )
230
230
 
231
231
  except Exception as e:
@@ -236,15 +236,17 @@ Usage:
236
236
  await tool_ctx.error(f"Error reading file: {str(e)}")
237
237
  return f"Error: {str(e)}"
238
238
 
239
- async def run(self, ctx: MCPContext, file_path: str, offset: int = 0, limit: int = 2000) -> str:
239
+ async def run(
240
+ self, ctx: MCPContext, file_path: str, offset: int = 0, limit: int = 2000
241
+ ) -> str:
240
242
  """Run method for backwards compatibility with test scripts.
241
-
243
+
242
244
  Args:
243
245
  ctx: MCP context
244
246
  file_path: Path to file to read
245
247
  offset: Line offset to start reading
246
248
  limit: Maximum lines to read
247
-
249
+
248
250
  Returns:
249
251
  File contents
250
252
  """
@@ -267,7 +269,7 @@ Usage:
267
269
  ctx: MCPContext,
268
270
  file_path: FilePath,
269
271
  offset: Offset = 0,
270
- limit: Limit = 2000
272
+ limit: Limit = 2000,
271
273
  ) -> str:
272
274
  return await tool_self.call(
273
275
  ctx, file_path=file_path, offset=offset, limit=limit