hanzo-mcp 0.5.1__py3-none-any.whl → 0.5.2__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 (54) hide show
  1. hanzo_mcp/__init__.py +1 -1
  2. hanzo_mcp/tools/__init__.py +135 -4
  3. hanzo_mcp/tools/common/base.py +7 -2
  4. hanzo_mcp/tools/common/stats.py +261 -0
  5. hanzo_mcp/tools/common/tool_disable.py +144 -0
  6. hanzo_mcp/tools/common/tool_enable.py +182 -0
  7. hanzo_mcp/tools/common/tool_list.py +263 -0
  8. hanzo_mcp/tools/database/__init__.py +71 -0
  9. hanzo_mcp/tools/database/database_manager.py +246 -0
  10. hanzo_mcp/tools/database/graph_add.py +257 -0
  11. hanzo_mcp/tools/database/graph_query.py +536 -0
  12. hanzo_mcp/tools/database/graph_remove.py +267 -0
  13. hanzo_mcp/tools/database/graph_search.py +348 -0
  14. hanzo_mcp/tools/database/graph_stats.py +345 -0
  15. hanzo_mcp/tools/database/sql_query.py +229 -0
  16. hanzo_mcp/tools/database/sql_search.py +296 -0
  17. hanzo_mcp/tools/database/sql_stats.py +254 -0
  18. hanzo_mcp/tools/editor/__init__.py +11 -0
  19. hanzo_mcp/tools/editor/neovim_command.py +272 -0
  20. hanzo_mcp/tools/editor/neovim_edit.py +290 -0
  21. hanzo_mcp/tools/editor/neovim_session.py +356 -0
  22. hanzo_mcp/tools/filesystem/__init__.py +15 -5
  23. hanzo_mcp/tools/filesystem/{unified_search.py → batch_search.py} +254 -131
  24. hanzo_mcp/tools/filesystem/find_files.py +348 -0
  25. hanzo_mcp/tools/filesystem/git_search.py +505 -0
  26. hanzo_mcp/tools/llm/__init__.py +27 -0
  27. hanzo_mcp/tools/llm/consensus_tool.py +351 -0
  28. hanzo_mcp/tools/llm/llm_manage.py +413 -0
  29. hanzo_mcp/tools/llm/llm_tool.py +346 -0
  30. hanzo_mcp/tools/llm/provider_tools.py +412 -0
  31. hanzo_mcp/tools/mcp/__init__.py +11 -0
  32. hanzo_mcp/tools/mcp/mcp_add.py +263 -0
  33. hanzo_mcp/tools/mcp/mcp_remove.py +127 -0
  34. hanzo_mcp/tools/mcp/mcp_stats.py +165 -0
  35. hanzo_mcp/tools/shell/__init__.py +27 -7
  36. hanzo_mcp/tools/shell/logs.py +265 -0
  37. hanzo_mcp/tools/shell/npx.py +194 -0
  38. hanzo_mcp/tools/shell/npx_background.py +254 -0
  39. hanzo_mcp/tools/shell/pkill.py +262 -0
  40. hanzo_mcp/tools/shell/processes.py +279 -0
  41. hanzo_mcp/tools/shell/run_background.py +326 -0
  42. hanzo_mcp/tools/shell/uvx.py +187 -0
  43. hanzo_mcp/tools/shell/uvx_background.py +249 -0
  44. hanzo_mcp/tools/vector/__init__.py +5 -0
  45. hanzo_mcp/tools/vector/git_ingester.py +3 -0
  46. hanzo_mcp/tools/vector/index_tool.py +358 -0
  47. hanzo_mcp/tools/vector/infinity_store.py +98 -0
  48. hanzo_mcp/tools/vector/vector_search.py +11 -6
  49. {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.5.2.dist-info}/METADATA +1 -1
  50. {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.5.2.dist-info}/RECORD +54 -16
  51. {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.5.2.dist-info}/WHEEL +0 -0
  52. {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.5.2.dist-info}/entry_points.txt +0 -0
  53. {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.5.2.dist-info}/licenses/LICENSE +0 -0
  54. {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.5.2.dist-info}/top_level.txt +0 -0
@@ -1,10 +1,14 @@
1
- """Unified search tool that combines grep, vector, AST, and semantic search.
1
+ """Batch search tool that runs multiple search queries in parallel.
2
2
 
