Files
python-sdk/pkg/hanzo-tools-cloud/hanzo_tools/cloud/cloud_tool.py
T
zeekay badff3db49 Read the fleet from the same catalog the other runtimes do
Python addressed the cloud through ten hand-written packages while TypeScript and
Rust read a generated catalog, and the hand-written ones had drifted: paas is
platform, storage is s3, auth folded into iam, and commerce asked for plurals the
fleet does not publish.

hanzo-tools-cloud embeds the file those two already embed, so the three cannot
disagree about what the API offers — a test compares them by digest. One routing
tool over 114 subsystems rather than 114 tools, for the reason the fleet groups
its own.

Naming a subsystem the fleet does not serve is answered before the credential
check: demanding a key for something that does not exist sends the caller to fix
the wrong thing.
2026-08-30 17:05:32 -07:00

158 lines
5.8 KiB
Python

"""The Hanzo Cloud surface, offered as the fleet publishes it.
One tool per subsystem, carrying that subsystem's operation names, plus
``describe`` — which answers one operation's prose and schema. That is the shape
the fleet serves at ``POST /v1/mcp``, and it is why this file is short: a flat
projection is 1,416 tools and roughly a megabyte to enumerate, which a model pays
for on every turn and a client with a tool cap truncates.
The names come from ``catalog.json``, generated out of cloud's own typed
operations (``plugin/gen-mcp-catalog``). It is the same file the TypeScript and
Rust runtimes embed, so the three cannot come to disagree about what the API
offers — which is what "the runtimes mirror one another" has to mean to be worth
saying.
It is a catalog rather than a fetch because a tool list is answered before any
request has been made, and a client that needs the network to say what it can do
has nothing to say when the network is what failed.
Every call goes to ``api.hanzo.ai`` — the one endpoint. The ten hand-written
packages this replaces dialled four hosts, named resources the fleet had renamed,
and asked for plurals where it publishes singulars.
"""
from __future__ import annotations
import json
import logging
import os
from functools import lru_cache
from pathlib import Path
from typing import Any
import httpx
logger = logging.getLogger(__name__)
_CATALOG = Path(__file__).with_name("catalog.json")
@lru_cache(maxsize=1)
def _fleet() -> dict[str, dict[str, list[str]]]:
return json.loads(_CATALOG.read_text())
def services() -> list[str]:
"""Every subsystem the fleet offers, in a stable order."""
return sorted(_fleet())
def operations(service: str) -> list[str]:
"""One subsystem's operations, empty for a name the fleet does not serve."""
return list(_fleet().get(service, {}).get("ops", []))
def reach() -> int:
"""What this client can address, for anything reporting coverage."""
return sum(len(e["ops"]) for e in _fleet().values())
def _endpoint() -> str:
return os.getenv("HANZO_API_URL", "https://api.hanzo.ai").rstrip("/")
def _token() -> str:
for name in ("HANZO_API_KEY", "API_KEY", "API_TOKEN", "HANZO_TOKEN"):
value = os.getenv(name)
if value:
return value
return ""
def call(service: str, op: str, params: dict[str, Any] | None = None) -> Any:
"""Hand one operation to the fleet and return what it answered.
A refusal is returned as a refusal. Folding it into a success whose body
happens to say it failed is a refusal the caller cannot see, which is worse
than no answer because it is acted on.
"""
# The name is checked first because it is a fact held here, needing no
# credential. Demanding a key for a subsystem that does not exist sends the
# caller to fix a credential when the request is what was wrong.
if service not in _fleet():
raise ValueError(f"the fleet does not serve {service!r}; it serves {', '.join(services())}")
key = _token()
if not key:
raise RuntimeError("HANZO_API_KEY required")
body = {
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {"name": service, "arguments": {"op": op, "input": params or {}}},
}
with httpx.Client(timeout=30.0) as client:
answer = client.post(
f"{_endpoint()}/v1/mcp",
json=body,
headers={"Authorization": f"Bearer {key}"},
)
answer.raise_for_status()
payload = answer.json()
if "error" in payload:
raise RuntimeError(payload["error"].get("message", "refused"))
result = payload.get("result", payload)
if isinstance(result, dict) and result.get("isError"):
content = result.get("content") or [{}]
raise RuntimeError(content[0].get("text", "refused"))
return result
class CloudTool:
"""The fleet behind one tool, routed by subsystem and operation.
One tool rather than 114, for the reason the fleet groups its own: a flat
projection is roughly a megabyte to enumerate, which a model pays for on
every turn and a client with a tool cap truncates. The subsystem names and
their operations are read from the generated catalog, so a subsystem the
fleet gains is reachable here the day the catalog is regenerated — there is
no list in this file to fall behind.
"""
@property
def name(self) -> str:
return "cloud"
@property
def description(self) -> str:
return (
f"The Hanzo Cloud API: {len(services())} subsystems, {reach()} operations. "
"Name a subsystem in `service` and one of its operations in `op`, and pass "
"that operation's own arguments in `input`. Omit `op` to list a subsystem's "
"operations; omit `service` to list the subsystems."
)
async def call(self, ctx: Any = None, **params: Any) -> Any:
service = (params.get("service") or "").strip()
if not service:
return {"services": services()}
if service not in _fleet():
return {"error": f"the fleet does not serve {service!r}", "services": services()}
op = (params.get("op") or "").strip()
if not op:
return {"service": service, "operations": operations(service)}
try:
return call(service, op, params.get("input") or {})
except Exception as exc: # a refusal is reported as one, never as an empty result
logger.debug("cloud %s.%s refused", service, op, exc_info=True)
return {"error": str(exc)}
def register(self, mcp_server: Any) -> None:
@mcp_server.tool(name=self.name, description=self.description)
async def cloud( # type: ignore[no-untyped-def]
service: str = "", op: str = "", input: dict[str, Any] | None = None
):
return await self.call(service=service, op=op, input=input)