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

273 lines
8.1 KiB
Python

"""Execute Neovim commands and macros."""
import os
import subprocess
import shutil
import tempfile
from typing import Annotated, Optional, TypedDict, Unpack, final, override, List
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
Command = Annotated[
Optional[str],
Field(
description="Neovim command to execute (Ex commands like :w, :q, etc.)",
default=None,
),
]
Commands = Annotated[
Optional[List[str]],
Field(
description="List of Neovim commands to execute in sequence",
default=None,
),
]
Macro = Annotated[
Optional[str],
Field(
description="Vim macro to execute (e.g., 'dd' to delete line)",
default=None,
),
]
FilePath = Annotated[
Optional[str],
Field(
description="File to operate on (optional, uses current buffer if not specified)",
default=None,
),
]
SaveAfter = Annotated[
bool,
Field(
description="Save file after executing commands",
default=True,
),
]
ReturnOutput = Annotated[
bool,
Field(
description="Return output/messages from Neovim",
default=True,
),
]
class NeovimCommandParams(TypedDict, total=False):
"""Parameters for Neovim command tool."""
command: Optional[str]
commands: Optional[List[str]]
macro: Optional[str]
file_path: Optional[str]
save_after: bool
return_output: bool
@final
class NeovimCommandTool(BaseTool):
"""Tool for executing Neovim commands and macros."""
def __init__(self, permission_manager: PermissionManager):
"""Initialize the Neovim command 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_command"
@property
@override
def description(self) -> str:
"""Get the tool description."""
return """Execute Neovim commands and macros programmatically.
Run Ex commands, normal mode commands, or complex macros in Neovim.
Can operate on files without opening the editor interface.
Examples:
- neovim_command --command ":set number" --file-path main.py
- neovim_command --command ":%s/old/new/g" --file-path config.json
- neovim_command --commands ":set expandtab" ":retab" --file-path script.sh
- neovim_command --macro "ggVG=" --file-path messy.py # Format entire file
- neovim_command --macro "dd10j" --file-path list.txt # Delete line and go down 10
Common commands:
- :w - Save file
- :q - Quit
- :%s/old/new/g - Replace all occurrences
- :set number - Show line numbers
- :set expandtab - Use spaces instead of tabs
- :retab - Convert tabs to spaces
Common macros:
- gg - Go to beginning of file
- G - Go to end of file
- dd - Delete line
- yy - Yank (copy) line
- p - Paste
- V - Visual line mode
- = - Format/indent
Note: Requires Neovim to be installed.
"""
@override
async def call(
self,
ctx: MCPContext,
**params: Unpack[NeovimCommandParams],
) -> str:
"""Execute Neovim command.
Args:
ctx: MCP context
**params: Tool parameters
Returns:
Result of the command execution
"""
tool_ctx = create_tool_context(ctx)
await tool_ctx.set_tool_info(self.name)
# Extract parameters
command = params.get("command")
commands = params.get("commands")
macro = params.get("macro")
file_path = params.get("file_path")
save_after = params.get("save_after", True)
return_output = params.get("return_output", True)
# Validate inputs
if not any([command, commands, macro]):
return "Error: Must provide either 'command', 'commands', or 'macro'"
if sum(bool(x) for x in [command, commands, macro]) > 1:
return "Error: Can only use one of 'command', 'commands', or 'macro' at a time"
# Check if Neovim is available
nvim_cmd = shutil.which("nvim")
if not nvim_cmd:
return "Error: Neovim (nvim) not found. Install it first."
# Prepare commands list
nvim_commands = []
if command:
nvim_commands.append(command)
elif commands:
nvim_commands.extend(commands)
elif macro:
# Convert macro to normal mode command
# Escape special characters
escaped_macro = macro.replace('"', '\\"')
nvim_commands.append(f':normal "{escaped_macro}"')
# Add save command if requested
if save_after:
nvim_commands.append(":w")
# Always quit at the end
nvim_commands.append(":q")
# Build Neovim command line
cmd = [nvim_cmd, "-n", "-i", "NONE"] # No swap file, no shada file
# Add commands
for vim_cmd in nvim_commands:
cmd.extend(["-c", vim_cmd])
# Add file if specified
if file_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}"
if not os.path.exists(file_path):
return f"Error: File not found: {file_path}"
cmd.append(file_path)
else:
# Create empty buffer
cmd.append("-")
await tool_ctx.info(f"Executing Neovim commands: {nvim_commands}")
try:
# Execute Neovim
if return_output:
# Capture output by redirecting messages
output_file = tempfile.NamedTemporaryFile(mode='w+', delete=False)
output_file.close()
# Add command to redirect messages
cmd.insert(3, "-c")
cmd.insert(4, f":redir! > {output_file.name}")
# Execute
result = subprocess.run(
cmd,
capture_output=True,
text=True
)
# Read output
output_content = ""
try:
with open(output_file.name, 'r') as f:
output_content = f.read().strip()
finally:
os.unlink(output_file.name)
if result.returncode == 0:
response = "Commands executed successfully"
if file_path:
response += f" on {os.path.basename(file_path)}"
if output_content:
response += f"\n\nOutput:\n{output_content}"
return response
else:
error_msg = "Error executing Neovim commands"
if result.stderr:
error_msg += f"\n\nError:\n{result.stderr}"
if output_content:
error_msg += f"\n\nOutput:\n{output_content}"
return error_msg
else:
# Just execute without capturing output
result = subprocess.run(cmd)
if result.returncode == 0:
response = "Commands executed successfully"
if file_path:
response += f" on {os.path.basename(file_path)}"
return response
else:
return f"Neovim exited with code {result.returncode}"
except Exception as e:
await tool_ctx.error(f"Failed to execute Neovim commands: {str(e)}")
return f"Error executing Neovim commands: {str(e)}"
def register(self, mcp_server) -> None:
"""Register this tool with the MCP server."""
pass