3
- This tool provides an intelligent multi-search approach that:
4
- 1. Always starts with fast grep/regex search
5
- 2. Enhances with vector similarity, AST context, and symbol search
6
- 3. Returns comprehensive results with function/method context
7
- 4. Optimizes performance through intelligent caching and batching
3
+ This tool allows running multiple searches of different types concurrently:
4
+ - grep: Fast pattern/regex search
5
+ - grep_ast: AST-aware code search
6
+ - vector_search: Semantic similarity search
7
+ - git_search: Search through git history
8
+
9
+ Results are combined and ranked for comprehensive search coverage.
10
+ Perfect for complex research and refactoring tasks where you need
11
+ to find all occurrences across different dimensions.
8
12
  """
9
13
 
10
14
  import asyncio
@@ -23,6 +27,7 @@ from typing_extensions import Annotated, TypedDict, Unpack, final, override
23
27
  from hanzo_mcp.tools.filesystem.base import FilesystemBaseTool
24
28
  from hanzo_mcp.tools.filesystem.grep import Grep
25
29
  from hanzo_mcp.tools.filesystem.grep_ast_tool import GrepAstTool
30
+ from hanzo_mcp.tools.filesystem.git_search import GitSearchTool
26
31
  from hanzo_mcp.tools.vector.vector_search import VectorSearchTool
27
32
  from hanzo_mcp.tools.vector.ast_analyzer import ASTAnalyzer, Symbol
28
33
  from hanzo_mcp.tools.common.permissions import PermissionManager
@@ -35,6 +40,7 @@ class SearchType(Enum):
35
40
  VECTOR = "vector"
36
41
  AST = "ast"
37
42
  SYMBOL = "symbol"
43
+ GIT = "git"
38
44
 
39
45
 
40
46
  @dataclass
@@ -59,8 +65,8 @@ class SearchResult:
59
65
 
60
66
 
61
67
  @dataclass
62
- class UnifiedSearchResults:
63
- """Container for all unified search results."""
68
+ class BatchSearchResults:
69
+ """Container for all batch search results."""
64
70
  query: str
65
71
  total_results: int
66
72
  results_by_type: Dict[SearchType, List[SearchResult]]
@@ -78,30 +84,34 @@ class UnifiedSearchResults:
78
84
  }
79
85
 
80
86
 
81
- Pattern = Annotated[str, Field(description="The search pattern/query", min_length=1)]
87
+ Queries = Annotated[List[Dict[str, Any]], Field(description="List of search queries with types", min_items=1)]
82
88
  SearchPath = Annotated[str, Field(description="Path to search in", default=".")]
83
89
  Include = Annotated[str, Field(description="File pattern to include", default="*")]
84
- MaxResults = Annotated[int, Field(description="Maximum results per search type", default=20)]
85
- EnableVector = Annotated[bool, Field(description="Enable vector/semantic search", default=True)]
86
- EnableAST = Annotated[bool, Field(description="Enable AST context search", default=True)]
87
- EnableSymbol = Annotated[bool, Field(description="Enable symbol search", default=True)]
90
+ MaxResults = Annotated[int, Field(description="Maximum results per query", default=20)]
88
91
  IncludeContext = Annotated[bool, Field(description="Include function/method context", default=True)]
92
+ CombineResults = Annotated[bool, Field(description="Combine and deduplicate results", default=True)]
89
93
 
90
94
 
91
- class UnifiedSearchParams(TypedDict):
92
- """Parameters for unified search."""
93
- pattern: Pattern
94
- path: SearchPath
95
+ class BatchSearchParams(TypedDict):
96
+ """Parameters for batch search.
97
+
98
+ queries format: [
99
+ {"type": "grep", "pattern": "TODO"},
100
+ {"type": "vector_search", "query": "error handling"},
101
+ {"type": "grep_ast", "pattern": "def.*test"},
102
+ {"type": "git_search", "pattern": "bug fix", "search_type": "commits"}
103
+ ]
104
+ """
105
+ queries: Queries
106
+ path: SearchPath
95
107
  include: Include
96
108
  max_results: MaxResults
97
- enable_vector: EnableVector
98
- enable_ast: EnableAST
99
- enable_symbol: EnableSymbol
100
109
  include_context: IncludeContext
110
+ combine_results: CombineResults
101
111
 
102
112
 
103
113
  @final
104
- class UnifiedSearchTool(FilesystemBaseTool):
114
+ class BatchSearchTool(FilesystemBaseTool):
105
115
  """Unified search tool combining multiple search strategies."""
106
116
 
107
117
  def __init__(self, permission_manager: PermissionManager,
@@ -113,6 +123,7 @@ class UnifiedSearchTool(FilesystemBaseTool):
113
123
  # Initialize component search tools
114
124
  self.grep_tool = Grep(permission_manager)
115
125
  self.grep_ast_tool = GrepAstTool(permission_manager)
126
+ self.git_search_tool = GitSearchTool(permission_manager)
116
127
  self.ast_analyzer = ASTAnalyzer()
117
128
 
118
129
  # Vector search is optional
@@ -128,23 +139,27 @@ class UnifiedSearchTool(FilesystemBaseTool):
128
139
  @override
129
140
  def name(self) -> str:
130
141
  """Get the tool name."""
131
- return "unified_search"
142
+ return "batch_search"
132
143
 
133
144
  @property
134
145
  @override
135
146
  def description(self) -> str:
136
147
  """Get the tool description."""
137
- return """Intelligent unified search combining grep, vector similarity, AST context, and symbol search.
148
+ return """Run multiple search queries in parallel across different search types.
149
+
150
+ Supports running concurrent searches:
151
+ - Multiple grep patterns
152
+ - Multiple vector queries
153
+ - Multiple AST searches
154
+ - Combined with git history search
138
155
 
139
- This tool provides the most comprehensive search experience by:
140
- 1. Starting with fast grep/regex search for immediate results
141
- 2. Enhancing with vector similarity for semantic matches
142
- 3. Adding AST context to show structural information
143
- 4. Including symbol search for code definitions
144
- 5. Providing function/method body context when relevant
156
+ Examples:
157
+ - Search for 'config' in code + 'configuration' in docs + 'CONFIG' in constants
158
+ - Find all references to a function across code, comments, and git history
159
+ - Search for concept across different naming conventions
145
160
 
146
- The tool intelligently combines results and provides relevance scoring across all search types.
147
- Use this when you need comprehensive search results or aren't sure which search type is best."""
161
+ Results are intelligently combined, deduplicated, and ranked by relevance.
162
+ Perfect for comprehensive code analysis and refactoring tasks."""
148
163
 
149
164
  def _detect_search_intent(self, pattern: str) -> Tuple[bool, bool, bool]:
150
165
  """Analyze pattern to determine which search types to enable.
