From 66829c9aea3cb1a0d6f7c16cfbb35b7488bad540 Mon Sep 17 00:00:00 2001 From: Kayos Date: Tue, 5 May 2026 09:41:19 -0700 Subject: [PATCH] mcp: declare tools capability in ServerInfo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit rmcp 0.1.5's #[tool(tool_box)] macro doesn't backfill ServerInfo::capabilities. Without an explicit ToolsCapability, clients read "capabilities":{} from initialize and skip tools/list entirely — the server looks connected (instructions field lands) but the tool surface is empty. Claude Code's MCP log: "hasTools":false,"hasPrompts":false,"hasResources":false Fix: capabilities = ServerCapabilities::builder().enable_tools().build() in get_info(). Adds a regression test on the wire shape. --- crates/aldabra-mcp/src/tools.rs | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/crates/aldabra-mcp/src/tools.rs b/crates/aldabra-mcp/src/tools.rs index d4c00db..6b16039 100644 --- a/crates/aldabra-mcp/src/tools.rs +++ b/crates/aldabra-mcp/src/tools.rs @@ -36,7 +36,10 @@ use aldabra_core::{ InputUtxo, Network, PaymentKey, PlutusExUnits, PlutusInput, PlutusVersion, PolicySpec, ProtocolParams, StakeKey, DEFAULT_EX_UNITS, }; -use rmcp::{model::ServerInfo, schemars, tool, ServerHandler}; +use rmcp::{ + model::{ServerCapabilities, ServerInfo}, + schemars, tool, ServerHandler, +}; use serde::Deserialize; /// MCP-facing asset spec — separate from `aldabra_core::AssetSpec` @@ -1268,7 +1271,13 @@ impl WalletService { #[tool(tool_box)] impl ServerHandler for WalletService { fn get_info(&self) -> ServerInfo { + // Declare `tools` capability explicitly. rmcp 0.1.5's `#[tool(tool_box)]` macro + // does NOT backfill ServerInfo::capabilities; without this line Claude Code (and + // any spec-compliant MCP client) reads `"capabilities": {}` from the initialize + // response and skips `tools/list` entirely. The instructions field still lands, + // so the failure mode is silent — server appears connected, no tool surface. ServerInfo { + capabilities: ServerCapabilities::builder().enable_tools().build(), instructions: Some( "aldabra — Cardano lite wallet over MCP. wallet_*: read (address/balance/utxos/network/stake_address), send (wallet_send with optional inline datum for script locks, wallet_send_unsigned + wallet_sign_partial + wallet_submit_signed_tx for cold/multi-sig, wallet_tx_status), mint (wallet_policy_create, wallet_mint with CIP-25 metadata, wallet_mint_cip68_nft for ref+user NFT pairs, wallet_mint_unsigned), Plutus (wallet_script_spend), stake (wallet_stake_delegate). chain_*: read-only Koios passthroughs (chain_tx_info, chain_address_info, chain_pool_list, chain_pool_info, chain_epoch_params, chain_asset_info, chain_account_info, chain_tip) — for inspecting the chain at addresses/txs/pools beyond this wallet.".into(), ), @@ -1276,3 +1285,25 @@ impl ServerHandler for WalletService { } } } + +#[cfg(test)] +mod server_info_tests { + use super::*; + + #[test] + fn capabilities_builder_declares_tools() { + // Regression for the silent "hasTools:false" bug. Claude Code reads + // `serverCapabilities.tools` from initialize and skips `tools/list` + // entirely if it's missing — leaving the server connected but with + // zero callable tools. If anyone refactors `get_info()` and drops + // the `enable_tools()` call, this test catches it. + let cap = ServerCapabilities::builder().enable_tools().build(); + assert!(cap.tools.is_some(), "tools capability must be declared"); + + let wire = serde_json::to_value(&cap).expect("ServerCapabilities serializes"); + assert!( + wire.get("tools").is_some(), + "wire-format capabilities must include 'tools' key, got: {wire}" + ); + } +}