hanzo-mcp 0.7.7__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 +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.0.dist-info}/METADATA +1 -1
  174. hanzo_mcp-0.8.0.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.0.dist-info}/WHEEL +0 -0
  177. {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.0.dist-info}/entry_points.txt +0 -0
  178. {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.0.dist-info}/top_level.txt +0 -0
@@ -11,41 +11,42 @@ Perfect for complex research and refactoring tasks where you need
11
11
  to find all occurrences across different dimensions.
12
12
  """
13
13
 
14
- import asyncio
15
- import json
16
14
  import re
17
- from dataclasses import dataclass, asdict
18
- from pathlib import Path
19
- from typing import Dict, List, Optional, Set, Tuple, Any, Union
15
+ import asyncio
20
16
  from enum import Enum
17
+ from typing import Any, Dict, List, Tuple, Optional
18
+ from pathlib import Path
19
+ from dataclasses import asdict, dataclass
20
+ from typing_extensions import Unpack, Annotated, TypedDict, final, override
21
21
 
22
- from mcp.server.fastmcp import Context as MCPContext
23
- from mcp.server import FastMCP
24
22
  from pydantic import Field
25
- from typing_extensions import Annotated, TypedDict, Unpack, final, override
23
+ from mcp.server import FastMCP
24
+ from mcp.server.fastmcp import Context as MCPContext
26
25
 
27
26
  from hanzo_mcp.tools.filesystem.base import FilesystemBaseTool
28
27
  from hanzo_mcp.tools.filesystem.grep import Grep
28
+ from hanzo_mcp.tools.common.permissions import PermissionManager
29
29
  from hanzo_mcp.tools.filesystem.ast_tool import ASTTool
30
- from hanzo_mcp.tools.filesystem.git_search import GitSearchTool
30
+ from hanzo_mcp.tools.vector.ast_analyzer import Symbol, ASTAnalyzer
31
31
  from hanzo_mcp.tools.vector.vector_search import VectorSearchTool
32
- from hanzo_mcp.tools.vector.ast_analyzer import ASTAnalyzer, Symbol
33
- from hanzo_mcp.tools.common.permissions import PermissionManager
32
+ from hanzo_mcp.tools.filesystem.git_search import GitSearchTool
34
33
  from hanzo_mcp.tools.vector.project_manager import ProjectVectorManager
35
34
 
36
35
 
37
36
  class SearchType(Enum):
38
37
  """Types of searches that can be performed."""
38
+
39
39
  GREP = "grep"
40
- VECTOR = "vector"
40
+ VECTOR = "vector"
41
41
  AST = "ast"
42
42
  SYMBOL = "symbol"
43
43
  GIT = "git"
44
44
 
45
45
 
46
- @dataclass
46
+ @dataclass
47
47
  class SearchResult:
48
48
  """Search result combining different search types."""
49
+
49
50
  file_path: str
50
51
  line_number: Optional[int]
51
52
  content: str
@@ -54,47 +55,58 @@ class SearchResult:
54
55
  context: Optional[str] = None # AST/function context
55
56
  symbol_info: Optional[Symbol] = None
56
57
  project: Optional[str] = None
57
-
58
+
58
59
  def to_dict(self) -> Dict[str, Any]:
59
60
  """Convert to dictionary for JSON serialization."""
60
61
  result = asdict(self)
61
- result['search_type'] = self.search_type.value
62
+ result["search_type"] = self.search_type.value
62
63
  if self.symbol_info:
63
- result['symbol_info'] = asdict(self.symbol_info)
64
+ result["symbol_info"] = asdict(self.symbol_info)
64
65
  return result
65
66
 
66
67
 
67
68
  @dataclass
68
69
  class BatchSearchResults:
69
70
  """Container for all batch search results."""
71
+
70
72
  query: str
71
73
  total_results: int
72
74
  results_by_type: Dict[SearchType, List[SearchResult]]
73
75
  combined_results: List[SearchResult]
74
76
  search_time_ms: float
75
-
77
+
76
78
  def to_dict(self) -> Dict[str, Any]:
77
79
  """Convert to dictionary for JSON serialization."""
78
80
  return {
79
- 'query': self.query,
80
- 'total_results': self.total_results,
81
- 'results_by_type': {k.value: [r.to_dict() for r in v] for k, v in self.results_by_type.items()},
82
- 'combined_results': [r.to_dict() for r in self.combined_results],
83
- 'search_time_ms': self.search_time_ms,
81
+ "query": self.query,
82
+ "total_results": self.total_results,
83
+ "results_by_type": {
84
+ k.value: [r.to_dict() for r in v]
85
+ for k, v in self.results_by_type.items()
86
+ },
87
+ "combined_results": [r.to_dict() for r in self.combined_results],
88
+ "search_time_ms": self.search_time_ms,
84
89
  }
85
90
 
86
91
 
87
- Queries = Annotated[List[Dict[str, Any]], Field(description="List of search queries with types", min_length=1)]
92
+ Queries = Annotated[
93
+ List[Dict[str, Any]],
94
+ Field(description="List of search queries with types", min_length=1),
95
+ ]
88
96
  SearchPath = Annotated[str, Field(description="Path to search in", default=".")]
89
97
  Include = Annotated[str, Field(description="File pattern to include", default="*")]
90
98
  MaxResults = Annotated[int, Field(description="Maximum results per query", default=20)]
91
- IncludeContext = Annotated[bool, Field(description="Include function/method context", default=True)]
92
- CombineResults = Annotated[bool, Field(description="Combine and deduplicate results", default=True)]
99
+ IncludeContext = Annotated[
100
+ bool, Field(description="Include function/method context", default=True)
101
+ ]
102
+ CombineResults = Annotated[
103
+ bool, Field(description="Combine and deduplicate results", default=True)
104
+ ]
93
105
 
94
106
 
95
107
  class BatchSearchParams(TypedDict):
96
108
  """Parameters for batch search.
