hanzo-mcp 0.7.7__py3-none-any.whl → 0.8.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 (178) hide show
  1. hanzo_mcp/__init__.py +6 -0
  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.7.dist-info → hanzo_mcp-0.8.1.dist-info}/METADATA +1 -1
  174. hanzo_mcp-0.8.1.dist-info/RECORD +185 -0
  175. hanzo_mcp-0.7.7.dist-info/RECORD +0 -182
  176. {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.1.dist-info}/WHEEL +0 -0
  177. {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.1.dist-info}/entry_points.txt +0 -0
  178. {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.1.dist-info}/top_level.txt +0 -0
@@ -10,28 +10,37 @@ This tool consolidates all search capabilities and runs them concurrently:
10
10
  Results are combined, deduplicated, and ranked for comprehensive search coverage.
11
11
  """
12
12
 
13
- import asyncio
14
13
  import re
15
- from dataclasses import dataclass
16
- from pathlib import Path
17
- from typing import Annotated, Dict, List, Optional, Set, Tuple, TypedDict, Unpack, final, override
14
+ import asyncio
18
15
  from enum import Enum
16
+ from typing import (
17
+ Dict,
18
+ List,
19
+ Unpack,
20
+ Optional,
21
+ Annotated,
22
+ TypedDict,
23
+ final,
24
+ override,
25
+ )
26
+ from dataclasses import dataclass
19
27
 
20
- from mcp.server.fastmcp import Context as MCPContext
21
- from mcp.server import FastMCP
22
28
  from pydantic import Field
29
+ from mcp.server import FastMCP
30
+ from mcp.server.fastmcp import Context as MCPContext
23
31
 
24
32
  from hanzo_mcp.tools.filesystem.base import FilesystemBaseTool
25
33
  from hanzo_mcp.tools.filesystem.grep import Grep
26
- from hanzo_mcp.tools.filesystem.symbols_tool import SymbolsTool
27
- from hanzo_mcp.tools.filesystem.git_search import GitSearchTool
28
- from hanzo_mcp.tools.vector.vector_search import VectorSearchTool
29
34
  from hanzo_mcp.tools.common.permissions import PermissionManager
35
+ from hanzo_mcp.tools.vector.vector_search import VectorSearchTool
36
+ from hanzo_mcp.tools.filesystem.git_search import GitSearchTool
30
37
  from hanzo_mcp.tools.vector.project_manager import ProjectVectorManager
38
+ from hanzo_mcp.tools.filesystem.symbols_tool import SymbolsTool
31
39
 
32
40
 
33
41
  class SearchType(Enum):
34
42
  """Types of searches that can be performed."""
43
+
35
44
  GREP = "grep"
36
45
  GREP_AST = "grep_ast"
37
46
  VECTOR = "vector"
@@ -42,6 +51,7 @@ class SearchType(Enum):
42
51
  @dataclass
43
52
  class SearchResult:
44
53
  """Search result from any search type."""
54
+
45
55
  file_path: str
46
56
  line_number: Optional[int]
47
57
  content: str
@@ -134,6 +144,7 @@ IncludeContext = Annotated[
134
144
 
135
145
  class UnifiedSearchParams(TypedDict):
136
146
  """Parameters for search."""
147
+
137
148
  pattern: Pattern
138
149
  path: SearchPath
139
150
  include: Include
@@ -149,34 +160,37 @@ class UnifiedSearchParams(TypedDict):
149
160
  @final
150
161
  class SearchTool(FilesystemBaseTool):
151
162
  """Search tool that runs multiple search types in parallel."""
152
-
153
- def __init__(self, permission_manager: PermissionManager,
154
- project_manager: Optional[ProjectVectorManager] = None):
163
+
164
+ def __init__(
165
+ self,
166
+ permission_manager: PermissionManager,
167
+ project_manager: Optional[ProjectVectorManager] = None,
168
+ ):
155
169
  """Initialize the search tool.
156
-
170
+
157
171
  Args:
158
172
  permission_manager: Permission manager for access control
159
173
  project_manager: Optional project manager for vector search