@@ -505,22 +520,20 @@ Use this when you need comprehensive search results or aren't sure which search
505
520
  return all_results
506
521
 
507
522
  @override
508
- async def call(self, ctx: MCPContext, **params: Unpack[UnifiedSearchParams]) -> str:
509
- """Execute unified search with all enabled search types."""
523
+ async def call(self, ctx: MCPContext, **params: Unpack[BatchSearchParams]) -> str:
524
+ """Execute batch search with multiple queries in parallel."""
510
525
  import time
511
526
  start_time = time.time()
512
527
 
513
528
  tool_ctx = self.create_tool_context(ctx)
514
529
 
515
530
  # Extract parameters
516
- pattern = params["pattern"]
531
+ queries = params["queries"]
517
532
  path = params.get("path", ".")
518
533
  include = params.get("include", "*")
519
534
  max_results = params.get("max_results", 20)
520
- enable_vector = params.get("enable_vector", True)
521
- enable_ast = params.get("enable_ast", True)
522
- enable_symbol = params.get("enable_symbol", True)
523
535
  include_context = params.get("include_context", True)
536
+ combine_results = params.get("combine_results", True)
524
537
 
525
538
  # Validate path
526
539
  path_validation = self.validate_path(path)
@@ -537,127 +550,237 @@ Use this when you need comprehensive search results or aren't sure which search
537
550
  if not exists:
538
551
  return error_msg
539
552
 
