hanzo-mcp 0.7.7__py3-none-any.whl → 0.8.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of hanzo-mcp might be problematic. Click here for more details.
- hanzo_mcp/__init__.py +6 -0
- hanzo_mcp/__main__.py +1 -1
- hanzo_mcp/analytics/__init__.py +2 -2
- hanzo_mcp/analytics/posthog_analytics.py +76 -82
- hanzo_mcp/cli.py +31 -36
- hanzo_mcp/cli_enhanced.py +94 -72
- hanzo_mcp/cli_plugin.py +27 -17
- hanzo_mcp/config/__init__.py +2 -2
- hanzo_mcp/config/settings.py +112 -88
- hanzo_mcp/config/tool_config.py +32 -34
- hanzo_mcp/dev_server.py +66 -67
- hanzo_mcp/prompts/__init__.py +94 -12
- hanzo_mcp/prompts/enhanced_prompts.py +809 -0
- hanzo_mcp/prompts/example_custom_prompt.py +6 -5
- hanzo_mcp/prompts/project_todo_reminder.py +0 -1
- hanzo_mcp/prompts/tool_explorer.py +10 -7
- hanzo_mcp/server.py +17 -21
- hanzo_mcp/server_enhanced.py +15 -22
- hanzo_mcp/tools/__init__.py +56 -28
- hanzo_mcp/tools/agent/__init__.py +16 -19
- hanzo_mcp/tools/agent/agent.py +82 -65
- hanzo_mcp/tools/agent/agent_tool.py +152 -122
- hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +66 -62
- hanzo_mcp/tools/agent/clarification_protocol.py +55 -50
- hanzo_mcp/tools/agent/clarification_tool.py +11 -10
- hanzo_mcp/tools/agent/claude_cli_tool.py +21 -20
- hanzo_mcp/tools/agent/claude_desktop_auth.py +130 -144
- hanzo_mcp/tools/agent/cli_agent_base.py +59 -53
- hanzo_mcp/tools/agent/code_auth.py +102 -107
- hanzo_mcp/tools/agent/code_auth_tool.py +28 -27
- hanzo_mcp/tools/agent/codex_cli_tool.py +20 -19
- hanzo_mcp/tools/agent/critic_tool.py +86 -73
- hanzo_mcp/tools/agent/gemini_cli_tool.py +21 -20
- hanzo_mcp/tools/agent/grok_cli_tool.py +21 -20
- hanzo_mcp/tools/agent/iching_tool.py +404 -139
- hanzo_mcp/tools/agent/network_tool.py +89 -73
- hanzo_mcp/tools/agent/prompt.py +2 -1
- hanzo_mcp/tools/agent/review_tool.py +101 -98
- hanzo_mcp/tools/agent/swarm_alias.py +87 -0
- hanzo_mcp/tools/agent/swarm_tool.py +246 -161
- hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +134 -92
- hanzo_mcp/tools/agent/tool_adapter.py +21 -11
- hanzo_mcp/tools/common/__init__.py +1 -1
- hanzo_mcp/tools/common/base.py +3 -5
- hanzo_mcp/tools/common/batch_tool.py +46 -39
- hanzo_mcp/tools/common/config_tool.py +120 -84
- hanzo_mcp/tools/common/context.py +1 -5
- hanzo_mcp/tools/common/context_fix.py +5 -3
- hanzo_mcp/tools/common/critic_tool.py +4 -8
- hanzo_mcp/tools/common/decorators.py +58 -56
- hanzo_mcp/tools/common/enhanced_base.py +29 -32
- hanzo_mcp/tools/common/fastmcp_pagination.py +91 -94
- hanzo_mcp/tools/common/forgiving_edit.py +91 -87
- hanzo_mcp/tools/common/mode.py +15 -17
- hanzo_mcp/tools/common/mode_loader.py +27 -24
- hanzo_mcp/tools/common/paginated_base.py +61 -53
- hanzo_mcp/tools/common/paginated_response.py +72 -79
- hanzo_mcp/tools/common/pagination.py +50 -53
- hanzo_mcp/tools/common/permissions.py +4 -4
- hanzo_mcp/tools/common/personality.py +186 -138
- hanzo_mcp/tools/common/plugin_loader.py +54 -54
- hanzo_mcp/tools/common/stats.py +65 -47
- hanzo_mcp/tools/common/test_helpers.py +31 -0
- hanzo_mcp/tools/common/thinking_tool.py +4 -8
- hanzo_mcp/tools/common/tool_disable.py +17 -12
- hanzo_mcp/tools/common/tool_enable.py +13 -14
- hanzo_mcp/tools/common/tool_list.py +36 -28
- hanzo_mcp/tools/common/truncate.py +23 -23
- hanzo_mcp/tools/config/__init__.py +4 -4
- hanzo_mcp/tools/config/config_tool.py +42 -29
- hanzo_mcp/tools/config/index_config.py +37 -34
- hanzo_mcp/tools/config/mode_tool.py +175 -55
- hanzo_mcp/tools/database/__init__.py +15 -12
- hanzo_mcp/tools/database/database_manager.py +77 -75
- hanzo_mcp/tools/database/graph.py +137 -91
- hanzo_mcp/tools/database/graph_add.py +30 -18
- hanzo_mcp/tools/database/graph_query.py +178 -102
- hanzo_mcp/tools/database/graph_remove.py +33 -28
- hanzo_mcp/tools/database/graph_search.py +97 -75
- hanzo_mcp/tools/database/graph_stats.py +91 -59
- hanzo_mcp/tools/database/sql.py +107 -79
- hanzo_mcp/tools/database/sql_query.py +30 -24
- hanzo_mcp/tools/database/sql_search.py +29 -25
- hanzo_mcp/tools/database/sql_stats.py +47 -35
- hanzo_mcp/tools/editor/neovim_command.py +25 -28
- hanzo_mcp/tools/editor/neovim_edit.py +21 -23
- hanzo_mcp/tools/editor/neovim_session.py +60 -54
- hanzo_mcp/tools/filesystem/__init__.py +31 -30
- hanzo_mcp/tools/filesystem/ast_multi_edit.py +329 -249
- hanzo_mcp/tools/filesystem/ast_tool.py +4 -4
- hanzo_mcp/tools/filesystem/base.py +1 -1
- hanzo_mcp/tools/filesystem/batch_search.py +316 -224
- hanzo_mcp/tools/filesystem/content_replace.py +4 -4
- hanzo_mcp/tools/filesystem/diff.py +71 -59
- hanzo_mcp/tools/filesystem/directory_tree.py +7 -7
- hanzo_mcp/tools/filesystem/directory_tree_paginated.py +49 -37
- hanzo_mcp/tools/filesystem/edit.py +4 -4
- hanzo_mcp/tools/filesystem/find.py +173 -80
- hanzo_mcp/tools/filesystem/find_files.py +73 -52
- hanzo_mcp/tools/filesystem/git_search.py +157 -104
- hanzo_mcp/tools/filesystem/grep.py +8 -8
- hanzo_mcp/tools/filesystem/multi_edit.py +4 -8
- hanzo_mcp/tools/filesystem/read.py +12 -10
- hanzo_mcp/tools/filesystem/rules_tool.py +59 -43
- hanzo_mcp/tools/filesystem/search_tool.py +263 -207
- hanzo_mcp/tools/filesystem/symbols_tool.py +94 -54
- hanzo_mcp/tools/filesystem/tree.py +35 -33
- hanzo_mcp/tools/filesystem/unix_aliases.py +13 -18
- hanzo_mcp/tools/filesystem/watch.py +37 -36
- hanzo_mcp/tools/filesystem/write.py +4 -8
- hanzo_mcp/tools/jupyter/__init__.py +4 -4
- hanzo_mcp/tools/jupyter/base.py +4 -5
- hanzo_mcp/tools/jupyter/jupyter.py +67 -47
- hanzo_mcp/tools/jupyter/notebook_edit.py +4 -4
- hanzo_mcp/tools/jupyter/notebook_read.py +4 -7
- hanzo_mcp/tools/llm/__init__.py +5 -7
- hanzo_mcp/tools/llm/consensus_tool.py +72 -52
- hanzo_mcp/tools/llm/llm_manage.py +101 -60
- hanzo_mcp/tools/llm/llm_tool.py +226 -166
- hanzo_mcp/tools/llm/provider_tools.py +25 -26
- hanzo_mcp/tools/lsp/__init__.py +1 -1
- hanzo_mcp/tools/lsp/lsp_tool.py +228 -143
- hanzo_mcp/tools/mcp/__init__.py +2 -3
- hanzo_mcp/tools/mcp/mcp_add.py +27 -25
- hanzo_mcp/tools/mcp/mcp_remove.py +7 -8
- hanzo_mcp/tools/mcp/mcp_stats.py +23 -22
- hanzo_mcp/tools/mcp/mcp_tool.py +129 -98
- hanzo_mcp/tools/memory/__init__.py +39 -21
- hanzo_mcp/tools/memory/knowledge_tools.py +124 -99
- hanzo_mcp/tools/memory/memory_tools.py +90 -108
- hanzo_mcp/tools/search/__init__.py +7 -2
- hanzo_mcp/tools/search/find_tool.py +297 -212
- hanzo_mcp/tools/search/unified_search.py +366 -314
- hanzo_mcp/tools/shell/__init__.py +8 -7
- hanzo_mcp/tools/shell/auto_background.py +56 -49
- hanzo_mcp/tools/shell/base.py +1 -1
- hanzo_mcp/tools/shell/base_process.py +75 -75
- hanzo_mcp/tools/shell/bash_session.py +2 -2
- hanzo_mcp/tools/shell/bash_session_executor.py +4 -4
- hanzo_mcp/tools/shell/bash_tool.py +24 -31
- hanzo_mcp/tools/shell/command_executor.py +12 -12
- hanzo_mcp/tools/shell/logs.py +43 -33
- hanzo_mcp/tools/shell/npx.py +13 -13
- hanzo_mcp/tools/shell/npx_background.py +24 -21
- hanzo_mcp/tools/shell/npx_tool.py +18 -22
- hanzo_mcp/tools/shell/open.py +19 -21
- hanzo_mcp/tools/shell/pkill.py +31 -26
- hanzo_mcp/tools/shell/process_tool.py +32 -32
- hanzo_mcp/tools/shell/processes.py +57 -58
- hanzo_mcp/tools/shell/run_background.py +24 -25
- hanzo_mcp/tools/shell/run_command.py +5 -5
- hanzo_mcp/tools/shell/run_command_windows.py +5 -5
- hanzo_mcp/tools/shell/session_storage.py +3 -3
- hanzo_mcp/tools/shell/streaming_command.py +141 -126
- hanzo_mcp/tools/shell/uvx.py +24 -25
- hanzo_mcp/tools/shell/uvx_background.py +35 -33
- hanzo_mcp/tools/shell/uvx_tool.py +18 -22
- hanzo_mcp/tools/todo/__init__.py +6 -2
- hanzo_mcp/tools/todo/todo.py +50 -37
- hanzo_mcp/tools/todo/todo_read.py +5 -8
- hanzo_mcp/tools/todo/todo_write.py +5 -7
- hanzo_mcp/tools/vector/__init__.py +40 -28
- hanzo_mcp/tools/vector/ast_analyzer.py +176 -143
- hanzo_mcp/tools/vector/git_ingester.py +170 -179
- hanzo_mcp/tools/vector/index_tool.py +96 -44
- hanzo_mcp/tools/vector/infinity_store.py +283 -228
- hanzo_mcp/tools/vector/mock_infinity.py +39 -40
- hanzo_mcp/tools/vector/project_manager.py +88 -78
- hanzo_mcp/tools/vector/vector.py +59 -42
- hanzo_mcp/tools/vector/vector_index.py +30 -27
- hanzo_mcp/tools/vector/vector_search.py +64 -45
- hanzo_mcp/types.py +6 -4
- {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.1.dist-info}/METADATA +1 -1
- hanzo_mcp-0.8.1.dist-info/RECORD +185 -0
- hanzo_mcp-0.7.7.dist-info/RECORD +0 -182
- {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.1.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.1.dist-info}/entry_points.txt +0 -0
- {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.1.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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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[
|
|
62
|
+
result["search_type"] = self.search_type.value
|
|
62
63
|
if self.symbol_info:
|
|
63
|
-
result[
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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[
|
|
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[
|
|
92
|
-
|
|
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__(
|
|
118
|
-
|
|
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 = [
|
|
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
|
|
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(
|
|
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(
|
|
193
|
-
|
|
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(
|
|
237
|
+
lines = grep_result.split("\n")
|
|
210
238
|
for line in lines[2:]: # Skip header lines
|
|
211
|
-
if
|
|
239
|
+
if ":" in line and len(line.strip()) > 0:
|
|
212
240
|
try:
|
|
213
|
-
parts = line.split(
|
|
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(
|
|
241
|
-
|
|
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(
|
|
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
|
|
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(
|
|
308
|
-
|
|
309
|
-
|
|
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(
|
|
328
|
-
if line.endswith(
|
|
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
|
|
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(
|
|
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=
|
|
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(
|
|
366
|
-
|
|
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 [
|
|
381
|
-
files_to_check.extend(path_obj.rglob(f
|
|
382
|
-
files_to_check = [
|
|
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}"
|
|
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(
|
|
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 (
|
|
449
|
-
symbol.
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
500
|
-
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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[
|
|
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: [
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
SearchType.
|
|
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(
|
|
647
|
-
|
|
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(
|
|
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
|
|
671
|
-
parts = line.split(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
+
)
|