mirror of
https://github.com/hanzoai/python-sdk.git
synced 2026-10-23 20:23:00 +00:00
fix(mcp): the default mode disabled the unified hanzo tool
0.15.13 shipped the 166-service surface switched off. `tools/list` against a
clean install returned 40 tools with no `hanzo`, and the server logged
"unified `hanzo` tool unavailable — keeping per-service tools enabled".
The import was fine; the mode gate was not. register_all_tools() asks
is_tool_enabled("hanzo", True), which returns tool_config["hanzo"] whenever the
key exists rather than falling back to the default. ModeLoader builds that
config by disabling every TOOL_REGISTRY key and re-enabling only the active
mode's tools. TOOL_REGISTRY has "hanzo"; the default personality's list did
not — so the "True" default was never reached and the tool was explicitly off.
Two things broke together, because the per-service cloud tools are retired only
once `hanzo` is enabled: the unified surface was absent AND the ten tools it
replaces were still mounted. The exact state 929e103e set out to end.
`hanzo` belongs in ESSENTIAL_TOOLS. It is an axis like fs or git — the single
seam to the platform — not a mode-specific extra. It stays out of
ESSENTIAL_SYSTEM_TOOLS so it remains disableable.
Verified in a clean uv venv from built wheels only (no editable, no PYTHONPATH):
tools/list now returns 31 tools including `hanzo`, and api/auth/billing/commerce/
iam/ingress/kms/mpc/paas/team are correctly gone (40 - 10 + 1). The tool answers
hanzo(service="services") with 166 services read live from
https://api.hanzo.ai/v1/openapi.json.
A test now pins the gate where it is actually decided. It fails against the
installed 0.15.13 (3 of 4) and passes against 0.15.14 — an import-level check
cannot catch this, because the import succeeds.
Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
@@ -116,6 +116,12 @@ ESSENTIAL_TOOLS = [
|
||||
"critic",
|
||||
# Agent spawning
|
||||
"agent",
|
||||
# Hanzo cloud — one tool projecting every service from the OpenAPI registry.
|
||||
# It is an axis like the rest: the single seam to the platform, and the tool
|
||||
# the per-service cloud tools are retired in favour of. A mode omitting it
|
||||
# does not merely lose `hanzo`, it un-retires those ten legacy tools, because
|
||||
# register_all_tools() only hides them once `hanzo` is enabled.
|
||||
"hanzo",
|
||||
# System / configuration
|
||||
"config",
|
||||
"mode",
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "hanzo-mcp"
|
||||
version = "0.15.13"
|
||||
version = "0.15.14"
|
||||
description = "The Zen of Hanzo MCP: One server to rule them all. The ultimate MCP that orchestrates all others."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
"""Regression: the default mode must leave the unified `hanzo` tool enabled.
|
||||
|
||||
The bug this guards against: ``register_all_tools`` gates the unified tool on
|
||||
``is_tool_enabled("hanzo", True)``, which returns ``tool_config["hanzo"]``
|
||||
whenever that key exists. ``ModeLoader.get_enabled_tools_from_mode`` builds
|
||||
``tool_config`` by disabling every key in ``TOOL_REGISTRY`` and then re-enabling
|
||||
only the active mode's ``tools`` list. ``TOOL_REGISTRY`` has ``"hanzo"``, so a
|
||||
mode that omits it does not fall back to the ``True`` default — it explicitly
|
||||
disables it.
|
||||
|
||||
That is what shipped in 0.15.13: the tool built, imported, and registered its
|
||||
entry point, yet ``tools/list`` came back with 40 tools and no ``hanzo``, while
|
||||
the server logged "unified `hanzo` tool unavailable". Two things broke at once,
|
||||
because ``register_all_tools`` only retires the per-service cloud tools once
|
||||
``hanzo`` is enabled — so the 166-service surface was off AND the ten tools it
|
||||
replaces were still on. An import-level check cannot catch this: the import
|
||||
succeeds. The gate has to be tested where it is actually decided.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from hanzo_mcp.config.tool_config import TOOL_REGISTRY
|
||||
from hanzo_mcp.tools import SUPERSEDED_BY_HANZO, _unified_hanzo_available
|
||||
from hanzo_mcp.tools.common.mode import ModeRegistry, activate_mode_from_env
|
||||
from hanzo_mcp.tools.common.mode_loader import ModeLoader
|
||||
|
||||
UNIFIED = "hanzo"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _modes_loaded():
|
||||
"""Modes are registered lazily; the gate reads whatever is active."""
|
||||
ModeLoader.initialize_modes()
|
||||
activate_mode_from_env()
|
||||
|
||||
|
||||
def test_unified_tool_is_in_the_registry():
|
||||
"""If it left TOOL_REGISTRY the clean-slate loop could no longer reach it."""
|
||||
assert UNIFIED in TOOL_REGISTRY
|
||||
|
||||
|
||||
def test_default_mode_lists_the_unified_tool():
|
||||
"""The active mode must name `hanzo`, or the clean slate leaves it off."""
|
||||
mode = ModeRegistry.get_active()
|
||||
assert mode is not None, "no active mode; the gate would read an empty config"
|
||||
assert UNIFIED in mode.tools, (
|
||||
f"mode {mode.name!r} omits {UNIFIED!r}, so ModeLoader disables it. "
|
||||
f"mode.tools={sorted(mode.tools)}"
|
||||
)
|
||||
|
||||
|
||||
def test_resolved_config_enables_the_unified_tool():
|
||||
"""The fingerprint of the 0.15.13 regression: tool_config['hanzo'] is False."""
|
||||
config = ModeLoader.get_enabled_tools_from_mode(
|
||||
base_enabled_tools=None, force_mode=None
|
||||
)
|
||||
assert config.get(UNIFIED) is True, (
|
||||
f"tool_config[{UNIFIED!r}]={config.get(UNIFIED)!r} — register_all_tools "
|
||||
"will skip the unified tool and keep the per-service tools it replaces."
|
||||
)
|
||||
|
||||
|
||||
def test_superseded_tools_are_retired_when_the_unified_tool_is_available():
|
||||
"""Enabling `hanzo` is what retires the ten per-service cloud tools.
|
||||
|
||||
Mirrors the branch in register_all_tools so the two cannot drift: if the
|
||||
unified tool imports and the mode enables it, none of the tools it replaces
|
||||
should survive into the resolved config.
|
||||
"""
|
||||
if not _unified_hanzo_available():
|
||||
pytest.skip("hanzo-tools-api not installed; nothing to supersede")
|
||||
|
||||
config = ModeLoader.get_enabled_tools_from_mode(
|
||||
base_enabled_tools=None, force_mode=None
|
||||
)
|
||||
assert config.get(UNIFIED) is True
|
||||
|
||||
still_on = [t for t in SUPERSEDED_BY_HANZO if config.get(t)]
|
||||
assert not still_on, (
|
||||
f"{still_on} stayed enabled alongside the unified tool that replaces them"
|
||||
)
|
||||
Reference in New Issue
Block a user