Skip to main content

agentis_pay/mcp/
handler.rs

1use std::sync::Arc;
2
3use rmcp::handler::server::ServerHandler;
4use rmcp::handler::server::tool::ToolRouter;
5use rmcp::handler::server::wrapper::Parameters;
6use rmcp::model::{
7    CallToolResult, Content, Implementation, InitializeRequestParams, InitializeResult,
8    ServerCapabilities,
9};
10use rmcp::service::RequestContext;
11use rmcp::{ErrorData as McpError, RoleServer, tool, tool_handler, tool_router};
12use serde_json::json;
13use tokio::sync::Mutex;
14
15use crate::CommandName;
16use crate::display::OutputEnvelope;
17use crate::mcp::backend::{
18    BrcodeDecodeInput, HistoryInput, HistoryQuery, HistoryRecord, PayInput, PixBackend,
19};
20
21#[derive(Clone)]
22pub struct AgentisPayServer {
23    backend: Arc<Mutex<Box<dyn PixBackend>>>,
24    tool_router: ToolRouter<Self>,
25}
26
27fn tool_result(command: CommandName, data: serde_json::Value) -> CallToolResult {
28    let envelope = OutputEnvelope { command, data };
29    let text =
30        serde_json::to_string(&envelope).unwrap_or_else(|e| format!("{{\"error\":\"{e}\"}}"));
31    CallToolResult::success(vec![Content::text(text)])
32}
33
34fn backend_error(error: anyhow::Error) -> McpError {
35    McpError::internal_error(error.to_string(), None)
36}
37
38#[tool_router]
39impl AgentisPayServer {
40    pub fn new(backend: Box<dyn PixBackend>) -> Self {
41        Self {
42            backend: Arc::new(Mutex::new(backend)),
43            tool_router: Self::tool_router(),
44        }
45    }
46
47    #[tool(
48        name = "agentispay_whoami",
49        description = "Show current session status."
50    )]
51    async fn whoami(&self) -> Result<CallToolResult, McpError> {
52        let mut backend = self.backend.lock().await;
53        let data = backend.whoami().await.map_err(backend_error)?;
54        Ok(tool_result(CommandName::Whoami, json!(data)))
55    }
56
57    #[tool(
58        name = "agentispay_account",
59        description = "Read account profile and metadata."
60    )]
61    async fn account(&self) -> Result<CallToolResult, McpError> {
62        let mut backend = self.backend.lock().await;
63        let data = backend.account().await.map_err(backend_error)?;
64        Ok(tool_result(CommandName::Account, json!(data)))
65    }
66
67    #[tool(
68        name = "agentispay_balance",
69        description = "Read available balance in cents."
70    )]
71    async fn balance(&self) -> Result<CallToolResult, McpError> {
72        let mut backend = self.backend.lock().await;
73        let data = backend.balance().await.map_err(backend_error)?;
74        Ok(tool_result(CommandName::Balance, json!(data)))
75    }
76
77    #[tool(
78        name = "agentispay_history",
79        description = "Read transaction history, optionally by id, with optional list limit."
80    )]
81    async fn history(&self, params: Parameters<HistoryInput>) -> Result<CallToolResult, McpError> {
82        let mut backend = self.backend.lock().await;
83        let query = if let Some(id) = params.0.id {
84            HistoryQuery::Detail(id)
85        } else {
86            let limit = params.0.limit.unwrap_or(20);
87            if limit == 0 {
88                return Err(McpError::invalid_params(
89                    "history limit must be greater than zero",
90                    None,
91                ));
92            }
93            if limit > 50 {
94                return Err(McpError::invalid_params(
95                    "history limit cannot exceed 50",
96                    None,
97                ));
98            }
99            HistoryQuery::List { limit }
100        };
101        let payload = match backend.history(query).await.map_err(backend_error)? {
102            HistoryRecord::List(list) => json!(list),
103            HistoryRecord::Detail(detail) => json!(detail),
104        };
105        Ok(tool_result(CommandName::History, payload))
106    }
107
108    #[tool(
109        name = "agentispay_brcode_decode",
110        description = "Decode a PIX BR Code payload."
111    )]
112    async fn brcode_decode(
113        &self,
114        params: Parameters<BrcodeDecodeInput>,
115    ) -> Result<CallToolResult, McpError> {
116        let data =
117            crate::commands::brcode::decode_payload(&params.0.code).map_err(backend_error)?;
118        Ok(tool_result(CommandName::BrcodeDecode, json!(data)))
119    }
120
121    #[tool(name = "agentispay_deposit", description = "List deposit PIX keys.")]
122    async fn deposit(&self) -> Result<CallToolResult, McpError> {
123        let mut backend = self.backend.lock().await;
124        let data = backend.deposit().await.map_err(backend_error)?;
125        Ok(tool_result(CommandName::Deposit, json!(data)))
126    }
127
128    #[tool(
129        name = "agentispay_pix_keys",
130        description = "List configured PIX keys (CLI parity alias)."
131    )]
132    async fn pix_keys(&self) -> Result<CallToolResult, McpError> {
133        let mut backend = self.backend.lock().await;
134        let data = backend.pix_list().await.map_err(backend_error)?;
135        Ok(tool_result(CommandName::PixList, json!(data)))
136    }
137
138    #[tool(name = "agentispay_limits", description = "Read transfer risk limits.")]
139    async fn limits(&self) -> Result<CallToolResult, McpError> {
140        let mut backend = self.backend.lock().await;
141        let data = backend.limits().await.map_err(backend_error)?;
142        Ok(tool_result(CommandName::Limits, json!(data)))
143    }
144
145    #[tool(name = "agentispay_pay", description = "Create a PIX transfer.")]
146    async fn pay(&self, params: Parameters<PayInput>) -> Result<CallToolResult, McpError> {
147        self.pay_impl(params.0).await
148    }
149
150    #[tool(
151        name = "agentispay_pix_send",
152        description = "Alias for agentispay_pay."
153    )]
154    async fn pix_send(&self, params: Parameters<PayInput>) -> Result<CallToolResult, McpError> {
155        self.pay_impl(params.0).await
156    }
157}
158
159impl AgentisPayServer {
160    async fn pay_impl(&self, mut input: PayInput) -> Result<CallToolResult, McpError> {
161        if input.amount_cents <= 0 {
162            return Err(McpError::invalid_params(
163                "amount_cents must be greater than zero",
164                None,
165            ));
166        }
167        if input.request_id.trim().is_empty() {
168            return Err(McpError::invalid_params("request_id is required", None));
169        }
170        if input.agent_message.trim().is_empty() {
171            return Err(McpError::invalid_params("agent_message is required", None));
172        }
173        if input
174            .idempotency_key
175            .as_deref()
176            .is_some_and(|token| token.trim().is_empty())
177        {
178            input.idempotency_key = None;
179        }
180
181        let mut backend = self.backend.lock().await;
182        let data = backend.pay(input).await.map_err(backend_error)?;
183        Ok(tool_result(CommandName::PixSend, json!(data)))
184    }
185}
186
187#[tool_handler]
188impl ServerHandler for AgentisPayServer {
189    async fn initialize(
190        &self,
191        request: InitializeRequestParams,
192        context: RequestContext<RoleServer>,
193    ) -> Result<InitializeResult, McpError> {
194        if context.peer.peer_info().is_none() {
195            context.peer.set_peer_info(request.clone());
196        }
197
198        let client_name = request.client_info.name.clone();
199        if !client_name.is_empty() {
200            let mut backend = self.backend.lock().await;
201            backend.set_mcp_client_name(client_name);
202        }
203
204        let cli_version =
205            std::env::var("AGENTIS_PAY_CLI_VERSION").unwrap_or_else(|_| "0.1.0".to_string());
206
207        Ok(
208            InitializeResult::new(ServerCapabilities::builder().enable_tools().build())
209                .with_server_info(Implementation::new("agentis-pay", cli_version)),
210        )
211    }
212}