aldabra/crates/aldabra-mcp/src/main.rs
Sulkta 173abb9322 feat(dao): wire 8 dao_* MCP tools (Phase 1)
Tools added to WalletService:

DAO management (filesystem-only, no chain calls):
- dao_register      — save a DaoConfig under \$ALDABRA_DATA/daos/<name>.json
- dao_list          — show all registered DAO names + active marker
- dao_use           — set active DAO; subsequent dao_* calls without
                      explicit `dao` arg target this one
- dao_remove        — delete config; clears active if it was the active one
- dao_show          — render full DaoConfig JSON for audit

DAO live-state reads (Koios-backed, decoded into typed Rust):
- dao_governor_state  — singleton governor UTxO + thresholds + timing
                        + nextProposalId + per-stake proposal cap
- dao_stake_list      — all stakes for the DAO (filtered to gov-token
                        policy so the shared MLabs stakes addr doesn't
                        leak other DAOs into output). Renders pkh,
                        amount, locks, delegation per stake.
- dao_my_stake        — filters dao_stake_list to just THIS wallet's
                        stake (matches wallet pkh against StakeDatum.owner).
                        Empty array if not staked yet.

Plumbing:
- WalletService::new gains data_dir param (for DaoStore root)
- WalletInner gains dao_store + dao_reader fields
- wallet_pkh() helper extracts the wallet's payment-credential hash from
  bech32 for owner-match in dao_my_stake
- get_info() instructions advertise the new dao_* surface
- aldabra-mcp/Cargo.toml: aldabra-dao path dep + hex + pallas-addresses
2026-05-05 13:51:04 -07:00

148 lines
5.1 KiB
Rust

//! aldabra — MCP server entry point.
//!
//! Speaks MCP over stdio. Any MCP client (e.g. Claude Code)
//! launches this as a subprocess and gets a wallet's worth of tools.
//!
//! ## Phase 1 tools (target — server wiring lands in 1.7)
//!
//! - `wallet.address` — derived CIP-1852 base address
//! - `wallet.balance` — ADA + native-asset balance via chain backend
//! - `wallet.utxos` — list UTXOs at the wallet address
//! - `wallet.network` — configured network selector
//!
//! ## Phase 2-4 tools
//!
//! See `ROADMAP.md` at the repo root.
//!
//! ## Logging
//!
//! Stderr only — stdout is the MCP transport, must stay clean.
mod bootstrap;
mod config;
mod tools;
use std::process::ExitCode;
use anyhow::Result;
use rmcp::{transport::stdio, ServiceExt};
use tracing_subscriber::EnvFilter;
use crate::config::Config;
use crate::tools::WalletService;
#[tokio::main]
async fn main() -> ExitCode {
tracing_subscriber::fmt()
.with_writer(std::io::stderr)
.with_env_filter(EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()))
.init();
match run().await {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
tracing::error!("{e:#}");
ExitCode::FAILURE
}
}
}
async fn run() -> Result<()> {
let cfg = Config::load()?;
tracing::info!(
network = ?cfg.network,
koios = %cfg.koios_base,
account = cfg.account,
index = cfg.index,
data_dir = %cfg.data_dir.display(),
"aldabra starting"
);
// CLI mode flags — all out-of-band, before the MCP transport
// takes over stdio.
//
// `--generate-mnemonic` — print a fresh phrase, exit. No disk write.
// `--bootstrap` — paste an existing phrase, encrypt, derive.
// `--bootstrap-new` — generate, display, encrypt, derive (one shot).
// `--bootstrap-from-xprv` — paste a root_xsk1... bech32 (cnode root.prv,
// cardano-address output), encrypt, derive.
// Power-user import path for keys that came
// from outside the BIP-39 mnemonic flow.
// (none) — load existing, start MCP server.
let args: Vec<String> = std::env::args().collect();
let generate_only = args.iter().any(|a| a == "--generate-mnemonic");
let bootstrap_only = args.iter().any(|a| a == "--bootstrap");
let bootstrap_new = args.iter().any(|a| a == "--bootstrap-new");
let bootstrap_from_xprv = args.iter().any(|a| a == "--bootstrap-from-xprv");
if generate_only {
bootstrap::print_fresh_mnemonic()?;
return Ok(());
}
let mnemonic_path = bootstrap::mnemonic_path(&cfg.data_dir);
let xprv_path = bootstrap::root_xprv_path(&cfg.data_dir);
let any_key_exists = mnemonic_path.exists() || xprv_path.exists();
// L-2 audit fix: scope `root` to a block so its XPrv drops + wipes
// as soon as we've extracted the keys we need.
let (payment_key, stake_key, address) = {
let root = if bootstrap_new {
bootstrap::generate_and_save_root_key(&cfg.data_dir)?
} else if bootstrap_from_xprv {
bootstrap::import_root_xprv(&cfg.data_dir)?
} else if any_key_exists || bootstrap_only {
// Loads existing (or runs the mnemonic-paste flow on
// first-run with --bootstrap). load_or_create_root_key
// itself picks between mnemonic.age and root-xprv.age.
bootstrap::load_or_create_root_key(&cfg.data_dir)?
} else {
anyhow::bail!(
"no key at {}. run one of:\n \
`aldabra --bootstrap` (paste existing 24-word phrase)\n \
`aldabra --bootstrap-new` (generate a fresh wallet)\n \
`aldabra --bootstrap-from-xprv` (paste a root_xsk1... bech32)",
cfg.data_dir.display()
);
};
let address = aldabra_core::derive_base_address(
&root,
cfg.network,
cfg.account,
cfg.index,
)?;
let payment_key =
aldabra_core::derive_payment_key(&root, cfg.account, cfg.index);
let stake_key = aldabra_core::derive_stake_key(&root, cfg.account);
(payment_key, stake_key, address)
// root drops here — XPrv::Drop wipes the 96 bytes
};
tracing::info!(%address, "derived base address");
if bootstrap_only || bootstrap_new {
eprintln!("aldabra: bootstrap complete. address = {address}");
return Ok(());
}
// Hand off to the MCP server. From this point on stdin/stdout
// belong to the JSON-RPC transport — no more eprintln-prompting.
let service = WalletService::new(
cfg.network,
address,
cfg.koios_base,
payment_key,
stake_key,
cfg.max_send_lovelace,
cfg.data_dir.clone(),
);
let server = service
.serve(stdio())
.await
.map_err(|e| anyhow::anyhow!("rmcp serve failed: {e}"))?;
server
.waiting()
.await
.map_err(|e| anyhow::anyhow!("rmcp wait failed: {e}"))?;
Ok(())
}