mirror of
https://github.com/hanzoai/python-sdk.git
synced 2026-10-27 04:20:09 +00:00
564 lines
20 KiB
Python
564 lines
20 KiB
Python
"""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 Any, Annotated, final
|
|
|
|
from pydantic import Field
|
|
from mcp.server import FastMCP
|
|
from mcp.server.fastmcp import Context as MCPContext
|
|
|
|
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,
|
|
)
|