540
- # Analyze search intent to optimize which searches to run
541
- should_vector, should_ast, should_symbol = self._detect_search_intent(pattern)
542
- enable_vector = enable_vector and should_vector
543
- enable_ast = enable_ast and should_ast
544
- enable_symbol = enable_symbol and should_symbol
553
+ await tool_ctx.info(f"Starting batch search with {len(queries)} queries in {path}")
545
554
 
546
- await tool_ctx.info(f"Starting unified search for '{pattern}' in {path}")
547
- await tool_ctx.info(f"Enabled searches: grep=True vector={enable_vector} ast={enable_ast} symbol={enable_symbol}")
548
-
549
- # Run searches in parallel for maximum efficiency
555
+ # Run all queries in parallel
550
556
  search_tasks = []
557
+ query_info = [] # Track query info for results
551
558
 
552
- # Always run grep first (fastest, most reliable)
553
- search_tasks.append(
554
- self._run_grep_search(pattern, path, include, tool_ctx, max_results)
555
- )
556
-
557
- if enable_vector and self.vector_tool:
558
- search_tasks.append(
559
- self._run_vector_search(pattern, path, tool_ctx, max_results)
560
- )
561
-
562
- if enable_ast:
563
- search_tasks.append(
564
- self._run_ast_search(pattern, path, include, tool_ctx, max_results)
565
- )
566
-
567
- if enable_symbol:
568
- search_tasks.append(
569
- self._run_symbol_search(pattern, path, tool_ctx, max_results)
570
- )
559
+ for query in queries:
560
+ query_type = query.get("type", "grep")
561
+ query_info.append(query)
562
+
563
+ if query_type == "grep":
564
+ pattern = query.get("pattern")
565
+ if pattern:
566
+ search_tasks.append(
567
+ self._run_grep_search(pattern, path, include, tool_ctx, max_results)
568
+ )
569
+
570
+ elif query_type == "grep_ast":
571
+ pattern = query.get("pattern")
572
+ if pattern:
573
+ search_tasks.append(
574
+ self._run_ast_search(pattern, path, include, tool_ctx, max_results)
575
+ )
576
+
577
+ elif query_type == "vector_search" and self.vector_tool:
578
+ search_query = query.get("query") or query.get("pattern")
579
+ if search_query:
580
+ search_tasks.append(
581
+ self._run_vector_search(search_query, path, tool_ctx, max_results)
582
+ )
583
+
584
+ elif query_type == "git_search":
585
+ pattern = query.get("pattern")
586
+ search_type = query.get("search_type", "content")
587
+ if pattern:
588
+ search_tasks.append(
589
+ self._run_git_search(pattern, path, search_type, tool_ctx, max_results)
590
+ )
591
+
592
+ else:
593
+ await tool_ctx.warning(f"Unknown or unavailable search type: {query_type}")
571
594
 
572
595
  # Execute all searches in parallel
573
596
  search_results = await asyncio.gather(*search_tasks, return_exceptions=True)
574
597
 
575
- # Organize results by type
576
- results_by_type = {}
577
- search_types = [SearchType.GREP]
578
- if enable_vector and self.vector_tool:
579
- search_types.append(SearchType.VECTOR)
580
- if enable_ast:
581
- search_types.append(SearchType.AST)
582
- if enable_symbol:
583
- search_types.append(SearchType.SYMBOL)
584
-
585
- for i, result in enumerate(search_results):
598
+ # Collect all results
599
+ all_results = []
600
+ results_by_query = {}
601
+
602
+ for i, (query, result) in enumerate(zip(query_info, search_results)):
586
603
  if isinstance(result, Exception):
587
- await tool_ctx.error(f"Search failed: {str(result)}")
588
- continue
589
-
590
- search_type = search_types[i]
591
- results_by_type[search_type] = result
604
+ await tool_ctx.error(f"Query {i+1} failed: {str(result)}")
605
+ results_by_query[i] = []
606
+ else:
607
+ results_by_query[i] = result
608
+ all_results.extend(result)
592
609
 
593
- # Add function context if requested
594
- if include_context:
595
- for search_type, results in results_by_type.items():
596
- if results:
597
- results_by_type[search_type] = await self._add_function_context(results, tool_ctx)
610
+ # Combine and deduplicate results if requested
611
+ if combine_results:
612
+ combined_results = self._combine_results(all_results)
613
+ else:
614
+ combined_results = all_results
598
615
 
