mirror of
https://github.com/hanzoai/mcp.git
synced 2026-10-29 02:55:49 +00:00
🎨 **Tool Palette System** - Created palette system for organizing development tools by ecosystem - Pre-defined palettes: Python (Guido), Ruby (Matz), JavaScript, Go, Rust, DevOps, Full Stack, Minimal - `palette` tool to list, activate, and manage tool sets - Environment variables applied from active palette - Keeps MCP interface clean while supporting specialized workflows 🔧 **Unified Process Tools with OO Design** - Refactored all process execution to use clean base class hierarchy - `BaseProcessTool`, `BaseBinaryTool`, `BaseScriptTool` for consistent interface - Unified `bash`, `npx`, `uvx` tools with action-based approach (run/background) - `process` tool for managing background processes - All tools use same execution pipeline internally 🐚 **Smart Shell Detection** - Shell tool now detects user's preferred shell from $SHELL environment - Automatically uses zsh if .zshrc exists (perfect for your setup\!) - Falls back to bash if no special configuration found - All zsh aliases, functions, and configurations now available in MCP 🚀 **Development Mode with Hot Reload** - Added `--dev` flag to hanzo-mcp CLI for development mode - File watching with automatic reload detection using watchdog - `hanzo-mcp-dev` command for dedicated dev server - Watches Python files and config changes - Debounced reloading to prevent spam 🛠️ **New Tools Added** - `open`: Opens files/URLs in default application (platform-aware) - `watch`: Monitors files/directories for changes with pattern matching - `diff`: Compares files with unified/context diff formats - `config`: Git-style configuration management 📚 **Documentation & Infrastructure** - Comprehensive Sphinx documentation with black theme - Automated deployment to mcp.hanzo.ai via Cloudflare Pages - GitHub Actions workflow for docs deployment - 25+ documentation pages organized by category ⚙️ **Configuration Improvements** - Tool enable/disable system with palette override - Per-project vs global indexing configuration - Environment variable management through palettes - Clean separation of concerns in tool registration This release provides a much cleaner, more extensible architecture while maintaining backward compatibility. The palette system makes it easy to switch between development environments, and the unified process tools eliminate code duplication while providing consistent behavior. Co-authored-by: Hanzo Dev <[email protected]>
230 lines
9.4 KiB
Python
230 lines
9.4 KiB
Python
"""Vector search tool for semantic document retrieval."""
|
|
|
|
from typing import Dict, List, Optional, TypedDict, Unpack, final
|
|
import json
|
|
import asyncio
|
|
|
|
from mcp.server.fastmcp import Context as MCPContext
|
|
from pydantic import Field
|
|
|
|
from hanzo_mcp.tools.common.base import BaseTool
|
|
from hanzo_mcp.tools.common.permissions import PermissionManager
|
|
|
|
from .infinity_store import InfinityVectorStore
|
|
from .project_manager import ProjectVectorManager
|
|
|
|
|
|
class VectorSearchParams(TypedDict, total=False):
|
|
"""Parameters for vector search operations."""
|
|
|
|
query: str
|
|
limit: Optional[int]
|
|
score_threshold: Optional[float]
|
|
include_content: Optional[bool]
|
|
file_filter: Optional[str]
|
|
project_filter: Optional[List[str]]
|
|
search_scope: Optional[str] # "all", "global", "current", or specific project name
|
|
|
|
|
|
@final
|
|
class VectorSearchTool(BaseTool):
|
|
"""Tool for semantic search in the vector database."""
|
|
|
|
def __init__(self, permission_manager: PermissionManager, project_manager: ProjectVectorManager):
|
|
"""Initialize the vector search tool.
|
|
|
|
Args:
|
|
permission_manager: Permission manager for access control
|
|
project_manager: Project-aware vector store manager
|
|
"""
|
|
self.permission_manager = permission_manager
|
|
self.project_manager = project_manager
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
"""Get the tool name."""
|
|
return "vector_search"
|
|
|
|
@property
|
|
def description(self) -> str:
|
|
"""Get the tool description."""
|
|
return """Pure semantic/vector search using Infinity embedded database.
|
|
|
|
Searches indexed documents using vector embeddings to find semantically similar content.
|
|
This is NOT keyword search - it finds documents based on meaning and context similarity.
|
|
|
|
Features:
|
|
- Searches across project-specific vector databases
|
|
- Returns similarity scores (0-1, higher is better)
|
|
- Supports filtering by project or file
|
|
- Automatically detects projects via LLM.md files
|
|
|
|
Use 'grep' for exact text/pattern matching, 'vector_search' for semantic similarity."""
|
|
|
|
async def call(
|
|
self,
|
|
ctx: MCPContext,
|
|
**params: Unpack[VectorSearchParams],
|
|
) -> str:
|
|
"""Search for similar documents in the vector database.
|
|
|
|
Args:
|
|
ctx: MCP context
|
|
**params: Tool parameters
|
|
|
|
Returns:
|
|
Search results formatted as text
|
|
"""
|
|
query = params.get("query")
|
|
if not query:
|
|
return "Error: query parameter is required"
|
|
|
|
limit = params.get("limit", 10)
|
|
score_threshold = params.get("score_threshold", 0.0)
|
|
include_content = params.get("include_content", True)
|
|
file_filter = params.get("file_filter")
|
|
project_filter = params.get("project_filter")
|
|
search_scope = params.get("search_scope", "all")
|
|
|
|
try:
|
|
# Determine search strategy based on scope
|
|
if search_scope == "all":
|
|
# Search across all projects
|
|
project_results = await self.project_manager.search_all_projects(
|
|
query=query,
|
|
limit_per_project=limit,
|
|
score_threshold=score_threshold,
|
|
include_global=True,
|
|
project_filter=project_filter,
|
|
)
|
|
|
|
# Combine and sort all results
|
|
all_results = []
|
|
for project_name, results in project_results.items():
|
|
for result in results:
|
|
# Add project info to metadata
|
|
result.document.metadata = result.document.metadata or {}
|
|
result.document.metadata["search_project"] = project_name
|
|
all_results.append(result)
|
|
|
|
# Sort by score and limit
|
|
all_results.sort(key=lambda x: x.score, reverse=True)
|
|
results = all_results[:limit]
|
|
|
|
elif search_scope == "global":
|
|
# Search only global store
|
|
global_store = self.project_manager._get_global_store()
|
|
results = global_store.search(
|
|
query=query,
|
|
limit=limit,
|
|
score_threshold=score_threshold,
|
|
)
|
|
for result in results:
|
|
result.document.metadata = result.document.metadata or {}
|
|
result.document.metadata["search_project"] = "global"
|
|
|
|
else:
|
|
# Search specific project or current context
|
|
if search_scope != "current":
|
|
# Search specific project by name
|
|
project_info = None
|
|
for proj_key, proj_info in self.project_manager.projects.items():
|
|
if proj_info.name == search_scope:
|
|
project_info = proj_info
|
|
break
|
|
|
|
if project_info:
|
|
vector_store = self.project_manager.get_vector_store(project_info)
|
|
results = vector_store.search(
|
|
query=query,
|
|
limit=limit,
|
|
score_threshold=score_threshold,
|
|
)
|
|
for result in results:
|
|
result.document.metadata = result.document.metadata or {}
|
|
result.document.metadata["search_project"] = project_info.name
|
|
else:
|
|
return f"Project '{search_scope}' not found"
|
|
else:
|
|
# For "current", try to detect from working directory
|
|
import os
|
|
current_dir = os.getcwd()
|
|
project_info = self.project_manager.get_project_for_path(current_dir)
|
|
|
|
if project_info:
|
|
vector_store = self.project_manager.get_vector_store(project_info)
|
|
results = vector_store.search(
|
|
query=query,
|
|
limit=limit,
|
|
score_threshold=score_threshold,
|
|
)
|
|
for result in results:
|
|
result.document.metadata = result.document.metadata or {}
|
|
result.document.metadata["search_project"] = project_info.name
|
|
else:
|
|
# Fall back to global store
|
|
global_store = self.project_manager._get_global_store()
|
|
results = global_store.search(
|
|
query=query,
|
|
limit=limit,
|
|
score_threshold=score_threshold,
|
|
)
|
|
for result in results:
|
|
result.document.metadata = result.document.metadata or {}
|
|
result.document.metadata["search_project"] = "global"
|
|
|
|
if not results:
|
|
return f"No results found for query: '{query}'"
|
|
|
|
# Filter by file if requested
|
|
if file_filter:
|
|
results = [r for r in results if file_filter in (r.document.file_path or "")]
|
|
|
|
# Format results
|
|
output_lines = [f"Found {len(results)} results for query: '{query}'\n"]
|
|
|
|
for i, result in enumerate(results, 1):
|
|
doc = result.document
|
|
score_percent = result.score * 100
|
|
|
|
# Header with score and metadata
|
|
project_name = doc.metadata.get("search_project", "unknown")
|
|
header = f"Result {i} (Score: {score_percent:.1f}%) - Project: {project_name}"
|
|
if doc.file_path:
|
|
header += f" - {doc.file_path}"
|
|
if doc.chunk_index is not None:
|
|
header += f" [Chunk {doc.chunk_index}]"
|
|
|
|
output_lines.append(header)
|
|
output_lines.append("-" * len(header))
|
|
|
|
# Add metadata if available
|
|
if doc.metadata:
|
|
relevant_metadata = {k: v for k, v in doc.metadata.items()
|
|
if k not in ['chunk_number', 'total_chunks', 'search_project']}
|
|
if relevant_metadata:
|
|
output_lines.append(f"Metadata: {json.dumps(relevant_metadata, indent=2)}")
|
|
|
|
# Add content if requested
|
|
if include_content:
|
|
content = doc.content
|
|
if len(content) > 500:
|
|
content = content[:500] + "..."
|
|
output_lines.append(f"Content:\n{content}")
|
|
|
|
output_lines.append("") # Empty line between results
|
|
|
|
return "\n".join(output_lines)
|
|
|
|
except Exception as e:
|
|
return f"Error searching vector database: {str(e)}"
|
|
|
|
def register(self, mcp_server) -> None:
|
|
"""Register this tool with the MCP server.
|
|
|
|
Args:
|
|
mcp_server: The FastMCP server instance
|
|
"""
|
|
# This is a placeholder - the actual registration would happen
|
|
# through the MCP server's tool registration mechanism
|
|
pass |