From 36bbd8033f7befecd7ee1fda20ae221ba86e635a Mon Sep 17 00:00:00 2001 From: Cobb Date: Mon, 4 May 2026 15:59:46 -0700 Subject: [PATCH] mcp: rename tool names to underscore-only (Claude Code compat) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- crates/aldabra-mcp/src/tools.rs | 79 +++++++++++++++++---------------- 1 file changed, 41 insertions(+), 38 deletions(-) diff --git a/crates/aldabra-mcp/src/tools.rs b/crates/aldabra-mcp/src/tools.rs index 26955a8..6351d00 100644 --- a/crates/aldabra-mcp/src/tools.rs +++ b/crates/aldabra-mcp/src/tools.rs @@ -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, /// 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 { @@ -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 { @@ -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 { @@ -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() }