97
-
109
+
98
110
  queries format: [
99
111
  {"type": "grep", "pattern": "TODO"},
100
112
  {"type": "vector_search", "query": "error handling"},
@@ -102,8 +114,9 @@ class BatchSearchParams(TypedDict):
102
114
  {"type": "git_search", "pattern": "bug fix", "search_type": "commits"}
103
115
  ]
104
116
  """
117
+
105
118
  queries: Queries
106
- path: SearchPath
119
+ path: SearchPath
107
120
  include: Include
108
121
  max_results: MaxResults
109
122
  include_context: IncludeContext
@@ -113,35 +126,38 @@ class BatchSearchParams(TypedDict):
113
126
  @final
114
127
  class BatchSearchTool(FilesystemBaseTool):
115
128
  """Search tool combining multiple search strategies."""
116
-
117
- def __init__(self, permission_manager: PermissionManager,
118
- project_manager: Optional[ProjectVectorManager] = None):
129
+
130
+ def __init__(
131
+ self,
132
+ permission_manager: PermissionManager,
133
+ project_manager: Optional[ProjectVectorManager] = None,
134
+ ):
119
135
  """Initialize the search tool."""
120
136
  super().__init__(permission_manager)
121
137
  self.project_manager = project_manager
122
-
138
+
123
139
  # Initialize component search tools
124
140
  self.grep_tool = Grep(permission_manager)
125
141
  self.grep_ast_tool = ASTTool(permission_manager)
126
142
  self.git_search_tool = GitSearchTool(permission_manager)
127
143
  self.ast_analyzer = ASTAnalyzer()
128
-
144
+
129
145
  # Vector search is optional
130
146
  self.vector_tool = None
131
147
  if project_manager:
132
148
  self.vector_tool = VectorSearchTool(permission_manager, project_manager)
133
-
149
+
134
150
  # Cache for AST analysis results
135
151
  self._ast_cache: Dict[str, Any] = {}
136
152
  self._symbol_cache: Dict[str, List[Symbol]] = {}
137
-
153
+
138
154
  @property
139
155
  @override
140
156
  def name(self) -> str:
141
157
  """Get the tool name."""
142
158
  return "batch_search"
143
-
144
- @property
159
+
160
+ @property
145
161
  @override
146
162
  def description(self) -> str:
147
163
  """Get the tool description."""
@@ -163,59 +179,71 @@ Perfect for comprehensive code analysis and refactoring tasks."""
163
179
 
164
180
  def _detect_search_intent(self, pattern: str) -> Tuple[bool, bool, bool]:
165
181
  """Analyze pattern to determine which search types to enable.
166
-
182
+
167
183
  Returns:
168
184
  Tuple of (should_use_vector, should_use_ast, should_use_symbol)
169
185
  """
170
186
  # Default to all enabled
171
- use_vector = True
187
+ use_vector = True
172
188
  use_ast = True
173
189
  use_symbol = True
174
-
190
+
175
191
  # If pattern looks like regex, focus on text search
176
- regex_indicators = ['.*', '\\w', '\\d', '\\s', '[', ']', '(', ')', '|', '^', '$']
192
+ regex_indicators = [
193
+ ".*",
194
+ "\\w",
195
+ "\\d",
196
+ "\\s",
197
+ "[",
198
+ "]",
199
+ "(",
200
+ ")",
201
+ "|",
202
+ "^",
203
+ "$",
204
+ ]
177
205
  if any(indicator in pattern for indicator in regex_indicators):
178
206
  use_vector = False # Regex patterns don't work well with vector search
179
-
207
+
180
208
  # If pattern looks like a function/class name, prioritize symbol search
181
- if re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', pattern):
209
+ if re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", pattern):
182
210
  use_symbol = True
183
211
  use_ast = True
184
-
185
- # If pattern contains natural language, prioritize vector search
212
+
213
+ # If pattern contains natural language, prioritize vector search
186
214
  words = pattern.split()
187
- if len(words) > 2 and not any(c in pattern for c in ['(', ')', '{', '}', '[', ']']):
215
+ if len(words) > 2 and not any(
216
+ c in pattern for c in ["(", ")", "{", "}", "[", "]"]
217
+ ):
188
218
  use_vector = True
189
-
219
+
190
220
  return use_vector, use_ast, use_symbol
191
-
192
- async def _run_grep_search(self, pattern: str, path: str, include: str,
193
- tool_ctx, max_results: int) -> List[SearchResult]:
221
+
222
+ async def _run_grep_search(
223
+ self, pattern: str, path: str, include: str, tool_ctx, max_results: int
224
+ ) -> List[SearchResult]:
194
225
  """Run grep search and convert results."""
195
226
  await tool_ctx.info(f"Running grep search for: {pattern}")
196
-
227
+
197
228
  try:
198
229
  # Use the existing grep tool
199
230
  grep_result = await self.grep_tool.call(
200
- tool_ctx.mcp_context,
201
- pattern=pattern,
202
- path=path,
203
- include=include
231
+ tool_ctx.mcp_context, pattern=pattern, path=path, include=include
204
232
  )
205
-
233
+
206
234
  results = []
207
235
  if "Found" in grep_result and "matches" in grep_result:
208
236
  # Parse grep results
209
- lines = grep_result.split('\n')
237
+ lines = grep_result.split("\n")
210
238
  for line in lines[2:]: # Skip header lines
211
- if ':' in line and len(line.strip()) > 0:
239
+ if ":" in line and len(line.strip()) > 0:
212
240
  try:
213
- parts = line.split(':', 2)
241
+ parts = line.split(":", 2)
214
242
  if len(parts) >= 3:
215
243
  file_path = parts[0]
216
244
  line_num = int(parts[1])
217
245
  content = parts[2].strip()
218
-
246
+
219
247
  result = SearchResult(
220
248
  file_path=file_path,
221
249
  line_number=line_num,
@@ -224,66 +252,67 @@ Perfect for comprehensive code analysis and refactoring tasks."""
224
252
  score=1.0, # Grep results are exact matches
225
253
  )
226
254
  results.append(result)
227
-
255
+
228
256
  if len(results) >= max_results:
229
257
  break
230
258
  except (ValueError, IndexError):
231
259
  continue
232
-
260
+
233
261
  await tool_ctx.info(f"Grep search found {len(results)} results")
234
262
  return results
235
-
263
+
236
264
  except Exception as e:
237
265
  await tool_ctx.error(f"Grep search failed: {str(e)}")
238
266
  return []
239
-
240
- async def _run_vector_search(self, pattern: str, path: str, tool_ctx,
241
- max_results: int) -> List[SearchResult]:
267
+
268
+ async def _run_vector_search(
269
+ self, pattern: str, path: str, tool_ctx, max_results: int
270
+ ) -> List[SearchResult]:
242
271
  """Run vector search and convert results."""
243
272
  if not self.vector_tool:
244
273
  return []
245
-
274
+
246
275
  await tool_ctx.info(f"Running vector search for: {pattern}")
247
-
276
+
248
277
  try:
249
278
  # Determine search scope based on path
250
279
  if path == ".":
251
280
  search_scope = "current"
252
281
  else:
253
282
  search_scope = "all" # Could be enhanced to detect project
254
-
283
+
255
284
  vector_result = await self.vector_tool.call(
256
285
  tool_ctx.mcp_context,
257
286
  query=pattern,
258
287
  limit=max_results,
259
288
  score_threshold=0.3,
260
289
  search_scope=search_scope,
261
- include_content=True
290
+ include_content=True,
262
291
  )
263
-
292
+
264
293
  results = []
265
294
  # Parse vector search results - this would need to be enhanced
266
295
  # based on the actual format returned by vector_tool
267
296
  if "Found" in vector_result:
268
297
  # This is a simplified parser - would need to match actual format
269
- lines = vector_result.split('\n')
298
+ lines = vector_result.split("\n")
270
299
  current_file = None
271
300
  current_score = 0.0
272
-
301
+
273
302
  for line in lines:
274
303
  if "Result" in line and "Score:" in line:
275
304
  # Extract score
276
- score_match = re.search(r'Score: ([\d.]+)%', line)
305
+ score_match = re.search(r"Score: ([\d.]+)%", line)
277
306
  if score_match:
278
307
  current_score = float(score_match.group(1)) / 100.0
279
-
308
+
280
309
  # Extract file path
281
310
  if " - " in line:
282
311
  parts = line.split(" - ")
283
312
  if len(parts) > 1:
284
313
  current_file = parts[-1].strip()
285
-
286
- elif current_file and line.strip() and not line.startswith('-'):
314
+
315
+ elif current_file and line.strip() and not line.startswith("-"):
287
316
  # This is content
288
317
  result = SearchResult(
289
318
  file_path=current_file,
@@ -293,99 +322,107 @@ Perfect for comprehensive code analysis and refactoring tasks."""
293
322
  score=current_score,
294
323
  )
295
324
  results.append(result)
296
-
325
+
297
326
  if len(results) >= max_results:
298
327
  break
299
-
328
+
300
329
  await tool_ctx.info(f"Vector search found {len(results)} results")
301
330
  return results
302
-
331
+
303
332
  except Exception as e:
304
333
  await tool_ctx.error(f"Vector search failed: {str(e)}")
305
334
  return []
