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

294 lines
9.9 KiB
Python

"""Base functionality for Jupyter notebook tools.
This module provides common functionality for Jupyter notebook tools, including notebook parsing,
cell processing, and output formatting.
"""
from abc import ABC
import json
import re
from pathlib import Path
from typing import Any, final
from mcp.server.fastmcp import Context as MCPContext
from hanzo_mcp.tools.common.base import FileSystemTool
from hanzo_mcp.tools.common.context import ToolContext, create_tool_context
# Pattern to match ANSI escape sequences
ANSI_ESCAPE_PATTERN = re.compile(r"\x1B\[[0-9;]*[a-zA-Z]")
# Function to clean ANSI escape codes from text
def clean_ansi_escapes(text: str) -> str:
"""Remove ANSI escape sequences from text.
Args:
text: Text containing ANSI escape sequences
Returns:
Text with ANSI escape sequences removed
"""
return ANSI_ESCAPE_PATTERN.sub("", text)
@final
class NotebookOutputImage:
"""Representation of an image output in a notebook cell."""
def __init__(self, image_data: str, media_type: str):
"""Initialize a notebook output image.
Args:
image_data: Base64-encoded image data
media_type: Media type of the image (e.g., "image/png")
"""
self.image_data = image_data
self.media_type = media_type
@final
class NotebookCellOutput:
"""Representation of an output from a notebook cell."""
def __init__(
self,
output_type: str,
text: str | None = None,
image: NotebookOutputImage | None = None,
):
"""Initialize a notebook cell output.
Args:
output_type: Type of output
text: Text output (if any)
image: Image output (if any)
"""
self.output_type = output_type
self.text = text
self.image = image
@final
class NotebookCellSource:
"""Representation of a source cell from a notebook."""
def __init__(
self,
cell_index: int,
cell_type: str,
source: str,
language: str,
execution_count: int | None = None,
outputs: list[NotebookCellOutput] | None = None,
):
"""Initialize a notebook cell source.
Args:
cell_index: Index of the cell in the notebook
cell_type: Type of cell (code or markdown)
source: Source code or text of the cell
language: Programming language of the cell
execution_count: Execution count of the cell (if any)
outputs: Outputs from the cell (if any)
"""
self.cell_index = cell_index
self.cell_type = cell_type
self.source = source
self.language = language
self.execution_count = execution_count
self.outputs = outputs or []
class JupyterBaseTool(FileSystemTool, ABC):
"""Base class for Jupyter notebook tools.
Provides common functionality for working with Jupyter notebooks, including
parsing, cell extraction, and output formatting.
"""
def create_tool_context(self, ctx: MCPContext) -> ToolContext:
"""Create a tool context with the tool name.
Args:
ctx: MCP context
Returns:
Tool context
"""
tool_ctx = create_tool_context(ctx)
return tool_ctx
def set_tool_context_info(self, tool_ctx: ToolContext) -> None:
"""Set the tool info on the context.
Args:
tool_ctx: Tool context
"""
tool_ctx.set_tool_info(self.name)
async def parse_notebook(
self, file_path: Path
) -> tuple[dict[str, Any], list[NotebookCellSource]]:
"""Parse a Jupyter notebook file.
Args:
file_path: Path to the notebook file
Returns:
Tuple of (notebook_data, processed_cells)
"""
with open(file_path, "r", encoding="utf-8") as f:
content = f.read()
notebook = json.loads(content)
# Get notebook language
language = (
notebook.get("metadata", {}).get("language_info", {}).get("name", "python")
)
cells = notebook.get("cells", [])
processed_cells = []
for i, cell in enumerate(cells):
cell_type = cell.get("cell_type", "code")
# Skip if not code or markdown
if cell_type not in ["code", "markdown"]:
continue
# Get source
source = cell.get("source", "")
if isinstance(source, list):
source = "".join(source)
# Get execution count for code cells
execution_count = None
if cell_type == "code":
execution_count = cell.get("execution_count")
# Process outputs for code cells
outputs = []
if cell_type == "code" and "outputs" in cell:
for output in cell["outputs"]:
output_type = output.get("output_type", "")
# Process different output types
if output_type == "stream":
text = output.get("text", "")
if isinstance(text, list):
text = "".join(text)
outputs.append(
NotebookCellOutput(output_type="stream", text=text)
)
elif output_type in ["execute_result", "display_data"]:
# Process text output
text = None
if "data" in output and "text/plain" in output["data"]:
text_data = output["data"]["text/plain"]
if isinstance(text_data, list):
text = "".join(text_data)
else:
text = text_data
# Process image output
image = None
if "data" in output:
if "image/png" in output["data"]:
image = NotebookOutputImage(
image_data=output["data"]["image/png"],
media_type="image/png",
)
elif "image/jpeg" in output["data"]:
image = NotebookOutputImage(
image_data=output["data"]["image/jpeg"],
media_type="image/jpeg",
)
outputs.append(
NotebookCellOutput(
output_type=output_type, text=text, image=image
)
)
elif output_type == "error":
# Format error traceback
ename = output.get("ename", "")
evalue = output.get("evalue", "")
traceback = output.get("traceback", [])
# Handle raw text strings and lists of strings
if isinstance(traceback, list):
# Clean ANSI escape codes and join the list but preserve the formatting
clean_traceback = [
clean_ansi_escapes(line) for line in traceback
]
traceback_text = "\n".join(clean_traceback)
else:
traceback_text = clean_ansi_escapes(str(traceback))
error_text = f"{ename}: {evalue}\n{traceback_text}"
outputs.append(
NotebookCellOutput(output_type="error", text=error_text)
)
# Create cell object
processed_cell = NotebookCellSource(
cell_index=i,
cell_type=cell_type,
source=source,
language=language,
execution_count=execution_count,
outputs=outputs,
)
processed_cells.append(processed_cell)
return notebook, processed_cells
def format_notebook_cells(self, cells: list[NotebookCellSource]) -> str:
"""Format notebook cells as a readable string.
Args:
cells: List of processed notebook cells
Returns:
Formatted string representation of the cells
"""
result = []
for cell in cells:
# Format the cell header
cell_header = f"Cell [{cell.cell_index}] {cell.cell_type}"
if cell.execution_count is not None:
cell_header += f" (execution_count: {cell.execution_count})"
if cell.cell_type == "code" and cell.language != "python":
cell_header += f" [{cell.language}]"
# Add cell to result
result.append(f"{cell_header}:")
result.append(f"```{cell.language if cell.cell_type == 'code' else ''}")
result.append(cell.source)
result.append("```")
# Add outputs if any
if cell.outputs:
result.append("Outputs:")
for output in cell.outputs:
if output.output_type == "error":
result.append("Error:")
result.append("```")
result.append(output.text)
result.append("```")
elif output.text:
result.append("Output:")
result.append("```")
result.append(output.text)
result.append("```")
if output.image:
result.append(f"[Image output: {output.image.media_type}]")
result.append("") # Empty line between cells
return "\n".join(result)