1use futu_auth::CheckCtx;
4use rmcp::{RoleServer, handler::server::wrapper::Parameters, service::RequestContext};
5
6use crate::guard;
7use crate::handlers;
8use crate::tool_args::{ComboOrderProtoJsonReq, ProtoJsonReq};
9use crate::tool_auth::{http_bearer_token, outcome_key_id_from_snapshot};
10
11use super::FutuServer;
12
13impl FutuServer {
14 async fn futu_get_option_quote_impl(
15 &self,
16 Parameters(req): Parameters<ProtoJsonReq>,
17 req_ctx: RequestContext<RoleServer>,
18 ) -> std::result::Result<String, String> {
19 let c2s = match handlers::proto_json::parse_c2s_json::<futu_proto::qot_get_option_quote::C2s>(
20 "option-quote",
21 &req.c2s_json,
22 ) {
23 Ok(c2s) => c2s,
24 Err(err) => return Self::tool_err(err),
25 };
26 let client = self
27 .read_client_or_err(
28 "futu_get_option_quote",
29 &req_ctx,
30 req.api_key.as_deref(),
31 None,
32 )
33 .await?;
34 Self::wrap_result(handlers::proto_json::option_quote(&client, c2s).await)
35 }
36
37 async fn futu_get_option_strategy_impl(
38 &self,
39 Parameters(req): Parameters<ProtoJsonReq>,
40 req_ctx: RequestContext<RoleServer>,
41 ) -> std::result::Result<String, String> {
42 let c2s = match handlers::proto_json::parse_c2s_json::<
43 futu_proto::qot_get_option_strategy::C2s,
44 >("option-strategy", &req.c2s_json)
45 {
46 Ok(c2s) => c2s,
47 Err(err) => return Self::tool_err(err),
48 };
49 let client = self
50 .read_client_or_err(
51 "futu_get_option_strategy",
52 &req_ctx,
53 req.api_key.as_deref(),
54 None,
55 )
56 .await?;
57 Self::wrap_result(handlers::proto_json::option_strategy(&client, c2s).await)
58 }
59
60 async fn futu_get_option_strategy_analysis_impl(
61 &self,
62 Parameters(req): Parameters<ProtoJsonReq>,
63 req_ctx: RequestContext<RoleServer>,
64 ) -> std::result::Result<String, String> {
65 let c2s = match handlers::proto_json::parse_c2s_json::<
66 futu_proto::qot_get_option_strategy_analysis::C2s,
67 >("option-strategy-analysis", &req.c2s_json)
68 {
69 Ok(c2s) => c2s,
70 Err(err) => return Self::tool_err(err),
71 };
72 let client = self
73 .read_client_or_err(
74 "futu_get_option_strategy_analysis",
75 &req_ctx,
76 req.api_key.as_deref(),
77 None,
78 )
79 .await?;
80 Self::wrap_result(handlers::proto_json::option_strategy_analysis(&client, c2s).await)
81 }
82
83 async fn futu_get_option_strategy_spread_impl(
84 &self,
85 Parameters(req): Parameters<ProtoJsonReq>,
86 req_ctx: RequestContext<RoleServer>,
87 ) -> std::result::Result<String, String> {
88 let c2s = match handlers::proto_json::parse_c2s_json::<
89 futu_proto::qot_get_option_strategy_spread::C2s,
90 >("option-strategy-spread", &req.c2s_json)
91 {
92 Ok(c2s) => c2s,
93 Err(err) => return Self::tool_err(err),
94 };
95 let client = self
96 .read_client_or_err(
97 "futu_get_option_strategy_spread",
98 &req_ctx,
99 req.api_key.as_deref(),
100 None,
101 )
102 .await?;
103 Self::wrap_result(handlers::proto_json::option_strategy_spread(&client, c2s).await)
104 }
105
106 async fn futu_get_combo_max_trd_qtys_impl(
107 &self,
108 Parameters(req): Parameters<ProtoJsonReq>,
109 req_ctx: RequestContext<RoleServer>,
110 ) -> std::result::Result<String, String> {
111 let c2s = match handlers::proto_json::parse_combo_max_c2s_json(&req.c2s_json) {
112 Ok(c2s) => c2s,
113 Err(err) => return Self::tool_err(err),
114 };
115 let acc_id = match handlers::proto_json::combo_max_context(&c2s) {
116 Ok(acc_id) => acc_id,
117 Err(err) => return Self::tool_err(err),
118 };
119 let client = self
120 .read_client_or_err(
121 "futu_get_combo_max_trd_qtys",
122 &req_ctx,
123 req.api_key.as_deref(),
124 Some(acc_id),
125 )
126 .await?;
127 Self::wrap_result(handlers::proto_json::combo_max_trd_qtys(&client, c2s).await)
128 }
129
130 async fn futu_place_combo_order_impl(
131 &self,
132 Parameters(req): Parameters<ComboOrderProtoJsonReq>,
133 req_ctx: RequestContext<RoleServer>,
134 ) -> std::result::Result<String, String> {
135 let header_token = http_bearer_token(&req_ctx);
136 let override_key = req.api_key.as_deref().or(header_token.as_deref());
137 let args_hash = guard::args_short_hash(&req);
138
139 let c2s = match handlers::proto_json::parse_place_combo_c2s_json(&req.c2s_json) {
140 Ok(c2s) => c2s,
141 Err(err) => return Self::tool_err(err),
142 };
143 let combo_ctx = match handlers::proto_json::place_combo_context(&c2s) {
144 Ok(ctx) => ctx,
145 Err(err) => return Self::tool_err(err),
146 };
147
148 tracing::warn!(
149 target: futu_auth::audit::TARGET,
150 iface = "mcp",
151 endpoint = "futu_place_combo_order",
152 env = %combo_ctx.env,
153 market = %combo_ctx.market,
154 acc_id = combo_ctx.acc_id,
155 order_value = crate::state::audit_fmt::opt_f64(combo_ctx.order_value),
156 args_hash = %args_hash,
157 outcome = "request",
158 "place_combo_order request received"
159 );
160
161 let caller_key_rec =
162 match self.require_caller_key_strict("futu_place_combo_order", override_key) {
163 Ok(rec) => rec,
164 Err(reject_json) => return Err(reject_json),
165 };
166 if let Some(reject) = self.require_trading_scope_only(
167 "futu_place_combo_order",
168 combo_ctx.env,
169 caller_key_rec.as_ref(),
170 ) {
171 return Err(reject);
172 }
173
174 let ctx = CheckCtx {
175 market: combo_ctx.market.clone(),
176 symbol: String::new(),
177 order_value: combo_ctx.order_value,
178 trd_side: None,
179 acc_id: Some(combo_ctx.acc_id),
180 mutation_no_exposure: false,
181 currency: None,
182 };
183 if let Some(reject) = self.require_trading(
184 "futu_place_combo_order",
185 combo_ctx.env,
186 Some(ctx),
187 override_key,
188 ) {
189 return Err(reject);
190 }
191
192 let client = self.client_or_err().await?;
193 let result = Self::wrap_result(
194 handlers::proto_json::place_combo_order(&client, c2s, req.idempotency_key).await,
195 );
196 let startup_key = self.state.authed_key();
197 let outcome_key_id =
198 outcome_key_id_from_snapshot(caller_key_rec.as_ref(), startup_key.as_ref());
199 guard::emit_trade_outcome(
200 "futu_place_combo_order",
201 outcome_key_id,
202 &args_hash,
203 Self::result_as_str(&result),
204 );
205 result
206 }
207}
208
209include!(concat!(env!("OUT_DIR"), "/generated_mcp_routes_combo.rs"));