160
174
  """
161
175
  super().__init__(permission_manager)
162
176
  self.project_manager = project_manager
163
-
177
+
164
178
  # Initialize component tools
165
179
  self.grep_tool = Grep(permission_manager)
166
180
  self.grep_ast_tool = SymbolsTool(permission_manager)
167
181
  self.git_search_tool = GitSearchTool(permission_manager)
168
-
182
+
169
183
  # Vector search is optional
170
184
  self.vector_tool = None
171
185
  if project_manager:
172
186
  self.vector_tool = VectorSearchTool(permission_manager, project_manager)
173
-
187
+
174
188
  @property
175
189
  @override
176
190
  def name(self) -> str:
177
191
  """Get the tool name."""
178
192
  return "search"
179
-
193
+
180
194
  @property
181
195
  @override
182
196
  def description(self) -> str:
@@ -200,75 +214,76 @@ Examples:
200
214
  - Track changes: pattern="bug fix" (searches git history too)
201
215
 
202
216
  This is the recommended search tool for comprehensive results."""
203
-
217
+
204
218
  def _analyze_pattern(self, pattern: str) -> Dict[str, bool]:
205
219
  """Analyze the pattern to determine optimal search strategies.
206
-
220
+
207
221
  Args:
208
222
  pattern: The search pattern
209
-
223
+
210
224
  Returns:
211
225
  Dictionary of search type recommendations
212
226
  """
213
227
  # Check if pattern looks like regex
214
- regex_chars = r'[.*+?^${}()|[\]\\]'
228
+ regex_chars = r"[.*+?^${}()|[\]\\]"
215
229
  has_regex = bool(re.search(regex_chars, pattern))
216
-
230
+
217
231
  # Check if pattern looks like a symbol name
