Skip to main content

futu_mcp/tools/
authenticated_rest.rs

1//! Hardened forwarding from authenticated MCP write tools to OpenD REST.
2
3use std::sync::OnceLock;
4use std::time::Duration;
5
6use serde::Serialize;
7
8// Match the native FTAPI request ceiling so authenticated REST forwarding
9// cannot outlive the equivalent gateway request. Ref: futu-net/src/client.rs:19.
10const OPEND_REST_FORWARD_TIMEOUT: Duration = Duration::from_secs(12);
11
12/// One shared WebPKI-hardened client for every authenticated forward so the
13/// root store is built once per process instead of per call.
14fn forward_client() -> anyhow::Result<&'static reqwest::Client> {
15    static CLIENT: OnceLock<reqwest::Client> = OnceLock::new();
16    if let Some(client) = CLIENT.get() {
17        return Ok(client);
18    }
19    let built = futu_core::http_client::webpki_builder()
20        .timeout(OPEND_REST_FORWARD_TIMEOUT)
21        .build()?;
22    Ok(CLIENT.get_or_init(|| built))
23}
24
25pub(super) async fn post_json<T: Serialize>(
26    base_url: &str,
27    path: &str,
28    bearer: &str,
29    c2s: &T,
30    operation: &str,
31) -> anyhow::Result<String> {
32    let response = forward_client()?
33        .post(format!("{}{}", base_url.trim_end_matches('/'), path))
34        .bearer_auth(bearer)
35        .json(c2s)
36        .send()
37        .await?;
38    let status = response.status();
39    let body = response.text().await?;
40    if !status.is_success() {
41        anyhow::bail!("OpenD REST {operation} failed with HTTP {status}: {body}");
42    }
43    let value: serde_json::Value = serde_json::from_str(&body)?;
44    Ok(serde_json::to_string_pretty(&value)?)
45}