MCP¶
Model Context Protocol server: exposes 19 gateway tools to LLM clients. Two transports: stdio / HTTP.
19 tools¶
Quote (qot:read, 11 tools)¶
| Tool | Description |
|---|---|
futu_ping |
Ping the gateway, returns RTT |
futu_get_quote |
Real-time quote (auto-subscribes Basic) |
futu_get_snapshot |
Snapshot (52-week high/low, turnover, book) |
futu_get_kline |
Historical candles (day / week / month / 1-60 min) |
futu_get_orderbook |
Level-2 book (auto-subscribes OrderBook) |
futu_get_ticker |
Tick-by-tick trades |
futu_get_rt |
Intraday time series |
futu_get_static |
Static info (name, lot size, list date) |
futu_get_broker |
Broker queue (HK only) |
futu_list_plates |
Plate/sector list |
futu_plate_stocks |
Plate constituents |
Account read (acc:read, 5 tools)¶
| Tool | Description |
|---|---|
futu_list_accounts |
Trading account list |
futu_get_funds |
Funds summary |
futu_get_positions |
Positions |
futu_get_orders |
Today's orders |
futu_get_deals |
Today's fills |
Trade (trade:real / trade:simulate, 3 tools)¶
| Tool | Description | Extra args |
|---|---|---|
futu_place_order |
Place order | api_key? per-call override |
futu_modify_order |
Modify / cancel | api_key? |
futu_cancel_order |
Cancel (shortcut) | api_key? |
Unlock trading (trade:unlock, v1.4+, 1 tool)¶
| Tool | Description | Extra args |
|---|---|---|
futu_unlock_trade |
Unlock / re-lock real trading | unlock: bool (default true) |
- Plaintext password never enters LLM prompt: the server reads it from
the OS keychain (preferred) or the
FUTU_TRADE_PWDenvironment variable (fallback). - Ops writes the password into the keychain via
futucli set-trade-pwd; the LLM only calls thefutu_unlock_tradetool — password never flows through the model. - MD5 is computed server-side, then sent to the gateway.
- After unlock, the gateway process caches the cipher until the gateway restarts; individual orders don't need to re-unlock.
Startup¶
export FUTU_MCP_API_KEY="fc_xxxx..."
./futu-mcp \
--gateway 127.0.0.1:11111 \
--keys-file ~/.config/futu/keys.json
JSON-RPC frames over stdin/stdout, launched by the LLM client as a child process.
./futu-mcp \
--gateway 127.0.0.1:11111 \
--keys-file ~/.config/futu/keys.json \
--http-listen 127.0.0.1:38765
POST /mcp— rmcp streamable HTTP transportGET /metrics— Prometheus (no token required)GET /.well-known/oauth-protected-resource— OAuth 2.0 Protected Resource Metadata (RFC 9728, v1.4+; lets MCP clients auto-discover scope requirements)
OAuth metadata endpoint (v1.4+)¶
A /mcp request without Authorization: Bearer returns 401 +
WWW-Authenticate: Bearer resource_metadata="/.well-known/oauth-protected-resource".
The client fetches that URL to learn the required scopes:
{
"resource": "/mcp",
"bearer_methods_supported": ["header"],
"scopes_supported": [
"qot:read",
"acc:read",
"trade:simulate",
"trade:real",
"trade:unlock"
],
"resource_name": "FutuOpenD-rs MCP",
"resource_documentation": "https://futuapi.com/guide/mcp/"
}
Why not a full OAuth server?
Futu API keys are manually-configured long-lived credentials — there's
no interactive consent flow. RFC 9728 is designed exactly for
"declare the auth requirement, don't perform the flow" resource
servers. The actual token (= API key) is still handed out out-of-band
by ops and written into the MCP client config's Authorization
header.
Per-call API key precedence (v1.1+)¶
1. `api_key` field in the tool args (explicit)
2. HTTP Authorization: Bearer <token> (HTTP transport only)
3. Startup-time --api-key / FUTU_MCP_API_KEY env var
stdio mode has no HTTP header → falls back 2→3. HTTP mode lets multiple LLMs each carry their own token, with audit / limits tracked independently.
Central scope registry¶
guard::scope_for_tool(name) -> Option<ToolScope> is the single source
of truth. Adding a new #[tool] requires updating this registry —
otherwise the handler returns unknown MCP tool and the
all_known_tools_have_scopes test breaks.
SIGHUP hot reload¶
Changes to a key's scope / limits / expires_at / revocation take effect
immediately on MCP (since v0.8+, MCP looks up KeyStore on every request
by key_id).
Trading-tool security boundary¶
unlock_tradetakes no password arg (v1.4+) —futu_unlock_tradeonly hasunlock: bool; the server reads the password from the OS keychain / env var. The LLM prompt and tool-call log never see the password.- Simulate by default —
PlaceOrderReq.envdefaults to"simulate"; the LLM must explicitly pass"real"to hit the real account. - Scope isolation —
trade:simulatecan never place a real order (even ifenvis spoofed toreal,require_tradingrejects it). - Limits always apply — the handler runs the full CheckCtx: market / symbol / value / side / daily checks.
- WS push defense in depth (v1.4+) — even if the subscription gate
slips, push dispatch runs another scope filter; filtered pushes
increment
futu_ws_push_filtered_total.
MCP client integration¶
See Tutorial: MCP integration for Claude Desktop / Cursor / Continue configuration examples.