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.

sulkta needs to /exit + relaunch one more time for Claude Code to
re-handshake with the rebuilt binary.
This commit is contained in:
Sulkta 2026-05-04 15:59:46 -07:00
parent d1cc77969f
commit 46be45cdf2

View file

@ -1,21 +1,24 @@
//! MCP tool handlers. //! MCP tool handlers.
//! //!
//! Each `#[tool]` becomes a discoverable MCP tool. Tool names use //! Each `#[tool]` becomes a discoverable MCP tool. Tool names use
//! dotted notation per the MCP convention; the underlying Rust fn //! `snake_case` only (no dots) — Claude Code's MCP client validates
//! names use snake_case. //! 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 //! ## Phase 1 — read path
//! //!
//! - `wallet.address` — bech32 base address //! - `wallet_address` — bech32 base address
//! - `wallet.network` — mainnet | preview | preprod //! - `wallet_network` — mainnet | preview | preprod
//! - `wallet.balance` — JSON `{lovelace, assets}` //! - `wallet_balance` — JSON `{lovelace, assets}`
//! - `wallet.utxos` — JSON list of UTXOs //! - `wallet_utxos` — JSON list of UTXOs
//! //!
//! ## Phase 2 — send path //! ## 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`) //! cap guard (`max_send_lovelace`)
//! - `wallet.tx_status` — poll a submitted tx hash //! - `wallet_tx_status` — poll a submitted tx hash
//! //!
//! Returns: //! Returns:
//! - `String` results pass through `IntoContents` directly. //! - `String` results pass through `IntoContents` directly.
@ -94,9 +97,9 @@ impl WalletService {
/// Reject if `lovelace` exceeds the wallet's hard cap unless /// Reject if `lovelace` exceeds the wallet's hard cap unless
/// `force=true`. Used by every tool that moves lovelace to a /// `force=true`. Used by every tool that moves lovelace to a
/// non-wallet destination — wallet.send, wallet.mint, /// non-wallet destination — wallet_send, wallet_mint,
/// wallet.mint.cip68_nft, wallet.script.spend. /// wallet_mint_cip68_nft, wallet_script_spend.
/// (HIGH-1 audit fix: previously only wallet.send had this guard.) /// (HIGH-1 audit fix: previously only wallet_send had this guard.)
fn enforce_value_cap(&self, lovelace: u64, force: bool) -> Result<(), String> { fn enforce_value_cap(&self, lovelace: u64, force: bool) -> Result<(), String> {
if lovelace > self.inner.max_send_lovelace && !force { if lovelace > self.inner.max_send_lovelace && !force {
return Err(format!( return Err(format!(
@ -127,7 +130,7 @@ pub struct SendArgs {
#[derive(Debug, Deserialize, schemars::JsonSchema)] #[derive(Debug, Deserialize, schemars::JsonSchema)]
pub struct TxStatusArgs { pub struct TxStatusArgs {
/// Hex-encoded transaction hash returned by `wallet.send`. /// Hex-encoded transaction hash returned by `wallet_send`.
pub tx_hash: String, pub tx_hash: String,
} }
@ -146,7 +149,7 @@ pub struct UnsignedSendArgs {
pub struct SubmitSignedArgs { pub struct SubmitSignedArgs {
/// Hex-encoded signed transaction CBOR — produced by an external /// Hex-encoded signed transaction CBOR — produced by an external
/// cold-signer that consumed the unsigned CBOR returned by /// cold-signer that consumed the unsigned CBOR returned by
/// `wallet.send.unsigned`. /// `wallet_send_unsigned`.
pub signed_cbor_hex: String, pub signed_cbor_hex: String,
} }
@ -168,7 +171,7 @@ pub struct MintUnsignedArgs {
/// PolicySpec as a JSON object with `type`: `single_sig` | /// PolicySpec as a JSON object with `type`: `single_sig` |
/// `single_sig_timelock` | `nofk`. If omitted, defaults to a /// `single_sig_timelock` | `nofk`. If omitted, defaults to a
/// single-sig policy bound to this wallet's payment key (same as /// single-sig policy bound to this wallet's payment key (same as
/// `wallet.mint`). /// `wallet_mint`).
#[serde(default)] #[serde(default)]
pub policy: Option<serde_json::Value>, pub policy: Option<serde_json::Value>,
/// Optional CIP-25 v2 metadata. /// Optional CIP-25 v2 metadata.
@ -243,7 +246,7 @@ pub struct TxSummaryArgs {
/// Hex-encoded Conway-era tx CBOR — unsigned, partially-signed, /// Hex-encoded Conway-era tx CBOR — unsigned, partially-signed,
/// or fully signed. Decoded read-only and turned into a /// or fully signed. Decoded read-only and turned into a
/// human-reviewable JSON summary. **Always run this before /// 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.** /// CBOR you didn't build yourself.**
pub cbor_hex: String, pub cbor_hex: String,
} }
@ -332,7 +335,7 @@ pub struct MintArgs {
#[tool(tool_box)] #[tool(tool_box)]
impl WalletService { impl WalletService {
#[tool( #[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" description = "Return the wallet's primary base address (CIP-1852, account 0, index 0) as a bech32 string"
)] )]
async fn wallet_address(&self) -> String { async fn wallet_address(&self) -> String {
@ -340,7 +343,7 @@ impl WalletService {
} }
#[tool( #[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." 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> { async fn wallet_stake_address(&self) -> Result<String, String> {
@ -351,7 +354,7 @@ impl WalletService {
} }
#[tool( #[tool(
name = "wallet.network", name = "wallet_network",
description = "Return the configured Cardano network: mainnet, preview, or preprod" description = "Return the configured Cardano network: mainnet, preview, or preprod"
)] )]
async fn wallet_network(&self) -> String { async fn wallet_network(&self) -> String {
@ -363,7 +366,7 @@ impl WalletService {
} }
#[tool( #[tool(
name = "wallet.balance", name = "wallet_balance",
description = "Query ADA + native-asset balance at the wallet address. Returns JSON {lovelace, assets}." description = "Query ADA + native-asset balance at the wallet address. Returns JSON {lovelace, assets}."
)] )]
async fn wallet_balance(&self) -> Result<String, String> { async fn wallet_balance(&self) -> Result<String, String> {
@ -377,7 +380,7 @@ impl WalletService {
} }
#[tool( #[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}." 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> { async fn wallet_utxos(&self) -> Result<String, String> {
@ -391,7 +394,7 @@ impl WalletService {
} }
#[tool( #[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." 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( async fn wallet_send(
@ -454,7 +457,7 @@ impl WalletService {
} }
#[tool( #[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?}." 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( async fn wallet_tx_status(
@ -471,8 +474,8 @@ impl WalletService {
} }
#[tool( #[tool(
name = "wallet.send.unsigned", 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." 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( async fn wallet_send_unsigned(
&self, &self,
@ -523,8 +526,8 @@ impl WalletService {
} }
#[tool( #[tool(
name = "wallet.submit_signed_tx", 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." 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( async fn wallet_submit_signed_tx(
&self, &self,
@ -539,7 +542,7 @@ impl WalletService {
} }
#[tool( #[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}." 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( async fn wallet_policy_create(
@ -570,7 +573,7 @@ impl WalletService {
} }
#[tool( #[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." 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( async fn wallet_mint(
@ -650,7 +653,7 @@ impl WalletService {
} }
#[tool( #[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." 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( async fn wallet_mint_cip68_nft(
@ -738,7 +741,7 @@ impl WalletService {
} }
#[tool( #[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." 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( async fn wallet_script_spend(
@ -838,7 +841,7 @@ impl WalletService {
} }
#[tool( #[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." 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( async fn wallet_stake_delegate(
@ -892,8 +895,8 @@ impl WalletService {
} }
#[tool( #[tool(
name = "wallet.mint.unsigned", 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." 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( async fn wallet_mint_unsigned(
&self, &self,
@ -976,8 +979,8 @@ impl WalletService {
} }
#[tool( #[tool(
name = "wallet.tx_summary", 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." 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( async fn wallet_tx_summary(
&self, &self,
@ -989,8 +992,8 @@ impl WalletService {
} }
#[tool( #[tool(
name = "wallet.sign_partial", 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. a 2-of-2 treasury): each party calls this in turn, then any party submits via wallet.submit_signed_tx." 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. a 2-of-2 treasury): each party calls this in turn, then any party submits via wallet_submit_signed_tx."
)] )]
async fn wallet_sign_partial( async fn wallet_sign_partial(
&self, &self,
@ -1012,7 +1015,7 @@ impl ServerHandler for WalletService {
fn get_info(&self) -> ServerInfo { fn get_info(&self) -> ServerInfo {
ServerInfo { ServerInfo {
instructions: Some( 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() ..Default::default()
} }