"""WalletPress AI Agent MCP Server — All wallet operations as AI-callable tools. Every tool is a @tool decorator. The MCP server can be: - Run standalone via `uvicorn agent.mcp_server:mcp_app` or `python -m agent.mcp_server` - Mounted as a FastAPI sub-app in main.py - Connected to any MCP client (Claude Code, opencode, Cursor, etc.) """ from __future__ import annotations import logging import time from mcp.server.fastmcp import FastMCP from core.config import cfg from core.vault import WalletEntry, get_vault from core.agent_safety import ( check_action_allowed, is_killed, request_confirmation, confirm_action, reject_action, list_pending, check_address, check_spending_limit, record_spending, audit_log, simulate_transaction, kill_agent as safety_kill, resurrect_agent as safety_resurrect, allowlist_add, blocklist_add, address_book_list, list_limits, audit_trail, verify_audit_chain, ) logger = logging.getLogger("wp.agent.mcp") mcp = FastMCP("WalletPress AI Agent", log_level="WARNING") def _safety_check(action: str, params: dict) -> dict | None: """Run safety checks for a WRITE action. Returns error dict or None if OK.""" # Kill switch if is_killed(): return {"error": "Agent is emergency stopped. Use agent_resurrect() to re-enable."} # Permission tier allowed, reason = check_action_allowed(action) if not allowed: return {"error": reason} # All checks passed return None import threading _HITL_SKIP_LOCAL = threading.local() def _is_hitl_skip() -> bool: return getattr(_HITL_SKIP_LOCAL, "skip", False) def _set_hitl_skip(val: bool) -> None: _HITL_SKIP_LOCAL.skip = val def _write_with_hitl(action: str, params: dict, reason: str = "") -> dict | None: """Run safety checks for a WRITE action. Returns: None if checks pass and the action should proceed directly. dict with error or confirmation request if action is blocked or needs HITL. """ if _is_hitl_skip(): return None # Called from agent_confirm — skip safety, execute directly # Kill switch if is_killed(): return {"error": "Agent is emergency stopped. Use agent_resurrect() to re-enable."} # Permission tier allowed, reason_tier = check_action_allowed(action) if not allowed: return {"error": reason_tier} # Request HITL confirmation (always for WRITE actions) result = request_confirmation(action, params, reason) audit_log(action, params, {"status": "pending_confirmation"}, confirmed_by="") return { "status": "pending_confirmation", "confirmation_id": result["confirmation_id"], "action": action, "reason": reason, "expires_at": result["expires_at"], "confirm_command": f"Use agent_confirm('{result['confirmation_id']}') to approve", "reject_command": f"Use agent_reject('{result['confirmation_id']}') to reject", } def _confirmed_call(action: str, params: dict, fn, *args, **kwargs): """Execute a tool function inside a confirmed HITL flow. Sets the per-thread skip flag so the tool runs without requesting confirmation again. Thread-safe under concurrent requests. """ _set_hitl_skip(True) try: return fn(*args, **kwargs) finally: _set_hitl_skip(False) # ── SAFETY & HITL TOOLS ────────────────────────────────── @mcp.tool() def agent_confirm(confirmation_id: str) -> dict: """Confirm a pending action and execute it. After reviewing a pending confirmation, call this to approve execution. Returns the result of the underlying action. Args: confirmation_id: The ID from a previous agent_execute or write request """ result = confirm_action(confirmation_id) if "error" in result: return result action = result["action"] params = result["params"] # Execute the approved action inside HITL-skip mode from agent.mcp_server import wallet_generate, wallet_from_mnemonic, vault_delete from agent.mcp_server import wallet_rotate, wallet_sweep from agent.mcp_server import schedule_dca, schedule_remove, anomaly_monitor TOOL_MAP = { "wallet_generate": lambda: _confirmed_call(action, params, wallet_generate, **params), "wallet_from_mnemonic": lambda: _confirmed_call(action, params, wallet_from_mnemonic, **params), "vault_delete": lambda: _confirmed_call(action, params, vault_delete, **params), "wallet_rotate": lambda: _confirmed_call(action, params, wallet_rotate, **params), "wallet_sweep": lambda: _confirmed_call(action, params, wallet_sweep, **params), "schedule_dca": lambda: _confirmed_call(action, params, schedule_dca, **params), "schedule_remove": lambda: _confirmed_call(action, params, schedule_remove, **params), "anomaly_monitor": lambda: _confirmed_call(action, params, anomaly_monitor, **params), } fn = TOOL_MAP.get(action) if not fn: return {"error": f"No confirmation handler for action: {action}"} audit_log(action, params, {"status": "executing"}, confirmed_by="human") try: exec_result = fn() audit_log(action, params, exec_result, confirmed_by="human") return {"confirmed": True, "action": action, "result": exec_result} except Exception as e: err_msg = f"{type(e).__name__}: {e}" audit_log(action, params, {"error": err_msg}, confirmed_by="human") return {"confirmed": True, "action": action, "error": err_msg} @mcp.tool() def agent_reject(confirmation_id: str, reason: str = "") -> dict: """Reject a pending action without executing it. Args: confirmation_id: The ID from a previous agent_execute or write request reason: Optional reason for rejection """ result = reject_action(confirmation_id, reason) if "error" in result: return result audit_log("agent_reject", {"confirmation_id": confirmation_id}, result) return result @mcp.tool() def agent_pending() -> list[dict]: """List all pending confirmation requests waiting for your approval.""" return list_pending() @mcp.tool() def agent_kill(reason: str = "") -> dict: """Emergency stop the agent immediately. All pending plans are canceled. Agent goes into read-only mode. Returns a resurrection key needed to re-enable the agent. Args: reason: Why are you killing the agent? """ result = safety_kill(reason) audit_log("agent_kill", {"reason": reason}, result) return result @mcp.tool() def agent_resurrect(key: str) -> dict: """Re-enable a killed agent using the resurrection key. Args: key: The resurrection key from agent_kill() """ result = safety_resurrect(key) if "error" not in result: audit_log("agent_resurrect", {}, result) return result @mcp.tool() def agent_limits() -> dict: """Show current spending limits and usage across all chains.""" return list_limits() @mcp.tool() async def agent_simulate(chain: str, from_address: str, to_address: str, amount: float) -> dict: """Simulate a transaction to see gas costs and expected outcome before executing. Args: chain: Chain key (eth, sol, etc.) from_address: Source wallet address to_address: Destination address amount: Amount in native tokens """ return await simulate_transaction(chain, from_address, to_address, amount) @mcp.tool() def agent_referral_status() -> dict: """Show the agent's revenue/referral configuration. In FREEMIUM mode, the agent uses Rug Munch Media's referral codes on every swap/trade — this costs you nothing and helps fund development. In SELF-REFERRAL mode, you keep 100% of referral revenue. P2-14: In multi-tenant (hosted) mode, this endpoint used to leak the operator's referral codes and wallets to any tenant. Now we redact sensitive fields (codes, wallet addresses) in hosted mode. The operator can see everything; tenants see only that the system is configured. Shows which platforms are configured, which need wallet setup, and what revenue share each platform provides. """ import os from plugins.defi import REVENUE_MODE, REF_JUPITER_WALLET, REF_HYPERLIQUID_WALLET from plugins.defi import REF_GMGN, REF_ODINBOT, REF_BANANA, REF_AXIOM, REF_PADRE # Hosted mode = multi-tenant. Redact operator secrets from response. is_hosted = bool(os.getenv("WP_HOSTED_MODE", "")) redact_secrets = is_hosted and REVENUE_MODE == "freemium" def _maybe_redact(value, kind: str = "value") -> str: if redact_secrets: if kind == "wallet": return "[REDACTED — operator-controlled]" return "[REDACTED]" return value or "NOT SET" platforms = [ { "platform": "Jupiter", "type": "fee_account", "share": "50bps", "configured": bool(REF_JUPITER_WALLET), "wallet": _maybe_redact(REF_JUPITER_WALLET, "wallet"), "docs": "https://jup.ag/referral", }, { "platform": "Hyperliquid", "type": "builder_code", "share": "up to 0.1%/trade", "configured": bool(REF_HYPERLIQUID_WALLET), "wallet": _maybe_redact(REF_HYPERLIQUID_WALLET, "wallet"), "docs": "https://hyperliquid.gitbook.io/hyperliquid-docs/trading/builder-codes.md", }, { "platform": "GMGN", "type": "code", "share": "40%", "configured": True, "code": _maybe_redact(REF_GMGN), "setup": "Use code at gmgn.ai", }, { "platform": "OdinBot", "type": "code", "share": "40%", "configured": True, "code": _maybe_redact(REF_ODINBOT), "setup": "Use code at t.me/OdinBot", }, { "platform": "Banana Gun", "type": "code", "share": "40%", "configured": True, "code": _maybe_redact(REF_BANANA), "setup": "Use code at t.me/BananaGunBot", }, { "platform": "Axiom", "type": "code", "share": "30%", "configured": True, "code": _maybe_redact(REF_AXIOM), "setup": "Use code at t.me/AxiomTrade", }, { "platform": "Padre", "type": "code", "share": "35%", "configured": True, "code": _maybe_redact(REF_PADRE), "setup": "Use code at t.me/PadreBot", }, ] freemium_note = "" if REVENUE_MODE == "freemium": configured = sum(1 for p in platforms if p.get("configured")) unconfigured = sum(1 for p in platforms if not p.get("configured")) freemium_note = ( f"{configured} platforms sending revenue, " f"{unconfigured} need wallet setup (Jupiter + Hyperliquid). " f"Set WP_REF_REVENUE_MODE=self to keep your own referral revenue." ) return { "revenue_mode": REVENUE_MODE.upper(), "your_cost": "free (we cover referral fees)" if REVENUE_MODE == "freemium" else "you control your own codes", "platforms": platforms, "note": freemium_note, "secrets_redacted": redact_secrets, "how_to_configure": ( "Jupiter: set WP_REF_JUPITER_WALLET (fee account). " "Hyperliquid: set WP_REF_HYPERLIQUID_WALLET (builder wallet, " "needs 100 USDC in perps)." ), } @mcp.tool() def agent_audit(limit: int = 50) -> list[dict]: """View the agent audit trail — every action logged with SHA-256 chain. Args: limit: Number of recent entries to show (default 50) """ return audit_trail(limit) @mcp.tool() def agent_audit_verify() -> dict: """Verify the integrity of the entire audit trail. Recomputes all SHA-256 hashes and checks the chain is unbroken. """ return verify_audit_chain() @mcp.tool() def address_allowlist(address: str, chain: str, label: str = "") -> dict: """Add an address to the agent's allowlist. The agent can only send to known addresses. Args: address: Wallet address to allow chain: Chain key (eth, sol, btc, etc.) label: Optional human-readable label (e.g. 'Exchange wallet') """ return allowlist_add(address, chain, label) @mcp.tool() def address_blocklist(address: str, chain: str, label: str = "") -> dict: """Add an address to the blocklist. The agent will never send funds here. Args: address: Wallet address to block chain: Chain key label: Reason for blocking """ return blocklist_add(address, chain, label) @mcp.tool() def address_list(list_type: str = "") -> list[dict]: """List addresses in the agent's address book. Args: list_type: 'allowlist', 'blocklist', or '' for all """ return address_book_list(list_type) @mcp.tool() def address_check(address: str, chain: str) -> dict: """Check if an address is safe to send funds to. Checks against: user's blocklist, known scam database, and optionally the allowlist (if WP_AGENT_REQUIRE_ALLOWLIST=1). Args: address: Wallet address to check chain: Chain key """ return check_address(address, chain) # ── WALLET GENERATION ────────────────────────────────────── @mcp.tool() def wallet_generate(chain: str, label: str = "", count: int = 1) -> dict: """Generate one or more wallets for any supported chain. Args: chain: Chain key (eth, sol, btc, trx, bsc, polygon, base, ...) label: Optional human-readable label count: Number of wallets to generate (default 1, max 100) """ hitl = _write_with_hitl("wallet_generate", {"chain": chain, "label": label, "count": count}, f"Generate {count} wallet(s) on {chain}") if "confirmation_id" in hitl: return hitl from wallet_engine.generator import get_generator vault = get_vault() generator = get_generator(cfg.vault_password) wallets = [] created_ids = [] try: for i in range(count): lbl = label or f"{chain}_{i + 1}_{int(time.time())}" try: wallet = generator.generate(chain, label=lbl) except ValueError as e: # P2-3: roll back any partial inserts so caller doesn't see # inconsistent state (e.g. 5 of 10 wallets persisted, error # in the middle, caller doesn't know which were saved). for wid in created_ids: vault.delete(wid) return {"error": str(e), "rolled_back": created_ids} entry = WalletEntry( id=f"wp_agent_{chain}_{int(time.time() * 1000)}_{i}", chain=wallet.chain, address=wallet.address, label=wallet.label, tags=wallet.tags, created_at=wallet.created_at, encrypted_key=wallet.private_key_hex if wallet.encrypted else "", public_key=wallet.public_key_hex, derivation_path=wallet.derivation_path, encrypted=wallet.encrypted, ) vault.put(entry) created_ids.append(entry.id) wallets.append({"id": entry.id, "chain": entry.chain, "address": entry.address, "label": entry.label}) except Exception as e: # Any unexpected error — roll back partial inserts. for wid in created_ids: vault.delete(wid) return {"error": f"{type(e).__name__}: {e}", "rolled_back": created_ids} return {"generated": len(wallets), "wallets": wallets} @mcp.tool() def wallet_from_mnemonic(mnemonic: str, chains: list[str] | None = None) -> dict: """Derive wallets from a BIP39 mnemonic phrase. Args: mnemonic: BIP39 seed phrase (12 or 24 words) chains: List of chains to derive (empty = all 55 chains) """ hitl = _write_with_hitl("wallet_from_mnemonic", {"chains": chains}, "Derive wallets from a mnemonic phrase") if hitl: return hitl from wallet_engine.chains import CHAINS from wallet_engine.generator import get_generator vault = get_vault() generator = get_generator(cfg.vault_password) chain_list = chains or list(CHAINS.keys()) results = {} for ck in chain_list: try: wallet = generator.generate(ck, mnemonic=mnemonic, label=f"mnemonic_{ck}") except ValueError: results[ck] = {"error": "chain not supported"} continue entry = WalletEntry( id=f"wp_mcp_mnemonic_{ck}_{int(time.time())}", chain=wallet.chain, address=wallet.address, label=wallet.label, tags=wallet.tags + ["from_mnemonic"], created_at=wallet.created_at, encrypted_key=wallet.private_key_hex if wallet.encrypted else "", public_key=wallet.public_key_hex, derivation_path=wallet.derivation_path, encrypted=wallet.encrypted, ) vault.put(entry) results[ck] = {"address": entry.address, "wallet_id": entry.id} return {"derived_for": len(results), "chains": results} # ── VAULT MANAGEMENT ─────────────────────────────────────── @mcp.tool() def vault_list(chain: str = "", limit: int = 50) -> list[dict]: """List wallets in the vault (no private keys returned). Args: chain: Optional filter by chain limit: Max results (default 50) """ vault = get_vault() wallets = vault.list(chain=chain, limit=limit) if chain else vault.list(limit=limit) return [{ "id": w.id, "chain": w.chain, "address": w.address, "label": w.label, "tags": w.tags, "created_at": w.created_at, "encrypted": w.encrypted, "balance_usd": w.balance_usd, } for w in wallets] @mcp.tool() def vault_get(wallet_id: str, include_private_key: bool = False) -> dict: """Get wallet details from the vault. By default returns only public info (address, public_key, derivation_path). Private key export requires explicit opt-in via include_private_key=True AND goes through HITL confirmation. This is the LAST line of defense before a plaintext key leaves the process — never bypass it. P2-4 fix: Previously the function unconditionally decrypted and returned the private key, with no audit trail of the export beyond the agent audit (which the caller controls). Now: - Default: returns public info only (no private key field at all) - include_private_key=True: requires HITL confirmation - Every export logged via audit_log with explicit 'key_export' tag Args: wallet_id: Wallet ID from vault_list include_private_key: Set True to export the decrypted private key """ vault = get_vault() wallet = vault.get(wallet_id) if not wallet: return {"error": f"Wallet {wallet_id} not found"} result = { "id": wallet.id, "chain": wallet.chain, "address": wallet.address, "label": wallet.label, "tags": wallet.tags, "created_at": wallet.created_at, "public_key": wallet.public_key, "derivation_path": wallet.derivation_path, "encrypted": wallet.encrypted, "balance_usd": wallet.balance_usd, "private_key_exported": False, } if not include_private_key: return result # Private key export requires HITL confirmation. This is a destructive # operation — the caller is about to receive a plaintext key. hitl = _write_with_hitl( "key_export", {"wallet_id": wallet_id, "chain": wallet.chain, "address": wallet.address}, f"Export private key for {wallet.chain} wallet {wallet.address[:12]}...", ) if "confirmation_id" in hitl: return hitl if wallet.encrypted_key and wallet.encrypted and cfg.vault_password: from wallet_engine.generator import get_generator generator = get_generator(cfg.vault_password) result["private_key"] = generator.decrypt_key(wallet.encrypted_key) result["private_key_exported"] = True audit_log( "key_export", {"wallet_id": wallet_id, "chain": wallet.chain, "address": wallet.address}, {"status": "exported"}, confirmed_by="human", ) logger.warning( f"Private key exported for wallet {wallet_id} ({wallet.chain}, {wallet.address[:12]}...)" ) elif wallet.encrypted_key: # Encrypted but we have no KEK. Return encrypted form, not the raw key. result["private_key_encrypted"] = wallet.encrypted_key result["note"] = ( "Wallet is encrypted but vault password is not set in this " "process. Cannot decrypt. Set WP_VAULT_PASSWORD or configure the " "file-based KEK." ) return result @mcp.tool() def vault_delete(wallet_id: str) -> dict: """Delete a wallet permanently. Args: wallet_id: Wallet ID to delete """ hitl = _write_with_hitl("vault_delete", {"wallet_id": wallet_id}, f"Permanently delete wallet {wallet_id}") if hitl: return hitl vault = get_vault() if vault.delete(wallet_id): return {"deleted": True, "wallet_id": wallet_id} return {"error": f"Wallet {wallet_id} not found"} @mcp.tool() def vault_stats() -> dict: """Get vault statistics — total wallets, encrypted count, chains used, rotations.""" vault = get_vault() return vault.stats() # ── BALANCE & ANALYSIS ───────────────────────────────────── @mcp.tool() async def balance_check(chain: str, address: str) -> dict: """Check wallet balance on any supported chain. Args: chain: Chain key (eth, sol, btc, trx, ...) address: Wallet address """ from routers.balance_fetcher import fetch_balance return await fetch_balance(chain, address) @mcp.tool() def vault_balances(chain: str = "") -> dict: """Get balances for all wallets in the vault. Args: chain: Optional chain filter """ vault = get_vault() wallets = vault.list(chain=chain, limit=500) if chain else vault.list(limit=500) return { "wallets": [{"id": w.id, "chain": w.chain, "address": w.address, "balance_usd": w.balance_usd} for w in wallets], "total_usd": sum(w.balance_usd for w in wallets), "wallet_count": len(wallets), } # ── WALLET MANAGEMENT ────────────────────────────────────── @mcp.tool() def wallet_rotate(wallet_id: str, reason: str = "agent_rotation") -> dict: """Rotate a wallet to a new address. Old wallet preserved with 'rotated' tag. Args: wallet_id: ID of existing wallet reason: Rotation reason """ hitl = _write_with_hitl("wallet_rotate", {"wallet_id": wallet_id, "reason": reason}, f"Rotate keys for wallet {wallet_id}: {reason}") if hitl: return hitl from wallet_engine.generator import get_generator vault = get_vault() old = vault.get(wallet_id) if not old: return {"error": f"Wallet {wallet_id} not found"} generator = get_generator(cfg.vault_password) new = generator.generate(old.chain, label=f"rotation_{old.chain}_{int(time.time())}", tags=["rotated", "active"]) new_entry = WalletEntry( id=f"wp_rot_{old.chain}_{int(time.time())}", chain=new.chain, address=new.address, label=new.label, tags=new.tags, created_at=new.created_at, encrypted_key=new.private_key_hex if new.encrypted else "", public_key=new.public_key_hex, derivation_path=new.derivation_path, encrypted=new.encrypted, ) vault.put(new_entry) vault.record_rotation(old.id, new.address, reason=reason) return { "rotated": True, "old_wallet_id": old.id, "old_address": old.address, "new_wallet_id": new_entry.id, "new_address": new_entry.address, "chain": old.chain, } @mcp.tool() def wallet_sweep(from_wallet_id: str, to_address: str) -> dict: """Sweep ALL funds from a vault wallet to an external address. Implements actual on-chain transfer for EVM chains (Ethereum, Base, Polygon, Arbitrum, Optimism, BNB Chain, Avalanche, Fantom, Gnosis, etc.). For non-EVM chains (Solana, TRON, BTC family), returns a clear "not implemented" error with the transaction params the caller can sign. Security checks performed BEFORE signing: 1. Address allow/block list (block known scam addresses) 2. Spending limits (per-chain, per-period) 3. HITL confirmation (human must approve via agent_confirm) Args: from_wallet_id: Source wallet ID from vault to_address: Destination address (must be valid for the chain) """ from core.balance_fetcher import EVM_RPC # Address safety check vault = get_vault() wallet = vault.get(from_wallet_id) chain = wallet.chain if wallet else "unknown" addr_check = check_address(to_address, chain) if not addr_check["allowed"]: return {"error": f"Address blocked: {addr_check['reason']}"} # Spending limit check limit_check = check_spending_limit(chain, 0) if not limit_check["allowed"]: return {"error": f"Spending limit exceeded: {limit_check['exceeded']}"} hitl = _write_with_hitl("wallet_sweep", {"from_wallet_id": from_wallet_id, "to_address": to_address}, f"Sweep funds from {from_wallet_id} to {to_address}") if hitl: return hitl wallet = vault.get(from_wallet_id) if not wallet: return {"error": f"Wallet {from_wallet_id} not found"} # EVM chain: build, sign, and broadcast via tx_broadcaster if chain in EVM_RPC: return _evm_sweep(wallet, to_address, chain) # Non-EVM: return intent + clear "not implemented" notice return { "status": "intent_only", "chain": chain, "wallet_id": from_wallet_id, "from_address": wallet.address, "to_address": to_address, "note": ( f"Auto-broadcast not yet implemented for {chain}. " f"Sign the transaction externally with the wallet's private key " f"and broadcast via tx_broadcaster.broadcast()." ), } def _evm_sweep(wallet, to_address: str, chain: str) -> dict: """Build, sign, and broadcast an EVM sweep transaction. Returns the tx_hash on success or an error dict on failure. """ import asyncio from eth_account import Account from web3 import Web3 from web3.exceptions import Web3Exception from core.balance_fetcher import EVM_RPC from routers.tx_broadcaster import _evm_broadcast rpc_url = EVM_RPC.get(chain) if not rpc_url: return {"error": f"No RPC configured for chain {chain}"} # Decrypt the private key from the vault from wallet_engine.generator import get_generator gen = get_generator(cfg.vault_password) if not wallet.encrypted_key or not wallet.encrypted: return {"error": f"Wallet {wallet.id} has no encrypted key (client-side only?)"} try: privkey_hex = gen.decrypt_key(wallet.encrypted_key) except Exception as e: return {"error": f"Failed to decrypt private key: {e}"} try: w3 = Web3(Web3.HTTPProvider(rpc_url, request_kwargs={"timeout": 15})) if not w3.is_connected(): return {"error": f"Cannot connect to RPC for {chain}"} account = Account.from_key(privkey_hex) nonce = w3.eth.get_transaction_count(account.address) gas_price = w3.eth.gas_price chain_id = w3.eth.chain_id # Get balance — sweep = balance - gas balance_wei = w3.eth.get_balance(account.address) # Estimate gas for a simple transfer (21k) + buffer gas_limit = 21000 gas_cost_wei = gas_limit * gas_price if balance_wei <= gas_cost_wei: return { "error": "Insufficient balance to cover gas", "balance_wei": balance_wei, "gas_cost_wei": gas_cost_wei, } value_wei = balance_wei - gas_cost_wei # Build the transaction tx = { "nonce": nonce, "to": to_address, "value": value_wei, "gas": gas_limit, "gasPrice": gas_price, "chainId": chain_id, } # Sign signed = account.sign_transaction(tx) # Broadcast via existing tx_broadcaster helper (handles errors uniformly) loop = asyncio.new_event_loop() try: result = loop.run_until_complete(_evm_broadcast(chain, signed.raw_transaction.hex())) finally: loop.close() if result.get("broadcast"): # Record the spending try: amount_eth = float(w3.from_wei(value_wei, "ether")) record_spending(chain, amount_eth) except Exception: pass audit_log( "wallet_sweep", {"from_wallet_id": wallet.id, "to_address": to_address, "chain": chain}, {"tx_hash": result["tx_hash"], "amount_wei": value_wei}, confirmed_by="human", ) return { "status": "broadcast", "chain": chain, "from_wallet_id": wallet.id, "from_address": wallet.address, "to_address": to_address, "tx_hash": result["tx_hash"], "amount_wei": value_wei, "amount_native": float(w3.from_wei(value_wei, "ether")), "gas_cost_wei": gas_cost_wei, "explorer_url": f"https://etherscan.io/tx/{result['tx_hash']}" if chain == "eth" else None, } else: return { "error": f"Broadcast failed: {result.get('error', 'unknown')}", "chain": chain, } except Web3Exception as e: return {"error": f"Web3 error: {e}", "chain": chain} except Exception as e: return {"error": f"{type(e).__name__}: {e}", "chain": chain} # ── CROSS-CHAIN ──────────────────────────────────────────── @mcp.tool() def validate_address(chain: str, address: str) -> dict: """Validate if an address is structurally valid for a given chain. Args: chain: Chain key address: Address to validate """ from wallet_engine.chains import validate_address as va, CHAINS chain_info = CHAINS.get(chain.lower()) if not chain_info: return {"error": f"Unsupported chain: {chain}"} valid = va(chain, address) return { "chain": chain, "address": address, "valid": valid, "expected_pattern": chain_info.address_pattern, } @mcp.tool() def chains_list() -> dict: """List all 55 supported blockchains with metadata.""" from wallet_engine.chains import CHAINS families = set() chain_list = [] for k, c in sorted(CHAINS.items()): families.add(c.family.value) chain_list.append({ "key": k, "name": c.name, "symbol": c.symbol, "family": c.family.value, "curve": c.curve, }) return { "total_chains": len(chain_list), "total_families": len(families), "chains": chain_list, } # ── AGENT INTELLIGENCE ───────────────────────────────────── @mcp.tool() def agent_plan(prompt: str, context: str = "") -> dict: """Plan a multi-step wallet operation from natural language. The LLM analyzes the prompt and returns a structured plan of tool calls to execute. Use this for complex operations like: - "sweep all ETH from my vault to 0x..." - "generate 5 SOL wallets for my airdrop campaign" - "rotate my oldest wallet and check the new balance" - "tell me my total portfolio value across all chains" Args: prompt: Natural language instruction context: Optional context (e.g. current vault state summary) """ from agent.orchestrator import plan_operation return plan_operation(prompt, context) @mcp.tool() def agent_execute(plan: list[dict] | str, confirm: bool = False) -> dict: """Execute a pre-approved multi-step plan. Args: plan: The plan from agent_plan, or a JSON string of steps confirm: If True, requires human confirmation before executing """ from agent.orchestrator import execute_plan return execute_plan(plan, confirm=confirm) # ── SCHEDULER ────────────────────────────────────────────── @mcp.tool() def schedule_dca(chain: str, amount: float, frequency: str = "weekly", to_address: str = "", label: str = "") -> dict: """Set up a DCA (dollar-cost averaging) schedule. Generates a wallet and periodically sends funds to it. Args: chain: Chain key amount: Amount in native token per interval frequency: 'daily', 'weekly', 'biweekly', 'monthly' to_address: Optional destination (empty = generate new wallet each time) label: Optional label for generated wallets """ hitl = _write_with_hitl("schedule_dca", {"chain": chain, "amount": amount, "frequency": frequency}, f"Set up {frequency} DCA of {amount} {chain}") if hitl: return hitl from agent.scheduler import add_dca_task return add_dca_task(chain, amount, frequency, to_address, label) @mcp.tool() def schedule_list() -> list[dict]: """List all active scheduled tasks (DCA, sweeps, monitors).""" from agent.scheduler import scheduler return scheduler.list_tasks() @mcp.tool() def schedule_remove(task_id: str) -> dict: """Remove a scheduled task. Args: task_id: ID from schedule_list """ hitl = _write_with_hitl("schedule_remove", {"task_id": task_id}, f"Remove scheduled task {task_id}") if hitl: return hitl from agent.scheduler import scheduler if scheduler.remove_task(task_id): return {"removed": True, "task_id": task_id} return {"error": f"Task {task_id} not found"} # ── ANOMALY DETECTION ────────────────────────────────────── @mcp.tool() async def anomaly_scan(address: str, chain: str = "eth") -> dict: """Scan a wallet for anomalous activity. Checks: unusually large transactions, new address interactions, rapid-fire transactions, unusual timing. Args: address: Wallet address to scan chain: Chain key """ from agent.detector import scan_wallet return await scan_wallet(address, chain) @mcp.tool() def anomaly_monitor(wallet_id: str, webhook_url: str = "") -> dict: """Set up continuous monitoring for a wallet. Triggers alerts on: large transfers, first-time interactions, unusual volume spikes, rapid consecutive transactions. Args: wallet_id: Wallet ID from vault webhook_url: Optional webhook for alerts """ from agent.scheduler import add_monitor_task return add_monitor_task(wallet_id, webhook_url) # ── PROOF OF GENERATION ──────────────────────────────────── @mcp.tool() def proof_attestations() -> dict: """Get Proof of Generation statistics — total attestations, committed, uncommitted roots.""" from core.proof import get_proof return get_proof().stats() @mcp.tool() def proof_commit_pending(chain: str = "local") -> dict: """Commit all pending attestations to a Merkle root. Args: chain: 'local' (SQLite, free), 'arweave' (permanent, ~$0.000001), 'ethereum' (on-chain, ~$5-50 gas) """ from core.proof import get_proof proof = get_proof() root_hash, count = proof.compute_merkle_root() if not root_hash: return {"committed": False, "reason": "No pending attestations"} result = proof.commit(root_hash, count, commitment_chain=chain) return { "committed": True, "root_hash": root_hash, "attestation_count": count, "chain": chain, "commitment_tx": result.get("commitment_tx", ""), "verification_url": result.get("verification_url", ""), } @mcp.tool() def proof_verify_wallet(wallet_id: str) -> dict: """Verify a wallet's Proof of Generation attestation. Args: wallet_id: Wallet ID from vault_list """ from core.proof import get_proof from core.vault import get_vault vault = get_vault() wallet = vault.get(wallet_id) if not wallet: return {"error": f"Wallet {wallet_id} not found"} result = get_proof().verify(wallet_id, wallet.address, wallet.public_key) return result @mcp.tool() def proof_provenance(wallet_id: str) -> dict: """Get the complete cryptographic provenance for a wallet. Includes the Merkle proof path so it can be independently verified. This is the wallet's birth certificate. Args: wallet_id: Wallet ID from vault_list """ from core.proof import get_proof from core.vault import get_vault vault = get_vault() wallet = vault.get(wallet_id) if not wallet: return {"error": f"Wallet {wallet_id} not found"} result = get_proof().get_proof_by_wallet(wallet_id) return result @mcp.tool() def proof_roots(limit: int = 10) -> dict: """List recent Merkle roots committed for wallet attestations. Args: limit: Max roots to return (default 10) """ from core.proof import get_proof proof = get_proof() return { "stats": proof.stats(), "roots": proof.get_roots(limit), } @mcp.tool() def verify_order(order_id: str) -> dict: """Verify an x402 marketplace order — proves we generated the wallets and didn't keep keys. Returns the order details, receipt signature, and attestation proof. Anyone can independently verify this order was fulfilled by WalletPress. Args: order_id: Order ID from the generate response (e.g. 'x402_abc123...') """ from core.db_pool import DbPool from core.config import cfg from core.proof import get_receipt_public_key pool = DbPool(cfg.data_dir / "marketplace.db") row = pool.execute("SELECT * FROM orders WHERE order_id = ?", (order_id,)).fetchone() if not row: return {"error": f"Order {order_id} not found"} pubkey = get_receipt_public_key() return { "order_id": order_id, "chain": row["chain"], "count": row["count"], "amount_usd": row["amount_usd"], "created_at": row["created_at"], "status": row["status"], "payment_method": "onchain" if row.get("payment_tx") and row["payment_tx"] != "free" else "free", "receipt_public_key": pubkey, "verification": { "keys_stored": False, "keys_returned_once": True, "open_source": "https://github.com/cryptorugmuncher/walletpress", "note": "This order was fulfilled by WalletPress. Keys were generated in memory and returned once.", }, } @mcp.tool() def marketplace_transparency() -> dict: """Get the public transparency dashboard — stats, roots, guarantees. Proves WalletPress is operating transparently. No secrets here. """ from core.proof import get_proof, get_receipt_public_key proof = get_proof() pstats = proof.stats() roots = proof.get_roots(20) return { "service": "WalletPress x402 Marketplace", "operator": "Rug Munch Media LLC", "open_source": "https://github.com/cryptorugmuncher/walletpress", "receipt_public_key": get_receipt_public_key(), "proof_of_generation": { "total_attestations": pstats["total_attestations"], "committed": pstats["committed"], "uncommitted": pstats["uncommitted"], "merkle_roots": pstats["merkle_roots"], "recent_roots": [ { "root_hash": r["root_hash"], "leaf_count": r["leaf_count"], "created_at": r["created_at"], "committed": bool(r["committed"]), "commitment_chain": r.get("commitment_chain", ""), "commitment_tx": r.get("commitment_tx", ""), } for r in roots ], }, "trust_guarantees": [ "Keys generated in memory, returned once, never stored", "Every order signed with Ed25519 receipt", "Every wallet attested in SHA-256 Merkle tree", "Merkle roots committed to Arweave for permanent proof", "Open source — verify every line of code", "Reproducible Docker builds — image hash matches source", "No telemetry, no tracking, no data collection", "Audit trail append-only, immutable", ], } # ── STANDALONE ───────────────────────────────────────────── if __name__ == "__main__": mcp.run()