hanzo-mcp 0.8.8__py3-none-any.whl → 0.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of hanzo-mcp might be problematic. Click here for more details.

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