futu_mcp/tools/
position_action.rs1use 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));