fix: finalize latest MCP/CLI/SDK updates

This commit is contained in:
2026-03-04 11:27:19 -08:00
parent 483d645dd9
commit 0279142e4a
49 changed files with 14328 additions and 271 deletions
+164
View File
@@ -0,0 +1,164 @@
"""Multi-cluster management for Hanzo K8s CLI.
Stores known clusters in ~/.hanzo/k8s/clusters.json and manages
the current active cluster context for kubectl operations.
Usage:
hanzo k8s clusters # List all clusters
hanzo k8s use <name> # Switch to a cluster
"""
from __future__ import annotations
import json
import shutil
import subprocess
from pathlib import Path
import click
from rich.console import Console
from rich.table import Table
console = Console()
CLUSTERS_FILE = Path.home() / ".hanzo" / "k8s" / "clusters.json"
DEFAULT_CLUSTERS: dict = {
"clusters": {
"hanzo": {
"context": "do-sfo3-hanzo-k8s",
"namespace": "hanzo",
"ip": "209.38.69.69",
},
"adnexus": {
"context": "do-sfo3-adnexus-k8s",
"namespace": "adnexus",
"ip": "134.199.141.68",
},
"lux": {
"context": "do-sfo3-lux-k8s",
"namespace": "lux",
},
"pars": {
"context": "do-sfo3-pars-k8s",
"namespace": "pars",
},
"zoo": {
"context": "do-sfo3-zoo-k8s",
"namespace": "zoo",
},
"bootnode": {
"context": "do-sfo3-bootnode-k8s",
"namespace": "bootnode",
},
},
"current": "hanzo",
}
def _load_clusters() -> dict:
"""Load clusters config, auto-creating with defaults on first use."""
if CLUSTERS_FILE.exists():
try:
return json.loads(CLUSTERS_FILE.read_text())
except (json.JSONDecodeError, OSError):
pass
_save_clusters(DEFAULT_CLUSTERS)
return DEFAULT_CLUSTERS
def _save_clusters(data: dict) -> None:
"""Persist clusters config to disk."""
CLUSTERS_FILE.parent.mkdir(parents=True, exist_ok=True)
CLUSTERS_FILE.write_text(json.dumps(data, indent=2))
def get_current_cluster() -> tuple[str, dict]:
"""Return (name, cluster_info) for the current active cluster."""
config = _load_clusters()
name = config.get("current", "hanzo")
cluster = config.get("clusters", {}).get(name, {})
return name, cluster
@click.command("clusters")
def clusters() -> None:
"""List all known Kubernetes clusters."""
config = _load_clusters()
current = config.get("current", "")
cluster_map = config.get("clusters", {})
if not cluster_map:
console.print("[yellow]No clusters configured.[/yellow]")
return
table = Table(title="Kubernetes Clusters")
table.add_column("", width=2)
table.add_column("Name", style="cyan")
table.add_column("Context", style="white")
table.add_column("Namespace", style="green")
table.add_column("IP", style="dim")
for name, info in sorted(cluster_map.items()):
marker = "[bold green]*[/bold green]" if name == current else ""
table.add_row(
marker,
name,
info.get("context", ""),
info.get("namespace", ""),
info.get("ip", ""),
)
console.print(table)
console.print(f"\nCurrent: [bold cyan]{current}[/bold cyan]")
@click.command("use")
@click.argument("name")
def use(name: str) -> None:
"""Set the current Kubernetes cluster.
Switches kubectl context and updates the default namespace for
subsequent hanzo k8s commands.
"""
config = _load_clusters()
cluster_map = config.get("clusters", {})
if name not in cluster_map:
available = ", ".join(sorted(cluster_map.keys()))
console.print(f"[red]Unknown cluster:[/red] {name}")
console.print(f"Available: {available}")
raise SystemExit(1)
info = cluster_map[name]
context = info.get("context", "")
# Switch kubectl context
if context:
kubectl = shutil.which("kubectl")
if kubectl:
result = subprocess.run(
[kubectl, "config", "use-context", context],
capture_output=True,
text=True,
)
if result.returncode != 0:
console.print(
f"[yellow]Warning:[/yellow] kubectl context switch failed: "
f"{result.stderr.strip()}"
)
console.print(
"You may need to add this context first with "
"'doctl kubernetes cluster kubeconfig save'"
)
else:
console.print(f"Switched kubectl context to [cyan]{context}[/cyan]")
else:
console.print("[yellow]kubectl not found, skipping context switch.[/yellow]")
# Update current cluster in config
config["current"] = name
_save_clusters(config)
ns = info.get("namespace", name)
console.print(f"Active cluster: [bold green]{name}[/bold green] (namespace: {ns})")
+21
View File
@@ -5,6 +5,12 @@ Unrecognized subcommands pass directly through to kubectl.
Usage:
hanzo k8s auth # Fetch kubeconfig from Hanzo PaaS
hanzo k8s clusters # List known clusters
hanzo k8s use <name> # Switch active cluster
hanzo k8s services # Show service status
hanzo k8s health # Run health checks
hanzo k8s dns zones # List Cloudflare zones
hanzo k8s rollout <deploy> # Restart a deployment
hanzo k8s get pods # kubectl get pods -n hanzo
hanzo k8s logs <pod> # kubectl logs <pod> -n hanzo
hanzo k8s apply -f file.yaml # kubectl apply -f file.yaml -n hanzo
@@ -294,3 +300,18 @@ def k8s_context() -> None:
console.print(f"\nCurrent context: [green]{result.stdout.strip()}[/green]")
else:
console.print("\n[yellow]No current context set.[/yellow]")
# ── register subcommands ──────────────────────────────────────────────
from hanzo_cli.k8s.clusters import clusters, use # noqa: E402
from hanzo_cli.k8s.dns import dns # noqa: E402
from hanzo_cli.k8s.health import health # noqa: E402
from hanzo_cli.k8s.services import rollout, services # noqa: E402
k8s.add_command(clusters)
k8s.add_command(use)
k8s.add_command(health)
k8s.add_command(dns)
k8s.add_command(services)
k8s.add_command(rollout)
+285
View File
@@ -0,0 +1,285 @@
"""Cloudflare DNS management for Hanzo K8s CLI.
Reads credentials from ~/.hanzo/credentials.json under the 'cloudflare' key.
Provides commands to list zones, list A records, and batch-update IPs.
Usage:
hanzo k8s dns zones # List all zones
hanzo k8s dns list --zone hanzo.ai # List A records
hanzo k8s dns update hanzo.ai 1.2.3.4 5.6.7.8 --dry-run
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
from typing import Any
import click
import httpx
from rich.console import Console
from rich.table import Table
console = Console()
CF_API = "https://api.cloudflare.com/client/v4"
CREDENTIALS_FILE = Path.home() / ".hanzo" / "credentials.json"
def _load_cf_credentials() -> tuple[str, str]:
"""Load Cloudflare email + API key from ~/.hanzo/credentials.json.
Returns (email, api_key) or exits with an error message.
"""
if not CREDENTIALS_FILE.exists():
console.print(
f"[red]Credentials file not found:[/red] {CREDENTIALS_FILE}\n"
"Create it with a 'cloudflare' section containing "
"'email' and 'api_key'."
)
sys.exit(1)
try:
data = json.loads(CREDENTIALS_FILE.read_text())
except (json.JSONDecodeError, OSError) as e:
console.print(f"[red]Failed to read credentials:[/red] {e}")
sys.exit(1)
cf = data.get("cloudflare", {})
email = cf.get("email", "")
api_key = cf.get("api_key", "")
if not email or not api_key:
console.print(
"[red]Missing Cloudflare credentials.[/red]\n"
f"Ensure {CREDENTIALS_FILE} has:\n"
' {"cloudflare": {"email": "...", "api_key": "..."}}'
)
sys.exit(1)
return email, api_key
def _cf_headers(email: str, api_key: str) -> dict[str, str]:
"""Build Cloudflare API auth headers."""
return {
"X-Auth-Email": email,
"X-Auth-Key": api_key,
"Content-Type": "application/json",
}
def _cf_get(
client: httpx.Client, path: str, params: dict[str, Any] | None = None
) -> Any:
"""GET from Cloudflare API, handling pagination and errors."""
resp = client.get(f"{CF_API}{path}", params=params)
data = resp.json()
if not data.get("success"):
errors = data.get("errors", [])
msg = "; ".join(e.get("message", str(e)) for e in errors)
raise click.ClickException(f"Cloudflare API error: {msg}")
return data
# ── Click group ───────────────────────────────────────────────────────
@click.group("dns")
def dns() -> None:
"""Cloudflare DNS management.
List zones, view A records, and batch-update IP addresses.
Reads credentials from ~/.hanzo/credentials.json.
"""
# ── zones ─────────────────────────────────────────────────────────────
@dns.command("zones")
def dns_zones() -> None:
"""List all Cloudflare zones in the account."""
email, api_key = _load_cf_credentials()
with httpx.Client(headers=_cf_headers(email, api_key), timeout=30.0) as client:
data = _cf_get(client, "/zones", params={"per_page": "50"})
zones = data.get("result", [])
if not zones:
console.print("[yellow]No zones found.[/yellow]")
return
table = Table(title="Cloudflare Zones")
table.add_column("Name", style="cyan")
table.add_column("ID", style="dim")
table.add_column("Status", style="green")
table.add_column("Plan", style="white")
for z in sorted(zones, key=lambda x: x.get("name", "")):
table.add_row(
z.get("name", ""),
z.get("id", ""),
z.get("status", ""),
z.get("plan", {}).get("name", ""),
)
console.print(table)
# ── list ──────────────────────────────────────────────────────────────
@dns.command("list")
@click.option(
"--zone",
"-z",
required=True,
help="Zone name (e.g. hanzo.ai).",
)
@click.option(
"--type",
"record_type",
default="A",
help="DNS record type (default: A).",
)
def dns_list(zone: str, record_type: str) -> None:
"""List DNS records for a zone."""
email, api_key = _load_cf_credentials()
with httpx.Client(headers=_cf_headers(email, api_key), timeout=30.0) as client:
# Resolve zone name to zone ID
zone_id = _resolve_zone_id(client, zone)
# Fetch records
data = _cf_get(
client,
f"/zones/{zone_id}/dns_records",
params={"type": record_type, "per_page": "100"},
)
records = data.get("result", [])
if not records:
console.print(f"[yellow]No {record_type} records found for {zone}.[/yellow]")
return
table = Table(title=f"DNS Records — {zone} ({record_type})")
table.add_column("Name", style="cyan", min_width=25)
table.add_column("Content", style="white")
table.add_column("TTL", justify="right")
table.add_column("Proxied", justify="center")
table.add_column("ID", style="dim")
for r in sorted(records, key=lambda x: x.get("name", "")):
ttl = r.get("ttl", 0)
ttl_str = "Auto" if ttl == 1 else str(ttl)
proxied = "[green]yes[/green]" if r.get("proxied") else "no"
table.add_row(
r.get("name", ""),
r.get("content", ""),
ttl_str,
proxied,
r.get("id", "")[:12],
)
console.print(table)
console.print(f"\n{len(records)} record(s)")
# ── update ────────────────────────────────────────────────────────────
@dns.command("update")
@click.argument("zone")
@click.argument("old_ip")
@click.argument("new_ip")
@click.option("--dry-run", is_flag=True, help="Show changes without applying.")
def dns_update(zone: str, old_ip: str, new_ip: str, dry_run: bool) -> None:
"""Batch update A records: replace OLD_IP with NEW_IP.
Finds all A records in ZONE pointing to OLD_IP and updates them
to NEW_IP. Use --dry-run to preview changes first.
"""
email, api_key = _load_cf_credentials()
with httpx.Client(headers=_cf_headers(email, api_key), timeout=30.0) as client:
zone_id = _resolve_zone_id(client, zone)
# Fetch A records matching old IP
data = _cf_get(
client,
f"/zones/{zone_id}/dns_records",
params={"type": "A", "content": old_ip, "per_page": "100"},
)
records = data.get("result", [])
if not records:
console.print(
f"[yellow]No A records found in {zone} pointing to {old_ip}.[/yellow]"
)
return
console.print(
f"Found [cyan]{len(records)}[/cyan] A record(s) "
f"pointing to [red]{old_ip}[/red]\n"
)
table = Table(title="Records to Update")
table.add_column("Name", style="cyan")
table.add_column("Current", style="red")
table.add_column("New", style="green")
table.add_column("Proxied", justify="center")
for r in records:
proxied = "yes" if r.get("proxied") else "no"
table.add_row(r.get("name", ""), old_ip, new_ip, proxied)
console.print(table)
if dry_run:
console.print("\n[yellow]Dry run — no changes applied.[/yellow]")
return
# Apply updates
updated = 0
failed = 0
for r in records:
record_id = r["id"]
try:
resp = client.patch(
f"{CF_API}/zones/{zone_id}/dns_records/{record_id}",
json={"content": new_ip},
)
result = resp.json()
if result.get("success"):
updated += 1
else:
failed += 1
errs = result.get("errors", [])
msg = "; ".join(e.get("message", "") for e in errs)
console.print(
f" [red]Failed:[/red] {r.get('name', '')} — {msg}"
)
except Exception as e:
failed += 1
console.print(f" [red]Error:[/red] {r.get('name', '')} — {e}")
console.print(
f"\n[green]{updated} updated[/green]"
+ (f", [red]{failed} failed[/red]" if failed else "")
)
# ── helpers ───────────────────────────────────────────────────────────
def _resolve_zone_id(client: httpx.Client, zone_name: str) -> str:
"""Resolve a zone name to its Cloudflare zone ID."""
data = _cf_get(client, "/zones", params={"name": zone_name, "per_page": "1"})
zones = data.get("result", [])
if not zones:
raise click.ClickException(
f"Zone '{zone_name}' not found in Cloudflare account."
)
return zones[0]["id"]
+206
View File
@@ -0,0 +1,206 @@
"""Service health checks for Hanzo K8s CLI.
Runs concurrent HTTP health checks against known domains and displays
results in a rich table with color-coded status.
Usage:
hanzo k8s health # Check default cluster domains
hanzo k8s health --cluster adnexus # Check a specific cluster
hanzo k8s health --timeout 10 # Custom timeout
"""
from __future__ import annotations
import asyncio
import time
import click
import httpx
from rich.console import Console
from rich.table import Table
console = Console()
# Known domains per cluster
CLUSTER_DOMAINS: dict[str, list[str]] = {
"hanzo": [
"billing.hanzo.ai",
"console.hanzo.ai",
"api.hanzo.ai",
"hanzo.chat",
"kms.hanzo.ai",
"models.hanzo.ai",
"platform.hanzo.ai",
"pricing.hanzo.ai",
"status.hanzo.ai",
"analytics.hanzo.ai",
"hanzo.id",
"hanzo.bot",
"mpc.hanzo.ai",
"cloud.hanzo.ai",
"s3.hanzo.ai",
"search.hanzo.ai",
"gateway.hanzo.ai",
"hanzo.space",
],
"adnexus": [
"ad.nexus",
"api.ad.nexus",
"api-ai.ad.nexus",
"dsp.ad.nexus",
"ssp.ad.nexus",
"docs.ad.nexus",
"grafana.ad.nexus",
"id.ad.nexus",
],
"lux": [
"lux.network",
"lux.id",
],
"zoo": [
"zoo.ngo",
"zips.zoo.ngo",
],
}
async def _check_domain(
client: httpx.AsyncClient, domain: str
) -> tuple[str, int, float, str]:
"""Check a single domain. Returns (domain, status, latency_ms, error)."""
url = f"https://{domain}/"
t0 = time.monotonic()
try:
resp = await client.get(url, follow_redirects=True)
latency = (time.monotonic() - t0) * 1000
return domain, resp.status_code, latency, ""
except httpx.TimeoutException:
latency = (time.monotonic() - t0) * 1000
return domain, 0, latency, "timeout"
except httpx.ConnectError as e:
latency = (time.monotonic() - t0) * 1000
return domain, 0, latency, f"connect: {e}"
except Exception as e:
latency = (time.monotonic() - t0) * 1000
return domain, 0, latency, str(e)
async def _run_checks(
domains: list[str], timeout: float
) -> list[tuple[str, int, float, str]]:
"""Run health checks concurrently against all domains."""
async with httpx.AsyncClient(
timeout=httpx.Timeout(timeout),
verify=False, # Some services may have cert issues
headers={"User-Agent": "hanzo-cli/health"},
) as client:
tasks = [_check_domain(client, d) for d in domains]
return await asyncio.gather(*tasks)
def _status_style(code: int, error: str) -> str:
"""Return rich style string for a status code."""
if error:
return "bold red"
if 200 <= code < 300:
return "bold green"
if 300 <= code < 400:
return "yellow"
if 400 <= code < 500:
return "yellow"
return "bold red"
def _status_text(code: int, error: str) -> str:
"""Return display text for status."""
if error:
return error[:30]
return str(code)
@click.command("health")
@click.option(
"--cluster",
"-c",
default=None,
help="Cluster name (default: current cluster).",
)
@click.option(
"--timeout",
"-t",
default=15.0,
type=float,
help="HTTP timeout in seconds (default: 15).",
)
def health(cluster: str | None, timeout: float) -> None:
"""Run HTTP health checks against cluster services.
Checks all known domains for the specified cluster (or the current
active cluster) and displays a table with status codes and latency.
"""
# Resolve cluster name
if cluster is None:
from hanzo_cli.k8s.clusters import get_current_cluster
cluster, _ = get_current_cluster()
domains = CLUSTER_DOMAINS.get(cluster, [])
if not domains:
available = ", ".join(sorted(CLUSTER_DOMAINS.keys()))
console.print(
f"[yellow]No domains configured for cluster '{cluster}'.[/yellow]"
)
console.print(f"Known clusters with domains: {available}")
return
console.print(
f"Checking [cyan]{len(domains)}[/cyan] domains for "
f"[bold]{cluster}[/bold] (timeout: {timeout}s)...\n"
)
# Run async checks
results = asyncio.run(_run_checks(domains, timeout))
# Sort: errors first, then by status code descending, then by domain
results.sort(key=lambda r: (r[3] == "", r[1], r[0]))
# Build table
table = Table(title=f"Health — {cluster}")
table.add_column("Domain", style="cyan", min_width=25)
table.add_column("Status", justify="center", min_width=8)
table.add_column("Latency", justify="right", min_width=10)
table.add_column("Note", style="dim")
ok_count = 0
for domain, code, latency_ms, error in results:
style = _status_style(code, error)
status_text = _status_text(code, error)
if latency_ms >= 5000:
latency_str = f"[red]{latency_ms:.0f}ms[/red]"
elif latency_ms >= 1000:
latency_str = f"[yellow]{latency_ms:.0f}ms[/yellow]"
else:
latency_str = f"{latency_ms:.0f}ms"
note = ""
if error:
note = error[:50]
elif code >= 400:
note = "client/server error"
if not error and 200 <= code < 400:
ok_count += 1
table.add_row(domain, f"[{style}]{status_text}[/{style}]", latency_str, note)
console.print(table)
total = len(results)
if ok_count == total:
console.print(f"\n[bold green]All {total} services healthy.[/bold green]")
else:
console.print(
f"\n[bold]{ok_count}/{total}[/bold] healthy, "
f"[red]{total - ok_count} issues[/red]"
)
+239
View File
@@ -0,0 +1,239 @@
"""Service status and rollout management for Hanzo K8s CLI.
Uses kubectl to query pod and deployment status and displays results
in rich tables.
Usage:
hanzo k8s services # List pods in default namespace
hanzo k8s services --namespace kube-system
hanzo k8s rollout <deployment> # Restart a deployment
"""
from __future__ import annotations
import json
import shutil
import subprocess
import sys
from datetime import datetime, timezone
import click
from rich.console import Console
from rich.table import Table
console = Console()
def _find_kubectl() -> str:
"""Locate kubectl or exit."""
path = shutil.which("kubectl")
if not path:
console.print(
"[red]kubectl not found.[/red] "
"Install: https://kubernetes.io/docs/tasks/tools/"
)
sys.exit(1)
return path
def _kubectl_json(args: list[str]) -> dict | list:
"""Run kubectl with -o json and return parsed output."""
kubectl = _find_kubectl()
cmd = [kubectl] + args + ["-o", "json"]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
err = result.stderr.strip()
raise click.ClickException(f"kubectl failed: {err}")
if not result.stdout.strip():
return {}
return json.loads(result.stdout)
def _parse_age(timestamp: str) -> str:
"""Convert an ISO timestamp to a human-readable age string."""
try:
created = datetime.fromisoformat(timestamp.replace("Z", "+00:00"))
delta = datetime.now(timezone.utc) - created
total_seconds = int(delta.total_seconds())
if total_seconds < 60:
return f"{total_seconds}s"
if total_seconds < 3600:
return f"{total_seconds // 60}m"
if total_seconds < 86400:
return f"{total_seconds // 3600}h"
days = total_seconds // 86400
if days < 30:
return f"{days}d"
return f"{days // 30}mo"
except (ValueError, TypeError):
return "?"
def _status_style(phase: str) -> str:
"""Return rich style for a pod phase."""
phase_lower = phase.lower()
if phase_lower == "running":
return "green"
if phase_lower in ("pending", "containercreating"):
return "yellow"
if "error" in phase_lower or "crash" in phase_lower or "backoff" in phase_lower:
return "red"
if phase_lower in ("succeeded", "completed"):
return "cyan"
return "white"
@click.command("services")
@click.option(
"--namespace",
"-n",
default=None,
help="Kubernetes namespace (default: current cluster namespace).",
)
def services(namespace: str | None) -> None:
"""Show service status from pod information.
Lists pods grouped by service (app label), showing replica counts,
status, age, and restart counts.
"""
if namespace is None:
from hanzo_cli.k8s.clusters import get_current_cluster
_, cluster_info = get_current_cluster()
namespace = cluster_info.get("namespace", "hanzo")
try:
data = _kubectl_json(["get", "pods", "-n", namespace])
except click.ClickException as e:
console.print(f"[red]{e.message}[/red]")
return
items = data.get("items", [])
if not items:
console.print(f"[yellow]No pods found in namespace '{namespace}'.[/yellow]")
return
# Group pods by service name (app label)
svc_map: dict[str, list[dict]] = {}
for pod in items:
labels = pod.get("metadata", {}).get("labels", {})
svc_name = (
labels.get("app")
or labels.get("app.kubernetes.io/name")
or labels.get("name")
or pod.get("metadata", {}).get("name", "unknown")
)
svc_map.setdefault(svc_name, []).append(pod)
table = Table(title=f"Services — {namespace}")
table.add_column("Service", style="cyan", min_width=20)
table.add_column("Ready", justify="center", min_width=8)
table.add_column("Status", justify="center", min_width=14)
table.add_column("Age", justify="right", min_width=6)
table.add_column("Restarts", justify="right", min_width=9)
for svc_name in sorted(svc_map.keys()):
pods = svc_map[svc_name]
total = len(pods)
ready = 0
total_restarts = 0
worst_phase = "Running"
for pod in pods:
status = pod.get("status", {})
phase = status.get("phase", "Unknown")
# Check container statuses for more detail
container_statuses = status.get("containerStatuses", [])
pod_ready = True
for cs in container_statuses:
total_restarts += cs.get("restartCount", 0)
if not cs.get("ready", False):
pod_ready = False
# Detect CrashLoopBackOff
waiting = cs.get("state", {}).get("waiting", {})
reason = waiting.get("reason", "")
if reason:
phase = reason
if pod_ready and phase == "Running":
ready += 1
# Track worst phase for display
if phase != "Running":
worst_phase = phase
# Pick display phase
if ready == total:
display_phase = "Running"
elif ready > 0:
display_phase = f"{worst_phase}" if worst_phase != "Running" else "Partial"
else:
display_phase = worst_phase
style = _status_style(display_phase)
ready_str = f"{ready}/{total}"
# Age from oldest pod
oldest = min(
(
p.get("metadata", {}).get("creationTimestamp", "")
for p in pods
),
default="",
)
age_str = _parse_age(oldest) if oldest else "?"
restart_style = "red" if total_restarts > 10 else "yellow" if total_restarts > 0 else "dim"
table.add_row(
svc_name,
ready_str,
f"[{style}]{display_phase}[/{style}]",
age_str,
f"[{restart_style}]{total_restarts}[/{restart_style}]",
)
console.print(table)
console.print(f"\n{len(svc_map)} service(s), {len(items)} pod(s)")
@click.command("rollout")
@click.argument("deployment")
@click.option(
"--namespace",
"-n",
default=None,
help="Kubernetes namespace (default: current cluster namespace).",
)
def rollout(deployment: str, namespace: str | None) -> None:
"""Trigger a rolling restart of a deployment.
Runs `kubectl rollout restart deployment/<name>` in the specified
namespace.
"""
if namespace is None:
from hanzo_cli.k8s.clusters import get_current_cluster
_, cluster_info = get_current_cluster()
namespace = cluster_info.get("namespace", "hanzo")
kubectl = _find_kubectl()
resource = deployment if "/" in deployment else f"deployment/{deployment}"
console.print(
f"Restarting [cyan]{resource}[/cyan] in namespace [cyan]{namespace}[/cyan]..."
)
result = subprocess.run(
[kubectl, "rollout", "restart", resource, "-n", namespace],
capture_output=True,
text=True,
)
if result.returncode != 0:
console.print(f"[red]Failed:[/red] {result.stderr.strip()}")
raise SystemExit(1)
console.print(f"[green]{result.stdout.strip()}[/green]")
+1081
View File
File diff suppressed because it is too large Load Diff
+24
View File
@@ -163,6 +163,30 @@ def register_all_tools(
# MCP tools
resolved_enabled_tools["mcp"] = is_tool_enabled("mcp", True)
# Billing tools
for tool in PACKAGE_TOOL_PREFIXES.get("billing", []):
resolved_enabled_tools[tool] = is_tool_enabled(tool, True)
# Commerce tools
for tool in PACKAGE_TOOL_PREFIXES.get("commerce", []):
resolved_enabled_tools[tool] = is_tool_enabled(tool, True)
# IAM tools
for tool in PACKAGE_TOOL_PREFIXES.get("iam_tools", []):
resolved_enabled_tools[tool] = is_tool_enabled(tool, True)
# Ingress tools
for tool in PACKAGE_TOOL_PREFIXES.get("ingress", []):
resolved_enabled_tools[tool] = is_tool_enabled(tool, True)
# MPC tools
for tool in PACKAGE_TOOL_PREFIXES.get("mpc", []):
resolved_enabled_tools[tool] = is_tool_enabled(tool, True)
# Team tools
for tool in PACKAGE_TOOL_PREFIXES.get("team", []):
resolved_enabled_tools[tool] = is_tool_enabled(tool, True)
# Jupyter tools
for tool in PACKAGE_TOOL_PREFIXES.get("jupyter", []):
resolved_enabled_tools[tool] = is_tool_enabled(tool, True)
@@ -59,6 +59,12 @@ PACKAGE_TOOL_PREFIXES: dict[str, list[str]] = {
"auth": ["auth"],
"kms": ["kms"],
"paas": ["paas"],
"billing": ["billing"],
"commerce": ["commerce"],
"iam_tools": ["iam"],
"ingress": ["ingress"],
"mpc": ["mpc"],
"team": ["team"],
}
+7 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "hanzo-mcp"
version = "0.11.8"
version = "0.12.0"
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"
@@ -43,6 +43,12 @@ dependencies = [
"hanzo-tools-auth>=0.1.0",
"hanzo-tools-kms>=0.1.0",
"hanzo-tools-paas>=0.1.0",
"hanzo-tools-billing>=0.1.0",
"hanzo-tools-commerce>=0.1.0",
"hanzo-tools-iam>=0.1.0",
"hanzo-tools-ingress>=0.1.0",
"hanzo-tools-mpc>=0.1.0",
"hanzo-tools-team>=0.1.0",
"hanzo-persona>=1.0.0",
"uvloop>=0.22.1",
]
+11
View File
@@ -0,0 +1,11 @@
# hanzo-tools-billing
MCP tool package for hanzo-mcp. Provides native billing management via the Hanzo platform.
## Installation
```bash
pip install hanzo-tools-billing
```
Part of the [hanzo-mcp](https://pypi.org/project/hanzo-mcp/) ecosystem.
@@ -0,0 +1 @@
@@ -0,0 +1,7 @@
"""Hanzo Billing Tools -- balance, usage, plans, subscriptions, and invoices via MCP."""
from .billing_tool import BillingTool
TOOLS = [BillingTool]
__all__ = ["BillingTool", "TOOLS"]
@@ -0,0 +1,306 @@
"""MCP tool for Hanzo Billing -- balance, usage, plans, subscriptions, invoices.
Wraps the Hanzo Commerce billing API at api.hanzo.ai/api/v1/billing/.
Auth: Uses HanzoSession from hanzo-tools-auth for bearer tokens.
"""
from __future__ import annotations
import json
import logging
import os
from typing import Annotated, Any, final
from mcp.server import FastMCP
from mcp.server.fastmcp import Context as MCPContext
from pydantic import Field
from hanzo_tools.core.base import BaseTool
logger = logging.getLogger(__name__)
DESCRIPTION = """Hanzo Billing -- account balance, usage, plans, subscriptions, and invoices.
Requires authentication via `hanzo login` (stored at ~/.hanzo/auth/token.json).
Actions:
- balance: Get current billing balance
- usage: Get usage summary (period param: current, previous, or YYYY-MM)
- plans: List available plans
- subscriptions: List all subscriptions
- subscription: Get subscription by ID (subscription_id required)
- invoices: List invoices
- invoice: Get invoice by ID (invoice_id required)
- payment_methods: List payment methods on file
- spend_alerts: List configured spend alerts
- credit_balance: Get credit/promotional balance
- meters: List usage meters
- deposit: Add a deposit (amount required)
- credit: Grant starter credit to account
"""
API_BASE = "https://api.hanzo.ai/api/v1/billing"
def _get_session():
"""Get HanzoSession singleton."""
from hanzo_tools.auth.session import HanzoSession
return HanzoSession.get()
def _api_base() -> str:
"""Get billing API base URL (overridable via env)."""
return os.getenv("HANZO_BILLING_API_URL", API_BASE).rstrip("/")
def _request(method: str, path: str, token: str, **kwargs: Any) -> Any:
"""Make an authenticated HTTP request to the billing API."""
import httpx
url = f"{_api_base()}{path}"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"User-Agent": "hanzo-mcp/0.1",
}
with httpx.Client(timeout=30.0) as client:
resp = client.request(method, url, headers=headers, **kwargs)
if resp.status_code >= 400:
try:
err = resp.json()
msg = err.get("error", err.get("message", resp.text))
except Exception:
msg = resp.text
raise RuntimeError(f"Billing API error {resp.status_code}: {msg}")
if not resp.content or resp.status_code == 204:
return {}
return resp.json()
def _get(path: str, token: str) -> Any:
return _request("GET", path, token)
def _post(path: str, token: str, **kwargs: Any) -> Any:
return _request("POST", path, token, **kwargs)
@final
class BillingTool(BaseTool):
"""MCP tool for Hanzo billing operations."""
@property
def name(self) -> str:
return "billing"
@property
def description(self) -> str:
return DESCRIPTION
def _get_token(self) -> str:
"""Get auth token or raise."""
session = _get_session()
token = session.get_iam_token()
if not token:
raise RuntimeError("Not authenticated. Run 'hanzo login' first.")
return token
async def call(
self,
ctx: MCPContext,
action: str = "balance",
user: str | None = None,
subscription_id: str | None = None,
invoice_id: str | None = None,
period: str | None = None,
amount: float | None = None,
**kwargs: Any,
) -> str:
try:
if action == "balance":
return await self._balance(user)
elif action == "usage":
return await self._usage(user, period)
elif action == "plans":
return await self._plans()
elif action == "subscriptions":
return await self._subscriptions(user)
elif action == "subscription":
return await self._subscription(subscription_id)
elif action == "invoices":
return await self._invoices(user)
elif action == "invoice":
return await self._invoice(invoice_id)
elif action == "payment_methods":
return await self._payment_methods(user)
elif action == "spend_alerts":
return await self._spend_alerts(user)
elif action == "credit_balance":
return await self._credit_balance(user)
elif action == "meters":
return await self._meters(user)
elif action == "deposit":
return await self._deposit(user, amount)
elif action == "credit":
return await self._credit(user)
else:
return json.dumps({
"error": f"Unknown action: {action}",
"available": [
"balance", "usage", "plans", "subscriptions", "subscription",
"invoices", "invoice", "payment_methods", "spend_alerts",
"credit_balance", "meters", "deposit", "credit",
],
})
except RuntimeError as e:
return json.dumps({"error": str(e)})
except Exception as e:
logger.exception(f"Billing tool error: {e}")
return json.dumps({"error": f"Billing error: {e}"})
# -- Actions -------------------------------------------------------------
async def _balance(self, user: str | None) -> str:
token = self._get_token()
params = f"?user={user}" if user else ""
data = _get(f"/balance{params}", token)
return json.dumps(data, indent=2)
async def _usage(self, user: str | None, period: str | None) -> str:
token = self._get_token()
parts = []
if user:
parts.append(f"user={user}")
if period:
parts.append(f"period={period}")
qs = f"?{'&'.join(parts)}" if parts else ""
data = _get(f"/usage{qs}", token)
return json.dumps(data, indent=2)
async def _plans(self) -> str:
token = self._get_token()
data = _get("/plans", token)
return json.dumps(data, indent=2)
async def _subscriptions(self, user: str | None) -> str:
token = self._get_token()
params = f"?user={user}" if user else ""
data = _get(f"/subscriptions{params}", token)
return json.dumps(data, indent=2)
async def _subscription(self, subscription_id: str | None) -> str:
if not subscription_id:
return json.dumps({"error": "Required: subscription_id"})
token = self._get_token()
data = _get(f"/subscriptions/{subscription_id}", token)
return json.dumps(data, indent=2)
async def _invoices(self, user: str | None) -> str:
token = self._get_token()
params = f"?user={user}" if user else ""
data = _get(f"/invoices{params}", token)
return json.dumps(data, indent=2)
async def _invoice(self, invoice_id: str | None) -> str:
if not invoice_id:
return json.dumps({"error": "Required: invoice_id"})
token = self._get_token()
data = _get(f"/invoices/{invoice_id}", token)
return json.dumps(data, indent=2)
async def _payment_methods(self, user: str | None) -> str:
token = self._get_token()
params = f"?user={user}" if user else ""
data = _get(f"/payment-methods{params}", token)
return json.dumps(data, indent=2)
async def _spend_alerts(self, user: str | None) -> str:
token = self._get_token()
params = f"?user={user}" if user else ""
data = _get(f"/spend-alerts{params}", token)
return json.dumps(data, indent=2)
async def _credit_balance(self, user: str | None) -> str:
token = self._get_token()
params = f"?user={user}" if user else ""
data = _get(f"/credits{params}", token)
return json.dumps(data, indent=2)
async def _meters(self, user: str | None) -> str:
token = self._get_token()
params = f"?user={user}" if user else ""
data = _get(f"/meters{params}", token)
return json.dumps(data, indent=2)
async def _deposit(self, user: str | None, amount: float | None) -> str:
if not amount or amount <= 0:
return json.dumps({"error": "Required: amount (positive number)"})
token = self._get_token()
body: dict[str, Any] = {"amount": amount}
if user:
body["user"] = user
data = _post("/deposit", token, json=body)
return json.dumps(data, indent=2)
async def _credit(self, user: str | None) -> str:
token = self._get_token()
body: dict[str, Any] = {}
if user:
body["user"] = user
data = _post("/credit", token, json=body)
return json.dumps(data, indent=2)
# -- Registration --------------------------------------------------------
def register(self, mcp_server: FastMCP) -> None:
"""Register billing tool with explicit parameters."""
tool_instance = self
@mcp_server.tool(
name="billing",
description=DESCRIPTION,
)
async def billing(
action: Annotated[
str,
Field(
description=(
"Action to perform. "
"balance, usage, plans, subscriptions, subscription, "
"invoices, invoice, payment_methods, spend_alerts, "
"credit_balance, meters, deposit, credit."
),
),
] = "balance",
user: Annotated[
str | None,
Field(description="User identifier (org/username) to scope queries"),
] = None,
subscription_id: Annotated[
str | None,
Field(description="Subscription ID (for subscription action)"),
] = None,
invoice_id: Annotated[
str | None,
Field(description="Invoice ID (for invoice action)"),
] = None,
period: Annotated[
str | None,
Field(description="Usage period: current, previous, or YYYY-MM (for usage action)"),
] = None,
amount: Annotated[
float | None,
Field(description="Deposit amount in USD (for deposit action)"),
] = None,
ctx: MCPContext = None,
) -> str:
return await tool_instance.call(
ctx,
action=action,
user=user,
subscription_id=subscription_id,
invoice_id=invoice_id,
period=period,
amount=amount,
)
+32
View File
@@ -0,0 +1,32 @@
[project]
name = "hanzo-tools-billing"
version = "0.1.0"
description = "Hanzo Billing MCP tool — balance, usage, plans, subscriptions, invoices, and payments"
readme = "README.md"
requires-python = ">=3.12"
license = { text = "MIT" }
authors = [{ name = "Hanzo AI", email = "[email protected]" }]
keywords = ["hanzo", "mcp", "billing", "subscriptions", "invoices", "tools"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
]
dependencies = [
"hanzo-tools-core>=0.1.0",
"hanzo-tools-auth>=0.1.0",
"httpx>=0.27.0",
]
[project.entry-points."hanzo.tools"]
billing = "hanzo_tools.billing:TOOLS"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["hanzo_tools"]
+11
View File
@@ -0,0 +1,11 @@
# hanzo-tools-commerce
MCP tool package for hanzo-mcp. Provides native commerce management via the Hanzo platform.
## Installation
```bash
pip install hanzo-tools-commerce
```
Part of the [hanzo-mcp](https://pypi.org/project/hanzo-mcp/) ecosystem.
@@ -0,0 +1 @@
@@ -0,0 +1,7 @@
"""Hanzo Commerce Tools -- orders, products, collections, stores, and discounts via MCP."""
from .commerce_tool import CommerceTool
TOOLS = [CommerceTool]
__all__ = ["CommerceTool", "TOOLS"]
@@ -0,0 +1,249 @@
"""MCP tool for Hanzo Commerce -- orders, products, collections, stores.
Wraps the Hanzo Commerce API at api.hanzo.ai/api/v1/.
Auth: Uses HanzoSession from hanzo-tools-auth for bearer tokens.
"""
from __future__ import annotations
import json
import logging
import os
from typing import Annotated, Any, final
from mcp.server import FastMCP
from mcp.server.fastmcp import Context as MCPContext
from pydantic import Field
from hanzo_tools.core.base import BaseTool
logger = logging.getLogger(__name__)
DESCRIPTION = """Hanzo Commerce -- orders, products, collections, stores, and discounts.
Requires authentication via `hanzo login` (stored at ~/.hanzo/auth/token.json).
Actions:
- orders: List orders (optional query param for filtering)
- order: Get order by ID (order_id required)
- products: List products
- product: Get product by ID (product_id required)
- collections: List product collections
- search_users: Search users (query required)
- search_orders: Search orders (query required)
- stores: List stores
- discounts: List discount codes
- webhooks: List configured webhooks
"""
API_BASE = "https://api.hanzo.ai/api/v1"
def _get_session():
"""Get HanzoSession singleton."""
from hanzo_tools.auth.session import HanzoSession
return HanzoSession.get()
def _api_base() -> str:
"""Get commerce API base URL (overridable via env)."""
return os.getenv("HANZO_COMMERCE_API_URL", API_BASE).rstrip("/")
def _request(method: str, path: str, token: str, **kwargs: Any) -> Any:
"""Make an authenticated HTTP request to the commerce API."""
import httpx
url = f"{_api_base()}{path}"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"User-Agent": "hanzo-mcp/0.1",
}
with httpx.Client(timeout=30.0) as client:
resp = client.request(method, url, headers=headers, **kwargs)
if resp.status_code >= 400:
try:
err = resp.json()
msg = err.get("error", err.get("message", resp.text))
except Exception:
msg = resp.text
raise RuntimeError(f"Commerce API error {resp.status_code}: {msg}")
if not resp.content or resp.status_code == 204:
return {}
return resp.json()
def _get(path: str, token: str, params: dict[str, str] | None = None) -> Any:
return _request("GET", path, token, params=params)
@final
class CommerceTool(BaseTool):
"""MCP tool for Hanzo commerce operations."""
@property
def name(self) -> str:
return "commerce"
@property
def description(self) -> str:
return DESCRIPTION
def _get_token(self) -> str:
"""Get auth token or raise."""
session = _get_session()
token = session.get_iam_token()
if not token:
raise RuntimeError("Not authenticated. Run 'hanzo login' first.")
return token
async def call(
self,
ctx: MCPContext,
action: str = "orders",
order_id: str | None = None,
product_id: str | None = None,
query: str | None = None,
**kwargs: Any,
) -> str:
try:
if action == "orders":
return await self._orders(query)
elif action == "order":
return await self._order(order_id)
elif action == "products":
return await self._products(query)
elif action == "product":
return await self._product(product_id)
elif action == "collections":
return await self._collections()
elif action == "search_users":
return await self._search_users(query)
elif action == "search_orders":
return await self._search_orders(query)
elif action == "stores":
return await self._stores()
elif action == "discounts":
return await self._discounts()
elif action == "webhooks":
return await self._webhooks()
else:
return json.dumps({
"error": f"Unknown action: {action}",
"available": [
"orders", "order", "products", "product", "collections",
"search_users", "search_orders", "stores", "discounts", "webhooks",
],
})
except RuntimeError as e:
return json.dumps({"error": str(e)})
except Exception as e:
logger.exception(f"Commerce tool error: {e}")
return json.dumps({"error": f"Commerce error: {e}"})
# -- Actions -------------------------------------------------------------
async def _orders(self, query: str | None) -> str:
token = self._get_token()
params = {"q": query} if query else None
data = _get("/orders", token, params=params)
return json.dumps(data, indent=2)
async def _order(self, order_id: str | None) -> str:
if not order_id:
return json.dumps({"error": "Required: order_id"})
token = self._get_token()
data = _get(f"/orders/{order_id}", token)
return json.dumps(data, indent=2)
async def _products(self, query: str | None) -> str:
token = self._get_token()
params = {"q": query} if query else None
data = _get("/products", token, params=params)
return json.dumps(data, indent=2)
async def _product(self, product_id: str | None) -> str:
if not product_id:
return json.dumps({"error": "Required: product_id"})
token = self._get_token()
data = _get(f"/products/{product_id}", token)
return json.dumps(data, indent=2)
async def _collections(self) -> str:
token = self._get_token()
data = _get("/collections", token)
return json.dumps(data, indent=2)
async def _search_users(self, query: str | None) -> str:
if not query:
return json.dumps({"error": "Required: query (search term)"})
token = self._get_token()
data = _get("/users", token, params={"q": query})
return json.dumps(data, indent=2)
async def _search_orders(self, query: str | None) -> str:
if not query:
return json.dumps({"error": "Required: query (search term)"})
token = self._get_token()
data = _get("/orders", token, params={"q": query})
return json.dumps(data, indent=2)
async def _stores(self) -> str:
token = self._get_token()
data = _get("/stores", token)
return json.dumps(data, indent=2)
async def _discounts(self) -> str:
token = self._get_token()
data = _get("/discounts", token)
return json.dumps(data, indent=2)
async def _webhooks(self) -> str:
token = self._get_token()
data = _get("/webhooks", token)
return json.dumps(data, indent=2)
# -- Registration --------------------------------------------------------
def register(self, mcp_server: FastMCP) -> None:
"""Register commerce tool with explicit parameters."""
tool_instance = self
@mcp_server.tool(
name="commerce",
description=DESCRIPTION,
)
async def commerce(
action: Annotated[
str,
Field(
description=(
"Action to perform. "
"orders, order, products, product, collections, "
"search_users, search_orders, stores, discounts, webhooks."
),
),
] = "orders",
order_id: Annotated[
str | None,
Field(description="Order ID (for order action)"),
] = None,
product_id: Annotated[
str | None,
Field(description="Product ID (for product action)"),
] = None,
query: Annotated[
str | None,
Field(description="Search query (for search_users, search_orders, or filtering orders/products)"),
] = None,
ctx: MCPContext = None,
) -> str:
return await tool_instance.call(
ctx,
action=action,
order_id=order_id,
product_id=product_id,
query=query,
)
+32
View File
@@ -0,0 +1,32 @@
[project]
name = "hanzo-tools-commerce"
version = "0.1.0"
description = "Hanzo Commerce MCP tool -- orders, products, collections, stores, and discounts"
readme = "README.md"
requires-python = ">=3.12"
license = { text = "MIT" }
authors = [{ name = "Hanzo AI", email = "[email protected]" }]
keywords = ["hanzo", "mcp", "commerce", "orders", "products", "tools"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
]
dependencies = [
"hanzo-tools-core>=0.1.0",
"hanzo-tools-auth>=0.1.0",
"httpx>=0.27.0",
]
[project.entry-points."hanzo.tools"]
commerce = "hanzo_tools.commerce:TOOLS"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["hanzo_tools"]
+11
View File
@@ -0,0 +1,11 @@
# hanzo-tools-iam
MCP tool package for hanzo-mcp. Provides native iam management via the Hanzo platform.
## Installation
```bash
pip install hanzo-tools-iam
```
Part of the [hanzo-mcp](https://pypi.org/project/hanzo-mcp/) ecosystem.
@@ -0,0 +1,7 @@
"""Hanzo IAM Tools — identity and access management via MCP."""
from .iam_tool import IAMTool
TOOLS = [IAMTool]
__all__ = ["IAMTool", "TOOLS"]
@@ -0,0 +1,614 @@
"""MCP tool for Hanzo IAM — identity and access management.
Full control over users, organizations, roles, permissions, providers,
applications, tokens, sessions, invitations, and audit records.
Auth: Uses HanzoSession from hanzo-tools-auth for Bearer JWT tokens.
Backend: Casdoor IAM at hanzo.id/api/
"""
from __future__ import annotations
import json
import logging
import os
from typing import Annotated, Any, final
import httpx
from mcp.server import FastMCP
from mcp.server.fastmcp import Context as MCPContext
from pydantic import Field
from hanzo_tools.core.base import BaseTool
logger = logging.getLogger(__name__)
IAM_BASE_URL = os.getenv("HANZO_IAM_URL", "https://hanzo.id")
DESCRIPTION = """Hanzo IAM — identity and access management.
Requires authentication via `hanzo login` (stored at ~/.hanzo/auth/token.json).
User actions:
- users: List users (params: owner)
- user: Get user by ID (params: id)
- create_user: Create a user (params: owner, name, email, password, display_name)
- update_user: Update a user (params: id, plus fields to update)
- delete_user: Delete a user (params: id)
Organization actions:
- orgs: List organizations
- org: Get organization details (params: id)
Role and permission actions:
- roles: List roles
- role: Get role details (params: id)
- permissions: List permissions
- enforce: Check permission (params: owner, model, resource, action)
Provider and application actions:
- providers: List auth providers
- apps: List applications
Token and session actions:
- tokens: List tokens
- sessions: List sessions
Invitation actions:
- invitations: List invitations
- invite: Send invitation (params: email, org)
Audit and system actions:
- records: List audit records
- system_info: Get IAM system info
- health: Health check
"""
def _get_session():
"""Get HanzoSession singleton."""
from hanzo_tools.auth.session import HanzoSession
return HanzoSession.get()
def _iam_url(path: str) -> str:
"""Build full IAM API URL."""
return f"{IAM_BASE_URL}/api/{path.lstrip('/')}"
def _auth_headers(token: str) -> dict[str, str]:
"""Build auth headers with Bearer token."""
return {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"User-Agent": "hanzo-mcp/0.1",
}
async def _iam_get(path: str, params: dict[str, Any] | None = None) -> Any:
"""GET request to IAM API."""
session = _get_session()
token = session.get_iam_token()
if not token:
raise RuntimeError("Not authenticated. Run 'hanzo login' first.")
async with httpx.AsyncClient(timeout=30.0) as client:
resp = await client.get(
_iam_url(path),
headers=_auth_headers(token),
params=params,
)
resp.raise_for_status()
return resp.json()
async def _iam_post(path: str, body: dict[str, Any] | None = None) -> Any:
"""POST request to IAM API."""
session = _get_session()
token = session.get_iam_token()
if not token:
raise RuntimeError("Not authenticated. Run 'hanzo login' first.")
async with httpx.AsyncClient(timeout=30.0) as client:
resp = await client.post(
_iam_url(path),
headers=_auth_headers(token),
json=body or {},
)
resp.raise_for_status()
return resp.json()
async def _iam_delete(path: str, body: dict[str, Any] | None = None) -> Any:
"""DELETE request to IAM API."""
session = _get_session()
token = session.get_iam_token()
if not token:
raise RuntimeError("Not authenticated. Run 'hanzo login' first.")
async with httpx.AsyncClient(timeout=30.0) as client:
resp = await client.post(
_iam_url(path),
headers=_auth_headers(token),
json=body or {},
)
resp.raise_for_status()
return resp.json()
@final
class IAMTool(BaseTool):
"""MCP tool for Hanzo IAM operations."""
@property
def name(self) -> str:
return "iam"
@property
def description(self) -> str:
return DESCRIPTION
async def call(
self,
ctx: MCPContext,
action: str = "health",
id: str | None = None,
owner: str | None = None,
name: str | None = None,
email: str | None = None,
password: str | None = None,
display_name: str | None = None,
org: str | None = None,
model: str | None = None,
resource: str | None = None,
permission_action: str | None = None,
**kwargs: Any,
) -> str:
try:
# User actions
if action == "users":
return await self._users(owner)
elif action == "user":
return await self._user(id)
elif action == "create_user":
return await self._create_user(owner, name, email, password, display_name)
elif action == "update_user":
return await self._update_user(id, name, email, display_name, **kwargs)
elif action == "delete_user":
return await self._delete_user(id)
# Organization actions
elif action == "orgs":
return await self._orgs()
elif action == "org":
return await self._org(id)
# Role and permission actions
elif action == "roles":
return await self._roles()
elif action == "role":
return await self._role(id)
elif action == "permissions":
return await self._permissions()
elif action == "enforce":
return await self._enforce(owner, model, resource, permission_action)
# Provider and application actions
elif action == "providers":
return await self._providers()
elif action == "apps":
return await self._apps()
# Token and session actions
elif action == "tokens":
return await self._tokens()
elif action == "sessions":
return await self._sessions()
# Invitation actions
elif action == "invitations":
return await self._invitations()
elif action == "invite":
return await self._invite(email, org)
# Audit and system actions
elif action == "records":
return await self._records()
elif action == "system_info":
return await self._system_info()
elif action == "health":
return await self._health()
else:
return json.dumps({
"error": f"Unknown action: {action}",
"available": [
"users", "user", "create_user", "update_user", "delete_user",
"orgs", "org",
"roles", "role", "permissions", "enforce",
"providers", "apps",
"tokens", "sessions",
"invitations", "invite",
"records", "system_info", "health",
],
})
except RuntimeError as e:
return json.dumps({"error": str(e)})
except httpx.HTTPStatusError as e:
body = e.response.text
try:
body = e.response.json()
except Exception:
pass
return json.dumps({"error": f"IAM API error {e.response.status_code}", "detail": body})
except Exception as e:
logger.exception(f"IAM tool error: {e}")
return json.dumps({"error": f"IAM error: {e}"})
# -- User actions --------------------------------------------------------
async def _users(self, owner: str | None) -> str:
owner = owner or "hanzo"
data = await _iam_get("get-users", params={"owner": owner})
users = data if isinstance(data, list) else []
result = []
for u in users:
result.append({
"id": u.get("id"),
"name": u.get("name"),
"email": u.get("email"),
"displayName": u.get("displayName"),
"createdTime": u.get("createdTime"),
})
return json.dumps({"owner": owner, "count": len(result), "users": result}, indent=2)
async def _user(self, id: str | None) -> str:
if not id:
return json.dumps({"error": "Required: id (user ID, format: org/username)"})
data = await _iam_get("get-user", params={"id": id})
return json.dumps(data, indent=2)
async def _create_user(
self,
owner: str | None,
name: str | None,
email: str | None,
password: str | None,
display_name: str | None,
) -> str:
if not name or not email:
return json.dumps({"error": "Required: name, email. Optional: owner, password, display_name"})
user_data = {
"owner": owner or "hanzo",
"name": name,
"email": email,
"displayName": display_name or name,
}
if password:
user_data["password"] = password
data = await _iam_post("add-user", body={"user": user_data})
return json.dumps({"action": "created", "result": data}, indent=2)
async def _update_user(
self,
id: str | None,
name: str | None,
email: str | None,
display_name: str | None,
**kwargs: Any,
) -> str:
if not id:
return json.dumps({"error": "Required: id (user ID, format: org/username)"})
# Fetch current user first
current = await _iam_get("get-user", params={"id": id})
if not isinstance(current, dict):
return json.dumps({"error": f"User not found: {id}"})
# Apply updates
if name is not None:
current["name"] = name
if email is not None:
current["email"] = email
if display_name is not None:
current["displayName"] = display_name
for k, v in kwargs.items():
if v is not None:
current[k] = v
data = await _iam_post("update-user", body={"user": current})
return json.dumps({"action": "updated", "id": id, "result": data}, indent=2)
async def _delete_user(self, id: str | None) -> str:
if not id:
return json.dumps({"error": "Required: id (user ID, format: org/username)"})
# Fetch user to get full object for deletion
current = await _iam_get("get-user", params={"id": id})
if not isinstance(current, dict):
return json.dumps({"error": f"User not found: {id}"})
data = await _iam_delete("delete-user", body={"user": current})
return json.dumps({"action": "deleted", "id": id, "result": data}, indent=2)
# -- Organization actions ------------------------------------------------
async def _orgs(self) -> str:
data = await _iam_get("get-organizations", params={"owner": "admin"})
orgs = data if isinstance(data, list) else []
result = []
for o in orgs:
result.append({
"name": o.get("name"),
"displayName": o.get("displayName"),
"websiteUrl": o.get("websiteUrl"),
"createdTime": o.get("createdTime"),
})
return json.dumps({"count": len(result), "organizations": result}, indent=2)
async def _org(self, id: str | None) -> str:
if not id:
return json.dumps({"error": "Required: id (organization ID, format: admin/org-name)"})
data = await _iam_get("get-organization", params={"id": id})
return json.dumps(data, indent=2)
# -- Role and permission actions -----------------------------------------
async def _roles(self) -> str:
data = await _iam_get("get-roles", params={"owner": "hanzo"})
roles = data if isinstance(data, list) else []
result = []
for r in roles:
result.append({
"name": r.get("name"),
"displayName": r.get("displayName"),
"users": len(r.get("users", [])),
"roles": len(r.get("roles", [])),
})
return json.dumps({"count": len(result), "roles": result}, indent=2)
async def _role(self, id: str | None) -> str:
if not id:
return json.dumps({"error": "Required: id (role ID, format: org/role-name)"})
data = await _iam_get("get-role", params={"id": id})
return json.dumps(data, indent=2)
async def _permissions(self) -> str:
data = await _iam_get("get-permissions", params={"owner": "hanzo"})
perms = data if isinstance(data, list) else []
result = []
for p in perms:
result.append({
"name": p.get("name"),
"displayName": p.get("displayName"),
"resources": p.get("resources", []),
"actions": p.get("actions", []),
"effect": p.get("effect"),
})
return json.dumps({"count": len(result), "permissions": result}, indent=2)
async def _enforce(
self,
owner: str | None,
model: str | None,
resource: str | None,
action: str | None,
) -> str:
if not model or not resource or not action:
return json.dumps({"error": "Required: model, resource, permission_action. Optional: owner"})
data = await _iam_get("enforce", params={
"owner": owner or "hanzo",
"model": model,
"resource": resource,
"action": action,
})
return json.dumps({
"allowed": data,
"model": model,
"resource": resource,
"action": action,
}, indent=2)
# -- Provider and application actions ------------------------------------
async def _providers(self) -> str:
data = await _iam_get("get-providers", params={"owner": "admin"})
providers = data if isinstance(data, list) else []
result = []
for p in providers:
result.append({
"name": p.get("name"),
"displayName": p.get("displayName"),
"type": p.get("type"),
"category": p.get("category"),
})
return json.dumps({"count": len(result), "providers": result}, indent=2)
async def _apps(self) -> str:
data = await _iam_get("get-applications", params={"owner": "admin"})
apps = data if isinstance(data, list) else []
result = []
for a in apps:
result.append({
"name": a.get("name"),
"displayName": a.get("displayName"),
"organization": a.get("organization"),
"clientId": a.get("clientId"),
})
return json.dumps({"count": len(result), "applications": result}, indent=2)
# -- Token and session actions -------------------------------------------
async def _tokens(self) -> str:
data = await _iam_get("get-tokens", params={"owner": "admin"})
tokens = data if isinstance(data, list) else []
result = []
for t in tokens:
result.append({
"name": t.get("name"),
"user": t.get("user"),
"application": t.get("application"),
"createdTime": t.get("createdTime"),
"expiresIn": t.get("expiresIn"),
})
return json.dumps({"count": len(result), "tokens": result}, indent=2)
async def _sessions(self) -> str:
data = await _iam_get("get-sessions", params={"owner": "admin"})
sessions = data if isinstance(data, list) else []
result = []
for s in sessions:
result.append({
"name": s.get("name"),
"application": s.get("application"),
"createdTime": s.get("createdTime"),
"sessionId": s.get("sessionId", []),
})
return json.dumps({"count": len(result), "sessions": result}, indent=2)
# -- Invitation actions --------------------------------------------------
async def _invitations(self) -> str:
data = await _iam_get("get-invitations", params={"owner": "admin"})
invitations = data if isinstance(data, list) else []
result = []
for i in invitations:
result.append({
"name": i.get("name"),
"email": i.get("email"),
"state": i.get("state"),
"createdTime": i.get("createdTime"),
})
return json.dumps({"count": len(result), "invitations": result}, indent=2)
async def _invite(self, email: str | None, org: str | None) -> str:
if not email:
return json.dumps({"error": "Required: email. Optional: org"})
invitation = {
"owner": "admin",
"name": email.replace("@", "-at-").replace(".", "-"),
"email": email,
"organization": org or "hanzo",
}
data = await _iam_post("add-invitation", body={"invitation": invitation})
return json.dumps({"action": "invited", "email": email, "result": data}, indent=2)
# -- Audit and system actions --------------------------------------------
async def _records(self) -> str:
data = await _iam_get("get-records", params={"owner": "admin"})
records = data if isinstance(data, list) else []
result = []
for r in records[:50]: # Limit to 50 most recent
result.append({
"name": r.get("name"),
"method": r.get("method"),
"requestUri": r.get("requestUri"),
"action": r.get("action"),
"createdTime": r.get("createdTime"),
"user": r.get("user"),
"ip": r.get("ip"),
})
return json.dumps({"count": len(result), "records": result}, indent=2)
async def _system_info(self) -> str:
data = await _iam_get("get-system-info")
return json.dumps(data, indent=2)
async def _health(self) -> str:
try:
async with httpx.AsyncClient(timeout=10.0) as client:
resp = await client.get(f"{IAM_BASE_URL}/api/health")
return json.dumps({
"status": "ok" if resp.status_code == 200 else "error",
"code": resp.status_code,
"url": IAM_BASE_URL,
}, indent=2)
except Exception as e:
return json.dumps({"status": "error", "error": str(e), "url": IAM_BASE_URL})
# -- Registration --------------------------------------------------------
def register(self, mcp_server: FastMCP) -> None:
"""Register IAM tool with explicit parameters."""
tool_instance = self
@mcp_server.tool(
name="iam",
description=DESCRIPTION,
)
async def iam(
action: Annotated[
str,
Field(
description=(
"Action to perform. "
"Users: users, user, create_user, update_user, delete_user. "
"Orgs: orgs, org. "
"Roles: roles, role, permissions, enforce. "
"Auth: providers, apps, tokens, sessions. "
"Invitations: invitations, invite. "
"System: records, system_info, health."
),
),
] = "health",
id: Annotated[
str | None,
Field(description="Entity ID (format: org/name for users, roles, etc.)"),
] = None,
owner: Annotated[
str | None,
Field(description="Organization owner (default: hanzo)"),
] = None,
name: Annotated[
str | None,
Field(description="Name for create/update operations"),
] = None,
email: Annotated[
str | None,
Field(description="Email for user creation or invitations"),
] = None,
password: Annotated[
str | None,
Field(description="Password for user creation"),
] = None,
display_name: Annotated[
str | None,
Field(description="Display name for create/update operations"),
] = None,
org: Annotated[
str | None,
Field(description="Organization name for invitations"),
] = None,
model: Annotated[
str | None,
Field(description="Permission model name (for enforce action)"),
] = None,
resource: Annotated[
str | None,
Field(description="Resource path (for enforce action)"),
] = None,
permission_action: Annotated[
str | None,
Field(description="Permission action to check (for enforce action)"),
] = None,
ctx: MCPContext = None,
) -> str:
return await tool_instance.call(
ctx,
action=action,
id=id,
owner=owner,
name=name,
email=email,
password=password,
display_name=display_name,
org=org,
model=model,
resource=resource,
permission_action=permission_action,
)
+32
View File
@@ -0,0 +1,32 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "hanzo-tools-iam"
version = "0.1.0"
description = "Hanzo MCP tool for IAM — identity, access management, users, orgs, roles, and permissions"
readme = "README.md"
requires-python = ">=3.12"
license = { text = "MIT" }
authors = [{ name = "Hanzo AI", email = "[email protected]" }]
keywords = ["hanzo", "mcp", "iam", "identity", "access", "tools"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
]
dependencies = [
"hanzo-tools-core>=0.1.0",
"hanzo-tools-auth>=0.1.0",
"httpx>=0.27.0",
]
[project.entry-points."hanzo.tools"]
iam = "hanzo_tools.iam:TOOLS"
[tool.hatch.build.targets.wheel]
packages = ["hanzo_tools"]
+11
View File
@@ -0,0 +1,11 @@
# hanzo-tools-ingress
MCP tool package for hanzo-mcp. Provides native ingress management via the Hanzo platform.
## Installation
```bash
pip install hanzo-tools-ingress
```
Part of the [hanzo-mcp](https://pypi.org/project/hanzo-mcp/) ecosystem.
@@ -0,0 +1,7 @@
"""Hanzo Ingress Tools -- Traefik inspection and PaaS domain management via MCP."""
from .ingress_tool import IngressTool
TOOLS = [IngressTool]
__all__ = ["IngressTool", "TOOLS"]
@@ -0,0 +1,309 @@
"""MCP tool for Traefik ingress inspection and PaaS domain management.
Traefik actions query the Traefik API for routers, services, middlewares,
entrypoints, and dashboard overview. Domain actions use the PaaS API to
manage custom domains, verify DNS, and check TLS certificate status.
Auth: Uses HanzoSession from hanzo-tools-auth for authenticated API calls.
"""
from __future__ import annotations
import json
import logging
from typing import Annotated, Any, final
from mcp.server import FastMCP
from mcp.server.fastmcp import Context as MCPContext
from pydantic import Field
from hanzo_tools.core.base import BaseTool
logger = logging.getLogger(__name__)
DESCRIPTION = """Traefik ingress inspection and PaaS domain management.
Requires authentication via `hanzo login` (stored at ~/.hanzo/auth/token.json).
Traefik actions:
- routers: List HTTP routers
- services: List HTTP services
- middlewares: List middlewares
- entrypoints: List entrypoints
- overview: Dashboard overview stats
- tcp_routers: List TCP routers
Domain actions (PaaS):
- domains: List custom domains for a project
- add_domain: Add a custom domain (params: project_id, domain)
- remove_domain: Remove a custom domain (params: project_id, domain)
- verify_domain: Verify domain DNS (params: project_id, domain)
- tls_status: Get TLS certificate status (params: domain)
"""
def _get_session():
"""Get HanzoSession singleton."""
from hanzo_tools.auth.session import HanzoSession
return HanzoSession.get()
@final
class IngressTool(BaseTool):
"""MCP tool for Traefik ingress and domain operations."""
@property
def name(self) -> str:
return "ingress"
@property
def description(self) -> str:
return DESCRIPTION
async def call(
self,
ctx: MCPContext,
action: str = "overview",
project_id: str | None = None,
domain: str | None = None,
**kwargs: Any,
) -> str:
try:
# Traefik actions
if action == "routers":
return await self._routers()
elif action == "services":
return await self._services()
elif action == "middlewares":
return await self._middlewares()
elif action == "entrypoints":
return await self._entrypoints()
elif action == "overview":
return await self._overview()
elif action == "tcp_routers":
return await self._tcp_routers()
# Domain actions
elif action == "domains":
return await self._domains(project_id)
elif action == "add_domain":
return await self._add_domain(project_id, domain)
elif action == "remove_domain":
return await self._remove_domain(project_id, domain)
elif action == "verify_domain":
return await self._verify_domain(project_id, domain)
elif action == "tls_status":
return await self._tls_status(domain)
else:
return json.dumps({
"error": f"Unknown action: {action}",
"available": [
"routers", "services", "middlewares", "entrypoints",
"overview", "tcp_routers",
"domains", "add_domain", "remove_domain",
"verify_domain", "tls_status",
],
})
except RuntimeError as e:
return json.dumps({"error": str(e)})
except Exception as e:
logger.exception(f"Ingress tool error: {e}")
return json.dumps({"error": f"Ingress error: {e}"})
# -- Traefik actions -----------------------------------------------------
async def _traefik_get(self, path: str) -> Any:
"""GET from the Traefik API via the PaaS gateway."""
session = _get_session()
paas = session.get_paas_client()
return paas.get(f"/v1/ingress{path}")
async def _routers(self) -> str:
data = await self._traefik_get("/http/routers")
routers = data if isinstance(data, list) else []
result = []
for r in routers:
result.append({
"name": r.get("name"),
"rule": r.get("rule"),
"service": r.get("service"),
"entryPoints": r.get("entryPoints"),
"status": r.get("status"),
"tls": bool(r.get("tls")),
"provider": r.get("provider"),
})
return json.dumps({"count": len(result), "routers": result}, indent=2)
async def _services(self) -> str:
data = await self._traefik_get("/http/services")
services = data if isinstance(data, list) else []
result = []
for s in services:
result.append({
"name": s.get("name"),
"type": s.get("type"),
"status": s.get("status"),
"provider": s.get("provider"),
"servers": s.get("loadBalancer", {}).get("servers") if isinstance(s.get("loadBalancer"), dict) else None,
})
return json.dumps({"count": len(result), "services": result}, indent=2)
async def _middlewares(self) -> str:
data = await self._traefik_get("/http/middlewares")
middlewares = data if isinstance(data, list) else []
result = []
for m in middlewares:
result.append({
"name": m.get("name"),
"type": m.get("type"),
"status": m.get("status"),
"provider": m.get("provider"),
})
return json.dumps({"count": len(result), "middlewares": result}, indent=2)
async def _entrypoints(self) -> str:
data = await self._traefik_get("/entrypoints")
entrypoints = data if isinstance(data, list) else []
result = []
for ep in entrypoints:
result.append({
"name": ep.get("name"),
"address": ep.get("address"),
"protocol": ep.get("protocol"),
})
return json.dumps({"count": len(result), "entrypoints": result}, indent=2)
async def _overview(self) -> str:
data = await self._traefik_get("/overview")
return json.dumps(data, indent=2)
async def _tcp_routers(self) -> str:
data = await self._traefik_get("/tcp/routers")
routers = data if isinstance(data, list) else []
result = []
for r in routers:
result.append({
"name": r.get("name"),
"rule": r.get("rule"),
"service": r.get("service"),
"entryPoints": r.get("entryPoints"),
"status": r.get("status"),
"tls": bool(r.get("tls")),
})
return json.dumps({"count": len(result), "tcp_routers": result}, indent=2)
# -- Domain actions (PaaS) -----------------------------------------------
async def _domains(self, project_id: str | None) -> str:
if not project_id:
return json.dumps({"error": "Required: project_id"})
session = _get_session()
paas = session.get_paas_client()
data = paas.get(f"/v1/project/{project_id}/domain")
domains = data if isinstance(data, list) else []
result = []
for d in domains:
result.append({
"id": d.get("id"),
"domain": d.get("domain"),
"verified": d.get("verified"),
"tls": d.get("tls"),
"created_at": d.get("createdAt"),
})
return json.dumps({
"project_id": project_id,
"count": len(result),
"domains": result,
}, indent=2)
async def _add_domain(self, project_id: str | None, domain: str | None) -> str:
if not project_id or not domain:
return json.dumps({"error": "Required: project_id and domain"})
session = _get_session()
paas = session.get_paas_client()
result = paas.post(f"/v1/project/{project_id}/domain", json={"domain": domain})
return json.dumps({
"action": "add_domain",
"project_id": project_id,
"domain": domain,
"result": result,
}, indent=2)
async def _remove_domain(self, project_id: str | None, domain: str | None) -> str:
if not project_id or not domain:
return json.dumps({"error": "Required: project_id and domain"})
session = _get_session()
paas = session.get_paas_client()
result = paas.delete(f"/v1/project/{project_id}/domain/{domain}")
return json.dumps({
"action": "remove_domain",
"project_id": project_id,
"domain": domain,
"result": result,
}, indent=2)
async def _verify_domain(self, project_id: str | None, domain: str | None) -> str:
if not project_id or not domain:
return json.dumps({"error": "Required: project_id and domain"})
session = _get_session()
paas = session.get_paas_client()
result = paas.post(f"/v1/project/{project_id}/domain/{domain}/verify")
return json.dumps({
"action": "verify_domain",
"project_id": project_id,
"domain": domain,
"result": result,
}, indent=2)
async def _tls_status(self, domain: str | None) -> str:
if not domain:
return json.dumps({"error": "Required: domain"})
session = _get_session()
paas = session.get_paas_client()
result = paas.get(f"/v1/domain/{domain}/tls")
return json.dumps({
"domain": domain,
"tls": result,
}, indent=2)
# -- Registration --------------------------------------------------------
def register(self, mcp_server: FastMCP) -> None:
"""Register ingress tool with explicit parameters."""
tool_instance = self
@mcp_server.tool(
name="ingress",
description=DESCRIPTION,
)
async def ingress(
action: Annotated[
str,
Field(
description=(
"Action to perform. "
"Traefik: routers, services, middlewares, entrypoints, overview, tcp_routers. "
"Domains: domains, add_domain, remove_domain, verify_domain, tls_status."
),
),
] = "overview",
project_id: Annotated[
str | None,
Field(description="Project ID (for domain actions)"),
] = None,
domain: Annotated[
str | None,
Field(description="Domain name (for add_domain, remove_domain, verify_domain, tls_status)"),
] = None,
ctx: MCPContext = None,
) -> str:
return await tool_instance.call(
ctx,
action=action,
project_id=project_id,
domain=domain,
)
+32
View File
@@ -0,0 +1,32 @@
[project]
name = "hanzo-tools-ingress"
version = "0.1.0"
description = "Hanzo MCP tool for Traefik ingress inspection and PaaS domain management"
readme = "README.md"
requires-python = ">=3.12"
license = { text = "MIT" }
authors = [{ name = "Hanzo AI", email = "[email protected]" }]
keywords = ["hanzo", "mcp", "ingress", "traefik", "domains", "tools"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
]
dependencies = [
"hanzo-tools-core>=0.1.0",
"hanzo-tools-auth>=0.1.0",
"httpx>=0.27.0",
]
[project.entry-points."hanzo.tools"]
ingress = "hanzo_tools.ingress:TOOLS"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["hanzo_tools"]
+11
View File
@@ -0,0 +1,11 @@
# hanzo-tools-mpc
MCP tool package for hanzo-mcp. Provides native mpc management via the Hanzo platform.
## Installation
```bash
pip install hanzo-tools-mpc
```
Part of the [hanzo-mcp](https://pypi.org/project/hanzo-mcp/) ecosystem.
@@ -0,0 +1,7 @@
"""Hanzo MPC Tools -- threshold signing, vaults, wallets, and policies via MCP."""
from .mpc_tool import MPCTool
TOOLS = [MPCTool]
__all__ = ["MPCTool", "TOOLS"]
@@ -0,0 +1,563 @@
"""MCP tool for MPC threshold signing -- vaults, wallets, transactions, policies.
Provides a unified interface for managing MPC (Multi-Party Computation)
threshold signing operations including vault management, wallet keygen,
transaction signing, approval workflows, and cross-chain bridge signing.
Auth: Uses HanzoSession from hanzo-tools-auth for authenticated API calls.
"""
from __future__ import annotations
import json
import logging
from typing import Annotated, Any, final
from mcp.server import FastMCP
from mcp.server.fastmcp import Context as MCPContext
from pydantic import Field
from hanzo_tools.core.base import BaseTool
logger = logging.getLogger(__name__)
DESCRIPTION = """MPC threshold signing -- vaults, wallets, transactions, policies.
Requires authentication via `hanzo login` (stored at ~/.hanzo/auth/token.json).
Status:
- status: MPC service health and status
Vault actions:
- vaults: List vaults
- vault: Get vault details (params: vault_id)
- create_vault: Create a new vault (params: name, description)
Wallet actions:
- wallets: List wallets in a vault (params: vault_id)
- wallet: Get wallet details (params: wallet_id)
- create_wallet: Create wallet / initiate keygen (params: vault_id, name, curve, threshold, parties)
Transaction actions:
- transactions: List transactions
- transaction: Get transaction details (params: transaction_id)
- create_transaction: Create a signing request (params: wallet_id, type, chain, to, amount)
- approve: Approve a transaction (params: transaction_id)
- reject: Reject a transaction (params: transaction_id)
- broadcast: Broadcast a signed transaction (params: transaction_id)
Policy actions:
- policies: List signing policies
Smart wallet actions:
- smart_wallets: List smart contract wallets
Whitelist actions:
- whitelist: List whitelisted addresses
Bridge actions:
- bridge_sign: Sign a cross-chain bridge transaction (params: wallet_id, source_chain, dest_chain, token, amount, recipient)
"""
def _get_session():
"""Get HanzoSession singleton."""
from hanzo_tools.auth.session import HanzoSession
return HanzoSession.get()
@final
class MPCTool(BaseTool):
"""MCP tool for MPC threshold signing operations."""
@property
def name(self) -> str:
return "mpc"
@property
def description(self) -> str:
return DESCRIPTION
async def call(
self,
ctx: MCPContext,
action: str = "status",
vault_id: str | None = None,
wallet_id: str | None = None,
transaction_id: str | None = None,
name: str | None = None,
description: str | None = None,
curve: str | None = None,
threshold: int | None = None,
parties: int | None = None,
type: str | None = None,
chain: str | None = None,
to: str | None = None,
amount: str | None = None,
source_chain: str | None = None,
dest_chain: str | None = None,
token: str | None = None,
recipient: str | None = None,
**kwargs: Any,
) -> str:
try:
# Status
if action == "status":
return await self._status()
# Vault actions
elif action == "vaults":
return await self._vaults()
elif action == "vault":
return await self._vault(vault_id)
elif action == "create_vault":
return await self._create_vault(name, description)
# Wallet actions
elif action == "wallets":
return await self._wallets(vault_id)
elif action == "wallet":
return await self._wallet(wallet_id)
elif action == "create_wallet":
return await self._create_wallet(vault_id, name, curve, threshold, parties)
# Transaction actions
elif action == "transactions":
return await self._transactions()
elif action == "transaction":
return await self._transaction(transaction_id)
elif action == "create_transaction":
return await self._create_transaction(wallet_id, type, chain, to, amount)
elif action == "approve":
return await self._approve(transaction_id)
elif action == "reject":
return await self._reject(transaction_id)
elif action == "broadcast":
return await self._broadcast(transaction_id)
# Policy / smart wallet / whitelist
elif action == "policies":
return await self._policies()
elif action == "smart_wallets":
return await self._smart_wallets()
elif action == "whitelist":
return await self._whitelist()
# Bridge
elif action == "bridge_sign":
return await self._bridge_sign(
wallet_id, source_chain, dest_chain, token, amount, recipient,
)
else:
return json.dumps({
"error": f"Unknown action: {action}",
"available": [
"status",
"vaults", "vault", "create_vault",
"wallets", "wallet", "create_wallet",
"transactions", "transaction", "create_transaction",
"approve", "reject", "broadcast",
"policies", "smart_wallets", "whitelist",
"bridge_sign",
],
})
except RuntimeError as e:
return json.dumps({"error": str(e)})
except Exception as e:
logger.exception(f"MPC tool error: {e}")
return json.dumps({"error": f"MPC error: {e}"})
# -- Helpers -------------------------------------------------------------
async def _mpc_get(self, path: str) -> Any:
"""GET from the MPC API via the PaaS gateway."""
session = _get_session()
paas = session.get_paas_client()
return paas.get(f"/v1/mpc{path}")
async def _mpc_post(self, path: str, body: dict | None = None) -> Any:
"""POST to the MPC API via the PaaS gateway."""
session = _get_session()
paas = session.get_paas_client()
return paas.post(f"/v1/mpc{path}", json=body or {})
async def _mpc_delete(self, path: str) -> Any:
"""DELETE from the MPC API via the PaaS gateway."""
session = _get_session()
paas = session.get_paas_client()
return paas.delete(f"/v1/mpc{path}")
# -- Status --------------------------------------------------------------
async def _status(self) -> str:
data = await self._mpc_get("/status")
return json.dumps(data, indent=2)
# -- Vault actions -------------------------------------------------------
async def _vaults(self) -> str:
data = await self._mpc_get("/vaults")
vaults = data if isinstance(data, list) else []
result = []
for v in vaults:
result.append({
"id": v.get("id"),
"name": v.get("name"),
"description": v.get("description"),
"wallet_count": v.get("walletCount"),
"created_at": v.get("createdAt"),
})
return json.dumps({"count": len(result), "vaults": result}, indent=2)
async def _vault(self, vault_id: str | None) -> str:
if not vault_id:
return json.dumps({"error": "Required: vault_id"})
data = await self._mpc_get(f"/vaults/{vault_id}")
return json.dumps(data, indent=2)
async def _create_vault(self, name: str | None, description: str | None) -> str:
if not name:
return json.dumps({"error": "Required: name"})
body = {"name": name}
if description:
body["description"] = description
result = await self._mpc_post("/vaults", body)
return json.dumps({
"action": "create_vault",
"result": result,
}, indent=2)
# -- Wallet actions ------------------------------------------------------
async def _wallets(self, vault_id: str | None) -> str:
if not vault_id:
return json.dumps({"error": "Required: vault_id"})
data = await self._mpc_get(f"/vaults/{vault_id}/wallets")
wallets = data if isinstance(data, list) else []
result = []
for w in wallets:
result.append({
"id": w.get("id"),
"name": w.get("name"),
"curve": w.get("curve"),
"threshold": w.get("threshold"),
"parties": w.get("parties"),
"address": w.get("address"),
"status": w.get("status"),
"created_at": w.get("createdAt"),
})
return json.dumps({
"vault_id": vault_id,
"count": len(result),
"wallets": result,
}, indent=2)
async def _wallet(self, wallet_id: str | None) -> str:
if not wallet_id:
return json.dumps({"error": "Required: wallet_id"})
data = await self._mpc_get(f"/wallets/{wallet_id}")
return json.dumps(data, indent=2)
async def _create_wallet(
self,
vault_id: str | None,
name: str | None,
curve: str | None,
threshold: int | None,
parties: int | None,
) -> str:
if not vault_id or not name:
return json.dumps({"error": "Required: vault_id and name"})
body: dict[str, Any] = {"name": name}
if curve:
body["curve"] = curve
if threshold is not None:
body["threshold"] = threshold
if parties is not None:
body["parties"] = parties
result = await self._mpc_post(f"/vaults/{vault_id}/wallets", body)
return json.dumps({
"action": "create_wallet",
"vault_id": vault_id,
"result": result,
}, indent=2)
# -- Transaction actions -------------------------------------------------
async def _transactions(self) -> str:
data = await self._mpc_get("/transactions")
txns = data if isinstance(data, list) else []
result = []
for t in txns:
result.append({
"id": t.get("id"),
"wallet_id": t.get("walletId"),
"type": t.get("type"),
"chain": t.get("chain"),
"status": t.get("status"),
"to": t.get("to"),
"amount": t.get("amount"),
"created_at": t.get("createdAt"),
})
return json.dumps({"count": len(result), "transactions": result}, indent=2)
async def _transaction(self, transaction_id: str | None) -> str:
if not transaction_id:
return json.dumps({"error": "Required: transaction_id"})
data = await self._mpc_get(f"/transactions/{transaction_id}")
return json.dumps(data, indent=2)
async def _create_transaction(
self,
wallet_id: str | None,
type: str | None,
chain: str | None,
to: str | None,
amount: str | None,
) -> str:
if not wallet_id:
return json.dumps({"error": "Required: wallet_id"})
body: dict[str, Any] = {"walletId": wallet_id}
if type:
body["type"] = type
if chain:
body["chain"] = chain
if to:
body["to"] = to
if amount:
body["amount"] = amount
result = await self._mpc_post("/transactions", body)
return json.dumps({
"action": "create_transaction",
"result": result,
}, indent=2)
async def _approve(self, transaction_id: str | None) -> str:
if not transaction_id:
return json.dumps({"error": "Required: transaction_id"})
result = await self._mpc_post(f"/transactions/{transaction_id}/approve")
return json.dumps({
"action": "approve",
"transaction_id": transaction_id,
"result": result,
}, indent=2)
async def _reject(self, transaction_id: str | None) -> str:
if not transaction_id:
return json.dumps({"error": "Required: transaction_id"})
result = await self._mpc_post(f"/transactions/{transaction_id}/reject")
return json.dumps({
"action": "reject",
"transaction_id": transaction_id,
"result": result,
}, indent=2)
async def _broadcast(self, transaction_id: str | None) -> str:
if not transaction_id:
return json.dumps({"error": "Required: transaction_id"})
result = await self._mpc_post(f"/transactions/{transaction_id}/broadcast")
return json.dumps({
"action": "broadcast",
"transaction_id": transaction_id,
"result": result,
}, indent=2)
# -- Policies / Smart Wallets / Whitelist --------------------------------
async def _policies(self) -> str:
data = await self._mpc_get("/policies")
policies = data if isinstance(data, list) else []
result = []
for p in policies:
result.append({
"id": p.get("id"),
"name": p.get("name"),
"type": p.get("type"),
"rules": p.get("rules"),
"created_at": p.get("createdAt"),
})
return json.dumps({"count": len(result), "policies": result}, indent=2)
async def _smart_wallets(self) -> str:
data = await self._mpc_get("/smart-wallets")
wallets = data if isinstance(data, list) else []
result = []
for w in wallets:
result.append({
"id": w.get("id"),
"name": w.get("name"),
"chain": w.get("chain"),
"address": w.get("address"),
"type": w.get("type"),
"status": w.get("status"),
})
return json.dumps({"count": len(result), "smart_wallets": result}, indent=2)
async def _whitelist(self) -> str:
data = await self._mpc_get("/whitelist")
addresses = data if isinstance(data, list) else []
result = []
for a in addresses:
result.append({
"id": a.get("id"),
"address": a.get("address"),
"chain": a.get("chain"),
"label": a.get("label"),
"created_at": a.get("createdAt"),
})
return json.dumps({"count": len(result), "addresses": result}, indent=2)
# -- Bridge signing ------------------------------------------------------
async def _bridge_sign(
self,
wallet_id: str | None,
source_chain: str | None,
dest_chain: str | None,
token: str | None,
amount: str | None,
recipient: str | None,
) -> str:
if not wallet_id or not source_chain or not dest_chain:
return json.dumps({"error": "Required: wallet_id, source_chain, dest_chain"})
body: dict[str, Any] = {
"walletId": wallet_id,
"sourceChain": source_chain,
"destChain": dest_chain,
}
if token:
body["token"] = token
if amount:
body["amount"] = amount
if recipient:
body["recipient"] = recipient
result = await self._mpc_post("/bridge/sign", body)
return json.dumps({
"action": "bridge_sign",
"wallet_id": wallet_id,
"source_chain": source_chain,
"dest_chain": dest_chain,
"result": result,
}, indent=2)
# -- Registration --------------------------------------------------------
def register(self, mcp_server: FastMCP) -> None:
"""Register MPC tool with explicit parameters."""
tool_instance = self
@mcp_server.tool(
name="mpc",
description=DESCRIPTION,
)
async def mpc(
action: Annotated[
str,
Field(
description=(
"Action to perform. "
"Status: status. "
"Vaults: vaults, vault, create_vault. "
"Wallets: wallets, wallet, create_wallet. "
"Transactions: transactions, transaction, create_transaction, approve, reject, broadcast. "
"Policies: policies. Smart wallets: smart_wallets. Whitelist: whitelist. "
"Bridge: bridge_sign."
),
),
] = "status",
vault_id: Annotated[
str | None,
Field(description="Vault ID (for vault, wallets, create_wallet)"),
] = None,
wallet_id: Annotated[
str | None,
Field(description="Wallet ID (for wallet, create_transaction, bridge_sign)"),
] = None,
transaction_id: Annotated[
str | None,
Field(description="Transaction ID (for transaction, approve, reject, broadcast)"),
] = None,
name: Annotated[
str | None,
Field(description="Name (for create_vault, create_wallet)"),
] = None,
description: Annotated[
str | None,
Field(description="Description (for create_vault)"),
] = None,
curve: Annotated[
str | None,
Field(description="Elliptic curve: secp256k1, ed25519 (for create_wallet)"),
] = None,
threshold: Annotated[
int | None,
Field(description="Signing threshold (for create_wallet)"),
] = None,
parties: Annotated[
int | None,
Field(description="Number of parties (for create_wallet)"),
] = None,
type: Annotated[
str | None,
Field(description="Transaction type: transfer, contract_call (for create_transaction)"),
] = None,
chain: Annotated[
str | None,
Field(description="Chain identifier (for create_transaction)"),
] = None,
to: Annotated[
str | None,
Field(description="Destination address (for create_transaction)"),
] = None,
amount: Annotated[
str | None,
Field(description="Amount as string (for create_transaction, bridge_sign)"),
] = None,
source_chain: Annotated[
str | None,
Field(description="Source chain (for bridge_sign)"),
] = None,
dest_chain: Annotated[
str | None,
Field(description="Destination chain (for bridge_sign)"),
] = None,
token: Annotated[
str | None,
Field(description="Token identifier (for bridge_sign)"),
] = None,
recipient: Annotated[
str | None,
Field(description="Recipient address (for bridge_sign)"),
] = None,
ctx: MCPContext = None,
) -> str:
return await tool_instance.call(
ctx,
action=action,
vault_id=vault_id,
wallet_id=wallet_id,
transaction_id=transaction_id,
name=name,
description=description,
curve=curve,
threshold=threshold,
parties=parties,
type=type,
chain=chain,
to=to,
amount=amount,
source_chain=source_chain,
dest_chain=dest_chain,
token=token,
recipient=recipient,
)
+32
View File
@@ -0,0 +1,32 @@
[project]
name = "hanzo-tools-mpc"
version = "0.1.0"
description = "Hanzo MCP tool for MPC threshold signing -- vaults, wallets, transactions, policies"
readme = "README.md"
requires-python = ">=3.12"
license = { text = "MIT" }
authors = [{ name = "Hanzo AI", email = "[email protected]" }]
keywords = ["hanzo", "mcp", "mpc", "threshold", "signing", "wallets", "tools"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
]
dependencies = [
"hanzo-tools-core>=0.1.0",
"hanzo-tools-auth>=0.1.0",
"httpx>=0.27.0",
]
[project.entry-points."hanzo.tools"]
mpc = "hanzo_tools.mpc:TOOLS"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["hanzo_tools"]
+11
View File
@@ -0,0 +1,11 @@
# hanzo-tools-team
MCP tool package for hanzo-mcp. Provides native team management via the Hanzo platform.
## Installation
```bash
pip install hanzo-tools-team
```
Part of the [hanzo-mcp](https://pypi.org/project/hanzo-mcp/) ecosystem.
@@ -0,0 +1,7 @@
"""Hanzo Team Tools — workspace and member management via MCP."""
from .team_tool import TeamTool
TOOLS = [TeamTool]
__all__ = ["TeamTool", "TOOLS"]
@@ -0,0 +1,294 @@
"""MCP tool for Hanzo Team — workspace and member management.
Manage workspaces, members, and invitations for collaborative
Hanzo Team accounts.
Auth: Uses HanzoSession from hanzo-tools-auth for Bearer JWT tokens.
Backend: Hanzo Team service.
"""
from __future__ import annotations
import json
import logging
import os
from typing import Annotated, Any, final
import httpx
from mcp.server import FastMCP
from mcp.server.fastmcp import Context as MCPContext
from pydantic import Field
from hanzo_tools.core.base import BaseTool
logger = logging.getLogger(__name__)
TEAM_BASE_URL = os.getenv("HANZO_TEAM_URL", "https://api.hanzo.ai/team")
DESCRIPTION = """Hanzo Team — workspace and member management.
Requires authentication via `hanzo login` (stored at ~/.hanzo/auth/token.json).
Actions:
- workspaces: List all workspaces
- workspace: Get workspace details (params: workspace_id)
- create_workspace: Create a new workspace (params: name)
- delete_workspace: Delete a workspace (params: workspace_id)
- members: List workspace members (params: workspace_id)
- invite: Invite a member to a workspace (params: workspace_id, email, role)
- account: Get current account info
"""
def _get_session():
"""Get HanzoSession singleton."""
from hanzo_tools.auth.session import HanzoSession
return HanzoSession.get()
def _team_url(path: str) -> str:
"""Build full Team API URL."""
return f"{TEAM_BASE_URL}/{path.lstrip('/')}"
def _auth_headers(token: str) -> dict[str, str]:
"""Build auth headers with Bearer token."""
return {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"User-Agent": "hanzo-mcp/0.1",
}
def _get_token() -> str:
"""Get Bearer token from session or raise."""
session = _get_session()
token = session.get_iam_token()
if not token:
raise RuntimeError("Not authenticated. Run 'hanzo login' first.")
return token
async def _team_get(path: str, params: dict[str, Any] | None = None) -> Any:
"""GET request to Team API."""
token = _get_token()
async with httpx.AsyncClient(timeout=30.0) as client:
resp = await client.get(
_team_url(path),
headers=_auth_headers(token),
params=params,
)
resp.raise_for_status()
return resp.json()
async def _team_post(path: str, body: dict[str, Any] | None = None) -> Any:
"""POST request to Team API."""
token = _get_token()
async with httpx.AsyncClient(timeout=30.0) as client:
resp = await client.post(
_team_url(path),
headers=_auth_headers(token),
json=body or {},
)
resp.raise_for_status()
return resp.json()
async def _team_delete(path: str) -> Any:
"""DELETE request to Team API."""
token = _get_token()
async with httpx.AsyncClient(timeout=30.0) as client:
resp = await client.delete(
_team_url(path),
headers=_auth_headers(token),
)
resp.raise_for_status()
if not resp.content or resp.status_code == 204:
return {}
return resp.json()
@final
class TeamTool(BaseTool):
"""MCP tool for Hanzo Team operations."""
@property
def name(self) -> str:
return "team"
@property
def description(self) -> str:
return DESCRIPTION
async def call(
self,
ctx: MCPContext,
action: str = "account",
workspace_id: str | None = None,
name: str | None = None,
email: str | None = None,
role: str | None = None,
**kwargs: Any,
) -> str:
try:
if action == "workspaces":
return await self._workspaces()
elif action == "workspace":
return await self._workspace(workspace_id)
elif action == "create_workspace":
return await self._create_workspace(name)
elif action == "delete_workspace":
return await self._delete_workspace(workspace_id)
elif action == "members":
return await self._members(workspace_id)
elif action == "invite":
return await self._invite(workspace_id, email, role)
elif action == "account":
return await self._account()
else:
return json.dumps({
"error": f"Unknown action: {action}",
"available": [
"workspaces", "workspace", "create_workspace",
"delete_workspace", "members", "invite", "account",
],
})
except RuntimeError as e:
return json.dumps({"error": str(e)})
except httpx.HTTPStatusError as e:
body = e.response.text
try:
body = e.response.json()
except Exception:
pass
return json.dumps({"error": f"Team API error {e.response.status_code}", "detail": body})
except Exception as e:
logger.exception(f"Team tool error: {e}")
return json.dumps({"error": f"Team error: {e}"})
# -- Workspace actions ---------------------------------------------------
async def _workspaces(self) -> str:
data = await _team_get("workspaces")
workspaces = data if isinstance(data, list) else data.get("workspaces", [])
result = []
for w in workspaces:
result.append({
"id": w.get("id"),
"name": w.get("name"),
"slug": w.get("slug"),
"createdAt": w.get("createdAt"),
"memberCount": w.get("memberCount"),
})
return json.dumps({"count": len(result), "workspaces": result}, indent=2)
async def _workspace(self, workspace_id: str | None) -> str:
if not workspace_id:
return json.dumps({"error": "Required: workspace_id"})
data = await _team_get(f"workspaces/{workspace_id}")
return json.dumps(data, indent=2)
async def _create_workspace(self, name: str | None) -> str:
if not name:
return json.dumps({"error": "Required: name"})
data = await _team_post("workspaces", body={"name": name})
return json.dumps({"action": "created", "result": data}, indent=2)
async def _delete_workspace(self, workspace_id: str | None) -> str:
if not workspace_id:
return json.dumps({"error": "Required: workspace_id"})
data = await _team_delete(f"workspaces/{workspace_id}")
return json.dumps({"action": "deleted", "workspace_id": workspace_id, "result": data}, indent=2)
# -- Member actions ------------------------------------------------------
async def _members(self, workspace_id: str | None) -> str:
if not workspace_id:
return json.dumps({"error": "Required: workspace_id"})
data = await _team_get(f"workspaces/{workspace_id}/members")
members = data if isinstance(data, list) else data.get("members", [])
result = []
for m in members:
result.append({
"id": m.get("id"),
"name": m.get("name"),
"email": m.get("email"),
"role": m.get("role"),
"joinedAt": m.get("joinedAt"),
})
return json.dumps({
"workspace_id": workspace_id,
"count": len(result),
"members": result,
}, indent=2)
async def _invite(self, workspace_id: str | None, email: str | None, role: str | None) -> str:
if not workspace_id or not email:
return json.dumps({"error": "Required: workspace_id, email. Optional: role"})
body: dict[str, Any] = {"email": email}
if role:
body["role"] = role
data = await _team_post(f"workspaces/{workspace_id}/invitations", body=body)
return json.dumps({
"action": "invited",
"workspace_id": workspace_id,
"email": email,
"role": role or "member",
"result": data,
}, indent=2)
# -- Account action ------------------------------------------------------
async def _account(self) -> str:
data = await _team_get("account")
return json.dumps(data, indent=2)
# -- Registration --------------------------------------------------------
def register(self, mcp_server: FastMCP) -> None:
"""Register Team tool with explicit parameters."""
tool_instance = self
@mcp_server.tool(
name="team",
description=DESCRIPTION,
)
async def team(
action: Annotated[
str,
Field(
description=(
"Action to perform. "
"Workspaces: workspaces, workspace, create_workspace, delete_workspace. "
"Members: members, invite. "
"Account: account."
),
),
] = "account",
workspace_id: Annotated[
str | None,
Field(description="Workspace ID (for workspace, delete_workspace, members, invite)"),
] = None,
name: Annotated[
str | None,
Field(description="Workspace name (for create_workspace)"),
] = None,
email: Annotated[
str | None,
Field(description="Email address (for invite)"),
] = None,
role: Annotated[
str | None,
Field(description="Member role (for invite, e.g. admin, member, viewer)"),
] = None,
ctx: MCPContext = None,
) -> str:
return await tool_instance.call(
ctx,
action=action,
workspace_id=workspace_id,
name=name,
email=email,
role=role,
)
+32
View File
@@ -0,0 +1,32 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "hanzo-tools-team"
version = "0.1.0"
description = "Hanzo MCP tool for Team — workspaces, members, and invitations"
readme = "README.md"
requires-python = ">=3.12"
license = { text = "MIT" }
authors = [{ name = "Hanzo AI", email = "[email protected]" }]
keywords = ["hanzo", "mcp", "team", "workspace", "collaboration", "tools"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
]
dependencies = [
"hanzo-tools-core>=0.1.0",
"hanzo-tools-auth>=0.1.0",
"httpx>=0.27.0",
]
[project.entry-points."hanzo.tools"]
team = "hanzo_tools.team:TOOLS"
[tool.hatch.build.targets.wheel]
packages = ["hanzo_tools"]
+71 -240
View File
@@ -215,15 +215,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" },
]
[[package]]
name = "asttokens"
version = "3.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/be/a5/8e3f9b6771b0b408517c82d97aed8f2036509bc247d46114925e32fe33f0/asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7", size = 62308, upload-time = "2025-11-15T16:43:48.578Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" },
]
[[package]]
name = "attrs"
version = "25.4.0"
@@ -865,15 +856,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/1c/7c/996760c30f1302704af57c66ff2d723f7d656d0d0b93563b5528a51484bb/cyclopts-4.5.1-py3-none-any.whl", hash = "sha256:0642c93601e554ca6b7b9abd81093847ea4448b2616280f2a0952416574e8c7a", size = 199807, upload-time = "2026-01-25T15:23:55.219Z" },
]
[[package]]
name = "decorator"
version = "5.2.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" },
]
[[package]]
name = "defusedxml"
version = "0.7.1"
@@ -995,15 +977,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
]
[[package]]
name = "executing"
version = "2.2.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" },
]
[[package]]
name = "fakeredis"
version = "2.33.0"
@@ -1464,19 +1437,16 @@ wheels = [
[[package]]
name = "hanzo"
version = "0.3.48"
version = "0.4.1"
source = { editable = "." }
dependencies = [
{ name = "anthropic" },
{ name = "click" },
{ name = "hanzo-cli" },
{ name = "hanzo-kms" },
{ name = "httpx" },
{ name = "openai" },
{ name = "prompt-toolkit" },
{ name = "pydantic" },
{ name = "pyyaml" },
{ name = "qrcode" },
{ name = "rich" },
{ name = "typer" },
]
[package.optional-dependencies]
@@ -1485,16 +1455,21 @@ agents = [
{ name = "hanzo-network" },
]
ai = [
{ name = "anthropic" },
{ name = "hanzoai" },
{ name = "openai" },
]
all = [
{ name = "anthropic" },
{ name = "hanzo-aci" },
{ name = "hanzo-agents" },
{ name = "hanzo-mcp" },
{ name = "hanzo-memory" },
{ name = "hanzo-network" },
{ name = "hanzo-repl" },
{ name = "hanzoai" },
{ name = "openai" },
{ name = "prompt-toolkit" },
{ name = "qrcode" },
]
cron = [
{ name = "croniter" },
@@ -1523,6 +1498,10 @@ infra = [
{ name = "redis" },
{ name = "temporalio" },
]
interactive = [
{ name = "prompt-toolkit" },
{ name = "qrcode" },
]
kv = [
{ name = "redis" },
]
@@ -1535,9 +1514,6 @@ pubsub = [
queues = [
{ name = "redis" },
]
repl = [
{ name = "hanzo-repl" },
]
search = [
{ name = "meilisearch-python-sdk" },
]
@@ -1556,7 +1532,8 @@ vector = [
requires-dist = [
{ name = "aiobotocore", marker = "extra == 'infra'", specifier = ">=2.9.0" },
{ name = "aiobotocore", marker = "extra == 'storage'", specifier = ">=2.9.0" },
{ name = "anthropic", specifier = ">=0.25.0" },
{ name = "anthropic", marker = "extra == 'ai'", specifier = ">=0.25.0" },
{ name = "anthropic", marker = "extra == 'all'", specifier = ">=0.25.0" },
{ name = "botocore", marker = "extra == 'infra'", specifier = ">=1.34.0" },
{ name = "botocore", marker = "extra == 'storage'", specifier = ">=1.34.0" },
{ name = "click", specifier = ">=8.1.0" },
@@ -1566,13 +1543,13 @@ requires-dist = [
{ name = "hanzo-aci", marker = "extra == 'dev'", specifier = ">=0.2.8" },
{ name = "hanzo-agents", marker = "extra == 'agents'", specifier = ">=0.1.0" },
{ name = "hanzo-agents", marker = "extra == 'all'", specifier = ">=0.1.0" },
{ name = "hanzo-cli", specifier = ">=0.2.0" },
{ name = "hanzo-kms", specifier = ">=1.1.0" },
{ name = "hanzo-mcp", marker = "extra == 'all'", specifier = ">=0.7.0" },
{ name = "hanzo-mcp", marker = "extra == 'mcp'", specifier = ">=0.7.0" },
{ name = "hanzo-memory", marker = "extra == 'all'", specifier = ">=1.0.0" },
{ name = "hanzo-network", marker = "extra == 'agents'", specifier = ">=0.1.3" },
{ name = "hanzo-network", marker = "extra == 'all'", specifier = ">=0.1.3" },
{ name = "hanzo-repl", marker = "extra == 'all'", specifier = ">=0.1.0" },
{ name = "hanzo-repl", marker = "extra == 'repl'", specifier = ">=0.1.0" },
{ name = "hanzoai", marker = "extra == 'ai'", specifier = ">=1.0.0" },
{ name = "hanzoai", marker = "extra == 'all'", specifier = ">=1.0.0" },
{ name = "httpx", specifier = ">=0.23.0" },
@@ -1584,15 +1561,18 @@ requires-dist = [
{ name = "motor", marker = "extra == 'infra'", specifier = ">=3.3.0" },
{ name = "nats-py", marker = "extra == 'infra'", specifier = ">=2.6.0" },
{ name = "nats-py", marker = "extra == 'pubsub'", specifier = ">=2.6.0" },
{ name = "openai", specifier = ">=1.0.0" },
{ name = "prompt-toolkit", specifier = ">=3.0.0" },
{ name = "openai", marker = "extra == 'ai'", specifier = ">=1.0.0" },
{ name = "openai", marker = "extra == 'all'", specifier = ">=1.0.0" },
{ name = "prompt-toolkit", marker = "extra == 'all'", specifier = ">=3.0.0" },
{ name = "prompt-toolkit", marker = "extra == 'interactive'", specifier = ">=3.0.0" },
{ name = "pydantic", specifier = ">=2.0.0" },
{ name = "pymongo", marker = "extra == 'documentdb'", specifier = ">=4.6.0" },
{ name = "pymongo", marker = "extra == 'infra'", specifier = ">=4.6.0" },
{ name = "pyyaml", specifier = ">=6.0" },
{ name = "qdrant-client", marker = "extra == 'infra'", specifier = ">=1.7.0" },
{ name = "qdrant-client", marker = "extra == 'vector'", specifier = ">=1.7.0" },
{ name = "qrcode", specifier = ">=7.4.2" },
{ name = "qrcode", marker = "extra == 'all'", specifier = ">=7.4.2" },
{ name = "qrcode", marker = "extra == 'interactive'", specifier = ">=7.4.2" },
{ name = "redis", marker = "extra == 'cron'", specifier = ">=5.0.0" },
{ name = "redis", marker = "extra == 'infra'", specifier = ">=5.0.0" },
{ name = "redis", marker = "extra == 'kv'", specifier = ">=5.0.0" },
@@ -1600,9 +1580,8 @@ requires-dist = [
{ name = "rich", specifier = ">=13.0.0" },
{ name = "temporalio", marker = "extra == 'infra'", specifier = ">=1.4.0" },
{ name = "temporalio", marker = "extra == 'tasks'", specifier = ">=1.4.0" },
{ name = "typer", specifier = ">=0.9.0" },
]
provides-extras = ["all", "ai", "router", "mcp", "agents", "dev", "repl", "vector", "kv", "documentdb", "storage", "search", "pubsub", "tasks", "queues", "cron", "functions", "infra"]
provides-extras = ["all", "ai", "router", "mcp", "agents", "dev", "interactive", "vector", "kv", "documentdb", "storage", "search", "pubsub", "tasks", "queues", "cron", "functions", "infra"]
[[package]]
name = "hanzo-aci"
@@ -1670,6 +1649,51 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/91/70/363dac59048d70653ce0d07ff37d5521f49b5b6ce30d6aba8c31bf31154f/hanzo_async-0.1.1-py3-none-any.whl", hash = "sha256:8f551b7b57e96b4f4c5b7e05d7781eb154a7a788c824c7a36bd69f607532cda2", size = 8649, upload-time = "2026-01-05T01:19:18.268Z" },
]
[[package]]
name = "hanzo-cli"
version = "0.2.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "hanzo-iam" },
{ name = "hanzo-kms" },
{ name = "httpx" },
{ name = "pyjwt" },
{ name = "rich" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d9/c0/92c16d871661fed8d0ab091713b468263dd170b652fcc976c1e0c86f5fe0/hanzo_cli-0.2.1.tar.gz", hash = "sha256:d898072a62f427da4a8cc77c834e26b6c7638c108db959dfc7c59b066651d734", size = 23540, upload-time = "2026-02-21T21:48:30.459Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6b/63/88800ec47608397868b92667ecb90e69502df457b83266ec21fb892f7349/hanzo_cli-0.2.1-py3-none-any.whl", hash = "sha256:2d6442e1d1ccb4f5e6a5517ad0b502e4c514f6335f167906c0c8e6ae4be0d193", size = 27435, upload-time = "2026-02-21T21:48:29.607Z" },
]
[[package]]
name = "hanzo-iam"
version = "1.29.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohttp" },
{ name = "cryptography" },
{ name = "pyjwt" },
{ name = "requests" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3e/4b/11d440a3a99e5b7967ae1a9d56f4bee7c9879f7e2a56a9a1398a3d931064/hanzo_iam-1.29.0.tar.gz", hash = "sha256:5979db89b791be181c259d103822424f389be5d82bff677bee9fad3f213f2578", size = 25123, upload-time = "2025-04-09T18:51:53.64Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e1/26/bc5dbd90e5fd0f2666646c2b4831cc4fef6867bbad8091da496851fe600c/hanzo_iam-1.29.0-py2.py3-none-any.whl", hash = "sha256:22aba50d91d642843570fd73853783cc26bad7ac618c778d2df49e54bd43bed9", size = 47149, upload-time = "2025-04-09T18:51:52.134Z" },
]
[[package]]
name = "hanzo-kms"
version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
{ name = "pydantic" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6d/78/c459ae92072e55d94e6b0718d927fb2801c06ea8ef8a5cacc417c237b385/hanzo_kms-1.1.0.tar.gz", hash = "sha256:13242266012dcc2a1b48705b48704504409f21c13ec022c32385f952cf4b9f8b", size = 7553, upload-time = "2026-02-21T06:20:06.542Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/80/13/77301a5216f4f3c85689061e590f606086f32f26bb0ea7a86ce850223e04/hanzo_kms-1.1.0-py3-none-any.whl", hash = "sha256:19ec34ae131917e153feade770f4aa03366ba43a662fba040d3e31cd78dd2fd3", size = 10672, upload-time = "2026-02-21T06:20:05.56Z" },
]
[[package]]
name = "hanzo-mcp"
version = "0.11.7"
@@ -1766,27 +1790,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/50/c7/53bdab3e003e839a7d574c8532d686c60642e85818ea861926a08b0f7822/hanzo_persona-1.0.0-py3-none-any.whl", hash = "sha256:e3c5e2d214d79af5f6dc728fdef35c7cad180ff8d3bcc6ba9fca30cbba96b068", size = 14857, upload-time = "2025-12-26T18:46:36.731Z" },
]
[[package]]
name = "hanzo-repl"
version = "0.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "colorama" },
{ name = "hanzo-mcp" },
{ name = "ipython" },
{ name = "litellm" },
{ name = "prompt-toolkit" },
{ name = "pygments" },
{ name = "python-dotenv" },
{ name = "rich" },
{ name = "textual" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d0/43/d60769f9eb5e46030f8072327bffe03cc968edfe238e86f4a26afb7853b2/hanzo_repl-0.1.0.tar.gz", hash = "sha256:4fed8869b2ebb2c9f808dd83e2f4b03f2f4ccc0427225decef6be3d8bfec7538", size = 29020, upload-time = "2025-07-26T02:48:42.12Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/57/bc/4d85d24c2b7f5e965962e0e66ce66bd8ad21ea0eaec2aa025d1e8f7f1099/hanzo_repl-0.1.0-py3-none-any.whl", hash = "sha256:198e639c18a7239e174adfe2d290212791aeb730b892a54f8f05e9c7829576ed", size = 35121, upload-time = "2025-07-26T02:48:40.739Z" },
]
[[package]]
name = "hanzo-tools"
version = "0.3.0"
@@ -2217,39 +2220,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec", size = 37461, upload-time = "2025-01-03T18:51:54.306Z" },
]
[[package]]
name = "ipython"
version = "9.10.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "decorator" },
{ name = "ipython-pygments-lexers" },
{ name = "jedi" },
{ name = "matplotlib-inline" },
{ name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
{ name = "prompt-toolkit" },
{ name = "pygments" },
{ name = "stack-data" },
{ name = "traitlets" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a6/60/2111715ea11f39b1535bed6024b7dec7918b71e5e5d30855a5b503056b50/ipython-9.10.0.tar.gz", hash = "sha256:cd9e656be97618a0676d058134cd44e6dc7012c0e5cb36a9ce96a8c904adaf77", size = 4426526, upload-time = "2026-02-02T10:00:33.594Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3d/aa/898dec789a05731cd5a9f50605b7b44a72bd198fd0d4528e11fc610177cc/ipython-9.10.0-py3-none-any.whl", hash = "sha256:c6ab68cc23bba8c7e18e9b932797014cc61ea7fd6f19de180ab9ba73e65ee58d", size = 622774, upload-time = "2026-02-02T10:00:31.503Z" },
]
[[package]]
name = "ipython-pygments-lexers"
version = "1.1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" },
]
[[package]]
name = "jaraco-classes"
version = "3.4.0"
@@ -2283,18 +2253,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl", hash = "sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176", size = 10481, upload-time = "2025-12-21T09:29:42.27Z" },
]
[[package]]
name = "jedi"
version = "0.19.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "parso" },
]
sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" },
]
[[package]]
name = "jeepney"
version = "0.9.0"
@@ -2634,18 +2592,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/9f/5b5481d716670ed5fbd8d06dfa94b7108272b645da2f2406eb909cb6a450/libcst-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:4d6acb0bdee1e55b44c6215c59755ec4693ac01e74bb1fde04c37358b378835d", size = 2029600, upload-time = "2024-10-10T14:14:54.815Z" },
]
[[package]]
name = "linkify-it-py"
version = "2.0.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "uc-micro-py" },
]
sdist = { url = "https://files.pythonhosted.org/packages/2a/ae/bb56c6828e4797ba5a4821eec7c43b8bf40f69cda4d4f5f8c8a2810ec96a/linkify-it-py-2.0.3.tar.gz", hash = "sha256:68cda27e162e9215c17d786649d1da0021a451bdc436ef9e0fa0ba5234b9b048", size = 27946, upload-time = "2024-02-04T14:48:04.179Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/1e/b832de447dee8b582cac175871d2f6c3d5077cc56d5575cadba1fd1cccfa/linkify_it_py-2.0.3-py3-none-any.whl", hash = "sha256:6bcbc417b0ac14323382aef5c5192c0075bf8a9d6b41820a2b66371eac6b6d79", size = 19820, upload-time = "2024-02-04T14:48:02.496Z" },
]
[[package]]
name = "litellm"
version = "1.81.6"
@@ -2838,11 +2784,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
]
[package.optional-dependencies]
linkify = [
{ name = "linkify-it-py" },
]
[[package]]
name = "markdownify"
version = "1.2.2"
@@ -2973,18 +2914,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5d/49/d651878698a0b67f23aa28e17f45a6d6dd3d3f933fa29087fa4ce5947b5a/matplotlib-3.10.8-cp314-cp314t-win_arm64.whl", hash = "sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f", size = 8192560, upload-time = "2025-12-10T22:56:38.008Z" },
]
[[package]]
name = "matplotlib-inline"
version = "0.2.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "traitlets" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c7/74/97e72a36efd4ae2bccb3463284300f8953f199b5ffbc04cbbb0ec78f74b1/matplotlib_inline-0.2.1.tar.gz", hash = "sha256:e1ee949c340d771fc39e241ea75683deb94762c8fa5f2927ec57c83c4dffa9fe", size = 8110, upload-time = "2025-10-23T09:00:22.126Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" },
]
[[package]]
name = "mccabe"
version = "0.7.0"
@@ -3019,18 +2948,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/fd/d9/eaa1f80170d2b7c5ba23f3b59f766f3a0bb41155fbc32a69adfa1adaaef9/mcp-1.26.0-py3-none-any.whl", hash = "sha256:904a21c33c25aa98ddbeb47273033c435e595bbacfdb177f4bd87f6dceebe1ca", size = 233615, upload-time = "2026-01-24T19:40:30.652Z" },
]
[[package]]
name = "mdit-py-plugins"
version = "0.5.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markdown-it-py" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b2/fd/a756d36c0bfba5f6e39a1cdbdbfdd448dc02692467d83816dff4592a1ebc/mdit_py_plugins-0.5.0.tar.gz", hash = "sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6", size = 44655, upload-time = "2025-08-11T07:25:49.083Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl", hash = "sha256:07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f", size = 57205, upload-time = "2025-08-11T07:25:47.597Z" },
]
[[package]]
name = "mdurl"
version = "0.1.2"
@@ -3811,15 +3728,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e6/3f/a80ac00acbc6b35166b42850e98a4f466e2c0d9c64054161ba9620f95680/pandas-3.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:1c39eab3ad38f2d7a249095f0a3d8f8c22cc0f847e98ccf5bbe732b272e2d9fa", size = 9441003, upload-time = "2026-01-21T15:52:02.281Z" },
]
[[package]]
name = "parso"
version = "0.8.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d4/de/53e0bcf53d13e005bd8c92e7855142494f41171b34c2536b86187474184d/parso-0.8.5.tar.gz", hash = "sha256:034d7354a9a018bdce352f48b2a8a450f05e9d6ee85db84764e9b6bd96dafe5a", size = 401205, upload-time = "2025-08-23T15:15:28.028Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl", hash = "sha256:646204b5ee239c396d040b90f9e272e9a8017c630092bf59980beb62fd033887", size = 106668, upload-time = "2025-08-23T15:15:25.663Z" },
]
[[package]]
name = "passlib"
version = "1.7.4"
@@ -3874,18 +3782,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/20/8b/28c4eaec9d6b036a52cb44720408f26b1a143ca9bce76cc19e8f5de00ab4/pdfminer_six-20260107-py3-none-any.whl", hash = "sha256:366585ba97e80dffa8f00cebe303d2f381884d8637af4ce422f1df3ef38111a9", size = 6592252, upload-time = "2026-01-07T13:29:10.742Z" },
]
[[package]]
name = "pexpect"
version = "4.9.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "ptyprocess", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" },
]
[[package]]
name = "pillow"
version = "11.3.0"
@@ -4165,24 +4061,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" },
]
[[package]]
name = "ptyprocess"
version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" },
]
[[package]]
name = "pure-eval"
version = "0.2.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" },
]
[[package]]
name = "puremagic"
version = "1.30"
@@ -5598,20 +5476,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/96/7f/832f015020844a8b8f7a9cbc103dd76ba8e3875004c41e08440ea3a2b41a/sse_starlette-3.2.0-py3-none-any.whl", hash = "sha256:5876954bd51920fc2cd51baee47a080eb88a37b5b784e615abb0b283f801cdbf", size = 12763, upload-time = "2026-01-17T13:11:03.775Z" },
]
[[package]]
name = "stack-data"
version = "0.6.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "asttokens" },
{ name = "executing" },
{ name = "pure-eval" },
]
sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" },
]
[[package]]
name = "standard-aifc"
version = "3.13.0"
@@ -5696,23 +5560,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" },
]
[[package]]
name = "textual"
version = "7.5.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markdown-it-py", extra = ["linkify"] },
{ name = "mdit-py-plugins" },
{ name = "platformdirs" },
{ name = "pygments" },
{ name = "rich" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9f/38/7d169a765993efde5095c70a668bf4f5831bb7ac099e932f2783e9b71abf/textual-7.5.0.tar.gz", hash = "sha256:c730cba1e3d704e8f1ca915b6a3af01451e3bca380114baacf6abf87e9dac8b6", size = 1592319, upload-time = "2026-01-30T13:46:39.881Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9c/78/96ddb99933e11d91bc6e05edae23d2687e44213066bcbaca338898c73c47/textual-7.5.0-py3-none-any.whl", hash = "sha256:849dfee9d705eab3b2d07b33152b7bd74fb1f5056e002873cc448bce500c6374", size = 718164, upload-time = "2026-01-30T13:46:37.635Z" },
]
[[package]]
name = "threadpoolctl"
version = "3.6.0"
@@ -5826,6 +5673,8 @@ dependencies = [
{ name = "typing-extensions" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/d3/54/a2ba279afcca44bbd320d4e73675b282fcee3d81400ea1b53934efca6462/torch-2.10.0-2-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:13ec4add8c3faaed8d13e0574f5cd4a323c11655546f91fbe6afa77b57423574", size = 79498202, upload-time = "2026-02-10T21:44:52.603Z" },
{ url = "https://files.pythonhosted.org/packages/ec/23/2c9fe0c9c27f7f6cb865abcea8a4568f29f00acaeadfc6a37f6801f84cb4/torch-2.10.0-2-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:e521c9f030a3774ed770a9c011751fb47c4d12029a3d6522116e48431f2ff89e", size = 79498254, upload-time = "2026-02-10T21:44:44.095Z" },
{ url = "https://files.pythonhosted.org/packages/cc/af/758e242e9102e9988969b5e621d41f36b8f258bb4a099109b7a4b4b50ea4/torch-2.10.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:5fd4117d89ffd47e3dcc71e71a22efac24828ad781c7e46aaaf56bf7f2796acf", size = 145996088, upload-time = "2026-01-21T16:24:44.171Z" },
{ url = "https://files.pythonhosted.org/packages/23/8e/3c74db5e53bff7ed9e34c8123e6a8bfef718b2450c35eefab85bb4a7e270/torch-2.10.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:787124e7db3b379d4f1ed54dd12ae7c741c16a4d29b49c0226a89bea50923ffb", size = 915711952, upload-time = "2026-01-21T16:23:53.503Z" },
{ url = "https://files.pythonhosted.org/packages/6e/01/624c4324ca01f66ae4c7cd1b74eb16fb52596dce66dbe51eff95ef9e7a4c/torch-2.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:2c66c61f44c5f903046cc696d088e21062644cbe541c7f1c4eaae88b2ad23547", size = 113757972, upload-time = "2026-01-21T16:24:39.516Z" },
@@ -5860,15 +5709,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f5/e2/31eac96de2915cf20ccaed0225035db149dfb9165a9ed28d4b252ef3f7f7/tqdm-4.67.2-py3-none-any.whl", hash = "sha256:9a12abcbbff58b6036b2167d9d3853042b9d436fe7330f06ae047867f2f8e0a7", size = 78354, upload-time = "2026-01-30T23:12:04.368Z" },
]
[[package]]
name = "traitlets"
version = "5.14.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" },
]
[[package]]
name = "transformers"
version = "5.0.0"
@@ -6056,15 +5896,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" },
]
[[package]]
name = "uc-micro-py"
version = "1.0.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/91/7a/146a99696aee0609e3712f2b44c6274566bc368dfe8375191278045186b8/uc-micro-py-1.0.3.tar.gz", hash = "sha256:d321b92cff673ec58027c04015fcaa8bb1e005478643ff4a500882eaab88c48a", size = 6043, upload-time = "2024-02-09T16:52:01.654Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/37/87/1f677586e8ac487e29672e4b17455758fce261de06a0d086167bb760361a/uc_micro_py-1.0.3-py3-none-any.whl", hash = "sha256:db1dffff340817673d7b466ec86114a9dc0e9d4d9b5ba229d9d60e5c12600cd5", size = 6229, upload-time = "2024-02-09T16:52:00.371Z" },
]
[[package]]
name = "urllib3"
version = "2.6.3"
+48
View File
@@ -35,6 +35,7 @@ from ._response import (
)
from .resources import (
add,
billing,
db,
dns,
pages,
@@ -55,6 +56,7 @@ from .resources import (
cart,
chain,
cohere,
commerce,
coupons,
delete,
device,
@@ -63,6 +65,7 @@ from .resources import (
gemini,
graphs,
health,
iam,
ingress,
jobs,
models,
@@ -105,6 +108,7 @@ from .resources import (
settings,
storage,
subscriptions,
team_workspace,
vectors,
wallets,
workflows,
@@ -271,6 +275,10 @@ class Hanzo(SyncAPIClient):
docdb: docdb.DocDBResource
ingress: ingress.IngressResource
datastore: datastore.DatastoreResource
billing: billing.BillingResource
iam: iam.IAMResource
commerce: commerce.CommerceResource
team_workspace: team_workspace.TeamWorkspaceResource
with_raw_response: HanzoWithRawResponse
with_streaming_response: HanzoWithStreamedResponse
@@ -457,6 +465,10 @@ class Hanzo(SyncAPIClient):
self.docdb = docdb.DocDBResource(self)
self.ingress = ingress.IngressResource(self)
self.datastore = datastore.DatastoreResource(self)
self.billing = billing.BillingResource(self)
self.iam = iam.IAMResource(self)
self.commerce = commerce.CommerceResource(self)
self.team_workspace = team_workspace.TeamWorkspaceResource(self)
self.with_raw_response = HanzoWithRawResponse(self)
self.with_streaming_response = HanzoWithStreamedResponse(self)
@@ -707,6 +719,10 @@ class AsyncHanzo(AsyncAPIClient):
docdb: docdb.AsyncDocDBResource
ingress: ingress.AsyncIngressResource
datastore: datastore.AsyncDatastoreResource
billing: billing.AsyncBillingResource
iam: iam.AsyncIAMResource
commerce: commerce.AsyncCommerceResource
team_workspace: team_workspace.AsyncTeamWorkspaceResource
with_raw_response: AsyncHanzoWithRawResponse
with_streaming_response: AsyncHanzoWithStreamedResponse
@@ -893,6 +909,10 @@ class AsyncHanzo(AsyncAPIClient):
self.docdb = docdb.AsyncDocDBResource(self)
self.ingress = ingress.AsyncIngressResource(self)
self.datastore = datastore.AsyncDatastoreResource(self)
self.billing = billing.AsyncBillingResource(self)
self.iam = iam.AsyncIAMResource(self)
self.commerce = commerce.AsyncCommerceResource(self)
self.team_workspace = team_workspace.AsyncTeamWorkspaceResource(self)
self.with_raw_response = AsyncHanzoWithRawResponse(self)
self.with_streaming_response = AsyncHanzoWithStreamedResponse(self)
@@ -1176,6 +1196,12 @@ class HanzoWithRawResponse:
self.docdb = docdb.DocDBResourceWithRawResponse(client.docdb)
self.ingress = ingress.IngressResourceWithRawResponse(client.ingress)
self.datastore = datastore.DatastoreResourceWithRawResponse(client.datastore)
self.billing = billing.BillingResourceWithRawResponse(client.billing)
self.iam = iam.IAMResourceWithRawResponse(client.iam)
self.commerce = commerce.CommerceResourceWithRawResponse(client.commerce)
self.team_workspace = team_workspace.TeamWorkspaceResourceWithRawResponse(
client.team_workspace
)
self.get_home = to_raw_response_wrapper(
client.get_home,
@@ -1339,6 +1365,12 @@ class AsyncHanzoWithRawResponse:
self.datastore = datastore.AsyncDatastoreResourceWithRawResponse(
client.datastore
)
self.billing = billing.AsyncBillingResourceWithRawResponse(client.billing)
self.iam = iam.AsyncIAMResourceWithRawResponse(client.iam)
self.commerce = commerce.AsyncCommerceResourceWithRawResponse(client.commerce)
self.team_workspace = team_workspace.AsyncTeamWorkspaceResourceWithRawResponse(
client.team_workspace
)
self.get_home = async_to_raw_response_wrapper(
client.get_home,
@@ -1502,6 +1534,12 @@ class HanzoWithStreamedResponse:
self.datastore = datastore.DatastoreResourceWithStreamingResponse(
client.datastore
)
self.billing = billing.BillingResourceWithStreamingResponse(client.billing)
self.iam = iam.IAMResourceWithStreamingResponse(client.iam)
self.commerce = commerce.CommerceResourceWithStreamingResponse(client.commerce)
self.team_workspace = team_workspace.TeamWorkspaceResourceWithStreamingResponse(
client.team_workspace
)
self.get_home = to_streamed_response_wrapper(
client.get_home,
@@ -1707,6 +1745,16 @@ class AsyncHanzoWithStreamedResponse:
self.datastore = datastore.AsyncDatastoreResourceWithStreamingResponse(
client.datastore
)
self.billing = billing.AsyncBillingResourceWithStreamingResponse(client.billing)
self.iam = iam.AsyncIAMResourceWithStreamingResponse(client.iam)
self.commerce = commerce.AsyncCommerceResourceWithStreamingResponse(
client.commerce
)
self.team_workspace = (
team_workspace.AsyncTeamWorkspaceResourceWithStreamingResponse(
client.team_workspace
)
)
self.get_home = async_to_streamed_response_wrapper(
client.get_home,
+1 -1
View File
@@ -1,4 +1,4 @@
# Hanzo AI SDK
__title__ = "hanzoai"
__version__ = "2.1.2" # x-release-please-version
__version__ = "2.2.0" # x-release-please-version
+68
View File
@@ -802,6 +802,46 @@ from .observability import (
AsyncObservabilityResourceWithStreamingResponse,
)
# Billing Resources
from .billing import (
BillingResource,
AsyncBillingResource,
BillingResourceWithRawResponse,
AsyncBillingResourceWithRawResponse,
BillingResourceWithStreamingResponse,
AsyncBillingResourceWithStreamingResponse,
)
# IAM Resources
from .iam import (
IAMResource,
AsyncIAMResource,
IAMResourceWithRawResponse,
AsyncIAMResourceWithRawResponse,
IAMResourceWithStreamingResponse,
AsyncIAMResourceWithStreamingResponse,
)
# Commerce Resources (unified)
from .commerce import (
CommerceResource,
AsyncCommerceResource,
CommerceResourceWithRawResponse,
AsyncCommerceResourceWithRawResponse,
CommerceResourceWithStreamingResponse,
AsyncCommerceResourceWithStreamingResponse,
)
# Team Workspace Resources
from .team_workspace import (
TeamWorkspaceResource,
AsyncTeamWorkspaceResource,
TeamWorkspaceResourceWithRawResponse,
AsyncTeamWorkspaceResourceWithRawResponse,
TeamWorkspaceResourceWithStreamingResponse,
AsyncTeamWorkspaceResourceWithStreamingResponse,
)
# New Platform Resources
from .mpc import (
MPCResource,
@@ -1485,4 +1525,32 @@ __all__ = [
"AsyncDatastoreResourceWithRawResponse",
"DatastoreResourceWithStreamingResponse",
"AsyncDatastoreResourceWithStreamingResponse",
# Billing Resources
"BillingResource",
"AsyncBillingResource",
"BillingResourceWithRawResponse",
"AsyncBillingResourceWithRawResponse",
"BillingResourceWithStreamingResponse",
"AsyncBillingResourceWithStreamingResponse",
# IAM Resources
"IAMResource",
"AsyncIAMResource",
"IAMResourceWithRawResponse",
"AsyncIAMResourceWithRawResponse",
"IAMResourceWithStreamingResponse",
"AsyncIAMResourceWithStreamingResponse",
# Commerce Resources (unified)
"CommerceResource",
"AsyncCommerceResource",
"CommerceResourceWithRawResponse",
"AsyncCommerceResourceWithRawResponse",
"CommerceResourceWithStreamingResponse",
"AsyncCommerceResourceWithStreamingResponse",
# Team Workspace Resources
"TeamWorkspaceResource",
"AsyncTeamWorkspaceResource",
"TeamWorkspaceResourceWithRawResponse",
"AsyncTeamWorkspaceResourceWithRawResponse",
"TeamWorkspaceResourceWithStreamingResponse",
"AsyncTeamWorkspaceResourceWithStreamingResponse",
]
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "hanzoai"
version = "2.1.2"
version = "2.2.0"
description = "The official Python library for the Hanzo API"
dynamic = ["readme"]
license = "BSD-3-Clause"
Generated
+27 -28
View File
@@ -868,6 +868,28 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
]
[[package]]
name = "hanzo-llm"
version = "1.81.15"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohttp" },
{ name = "click" },
{ name = "fastuuid" },
{ name = "httpx" },
{ name = "importlib-metadata" },
{ name = "jinja2" },
{ name = "jsonschema" },
{ name = "openai" },
{ name = "pydantic" },
{ name = "python-dotenv" },
{ name = "tiktoken" },
{ name = "tokenizers" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/37/15/3cd25b2908b6ec404d073ca57bbe1580e332a0ee094d0a33c36cd32d2e22/hanzo_llm-1.81.15-py3-none-any.whl", hash = "sha256:be3849567dc6a59b711a34f85bff1a74ad417df322cc38f3aebd16f9fa29823c", size = 14677430, upload-time = "2026-02-26T04:25:46.164Z" },
]
[[package]]
name = "hanzo-memory"
version = "1.0.1"
@@ -976,7 +998,7 @@ provides-extras = ["dev"]
[[package]]
name = "hanzoai"
version = "2.1.2"
version = "2.2.0"
source = { editable = "." }
dependencies = [
{ name = "anyio" },
@@ -990,8 +1012,8 @@ dependencies = [
]
[package.optional-dependencies]
litellm = [
{ name = "litellm" },
llm = [
{ name = "hanzo-llm" },
]
[package.dev-dependencies]
@@ -1011,14 +1033,14 @@ requires-dist = [
{ name = "anyio", specifier = ">=3.5.0,<5" },
{ name = "distro", specifier = ">=1.7.0,<2" },
{ name = "h11", specifier = ">=0.16.0" },
{ name = "hanzo-llm", marker = "extra == 'llm'", specifier = ">=1.0.0" },
{ name = "httpx", specifier = ">=0.23.0,<1" },
{ name = "litellm", marker = "extra == 'litellm'", specifier = ">=1.0.0" },
{ name = "pydantic", specifier = ">=1.9.0,<3" },
{ name = "sniffio" },
{ name = "typing-extensions", specifier = ">=4.10,<5" },
{ name = "urllib3", specifier = ">=2.6.0" },
]
provides-extras = ["litellm"]
provides-extras = ["llm"]
[package.metadata.requires-dev]
dev = [
@@ -1458,29 +1480,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/fc/85/69f92b2a7b3c0f88ffe107c86b952b397004b5b8ea5a81da3d9c04c04422/librt-0.7.8-cp314-cp314t-win_arm64.whl", hash = "sha256:8766ece9de08527deabcd7cb1b4f1a967a385d26e33e536d6d8913db6ef74f06", size = 40550, upload-time = "2026-01-14T12:56:01.542Z" },
]
[[package]]
name = "litellm"
version = "1.81.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohttp" },
{ name = "click" },
{ name = "fastuuid" },
{ name = "httpx" },
{ name = "importlib-metadata" },
{ name = "jinja2" },
{ name = "jsonschema" },
{ name = "openai" },
{ name = "pydantic" },
{ name = "python-dotenv" },
{ name = "tiktoken" },
{ name = "tokenizers" },
]
sdist = { url = "https://files.pythonhosted.org/packages/2e/f3/194a2dca6cb3eddb89f4bc2920cf5e27542256af907c23be13c61fe7e021/litellm-1.81.6.tar.gz", hash = "sha256:f02b503dfb7d66d1c939f82e4db21aeec1d6e2ed1fe3f5cd02aaec3f792bc4ae", size = 13878107, upload-time = "2026-02-01T04:02:27.36Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e6/05/3516cc7386b220d388aa0bd833308c677e94eceb82b2756dd95e06f6a13f/litellm-1.81.6-py3-none-any.whl", hash = "sha256:573206ba194d49a1691370ba33f781671609ac77c35347f8a0411d852cf6341a", size = 12224343, upload-time = "2026-02-01T04:02:23.704Z" },
]
[[package]]
name = "loguru"
version = "0.7.3"