Skip to main content

futu_mcp/tools/
position_action.rs

1//! MCP adapters for guarded v1.8 position actions.
2
3use std::sync::Arc;
4
5use futu_auth::CheckCtx;
6use futu_net::client::FutuClient;
7use rmcp::{RoleServer, handler::server::wrapper::Parameters, service::RequestContext};
8use serde::Serialize;
9
10use crate::handlers;
11use crate::tool_args::ProtoJsonReq;
12use crate::tool_auth::http_bearer_token;
13
14use super::FutuServer;
15
16async fn call_authenticated_rest<T: Serialize>(
17    base_url: &str,
18    path: &str,
19    bearer: &str,
20    c2s: &T,
21) -> anyhow::Result<String> {
22    super::authenticated_rest::post_json(base_url, path, bearer, c2s, "position-action request")
23        .await
24}
25
26macro_rules! position_tool {
27    ($impl_name:ident, $tool:literal, $label:literal, $path:literal, $c2s:path, $handler:ident, $call:ident) => {
28        impl FutuServer {
29            async fn $impl_name(
30                &self,
31                Parameters(req): Parameters<ProtoJsonReq>,
32                req_ctx: RequestContext<RoleServer>,
33            ) -> std::result::Result<String, String> {
34                let c2s = handlers::proto_json::parse_c2s_json::<$c2s>($label, &req.c2s_json)
35                    .map_err(|error| error.to_string())?;
36                let header_token = http_bearer_token(&req_ctx);
37                let override_key = req.api_key.as_deref().or(header_token.as_deref());
38                let caller = self.require_caller_key_strict($tool, override_key)?;
39                let ctx = CheckCtx {
40                    market: futu_auth_pipeline::body_aware::trd_market_str(c2s.header.trd_market)
41                        .into(),
42                    symbol: String::new(),
43                    order_value: None,
44                    trd_side: None,
45                    acc_id: Some(c2s.header.acc_id),
46                    mutation_no_exposure: false,
47                    currency: None,
48                };
49                if caller.is_some() {
50                    if let Some(reject) = self.require_trading_scope_only($tool, "real", caller.as_ref()) {
51                        return Err(reject);
52                    }
53                } else if let Some(reject) = self.require_trading($tool, "real", Some(ctx), override_key) {
54                    return Err(reject);
55                }
56                let result = if caller.is_some() {
57                    let base_url = self.state.opend_rest_url().ok_or_else(|| {
58                        "authenticated position actions require --opend-rest-url (or FUTU_MCP_OPEND_REST_URL)".to_string()
59                    })?;
60                    let bearer = override_key
61                        .or(self.state.opend_api_key())
62                        .ok_or_else(|| "authenticated OpenD REST forwarding lacks a bearer key".to_string())?;
63                    call_authenticated_rest(base_url, $path, bearer, &c2s).await
64                } else {
65                    let client = self.client_or_err().await?;
66                    $call(client, c2s).await
67                };
68                FutuServer::wrap_result(result)
69            }
70        }
71
72        async fn $call(client: Arc<FutuClient>, c2s: $c2s) -> anyhow::Result<String> {
73            handlers::proto_json::$handler(&client, c2s).await
74        }
75    };
76}
77
78position_tool!(
79    futu_reverse_position_impl,
80    "futu_reverse_position",
81    "reverse-position",
82    "/api/trd/reverse-position",
83    futu_proto::trd_reverse_position::C2s,
84    reverse_position,
85    call_reverse_position
86);
87position_tool!(
88    futu_roll_position_impl,
89    "futu_roll_position",
90    "roll-position",
91    "/api/trd/roll-position",
92    futu_proto::trd_roll_position::C2s,
93    roll_position,
94    call_roll_position
95);
96position_tool!(
97    futu_clear_futures_positions_impl,
98    "futu_clear_futures_positions",
99    "clear-futures-positions",
100    "/api/trd/clear-futures-positions",
101    futu_proto::trd_clear_futures_positions::C2s,
102    clear_futures_positions,
103    call_clear_futures_positions
104);
105position_tool!(
106    futu_batch_close_positions_impl,
107    "futu_batch_close_positions",
108    "batch-close-positions",
109    "/api/trd/batch-close-positions",
110    futu_proto::trd_batch_close_positions::C2s,
111    batch_close_positions,
112    call_batch_close_positions
113);
114
115include!(concat!(
116    env!("OUT_DIR"),
117    "/generated_mcp_routes_position_action.rs"
118));