hanzo-mcp 0.8.8__py3-none-any.whl → 0.8.13__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 (154) hide show
  1. hanzo_mcp/__init__.py +1 -3
  2. hanzo_mcp/analytics/posthog_analytics.py +4 -17
  3. hanzo_mcp/bridge.py +9 -25
  4. hanzo_mcp/cli.py +8 -17
  5. hanzo_mcp/cli_enhanced.py +5 -14
  6. hanzo_mcp/cli_plugin.py +3 -9
  7. hanzo_mcp/config/settings.py +6 -20
  8. hanzo_mcp/config/tool_config.py +2 -4
  9. hanzo_mcp/core/base_agent.py +88 -88
  10. hanzo_mcp/core/model_registry.py +238 -210
  11. hanzo_mcp/dev_server.py +5 -15
  12. hanzo_mcp/prompts/__init__.py +2 -6
  13. hanzo_mcp/prompts/project_todo_reminder.py +3 -9
  14. hanzo_mcp/prompts/tool_explorer.py +1 -3
  15. hanzo_mcp/prompts/utils.py +7 -21
  16. hanzo_mcp/server.py +6 -7
  17. hanzo_mcp/tools/__init__.py +13 -29
  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 +6 -17
  21. hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +15 -42
  22. hanzo_mcp/tools/agent/claude_desktop_auth.py +3 -9
  23. hanzo_mcp/tools/agent/cli_agent_base.py +7 -24
  24. hanzo_mcp/tools/agent/cli_tools.py +76 -75
  25. hanzo_mcp/tools/agent/code_auth.py +1 -3
  26. hanzo_mcp/tools/agent/code_auth_tool.py +2 -6
  27. hanzo_mcp/tools/agent/critic_tool.py +8 -24
  28. hanzo_mcp/tools/agent/iching_tool.py +12 -36
  29. hanzo_mcp/tools/agent/network_tool.py +7 -18
  30. hanzo_mcp/tools/agent/prompt.py +1 -5
  31. hanzo_mcp/tools/agent/review_tool.py +10 -25
  32. hanzo_mcp/tools/agent/swarm_alias.py +1 -3
  33. hanzo_mcp/tools/agent/swarm_tool.py +16 -41
  34. hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +11 -39
  35. hanzo_mcp/tools/agent/unified_cli_tools.py +38 -38
  36. hanzo_mcp/tools/common/batch_tool.py +15 -45
  37. hanzo_mcp/tools/common/config_tool.py +9 -28
  38. hanzo_mcp/tools/common/context.py +1 -3
  39. hanzo_mcp/tools/common/critic_tool.py +1 -3
  40. hanzo_mcp/tools/common/decorators.py +2 -6
  41. hanzo_mcp/tools/common/enhanced_base.py +2 -6
  42. hanzo_mcp/tools/common/fastmcp_pagination.py +4 -12
  43. hanzo_mcp/tools/common/forgiving_edit.py +9 -28
  44. hanzo_mcp/tools/common/mode.py +1 -5
  45. hanzo_mcp/tools/common/paginated_base.py +3 -11
  46. hanzo_mcp/tools/common/paginated_response.py +10 -30
  47. hanzo_mcp/tools/common/pagination.py +3 -9
  48. hanzo_mcp/tools/common/permissions.py +3 -9
  49. hanzo_mcp/tools/common/personality.py +9 -34
  50. hanzo_mcp/tools/common/plugin_loader.py +3 -15
  51. hanzo_mcp/tools/common/stats.py +7 -19
  52. hanzo_mcp/tools/common/thinking_tool.py +1 -3
  53. hanzo_mcp/tools/common/tool_disable.py +2 -6
  54. hanzo_mcp/tools/common/tool_list.py +2 -6
  55. hanzo_mcp/tools/common/validation.py +1 -3
  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/filesystem/__init__.py +2 -3
  73. hanzo_mcp/tools/filesystem/ast_multi_edit.py +14 -43
  74. hanzo_mcp/tools/filesystem/base.py +4 -12
  75. hanzo_mcp/tools/filesystem/batch_search.py +35 -115
  76. hanzo_mcp/tools/filesystem/content_replace.py +4 -12
  77. hanzo_mcp/tools/filesystem/diff.py +2 -10
  78. hanzo_mcp/tools/filesystem/directory_tree.py +9 -27
  79. hanzo_mcp/tools/filesystem/directory_tree_paginated.py +5 -15
  80. hanzo_mcp/tools/filesystem/edit.py +6 -18
  81. hanzo_mcp/tools/filesystem/find.py +3 -9
  82. hanzo_mcp/tools/filesystem/find_files.py +2 -6
  83. hanzo_mcp/tools/filesystem/git_search.py +9 -24
  84. hanzo_mcp/tools/filesystem/grep.py +9 -27
  85. hanzo_mcp/tools/filesystem/multi_edit.py +6 -18
  86. hanzo_mcp/tools/filesystem/read.py +8 -26
  87. hanzo_mcp/tools/filesystem/rules_tool.py +6 -17
  88. hanzo_mcp/tools/filesystem/search_tool.py +18 -62
  89. hanzo_mcp/tools/filesystem/symbols_tool.py +5 -15
  90. hanzo_mcp/tools/filesystem/tree.py +1 -3
  91. hanzo_mcp/tools/filesystem/watch.py +1 -3
  92. hanzo_mcp/tools/filesystem/write.py +1 -3
  93. hanzo_mcp/tools/jupyter/base.py +6 -20
  94. hanzo_mcp/tools/jupyter/jupyter.py +4 -12
  95. hanzo_mcp/tools/jupyter/notebook_edit.py +11 -35
  96. hanzo_mcp/tools/jupyter/notebook_read.py +2 -6
  97. hanzo_mcp/tools/llm/consensus_tool.py +8 -24
  98. hanzo_mcp/tools/llm/llm_manage.py +2 -6
  99. hanzo_mcp/tools/llm/llm_tool.py +17 -58
  100. hanzo_mcp/tools/llm/llm_unified.py +18 -59
  101. hanzo_mcp/tools/llm/provider_tools.py +1 -3
  102. hanzo_mcp/tools/lsp/lsp_tool.py +5 -17
  103. hanzo_mcp/tools/mcp/mcp_add.py +3 -5
  104. hanzo_mcp/tools/mcp/mcp_remove.py +1 -1
  105. hanzo_mcp/tools/mcp/mcp_stats.py +1 -3
  106. hanzo_mcp/tools/mcp/mcp_tool.py +9 -23
  107. hanzo_mcp/tools/memory/__init__.py +33 -40
  108. hanzo_mcp/tools/memory/knowledge_tools.py +7 -25
  109. hanzo_mcp/tools/memory/memory_tools.py +7 -19
  110. hanzo_mcp/tools/search/find_tool.py +10 -32
  111. hanzo_mcp/tools/search/unified_search.py +27 -81
  112. hanzo_mcp/tools/shell/__init__.py +2 -2
  113. hanzo_mcp/tools/shell/auto_background.py +2 -6
  114. hanzo_mcp/tools/shell/base.py +1 -5
  115. hanzo_mcp/tools/shell/base_process.py +5 -7
  116. hanzo_mcp/tools/shell/bash_session.py +7 -24
  117. hanzo_mcp/tools/shell/bash_session_executor.py +5 -15
  118. hanzo_mcp/tools/shell/bash_tool.py +3 -7
  119. hanzo_mcp/tools/shell/command_executor.py +26 -79
  120. hanzo_mcp/tools/shell/logs.py +4 -16
  121. hanzo_mcp/tools/shell/npx.py +2 -8
  122. hanzo_mcp/tools/shell/npx_tool.py +1 -3
  123. hanzo_mcp/tools/shell/pkill.py +4 -12
  124. hanzo_mcp/tools/shell/process_tool.py +2 -8
  125. hanzo_mcp/tools/shell/processes.py +5 -17
  126. hanzo_mcp/tools/shell/run_background.py +1 -3
  127. hanzo_mcp/tools/shell/run_command.py +1 -3
  128. hanzo_mcp/tools/shell/run_command_windows.py +1 -3
  129. hanzo_mcp/tools/shell/session_manager.py +2 -6
  130. hanzo_mcp/tools/shell/session_storage.py +2 -6
  131. hanzo_mcp/tools/shell/streaming_command.py +7 -23
  132. hanzo_mcp/tools/shell/uvx.py +4 -14
  133. hanzo_mcp/tools/shell/uvx_background.py +2 -6
  134. hanzo_mcp/tools/shell/uvx_tool.py +1 -3
  135. hanzo_mcp/tools/shell/zsh_tool.py +12 -20
  136. hanzo_mcp/tools/todo/todo.py +1 -3
  137. hanzo_mcp/tools/todo/todo_read.py +3 -9
  138. hanzo_mcp/tools/todo/todo_write.py +6 -18
  139. hanzo_mcp/tools/vector/__init__.py +3 -9
  140. hanzo_mcp/tools/vector/ast_analyzer.py +6 -20
  141. hanzo_mcp/tools/vector/git_ingester.py +10 -30
  142. hanzo_mcp/tools/vector/index_tool.py +3 -9
  143. hanzo_mcp/tools/vector/infinity_store.py +11 -30
  144. hanzo_mcp/tools/vector/mock_infinity.py +159 -0
  145. hanzo_mcp/tools/vector/project_manager.py +4 -12
  146. hanzo_mcp/tools/vector/vector.py +2 -6
  147. hanzo_mcp/tools/vector/vector_index.py +8 -8
  148. hanzo_mcp/tools/vector/vector_search.py +7 -21
  149. {hanzo_mcp-0.8.8.dist-info → hanzo_mcp-0.8.13.dist-info}/METADATA +2 -2
  150. hanzo_mcp-0.8.13.dist-info/RECORD +193 -0
  151. hanzo_mcp-0.8.8.dist-info/RECORD +0 -192
  152. {hanzo_mcp-0.8.8.dist-info → hanzo_mcp-0.8.13.dist-info}/WHEEL +0 -0
  153. {hanzo_mcp-0.8.8.dist-info → hanzo_mcp-0.8.13.dist-info}/entry_points.txt +0 -0
  154. {hanzo_mcp-0.8.8.dist-info → hanzo_mcp-0.8.13.dist-info}/top_level.txt +0 -0
