mirror of
https://github.com/hanzoai/python-sdk.git
synced 2026-10-27 04:20:09 +00:00
120 lines
4.6 KiB
Python
120 lines
4.6 KiB
Python
"""repo — your repositories on Hanzo git and the GitHub links that feed them (HIP-0300).
|
|
|
|
list GET /v1/sync every link: source, target, direction, where it stands
|
|
sync POST /v1/sync/{id}/run sync one now; the reconcile is queued and runs in the background
|
|
|
|
A link is a GitHub repository (or a whole account) declared once and kept in step
|
|
with its copy on git.hanzo.ai. The calls go to api.hanzo.ai through the one
|
|
HanzoCloud client and the caller's Hanzo credential.
|
|
"""
|
|
|
|
from typing import Any, ClassVar
|
|
from urllib.parse import quote
|
|
|
|
from mcp.server.fastmcp import Context as MCPContext
|
|
|
|
from hanzo_tools.core import BaseTool, HanzoCloud, InvalidParamsError, ToolError
|
|
from hanzo_tools.core.cloud import NO_KEY, CloudError
|
|
|
|
|
|
def row(v: Any) -> dict[str, Any]:
|
|
"""One link as an agent reads it: where it comes from, where it goes, how it stands."""
|
|
v = v if isinstance(v, dict) else {}
|
|
native = v.get("native") if isinstance(v.get("native"), dict) else {}
|
|
return {
|
|
"id": v.get("id"),
|
|
"scope": v.get("scope"),
|
|
"source": (v.get("source") or {}).get("locator"),
|
|
"target": (v.get("target") or {}).get("locator"),
|
|
"direction": v.get("direction"),
|
|
"trigger": v.get("trigger"),
|
|
"status": native.get("status"),
|
|
"branch": native.get("branch"),
|
|
"url": native.get("url"),
|
|
# bumped by every reconcile, so it reads as the last sync
|
|
"synced_at": v.get("updatedAt"),
|
|
}
|
|
|
|
|
|
LIST_SCHEMA = {
|
|
"type": "object",
|
|
"properties": {
|
|
"search": {
|
|
"type": "string",
|
|
"description": "list: words that must all appear in the source or target",
|
|
},
|
|
"status": {
|
|
"type": "string",
|
|
"enum": ["synced", "conflict", "pending", "paused"],
|
|
"description": "list: only links that stand so",
|
|
},
|
|
},
|
|
"required": [],
|
|
}
|
|
SYNC_SCHEMA = {
|
|
"type": "object",
|
|
"properties": {
|
|
"id": {"type": "string", "description": "sync: the link's id from list (sync_...)"}
|
|
},
|
|
"required": [],
|
|
}
|
|
|
|
|
|
class RepoTool(BaseTool):
|
|
"""Repositories on Hanzo git linked to GitHub: list them, sync one now."""
|
|
|
|
name: ClassVar[str] = "repo"
|
|
VERSION: ClassVar[str] = "0.1.0"
|
|
DEFAULT_ACTION: ClassVar[str] = "list"
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self._cloud: HanzoCloud | None = None
|
|
|
|
@self.action("list", "Every repository link and where it stands", schema=LIST_SCHEMA)
|
|
async def list_(
|
|
ctx: MCPContext, search: str | None = None, status: str | None = None
|
|
) -> dict:
|
|
body = await self._send("GET", "/v1/sync")
|
|
rows = body.get("data") if isinstance(body, dict) else None
|
|
words = (search or "").lower().split()
|
|
links = []
|
|
for v in rows if isinstance(rows, list) else []:
|
|
r = row(v)
|
|
if status and r["status"] != status:
|
|
continue
|
|
hay = f"{r['source'] or ''} {r['target'] or ''}".lower()
|
|
if all(w in hay for w in words):
|
|
links.append(r)
|
|
return {"count": len(links), "repos": links}
|
|
|
|
@self.action("sync", "Sync one repository link now", schema=SYNC_SCHEMA)
|
|
async def sync(ctx: MCPContext, id: str | None = None) -> dict:
|
|
if not isinstance(id, str) or not id.strip():
|
|
raise InvalidParamsError(
|
|
"id required: a link's id from repo list (sync_...)", param="id"
|
|
)
|
|
return await self._send("POST", f"/v1/sync/{quote(id.strip(), safe='')}/run")
|
|
|
|
@property
|
|
def description(self) -> str:
|
|
return (
|
|
"Your repositories on Hanzo git and the GitHub links that keep them in step (/v1/sync). "
|
|
"list (default): every link with its id, scope (repo or account), source, target, direction "
|
|
"(both, pull, push, off), trigger, status (synced, conflict, pending, paused), branch, url and "
|
|
"synced_at; search (words in source or target) and status narrow it. "
|
|
"sync: sync one link now by its id; answers {queued, id} and the reconcile runs in the background."
|
|
)
|
|
|
|
async def _send(self, method: str, path: str) -> Any:
|
|
if self._cloud is None:
|
|
self._cloud = HanzoCloud()
|
|
if not self._cloud.configured():
|
|
raise ToolError(code="INVALID_PARAMS", message=NO_KEY)
|
|
try:
|
|
return await self._cloud.call(method, path)
|
|
except CloudError as e:
|
|
if e.status == 404:
|
|
raise ToolError(code="NOT_FOUND", message=str(e))
|
|
raise ToolError(code="UPSTREAM", message=str(e))
|