Skip to main content

futu_mcp/tools/
group_order.rs

1//! MCP adapters for guarded v1.8 atomic group-order writes.
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_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;