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.
- hanzo_mcp/__init__.py +1 -3
- hanzo_mcp/analytics/posthog_analytics.py +4 -17
- hanzo_mcp/bridge.py +9 -25
- hanzo_mcp/cli.py +8 -17
- hanzo_mcp/cli_enhanced.py +5 -14
- hanzo_mcp/cli_plugin.py +3 -9
- hanzo_mcp/config/settings.py +6 -20
- hanzo_mcp/config/tool_config.py +2 -4
- hanzo_mcp/core/base_agent.py +88 -88
- hanzo_mcp/core/model_registry.py +238 -210
- hanzo_mcp/dev_server.py +5 -15
- hanzo_mcp/prompts/__init__.py +2 -6
- hanzo_mcp/prompts/project_todo_reminder.py +3 -9
- hanzo_mcp/prompts/tool_explorer.py +1 -3
- hanzo_mcp/prompts/utils.py +7 -21
- hanzo_mcp/server.py +6 -7
- hanzo_mcp/tools/__init__.py +29 -32
- hanzo_mcp/tools/agent/__init__.py +2 -1
- hanzo_mcp/tools/agent/agent.py +10 -30
- hanzo_mcp/tools/agent/agent_tool.py +23 -17
- hanzo_mcp/tools/agent/claude_desktop_auth.py +3 -9
- hanzo_mcp/tools/agent/cli_agent_base.py +7 -24
- hanzo_mcp/tools/agent/cli_tools.py +76 -75
- hanzo_mcp/tools/agent/code_auth.py +1 -3
- hanzo_mcp/tools/agent/code_auth_tool.py +2 -6
- hanzo_mcp/tools/agent/critic_tool.py +8 -24
- hanzo_mcp/tools/agent/iching_tool.py +12 -36
- hanzo_mcp/tools/agent/network_tool.py +7 -18
- hanzo_mcp/tools/agent/prompt.py +1 -5
- hanzo_mcp/tools/agent/review_tool.py +10 -25
- hanzo_mcp/tools/agent/swarm_alias.py +1 -3
- hanzo_mcp/tools/agent/unified_cli_tools.py +38 -38
- hanzo_mcp/tools/common/batch_tool.py +15 -45
- hanzo_mcp/tools/common/config_tool.py +9 -28
- hanzo_mcp/tools/common/context.py +1 -3
- hanzo_mcp/tools/common/critic_tool.py +1 -3
- hanzo_mcp/tools/common/decorators.py +2 -6
- hanzo_mcp/tools/common/enhanced_base.py +2 -6
- hanzo_mcp/tools/common/fastmcp_pagination.py +4 -12
- hanzo_mcp/tools/common/forgiving_edit.py +9 -28
- hanzo_mcp/tools/common/mode.py +1 -5
- hanzo_mcp/tools/common/paginated_base.py +3 -11
- hanzo_mcp/tools/common/paginated_response.py +10 -30
- hanzo_mcp/tools/common/pagination.py +3 -9
- hanzo_mcp/tools/common/path_utils.py +34 -0
- hanzo_mcp/tools/common/permissions.py +14 -13
- hanzo_mcp/tools/common/personality.py +983 -701
- hanzo_mcp/tools/common/plugin_loader.py +3 -15
- hanzo_mcp/tools/common/stats.py +7 -19
- hanzo_mcp/tools/common/thinking_tool.py +1 -3
- hanzo_mcp/tools/common/tool_disable.py +2 -6
- hanzo_mcp/tools/common/tool_list.py +2 -6
- hanzo_mcp/tools/common/validation.py +1 -3
- hanzo_mcp/tools/compiler/__init__.py +8 -0
- hanzo_mcp/tools/compiler/sandboxed_compiler.py +681 -0
- hanzo_mcp/tools/config/config_tool.py +7 -13
- hanzo_mcp/tools/config/index_config.py +1 -3
- hanzo_mcp/tools/config/mode_tool.py +5 -15
- hanzo_mcp/tools/database/database_manager.py +3 -9
- hanzo_mcp/tools/database/graph.py +1 -3
- hanzo_mcp/tools/database/graph_add.py +3 -9
- hanzo_mcp/tools/database/graph_query.py +11 -34
- hanzo_mcp/tools/database/graph_remove.py +3 -9
- hanzo_mcp/tools/database/graph_search.py +6 -20
- hanzo_mcp/tools/database/graph_stats.py +11 -33
- hanzo_mcp/tools/database/sql.py +4 -12
- hanzo_mcp/tools/database/sql_query.py +6 -10
- hanzo_mcp/tools/database/sql_search.py +2 -6
- hanzo_mcp/tools/database/sql_stats.py +5 -15
- hanzo_mcp/tools/editor/neovim_command.py +1 -3
- hanzo_mcp/tools/editor/neovim_session.py +7 -13
- hanzo_mcp/tools/environment/__init__.py +8 -0
- hanzo_mcp/tools/environment/environment_detector.py +594 -0
- hanzo_mcp/tools/filesystem/__init__.py +28 -26
- hanzo_mcp/tools/filesystem/ast_multi_edit.py +14 -43
- hanzo_mcp/tools/filesystem/ast_tool.py +3 -0
- hanzo_mcp/tools/filesystem/base.py +20 -12
- hanzo_mcp/tools/filesystem/content_replace.py +7 -12
- hanzo_mcp/tools/filesystem/diff.py +2 -10
- hanzo_mcp/tools/filesystem/directory_tree.py +285 -51
- hanzo_mcp/tools/filesystem/edit.py +10 -18
- hanzo_mcp/tools/filesystem/find.py +312 -179
- hanzo_mcp/tools/filesystem/git_search.py +12 -24
- hanzo_mcp/tools/filesystem/multi_edit.py +10 -18
- hanzo_mcp/tools/filesystem/read.py +14 -30
- hanzo_mcp/tools/filesystem/rules_tool.py +9 -17
- hanzo_mcp/tools/filesystem/search.py +1160 -0
- hanzo_mcp/tools/filesystem/watch.py +2 -4
- hanzo_mcp/tools/filesystem/write.py +7 -10
- hanzo_mcp/tools/framework/__init__.py +8 -0
- hanzo_mcp/tools/framework/framework_modes.py +714 -0
- hanzo_mcp/tools/jupyter/base.py +6 -20
- hanzo_mcp/tools/jupyter/jupyter.py +4 -12
- hanzo_mcp/tools/llm/consensus_tool.py +8 -24
- hanzo_mcp/tools/llm/llm_manage.py +2 -6
- hanzo_mcp/tools/llm/llm_tool.py +17 -58
- hanzo_mcp/tools/llm/llm_unified.py +18 -59
- hanzo_mcp/tools/llm/provider_tools.py +1 -3
- hanzo_mcp/tools/lsp/lsp_tool.py +621 -481
- hanzo_mcp/tools/mcp/mcp_add.py +3 -5
- hanzo_mcp/tools/mcp/mcp_remove.py +1 -1
- hanzo_mcp/tools/mcp/mcp_stats.py +1 -3
- hanzo_mcp/tools/mcp/mcp_tool.py +9 -23
- hanzo_mcp/tools/memory/__init__.py +33 -40
- hanzo_mcp/tools/memory/conversation_memory.py +636 -0
- hanzo_mcp/tools/memory/knowledge_tools.py +7 -25
- hanzo_mcp/tools/memory/memory_tools.py +7 -19
- hanzo_mcp/tools/search/find_tool.py +12 -34
- hanzo_mcp/tools/search/unified_search.py +27 -81
- hanzo_mcp/tools/shell/__init__.py +16 -4
- hanzo_mcp/tools/shell/auto_background.py +2 -6
- hanzo_mcp/tools/shell/base.py +1 -5
- hanzo_mcp/tools/shell/base_process.py +5 -7
- hanzo_mcp/tools/shell/bash_session.py +7 -24
- hanzo_mcp/tools/shell/bash_session_executor.py +5 -15
- hanzo_mcp/tools/shell/bash_tool.py +3 -7
- hanzo_mcp/tools/shell/command_executor.py +26 -79
- hanzo_mcp/tools/shell/logs.py +4 -16
- hanzo_mcp/tools/shell/npx.py +2 -8
- hanzo_mcp/tools/shell/npx_tool.py +1 -3
- hanzo_mcp/tools/shell/pkill.py +4 -12
- hanzo_mcp/tools/shell/process_tool.py +2 -8
- hanzo_mcp/tools/shell/processes.py +5 -17
- hanzo_mcp/tools/shell/run_background.py +1 -3
- hanzo_mcp/tools/shell/run_command.py +1 -3
- hanzo_mcp/tools/shell/run_command_windows.py +1 -3
- hanzo_mcp/tools/shell/run_tool.py +56 -0
- hanzo_mcp/tools/shell/session_manager.py +2 -6
- hanzo_mcp/tools/shell/session_storage.py +2 -6
- hanzo_mcp/tools/shell/streaming_command.py +7 -23
- hanzo_mcp/tools/shell/uvx.py +4 -14
- hanzo_mcp/tools/shell/uvx_background.py +2 -6
- hanzo_mcp/tools/shell/uvx_tool.py +1 -3
- hanzo_mcp/tools/shell/zsh_tool.py +12 -20
- hanzo_mcp/tools/todo/todo.py +1 -3
- hanzo_mcp/tools/vector/__init__.py +97 -50
- hanzo_mcp/tools/vector/ast_analyzer.py +6 -20
- hanzo_mcp/tools/vector/git_ingester.py +10 -30
- hanzo_mcp/tools/vector/index_tool.py +3 -9
- hanzo_mcp/tools/vector/infinity_store.py +11 -30
- hanzo_mcp/tools/vector/mock_infinity.py +159 -0
- hanzo_mcp/tools/vector/node_tool.py +538 -0
- hanzo_mcp/tools/vector/project_manager.py +4 -12
- hanzo_mcp/tools/vector/unified_vector.py +384 -0
- hanzo_mcp/tools/vector/vector.py +2 -6
- hanzo_mcp/tools/vector/vector_index.py +8 -8
- hanzo_mcp/tools/vector/vector_search.py +7 -21
- {hanzo_mcp-0.8.8.dist-info → hanzo_mcp-0.9.0.dist-info}/METADATA +2 -2
- hanzo_mcp-0.9.0.dist-info/RECORD +191 -0
- hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +0 -645
- hanzo_mcp/tools/agent/swarm_tool.py +0 -723
- hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +0 -577
- hanzo_mcp/tools/filesystem/batch_search.py +0 -900
- hanzo_mcp/tools/filesystem/directory_tree_paginated.py +0 -350
- hanzo_mcp/tools/filesystem/find_files.py +0 -369
- hanzo_mcp/tools/filesystem/grep.py +0 -467
- hanzo_mcp/tools/filesystem/search_tool.py +0 -767
- hanzo_mcp/tools/filesystem/symbols_tool.py +0 -515
- hanzo_mcp/tools/filesystem/tree.py +0 -270
- hanzo_mcp/tools/jupyter/notebook_edit.py +0 -317
- hanzo_mcp/tools/jupyter/notebook_read.py +0 -147
- hanzo_mcp/tools/todo/todo_read.py +0 -143
- hanzo_mcp/tools/todo/todo_write.py +0 -374
- hanzo_mcp-0.8.8.dist-info/RECORD +0 -192
- {hanzo_mcp-0.8.8.dist-info → hanzo_mcp-0.9.0.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.8.8.dist-info → hanzo_mcp-0.9.0.dist-info}/entry_points.txt +0 -0
- {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
|
-
)
|