agentis_pay/mcp/
handler.rs1use 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(¶ms.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}