HIP-0300 tools serialize results to JSON text, which flattened images into useless base64-in-text. Add a ToolImage value type + one converter in BaseTool.register: any ToolImage anywhere in a result becomes a real mcp.types.ImageContent block; text-only results stay byte-for-byte JSON. Emitters wired: fs read of image files, browser screenshots (bidi+playwright). This gives hanzo-mcp vision parity with zai-mcp — an agent with native vision (Claude) sees pixels directly, no per-task vision tool needed. Also decomplect the core-package collision that caused this class of bug: hanzo-tools-core shipped a DUPLICATE hanzo_tools/core/ that nondeterministically shadowed canonical hanzo-tools (dropping ToolImage, the sophisticated register, etc. — same shadow class as the 0.3.0 error-reexport gap). Finish the 'merge core into hanzo-tools' refactor: move id_tool into hanzo-tools, make hanzo-tools-core a pure empty metapackage. Now ONE package owns hanzo_tools.core. Versions: hanzo-tools 0.3.2, -core 0.3.1 (shim), -browser 0.5.9, -fs 0.3.4, hanzo-mcp 0.15.12 (floors bumped). Verified: fs read PNG -> [text, image]; fs stat -> [text]; 29 tools intact; 9 tests pass.
hanzo-tools
Core infrastructure and plugin framework for Hanzo AI's modular MCP tool system.
Install
pip install hanzo-tools # Core only
pip install hanzo-tools[dev] # filesystem, shell, editor, lsp, refactor, todo, reasoning, config
pip install hanzo-tools[ai] # llm, agent, memory
pip install hanzo-tools[all] # Everything
Individual tool packages can be installed separately:
pip install hanzo-tools-fs
pip install hanzo-tools-shell
pip install hanzo-tools-browser
pip install hanzo-tools-llm
pip install hanzo-tools-memory
pip install hanzo-tools-editor
pip install hanzo-tools-vector
# ... etc
Usage
Register all discovered tools
from hanzo_tools import discover_tools, register_all
from mcp.server import FastMCP
mcp = FastMCP("my-server")
# Auto-discover and register all installed tool packages
registered = register_all(mcp)
Register individual tool packages
from hanzo_tools.fs import register_tools as register_fs
from hanzo_tools.shell import register_tools as register_shell
register_fs(mcp, permission_manager)
register_shell(mcp)
Build a custom tool
from typing import Any
from mcp.server import FastMCP
from mcp.server.fastmcp import Context as MCPContext
from hanzo_tools.core import BaseTool, ToolRegistry, with_error_logging
class WeatherTool(BaseTool):
@property
def name(self) -> str:
return "get_weather"
@property
def description(self) -> str:
return "Get current weather for a location"
@with_error_logging("get_weather")
async def call(self, ctx: MCPContext, **params: Any) -> str:
location = params["location"]
return f"Weather in {location}: 72°F, sunny"
def register(self, mcp_server: FastMCP) -> None:
@mcp_server.tool()
async def get_weather(location: str, ctx: MCPContext) -> str:
"""Get current weather for a location."""
return await self.call(ctx, location=location)
# Register with the MCP server
ToolRegistry.register_tool(mcp, WeatherTool())
Filesystem tools with permissions
from hanzo_tools.core import FileSystemTool, PermissionManager
pm = PermissionManager(
allowed_paths=["/home/user/projects"],
deny_patterns=[".git", "node_modules", ".env"],
)
class ReadFileTool(FileSystemTool):
def __init__(self):
super().__init__(permission_manager=pm)
@property
def name(self) -> str:
return "read_file"
@property
def description(self) -> str:
return "Read a file"
async def call(self, ctx, **params):
path = params["path"]
if not self.is_path_allowed(path):
return "Access denied"
return open(path).read()
def register(self, mcp_server):
@mcp_server.tool()
async def read_file(path: str, ctx) -> str:
"""Read a file."""
return await self.call(ctx, path=path)
Decorators
from hanzo_tools.core import auto_timeout, with_error_logging, handle_connection_errors
@auto_timeout("search", timeout=120)
@with_error_logging("search")
@handle_connection_errors
async def search_files(pattern: str, path: str) -> str:
...
Timeouts are configurable via environment variables:
HANZO_TIMEOUT_SEARCH=300 # Override search timeout to 5 minutes
HANZO_TIMEOUT_BROWSER=600 # Override browser timeout to 10 minutes
Tool enable/disable
from hanzo_tools.core import ToolRegistry
# Disable a tool at runtime
ToolRegistry.set_tool_enabled("browser", False)
# Check if a tool is enabled
if ToolRegistry.is_tool_enabled("browser"):
...
Tool states persist to ~/.hanzo/mcp/tool_states.json.
Architecture
hanzo-tools (core)
├── BaseTool — Abstract base for all tools
├── FileSystemTool — Base for filesystem tools with permissions
├── ToolRegistry — Enable/disable and registration
├── ToolContext — Execution context with logging/progress
├── PermissionManager — Path-based access control
├── MCPResourceDocument — Structured response formatting
├── auto_timeout — Configurable async timeouts
├── with_error_logging — Error logging to ~/.hanzo/mcp/logs/
├── handle_connection_errors — Graceful disconnect handling
├── validate_path_parameter — Path validation
├── discover_tools() — Plugin discovery via entry points
└── register_all() — Auto-register all discovered tools
hanzo-tools-fs — File read/write/search/glob
hanzo-tools-shell — Shell command execution
hanzo-tools-browser — Browser automation (Playwright)
hanzo-tools-editor — Code editing with AST awareness
hanzo-tools-lsp — Language Server Protocol integration
hanzo-tools-refactor — Refactoring operations
hanzo-tools-llm — LLM inference tools
hanzo-tools-agent — Agent orchestration
hanzo-tools-memory — Persistent memory/context
hanzo-tools-vector — Vector DB operations
hanzo-tools-database — Database query tools
hanzo-tools-jupyter — Jupyter notebook tools
hanzo-tools-todo — Task/todo management
hanzo-tools-reasoning — Chain-of-thought reasoning
hanzo-tools-config — Configuration management
hanzo-tools-mcp — MCP protocol utilities
hanzo-tools-computer — Computer use (screen/keyboard)
Tool packages register via importlib.metadata entry points (group="hanzo.tools"), enabling automatic discovery without explicit imports.
API
Core Classes
| Class | Purpose |
|---|---|
BaseTool |
Abstract base — implement name, description, call(), register() |
FileSystemTool |
Extends BaseTool with permission_manager and validate_path() |
ToolRegistry |
Class-level enable/disable with JSON persistence |
ToolContext |
Wraps MCP context with info(), warning(), error(), progress() |
PermissionManager |
Allowed paths + deny patterns for filesystem access control |
MCPResourceDocument |
Response type with to_json_string(), to_readable_string(), to_dict() |
ValidationResult |
Boolean result with optional error message |
Decorators
| Decorator | Purpose |
|---|---|
@auto_timeout(name, timeout=None) |
Async timeout with env var override (HANZO_TIMEOUT_*) |
@with_error_logging(name) |
Log errors to ~/.hanzo/mcp/logs/ and return friendly messages |
@handle_connection_errors |
Catch disconnects gracefully |
@retry(max_attempts, delay, backoff) |
Exponential backoff retry |
Top-level Functions
| Function | Purpose |
|---|---|
discover_tools() |
Find all installed tool packages via entry points |
register_all(mcp, pm, enabled) |
Register all discovered tools with an MCP server |
License
MIT