mcp-banking 1.0.2

Enterprise Banking MCP Server — unified access to Plaid, Mono (Africa), and Open Banking UK/EU for accounts, transactions, balances, and payments
Documentation
//! Mono (Africa) API backend — Nigeria, Kenya, Ghana, South Africa.
use crate::types::*;
use anyhow::Result;
use reqwest::Client;

const BASE: &str = "https://api.withmono.com/v2";

#[derive(Clone)]
pub struct MonoBackend { http: Client, secret_key: String, account_id: String }

impl MonoBackend {
    pub fn new(secret_key: String, account_id: String) -> Self {
        Self { http: Client::new(), secret_key, account_id }
    }
    async fn get(&self, path: &str) -> Result<serde_json::Value> {
        Ok(self.http.get(format!("{BASE}/{path}")).header("mono-sec-key", &self.secret_key).send().await?.error_for_status()?.json().await?)
    }
    async fn post(&self, path: &str, body: &serde_json::Value) -> Result<serde_json::Value> {
        Ok(self.http.post(format!("{BASE}/{path}")).header("mono-sec-key", &self.secret_key).json(body).send().await?.error_for_status()?.json().await?)
    }
}

#[async_trait::async_trait]
impl BankingBackend for MonoBackend {
    fn name(&self) -> &str { "mono" }

    async fn list_accounts(&self) -> Result<Vec<BankAccount>> {
        let acc = self.get_account(&self.account_id).await?;
        Ok(vec![acc])
    }

    async fn get_account(&self, id: &str) -> Result<BankAccount> {
        let resp = self.get(&format!("accounts/{id}")).await?;
        let d = &resp["data"];
        Ok(BankAccount { id: id.into(), name: d["name"].as_str().unwrap_or("").into(), account_type: d["type"].as_str().map(Into::into), subtype: None, mask: d["account_number"].as_str().map(|s| s[s.len().saturating_sub(4)..].to_string()), currency: d["currency"].as_str().map(Into::into), balance_available: None, balance_current: d["balance"].as_f64().map(|b| b / 100.0), institution_id: None, institution_name: d["institution"]["name"].as_str().map(Into::into), backend: "mono".into() })
    }

    async fn get_balances(&self) -> Result<Vec<Balance>> {
        let resp = self.get(&format!("accounts/{}/balance", self.account_id)).await?;
        let d = &resp["data"];
        Ok(vec![Balance { account_id: self.account_id.clone(), account_name: None, available: d["available_balance"].as_f64().map(|b| b / 100.0), current: d["balance"].as_f64().map(|b| b / 100.0).unwrap_or(0.0), currency: d["currency"].as_str().map(Into::into), backend: "mono".into() }])
    }

    async fn list_transactions(&self, _account_id: &str, from: &str, to: &str, limit: u32) -> Result<Vec<Transaction>> {
        let resp = self.get(&format!("accounts/{}/transactions?start={}&end={}&limit={}", self.account_id, from, to, limit)).await?;
        Ok(resp["data"].as_array().map(|a| a.iter().map(|t| Transaction { id: t["_id"].as_str().unwrap_or("").into(), account_id: self.account_id.clone(), amount: t["amount"].as_f64().map(|a| a / 100.0).unwrap_or(0.0), currency: None, date: t["date"].as_str().unwrap_or("").into(), description: t["narration"].as_str().unwrap_or("").into(), category: t["category"].as_str().map(Into::into), merchant_name: None, pending: false, transaction_type: t["type"].as_str().map(Into::into), backend: "mono".into() }).collect()).unwrap_or_default())
    }

    async fn get_transaction(&self, id: &str) -> Result<Transaction> {
        let resp = self.get(&format!("accounts/{}/transactions/{}", self.account_id, id)).await?;
        let t = &resp["data"];
        Ok(Transaction { id: t["_id"].as_str().unwrap_or("").into(), account_id: self.account_id.clone(), amount: t["amount"].as_f64().map(|a| a / 100.0).unwrap_or(0.0), currency: None, date: t["date"].as_str().unwrap_or("").into(), description: t["narration"].as_str().unwrap_or("").into(), category: t["category"].as_str().map(Into::into), merchant_name: None, pending: false, transaction_type: t["type"].as_str().map(Into::into), backend: "mono".into() })
    }