218
- is_symbol = bool(re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', pattern))
219
-
232
+ is_symbol = bool(re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", pattern))
233
+
220
234
  # Check if pattern is natural language
221
235
  words = pattern.split()
222
236
  is_natural_language = len(words) > 2 and not has_regex
223
-
237
+
224
238
  return {
225
- 'use_grep': True, # Always useful
226
- 'use_grep_ast': not has_regex, # AST doesn't handle regex well
227
- 'use_vector': is_natural_language or len(pattern) > 10,
228
- 'use_git': True, # Always check history
229
- 'use_symbol': is_symbol or 'def' in pattern or 'class' in pattern
239
+ "use_grep": True, # Always useful
240
+ "use_grep_ast": not has_regex, # AST doesn't handle regex well
241
+ "use_vector": is_natural_language or len(pattern) > 10,
242
+ "use_git": True, # Always check history
243
+ "use_symbol": is_symbol or "def" in pattern or "class" in pattern,
230
244
  }
231
-
232
- async def _run_grep_search(self, pattern: str, path: str, include: str,
233
- tool_ctx, max_results: int) -> List[SearchResult]:
245
+
246
+ async def _run_grep_search(
247
+ self, pattern: str, path: str, include: str, tool_ctx, max_results: int
248
+ ) -> List[SearchResult]:
234
249
  """Run grep search and parse results."""
235
250
  try:
236
251
  result = await self.grep_tool.call(
237
- tool_ctx.mcp_context,
238
- pattern=pattern,
239
- path=path,
240
- include=include
252
+ tool_ctx.mcp_context, pattern=pattern, path=path, include=include
241
253
  )
242
-
254
+
243
255
  results = []
244
256
  if "Found" in result and "matches" in result:
245
- lines = result.split('\n')
257
+ lines = result.split("\n")
246
258
  for line in lines[2:]: # Skip header
247
- if ':' in line and line.strip():
259
+ if ":" in line and line.strip():
248
260
  try:
249
- parts = line.split(':', 2)
261
+ parts = line.split(":", 2)
250
262
  if len(parts) >= 3:
251
- results.append(SearchResult(
252
- file_path=parts[0],
253
- line_number=int(parts[1]),
254
- content=parts[2].strip(),
255
- search_type=SearchType.GREP,
256
- score=1.0 # Exact matches get perfect score
257
- ))
263
+ results.append(
264
+ SearchResult(
265
+ file_path=parts[0],
266
+ line_number=int(parts[1]),
267
+ content=parts[2].strip(),
268
+ search_type=SearchType.GREP,
269
+ score=1.0, # Exact matches get perfect score
270
+ )
271
+ )
258
272
  if len(results) >= max_results:
259
273
  break
260
274
  except ValueError:
261
275
  continue
262
-
276
+
263
277
  await tool_ctx.info(f"Grep found {len(results)} results")
264
278
  return results
265
-
279
+
266
280
  except Exception as e:
267
281
  await tool_ctx.error(f"Grep search failed: {e}")
268
282
  return []
269
-
270
- async def _run_grep_ast_search(self, pattern: str, path: str,
271
- tool_ctx, max_results: int) -> List[SearchResult]:
283
+
284
+ async def _run_grep_ast_search(
285
+ self, pattern: str, path: str, tool_ctx, max_results: int
286
+ ) -> List[SearchResult]:
272
287
  """Run AST-aware search and parse results."""
273
288
  try:
274
289
  result = await self.grep_ast_tool.call(
@@ -276,107 +291,117 @@ This is the recommended search tool for comprehensive results."""
276
291
  pattern=pattern,
277
292
  path=path,
278
293
  ignore_case=True,
279
- line_number=True
294
+ line_number=True,
280
295
  )
281
-
296
+
282
297
  results = []
283
298
  if result and not result.startswith("No matches"):
284
299
  current_file = None
285
300
  current_context = []
286
-
287
- for line in result.split('\n'):
288
- if line.endswith(':') and '/' in line:
301
+
302
+ for line in result.split("\n"):
303
+ if line.endswith(":") and "/" in line:
289
304
  current_file = line[:-1]
290
305
  current_context = []
291
- elif current_file and ':' in line:
306
+ elif current_file and ":" in line:
292
307
  try:
293
308
  # Try to parse line with number
294
- parts = line.split(':', 1)
309
+ parts = line.split(":", 1)
295
310
  line_num = int(parts[0].strip())
296
311
  content = parts[1].strip() if len(parts) > 1 else ""
297
-
298
- results.append(SearchResult(
299
- file_path=current_file,
300
- line_number=line_num,
301
- content=content,
302
- search_type=SearchType.GREP_AST,
303
- score=0.95, # High score for AST matches
304
- context=" > ".join(current_context) if current_context else None
305
- ))
306
-
312
+
313
+ results.append(
314
+ SearchResult(
315
+ file_path=current_file,
316
+ line_number=line_num,
317
+ content=content,
318
+ search_type=SearchType.GREP_AST,
319
+ score=0.95, # High score for AST matches
320
+ context=(
321
+ " > ".join(current_context)
322
+ if current_context
323
+ else None
324
+ ),
325
+ )
326
+ )
327
+
307
328
  if len(results) >= max_results:
308
329
  break
309
330
  except ValueError:
310
331
  # This might be context info
311
332
  if line.strip():
312
333
  current_context.append(line.strip())
313
-
334
+
314
335
  await tool_ctx.info(f"AST search found {len(results)} results")
315
336
  return results
316
-
337
+
317
338
  except Exception as e:
318
339
  await tool_ctx.error(f"AST search failed: {e}")
319
340
  return []
320
-
321
- async def _run_vector_search(self, pattern: str, path: str,
322
- tool_ctx, max_results: int) -> List[SearchResult]:
341
+
342
+ async def _run_vector_search(
343
+ self, pattern: str, path: str, tool_ctx, max_results: int
344
+ ) -> List[SearchResult]:
323
345
  """Run semantic vector search."""
324
346
  if not self.vector_tool:
325
347
  return []
326
-
348
+
327
349
  try:
328
350
  # Determine search scope
329
351
  search_scope = "current" if path == "." else "all"
330
-
352
+
331
353
  result = await self.vector_tool.call(
332
354
  tool_ctx.mcp_context,
333
355
  query=pattern,
334
356
  limit=max_results,
335
357
  score_threshold=0.3,
336
358
  search_scope=search_scope,
337
- include_content=True
359
+ include_content=True,
338
360
  )
339
-
361
+
340
362
  results = []
341
363
  if "Found" in result:
342
364
  # Parse vector search results
343
- lines = result.split('\n')
365
+ lines = result.split("\n")
344
366
  current_file = None
345
367
  current_score = 0.0
346
-
368
+
347
369
  for line in lines:
348
370
  if "Result" in line and "Score:" in line:
349
371
  # Extract score and file
350
- score_match = re.search(r'Score: ([\d.]+)%', line)
372
+ score_match = re.search(r"Score: ([\d.]+)%", line)
351
373
  if score_match:
352
374
  current_score = float(score_match.group(1)) / 100.0
353
-
354
- file_match = re.search(r' - ([^\s]+)$', line)
375
+
376
+ file_match = re.search(r" - ([^\s]+)$", line)
355
377
  if file_match:
356
378
  current_file = file_match.group(1)
357
-
358
- elif current_file and line.strip() and not line.startswith('-'):
379
+
380
+ elif current_file and line.strip() and not line.startswith("-"):
359
381
  # Content line
360
- results.append(SearchResult(
361
- file_path=current_file,
362
- line_number=None,
363
- content=line.strip()[:200], # Limit content length
364
- search_type=SearchType.VECTOR,
365
- score=current_score
366
- ))
367
-
382
+ results.append(
383
+ SearchResult(
384
+ file_path=current_file,
385
+ line_number=None,
386
+ content=line.strip()[:200], # Limit content length
387
+ search_type=SearchType.VECTOR,
388
+ score=current_score,
389
+ )
390
+ )
391
+
368
392
  if len(results) >= max_results:
369
393
  break
370
-
394
+
371
395
  await tool_ctx.info(f"Vector search found {len(results)} results")
372
396
  return results
373
-
397
+
374
398
  except Exception as e:
375
399
  await tool_ctx.error(f"Vector search failed: {e}")
376
400
  return []
377
-
378
- async def _run_git_search(self, pattern: str, path: str,
379
- tool_ctx, max_results: int) -> List[SearchResult]:
401
+
402
+ async def _run_git_search(
403
+ self, pattern: str, path: str, tool_ctx, max_results: int
404
+ ) -> List[SearchResult]:
380
405
  """Run git history search."""
381
406
  try:
382
407
  # Search in both content and commits
@@ -386,51 +411,58 @@ This is the recommended search tool for comprehensive results."""
386
411
  pattern=pattern,
387
412
  path=path,
388
413
  search_type="content",
389
- max_count=max_results // 2
414
+ max_count=max_results // 2,
390
415
  ),
391
416
  self.git_search_tool.call(
392
417
  tool_ctx.mcp_context,
393
418
  pattern=pattern,
394
419
  path=path,
395
420
  search_type="commits",
396
- max_count=max_results // 2
397
- )
421
+ max_count=max_results // 2,
422
+ ),
398
423
  ]
399
-
424
+
400
425
  git_results = await asyncio.gather(*tasks, return_exceptions=True)
401
-
426
+
402
427
  results = []
403
- for i, result in enumerate(git_results):
428
+ for _i, result in enumerate(git_results):
404
429
  if isinstance(result, Exception):
405
430
  continue
406
-
431
+
407
432
  if "Found" in result:
408
433
  # Parse git results
409
- lines = result.split('\n')
434
+ lines = result.split("\n")
410
435
  for line in lines:
411
- if ':' in line and line.strip():
412
- parts = line.split(':', 2)
436
+ if ":" in line and line.strip():
437
+ parts = line.split(":", 2)
413
438
  if len(parts) >= 2:
414
- results.append(SearchResult(
415
- file_path=parts[0].strip(),
416
- line_number=None,
417
- content=parts[-1].strip() if len(parts) > 2 else line,
418
- search_type=SearchType.GIT,
419
- score=0.8 # Good score for git matches
420
- ))
421
-
439
+ results.append(
440
+ SearchResult(
441
+ file_path=parts[0].strip(),
442
+ line_number=None,
443
+ content=(
444
+ parts[-1].strip()
445
+ if len(parts) > 2
446
+ else line
447
+ ),
448
+ search_type=SearchType.GIT,
449
+ score=0.8, # Good score for git matches
450
+ )
451
+ )
452
+
422
453
  if len(results) >= max_results:
423
454
  break
424
-
455
+
425
456
  await tool_ctx.info(f"Git search found {len(results)} results")
426
457
  return results
427
-
458
+
428
459
  except Exception as e:
