v0.2: 8 chain_* read-only Koios passthrough MCP tools
Adds a parallel read-only API surface alongside wallet_*: chain_tx_info full Koios tx_info (any hash) chain_address_info balance + utxos at any address chain_pool_list filter by ticker / pool_id_bech32 chain_pool_info detail per pool (delegators, blocks) chain_epoch_params protocol params for an epoch chain_asset_info supply, holders, mint history chain_account_info stake address state chain_tip current chain tip All passthrough — Koios JSON returned verbatim, no re-shaping. Network-aware via existing ALDABRA_KOIOS_BASE; mainnet vs preprod just changes the URL. No keys touched, no signing path. Saves the bash-curl friction Cobb flagged 2026-05-05 mid-mainnet testing arc. Wire-up: KoiosClient gets `post_raw_json` + `get_raw_json` helpers that return raw response strings instead of decoding into typed structures. The chain_* tools are thin wrappers around those. ServerInfo `instructions` updated to advertise the chain_* surface alongside wallet_*.
This commit is contained in:
parent
1ee124b545
commit
ffdafc2028
2 changed files with 247 additions and 1 deletions
|
|
@ -145,6 +145,51 @@ impl KoiosClient {
|
|||
.await
|
||||
.map_err(|e| ChainError::Decode(e.to_string()))
|
||||
}
|
||||
|
||||
/// Generic POST that returns the raw JSON response as a `String` —
|
||||
/// for the `chain_*` MCP passthrough tools where we don't want to
|
||||
/// re-shape Koios's response into typed Rust structures. Caller
|
||||
/// passes a serializable body (often `serde_json::json!({...})`)
|
||||
/// and gets back the response body verbatim.
|
||||
pub async fn post_raw_json<T: Serialize>(
|
||||
&self,
|
||||
path: &str,
|
||||
body: &T,
|
||||
) -> Result<String, ChainError> {
|
||||
self.http
|
||||
.post(self.url(path))
|
||||
.json(body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| ChainError::Network(e.to_string()))?
|
||||
.error_for_status()
|
||||
.map_err(|e| ChainError::Network(e.to_string()))?
|
||||
.text()
|
||||
.await
|
||||
.map_err(|e| ChainError::Decode(e.to_string()))
|
||||
}
|
||||
|
||||
/// Generic GET (with optional query string) that returns the raw
|
||||
/// JSON response as a `String`. Used for Koios endpoints that
|
||||
/// take filters as query params (`pool_list?ticker=eq.AHL`,
|
||||
/// `epoch_params`, `tip`, etc.).
|
||||
pub async fn get_raw_json(
|
||||
&self,
|
||||
path: &str,
|
||||
query: &[(&str, &str)],
|
||||
) -> Result<String, ChainError> {
|
||||
self.http
|
||||
.get(self.url(path))
|
||||
.query(query)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| ChainError::Network(e.to_string()))?
|
||||
.error_for_status()
|
||||
.map_err(|e| ChainError::Network(e.to_string()))?
|
||||
.text()
|
||||
.await
|
||||
.map_err(|e| ChainError::Decode(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_u64(s: &str, field: &str) -> Result<u64, ChainError> {
|
||||
|
|
|
|||
|
|
@ -143,6 +143,56 @@ pub struct TxStatusArgs {
|
|||
pub tx_hash: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, schemars::JsonSchema)]
|
||||
pub struct ChainTxInfoArgs {
|
||||
/// Hex-encoded transaction hash to look up.
|
||||
pub tx_hash: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, schemars::JsonSchema)]
|
||||
pub struct ChainAddressArgs {
|
||||
/// Bech32 address (any network).
|
||||
pub address: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, schemars::JsonSchema)]
|
||||
pub struct ChainPoolListArgs {
|
||||
/// Optional exact-match ticker filter (e.g. "AHL", "BLOOM", "1PCT").
|
||||
#[serde(default)]
|
||||
pub ticker: Option<String>,
|
||||
/// Optional exact-match bech32 pool id filter.
|
||||
#[serde(default)]
|
||||
pub pool_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, schemars::JsonSchema)]
|
||||
pub struct ChainPoolInfoArgs {
|
||||
/// One or more bech32 pool ids to fetch detail for.
|
||||
pub pool_ids: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, schemars::JsonSchema)]
|
||||
pub struct ChainEpochArgs {
|
||||
/// Specific epoch number; omit for the latest active epoch.
|
||||
#[serde(default)]
|
||||
pub epoch_no: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, schemars::JsonSchema)]
|
||||
pub struct ChainAssetArgs {
|
||||
/// 56-hex-char policy id.
|
||||
pub policy_id_hex: String,
|
||||
/// Hex of raw asset name bytes (0–64 chars). Empty string for
|
||||
/// policy-only / no-asset-name native assets.
|
||||
pub asset_name_hex: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, schemars::JsonSchema)]
|
||||
pub struct ChainAccountArgs {
|
||||
/// Bech32 stake address (`stake1...` mainnet or `stake_test1...`).
|
||||
pub stake_address: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, schemars::JsonSchema)]
|
||||
pub struct UnsignedSendArgs {
|
||||
/// Recipient bech32 address.
|
||||
|
|
@ -1062,6 +1112,157 @@ impl WalletService {
|
|||
}
|
||||
Ok(hex)
|
||||
}
|
||||
|
||||
// ===========================================================
|
||||
// chain_* — read-only Koios passthroughs.
|
||||
//
|
||||
// No signing, no key access. Same network as the configured
|
||||
// wallet (mainnet/preprod/preview routed via ALDABRA_KOIOS_BASE).
|
||||
// Returns Koios JSON verbatim — caller parses what they need.
|
||||
//
|
||||
// Saves the bash-curl friction when looking at addresses/txs/
|
||||
// pools/etc that aren't this wallet specifically.
|
||||
// ===========================================================
|
||||
|
||||
#[tool(
|
||||
name = "chain_tx_info",
|
||||
description = "Full Koios `tx_info` for a transaction hash — inputs, outputs, certs, mint, metadata, plutus_contracts, withdrawals, etc. Heavy response (multi-MB possible on complex txs). Args: tx_hash (hex). Returns the raw Koios JSON."
|
||||
)]
|
||||
async fn chain_tx_info(
|
||||
&self,
|
||||
#[tool(aggr)] ChainTxInfoArgs { tx_hash }: ChainTxInfoArgs,
|
||||
) -> Result<String, String> {
|
||||
let body = serde_json::json!({"_tx_hashes": [tx_hash]});
|
||||
self.inner
|
||||
.chain
|
||||
.post_raw_json("tx_info", &body)
|
||||
.await
|
||||
.map_err(|e| format!("koios: {e}"))
|
||||
}
|
||||
|
||||
#[tool(
|
||||
name = "chain_address_info",
|
||||
description = "Full Koios `address_info` for any address — balance + utxo set + stake address. Use this to look up addresses other than this wallet (this wallet has the dedicated wallet_balance / wallet_utxos)."
|
||||
)]
|
||||
async fn chain_address_info(
|
||||
&self,
|
||||
#[tool(aggr)] ChainAddressArgs { address }: ChainAddressArgs,
|
||||
) -> Result<String, String> {
|
||||
let body = serde_json::json!({"_addresses": [address]});
|
||||
self.inner
|
||||
.chain
|
||||
.post_raw_json("address_info", &body)
|
||||
.await
|
||||
.map_err(|e| format!("koios: {e}"))
|
||||
}
|
||||
|
||||
#[tool(
|
||||
name = "chain_pool_list",
|
||||
description = "Filter the Koios pool list. Args: ticker (optional, exact match — e.g. \"AHL\"), pool_id (optional bech32). Returns array of matching pools with margin/pledge/relay info. Empty filter set returns all active pools (large)."
|
||||
)]
|
||||
async fn chain_pool_list(
|
||||
&self,
|
||||
#[tool(aggr)] ChainPoolListArgs { ticker, pool_id }: ChainPoolListArgs,
|
||||
) -> Result<String, String> {
|
||||
let mut q: Vec<(String, String)> = Vec::new();
|
||||
if let Some(t) = ticker {
|
||||
q.push(("ticker".into(), format!("eq.{t}")));
|
||||
}
|
||||
if let Some(p) = pool_id {
|
||||
q.push(("pool_id_bech32".into(), format!("eq.{p}")));
|
||||
}
|
||||
let q_refs: Vec<(&str, &str)> = q.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
|
||||
self.inner
|
||||
.chain
|
||||
.get_raw_json("pool_list", &q_refs)
|
||||
.await
|
||||
.map_err(|e| format!("koios: {e}"))
|
||||
}
|
||||
|
||||
#[tool(
|
||||
name = "chain_pool_info",
|
||||
description = "Detailed Koios `pool_info` for one or more pools. Args: pool_ids (array of bech32 pool ids). Returns delegators, live stake, recent blocks, metadata."
|
||||
)]
|
||||
async fn chain_pool_info(
|
||||
&self,
|
||||
#[tool(aggr)] ChainPoolInfoArgs { pool_ids }: ChainPoolInfoArgs,
|
||||
) -> Result<String, String> {
|
||||
let body = serde_json::json!({"_pool_bech32_ids": pool_ids});
|
||||
self.inner
|
||||
.chain
|
||||
.post_raw_json("pool_info", &body)
|
||||
.await
|
||||
.map_err(|e| format!("koios: {e}"))
|
||||
}
|
||||
|
||||
#[tool(
|
||||
name = "chain_epoch_params",
|
||||
description = "Koios `epoch_params` for the current (or specified) epoch — protocol params: cost models, fees a/b, deposits, ex_units prices, max tx size, etc. Args: epoch_no (optional u64; latest if omitted)."
|
||||
)]
|
||||
async fn chain_epoch_params(
|
||||
&self,
|
||||
#[tool(aggr)] ChainEpochArgs { epoch_no }: ChainEpochArgs,
|
||||
) -> Result<String, String> {
|
||||
let mut q: Vec<(String, String)> = Vec::new();
|
||||
if let Some(e) = epoch_no {
|
||||
q.push(("_epoch_no".into(), e.to_string()));
|
||||
}
|
||||
let q_refs: Vec<(&str, &str)> = q.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect();
|
||||
self.inner
|
||||
.chain
|
||||
.get_raw_json("epoch_params", &q_refs)
|
||||
.await
|
||||
.map_err(|e| format!("koios: {e}"))
|
||||
}
|
||||
|
||||
#[tool(
|
||||
name = "chain_asset_info",
|
||||
description = "Koios `asset_info` for a native asset. Args: policy_id_hex (56 hex chars), asset_name_hex (hex of raw asset-name bytes). Returns supply, holders, mint history, metadata."
|
||||
)]
|
||||
async fn chain_asset_info(
|
||||
&self,
|
||||
#[tool(aggr)] ChainAssetArgs {
|
||||
policy_id_hex,
|
||||
asset_name_hex,
|
||||
}: ChainAssetArgs,
|
||||
) -> Result<String, String> {
|
||||
let body = serde_json::json!({
|
||||
"_asset_list": [[policy_id_hex, asset_name_hex]]
|
||||
});
|
||||
self.inner
|
||||
.chain
|
||||
.post_raw_json("asset_info", &body)
|
||||
.await
|
||||
.map_err(|e| format!("koios: {e}"))
|
||||
}
|
||||
|
||||
#[tool(
|
||||
name = "chain_account_info",
|
||||
description = "Koios `account_info` for a stake address. Args: stake_address (bech32 stake1...). Returns registered/active state, delegated_pool, rewards balance, total stake."
|
||||
)]
|
||||
async fn chain_account_info(
|
||||
&self,
|
||||
#[tool(aggr)] ChainAccountArgs { stake_address }: ChainAccountArgs,
|
||||
) -> Result<String, String> {
|
||||
let body = serde_json::json!({"_stake_addresses": [stake_address]});
|
||||
self.inner
|
||||
.chain
|
||||
.post_raw_json("account_info", &body)
|
||||
.await
|
||||
.map_err(|e| format!("koios: {e}"))
|
||||
}
|
||||
|
||||
#[tool(
|
||||
name = "chain_tip",
|
||||
description = "Koios `tip` — current chain tip: block height, slot, epoch, hash, timestamp. Useful for absolute-time anchoring + stale-data detection."
|
||||
)]
|
||||
async fn chain_tip(&self) -> Result<String, String> {
|
||||
self.inner
|
||||
.chain
|
||||
.get_raw_json("tip", &[])
|
||||
.await
|
||||
.map_err(|e| format!("koios: {e}"))
|
||||
}
|
||||
}
|
||||
|
||||
#[tool(tool_box)]
|
||||
|
|
@ -1069,7 +1270,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. 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(),
|
||||
),
|
||||
..Default::default()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue