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