mirror of
https://github.com/hanzoai/python-sdk.git
synced 2026-10-23 20:23:00 +00:00
fix: finalize latest MCP/CLI/SDK updates
This commit is contained in:
@@ -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})")
|
||||
@@ -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)
|
||||
|
||||
@@ -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"]
|
||||
@@ -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]"
|
||||
)
|
||||
@@ -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]")
|
||||
Generated
+1081
File diff suppressed because it is too large
Load Diff
@@ -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"],
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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"]
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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"]
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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"]
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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"]
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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"]
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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"]
|
||||
Generated
+71
-240
@@ -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"
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
# Hanzo AI SDK
|
||||
|
||||
__title__ = "hanzoai"
|
||||
__version__ = "2.1.2" # x-release-please-version
|
||||
__version__ = "2.2.0" # x-release-please-version
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user