Files
zeekay 7374804405 hanzo-mcp 0.15.21: the active mode is the whole manifest, and a flood of output has a ceiling
The default surface was 34 tools, 61 KB, ~15,300 tokens of schema in every
prompt of every agent. The mode system already names what a session should
carry, but only the tools register_all_tools enumerates obeyed it: zsh,
curl, playwright and the helpers (ps, npx, uvx, jq, wget, cdp, open) stayed
on in every mode. A mode now decides every discovered tool, the `hanzo`
mode lists what a coding agent calls every few steps (18 tools, 32 KB,
~8,000 tokens), and the rest is a name away: HANZO_MCP_ENABLED_TOOLS=a,b
or =all, `tool enable`, or `mode`.

hanzo-tools-shell 0.6.6: run_shell kept every byte a foreground command
wrote until the command ended. `yes` for thirty seconds is gigabytes. It
keeps the first 16 MiB per stream and drains the rest.

The tests' session-wide patch of asyncio.create_subprocess_exec handed
every shell command a stream that never reached EOF; the first test to run
one read it in a loop until the machine was out of memory (22 GB, killed).
The patch is now opt-in and its process has exited.

bench/latency.py measures the cold start, the tools/list bill and the common
calls through a real MCP client over stdio. Interleaved rounds, wall and CPU
seconds, p50 over 6 cold starts:

                         cold wall  cold CPU  tools  bytes   ~tokens
  installed 0.15.18      9.34 s     10.46 s   30     52,601  13,150
  main 0.15.20           1.52 s      1.13 s   34     61,287  15,321
  this                   0.96 s      1.06 s   18     32,222   8,055

The installed environment's 9 s is its hanzoai 8.5.156, whose package init
imports the whole generated cloud client; hanzo-mcp >= 0.15.20 requires
hanzoai >= 8.5.624, which loads it lazily. Per-call latency is spawn-bound
(zsh 17.6 ms, git status 33 ms, fs.read 2.5 ms at p50) and moves with the
machine's load; no call path changed.
2026-10-12 08:47:57 -07:00

331 lines
12 KiB
Python

