mcp: rename tool names to underscore-only (Claude Code compat)

Claude Code's MCP client validates tool names against
[a-zA-Z0-9_-]{1,64} and silently drops names containing dots.
aldabra was registering wallet.address etc. with dots; despite the
daemon running fine and rmcp accepting the names, Claude Code's
tools/list cache was empty for aldabra after `/exit + relaunch`.

discovered integration-time 2026-05-04 after first real session
restart with the wallet registered.

renamed:
  wallet.address          → wallet_address
  wallet.network          → wallet_network
  wallet.balance          → wallet_balance
  wallet.utxos            → wallet_utxos
  wallet.send             → wallet_send
  wallet.send.unsigned    → wallet_send_unsigned
  wallet.tx_status        → wallet_tx_status
  wallet.tx_summary       → wallet_tx_summary
  wallet.sign_partial     → wallet_sign_partial   (already underscored)
  wallet.submit_signed_tx → wallet_submit_signed_tx (ditto)
  wallet.policy.create    → wallet_policy_create
  wallet.mint             → wallet_mint           (no change)
  wallet.mint.cip68_nft   → wallet_mint_cip68_nft
  wallet.mint.unsigned    → wallet_mint_unsigned
  wallet.script.spend     → wallet_script_spend
  wallet.stake.address    → wallet_stake_address
  wallet.stake.delegate   → wallet_stake_delegate

instructions blurb + module docstring updated. all 93 unit tests
still pass. fresh tools/list smoke confirmed: 17 tools all
underscore-only.

cobb needs to /exit + relaunch one more time for Claude Code to
re-handshake with the rebuilt binary.
This commit is contained in:
Cobb 2026-05-04 15:59:46 -07:00
parent f17479ab92
commit 36bbd8033f

View file