@@ -335,9 +335,7 @@ For database search, use 'sql_search' or 'vector_search'.
335
335
  return f"No files found matching '{pattern}' (using fallback search)"
336
336
 
337
337
  # Format output
338
- output = [
339
- f"Found {len(results)} file(s) matching '{pattern}' (using fallback search):"
340
- ]
338
+ output = [f"Found {len(results)} file(s) matching '{pattern}' (using fallback search):"]
341
339
  output.append("")
342
340
 
343
341
  for filepath in sorted(results):
@@ -346,9 +344,7 @@ For database search, use 'sql_search' or 'vector_search'.
346
344
  if count >= max_results:
347
345
  output.append(f"\n... (showing first {max_results} results)")
348
346
 
349
- output.append(
350
- "\nNote: Install 'ffind' for faster searching: pip install ffind"
351
- )
347
+ output.append("\nNote: Install 'ffind' for faster searching: pip install ffind")
352
348
 
353
349
  return "\n".join(output)
354
350
 
@@ -256,9 +256,7 @@ Examples:
256
256
  tool_ctx,
257
257
  )
258
258
  elif search_type == "blame":
259
- return await self._search_blame(
260
- abs_path, pattern, case_sensitive, file_pattern, tool_ctx
261
- )
259
+ return await self._search_blame(abs_path, pattern, case_sensitive, file_pattern, tool_ctx)
262
260
  else:
