Skip to main content

futu_mcp/
tool_account.rs

1//! MCP account-locator helpers shared by thin tool wrappers.
2//!
3//! Keep account resolution semantics out of `tools.rs`: caller snapshot,
4//! `card_num` lookup, and `allowed_acc_ids` enforcement must stay identical
5//! across funds / positions / orders / deals style tools.
6
7use std::sync::Arc;
8
9use futu_core::account_locator;
10use futu_net::client::FutuClient;
11use rmcp::{RoleServer, service::RequestContext};
12
13use crate::handlers;
14use crate::tool_args::TrdAccReq;
15use crate::tools::FutuServer;
16
17pub(crate) struct ResolvedTrdAccount {
18    pub(crate) client: Arc<FutuClient>,
19    pub(crate) acc_id: u64,
20}
21
22impl FutuServer {
23    /// Resolve a `TrdAccReq` account selector for MCP read tools.
24    ///
25    /// This preserves the v1.4.103-v1.4.106 security order:
26    /// 1. authenticate caller with the same API key priority as the tool;
27    /// 2. open the gateway client;
28    /// 3. resolve `card_num` inside the caller's allowed account snapshot;
29    /// 4. re-check the final `acc_id` before dispatch.
30    pub(crate) async fn resolve_read_trd_account(
31        &self,
32        tool: &'static str,
33        req: &TrdAccReq,
34        req_ctx: &RequestContext<RoleServer>,
35    ) -> std::result::Result<ResolvedTrdAccount, String> {
36        let snap =
37            self.require_acc_read_with_acc_id(tool, req_ctx, req.api_key.as_deref(), None)?;
38        let client = self.client_or_err().await?;
39        let allowed_card_nums = snap
40            .rec
41            .as_ref()
42            .and_then(|r| r.allowed_card_nums.as_deref());
43        let acc_id = handlers::trade_write::resolve_acc_id_with_card_num(
44            &client,
45            req.acc_id.unwrap_or(0),
46            req.card_num.as_deref(),
47            allowed_card_nums,
48            snap.allowed_acc_ids.as_ref(),
49        )
50        .await
51        .map_err(|msg| {
52            serde_json::json!({
53                "error": msg,
54                "status": "error",
55            })
56            .to_string()
57        })?;
58        if !account_locator::acc_id_visible_to_caller(acc_id, snap.allowed_acc_ids.as_ref()) {
59            return Err(serde_json::json!({
60                "error": format!("{tool}: acc_id {acc_id} not in caller allowed_acc_ids"),
61                "status": "error",
62            })
63            .to_string());
64        }
65        Ok(ResolvedTrdAccount { client, acc_id })
66    }
67}
68
69#[cfg(test)]
70mod tests;