@ -1,21 +1,24 @@
//! MCP tool handlers.
//!
//! Each `#[tool]` becomes a discoverable MCP tool. Tool names use
//! dotted notation per the MCP convention; the underlying Rust fn
//! names use snake_case.
//! `snake_case` only (no dots) — Claude Code's MCP client validates
//! tool names against `[a-zA-Z0-9_-]{1,64}` and silently drops names
//! with dots. This was an integration-time discovery 2026-05-04 after
//! the first session restart found zero aldabra tools advertised
//! despite the daemon running.
//!
//! ## Phase 1 — read path
//!
//! - `wallet.address` — bech32 base address
//! - `wallet.network` — mainnet | preview | preprod
//! - `wallet.balance` — JSON `{lovelace, assets}`
//! - `wallet.utxos` — JSON list of UTXOs
//! - `wallet_address` — bech32 base address
//! - `wallet_network` — mainnet | preview | preprod
//! - `wallet_balance` — JSON `{lovelace, assets}`
//! - `wallet_utxos` — JSON list of UTXOs
//!
//! ## Phase 2 — send path
//!
//! - `wallet.send` — build + sign + submit ADA payment, with hard
//! - `wallet_send` — build + sign + submit ADA payment, with hard
//! cap guard (`max_send_lovelace`)
//! - `wallet.tx_status` — poll a submitted tx hash
//! - `wallet_tx_status` — poll a submitted tx hash
//!
//! Returns:
//! - `String` results pass through `IntoContents` directly.
@ -94,9 +97,9 @@ impl WalletService {
/// Reject if `lovelace` exceeds the wallet's hard cap unless
/// `force=true`. Used by every tool that moves lovelace to a
/// non-wallet destination — wallet.send, wallet.mint,
/// wallet.mint.cip68_nft, wallet.script.spend.
/// (HIGH-1 audit fix: previously only wallet.send had this guard.)
/// non-wallet destination — wallet_send, wallet_mint,
/// wallet_mint_cip68_nft, wallet_script_spend.
/// (HIGH-1 audit fix: previously only wallet_send had this guard.)
fn enforce_value_cap(&self, lovelace: u64, force: bool) -> Result<(), String> {
if lovelace > self.inner.max_send_lovelace && !force {
return Err(format!(
@ -127,7 +130,7 @@ pub struct SendArgs {
#[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct TxStatusArgs {
/// Hex-encoded transaction hash returned by `wallet.send`.
/// Hex-encoded transaction hash returned by `wallet_send`.
pub tx_hash: String,
}
@ -146,7 +149,7 @@ pub struct UnsignedSendArgs {
pub struct SubmitSignedArgs {
/// Hex-encoded signed transaction CBOR — produced by an external
/// cold-signer that consumed the unsigned CBOR returned by
/// `wallet.send.unsigned`.
/// `wallet_send_unsigned`.
pub signed_cbor_hex: String,
}
@ -168,7 +171,7 @@ pub struct MintUnsignedArgs {
/// PolicySpec as a JSON object with `type`: `single_sig` |
/// `single_sig_timelock` | `nofk`. If omitted, defaults to a
/// single-sig policy bound to this wallet's payment key (same as
/// `wallet.mint`).
/// `wallet_mint`).
#[serde(default)]
pub policy: Option<serde_json::Value>,
/// Optional CIP-25 v2 metadata.
@ -243,7 +246,7 @@ pub struct TxSummaryArgs {
/// Hex-encoded Conway-era tx CBOR — unsigned, partially-signed,
/// or fully signed. Decoded read-only and turned into a
/// human-reviewable JSON summary. **Always run this before
/// `wallet.sign_partial` or `wallet.submit_signed_tx` on a
/// `wallet_sign_partial` or `wallet_submit_signed_tx` on a
/// CBOR you didn't build yourself.**
pub cbor_hex: String,
}
@ -332,7 +335,7 @@ pub struct MintArgs {
#[tool(tool_box)]
impl WalletService {
#[tool(
name = "wallet.address",
name = "wallet_address",
description = "Return the wallet's primary base address (CIP-1852, account 0, index 0) as a bech32 string"
)]
async fn wallet_address(&self) -> String {
@ -340,7 +343,7 @@ impl WalletService {
}
#[tool(
name = "wallet.stake.address",
name = "wallet_stake_address",
description = "Return the wallet's reward (stake) address as bech32 — `stake1...` on mainnet, `stake_test1...` on testnet. This is what gets pointed at a stake pool when delegating."
)]
async fn wallet_stake_address(&self) -> Result<String, String> {
@ -351,7 +354,7 @@ impl WalletService {
}
#[tool(
name = "wallet.network",
name = "wallet_network",
description = "Return the configured Cardano network: mainnet, preview, or preprod"
)]
async fn wallet_network(&self) -> String {
@ -363,7 +366,7 @@ impl WalletService {
}
#[tool(
name = "wallet.balance",
name = "wallet_balance",
description = "Query ADA + native-asset balance at the wallet address. Returns JSON {lovelace, assets}."
)]
async fn wallet_balance(&self) -> Result<String, String> {
@ -377,7 +380,7 @@ impl WalletService {
}
#[tool(
name = "wallet.utxos",
name = "wallet_utxos",
description = "List UTXOs at the wallet address as a JSON array of {tx_hash, output_index, lovelace, assets}."
)]
async fn wallet_utxos(&self) -> Result<String, String> {
@ -391,7 +394,7 @@ impl WalletService {
}
#[tool(
name = "wallet.send",
name = "wallet_send",
description = "Build, sign, and submit a payment (ADA + optional native assets) from this wallet. Args: to_address (bech32), lovelace (u64), assets (optional array of {policy_id_hex, asset_name_hex, quantity}), force (bool, optional). Refuses sends > max_send_lovelace unless force=true. Returns the tx hash on success."
)]
async fn wallet_send(
@ -454,7 +457,7 @@ impl WalletService {
}
#[tool(
name = "wallet.tx_status",
name = "wallet_tx_status",
description = "Poll a submitted transaction's confirmation status. Args: tx_hash (hex). Returns JSON {status: confirmed|not_found, block_height?, epoch?}."
)]
async fn wallet_tx_status(
@ -471,8 +474,8 @@ impl WalletService {
}
#[tool(
name = "wallet.send.unsigned",
description = "Build a payment without signing or submitting. Returns JSON {cbor_hex, summary}: the unsigned tx CBOR for a cold-signer + a human-readable summary (predicted tx_hash, send/fee/change amounts). For high-value flows where the daemon must not auto-sign. After review + offline signing, submit the signed bytes via wallet.submit_signed_tx."
name = "wallet_send_unsigned",
description = "Build a payment without signing or submitting. Returns JSON {cbor_hex, summary}: the unsigned tx CBOR for a cold-signer + a human-readable summary (predicted tx_hash, send/fee/change amounts). For high-value flows where the daemon must not auto-sign. After review + offline signing, submit the signed bytes via wallet_submit_signed_tx."
)]
async fn wallet_send_unsigned(
&self,
@ -523,8 +526,8 @@ impl WalletService {
}
#[tool(
name = "wallet.submit_signed_tx",
description = "Submit a pre-signed transaction. Args: signed_cbor_hex (hex-encoded signed tx CBOR from a cold-signer). Returns the on-chain tx hash on success. Use after wallet.send.unsigned + offline signing."
name = "wallet_submit_signed_tx",
description = "Submit a pre-signed transaction. Args: signed_cbor_hex (hex-encoded signed tx CBOR from a cold-signer). Returns the on-chain tx hash on success. Use after wallet_send_unsigned + offline signing."
)]
async fn wallet_submit_signed_tx(
&self,
@ -539,7 +542,7 @@ impl WalletService {
}
#[tool(
name = "wallet.policy.create",
name = "wallet_policy_create",
description = "Generate a single-sig native policy bound to this wallet's payment key. Args: invalid_after_slot (optional u64 — omit for open-ended, supply for time-locked supply). Returns JSON {policy_id_hex, script_cbor_hex, type}."
)]
async fn wallet_policy_create(
@ -570,7 +573,7 @@ impl WalletService {
}
#[tool(
name = "wallet.mint",
name = "wallet_mint",
description = "Mint or burn a native asset under a wallet-generated single-sig policy, optionally with CIP-25 v2 metadata. Args: dest_address, dest_lovelace (≥ 1.5 ADA for asset-bearing UTXO), asset_name_hex, quantity (positive=mint, negative=burn), invalid_after_slot (optional), metadata (optional CIP-25 JSON object: {name, image, description, mediaType, files, ...}). Returns the tx hash on success."
)]
async fn wallet_mint(
@ -650,7 +653,7 @@ impl WalletService {
}
#[tool(
name = "wallet.mint.cip68_nft",
name = "wallet_mint_cip68_nft",
description = "Mint a CIP-68 NFT pair (label 100 ref + label 222 user) under a wallet-generated single-sig policy. Args: user_address, name_body_hex (raw asset-name body, ≤28 bytes), metadata (JSON object: name, image, description, mediaType, files, ...), user_lovelace (defaults 1.5 ADA), ref_address (defaults wallet — mutable NFT), ref_lovelace (defaults 1.5 ADA), invalid_after_slot? Returns the tx hash."
)]
async fn wallet_mint_cip68_nft(
@ -738,7 +741,7 @@ impl WalletService {
}
#[tool(
name = "wallet.script.spend",
name = "wallet_script_spend",
description = "Spend a Plutus-locked UTXO. Args: locked_tx_hash, locked_output_index, locked_lovelace, plutus_version (v1|v2|v3), script_cbor_hex, redeemer_cbor_hex, witness_datum_hex (optional, omit if datum is inline on the locked utxo), payout_address, payout_lovelace, ex_units (optional {mem,steps} — defaults to a generous budget for trivial validators). Wallet picks its own collateral UTXO (≥ 5 ADA). Returns the tx hash."
)]
async fn wallet_script_spend(
@ -838,7 +841,7 @@ impl WalletService {
}
#[tool(
name = "wallet.stake.delegate",
name = "wallet_stake_delegate",
description = "Delegate this wallet's stake to a Cardano pool. Args: pool_id (bech32 'pool1...'), register_first (bool, defaults true — prepends a 2 ADA stake-registration cert; set false if the stake key is already registered). Signs with both the payment and stake keys, submits, returns the tx hash."
)]
async fn wallet_stake_delegate(
@ -892,8 +895,8 @@ impl WalletService {
}
#[tool(
name = "wallet.mint.unsigned",
description = "Build a mint TX without signing — for cold-sign or multi-sig flows. Args: dest_address, dest_lovelace, asset_name_hex, quantity, policy (optional, defaults to wallet single-sig; pass {type:'nofk',n:2,signer_pkhs_hex:[..]} for multi-sig treasury), metadata (optional CIP-25), disclosed_signer_pkh_hex (optional, defaults to wallet's pkh). Returns JSON {cbor_hex, summary}. Pass through wallet.sign_partial chain, then wallet.submit_signed_tx."
name = "wallet_mint_unsigned",
description = "Build a mint TX without signing — for cold-sign or multi-sig flows. Args: dest_address, dest_lovelace, asset_name_hex, quantity, policy (optional, defaults to wallet single-sig; pass {type:'nofk',n:2,signer_pkhs_hex:[..]} for multi-sig treasury), metadata (optional CIP-25), disclosed_signer_pkh_hex (optional, defaults to wallet's pkh). Returns JSON {cbor_hex, summary}. Pass through wallet_sign_partial chain, then wallet_submit_signed_tx."
)]
async fn wallet_mint_unsigned(
&self,
@ -976,8 +979,8 @@ impl WalletService {
}
#[tool(
name = "wallet.tx_summary",
description = "Decode a Conway-era tx CBOR (unsigned, partial, or signed) into a human-reviewable JSON summary: tx_hash, inputs count, outputs (address+lovelace+assets+inline_datum flag), fee, certificates, mint, witness count, aux-data presence. **Read-only — does not sign or submit.** Run this before `wallet.sign_partial` on any CBOR you didn't build yourself."
name = "wallet_tx_summary",
description = "Decode a Conway-era tx CBOR (unsigned, partial, or signed) into a human-reviewable JSON summary: tx_hash, inputs count, outputs (address+lovelace+assets+inline_datum flag), fee, certificates, mint, witness count, aux-data presence. **Read-only — does not sign or submit.** Run this before `wallet_sign_partial` on any CBOR you didn't build yourself."
)]
async fn wallet_tx_summary(
&self,
@ -989,8 +992,8 @@ impl WalletService {
}
#[tool(
name = "wallet.sign_partial",
description = "Append this wallet's VKeyWitness to a Conway-era tx (unsigned or partially-signed). Args: cbor_hex (hex-encoded tx CBOR). Returns the updated CBOR hex with our signature added. For multi-sig flows (e.g. ADAMaps treasury 2-of-2): each party calls this in turn, then any party submits via wallet.submit_signed_tx."
name = "wallet_sign_partial",
description = "Append this wallet's VKeyWitness to a Conway-era tx (unsigned or partially-signed). Args: cbor_hex (hex-encoded tx CBOR). Returns the updated CBOR hex with our signature added. For multi-sig flows (e.g. ADAMaps treasury 2-of-2): each party calls this in turn, then any party submits via wallet_submit_signed_tx."
)]
async fn wallet_sign_partial(
&self,
@ -1012,7 +1015,7 @@ impl ServerHandler for WalletService {
fn get_info(&self) -> ServerInfo {
ServerInfo {
instructions: Some(
"aldabra — Cardano lite wallet over MCP. Phase 1 (read): wallet.address, wallet.network, wallet.balance, wallet.utxos. Phase 2 (send): wallet.send (with native-asset bundle), wallet.send.unsigned + wallet.submit_signed_tx + wallet.sign_partial (cold-sign + multi-sig), wallet.tx_status. Phase 3 (mint): wallet.policy.create, wallet.mint (with CIP-25 metadata), wallet.mint.cip68_nft (ref + user NFT pair w/ inline datum). Plutus + stake delegation land in Phase 4.".into(),
"aldabra — Cardano lite wallet over MCP. Phase 1 (read): wallet_address, wallet_network, wallet_balance, wallet_utxos. Phase 2 (send): wallet_send (with native-asset bundle), wallet_send_unsigned + wallet_submit_signed_tx + wallet_sign_partial (cold-sign + multi-sig), wallet_tx_status. Phase 3 (mint): wallet_policy_create, wallet_mint (with CIP-25 metadata), wallet_mint_cip68_nft (ref + user NFT pair w/ inline datum). Plutus + stake delegation land in Phase 4.".into(),
),
..Default::default()
}