263
261
  return f"Unknown search type: {search_type}"
264
262
 
@@ -307,9 +305,7 @@ Examples:
307
305
  elif result.returncode == 1:
308
306
  return f"No matches found for pattern: {pattern}"
309
307
  else:
310
- raise subprocess.CalledProcessError(
311
- result.returncode, cmd, result.stdout, result.stderr
312
- )
308
+ raise subprocess.CalledProcessError(result.returncode, cmd, result.stdout, result.stderr)
313
309
 
314
310
  async def _search_commits(
315
311
  self,
@@ -353,16 +349,11 @@ Examples:
353
349
  lines = result.stdout.strip().split("\n")
354
350
  if lines and lines[0]:
355
351
  await tool_ctx.info(f"Found {len(lines)} commits")
356
- return (
357
- f"Found {len(lines)} commits matching '{pattern}':\n\n"
358
- + result.stdout
359
- )
352
+ return f"Found {len(lines)} commits matching '{pattern}':\n\n" + result.stdout
360
353
  else:
361
354
  return f"No commits found matching: {pattern}"
362
355
  else:
363
- raise subprocess.CalledProcessError(
364
- result.returncode, cmd, result.stdout, result.stderr
365
- )
356
+ raise subprocess.CalledProcessError(result.returncode, cmd, result.stdout, result.stderr)
366
357
 
367
358
  async def _search_diff(
368
359
  self,
@@ -387,8 +378,7 @@ Examples:
387
378
  import re
388
379
 
389
380
  case_insensitive_pattern = "".join(
390
- f"[{c.upper()}{c.lower()}]" if c.isalpha() else re.escape(c)
391
- for c in pattern
381
+ f"[{c.upper()}{c.lower()}]" if c.isalpha() else re.escape(c) for c in pattern
392
382
  )
393
383
  search_flag = f"-G{case_insensitive_pattern}"
394
384
 
@@ -415,9 +405,7 @@ Examples:
415
405
 
416
406
  if result.returncode == 0 and result.stdout.strip():
417
407
  # Parse and highlight matching lines
418
- output = self._highlight_diff_matches(
419
- result.stdout, pattern, case_sensitive
420
- )
408
+ output = self._highlight_diff_matches(result.stdout, pattern, case_sensitive)
421
409
  matches = output.count("commit ")
422
410
  await tool_ctx.info(f"Found {matches} commits with changes")
423
411
  return f"Found {matches} commits with changes matching '{pattern}':\n\n{output}"
@@ -505,9 +493,8 @@ Examples:
505
493
 
506
494
  if all_matches:
507
495
  await tool_ctx.info(f"Found {len(all_matches)} matching lines")
508
- return (
509
- f"Found {len(all_matches)} lines matching '{pattern}':\n\n"
510
- + "\n".join(all_matches[:50])
496
+ return f"Found {len(all_matches)} lines matching '{pattern}':\n\n" + "\n".join(
497
+ all_matches[:50]
511
498
  ) # Limit output
512
499
  else:
513
500
  return f"No lines found matching: {pattern}"
@@ -534,9 +521,7 @@ Examples:
534
521
 
535
522
  return f"Found matches for '{pattern}':\n" + "\n".join(output)
536
523
 
537
- def _highlight_diff_matches(
538
- self, diff_output: str, pattern: str, case_sensitive: bool
539
- ) -> str:
524
+ def _highlight_diff_matches(self, diff_output: str, pattern: str, case_sensitive: bool) -> str:
540
525
  """Highlight matching lines in diff output."""
541
526
  lines = diff_output.split("\n")
542
527
  output = []
@@ -118,9 +118,7 @@ When you are doing an open ended search that may require multiple rounds of glob
118
118
  # Special case for tests: direct file path with include pattern that doesn't match
119
119
  if Path(path).is_file() and include_pattern and include_pattern != "*":
120
120
  if not fnmatch.fnmatch(Path(path).name, include_pattern):
121
- await tool_ctx.info(
122
- f"File does not match pattern '{include_pattern}': {path}"
123
- )
121
+ await tool_ctx.info(f"File does not match pattern '{include_pattern}': {path}")
124
122
  return f"File does not match pattern '{include_pattern}': {path}"
125
123
 
126
124
  cmd = ["rg", "--json", pattern]
@@ -144,9 +142,7 @@ When you are doing an open ended search that may require multiple rounds of glob
144
142
 
145
143
  if process.returncode != 0 and process.returncode != 1:
146
144
  # rg returns 1 when no matches are found, which is not an error
147
- await tool_ctx.error(
148
- f"ripgrep failed with exit code {process.returncode}: {stderr.decode()}"
149
- )
145
+ await tool_ctx.error(f"ripgrep failed with exit code {process.returncode}: {stderr.decode()}")
150
146
  return f"Error executing ripgrep: {stderr.decode()}"
151
147
 
152
148
  # Parse the JSON output
@@ -182,9 +178,7 @@ When you are doing an open ended search that may require multiple rounds of glob
182
178
  if data.get("type") == "match":
183
179
  path = data.get("data", {}).get("path", {}).get("text", "")
184
180
  line_number = data.get("data", {}).get("line_number", 0)
185
- line_text = (
186
- data.get("data", {}).get("lines", {}).get("text", "").rstrip()
187
- )
181
+ line_text = data.get("data", {}).get("lines", {}).get("text", "").rstrip()
188
182
 
189
183
  if path not in file_results:
190
184
  file_results[path] = []
@@ -251,9 +245,7 @@ When you are doing an open ended search that may require multiple rounds of glob
251
245
  await tool_ctx.info(f"Searching single file: {path}")
252
246
  else:
253
247
  # File doesn't match the pattern, return immediately
254
- await tool_ctx.info(
255
- f"File does not match pattern '{include_pattern}': {path}"
256
- )
248
+ await tool_ctx.info(f"File does not match pattern '{include_pattern}': {path}")
257
249
  return f"File does not match pattern '{include_pattern}': {path}"
258
250
  elif input_path.is_dir():
259
251
  # Directory search - find all files
@@ -290,9 +282,7 @@ When you are doing an open ended search that may require multiple rounds of glob
290
282
  if input_path.is_file():
291
283
  await tool_ctx.info(f"Searching file: {path}")
292
284
  else:
293
- await tool_ctx.info(
294
- f"Searching through {total_files} files in directory"
295
- )
285
+ await tool_ctx.info(f"Searching through {total_files} files in directory")
296
286
 
297
287
  # Set up for parallel processing
298
288
  results: list[str] = []
@@ -314,18 +304,14 @@ When you are doing an open ended search that may require multiple rounds of glob
314
304
  with open(file_path, "r", encoding="utf-8") as f:
315
305
  for line_num, line in enumerate(f, 1):
316
306
  if re.search(pattern, line):
317
- file_results.append(
318
- f"{file_path}:{line_num}: {line.rstrip()}"
319
- )
307
+ file_results.append(f"{file_path}:{line_num}: {line.rstrip()}")
320
308
  matches_found += 1
321
309
  files_processed += 1
322
310
  except UnicodeDecodeError:
323
311
  # Skip binary files
324
312
  files_processed += 1
325
313
  except Exception as e:
326
- await tool_ctx.warning(
327
- f"Error reading {file_path}: {str(e)}"
328
- )
314
+ await tool_ctx.warning(f"Error reading {file_path}: {str(e)}")
329
315
  except Exception as e:
330
316
  await tool_ctx.warning(f"Error processing {file_path}: {str(e)}")
331
317
 
@@ -429,9 +415,7 @@ When you are doing an open ended search that may require multiple rounds of glob
429
415
  truncation_message="\n\n[Grep results truncated due to token limit. Use more specific patterns or paths to reduce output.]",
430
416
  )
431
417
  else:
432
- await tool_ctx.info(
433
- "ripgrep is not installed, using fallback implementation"
434
- )
418
+ await tool_ctx.info("ripgrep is not installed, using fallback implementation")
435
419
  result = await self.fallback_grep(pattern, path, tool_ctx, include)
436
420
  return truncate_response(
437
421
  result,
@@ -462,6 +446,4 @@ When you are doing an open ended search that may require multiple rounds of glob
462
446
  include: Include,
463
447
  ) -> str:
464
448
  # Use 'include' parameter if provided, otherwise fall back to 'file_pattern'
465
- return await tool_self.call(
466
- ctx, pattern=pattern, path=path, include=include
467
- )
449
+ return await tool_self.call(ctx, pattern=pattern, path=path, include=include)
@@ -170,15 +170,11 @@ If you want to create a new file, use:
170
170
  expected_replacements = edit.get("expected_replacements", 1)
171
171
 
172
172
  if old_string is None:
173
- await tool_ctx.error(
174
- f"Parameter 'old_string' in edit at index {i} is required but was None"
175
- )
173
+ await tool_ctx.error(f"Parameter 'old_string' in edit at index {i} is required but was None")
176
174
  return f"Error: Parameter 'old_string' in edit at index {i} is required but was None"
177
175
 
178
176
  if new_string is None:
179
- await tool_ctx.error(
180
- f"Parameter 'new_string' in edit at index {i} is required but was None"
181
- )
177
+ await tool_ctx.error(f"Parameter 'new_string' in edit at index {i} is required but was None")
182
178
  return f"Error: Parameter 'new_string' in edit at index {i} is required but was None"
183
179
 
184
180
  if (
@@ -192,12 +188,8 @@ If you want to create a new file, use:
192
188
  return f"Error: Parameter 'expected_replacements' in edit at index {i} must be a non-negative number"
193
189
 
194
190
  if old_string == new_string:
195
- await tool_ctx.error(
196
- f"Edit at index {i}: old_string and new_string are identical"
197
- )
198
- return (
199
- f"Error: Edit at index {i}: old_string and new_string are identical"
200
- )
191
+ await tool_ctx.error(f"Edit at index {i}: old_string and new_string are identical")
192
+ return f"Error: Edit at index {i}: old_string and new_string are identical"
201
193
 
202
194
  await tool_ctx.info(f"Applying {len(edits)} edits to file: {file_path}")
203
195
 
@@ -261,9 +253,7 @@ If you want to create a new file, use:
261
253
 
262
254
  # Check if old_string exists in current content
263
255
  if old_string not in current_content:
264
- edit_index = (
265
- i + 1 if not creation_mode else i + 2
266
- ) # Adjust for display
256
+ edit_index = i + 1 if not creation_mode else i + 2 # Adjust for display
267
257
  await tool_ctx.error(
268
258
  f"Edit {edit_index}: The specified old_string was not found in the file content"
269
259
  )
@@ -274,9 +264,7 @@ If you want to create a new file, use:
274
264
 
275
265
  # Check if the number of occurrences matches expected_replacements
276
266
  if occurrences != expected_replacements:
277
- edit_index = (
278
- i + 1 if not creation_mode else i + 2
279
- ) # Adjust for display
267
+ edit_index = i + 1 if not creation_mode else i + 2 # Adjust for display
280
268
  await tool_ctx.error(
281
269
  f"Edit {edit_index}: Found {occurrences} occurrences of the specified old_string, but expected {expected_replacements}"
282
270
  )
@@ -118,18 +118,12 @@ Usage:
118
118
  await tool_ctx.error("Parameter 'file_path' is required but was None")
119
119
  return "Error: Parameter 'file_path' is required but was None"
120
120
 
121
- await tool_ctx.info(
122
- f"Reading file: {file_path} (offset: {offset}, limit: {limit})"
123
- )
121
+ await tool_ctx.info(f"Reading file: {file_path} (offset: {offset}, limit: {limit})")
124
122
 
125
123
  # Check if path is allowed
126
124
  if not self.is_path_allowed(file_path):
127
- await tool_ctx.error(
128
- f"Access denied - path outside allowed directories: {file_path}"
129
- )
130
- return (
131
- f"Error: Access denied - path outside allowed directories: {file_path}"
132
- )
125
+ await tool_ctx.error(f"Access denied - path outside allowed directories: {file_path}")
126
+ return f"Error: Access denied - path outside allowed directories: {file_path}"
133
127
 
134
128
  try:
135
129
  file_path_obj = Path(file_path)
@@ -166,10 +160,7 @@ Usage:
166
160
 
167
161
  # Truncate long lines
168
162
  if len(line) > self.MAX_LINE_LENGTH:
169
- line = (
170
- line[: self.MAX_LINE_LENGTH]
171
- + self.LINE_TRUNCATION_INDICATOR
172
- )
163
+ line = line[: self.MAX_LINE_LENGTH] + self.LINE_TRUNCATION_INDICATOR
173
164
 
174
165
  # Add line with line number (1-based)
175
166
  lines.append(f"{i + 1:6d} {line.rstrip()}")
@@ -196,17 +187,12 @@ Usage:
196
187
 
197
188
  # Truncate long lines
198
189
  if len(line) > self.MAX_LINE_LENGTH:
199
- line = (
200
- line[: self.MAX_LINE_LENGTH]
201
- + self.LINE_TRUNCATION_INDICATOR
202
- )
190
+ line = line[: self.MAX_LINE_LENGTH] + self.LINE_TRUNCATION_INDICATOR
203
191
 
204
192
  # Add line with line number (1-based)
205
193
  lines.append(f"{i + 1:6d} {line.rstrip()}")
206
194
 
207
- await tool_ctx.warning(
208
- f"File read with latin-1 encoding: {file_path}"
209
- )
195
+ await tool_ctx.warning(f"File read with latin-1 encoding: {file_path}")
210
196
 
211
197
  except Exception:
212
198
  await tool_ctx.error(f"Cannot read binary file: {file_path}")
@@ -236,9 +222,7 @@ Usage:
236
222
  await tool_ctx.error(f"Error reading file: {str(e)}")
237
223
  return f"Error: {str(e)}"
238
224
 
239
- async def run(
240
- self, ctx: MCPContext, file_path: str, offset: int = 0, limit: int = 2000
241
- ) -> str:
225
+ async def run(self, ctx: MCPContext, file_path: str, offset: int = 0, limit: int = 2000) -> str:
242
226
  """Run method for backwards compatibility with test scripts.
243
227
 
244
228
  Args:
@@ -271,6 +255,4 @@ Usage:
271
255
  offset: Offset = 0,
272
256
  limit: Limit = 2000,
273
257
  ) -> str:
274
- return await tool_self.call(
275
- ctx, file_path=file_path, offset=offset, limit=limit
276
- )
258
+ return await tool_self.call(ctx, file_path=file_path, offset=offset, limit=limit)
@@ -142,9 +142,7 @@ understand project-specific requirements and preferences."""
142
142
  found_configs.append(
143
143
  {
144
144
  "path": str(config_path),
145
- "relative_path": str(
146
- config_path.relative_to(start_path)
147
- ),
145
+ "relative_path": str(config_path.relative_to(start_path)),
148
146
  "content": content,
149
147
  "size": len(content),
150
148
  }
@@ -152,9 +150,7 @@ understand project-specific requirements and preferences."""
152
150
 
153
151
  await tool_ctx.info(f"Found configuration: {config_path}")
154
152
  except Exception as e:
155
- await tool_ctx.warning(
156
- f"Could not read {config_path}: {str(e)}"
157
- )
153
+ await tool_ctx.warning(f"Could not read {config_path}: {str(e)}")
158
154
 
159
155
  # Check if we've reached the root or a git repository root
160
156
  if current_path.parent == current_path:
@@ -169,8 +165,7 @@ understand project-specific requirements and preferences."""
169
165
  if (
170
166
  config_path.exists()
171
167
  and config_path.is_file()
172
- and str(config_path)
173
- not in [c["path"] for c in found_configs]
168
+ and str(config_path) not in [c["path"] for c in found_configs]
174
169
  ):
175
170
  try:
176
171
  if self.is_path_allowed(str(config_path)):
@@ -180,21 +175,15 @@ understand project-specific requirements and preferences."""
180
175
  found_configs.append(
181
176
  {
182
177
  "path": str(config_path),
183
- "relative_path": str(
184
- config_path.relative_to(start_path)
185
- ),
178
+ "relative_path": str(config_path.relative_to(start_path)),
186
179
  "content": content,
187
180
  "size": len(content),
188
181
  }
189
182
  )
190
183
 
191
- await tool_ctx.info(
192
- f"Found configuration: {config_path}"
193
- )
184
+ await tool_ctx.info(f"Found configuration: {config_path}")
194
185
  except Exception as e:
195
- await tool_ctx.warning(
196
- f"Could not read {config_path}: {str(e)}"
197
- )
186
+ await tool_ctx.warning(f"Could not read {config_path}: {str(e)}")
198
187
  break
199
188
 
200
189
  # Move to parent directory
@@ -248,9 +248,7 @@ This is the recommended search tool for comprehensive results."""
248
248
  ) -> List[SearchResult]:
249
249
  """Run grep search and parse results."""
250
250
  try:
251
- result = await self.grep_tool.call(
252
- tool_ctx.mcp_context, pattern=pattern, path=path, include=include
253
- )
251
+ result = await self.grep_tool.call(tool_ctx.mcp_context, pattern=pattern, path=path, include=include)
254
252
 
255
253
  results = []
256
254
  if "Found" in result and "matches" in result:
@@ -281,9 +279,7 @@ This is the recommended search tool for comprehensive results."""
281
279
  await tool_ctx.error(f"Grep search failed: {e}")
282
280
  return []
283
281
 
284
- async def _run_grep_ast_search(
285
- self, pattern: str, path: str, tool_ctx, max_results: int
286
- ) -> List[SearchResult]:
282
+ async def _run_grep_ast_search(self, pattern: str, path: str, tool_ctx, max_results: int) -> List[SearchResult]:
287
283
  """Run AST-aware search and parse results."""
288
284
  try:
289
285
  result = await self.grep_ast_tool.call(
@@ -317,11 +313,7 @@ This is the recommended search tool for comprehensive results."""
317
313
  content=content,
318
314
  search_type=SearchType.GREP_AST,
319
315
  score=0.95, # High score for AST matches
320
- context=(
321
- " > ".join(current_context)
322
- if current_context
323
- else None
324
- ),
316
+ context=(" > ".join(current_context) if current_context else None),
325
317
  )
326
318
  )
327
319
 
@@ -339,9 +331,7 @@ This is the recommended search tool for comprehensive results."""
339
331
  await tool_ctx.error(f"AST search failed: {e}")
340
332
  return []
341
333
 
342
- async def _run_vector_search(
343
- self, pattern: str, path: str, tool_ctx, max_results: int
344
- ) -> List[SearchResult]:
334
+ async def _run_vector_search(self, pattern: str, path: str, tool_ctx, max_results: int) -> List[SearchResult]:
345
335
  """Run semantic vector search."""
346
336
  if not self.vector_tool:
347
337
  return []
@@ -399,9 +389,7 @@ This is the recommended search tool for comprehensive results."""
399
389
  await tool_ctx.error(f"Vector search failed: {e}")
400
390
  return []
401
391
 
402
- async def _run_git_search(
403
- self, pattern: str, path: str, tool_ctx, max_results: int
404
- ) -> List[SearchResult]:
392
+ async def _run_git_search(self, pattern: str, path: str, tool_ctx, max_results: int) -> List[SearchResult]:
405
393
  """Run git history search."""
406
394
  try:
407
395
  # Search in both content and commits
@@ -440,11 +428,7 @@ This is the recommended search tool for comprehensive results."""
440
428
  SearchResult(
441
429
  file_path=parts[0].strip(),
442
430
  line_number=None,
443
- content=(
444
- parts[-1].strip()
445
- if len(parts) > 2
446
- else line
447
- ),
431
+ content=(parts[-1].strip() if len(parts) > 2 else line),
448
432
  search_type=SearchType.GIT,
449
433
  score=0.8, # Good score for git matches
450
434
  )
@@ -460,9 +444,7 @@ This is the recommended search tool for comprehensive results."""
460
444
  await tool_ctx.error(f"Git search failed: {e}")
461
445
  return []
462
446
 
463
- async def _run_symbol_search(
464
- self, pattern: str, path: str, tool_ctx, max_results: int
465
- ) -> List[SearchResult]:
447
+ async def _run_symbol_search(self, pattern: str, path: str, tool_ctx, max_results: int) -> List[SearchResult]:
466
448
  """Search for symbol definitions using grep with specific patterns."""
467
449
  try:
468
450
  # Create patterns for common symbol definitions
@@ -477,11 +459,7 @@ This is the recommended search tool for comprehensive results."""
477
459
  # Run grep searches in parallel for each pattern
478
460
  tasks = []
479
461
  for sp in symbol_patterns:
480
- tasks.append(
481
- self.grep_tool.call(
482
- tool_ctx.mcp_context, pattern=sp, path=path, include="*"
483
- )
484
- )
462
+ tasks.append(self.grep_tool.call(tool_ctx.mcp_context, pattern=sp, path=path, include="*"))
485
463
 
486
464
  grep_results = await asyncio.gather(*tasks, return_exceptions=True)
487
465
 
@@ -518,9 +496,7 @@ This is the recommended search tool for comprehensive results."""
518
496
  await tool_ctx.error(f"Symbol search failed: {e}")
519
497
  return []
520
498
 
521
- def _deduplicate_results(
522
- self, all_results: List[SearchResult]
523
- ) -> List[SearchResult]:
499
+ def _deduplicate_results(self, all_results: List[SearchResult]) -> List[SearchResult]:
524
500
  """Deduplicate results, keeping the highest scoring version."""
525
501
  seen = {}
526
502
 
@@ -547,9 +523,7 @@ This is the recommended search tool for comprehensive results."""
547
523
  }
548
524
 
549
525
  # Sort by score (descending) and then by type priority
550
- results.sort(
551
- key=lambda r: (r.score, type_priority.get(r.search_type, 0)), reverse=True
552
- )
526
+ results.sort(key=lambda r: (r.score, type_priority.get(r.search_type, 0)), reverse=True)
553
527
 
554
528
  return results
555
529
 
@@ -599,42 +573,26 @@ This is the recommended search tool for comprehensive results."""
599
573
  search_names = []
600
574
 
601
575
  if params.get("enable_grep", True) and pattern_analysis["use_grep"]:
602
- search_tasks.append(
603
- self._run_grep_search(pattern, path, include, tool_ctx, max_results)
604
- )
576
+ search_tasks.append(self._run_grep_search(pattern, path, include, tool_ctx, max_results))
605
577
  search_names.append("grep")
606
578
 
607
579
  if params.get("enable_grep_ast", True) and pattern_analysis["use_grep_ast"]:
608
- search_tasks.append(
609
- self._run_grep_ast_search(pattern, path, tool_ctx, max_results)
610
- )
580
+ search_tasks.append(self._run_grep_ast_search(pattern, path, tool_ctx, max_results))
611
581
  search_names.append("grep_ast")
612
582
 
613
- if (
614
- params.get("enable_vector", True)
615
- and self.vector_tool
616
- and pattern_analysis["use_vector"]
617
- ):
618
- search_tasks.append(
619
- self._run_vector_search(pattern, path, tool_ctx, max_results)
620
- )
583
+ if params.get("enable_vector", True) and self.vector_tool and pattern_analysis["use_vector"]:
584
+ search_tasks.append(self._run_vector_search(pattern, path, tool_ctx, max_results))
621
585
  search_names.append("vector")
622
586
 
623
587
  if params.get("enable_git", True) and pattern_analysis["use_git"]:
624
- search_tasks.append(
625
- self._run_git_search(pattern, path, tool_ctx, max_results)
626
- )
588
+ search_tasks.append(self._run_git_search(pattern, path, tool_ctx, max_results))
627
589
  search_names.append("git")
628
590
 
629
591
  if params.get("enable_symbol", True) and pattern_analysis["use_symbol"]:
630
- search_tasks.append(
631
- self._run_symbol_search(pattern, path, tool_ctx, max_results)
632
- )
592
+ search_tasks.append(self._run_symbol_search(pattern, path, tool_ctx, max_results))
633
593
  search_names.append("symbol")
634
594
 
635
- await tool_ctx.info(
636
- f"Running {len(search_tasks)} search types in parallel: {', '.join(search_names)}"
637
- )
595
+ await tool_ctx.info(f"Running {len(search_tasks)} search types in parallel: {', '.join(search_names)}")
638
596
 
639
597
  # Run all searches in parallel
640
598
  search_results = await asyncio.gather(*search_tasks, return_exceptions=True)
@@ -719,9 +677,7 @@ This is the recommended search tool for comprehensive results."""
719
677
  score_str = f"[{result.search_type.value} {result.score:.2f}]"
720
678
 
721
679
  if result.line_number:
722
- output.append(
723
- f" {result.line_number:>4}: {score_str} {result.content}"
724
- )
680
+ output.append(f" {result.line_number:>4}: {score_str} {result.content}")
725
681
  else:
726
682
  output.append(f" {score_str} {result.content}")
727
683
 
@@ -141,9 +141,7 @@ Finds code structures (functions, classes, methods) with full context."""
141
141
  # Route to appropriate handler
142
142
  if action == "search":
143
143
  return await self._handle_search(params, tool_ctx)
144
- elif (
145
- action == "ast" or action == "grep_ast"
146
- ): # Support both for backward compatibility
144
+ elif action == "ast" or action == "grep_ast": # Support both for backward compatibility
147
145
  return await self._handle_ast(params, tool_ctx)
148
146
  elif action == "index":
149
147
  return await self._handle_index(params, tool_ctx)
@@ -218,9 +216,7 @@ Finds code structures (functions, classes, methods) with full context."""
218
216
  output = tc.format()
219
217
  else:
220
218
  # Just show matching lines
221
- output = "\n".join(
222
- [f"{line}: {code.splitlines()[line - 1]}" for line in loi]
223
- )
219
+ output = "\n".join([f"{line}: {code.splitlines()[line - 1]}" for line in loi])
224
220
 
225
221
  results.append(f"\n{file_path}:\n{output}\n")
226
222
  match_count += len(loi)
@@ -325,9 +321,7 @@ Finds code structures (functions, classes, methods) with full context."""
325
321
  return f"No matches found for '{pattern}' in {path}"
326
322
 
327
323
  output = [f"=== AST-aware Grep Results for '{pattern}' ==="]
328
- output.append(
329
- f"Total matches: {match_count} in {len([r for r in results if '===' in str(r)]) // 4} files\n"
330
- )
324
+ output.append(f"Total matches: {match_count} in {len([r for r in results if '===' in str(r)]) // 4} files\n")
331
325
  output.extend(results)
332
326
 
333
327
  if match_count >= limit:
@@ -490,16 +484,12 @@ Finds code structures (functions, classes, methods) with full context."""
490
484
  for root, _, files in os.walk(path_obj):
491
485
  for file in files:
492
486
  file_path = Path(root) / file
493
- if file_path.suffix in extensions and self.is_path_allowed(
494
- str(file_path)
495
- ):
487
+ if file_path.suffix in extensions and self.is_path_allowed(str(file_path)):
496
488
  files_to_process.append(str(file_path))
497
489
 
498
490
  return files_to_process
499
491
 
500
- def _extract_symbols(
501
- self, tc: TreeContext, file_path: str
502
- ) -> Dict[str, List[Dict[str, Any]]]:
492
+ def _extract_symbols(self, tc: TreeContext, file_path: str) -> Dict[str, List[Dict[str, Any]]]:
503
493
  """Extract symbols from a TreeContext (placeholder implementation)."""
504
494
  # This would need proper tree-sitter queries to extract symbols
505
495
  # For now, return empty structure
@@ -188,9 +188,7 @@ tree --pattern "*.py" --show-size"""
188
188
  if pattern:
189
189
  import fnmatch
190
190
 
191
- entries = [
192
- e for e in entries if fnmatch.fnmatch(e.name, pattern) or e.is_dir()
193
- ]
191
+ entries = [e for e in entries if fnmatch.fnmatch(e.name, pattern) or e.is_dir()]
194
192
 
195
193
  # Filter dirs only
196
194
  if dirs_only:
@@ -191,9 +191,7 @@ watch . --recursive --exclude "__pycache__"
191
191
  output.append("\nWatch cancelled")
192
192
 
193
193
  # Summary
194
- output.append(
195
- f"\nWatch completed after {int(time.time() - start_time)} seconds"
196
- )
194
+ output.append(f"\nWatch completed after {int(time.time() - start_time)} seconds")
197
195
  output.append(f"Total changes detected: {len(changes)}")
198
196
 
199
197
  return "\n".join(output)
@@ -125,9 +125,7 @@ Usage:
125
125
  with open(path_obj, "w", encoding="utf-8") as f:
126
126
  f.write(content)
127
127
 
128
- await tool_ctx.info(
129
- f"Successfully wrote file: {file_path} ({len(content)} bytes)"
130
- )
128
+ await tool_ctx.info(f"Successfully wrote file: {file_path} ({len(content)} bytes)")
131
129
  return f"Successfully wrote file: {file_path} ({len(content)} bytes)"
132
130
  except Exception as e:
133
131
  await tool_ctx.error(f"Error writing file: {str(e)}")