Files
zeekayandHanzo Dev d3110e47ea Major refactor: Unified tools, palette system, dev mode, and shell improvements (v0.6.0)
🎨 **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]>
2025-06-26 17:59:24 -04:00

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