    async fn search_transactions(&self, query: &str, limit: u32) -> Result<Vec<Transaction>> {
        let txns = self.list_transactions(&self.account_id, "2020-01-01", "2030-12-31", limit).await?;
        let q = query.to_lowercase();
        Ok(txns.into_iter().filter(|t| t.description.to_lowercase().contains(&q)).collect())
    }

    async fn categorize_transaction(&self, id: &str) -> Result<Transaction> { self.get_transaction(id).await }

    async fn get_identity(&self) -> Result<Identity> {
        let resp = self.get(&format!("accounts/{}/identity", self.account_id)).await?;
        let d = &resp["data"];
        Ok(Identity { name: d["full_name"].as_str().map(Into::into), email: d["email"].as_str().map(Into::into), phone: d["phone"].as_str().map(Into::into), address: d["address_line1"].as_str().map(Into::into), backend: "mono".into() })
    }

    async fn list_institutions(&self, country: Option<&str>, _limit: u32) -> Result<Vec<Institution>> {
        let path = match country {
            Some(c) => format!("coverage?country={c}"),
            None => "coverage".into(),
        };
        let resp = self.get(&path).await?;
        Ok(resp["data"].as_array().map(|a| a.iter().map(|i| Institution { id: i["_id"].as_str().unwrap_or("").into(), name: i["name"].as_str().unwrap_or("").into(), country: i["country"].as_str().map(Into::into), url: None, logo: i["icon"].as_str().map(Into::into), backend: "mono".into() }).collect()).unwrap_or_default())
    }

    async fn get_institution(&self, id: &str) -> Result<Institution> {
        let insts = self.list_institutions(None, 100).await?;
        insts.into_iter().find(|i| i.id == id).ok_or_else(|| anyhow::anyhow!("institution not found"))
    }

    async fn initiate_payment(&self, _account_id: &str, amount: f64, _currency: &str, recipient: &str, reference: &str) -> Result<Payment> {
        let resp = self.post("payments/initiate", &serde_json::json!({"amount": (amount * 100.0) as i64, "type": "onetime", "description": reference, "account": recipient})).await?;
        Ok(Payment { id: resp["data"]["id"].as_str().unwrap_or("").into(), status: resp["data"]["status"].as_str().unwrap_or("pending").into(), amount, currency: None, recipient: Some(recipient.into()), reference: Some(reference.into()), created_at: None, backend: "mono".into() })
    }

    async fn get_payment_status(&self, id: &str) -> Result<Payment> {
        let resp = self.get(&format!("payments/{id}")).await?;
        let d = &resp["data"];
        Ok(Payment { id: d["id"].as_str().unwrap_or("").into(), status: d["status"].as_str().unwrap_or("").into(), amount: d["amount"].as_f64().map(|a| a / 100.0).unwrap_or(0.0), currency: None, recipient: d["account"].as_str().map(Into::into), reference: d["description"].as_str().map(Into::into), created_at: d["created_at"].as_str().map(Into::into), backend: "mono".into() })
    }

    async fn list_payments(&self, _limit: u32) -> Result<Vec<Payment>> {
        let resp = self.get("payments").await?;
        Ok(resp["data"].as_array().map(|a| a.iter().map(|p| Payment { id: p["id"].as_str().unwrap_or("").into(), status: p["status"].as_str().unwrap_or("").into(), amount: p["amount"].as_f64().map(|a| a / 100.0).unwrap_or(0.0), currency: None, recipient: p["account"].as_str().map(Into::into), reference: p["description"].as_str().map(Into::into), created_at: p["created_at"].as_str().map(Into::into), backend: "mono".into() }).collect()).unwrap_or_default())
    }

    async fn get_statement(&self, _account_id: &str, from: &str, to: &str) -> Result<Statement> {
        let resp = self.post(&format!("accounts/{}/statement", self.account_id), &serde_json::json!({"period": format!("{from}_{to}")})).await?;
        let d = &resp["data"];
        Ok(Statement { account_id: self.account_id.clone(), period_start: from.into(), period_end: to.into(), opening_balance: d["opening_balance"].as_f64().map(|b| b / 100.0), closing_balance: d["closing_balance"].as_f64().map(|b| b / 100.0), transaction_count: d["transactions"].as_array().map(|a| a.len() as u32).unwrap_or(0), backend: "mono".into() })
    }

    async fn sync_transactions(&self) -> Result<String> {
        self.post(&format!("accounts/{}/sync", self.account_id), &serde_json::json!({})).await?;
        Ok("Sync triggered".into())
    }
}