429
460
  await tool_ctx.error(f"Git search failed: {e}")
430
461
  return []
431
-
432
- async def _run_symbol_search(self, pattern: str, path: str,
433
- tool_ctx, max_results: int) -> List[SearchResult]:
462
+
463
+ async def _run_symbol_search(
464
+ self, pattern: str, path: str, tool_ctx, max_results: int
465
+ ) -> List[SearchResult]:
434
466
  """Search for symbol definitions using grep with specific patterns."""
435
467
  try:
436
468
  # Create patterns for common symbol definitions
@@ -441,67 +473,68 @@ This is the recommended search tool for comprehensive results."""
441
473
  f"let\\s+{pattern}\\s*=", # JS/TS let
442
474
  f"var\\s+{pattern}\\s*=", # JS/TS var
443
475
  ]
444
-
476
+
445
477
  # Run grep searches in parallel for each pattern
446
478
  tasks = []
447
479
  for sp in symbol_patterns:
448
480
  tasks.append(
449
481
  self.grep_tool.call(
450
- tool_ctx.mcp_context,
451
- pattern=sp,
452
- path=path,
453
- include="*"
482
+ tool_ctx.mcp_context, pattern=sp, path=path, include="*"
454
483
  )
455
484
  )
456
-
485
+
457
486
  grep_results = await asyncio.gather(*tasks, return_exceptions=True)
458
-
487
+
459
488
  results = []
460
489
  for result in grep_results:
461
490
  if isinstance(result, Exception):
462
491
  continue
463
-
492
+
464
493
  if "Found" in result and "matches" in result:
465
- lines = result.split('\n')
494
+ lines = result.split("\n")
466
495
  for line in lines[2:]: # Skip header
467
- if ':' in line and line.strip():
496
+ if ":" in line and line.strip():
468
497
  try:
469
- parts = line.split(':', 2)
498
+ parts = line.split(":", 2)
470
499
  if len(parts) >= 3:
471
- results.append(SearchResult(
472
- file_path=parts[0],
473
- line_number=int(parts[1]),
474
- content=parts[2].strip(),
475
- search_type=SearchType.SYMBOL,
476
- score=0.98 # Very high score for symbol definitions
477
- ))
500
+ results.append(
501
+ SearchResult(
502
+ file_path=parts[0],
503
+ line_number=int(parts[1]),
504
+ content=parts[2].strip(),
505
+ search_type=SearchType.SYMBOL,
506
+ score=0.98, # Very high score for symbol definitions
507
+ )
508
+ )
478
509
  if len(results) >= max_results:
479
510
  break
480
511
  except ValueError:
481
512
  continue
482
-
513
+
483
514
  await tool_ctx.info(f"Symbol search found {len(results)} results")
484
515
  return results
485
-
516
+
486
517
  except Exception as e:
487
518
  await tool_ctx.error(f"Symbol search failed: {e}")
488
519
  return []
489
-
490
- def _deduplicate_results(self, all_results: List[SearchResult]) -> List[SearchResult]:
520
+
521
+ def _deduplicate_results(
522
+ self, all_results: List[SearchResult]
523
+ ) -> List[SearchResult]:
491
524
  """Deduplicate results, keeping the highest scoring version."""
492
525
  seen = {}
493
-
526
+
494
527
  for result in all_results:
495
528
  key = (result.file_path, result.line_number)
496
-
529
+
497
530
  if key not in seen or result.score > seen[key].score:
498
531
  seen[key] = result
499
532
  elif key in seen and result.context and not seen[key].context:
500
533
  # Add context if missing
501
534
  seen[key].context = result.context
502
-
535
+
503
536
  return list(seen.values())
504
-
537
+
505
538
  def _rank_results(self, results: List[SearchResult]) -> List[SearchResult]:
506
539
  """Rank results by relevance score and search type priority."""
507
540
  # Define search type priorities
@@ -510,17 +543,16 @@ This is the recommended search tool for comprehensive results."""
510
543
  SearchType.GREP: 4,
511
544
  SearchType.GREP_AST: 3,
512
545
  SearchType.GIT: 2,
513
- SearchType.VECTOR: 1
546
+ SearchType.VECTOR: 1,
514
547
  }
