Skip to main content

futu_mcp/tools/
stock_note.rs

1//! Private stock-note reads use authenticated OpenD REST, never raw FTAPI.
2
3use rmcp::{RoleServer, handler::server::wrapper::Parameters, service::RequestContext};
4
5use crate::handlers;
6use crate::tool_args::ProtoJsonReq;
7use crate::tool_auth::http_bearer_token;
8
9use super::FutuServer;
10
11macro_rules! tool {
12    ($method:ident, $tool:literal, $label:literal, $path:literal, $module:ident) => {
13        impl FutuServer {
14            async fn $method(
15                &self,
16                Parameters(req): Parameters<ProtoJsonReq>,
17                req_ctx: RequestContext<RoleServer>,
18            ) -> std::result::Result<String, String> {
19                let c2s = handlers::proto_json::parse_c2s_json::<futu_proto::$module::C2s>(
20                    $label,
21                    &req.c2s_json,
22                )
23                .map_err(|error| error.to_string())?;
24                let header_token = http_bearer_token(&req_ctx);
25                let override_key = req.api_key.as_deref().or(header_token.as_deref());
26                let caller = self.require_caller_key_strict($tool, override_key)?;
27                let Some(caller) = caller else {
28                    return Err("stock-note tools require configured API-key authentication".into());
29                };
30                if !caller.scopes.contains(&futu_auth::Scope::AccRead) {
31                    return Err("forbidden".into());
32                }
33                let base_url = self.state.opend_rest_url().ok_or_else(|| {
34                    "stock-note tools require --opend-rest-url (or FUTU_MCP_OPEND_REST_URL)"
35                        .to_string()
36                })?;
37                let bearer = override_key.or(self.state.opend_api_key()).ok_or_else(|| {
38                    "authenticated OpenD REST forwarding lacks a bearer key".to_string()
39                })?;
40                FutuServer::wrap_result(
41                    super::authenticated_rest::post_json(
42                        base_url,
43                        $path,
44                        bearer,
45                        &c2s,
46                        "stock-note read",
47                    )
48                    .await,
49                )
50            }
51        }
52    };
53}
54
55tool!(
56    futu_get_stock_note_labels_impl,
57    "futu_get_stock_note_labels",
58    "stock-note-labels",
59    "/api/stock-note-labels",
60    qot_get_stock_note_labels
61);
62tool!(
63    futu_get_stock_notes_impl,
64    "futu_get_stock_notes",
65    "stock-notes",
66    "/api/stock-notes",
67    qot_get_stock_notes
68);
69
70impl FutuServer {
71    async fn futu_set_stock_note_impl(
72        &self,
73        Parameters(req): Parameters<ProtoJsonReq>,
74        req_ctx: RequestContext<RoleServer>,
75    ) -> std::result::Result<String, String> {
76        let c2s = handlers::proto_json::parse_c2s_json::<futu_proto::qot_set_stock_note::C2s>(
77            "set-stock-note",
78            &req.c2s_json,
79        )
80        .map_err(|error| error.to_string())?;
81        let header_token = http_bearer_token(&req_ctx);
82        let override_key = req.api_key.as_deref().or(header_token.as_deref());
83        let caller = self.require_caller_key_strict("futu_set_stock_note", override_key)?;
84        let Some(caller) = caller else {
85            return Err("stock-note tools require configured API-key authentication".into());
86        };
87        if !caller.scopes.contains(&futu_auth::Scope::QotWrite) {
88            return Err("forbidden".into());
89        }
90        let base_url = self.state.opend_rest_url().ok_or_else(|| {
91            "stock-note tools require --opend-rest-url (or FUTU_MCP_OPEND_REST_URL)".to_string()
92        })?;
93        let bearer = override_key
94            .or(self.state.opend_api_key())
95            .ok_or_else(|| "authenticated OpenD REST forwarding lacks a bearer key".to_string())?;
96        FutuServer::wrap_result(
97            super::authenticated_rest::post_json(
98                base_url,
99                "/api/set-stock-note",
100                bearer,
101                &c2s,
102                "stock-note write",
103            )
104            .await,
105        )
106    }
107}
108
109include!(concat!(
110    env!("OUT_DIR"),
111    "/generated_mcp_routes_stock_note.rs"
112));
113
114#[cfg(test)]
115#[path = "stock_note_authenticated_rest_tests.rs"]
116mod authenticated_rest_tests;