"""Tools package for Hanzo AI.
This package provides dynamic tool loading from hanzo-tools-* packages via entry points.
Tools are discovered and loaded at runtime, enabling:
- Install/uninstall tool packages independently
- Enable/disable individual tools
- Hot-reload without server restart
IMPORTANT: All tool implementations live in hanzo-tools-* packages.
This module only handles discovery and registration.
"""
from __future__ import annotations
import json
import logging
import os
from pathlib import Path
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from hanzo_tools.core import BaseTool, PermissionManager
from mcp.server import FastMCP
logger = logging.getLogger(__name__)
# Per-service tools the unified `hanzo` tool replaces, each now reachable as
# hanzo(service=...) straight from cloud's OpenAPI registry.
SUPERSEDED_BY_HANZO = (
"api",
"auth",
"billing",
"commerce",
"iam",
"ingress",
"kms",
"mpc",
"paas",
"team",
)
def _asked() -> dict[str, bool]:
"""Tools the user turned on or off beyond the mode: `tool enable|disable` (tool_states.json), then
HANZO_MCP_ENABLED_TOOLS, a comma list of names or `all`."""
states: dict[str, bool] = {}
try:
with open(Path.home() / ".hanzo" / "mcp" / "tool_states.json") as f:
states = {str(k): bool(v) for k, v in json.load(f).items()}
except (OSError, ValueError, AttributeError):
pass
for name in os.environ.get("HANZO_MCP_ENABLED_TOOLS", "").split(","):
if name.strip():
states[name.strip()] = True
return states
def _unified_hanzo_available() -> bool:
"""Whether the unified `hanzo` tool can actually be constructed.
Importing is the honest test: it is what the loader will do, so a success
here means the replacement really is there to take over.
"""
try:
from hanzo_tools.api.hanzo_tool import HanzoTool # noqa: F401
except Exception as exc: # ImportError, or a broken dependency
logger.warning(f"unified hanzo tool not importable: {exc}")
return False
return True
def register_all_tools(
mcp_server: "FastMCP",
permission_manager: "PermissionManager",
agent_model: str | None = None,
agent_max_tokens: int | None = None,
agent_api_key: str | None = None,
agent_base_url: str | None = None,
agent_max_iterations: int = 10,
agent_max_tool_uses: int = 30,
enable_agent_tool: bool = False,
disable_write_tools: bool = False,
disable_search_tools: bool = False,
enabled_tools: dict[str, bool] | None = None,
vector_config: dict | None = None,
use_mode: bool = True,
force_mode: str | None = None,
) -> list["BaseTool"]:
"""Register all Hanzo tools with the MCP server.
Tools are discovered from installed hanzo-tools-* packages via entry points.
Args:
mcp_server: The FastMCP server instance
permission_manager: Permission manager for access control
agent_model: Optional model name for agent tool in LLM format
agent_max_tokens: Optional maximum tokens for agent responses
agent_api_key: Optional API key for the LLM provider
agent_base_url: Optional base URL for the LLM provider API endpoint
agent_max_iterations: Maximum number of iterations for agent (default: 10)
agent_max_tool_uses: Maximum number of total tool uses for agent (default: 30)
enable_agent_tool: Whether to enable the agent tool (default: False)
disable_write_tools: Whether to disable write tools (default: False)
disable_search_tools: Whether to disable search tools (default: False)
enabled_tools: Dictionary of individual tool enable/disable states (default: None)
vector_config: Vector store configuration (default: None)
use_mode: Whether to use mode system for tool configuration (default: True)
force_mode: Force a specific mode to be active (default: None)
Returns:
List of registered BaseTool instances
"""
from hanzo_mcp.tools.common.entrypoint_loader import (
PACKAGE_TOOL_PREFIXES,
EntryPointToolLoader,
)
all_tools: dict[str, "BaseTool"] = {}
tool_config = enabled_tools or {}
def is_tool_enabled(tool_name: str, category_enabled: bool = True) -> bool:
"""Check if a specific tool should be enabled."""
if tool_name in tool_config:
return tool_config[tool_name]
return category_enabled
# Apply mode configuration if enabled
if use_mode:
try:
from hanzo_mcp.tools.common.mode import activate_mode_from_env
from hanzo_mcp.tools.common.mode_loader import ModeLoader
activate_mode_from_env()
tool_config = ModeLoader.get_enabled_tools_from_mode(
base_enabled_tools=enabled_tools, force_mode=force_mode
)
ModeLoader.apply_environment_from_mode()
except ImportError:
logger.debug("Mode system not available")
# Build enabled state for each tool based on configuration
resolved_enabled_tools: dict[str, bool] = {}
# Filesystem tools
for tool in PACKAGE_TOOL_PREFIXES.get("filesystem", []):
if tool in ["write", "edit", "multi_edit"]:
resolved_enabled_tools[tool] = is_tool_enabled(
tool, not disable_write_tools
)
elif tool in ["ast", "search"]:
resolved_enabled_tools[tool] = is_tool_enabled(
tool, not disable_search_tools
)
else:
resolved_enabled_tools[tool] = is_tool_enabled(tool, True)
# Shell tools - always enabled by default
for tool in PACKAGE_TOOL_PREFIXES.get("shell", []):
resolved_enabled_tools[tool] = is_tool_enabled(tool, True)
# Browser tool
resolved_enabled_tools["browser"] = is_tool_enabled("browser", True)
# Memory tools
for tool in PACKAGE_TOOL_PREFIXES.get("memory", []):
resolved_enabled_tools[tool] = is_tool_enabled(tool, True)
# Tasks tools
resolved_enabled_tools["tasks"] = is_tool_enabled("tasks", True)
# Reasoning tools
resolved_enabled_tools["think"] = is_tool_enabled("think", True)
resolved_enabled_tools["critic"] = is_tool_enabled("critic", True)
# LSP tool
resolved_enabled_tools["lsp"] = is_tool_enabled("lsp", True)
# Refactor tool
resolved_enabled_tools["refactor"] = is_tool_enabled("refactor", True)
# Database tools
for tool in PACKAGE_TOOL_PREFIXES.get("database", []):
resolved_enabled_tools[tool] = is_tool_enabled(tool, True)
# Agent tools
resolved_enabled_tools["agent"] = enable_agent_tool or is_tool_enabled(
"agent", True
)
resolved_enabled_tools["swarm"] = is_tool_enabled("swarm", False)
for tool in ["claude", "codex", "gemini", "grok", "code_auth"]:
resolved_enabled_tools[tool] = is_tool_enabled(tool, enable_agent_tool)
# Editor tools
for tool in PACKAGE_TOOL_PREFIXES.get("editor", []):
resolved_enabled_tools[tool] = is_tool_enabled(tool, True)
# LLM tools
resolved_enabled_tools["llm"] = is_tool_enabled("llm", True)
resolved_enabled_tools["kai_decide"] = is_tool_enabled("kai_decide", True)
resolved_enabled_tools["consensus"] = is_tool_enabled("consensus", True)
# Vector tools (usually disabled by default unless config provided)
vector_enabled = vector_config is not None
for tool in PACKAGE_TOOL_PREFIXES.get("vector", []):
resolved_enabled_tools[tool] = is_tool_enabled(tool, vector_enabled)
# Config tools
resolved_enabled_tools["config"] = is_tool_enabled("config", True)
resolved_enabled_tools["mode"] = is_tool_enabled("mode", True)
resolved_enabled_tools["workspace"] = is_tool_enabled("workspace", True)
# MCP tools
resolved_enabled_tools["mcp"] = is_tool_enabled("mcp", True)
# Unified Hanzo platform surface: `hanzo` covers every cloud service by
# projecting them from the OpenAPI registry, so the per-service tools are
# redundant. Hide them only once the replacement genuinely exists —
# unconditionally disabling them would strand the user with no cloud tools
# at all on any import error in the unified surface.
resolved_enabled_tools["hanzo"] = is_tool_enabled("hanzo", True)
if resolved_enabled_tools["hanzo"] and _unified_hanzo_available():
for legacy_tool in SUPERSEDED_BY_HANZO:
resolved_enabled_tools[legacy_tool] = False
else:
logger.warning(
"unified `hanzo` tool unavailable — keeping per-service tools enabled"
)
# Jupyter tools
for tool in PACKAGE_TOOL_PREFIXES.get("jupyter", []):
resolved_enabled_tools[tool] = is_tool_enabled(tool, True)
# Computer tools (screen capture, recording, native control)
for tool in PACKAGE_TOOL_PREFIXES.get("computer", []):
resolved_enabled_tools[tool] = is_tool_enabled(tool, True)
# UI component registry tool
resolved_enabled_tools["ui"] = is_tool_enabled("ui", True)
# Create loader and discover packages
loader = EntryPointToolLoader(permission_manager=permission_manager)
discovered = loader.discover_packages()
# An active mode is the whole manifest: a tool it does not list is off, whichever package ships it.
# What the user asked for by name stands over the mode.
asked = _asked()
from hanzo_mcp.tools.common.mode import ModeRegistry
mode = ModeRegistry.get_active() if use_mode else None
for name in {n for names in discovered.values() for n in names}:
if name in asked:
resolved_enabled_tools[name] = asked[name]
elif asked.get("all"):
resolved_enabled_tools[name] = True
elif mode and mode.tools:
resolved_enabled_tools[name] = bool(tool_config.get(name, False))
if discovered:
logger.info(
f"Discovered {len(discovered)} tool packages: {', '.join(discovered.keys())}"
)
# Load all discovered tools
loaded = loader.load_all(
mcp_server,
enabled_tools=resolved_enabled_tools,
# Database package tools currently require explicit db_manager wiring.
# Keep disabled by default unless explicitly enabled.
enabled_packages={"database": is_tool_enabled("database", False)},
)
all_tools.update(loaded)
logger.info(f"Loaded {len(loaded)} tools from entry points")
else:
logger.warning("No tool packages discovered via entry points")
# Register system tools that are always available
_register_system_tools(mcp_server, all_tools)
return list(all_tools.values())
def _register_system_tools(
mcp_server: "FastMCP",
all_tools: dict[str, "BaseTool"],
) -> None:
"""Register built-in system tools that are always available.
These are core tools that don't come from hanzo-tools-* packages.
"""
# Version tool
try:
from hanzo_mcp.tools.common.version_tool import register_version_tool
register_version_tool(mcp_server)
except ImportError:
logger.debug("Version tool not available")
# Unified tool command (replaces tool_install, tool_enable, tool_disable, tool_list)
try:
from hanzo_mcp.tools.common.tool import register_unified_tool
unified_tools = register_unified_tool(mcp_server)
for tool in unified_tools:
all_tools[tool.name] = tool
logger.info("Registered unified 'tool' command")
except ImportError as e:
logger.debug(f"Unified tool not available: {e}")
# Fallback to legacy tools if unified tool fails
try:
from hanzo_mcp.tools.common.tool_install import register_tool_install
install_tools = register_tool_install(mcp_server)
for tool in install_tools:
all_tools[tool.name] = tool
except ImportError:
pass
# Stats tool
try:
from hanzo_mcp.tools.common.stats import StatsTool
stats_tool = StatsTool()
stats_tool.register(mcp_server)
all_tools[stats_tool.name] = stats_tool
except ImportError:
logger.debug("Stats tool not available")
# Re-export for backward compatibility
__all__ = ["register_all_tools"]