515
-
548
+
516
549
  # Sort by score (descending) and then by type priority
517
550
  results.sort(
518
- key=lambda r: (r.score, type_priority.get(r.search_type, 0)),
519
- reverse=True
551
+ key=lambda r: (r.score, type_priority.get(r.search_type, 0)), reverse=True
520
552
  )
521
-
553
+
522
554
  return results
523
-
555
+
524
556
  @override
525
557
  async def call(
526
558
  self,
@@ -529,71 +561,88 @@ This is the recommended search tool for comprehensive results."""
529
561
  ) -> str:
530
562
  """Execute search across all enabled search types."""
531
563
  import time
564
+
532
565
  start_time = time.time()
533
-
566
+
534
567
  tool_ctx = self.create_tool_context(ctx)
535
-
568
+
536
569
  # Extract parameters
537
570
  pattern = params["pattern"]
538
571
  path = params.get("path", ".")
539
572
  include = params.get("include", "*")
540
573
  max_results = params.get("max_results", 50)
541
574
  include_context = params.get("include_context", True)
542
-
575
+
543
576
  # Validate path
544
577
  path_validation = self.validate_path(path)
545
578
  if path_validation.is_error:
546
579
  await tool_ctx.error(path_validation.error_message)
547
580
  return f"Error: {path_validation.error_message}"
548
-
581
+
549
582
  # Check permissions
550
583
  allowed, error_msg = await self.check_path_allowed(path, tool_ctx)
551
584
  if not allowed:
552
585
  return error_msg
553
-
586
+
554
587
  # Check existence
555
588
  exists, error_msg = await self.check_path_exists(path, tool_ctx)
556
589
  if not exists:
557
590
  return error_msg
558
-
591
+
559
592
  # Analyze pattern to determine best search strategies
560
593
  pattern_analysis = self._analyze_pattern(pattern)
561
-
594
+
562
595
  await tool_ctx.info(f"Starting search for '{pattern}' in {path}")
563
-
596
+
564
597
  # Build list of search tasks based on enabled types and pattern analysis
565
598
  search_tasks = []
566
599
  search_names = []
567
-
568
- if params.get("enable_grep", True) and pattern_analysis['use_grep']:
569
- search_tasks.append(self._run_grep_search(pattern, path, include, tool_ctx, max_results))
600
+
601
+ 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
+ )
570
605
  search_names.append("grep")
571
-
572
- if params.get("enable_grep_ast", True) and pattern_analysis['use_grep_ast']:
573
- search_tasks.append(self._run_grep_ast_search(pattern, path, tool_ctx, max_results))
606
+
607
+ 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
+ )
574
611
  search_names.append("grep_ast")
575
-
576
- if params.get("enable_vector", True) and self.vector_tool and pattern_analysis['use_vector']:
577
- search_tasks.append(self._run_vector_search(pattern, path, tool_ctx, max_results))
612
+
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
+ )
578
621
  search_names.append("vector")
579
-
580
- if params.get("enable_git", True) and pattern_analysis['use_git']:
581
- search_tasks.append(self._run_git_search(pattern, path, tool_ctx, max_results))
622
+
623
+ 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
+ )
582
627
  search_names.append("git")
583
-
584
- if params.get("enable_symbol", True) and pattern_analysis['use_symbol']:
585
- search_tasks.append(self._run_symbol_search(pattern, path, tool_ctx, max_results))
628
+
629
+ 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
+ )
586
633
  search_names.append("symbol")
587
-
588
- await tool_ctx.info(f"Running {len(search_tasks)} search types in parallel: {', '.join(search_names)}")
589
-
634
+
635
+ await tool_ctx.info(
636
+ f"Running {len(search_tasks)} search types in parallel: {', '.join(search_names)}"
637
+ )
638
+
590
639
  # Run all searches in parallel
591
640
  search_results = await asyncio.gather(*search_tasks, return_exceptions=True)
592
-
641
+
593
642
  # Collect all results
594
643
  all_results = []
595
644
  results_by_type = {}
596
-
645
+
597
646
  for search_type, results in zip(search_names, search_results):
598
647
  if isinstance(results, Exception):
599
648
  await tool_ctx.error(f"{search_type} search failed: {results}")
@@ -601,87 +650,94 @@ This is the recommended search tool for comprehensive results."""
601
650
  else:
602
651
  results_by_type[search_type] = results
603
652
  all_results.extend(results)
604
-
653
+
605
654
  # Deduplicate and rank results
606
655
  unique_results = self._deduplicate_results(all_results)
607
656
  ranked_results = self._rank_results(unique_results)
608
-
657
+
609
658
  # Limit total results
610
659
  final_results = ranked_results[:max_results]
611
-
660
+
612
661
  # Calculate search time
613
662
  search_time = (time.time() - start_time) * 1000
614
-
663
+
615
664
  # Format output
616
665
  return self._format_results(
617
666
  pattern=pattern,
618
667
  results=final_results,
619
668
  results_by_type=results_by_type,
620
669
  search_time_ms=search_time,
621
- include_context=include_context
670
+ include_context=include_context,
622
671
  )
623
-
624
- def _format_results(self, pattern: str, results: List[SearchResult],
625
- results_by_type: Dict[str, List[SearchResult]],
626
- search_time_ms: float, include_context: bool) -> str:
672
+
673
+ def _format_results(
674
+ self,
675
+ pattern: str,
676
+ results: List[SearchResult],
677
+ results_by_type: Dict[str, List[SearchResult]],
678
+ search_time_ms: float,
679
+ include_context: bool,
680
+ ) -> str:
627
681
  """Format search results for display."""
628
682
  output = []
629
-
683
+
630
684
  # Header
631
685
  output.append(f"=== Unified Search Results ===")
632
686
  output.append(f"Pattern: '{pattern}'")
633
687
  output.append(f"Total results: {len(results)}")
634
688
  output.append(f"Search time: {search_time_ms:.1f}ms")
635
-
689
+
636
690
  # Summary by type
637
691
  output.append("\nResults by type:")
638
692
  for search_type, type_results in results_by_type.items():
639
693
  if type_results:
640
694
  output.append(f" {search_type}: {len(type_results)} matches")
641
-
695
+
642
696
  if not results:
643
697
  output.append("\nNo results found.")
644
698
  return "\n".join(output)
645
-
699
+
646
700
  # Group results by file
647
701
  results_by_file = {}
648
702
  for result in results:
649
703
  if result.file_path not in results_by_file:
650
704
  results_by_file[result.file_path] = []
651
705
  results_by_file[result.file_path].append(result)
652
-
706
+
653
707
  # Display results
654
708
  output.append(f"\n=== Results ({len(results)} total) ===\n")
655
-
709
+
656
710
  for file_path, file_results in results_by_file.items():
657
711
  output.append(f"{file_path}")
658
712
  output.append("-" * len(file_path))
659
-
713
+
660
714
  # Sort by line number
661
715
  file_results.sort(key=lambda r: r.line_number or 0)
662
-
716
+
663
717
  for result in file_results:
664
718
  # Format result line
665
719
  score_str = f"[{result.search_type.value} {result.score:.2f}]"
666
-
720
+
667
721
  if result.line_number:
668
- output.append(f" {result.line_number:>4}: {score_str} {result.content}")
722
+ output.append(
723
+ f" {result.line_number:>4}: {score_str} {result.content}"
724
+ )
669
725
  else:
670
726
  output.append(f" {score_str} {result.content}")
671
-
727
+
672
728
  # Add context if available and requested
673
729
  if include_context and result.context:
674
730
  output.append(f" Context: {result.context}")
675
-
731
+
676
732
  output.append("") # Empty line between files
677
-
733
+
678
734
  return "\n".join(output)
679
-
735
+
680
736
  @override
681
737
  def register(self, mcp_server: FastMCP) -> None:
682
738
  """Register the search tool with the MCP server."""
683
739
  tool_self = self
684
-
740
+
685
741
  @mcp_server.tool(name=self.name, description=self.description)
686
742
  async def search(
687
743
  ctx: MCPContext,
@@ -708,4 +764,4 @@ This is the recommended search tool for comprehensive results."""
708
764
  enable_git=enable_git,
709
765
  enable_symbol=enable_symbol,
710
766
  include_context=include_context,
711
- )
767
+ )