hanzo-mcp 0.9.0__py3-none-any.whl → 0.9.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 +1 -1
- hanzo_mcp/analytics/posthog_analytics.py +14 -1
- hanzo_mcp/cli.py +108 -4
- hanzo_mcp/server.py +11 -0
- hanzo_mcp/tools/__init__.py +3 -16
- hanzo_mcp/tools/agent/__init__.py +5 -0
- hanzo_mcp/tools/agent/agent.py +5 -0
- hanzo_mcp/tools/agent/agent_tool.py +3 -17
- hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +623 -0
- hanzo_mcp/tools/agent/clarification_tool.py +7 -1
- hanzo_mcp/tools/agent/claude_desktop_auth.py +16 -6
- hanzo_mcp/tools/agent/cli_agent_base.py +5 -0
- hanzo_mcp/tools/agent/cli_tools.py +26 -0
- hanzo_mcp/tools/agent/code_auth_tool.py +5 -0
- hanzo_mcp/tools/agent/critic_tool.py +7 -1
- hanzo_mcp/tools/agent/iching_tool.py +5 -0
- hanzo_mcp/tools/agent/network_tool.py +5 -0
- hanzo_mcp/tools/agent/review_tool.py +7 -1
- hanzo_mcp/tools/agent/swarm_alias.py +5 -0
- hanzo_mcp/tools/agent/swarm_tool.py +701 -0
- hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +554 -0
- hanzo_mcp/tools/agent/unified_cli_tools.py +5 -0
- hanzo_mcp/tools/common/auto_timeout.py +234 -0
- hanzo_mcp/tools/common/base.py +4 -0
- hanzo_mcp/tools/common/batch_tool.py +5 -0
- hanzo_mcp/tools/common/config_tool.py +5 -0
- hanzo_mcp/tools/common/critic_tool.py +5 -0
- hanzo_mcp/tools/common/paginated_base.py +4 -0
- hanzo_mcp/tools/common/permissions.py +38 -12
- hanzo_mcp/tools/common/personality.py +673 -980
- hanzo_mcp/tools/common/stats.py +5 -0
- hanzo_mcp/tools/common/thinking_tool.py +5 -0
- hanzo_mcp/tools/common/timeout_parser.py +103 -0
- hanzo_mcp/tools/common/tool_disable.py +5 -0
- hanzo_mcp/tools/common/tool_enable.py +5 -0
- hanzo_mcp/tools/common/tool_list.py +5 -0
- hanzo_mcp/tools/config/config_tool.py +5 -0
- hanzo_mcp/tools/config/mode_tool.py +5 -0
- hanzo_mcp/tools/database/graph.py +5 -0
- hanzo_mcp/tools/database/graph_add.py +5 -0
- hanzo_mcp/tools/database/graph_query.py +5 -0
- hanzo_mcp/tools/database/graph_remove.py +5 -0
- hanzo_mcp/tools/database/graph_search.py +5 -0
- hanzo_mcp/tools/database/graph_stats.py +5 -0
- hanzo_mcp/tools/database/sql.py +5 -0
- hanzo_mcp/tools/database/sql_query.py +2 -0
- hanzo_mcp/tools/database/sql_search.py +5 -0
- hanzo_mcp/tools/database/sql_stats.py +5 -0
- hanzo_mcp/tools/editor/neovim_command.py +5 -0
- hanzo_mcp/tools/editor/neovim_edit.py +7 -2
- hanzo_mcp/tools/editor/neovim_session.py +5 -0
- hanzo_mcp/tools/filesystem/__init__.py +23 -26
- hanzo_mcp/tools/filesystem/ast_tool.py +2 -3
- hanzo_mcp/tools/filesystem/base.py +0 -16
- hanzo_mcp/tools/filesystem/batch_search.py +825 -0
- hanzo_mcp/tools/filesystem/content_replace.py +5 -3
- hanzo_mcp/tools/filesystem/diff.py +5 -0
- hanzo_mcp/tools/filesystem/directory_tree.py +34 -281
- hanzo_mcp/tools/filesystem/directory_tree_paginated.py +345 -0
- hanzo_mcp/tools/filesystem/edit.py +5 -4
- hanzo_mcp/tools/filesystem/find.py +177 -311
- hanzo_mcp/tools/filesystem/find_files.py +370 -0
- hanzo_mcp/tools/filesystem/git_search.py +5 -3
- hanzo_mcp/tools/filesystem/grep.py +454 -0
- hanzo_mcp/tools/filesystem/multi_edit.py +5 -4
- hanzo_mcp/tools/filesystem/read.py +11 -8
- hanzo_mcp/tools/filesystem/rules_tool.py +5 -3
- hanzo_mcp/tools/filesystem/search_tool.py +728 -0
- hanzo_mcp/tools/filesystem/symbols_tool.py +510 -0
- hanzo_mcp/tools/filesystem/tree.py +273 -0
- hanzo_mcp/tools/filesystem/watch.py +6 -1
- hanzo_mcp/tools/filesystem/write.py +12 -6
- hanzo_mcp/tools/jupyter/jupyter.py +30 -2
- hanzo_mcp/tools/jupyter/notebook_edit.py +298 -0
- hanzo_mcp/tools/jupyter/notebook_read.py +148 -0
- hanzo_mcp/tools/llm/consensus_tool.py +8 -6
- hanzo_mcp/tools/llm/llm_manage.py +5 -0
- hanzo_mcp/tools/llm/llm_tool.py +2 -0
- hanzo_mcp/tools/llm/llm_unified.py +5 -0
- hanzo_mcp/tools/llm/provider_tools.py +5 -0
- hanzo_mcp/tools/lsp/lsp_tool.py +475 -622
- hanzo_mcp/tools/mcp/mcp_add.py +7 -2
- hanzo_mcp/tools/mcp/mcp_remove.py +15 -2
- hanzo_mcp/tools/mcp/mcp_stats.py +5 -0
- hanzo_mcp/tools/mcp/mcp_tool.py +5 -0
- hanzo_mcp/tools/memory/knowledge_tools.py +14 -0
- hanzo_mcp/tools/memory/memory_tools.py +17 -0
- hanzo_mcp/tools/search/find_tool.py +5 -3
- hanzo_mcp/tools/search/unified_search.py +3 -1
- hanzo_mcp/tools/shell/__init__.py +2 -14
- hanzo_mcp/tools/shell/base_process.py +4 -2
- hanzo_mcp/tools/shell/bash_tool.py +2 -0
- hanzo_mcp/tools/shell/command_executor.py +7 -7
- hanzo_mcp/tools/shell/logs.py +5 -0
- hanzo_mcp/tools/shell/npx.py +5 -0
- hanzo_mcp/tools/shell/npx_background.py +5 -0
- hanzo_mcp/tools/shell/npx_tool.py +5 -0
- hanzo_mcp/tools/shell/open.py +5 -0
- hanzo_mcp/tools/shell/pkill.py +5 -0
- hanzo_mcp/tools/shell/process_tool.py +5 -0
- hanzo_mcp/tools/shell/processes.py +5 -0
- hanzo_mcp/tools/shell/run_background.py +5 -0
- hanzo_mcp/tools/shell/run_command.py +2 -0
- hanzo_mcp/tools/shell/run_command_windows.py +5 -0
- hanzo_mcp/tools/shell/streaming_command.py +5 -0
- hanzo_mcp/tools/shell/uvx.py +5 -0
- hanzo_mcp/tools/shell/uvx_background.py +5 -0
- hanzo_mcp/tools/shell/uvx_tool.py +5 -0
- hanzo_mcp/tools/shell/zsh_tool.py +3 -0
- hanzo_mcp/tools/todo/todo.py +5 -0
- hanzo_mcp/tools/todo/todo_read.py +142 -0
- hanzo_mcp/tools/todo/todo_write.py +367 -0
- hanzo_mcp/tools/vector/__init__.py +42 -95
- hanzo_mcp/tools/vector/index_tool.py +5 -0
- hanzo_mcp/tools/vector/vector.py +5 -0
- hanzo_mcp/tools/vector/vector_index.py +5 -0
- hanzo_mcp/tools/vector/vector_search.py +5 -0
- {hanzo_mcp-0.9.0.dist-info → hanzo_mcp-0.9.1.dist-info}/METADATA +1 -1
- hanzo_mcp-0.9.1.dist-info/RECORD +195 -0
- hanzo_mcp/tools/common/path_utils.py +0 -34
- hanzo_mcp/tools/compiler/__init__.py +0 -8
- hanzo_mcp/tools/compiler/sandboxed_compiler.py +0 -681
- hanzo_mcp/tools/environment/__init__.py +0 -8
- hanzo_mcp/tools/environment/environment_detector.py +0 -594
- hanzo_mcp/tools/filesystem/search.py +0 -1160
- hanzo_mcp/tools/framework/__init__.py +0 -8
- hanzo_mcp/tools/framework/framework_modes.py +0 -714
- hanzo_mcp/tools/memory/conversation_memory.py +0 -636
- hanzo_mcp/tools/shell/run_tool.py +0 -56
- hanzo_mcp/tools/vector/node_tool.py +0 -538
- hanzo_mcp/tools/vector/unified_vector.py +0 -384
- hanzo_mcp-0.9.0.dist-info/RECORD +0 -191
- {hanzo_mcp-0.9.0.dist-info → hanzo_mcp-0.9.1.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.9.0.dist-info → hanzo_mcp-0.9.1.dist-info}/entry_points.txt +0 -0
- {hanzo_mcp-0.9.0.dist-info → hanzo_mcp-0.9.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,728 @@
|
|
|
1
|
+
"""Search tool that runs multiple search types in parallel.
|
|
2
|
+
|
|
3
|
+
This tool consolidates all search capabilities and runs them concurrently:
|
|
4
|
+
- grep: Fast pattern/regex search using ripgrep
|
|
5
|
+
- grep_ast: AST-aware code search with structural context
|
|
6
|
+
- vector_search: Semantic similarity search
|
|
7
|
+
- git_search: Search through git history
|
|
8
|
+
- symbol_search: Find symbols (functions, classes) in code
|
|
9
|
+
|
|
10
|
+
Results are combined, deduplicated, and ranked for comprehensive search coverage.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
import asyncio
|
|
15
|
+
from enum import Enum
|
|
16
|
+
from typing import (
|
|
17
|
+
Dict,
|
|
18
|
+
List,
|
|
19
|
+
Unpack,
|
|
20
|
+
Optional,
|
|
21
|
+
Annotated,
|
|
22
|
+
TypedDict,
|
|
23
|
+
final,
|
|
24
|
+
override,
|
|
25
|
+
)
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
|
|
28
|
+
from pydantic import Field
|
|
29
|
+
from mcp.server import FastMCP
|
|
30
|
+
|
|
31
|
+
from hanzo_mcp.tools.common.auto_timeout import auto_timeout
|
|
32
|
+
from mcp.server.fastmcp import Context as MCPContext
|
|
33
|
+
|
|
34
|
+
from hanzo_mcp.tools.filesystem.base import FilesystemBaseTool
|
|
35
|
+
from hanzo_mcp.tools.filesystem.grep import Grep
|
|
36
|
+
from hanzo_mcp.tools.common.permissions import PermissionManager
|
|
37
|
+
from hanzo_mcp.tools.vector.vector_search import VectorSearchTool
|
|
38
|
+
from hanzo_mcp.tools.filesystem.git_search import GitSearchTool
|
|
39
|
+
from hanzo_mcp.tools.vector.project_manager import ProjectVectorManager
|
|
40
|
+
from hanzo_mcp.tools.filesystem.symbols_tool import SymbolsTool
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class SearchType(Enum):
|
|
44
|
+
"""Types of searches that can be performed."""
|
|
45
|
+
|
|
46
|
+
GREP = "grep"
|
|
47
|
+
GREP_AST = "grep_ast"
|
|
48
|
+
VECTOR = "vector"
|
|
49
|
+
GIT = "git"
|
|
50
|
+
SYMBOL = "symbol" # Searches for function/class definitions
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class SearchResult:
|
|
55
|
+
"""Search result from any search type."""
|
|
56
|
+
|
|
57
|
+
file_path: str
|
|
58
|
+
line_number: Optional[int]
|
|
59
|
+
content: str
|
|
60
|
+
search_type: SearchType
|
|
61
|
+
score: float # Relevance score (0-1)
|
|
62
|
+
context: Optional[str] = None # Function/class context
|
|
63
|
+
match_count: int = 1 # Number of matches in this location
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
Pattern = Annotated[
|
|
67
|
+
str,
|
|
68
|
+
Field(
|
|
69
|
+
description="The search pattern (supports regex for grep, natural language for vector search)",
|
|
70
|
+
min_length=1,
|
|
71
|
+
),
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
SearchPath = Annotated[
|
|
75
|
+
str,
|
|
76
|
+
Field(
|
|
77
|
+
description="The directory to search in. Defaults to current directory.",
|
|
78
|
+
default=".",
|
|
79
|
+
),
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
Include = Annotated[
|
|
83
|
+
str,
|
|
84
|
+
Field(
|
|
85
|
+
description='File pattern to include (e.g. "*.js", "*.{ts,tsx}")',
|
|
86
|
+
default="*",
|
|
87
|
+
),
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
MaxResults = Annotated[
|
|
91
|
+
int,
|
|
92
|
+
Field(
|
|
93
|
+
description="Maximum number of results to return",
|
|
94
|
+
default=50,
|
|
95
|
+
),
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
EnableGrep = Annotated[
|
|
99
|
+
bool,
|
|
100
|
+
Field(
|
|
101
|
+
description="Enable fast pattern/regex search",
|
|
102
|
+
default=True,
|
|
103
|
+
),
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
EnableGrepAst = Annotated[
|
|
107
|
+
bool,
|
|
108
|
+
Field(
|
|
109
|
+
description="Enable AST-aware search with code structure context",
|
|
110
|
+
default=True,
|
|
111
|
+
),
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
EnableVector = Annotated[
|
|
115
|
+
bool,
|
|
116
|
+
Field(
|
|
117
|
+
description="Enable semantic similarity search",
|
|
118
|
+
default=True,
|
|
119
|
+
),
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
EnableGit = Annotated[
|
|
123
|
+
bool,
|
|
124
|
+
Field(
|
|
125
|
+
description="Enable git history search",
|
|
126
|
+
default=True,
|
|
127
|
+
),
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
EnableSymbol = Annotated[
|
|
131
|
+
bool,
|
|
132
|
+
Field(
|
|
133
|
+
description="Enable symbol search (functions, classes)",
|
|
134
|
+
default=True,
|
|
135
|
+
),
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
IncludeContext = Annotated[
|
|
139
|
+
bool,
|
|
140
|
+
Field(
|
|
141
|
+
description="Include function/class context for matches",
|
|
142
|
+
default=True,
|
|
143
|
+
),
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class UnifiedSearchParams(TypedDict):
|
|
148
|
+
"""Parameters for search."""
|
|
149
|
+
|
|
150
|
+
pattern: Pattern
|
|
151
|
+
path: SearchPath
|
|
152
|
+
include: Include
|
|
153
|
+
max_results: MaxResults
|
|
154
|
+
enable_grep: EnableGrep
|
|
155
|
+
enable_grep_ast: EnableGrepAst
|
|
156
|
+
enable_vector: EnableVector
|
|
157
|
+
enable_git: EnableGit
|
|
158
|
+
enable_symbol: EnableSymbol
|
|
159
|
+
include_context: IncludeContext
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@final
|
|
163
|
+
class SearchTool(FilesystemBaseTool):
|
|
164
|
+
"""Search tool that runs multiple search types in parallel."""
|
|
165
|
+
|
|
166
|
+
def __init__(
|
|
167
|
+
self,
|
|
168
|
+
permission_manager: PermissionManager,
|
|
169
|
+
project_manager: Optional[ProjectVectorManager] = None,
|
|
170
|
+
):
|
|
171
|
+
"""Initialize the search tool.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
permission_manager: Permission manager for access control
|
|
175
|
+
project_manager: Optional project manager for vector search
|
|
176
|
+
"""
|
|
177
|
+
super().__init__(permission_manager)
|
|
178
|
+
self.project_manager = project_manager
|
|
179
|
+
|
|
180
|
+
# Initialize component tools
|
|
181
|
+
self.grep_tool = Grep(permission_manager)
|
|
182
|
+
self.grep_ast_tool = SymbolsTool(permission_manager)
|
|
183
|
+
self.git_search_tool = GitSearchTool(permission_manager)
|
|
184
|
+
|
|
185
|
+
# Vector search is optional
|
|
186
|
+
self.vector_tool = None
|
|
187
|
+
if project_manager:
|
|
188
|
+
self.vector_tool = VectorSearchTool(permission_manager, project_manager)
|
|
189
|
+
|
|
190
|
+
@property
|
|
191
|
+
@override
|
|
192
|
+
def name(self) -> str:
|
|
193
|
+
"""Get the tool name."""
|
|
194
|
+
return "search"
|
|
195
|
+
|
|
196
|
+
@property
|
|
197
|
+
@override
|
|
198
|
+
def description(self) -> str:
|
|
199
|
+
"""Get the tool description."""
|
|
200
|
+
return """Search that runs multiple search strategies in parallel.
|
|
201
|
+
|
|
202
|
+
Automatically runs the most appropriate search types based on your pattern:
|
|
203
|
+
- Pattern matching (grep) for exact text/regex
|
|
204
|
+
- AST search for code structure understanding
|
|
205
|
+
- Semantic search for concepts and meaning
|
|
206
|
+
- Git history for tracking changes
|
|
207
|
+
- Symbol search for finding definitions
|
|
208
|
+
|
|
209
|
+
All searches run concurrently for maximum speed. Results are combined,
|
|
210
|
+
deduplicated, and ranked by relevance.
|
|
211
|
+
|
|
212
|
+
Examples:
|
|
213
|
+
- Search for TODO comments: pattern="TODO"
|
|
214
|
+
- Find error handling: pattern="error handling implementation"
|
|
215
|
+
- Locate function: pattern="processPayment"
|
|
216
|
+
- Track changes: pattern="bug fix" (searches git history too)
|
|
217
|
+
|
|
218
|
+
This is the recommended search tool for comprehensive results."""
|
|
219
|
+
|
|
220
|
+
def _analyze_pattern(self, pattern: str) -> Dict[str, bool]:
|
|
221
|
+
"""Analyze the pattern to determine optimal search strategies.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
pattern: The search pattern
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
Dictionary of search type recommendations
|
|
228
|
+
"""
|
|
229
|
+
# Check if pattern looks like regex
|
|
230
|
+
regex_chars = r"[.*+?^${}()|[\]\\]"
|
|
231
|
+
has_regex = bool(re.search(regex_chars, pattern))
|
|
232
|
+
|
|
233
|
+
# Check if pattern looks like a symbol name
|
|
234
|
+
is_symbol = bool(re.match(r"^[a-zA-Z_][a-zA-Z0-9_]*$", pattern))
|
|
235
|
+
|
|
236
|
+
# Check if pattern is natural language
|
|
237
|
+
words = pattern.split()
|
|
238
|
+
is_natural_language = len(words) > 2 and not has_regex
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
"use_grep": True, # Always useful
|
|
242
|
+
"use_grep_ast": not has_regex, # AST doesn't handle regex well
|
|
243
|
+
"use_vector": is_natural_language or len(pattern) > 10,
|
|
244
|
+
"use_git": True, # Always check history
|
|
245
|
+
"use_symbol": is_symbol or "def" in pattern or "class" in pattern,
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async def _run_grep_search(
|
|
249
|
+
self, pattern: str, path: str, include: str, tool_ctx, max_results: int
|
|
250
|
+
) -> List[SearchResult]:
|
|
251
|
+
"""Run grep search and parse results."""
|
|
252
|
+
try:
|
|
253
|
+
result = await self.grep_tool.call(tool_ctx.mcp_context, pattern=pattern, path=path, include=include)
|
|
254
|
+
|
|
255
|
+
results = []
|
|
256
|
+
if "Found" in result and "matches" in result:
|
|
257
|
+
lines = result.split("\n")
|
|
258
|
+
for line in lines[2:]: # Skip header
|
|
259
|
+
if ":" in line and line.strip():
|
|
260
|
+
try:
|
|
261
|
+
parts = line.split(":", 2)
|
|
262
|
+
if len(parts) >= 3:
|
|
263
|
+
results.append(
|
|
264
|
+
SearchResult(
|
|
265
|
+
file_path=parts[0],
|
|
266
|
+
line_number=int(parts[1]),
|
|
267
|
+
content=parts[2].strip(),
|
|
268
|
+
search_type=SearchType.GREP,
|
|
269
|
+
score=1.0, # Exact matches get perfect score
|
|
270
|
+
)
|
|
271
|
+
)
|
|
272
|
+
if len(results) >= max_results:
|
|
273
|
+
break
|
|
274
|
+
except ValueError:
|
|
275
|
+
continue
|
|
276
|
+
|
|
277
|
+
await tool_ctx.info(f"Grep found {len(results)} results")
|
|
278
|
+
return results
|
|
279
|
+
|
|
280
|
+
except Exception as e:
|
|
281
|
+
await tool_ctx.error(f"Grep search failed: {e}")
|
|
282
|
+
return []
|
|
283
|
+
|
|
284
|
+
async def _run_grep_ast_search(self, pattern: str, path: str, tool_ctx, max_results: int) -> List[SearchResult]:
|
|
285
|
+
"""Run AST-aware search and parse results."""
|
|
286
|
+
try:
|
|
287
|
+
result = await self.grep_ast_tool.call(
|
|
288
|
+
tool_ctx.mcp_context,
|
|
289
|
+
pattern=pattern,
|
|
290
|
+
path=path,
|
|
291
|
+
ignore_case=True,
|
|
292
|
+
line_number=True,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
results = []
|
|
296
|
+
if result and not result.startswith("No matches"):
|
|
297
|
+
current_file = None
|
|
298
|
+
current_context = []
|
|
299
|
+
|
|
300
|
+
for line in result.split("\n"):
|
|
301
|
+
if line.endswith(":") and "/" in line:
|
|
302
|
+
current_file = line[:-1]
|
|
303
|
+
current_context = []
|
|
304
|
+
elif current_file and ":" in line:
|
|
305
|
+
try:
|
|
306
|
+
# Try to parse line with number
|
|
307
|
+
parts = line.split(":", 1)
|
|
308
|
+
line_num = int(parts[0].strip())
|
|
309
|
+
content = parts[1].strip() if len(parts) > 1 else ""
|
|
310
|
+
|
|
311
|
+
results.append(
|
|
312
|
+
SearchResult(
|
|
313
|
+
file_path=current_file,
|
|
314
|
+
line_number=line_num,
|
|
315
|
+
content=content,
|
|
316
|
+
search_type=SearchType.GREP_AST,
|
|
317
|
+
score=0.95, # High score for AST matches
|
|
318
|
+
context=(" > ".join(current_context) if current_context else None),
|
|
319
|
+
)
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
if len(results) >= max_results:
|
|
323
|
+
break
|
|
324
|
+
except ValueError:
|
|
325
|
+
# This might be context info
|
|
326
|
+
if line.strip():
|
|
327
|
+
current_context.append(line.strip())
|
|
328
|
+
|
|
329
|
+
await tool_ctx.info(f"AST search found {len(results)} results")
|
|
330
|
+
return results
|
|
331
|
+
|
|
332
|
+
except Exception as e:
|
|
333
|
+
await tool_ctx.error(f"AST search failed: {e}")
|
|
334
|
+
return []
|
|
335
|
+
|
|
336
|
+
async def _run_vector_search(self, pattern: str, path: str, tool_ctx, max_results: int) -> List[SearchResult]:
|
|
337
|
+
"""Run semantic vector search."""
|
|
338
|
+
if not self.vector_tool:
|
|
339
|
+
return []
|
|
340
|
+
|
|
341
|
+
try:
|
|
342
|
+
# Determine search scope
|
|
343
|
+
search_scope = "current" if path == "." else "all"
|
|
344
|
+
|
|
345
|
+
result = await self.vector_tool.call(
|
|
346
|
+
tool_ctx.mcp_context,
|
|
347
|
+
query=pattern,
|
|
348
|
+
limit=max_results,
|
|
349
|
+
score_threshold=0.3,
|
|
350
|
+
search_scope=search_scope,
|
|
351
|
+
include_content=True,
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
results = []
|
|
355
|
+
if "Found" in result:
|
|
356
|
+
# Parse vector search results
|
|
357
|
+
lines = result.split("\n")
|
|
358
|
+
current_file = None
|
|
359
|
+
current_score = 0.0
|
|
360
|
+
|
|
361
|
+
for line in lines:
|
|
362
|
+
if "Result" in line and "Score:" in line:
|
|
363
|
+
# Extract score and file
|
|
364
|
+
score_match = re.search(r"Score: ([\d.]+)%", line)
|
|
365
|
+
if score_match:
|
|
366
|
+
current_score = float(score_match.group(1)) / 100.0
|
|
367
|
+
|
|
368
|
+
file_match = re.search(r" - ([^\s]+)$", line)
|
|
369
|
+
if file_match:
|
|
370
|
+
current_file = file_match.group(1)
|
|
371
|
+
|
|
372
|
+
elif current_file and line.strip() and not line.startswith("-"):
|
|
373
|
+
# Content line
|
|
374
|
+
results.append(
|
|
375
|
+
SearchResult(
|
|
376
|
+
file_path=current_file,
|
|
377
|
+
line_number=None,
|
|
378
|
+
content=line.strip()[:200], # Limit content length
|
|
379
|
+
search_type=SearchType.VECTOR,
|
|
380
|
+
score=current_score,
|
|
381
|
+
)
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
if len(results) >= max_results:
|
|
385
|
+
break
|
|
386
|
+
|
|
387
|
+
await tool_ctx.info(f"Vector search found {len(results)} results")
|
|
388
|
+
return results
|
|
389
|
+
|
|
390
|
+
except Exception as e:
|
|
391
|
+
await tool_ctx.error(f"Vector search failed: {e}")
|
|
392
|
+
return []
|
|
393
|
+
|
|
394
|
+
async def _run_git_search(self, pattern: str, path: str, tool_ctx, max_results: int) -> List[SearchResult]:
|
|
395
|
+
"""Run git history search."""
|
|
396
|
+
try:
|
|
397
|
+
# Search in both content and commits
|
|
398
|
+
tasks = [
|
|
399
|
+
self.git_search_tool.call(
|
|
400
|
+
tool_ctx.mcp_context,
|
|
401
|
+
pattern=pattern,
|
|
402
|
+
path=path,
|
|
403
|
+
search_type="content",
|
|
404
|
+
max_count=max_results // 2,
|
|
405
|
+
),
|
|
406
|
+
self.git_search_tool.call(
|
|
407
|
+
tool_ctx.mcp_context,
|
|
408
|
+
pattern=pattern,
|
|
409
|
+
path=path,
|
|
410
|
+
search_type="commits",
|
|
411
|
+
max_count=max_results // 2,
|
|
412
|
+
),
|
|
413
|
+
]
|
|
414
|
+
|
|
415
|
+
git_results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
416
|
+
|
|
417
|
+
results = []
|
|
418
|
+
for _i, result in enumerate(git_results):
|
|
419
|
+
if isinstance(result, Exception):
|
|
420
|
+
continue
|
|
421
|
+
|
|
422
|
+
if "Found" in result:
|
|
423
|
+
# Parse git results
|
|
424
|
+
lines = result.split("\n")
|
|
425
|
+
for line in lines:
|
|
426
|
+
if ":" in line and line.strip():
|
|
427
|
+
parts = line.split(":", 2)
|
|
428
|
+
if len(parts) >= 2:
|
|
429
|
+
results.append(
|
|
430
|
+
SearchResult(
|
|
431
|
+
file_path=parts[0].strip(),
|
|
432
|
+
line_number=None,
|
|
433
|
+
content=(parts[-1].strip() if len(parts) > 2 else line),
|
|
434
|
+
search_type=SearchType.GIT,
|
|
435
|
+
score=0.8, # Good score for git matches
|
|
436
|
+
)
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
if len(results) >= max_results:
|
|
440
|
+
break
|
|
441
|
+
|
|
442
|
+
await tool_ctx.info(f"Git search found {len(results)} results")
|
|
443
|
+
return results
|
|
444
|
+
|
|
445
|
+
except Exception as e:
|
|
446
|
+
await tool_ctx.error(f"Git search failed: {e}")
|
|
447
|
+
return []
|
|
448
|
+
|
|
449
|
+
async def _run_symbol_search(self, pattern: str, path: str, tool_ctx, max_results: int) -> List[SearchResult]:
|
|
450
|
+
"""Search for symbol definitions using grep with specific patterns."""
|
|
451
|
+
try:
|
|
452
|
+
# Create patterns for common symbol definitions
|
|
453
|
+
symbol_patterns = [
|
|
454
|
+
f"(def|class|function|func|fn)\\s+{pattern}", # Python, JS, various
|
|
455
|
+
f"(public|private|protected)?\\s*(static)?\\s*\\w+\\s+{pattern}\\s*\\(", # Java/C++
|
|
456
|
+
f"const\\s+{pattern}\\s*=", # JS/TS const
|
|
457
|
+
f"let\\s+{pattern}\\s*=", # JS/TS let
|
|
458
|
+
f"var\\s+{pattern}\\s*=", # JS/TS var
|
|
459
|
+
]
|
|
460
|
+
|
|
461
|
+
# Run grep searches in parallel for each pattern
|
|
462
|
+
tasks = []
|
|
463
|
+
for sp in symbol_patterns:
|
|
464
|
+
tasks.append(self.grep_tool.call(tool_ctx.mcp_context, pattern=sp, path=path, include="*"))
|
|
465
|
+
|
|
466
|
+
grep_results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
467
|
+
|
|
468
|
+
results = []
|
|
469
|
+
for result in grep_results:
|
|
470
|
+
if isinstance(result, Exception):
|
|
471
|
+
continue
|
|
472
|
+
|
|
473
|
+
if "Found" in result and "matches" in result:
|
|
474
|
+
lines = result.split("\n")
|
|
475
|
+
for line in lines[2:]: # Skip header
|
|
476
|
+
if ":" in line and line.strip():
|
|
477
|
+
try:
|
|
478
|
+
parts = line.split(":", 2)
|
|
479
|
+
if len(parts) >= 3:
|
|
480
|
+
results.append(
|
|
481
|
+
SearchResult(
|
|
482
|
+
file_path=parts[0],
|
|
483
|
+
line_number=int(parts[1]),
|
|
484
|
+
content=parts[2].strip(),
|
|
485
|
+
search_type=SearchType.SYMBOL,
|
|
486
|
+
score=0.98, # Very high score for symbol definitions
|
|
487
|
+
)
|
|
488
|
+
)
|
|
489
|
+
if len(results) >= max_results:
|
|
490
|
+
break
|
|
491
|
+
except ValueError:
|
|
492
|
+
continue
|
|
493
|
+
|
|
494
|
+
await tool_ctx.info(f"Symbol search found {len(results)} results")
|
|
495
|
+
return results
|
|
496
|
+
|
|
497
|
+
except Exception as e:
|
|
498
|
+
await tool_ctx.error(f"Symbol search failed: {e}")
|
|
499
|
+
return []
|
|
500
|
+
|
|
501
|
+
def _deduplicate_results(self, all_results: List[SearchResult]) -> List[SearchResult]:
|
|
502
|
+
"""Deduplicate results, keeping the highest scoring version."""
|
|
503
|
+
seen = {}
|
|
504
|
+
|
|
505
|
+
for result in all_results:
|
|
506
|
+
key = (result.file_path, result.line_number)
|
|
507
|
+
|
|
508
|
+
if key not in seen or result.score > seen[key].score:
|
|
509
|
+
seen[key] = result
|
|
510
|
+
elif key in seen and result.context and not seen[key].context:
|
|
511
|
+
# Add context if missing
|
|
512
|
+
seen[key].context = result.context
|
|
513
|
+
|
|
514
|
+
return list(seen.values())
|
|
515
|
+
|
|
516
|
+
def _rank_results(self, results: List[SearchResult]) -> List[SearchResult]:
|
|
517
|
+
"""Rank results by relevance score and search type priority."""
|
|
518
|
+
# Define search type priorities
|
|
519
|
+
type_priority = {
|
|
520
|
+
SearchType.SYMBOL: 5,
|
|
521
|
+
SearchType.GREP: 4,
|
|
522
|
+
SearchType.GREP_AST: 3,
|
|
523
|
+
SearchType.GIT: 2,
|
|
524
|
+
SearchType.VECTOR: 1,
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
# Sort by score (descending) and then by type priority
|
|
528
|
+
results.sort(key=lambda r: (r.score, type_priority.get(r.search_type, 0)), reverse=True)
|
|
529
|
+
|
|
530
|
+
return results
|
|
531
|
+
|
|
532
|
+
@override
|
|
533
|
+
@auto_timeout("search")
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
async def call(
|
|
537
|
+
self,
|
|
538
|
+
ctx: MCPContext,
|
|
539
|
+
**params: Unpack[UnifiedSearchParams],
|
|
540
|
+
) -> str:
|
|
541
|
+
"""Execute search across all enabled search types."""
|
|
542
|
+
import time
|
|
543
|
+
|
|
544
|
+
start_time = time.time()
|
|
545
|
+
|
|
546
|
+
tool_ctx = self.create_tool_context(ctx)
|
|
547
|
+
|
|
548
|
+
# Extract parameters
|
|
549
|
+
pattern = params["pattern"]
|
|
550
|
+
path = params.get("path", ".")
|
|
551
|
+
include = params.get("include", "*")
|
|
552
|
+
max_results = params.get("max_results", 50)
|
|
553
|
+
include_context = params.get("include_context", True)
|
|
554
|
+
|
|
555
|
+
# Validate path
|
|
556
|
+
path_validation = self.validate_path(path)
|
|
557
|
+
if path_validation.is_error:
|
|
558
|
+
await tool_ctx.error(path_validation.error_message)
|
|
559
|
+
return f"Error: {path_validation.error_message}"
|
|
560
|
+
|
|
561
|
+
# Check permissions
|
|
562
|
+
allowed, error_msg = await self.check_path_allowed(path, tool_ctx)
|
|
563
|
+
if not allowed:
|
|
564
|
+
return error_msg
|
|
565
|
+
|
|
566
|
+
# Check existence
|
|
567
|
+
exists, error_msg = await self.check_path_exists(path, tool_ctx)
|
|
568
|
+
if not exists:
|
|
569
|
+
return error_msg
|
|
570
|
+
|
|
571
|
+
# Analyze pattern to determine best search strategies
|
|
572
|
+
pattern_analysis = self._analyze_pattern(pattern)
|
|
573
|
+
|
|
574
|
+
await tool_ctx.info(f"Starting search for '{pattern}' in {path}")
|
|
575
|
+
|
|
576
|
+
# Build list of search tasks based on enabled types and pattern analysis
|
|
577
|
+
search_tasks = []
|
|
578
|
+
search_names = []
|
|
579
|
+
|
|
580
|
+
if params.get("enable_grep", True) and pattern_analysis["use_grep"]:
|
|
581
|
+
search_tasks.append(self._run_grep_search(pattern, path, include, tool_ctx, max_results))
|
|
582
|
+
search_names.append("grep")
|
|
583
|
+
|
|
584
|
+
if params.get("enable_grep_ast", True) and pattern_analysis["use_grep_ast"]:
|
|
585
|
+
search_tasks.append(self._run_grep_ast_search(pattern, path, tool_ctx, max_results))
|
|
586
|
+
search_names.append("grep_ast")
|
|
587
|
+
|
|
588
|
+
if params.get("enable_vector", True) and self.vector_tool and pattern_analysis["use_vector"]:
|
|
589
|
+
search_tasks.append(self._run_vector_search(pattern, path, tool_ctx, max_results))
|
|
590
|
+
search_names.append("vector")
|
|
591
|
+
|
|
592
|
+
if params.get("enable_git", True) and pattern_analysis["use_git"]:
|
|
593
|
+
search_tasks.append(self._run_git_search(pattern, path, tool_ctx, max_results))
|
|
594
|
+
search_names.append("git")
|
|
595
|
+
|
|
596
|
+
if params.get("enable_symbol", True) and pattern_analysis["use_symbol"]:
|
|
597
|
+
search_tasks.append(self._run_symbol_search(pattern, path, tool_ctx, max_results))
|
|
598
|
+
search_names.append("symbol")
|
|
599
|
+
|
|
600
|
+
await tool_ctx.info(f"Running {len(search_tasks)} search types in parallel: {', '.join(search_names)}")
|
|
601
|
+
|
|
602
|
+
# Run all searches in parallel
|
|
603
|
+
search_results = await asyncio.gather(*search_tasks, return_exceptions=True)
|
|
604
|
+
|
|
605
|
+
# Collect all results
|
|
606
|
+
all_results = []
|
|
607
|
+
results_by_type = {}
|
|
608
|
+
|
|
609
|
+
for search_type, results in zip(search_names, search_results):
|
|
610
|
+
if isinstance(results, Exception):
|
|
611
|
+
await tool_ctx.error(f"{search_type} search failed: {results}")
|
|
612
|
+
results_by_type[search_type] = []
|
|
613
|
+
else:
|
|
614
|
+
results_by_type[search_type] = results
|
|
615
|
+
all_results.extend(results)
|
|
616
|
+
|
|
617
|
+
# Deduplicate and rank results
|
|
618
|
+
unique_results = self._deduplicate_results(all_results)
|
|
619
|
+
ranked_results = self._rank_results(unique_results)
|
|
620
|
+
|
|
621
|
+
# Limit total results
|
|
622
|
+
final_results = ranked_results[:max_results]
|
|
623
|
+
|
|
624
|
+
# Calculate search time
|
|
625
|
+
search_time = (time.time() - start_time) * 1000
|
|
626
|
+
|
|
627
|
+
# Format output
|
|
628
|
+
return self._format_results(
|
|
629
|
+
pattern=pattern,
|
|
630
|
+
results=final_results,
|
|
631
|
+
results_by_type=results_by_type,
|
|
632
|
+
search_time_ms=search_time,
|
|
633
|
+
include_context=include_context,
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
def _format_results(
|
|
637
|
+
self,
|
|
638
|
+
pattern: str,
|
|
639
|
+
results: List[SearchResult],
|
|
640
|
+
results_by_type: Dict[str, List[SearchResult]],
|
|
641
|
+
search_time_ms: float,
|
|
642
|
+
include_context: bool,
|
|
643
|
+
) -> str:
|
|
644
|
+
"""Format search results for display."""
|
|
645
|
+
output = []
|
|
646
|
+
|
|
647
|
+
# Header
|
|
648
|
+
output.append(f"=== Unified Search Results ===")
|
|
649
|
+
output.append(f"Pattern: '{pattern}'")
|
|
650
|
+
output.append(f"Total results: {len(results)}")
|
|
651
|
+
output.append(f"Search time: {search_time_ms:.1f}ms")
|
|
652
|
+
|
|
653
|
+
# Summary by type
|
|
654
|
+
output.append("\nResults by type:")
|
|
655
|
+
for search_type, type_results in results_by_type.items():
|
|
656
|
+
if type_results:
|
|
657
|
+
output.append(f" {search_type}: {len(type_results)} matches")
|
|
658
|
+
|
|
659
|
+
if not results:
|
|
660
|
+
output.append("\nNo results found.")
|
|
661
|
+
return "\n".join(output)
|
|
662
|
+
|
|
663
|
+
# Group results by file
|
|
664
|
+
results_by_file = {}
|
|
665
|
+
for result in results:
|
|
666
|
+
if result.file_path not in results_by_file:
|
|
667
|
+
results_by_file[result.file_path] = []
|
|
668
|
+
results_by_file[result.file_path].append(result)
|
|
669
|
+
|
|
670
|
+
# Display results
|
|
671
|
+
output.append(f"\n=== Results ({len(results)} total) ===\n")
|
|
672
|
+
|
|
673
|
+
for file_path, file_results in results_by_file.items():
|
|
674
|
+
output.append(f"{file_path}")
|
|
675
|
+
output.append("-" * len(file_path))
|
|
676
|
+
|
|
677
|
+
# Sort by line number
|
|
678
|
+
file_results.sort(key=lambda r: r.line_number or 0)
|
|
679
|
+
|
|
680
|
+
for result in file_results:
|
|
681
|
+
# Format result line
|
|
682
|
+
score_str = f"[{result.search_type.value} {result.score:.2f}]"
|
|
683
|
+
|
|
684
|
+
if result.line_number:
|
|
685
|
+
output.append(f" {result.line_number:>4}: {score_str} {result.content}")
|
|
686
|
+
else:
|
|
687
|
+
output.append(f" {score_str} {result.content}")
|
|
688
|
+
|
|
689
|
+
# Add context if available and requested
|
|
690
|
+
if include_context and result.context:
|
|
691
|
+
output.append(f" Context: {result.context}")
|
|
692
|
+
|
|
693
|
+
output.append("") # Empty line between files
|
|
694
|
+
|
|
695
|
+
return "\n".join(output)
|
|
696
|
+
|
|
697
|
+
@override
|
|
698
|
+
def register(self, mcp_server: FastMCP) -> None:
|
|
699
|
+
"""Register the search tool with the MCP server."""
|
|
700
|
+
tool_self = self
|
|
701
|
+
|
|
702
|
+
@mcp_server.tool(name=self.name, description=self.description)
|
|
703
|
+
async def search(
|
|
704
|
+
ctx: MCPContext,
|
|
705
|
+
pattern: Pattern,
|
|
706
|
+
path: SearchPath = ".",
|
|
707
|
+
include: Include = "*",
|
|
708
|
+
max_results: MaxResults = 50,
|
|
709
|
+
enable_grep: EnableGrep = True,
|
|
710
|
+
enable_grep_ast: EnableGrepAst = True,
|
|
711
|
+
enable_vector: EnableVector = True,
|
|
712
|
+
enable_git: EnableGit = True,
|
|
713
|
+
enable_symbol: EnableSymbol = True,
|
|
714
|
+
include_context: IncludeContext = True,
|
|
715
|
+
) -> str:
|
|
716
|
+
return await tool_self.call(
|
|
717
|
+
ctx,
|
|
718
|
+
pattern=pattern,
|
|
719
|
+
path=path,
|
|
720
|
+
include=include,
|
|
721
|
+
max_results=max_results,
|
|
722
|
+
enable_grep=enable_grep,
|
|
723
|
+
enable_grep_ast=enable_grep_ast,
|
|
724
|
+
enable_vector=enable_vector,
|
|
725
|
+
enable_git=enable_git,
|
|
726
|
+
enable_symbol=enable_symbol,
|
|
727
|
+
include_context=include_context,
|
|
728
|
+
)
|