599
- # Combine and rank all results
600
- combined_results = self._combine_and_rank_results(results_by_type)
616
+ # Add context if requested
617
+ if include_context:
618
+ combined_results = await self._add_context_to_results(combined_results, tool_ctx)
601
619
 
602
620
  end_time = time.time()
603
621
  search_time_ms = (end_time - start_time) * 1000
604
622
 
605
- # Create unified results object
606
- unified_results = UnifiedSearchResults(
607
- query=pattern,
623
+ # Sort by relevance score
624
+ combined_results.sort(key=lambda r: r.score, reverse=True)
625
+
626
+ # Limit total results
627
+ combined_results = combined_results[:max_results * 2] # Allow more when combining
628
+
629
+ # Create batch results object
630
+ batch_results = BatchSearchResults(
631
+ query=f"Batch search with {len(queries)} queries",
608
632
  total_results=len(combined_results),
609
- results_by_type=results_by_type,
610
- combined_results=combined_results[:max_results * 2], # Allow some extra for variety
633
+ results_by_type={
634
+ SearchType.GREP: [r for r in combined_results if r.search_type == SearchType.GREP],
635
+ SearchType.VECTOR: [r for r in combined_results if r.search_type == SearchType.VECTOR],
636
+ SearchType.AST: [r for r in combined_results if r.search_type == SearchType.AST],
637
+ SearchType.GIT: [r for r in combined_results if r.search_type == SearchType.GIT],
638
+ },
639
+ combined_results=combined_results,
611
640
  search_time_ms=search_time_ms
612
641
  )
613
642
 
614
643
  # Format output
615
- return self._format_unified_results(unified_results)
644
+ return self._format_batch_results(batch_results, query_info)
616
645
 
617
- def _format_unified_results(self, results: UnifiedSearchResults) -> str:
618
- """Format unified search results for display."""
619
- if results.total_results == 0:
620
- return f"No results found for query: '{results.query}'"
621
-
622
- lines = [
623
- f"Unified Search Results for '{results.query}' ({results.search_time_ms:.1f}ms)",
624
- f"Found {results.total_results} total results across {len(results.results_by_type)} search types",
625
- ""
626
- ]
627
-
628
- # Show summary by type
629
- for search_type, type_results in results.results_by_type.items():
630
- if type_results:
631
- lines.append(f"• {search_type.value.title()}: {len(type_results)} results")
632
- lines.append("")
646
+ async def _run_git_search(self, pattern: str, path: str, search_type: str,
647
+ tool_ctx, max_results: int) -> List[SearchResult]:
648
+ """Run git search and convert results."""
649
+ await tool_ctx.info(f"Running git search for: {pattern} (type: {search_type})")
633
650
 
634
- # Show top combined results
635
- lines.append("=== Top Results (Combined & Ranked) ===")
636
- for i, result in enumerate(results.combined_results[:20], 1):
637
- score_display = f"{result.score:.2f}" if result.score < 1.0 else "1.00"
638
-
639
- header = f"Result {i} [{result.search_type.value}] (Score: {score_display})"
640
- if result.line_number:
641
- header += f" - {result.file_path}:{result.line_number}"
642
- else:
643
- header += f" - {result.file_path}"
651
+ try:
652
+ # Use the git search tool
653
+ git_result = await self.git_search_tool.call(
654
+ tool_ctx.mcp_context,
655
+ pattern=pattern,
656
+ path=path,
657
+ search_type=search_type,
658
+ max_count=max_results
659
+ )
644
660
 
645
- lines.append(header)
646
- lines.append("-" * len(header))
661
+ results = []
662
+ if "Found" in git_result:
663
+ # Parse git search results - simplified parser
664
+ lines = git_result.split('\n')
665
+ current_file = None
666
+
667
+ for line in lines:
668
+ if line.strip():
669
+ # Extract file path and content
670
+ if ':' in line:
671
+ parts = line.split(':', 2)
672
+ if len(parts) >= 2:
673
+ file_path = parts[0].strip()
674
+ content = parts[-1].strip() if len(parts) > 2 else line
675
+
676
+ result = SearchResult(
677
+ file_path=file_path,
678
+ line_number=None,
679
+ content=content,
680
+ search_type=SearchType.GIT,
681
+ score=0.8, # Git results are relevant
682
+ )
683
+ results.append(result)
684
+
685
+ if len(results) >= max_results:
686
+ break
647
687
 
648
- if result.context:
649
- lines.append(f"Context: {result.context}")
688
+ await tool_ctx.info(f"Git search found {len(results)} results")
689
+ return results
690
+
691
+ except Exception as e:
692
+ await tool_ctx.error(f"Git search failed: {str(e)}")
693
+ return []
694
+
695
+ def _combine_results(self, results: List[SearchResult]) -> List[SearchResult]:
696
+ """Combine and deduplicate search results."""
697
+ # Use file path and line number as key for deduplication
698
+ seen = {}
699
+ combined = []
700
+
701
+ for result in results:
702
+ key = (result.file_path, result.line_number)
650
703
 
651
- lines.append(f"Content: {result.content}")
704
+ if key not in seen:
705
+ seen[key] = result
706
+ combined.append(result)
707
+ else:
708
+ # If we've seen this location, keep the one with higher score
709
+ existing = seen[key]
710
+ if result.score > existing.score:
711
+ # Replace with higher scored result
712
+ idx = combined.index(existing)
713
+ combined[idx] = result
714
+ seen[key] = result
715
+
716
+ return combined
717
+
718
+ async def _add_context_to_results(self, results: List[SearchResult], tool_ctx) -> List[SearchResult]:
719
+ """Add function/method context to results."""
720
+ # This is a simplified version - you could enhance with full AST context
721
+ return await self._add_function_context(results, tool_ctx)
722
+
723
+ def _format_batch_results(self, results: BatchSearchResults, query_info: List[Dict]) -> str:
724
+ """Format batch search results for display."""
725
+ output = []
726
+
727
+ # Header
728
+ output.append(f"=== Batch Search Results ===")
729
+ output.append(f"Queries: {len(query_info)}")
730
+ output.append(f"Total results: {results.total_results}")
731
+ output.append(f"Search time: {results.search_time_ms:.1f}ms\n")
732
+
733
+ # Summary by type
734
+ output.append("Results by type:")
735
+ for search_type, type_results in results.results_by_type.items():
736
+ if type_results:
737
+ output.append(f" {search_type.value}: {len(type_results)} results")
738
+ output.append("")
739
+
740
+ # Query summary
741
+ output.append("Queries executed:")
742
+ for i, query in enumerate(query_info):
743
+ query_type = query.get("type", "grep")
744
+ pattern = query.get("pattern") or query.get("query", "")
745
+ output.append(f" {i+1}. {query_type}: {pattern}")
746
+ output.append("")
747
+
748
+ # Results
749
+ if results.combined_results:
750
+ output.append("=== Top Results ===\n")
652
751
 
653
- if result.symbol_info:
654
- lines.append(f"Symbol: {result.symbol_info.type} {result.symbol_info.name}")
655
- if result.symbol_info.signature:
656
- lines.append(f"Signature: {result.symbol_info.signature}")
752
+ # Group by file
753
+ results_by_file = {}
754
+ for result in results.combined_results[:50]: # Limit display
755
+ if result.file_path not in results_by_file:
756
+ results_by_file[result.file_path] = []
757
+ results_by_file[result.file_path].append(result)
657
758
 
658
- lines.append("")
759
+ # Display results by file
760
+ for file_path, file_results in results_by_file.items():
761
+ output.append(f"{file_path}")
762
+ output.append("-" * len(file_path))
763
+
764
+ # Sort by line number if available
765
+ file_results.sort(key=lambda r: r.line_number or 0)
766
+
767
+ for result in file_results:
768
+ score_str = f"[{result.search_type.value} {result.score:.2f}]"
769
+
770
+ if result.line_number:
771
+ output.append(f" {result.line_number:>4}: {score_str} {result.content}")
772
+ else:
773
+ output.append(f" {score_str} {result.content}")
774
+
775
+ if result.context:
776
+ output.append(f" Context: {result.context}")
777
+
778
+ output.append("")
779
+ else:
780
+ output.append("No results found.")
659
781
 
660
- return "\n".join(lines)
782
+ return "\n".join(output)
783
+
661
784
 
662
785
  @override
663
786
  def register(self, mcp_server: FastMCP) -> None: