futu_mcp/tools/
group_order.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_group_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, "group write").await
23}
24
25macro_rules! tool {
26 ($impl_name:ident, $tool:literal, $label:literal, $path:literal, $c2s:path, $handler:ident, $call:ident) => {
27 impl FutuServer {
28 async fn $impl_name(
29 &self,
30 Parameters(req): Parameters<ProtoJsonReq>,
31 req_ctx: RequestContext<RoleServer>,
32 ) -> std::result::Result<String, String> {
33 let c2s = handlers::proto_json::parse_c2s_json::<$c2s>($label, &req.c2s_json)
34 .map_err(|error| error.to_string())?;
35 let header_token = http_bearer_token(&req_ctx);
36 let override_key = req.api_key.as_deref().or(header_token.as_deref());
37 let caller = self.require_caller_key_strict($tool, override_key)?;
38 let ctx = CheckCtx {
39 market: futu_auth_pipeline::body_aware::trd_market_str(
40 c2s.header.trd_market,
41 )
42 .to_string(),
43 symbol: String::new(),
44 order_value: None,
45 trd_side: None,
46 acc_id: Some(c2s.header.acc_id),
47 mutation_no_exposure: false,
48 currency: None,
49 };
50 if caller.is_some() {
51 if let Some(reject) =
52 self.require_trading_scope_only($tool, "real", caller.as_ref())
53 {
54 return Err(reject);
55 }
56 } else if let Some(reject) =
57 self.require_trading($tool, "real", Some(ctx), override_key)
58 {
59 return Err(reject);
60 }
61 let result = if caller.is_some() {
62 let base_url = self.state.opend_rest_url().ok_or_else(|| {
63 "authenticated group writes require --opend-rest-url (or FUTU_MCP_OPEND_REST_URL); secure OpenD deployments disable raw TCP"
64 .to_string()
65 })?;
66 let bearer = override_key
67 .or(self.state.opend_api_key())
68 .ok_or_else(|| "authenticated OpenD REST forwarding lacks a bearer key".to_string())?;
69 call_authenticated_group_rest(base_url, $path, bearer, &c2s).await
70 } else {
71 let client = self.client_or_err().await?;
72 $call(client, c2s).await
73 };
74 FutuServer::wrap_result(result)
75 }
76 }
77
78 async fn $call(client: Arc<FutuClient>, c2s: $c2s) -> anyhow::Result<String> {
79 handlers::proto_json::$handler(&client, c2s).await
80 }
81 };
82}
83
84tool!(
85 futu_place_order_group_impl,
86 "futu_place_order_group",
87 "place-order-group",
88 "/api/trd/place-order-group",
89 futu_proto::trd_place_order_group::C2s,
90 place_order_group,
91 call_place_order_group
92);
93tool!(
94 futu_modify_order_group_impl,
95 "futu_modify_order_group",
96 "modify-order-group",
97 "/api/trd/modify-order-group",
98 futu_proto::trd_modify_order_group::C2s,
99 modify_order_group,
100 call_modify_order_group
101);
102tool!(
103 futu_cancel_order_group_impl,
104 "futu_cancel_order_group",
105 "cancel-order-group",
106 "/api/trd/cancel-order-group",
107 futu_proto::trd_cancel_order_group::C2s,
108 cancel_order_group,
109 call_cancel_order_group
110);
111tool!(
112 futu_delete_order_group_impl,
113 "futu_delete_order_group",
114 "delete-order-group",
115 "/api/trd/delete-order-group",
116 futu_proto::trd_delete_order_group::C2s,
117 delete_order_group,
118 call_delete_order_group
119);
120
121include!(concat!(
122 env!("OUT_DIR"),
123 "/generated_mcp_routes_group_order.rs"
124));
125
126#[cfg(test)]
127#[path = "group_order_authenticated_rest_tests.rs"]
128mod authenticated_rest_tests;