306
-
307
- async def _run_ast_search(self, pattern: str, path: str, include: str,
308
- tool_ctx, max_results: int) -> List[SearchResult]:
309
- """Run AST-aware search and convert results."""
335
+
336
+ async def _run_ast_search(
337
+ self, pattern: str, path: str, include: str, tool_ctx, max_results: int
338
+ ) -> List[SearchResult]:
339
+ """Run AST-aware search and convert results."""
310
340
  await tool_ctx.info(f"Running AST search for: {pattern}")
311
-
341
+
312
342
  try:
313
343
  ast_result = await self.grep_ast_tool.call(
314
344
  tool_ctx.mcp_context,
315
345
  pattern=pattern,
316
346
  path=path,
317
347
  ignore_case=False,
318
- line_number=True
348
+ line_number=True,
319
349
  )
320
-
350
+
321
351
  results = []
322
352
  if ast_result and not ast_result.startswith("No matches"):
323
353
  # Parse AST results - they include structural context
324
354
  current_file = None
325
355
  context_lines = []
326
-
327
- for line in ast_result.split('\n'):
328
- if line.endswith(':') and '/' in line:
356
+
357
+ for line in ast_result.split("\n"):
358
+ if line.endswith(":") and "/" in line:
329
359
  # This is a file header
330
360
  current_file = line[:-1]
331
361
  context_lines = []
332
362
  elif current_file and line.strip():
333
- if ':' in line and line.strip()[0].isdigit():
363
+ if ":" in line and line.strip()[0].isdigit():
334
364
  # This looks like a line with number
335
365
  try:
336
- parts = line.split(':', 1)
366
+ parts = line.split(":", 1)
337
367
  line_num = int(parts[0].strip())
338
368
  content = parts[1].strip() if len(parts) > 1 else ""
339
-
369
+
340
370
  result = SearchResult(
341
371
  file_path=current_file,
342
372
  line_number=line_num,
343
373
  content=content,
344
374
  search_type=SearchType.AST,
345
375
  score=0.9, # High score for AST matches
346
- context='\n'.join(context_lines) if context_lines else None
376
+ context=(
377
+ "\n".join(context_lines)
378
+ if context_lines
379
+ else None
380
+ ),
347
381
  )
348
382
  results.append(result)
349
-
383
+
350
384
  if len(results) >= max_results:
351
385
  break
352
-
386
+
353
387
  except ValueError:
354
388
  context_lines.append(line)
355
389
  else:
356
390
  context_lines.append(line)
357
-
391
+
358
392
  await tool_ctx.info(f"AST search found {len(results)} results")
359
393
  return results
360
-
394
+
361
395
  except Exception as e:
362
396
  await tool_ctx.error(f"AST search failed: {str(e)}")
363
397
  return []
364
-
365
- async def _run_symbol_search(self, pattern: str, path: str, tool_ctx,
366
- max_results: int) -> List[SearchResult]:
398
+
399
+ async def _run_symbol_search(
400
+ self, pattern: str, path: str, tool_ctx, max_results: int
401
+ ) -> List[SearchResult]:
367
402
  """Run symbol search using AST analysis."""
368
403
  await tool_ctx.info(f"Running symbol search for: {pattern}")
369
-
404
+
370
405
  try:
371
406
  results = []
372
407
  path_obj = Path(path)
373
-
408
+
374
409
  # Find files to analyze
375
410
  files_to_check = []
376
411
  if path_obj.is_file():
377
412
  files_to_check.append(str(path_obj))
378
413
  elif path_obj.is_dir():
379
414
  # Look for source files
380
- for ext in ['.py', '.js', '.ts', '.java', '.cpp', '.c']:
381
- files_to_check.extend(path_obj.rglob(f'*{ext}'))
382
- files_to_check = [str(f) for f in files_to_check[:50]] # Limit for performance
383
-
415
+ for ext in [".py", ".js", ".ts", ".java", ".cpp", ".c"]:
416
+ files_to_check.extend(path_obj.rglob(f"*{ext}"))
417
+ files_to_check = [
418
+ str(f) for f in files_to_check[:50]
419
+ ] # Limit for performance
420
+
384
421
  # Analyze files for symbols
385
422
  for file_path in files_to_check:
386
423
  if not self.is_path_allowed(file_path):
387
424
  continue
388
-
425
+
389
426
  # Check cache first
390
427
  if file_path in self._symbol_cache:
391
428
  symbols = self._symbol_cache[file_path]
@@ -394,59 +431,73 @@ Perfect for comprehensive code analysis and refactoring tasks."""
394
431
  file_ast = self.ast_analyzer.analyze_file(file_path)
395
432
  symbols = file_ast.symbols if file_ast else []
396
433
  self._symbol_cache[file_path] = symbols
397
-
434
+
398
435
  # Search symbols
399
436
  for symbol in symbols:
400
437
  if re.search(pattern, symbol.name, re.IGNORECASE):
401
438
  result = SearchResult(
402
439
  file_path=symbol.file_path,
403
440
  line_number=symbol.line_start,
404
- content=f"{symbol.type} {symbol.name}" + (f" - {symbol.docstring[:100]}..." if symbol.docstring else ""),
441
+ content=f"{symbol.type} {symbol.name}"
442
+ + (
443
+ f" - {symbol.docstring[:100]}..."
444
+ if symbol.docstring
445
+ else ""
446
+ ),
405
447
  search_type=SearchType.SYMBOL,
406
448
  score=0.95, # Very high score for symbol matches
407
449
  symbol_info=symbol,
408
- context=symbol.signature
450
+ context=symbol.signature,
409
451
  )
410
452
  results.append(result)
411
-
453
+
412
454
  if len(results) >= max_results:
413
455
  break
414
-
456
+
415
457
  if len(results) >= max_results:
416
458
  break
417
-
459
+
418
460
  await tool_ctx.info(f"Symbol search found {len(results)} results")
419
461
  return results
420
-
462
+
421
463
  except Exception as e:
422
464
  await tool_ctx.error(f"Symbol search failed: {str(e)}")
423
465
  return []
424
-
425
- async def _add_function_context(self, results: List[SearchResult], tool_ctx) -> List[SearchResult]:
466
+
467
+ async def _add_function_context(
468
+ self, results: List[SearchResult], tool_ctx
469
+ ) -> List[SearchResult]:
426
470
  """Add function/method context to results where relevant."""
427
471
  enhanced_results = []
428
-
472
+
429
473
  for result in results:
430
474
  enhanced_result = result
431
-
475
+
432
476
  if result.line_number and not result.context:
433
477
  try:
434
478
  # Read the file and find surrounding function
435
479
  file_path = Path(result.file_path)
436
480
  if file_path.exists() and self.is_path_allowed(str(file_path)):
437
-
438
481
  # Check if we have AST analysis cached
439
482
  if str(file_path) not in self._ast_cache:
440
483
  file_ast = self.ast_analyzer.analyze_file(str(file_path))
441
484
  self._ast_cache[str(file_path)] = file_ast
442
485
  else:
443
486
  file_ast = self._ast_cache[str(file_path)]
444
-
487
+
445
488
  if file_ast:
446
489
  # Find symbol containing this line
447
490
  for symbol in file_ast.symbols:
448
- if (symbol.line_start <= result.line_number <= symbol.line_end and
449
- symbol.type in ['function', 'method']):
491
+ if (
492
+ symbol.line_start
493
+ <= result.line_number
494
+ <= symbol.line_end
495
+ and symbol.type
496
+ in [
497
+ "function",
498
+ "method",
499
+ ]
500
+ ):
450
501
  enhanced_result = SearchResult(
451
502
  file_path=result.file_path,
452
503
  line_number=result.line_number,
@@ -455,27 +506,31 @@ Perfect for comprehensive code analysis and refactoring tasks."""
455
506
  score=result.score,
456
507
  context=f"In {symbol.type} {symbol.name}(): {symbol.signature or ''}",
457
508
  symbol_info=symbol,
458
- project=result.project
509
+ project=result.project,
459
510
  )
460
511
  break
461
512
  except Exception as e:
462
- await tool_ctx.warning(f"Could not add context for {result.file_path}: {str(e)}")
463
-
513
+ await tool_ctx.warning(
514
+ f"Could not add context for {result.file_path}: {str(e)}"
515
+ )
516
+
464
517
  enhanced_results.append(enhanced_result)
465
-
518
+
466
519
  return enhanced_results
467
-
468
- def _combine_and_rank_results(self, results_by_type: Dict[SearchType, List[SearchResult]]) -> List[SearchResult]:
520
+
521
+ def _combine_and_rank_results(
522
+ self, results_by_type: Dict[SearchType, List[SearchResult]]
523
+ ) -> List[SearchResult]:
469
524
  """Combine results from different search types and rank by relevance."""
470
525
  all_results = []
471
526
  seen_combinations = set()
472
-
527
+
473
528
  # Combine all results, avoiding duplicates
474
- for search_type, results in results_by_type.items():
529
+ for _search_type, results in results_by_type.items():
475
530
  for result in results:
476
531
  # Create a key to identify duplicates
477
532
  key = (result.file_path, result.line_number)
478
-
533
+
479
534
  if key not in seen_combinations:
480
535
  seen_combinations.add(key)
481
536
  all_results.append(result)
@@ -483,50 +538,57 @@ Perfect for comprehensive code analysis and refactoring tasks."""
483
538
  # Merge with existing result based on score and type priority
484
539
  type_priority = {
485
540
  SearchType.SYMBOL: 4,
486
- SearchType.GREP: 3,
541
+ SearchType.GREP: 3,
487
542
  SearchType.AST: 2,
488
- SearchType.VECTOR: 1
543
+ SearchType.VECTOR: 1,
489
544
  }
490
-
545
+
491
546
  for existing in all_results:
492
547
  existing_key = (existing.file_path, existing.line_number)
493
548
  if existing_key == key:
494
549
  # Update if the new result has higher priority or better score
495
550
  result_priority = type_priority[result.search_type]
496
551
  existing_priority = type_priority[existing.search_type]
497
-
552
+
498
553
  # Replace existing if: higher priority type, or same priority but higher score
499
- if (result_priority > existing_priority or
500
- (result_priority == existing_priority and result.score > existing.score)):
554
+ if result_priority > existing_priority or (
555
+ result_priority == existing_priority
556
+ and result.score > existing.score
557
+ ):
501
558
  # Replace the entire result to preserve type
502
559
  idx = all_results.index(existing)
503
560
  all_results[idx] = result
504
561
  else:
505
562
  # Still merge useful information
506
563
  existing.context = existing.context or result.context
507
- existing.symbol_info = existing.symbol_info or result.symbol_info
564
+ existing.symbol_info = (
565
+ existing.symbol_info or result.symbol_info
566
+ )
508
567
  break
509
-
568
+
510
569
  # Sort by score (descending) then by search type priority
511
570
  type_priority = {
512
571
  SearchType.SYMBOL: 4,
513
- SearchType.GREP: 3,
572
+ SearchType.GREP: 3,
514
573
  SearchType.AST: 2,
515
- SearchType.VECTOR: 1
574
+ SearchType.VECTOR: 1,
516
575
  }
517
-
518
- all_results.sort(key=lambda r: (r.score, type_priority[r.search_type]), reverse=True)
519
-
576
+
577
+ all_results.sort(
578
+ key=lambda r: (r.score, type_priority[r.search_type]), reverse=True
579
+ )
580
+
520
581
  return all_results
521
-
582
+
522
583
  @override
523
584
  async def call(self, ctx: MCPContext, **params: Unpack[BatchSearchParams]) -> str:
524
585
  """Execute batch search with multiple queries in parallel."""
525
586
  import time
587
+
526
588
  start_time = time.time()
527
-
589
+
528
590
  tool_ctx = self.create_tool_context(ctx)
529
-
591
+
530
592
  # Extract parameters
531
593
  queries = params["queries"]
532
594
  path = params.get("path", ".")
@@ -534,120 +596,145 @@ Perfect for comprehensive code analysis and refactoring tasks."""
534
596
  max_results = params.get("max_results", 20)
535
597
  include_context = params.get("include_context", True)
536
598
  combine_results = params.get("combine_results", True)
537
-
599
+
538
600
  # Validate path
539
601
  path_validation = self.validate_path(path)
540
602
  if path_validation.is_error:
541
603
  await tool_ctx.error(path_validation.error_message)
542
604
  return f"Error: {path_validation.error_message}"
543
-
605
+
544
606
  # Check path permissions and existence
545
607
  allowed, error_msg = await self.check_path_allowed(path, tool_ctx)
546
608
  if not allowed:
547
609
  return error_msg
548
-
610
+
549
611
  exists, error_msg = await self.check_path_exists(path, tool_ctx)
550
612
  if not exists:
551
613
  return error_msg
552
-
553
- await tool_ctx.info(f"Starting batch search with {len(queries)} queries in {path}")
554
-
614
+
615
+ await tool_ctx.info(
616
+ f"Starting batch search with {len(queries)} queries in {path}"
617
+ )
618
+
555
619
  # Run all queries in parallel
556
620
  search_tasks = []
557
621
  query_info = [] # Track query info for results
558
-
622
+
559
623
  for query in queries:
560
624
  query_type = query.get("type", "grep")
561
625
  query_info.append(query)
562
-
626
+
563
627
  if query_type == "grep":
564
628
  pattern = query.get("pattern")
565
629
  if pattern:
566
630
  search_tasks.append(
567
- self._run_grep_search(pattern, path, include, tool_ctx, max_results)
631
+ self._run_grep_search(
632
+ pattern, path, include, tool_ctx, max_results
633
+ )
568
634
  )
569
-
635
+
570
636
  elif query_type == "grep_ast":
571
637
  pattern = query.get("pattern")
572
638
  if pattern:
573
639
  search_tasks.append(
574
- self._run_ast_search(pattern, path, include, tool_ctx, max_results)
640
+ self._run_ast_search(
641
+ pattern, path, include, tool_ctx, max_results
642
+ )
575
643
  )
576
-
644
+
577
645
  elif query_type == "vector_search" and self.vector_tool:
578
646
  search_query = query.get("query") or query.get("pattern")
579
647
  if search_query:
580
648
  search_tasks.append(
581
- self._run_vector_search(search_query, path, tool_ctx, max_results)
649
+ self._run_vector_search(
650
+ search_query, path, tool_ctx, max_results
651
+ )
582
652
  )
583
-
653
+
584
654
  elif query_type == "git_search":
585
655
  pattern = query.get("pattern")
586
656
  search_type = query.get("search_type", "content")
587
657
  if pattern:
588
658
  search_tasks.append(
589
- self._run_git_search(pattern, path, search_type, tool_ctx, max_results)
659
+ self._run_git_search(
660
+ pattern, path, search_type, tool_ctx, max_results
661
+ )
590
662
  )
591
-
663
+
592
664
  else:
593
- await tool_ctx.warning(f"Unknown or unavailable search type: {query_type}")
594
-
665
+ await tool_ctx.warning(
666
+ f"Unknown or unavailable search type: {query_type}"
667
+ )
668
+
595
669
  # Execute all searches in parallel
596
670
  search_results = await asyncio.gather(*search_tasks, return_exceptions=True)
597
-
671
+
598
672
  # Collect all results
599
673
  all_results = []
600
674
  results_by_query = {}
601
-
675
+
602
676
  for i, (query, result) in enumerate(zip(query_info, search_results)):
603
677
  if isinstance(result, Exception):
604
- await tool_ctx.error(f"Query {i+1} failed: {str(result)}")
678
+ await tool_ctx.error(f"Query {i + 1} failed: {str(result)}")
605
679
  results_by_query[i] = []
606
680
  else:
607
681
  results_by_query[i] = result
608
682
  all_results.extend(result)
609
-
683
+
610
684
  # Combine and deduplicate results if requested
611
685
  if combine_results:
612
686
  combined_results = self._combine_results(all_results)
613
687
  else:
614
688
  combined_results = all_results
615
-
689
+
616
690
  # Add context if requested
617
691
  if include_context:
618
- combined_results = await self._add_context_to_results(combined_results, tool_ctx)
619
-
692
+ combined_results = await self._add_context_to_results(
693
+ combined_results, tool_ctx
694
+ )
695
+
620
696
  end_time = time.time()
621
697
  search_time_ms = (end_time - start_time) * 1000
622
-
698
+
623
699
  # Sort by relevance score
624
700
  combined_results.sort(key=lambda r: r.score, reverse=True)
625
-
701
+
626
702
  # Limit total results
627
- combined_results = combined_results[:max_results * 2] # Allow more when combining
628
-
703
+ combined_results = combined_results[
704
+ : max_results * 2
705
+ ] # Allow more when combining
706
+
629
707
  # Create batch results object
630
708
  batch_results = BatchSearchResults(
631
709
  query=f"Batch search with {len(queries)} queries",
632
710
  total_results=len(combined_results),
633
711
  results_by_type={
634
- SearchType.GREP: [r for r in combined_results if r.search_type == SearchType.GREP],
635
- SearchType.VECTOR: [r for r in combined_results if r.search_type == SearchType.VECTOR],
636
- SearchType.AST: [r for r in combined_results if r.search_type == SearchType.AST],
637
- SearchType.GIT: [r for r in combined_results if r.search_type == SearchType.GIT],
712
+ SearchType.GREP: [
713
+ r for r in combined_results if r.search_type == SearchType.GREP
714
+ ],
715
+ SearchType.VECTOR: [
716
+ r for r in combined_results if r.search_type == SearchType.VECTOR
717
+ ],
718
+ SearchType.AST: [
719
+ r for r in combined_results if r.search_type == SearchType.AST
720
+ ],
721
+ SearchType.GIT: [
722
+ r for r in combined_results if r.search_type == SearchType.GIT
723
+ ],
638
724
  },
639
725
  combined_results=combined_results,
640
- search_time_ms=search_time_ms
726
+ search_time_ms=search_time_ms,
641
727
  )
642
-
728
+
643
729
  # Format output
644
730
  return self._format_batch_results(batch_results, query_info)
645
-
646
- async def _run_git_search(self, pattern: str, path: str, search_type: str,
647
- tool_ctx, max_results: int) -> List[SearchResult]:
731
+
732
+ async def _run_git_search(
733
+ self, pattern: str, path: str, search_type: str, tool_ctx, max_results: int
734
+ ) -> List[SearchResult]:
648
735
  """Run git search and convert results."""
649
736
  await tool_ctx.info(f"Running git search for: {pattern} (type: {search_type})")
650
-
737
+
651
738
  try:
652
739
  # Use the git search tool
653
740
  git_result = await self.git_search_tool.call(
@@ -655,24 +742,24 @@ Perfect for comprehensive code analysis and refactoring tasks."""
655
742
  pattern=pattern,
656
743
  path=path,
657
744
  search_type=search_type,
658
- max_count=max_results
745
+ max_count=max_results,
659
746
  )
660
-
747
+
661
748
  results = []
662
749
  if "Found" in git_result:
663
750
  # Parse git search results - simplified parser
664
- lines = git_result.split('\n')
751
+ lines = git_result.split("\n")
665
752
  current_file = None
666
-
753
+
667
754
  for line in lines:
668
755
  if line.strip():
669
756
  # Extract file path and content
670
- if ':' in line:
671
- parts = line.split(':', 2)
757
+ if ":" in line:
758
+ parts = line.split(":", 2)
672
759
  if len(parts) >= 2:
673
760
  file_path = parts[0].strip()
674
761
  content = parts[-1].strip() if len(parts) > 2 else line
675
-
762
+
676
763
  result = SearchResult(
677
764
  file_path=file_path,
678
765
  line_number=None,
@@ -681,26 +768,26 @@ Perfect for comprehensive code analysis and refactoring tasks."""
681
768
  score=0.8, # Git results are relevant
682
769
  )
683
770
  results.append(result)
684
-
771
+
685
772
  if len(results) >= max_results:
686
773
  break
687
-
774
+
688
775
  await tool_ctx.info(f"Git search found {len(results)} results")
689
776
  return results
690
-
777
+
691
778
  except Exception as e:
692
779
  await tool_ctx.error(f"Git search failed: {str(e)}")
693
780
  return []
694
-
781
+
695
782
  def _combine_results(self, results: List[SearchResult]) -> List[SearchResult]:
696
783
  """Combine and deduplicate search results."""
697
784
  # Use file path and line number as key for deduplication
698
785
  seen = {}
699
786
  combined = []
700
-
787
+
701
788
  for result in results:
702
789
  key = (result.file_path, result.line_number)
703
-
790
+
704
791
  if key not in seen:
705
792
  seen[key] = result
706
793
  combined.append(result)
@@ -712,81 +799,86 @@ Perfect for comprehensive code analysis and refactoring tasks."""
712
799
  idx = combined.index(existing)
713
800
  combined[idx] = result
714
801
  seen[key] = result
715
-
802
+
716
803
  return combined
717
-
718
- async def _add_context_to_results(self, results: List[SearchResult], tool_ctx) -> List[SearchResult]:
804
+
805
+ async def _add_context_to_results(
806
+ self, results: List[SearchResult], tool_ctx
807
+ ) -> List[SearchResult]:
719
808
  """Add function/method context to results."""
720
809
  # This is a simplified version - you could enhance with full AST context
721
810
  return await self._add_function_context(results, tool_ctx)
722
-
723
- def _format_batch_results(self, results: BatchSearchResults, query_info: List[Dict]) -> str:
811
+
812
+ def _format_batch_results(
813
+ self, results: BatchSearchResults, query_info: List[Dict]
814
+ ) -> str:
724
815
  """Format batch search results for display."""
725
816
  output = []
726
-
817
+
727
818
  # Header
728
819
  output.append(f"=== Batch Search Results ===")
729
820
  output.append(f"Queries: {len(query_info)}")
730
821
  output.append(f"Total results: {results.total_results}")
731
822
  output.append(f"Search time: {results.search_time_ms:.1f}ms\n")
732
-
823
+
733
824
  # Summary by type
734
825
  output.append("Results by type:")
735
826
  for search_type, type_results in results.results_by_type.items():
736
827
  if type_results:
737
828
  output.append(f" {search_type.value}: {len(type_results)} results")
738
829
  output.append("")
739
-
830
+
740
831
  # Query summary
741
832
  output.append("Queries executed:")
742
833
  for i, query in enumerate(query_info):
743
834
  query_type = query.get("type", "grep")
744
835
  pattern = query.get("pattern") or query.get("query", "")
745
- output.append(f" {i+1}. {query_type}: {pattern}")
836
+ output.append(f" {i + 1}. {query_type}: {pattern}")
746
837
  output.append("")
747
-
838
+
748
839
  # Results
749
840
  if results.combined_results:
750
841
  output.append("=== Top Results ===\n")
751
-
842
+
752
843
  # Group by file
753
844
  results_by_file = {}
754
845
  for result in results.combined_results[:50]: # Limit display
755
846
  if result.file_path not in results_by_file:
756
847
  results_by_file[result.file_path] = []
757
848
  results_by_file[result.file_path].append(result)
758
-
849
+
759
850
  # Display results by file
760
851
  for file_path, file_results in results_by_file.items():
761
852
  output.append(f"{file_path}")
762
853
  output.append("-" * len(file_path))
763
-
854
+
764
855
  # Sort by line number if available
765
856
  file_results.sort(key=lambda r: r.line_number or 0)
766
-
857
+
767
858
  for result in file_results:
768
859
  score_str = f"[{result.search_type.value} {result.score:.2f}]"
769
-
860
+
770
861
  if result.line_number:
771
- output.append(f" {result.line_number:>4}: {score_str} {result.content}")
862
+ output.append(
863
+ f" {result.line_number:>4}: {score_str} {result.content}"
864
+ )
772
865
  else:
773
866
  output.append(f" {score_str} {result.content}")
774
-
867
+
775
868
  if result.context:
776
869
  output.append(f" Context: {result.context}")
777
-
870
+
778
871
  output.append("")
779
872
  else:
780
873
  output.append("No results found.")
781
-
874
+
782
875
  return "\n".join(output)
783
-
784
-
876
+
785
877
  @override
786
878
  def register(self, mcp_server: FastMCP) -> None:
787
879
  """Register the batch search tool with the MCP server."""
788
880
  tool_self = self
789
-
881
+
790
882
  @mcp_server.tool(name=self.name, description=self.description)
791
883
  async def batch_search(
792
884
  ctx: MCPContext,
@@ -795,7 +887,7 @@ Perfect for comprehensive code analysis and refactoring tasks."""
795
887
  include: Include = "*",
796
888
  max_results: MaxResults = 20,
797
889
  include_context: IncludeContext = True,
798
- combine_results: CombineResults = True
890
+ combine_results: CombineResults = True,
799
891
  ) -> str:
800
892
  return await tool_self.call(
801
893
  ctx,
@@ -804,5 +896,5 @@ Perfect for comprehensive code analysis and refactoring tasks."""
804
896
  include=include,
805
897
  max_results=max_results,
806
898
  include_context=include_context,
807
- combine_results=combine_results
808
- )
899
+ combine_results=combine_results,
900
+ )