mirror of
https://github.com/hanzoai/mcp.git
synced 2026-11-02 00:44:04 +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]>
291 lines
8.6 KiB
Python
291 lines
8.6 KiB
Python
"""Open files in Neovim editor."""
|
|
|
|
import os
|
|
import subprocess
|
|
import shutil
|
|
from typing import Annotated, Optional, TypedDict, Unpack, final, override
|
|
from pathlib import Path
|
|
|
|
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.context import create_tool_context
|
|
from hanzo_mcp.tools.common.permissions import PermissionManager
|
|
|
|
|
|
FilePath = Annotated[
|
|
str,
|
|
Field(
|
|
description="Path to the file to open",
|
|
min_length=1,
|
|
),
|
|
]
|
|
|
|
LineNumber = Annotated[
|
|
Optional[int],
|
|
Field(
|
|
description="Line number to jump to",
|
|
default=None,
|
|
),
|
|
]
|
|
|
|
ColumnNumber = Annotated[
|
|
Optional[int],
|
|
Field(
|
|
description="Column number to jump to",
|
|
default=None,
|
|
),
|
|
]
|
|
|
|
ReadOnly = Annotated[
|
|
bool,
|
|
Field(
|
|
description="Open file in read-only mode",
|
|
default=False,
|
|
),
|
|
]
|
|
|
|
Split = Annotated[
|
|
Optional[str],
|
|
Field(
|
|
description="Split mode: vsplit, split, tab",
|
|
default=None,
|
|
),
|
|
]
|
|
|
|
Wait = Annotated[
|
|
bool,
|
|
Field(
|
|
description="Wait for Neovim to exit before returning",
|
|
default=True,
|
|
),
|
|
]
|
|
|
|
InTerminal = Annotated[
|
|
bool,
|
|
Field(
|
|
description="Open in terminal (requires terminal that supports it)",
|
|
default=True,
|
|
),
|
|
]
|
|
|
|
|
|
class NeovimEditParams(TypedDict, total=False):
|
|
"""Parameters for Neovim edit tool."""
|
|
|
|
file_path: str
|
|
line_number: Optional[int]
|
|
column_number: Optional[int]
|
|
read_only: bool
|
|
split: Optional[str]
|
|
wait: bool
|
|
in_terminal: bool
|
|
|
|
|
|
@final
|
|
class NeovimEditTool(BaseTool):
|
|
"""Tool for opening files in Neovim."""
|
|
|
|
def __init__(self, permission_manager: PermissionManager):
|
|
"""Initialize the Neovim edit tool.
|
|
|
|
Args:
|
|
permission_manager: Permission manager for access control
|
|
"""
|
|
self.permission_manager = permission_manager
|
|
|
|
@property
|
|
@override
|
|
def name(self) -> str:
|
|
"""Get the tool name."""
|
|
return "neovim_edit"
|
|
|
|
@property
|
|
@override
|
|
def description(self) -> str:
|
|
"""Get the tool description."""
|
|
return """Open files in Neovim editor with advanced options.
|
|
|
|
Open files at specific lines/columns, in different split modes, or read-only.
|
|
Integrates with your existing Neovim configuration.
|
|
|
|
Examples:
|
|
- neovim_edit --file-path main.py
|
|
- neovim_edit --file-path main.py --line-number 42
|
|
- neovim_edit --file-path main.py --line-number 42 --column-number 10
|
|
- neovim_edit --file-path config.json --read-only
|
|
- neovim_edit --file-path test.py --split vsplit
|
|
- neovim_edit --file-path README.md --split tab
|
|
|
|
Split modes:
|
|
- vsplit: Open in vertical split
|
|
- split: Open in horizontal split
|
|
- tab: Open in new tab
|
|
|
|
Note: Requires Neovim to be installed and available in PATH.
|
|
"""
|
|
|
|
@override
|
|
async def call(
|
|
self,
|
|
ctx: MCPContext,
|
|
**params: Unpack[NeovimEditParams],
|
|
) -> str:
|
|
"""Open file in Neovim.
|
|
|
|
Args:
|
|
ctx: MCP context
|
|
**params: Tool parameters
|
|
|
|
Returns:
|
|
Result of the operation
|
|
"""
|
|
tool_ctx = create_tool_context(ctx)
|
|
await tool_ctx.set_tool_info(self.name)
|
|
|
|
# Extract parameters
|
|
file_path = params.get("file_path")
|
|
if not file_path:
|
|
return "Error: file_path is required"
|
|
|
|
line_number = params.get("line_number")
|
|
column_number = params.get("column_number")
|
|
read_only = params.get("read_only", False)
|
|
split = params.get("split")
|
|
wait = params.get("wait", True)
|
|
in_terminal = params.get("in_terminal", True)
|
|
|
|
# Check if Neovim is available
|
|
nvim_cmd = shutil.which("nvim")
|
|
if not nvim_cmd:
|
|
# Try common locations
|
|
common_paths = [
|
|
"/usr/local/bin/nvim",
|
|
"/usr/bin/nvim",
|
|
"/opt/homebrew/bin/nvim",
|
|
os.path.expanduser("~/.local/bin/nvim"),
|
|
]
|
|
for path in common_paths:
|
|
if os.path.exists(path):
|
|
nvim_cmd = path
|
|
break
|
|
|
|
if not nvim_cmd:
|
|
return """Error: Neovim (nvim) not found. Install it with:
|
|
|
|
On macOS:
|
|
brew install neovim
|
|
|
|
On Ubuntu/Debian:
|
|
sudo apt install neovim
|
|
|
|
On Arch:
|
|
sudo pacman -S neovim
|
|
|
|
Or visit: https://neovim.io/"""
|
|
|
|
# Convert to absolute path
|
|
file_path = os.path.abspath(file_path)
|
|
|
|
# Check permissions
|
|
if not self.permission_manager.has_permission(file_path):
|
|
return f"Error: No permission to access {file_path}"
|
|
|
|
# Build Neovim command
|
|
cmd = [nvim_cmd]
|
|
|
|
# Add read-only flag
|
|
if read_only:
|
|
cmd.append("-R")
|
|
|
|
# Add split mode
|
|
if split:
|
|
if split == "vsplit":
|
|
cmd.extend(["-c", "vsplit"])
|
|
elif split == "split":
|
|
cmd.extend(["-c", "split"])
|
|
elif split == "tab":
|
|
cmd.extend(["-c", "tabnew"])
|
|
else:
|
|
return f"Error: Invalid split mode '{split}'. Use 'vsplit', 'split', or 'tab'"
|
|
|
|
# Add file path
|
|
cmd.append(file_path)
|
|
|
|
# Add line/column positioning
|
|
if line_number:
|
|
if column_number:
|
|
# Go to specific line and column
|
|
cmd.extend(["+call cursor({}, {})".format(line_number, column_number)])
|
|
else:
|
|
# Go to specific line
|
|
cmd.append(f"+{line_number}")
|
|
|
|
await tool_ctx.info(f"Opening {file_path} in Neovim")
|
|
|
|
try:
|
|
# Determine how to run Neovim
|
|
if in_terminal and not wait:
|
|
# Open in a new terminal window (platform-specific)
|
|
if os.uname().sysname == "Darwin": # macOS
|
|
# Try to use iTerm2 if available, otherwise Terminal
|
|
if shutil.which("osascript"):
|
|
# Build AppleScript to open in iTerm2 or Terminal
|
|
nvim_cmd_str = " ".join(f'"{arg}"' for arg in cmd)
|
|
|
|
# Try iTerm2 first
|
|
applescript = f'''tell application "System Events"
|
|
if exists application process "iTerm2" then
|
|
tell application "iTerm"
|
|
activate
|
|
tell current window
|
|
create tab with default profile
|
|
tell current session
|
|
write text "{nvim_cmd_str}"
|
|
end tell
|
|
end tell
|
|
end tell
|
|
else
|
|
tell application "Terminal"
|
|
activate
|
|
do script "{nvim_cmd_str}"
|
|
end tell
|
|
end if
|
|
end tell'''
|
|
|
|
subprocess.run(["osascript", "-e", applescript])
|
|
return f"Opened {file_path} in Neovim (new terminal window)"
|
|
|
|
elif shutil.which("gnome-terminal"):
|
|
# Linux with GNOME
|
|
subprocess.Popen(["gnome-terminal", "--"] + cmd)
|
|
return f"Opened {file_path} in Neovim (new terminal window)"
|
|
|
|
elif shutil.which("xterm"):
|
|
# Fallback to xterm
|
|
subprocess.Popen(["xterm", "-e"] + cmd)
|
|
return f"Opened {file_path} in Neovim (new terminal window)"
|
|
|
|
else:
|
|
# Can't open in terminal, fall back to subprocess
|
|
subprocess.Popen(cmd)
|
|
return f"Opened {file_path} in Neovim (background process)"
|
|
|
|
else:
|
|
# Run and wait for completion
|
|
result = subprocess.run(cmd)
|
|
|
|
if result.returncode == 0:
|
|
return f"Successfully edited {file_path} in Neovim"
|
|
else:
|
|
return f"Neovim exited with code {result.returncode}"
|
|
|
|
except Exception as e:
|
|
await tool_ctx.error(f"Failed to open Neovim: {str(e)}")
|
|
return f"Error opening Neovim: {str(e)}"
|
|
|
|
def register(self, mcp_server) -> None:
|
|
"""Register this tool with the MCP server."""
|
|
pass
|