Files
foxhunt/bin/fxt/src/mcp/tools.rs
jgrusewski ab101f1110 feat: MCP server mode + TUI cockpit framework
MCP server (bin/fxt/src/mcp/):
- JSON-RPC 2.0 protocol over stdin/stdout
- 32 tool definitions across 11 domains
- McpServer with initialize/tools_list/tools_call handlers
- 21 unit tests

TUI cockpits (bin/fxt/src/tui/):
- Purple/cyan/dark navy theme from design spec
- 6 cockpit views: overview, training, trading, services, risk, data
- Crossterm event loop with key handling (1-6 switch, q quit, ? help)
- CockpitView trait for pluggable cockpit rendering

All 75 tests pass, 0 clippy warnings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 21:48:59 +01:00

362 lines
13 KiB
Rust

//! MCP tool definitions -- maps CLI commands to tool descriptors.
use serde::Serialize;
use serde_json::{json, Value};
/// A single MCP tool descriptor.
#[derive(Debug, Clone, Serialize)]
pub struct ToolDefinition {
/// Unique tool name (e.g. `"fxt_service_list"`).
pub name: String,
/// Human-readable description of what the tool does.
pub description: String,
/// JSON Schema for the tool's input arguments.
#[serde(rename = "inputSchema")]
pub input_schema: Value,
}
/// Returns all available tool definitions.
///
/// Organised by domain: services, training, trading, models,
/// broker, risk, data, cluster, agent, config, tune.
pub fn all_tools() -> Vec<ToolDefinition> {
vec![
// ── Service operations ──────────────────────────────────────
tool(
"fxt_service_list",
"List all services with health status",
json!({"type": "object", "properties": {}}),
),
tool(
"fxt_service_status",
"Get detailed status of a specific service",
json!({
"type": "object",
"properties": {
"service": {"type": "string", "description": "Service name"}
},
"required": ["service"]
}),
),
tool(
"fxt_service_health",
"Health check all services",
json!({"type": "object", "properties": {}}),
),
// ── Training ────────────────────────────────────────────────
tool(
"fxt_train_start",
"Start ML training job",
json!({
"type": "object",
"properties": {
"model": {
"type": "string",
"description": "Model type (dqn, ppo, tft, mamba2, tggn, tlob, liquid, kan, xlstm, diffusion)"
},
"symbol": {
"type": "string",
"description": "Trading symbol (e.g. ES.FUT)"
}
},
"required": ["model"]
}),
),
tool(
"fxt_train_stop",
"Stop a training job",
json!({
"type": "object",
"properties": {
"job_id": {"type": "string", "description": "Training job identifier"}
},
"required": ["job_id"]
}),
),
tool(
"fxt_train_status",
"Get training job status and metrics",
json!({
"type": "object",
"properties": {
"job_id": {"type": "string", "description": "Training job identifier (omit for latest)"}
}
}),
),
tool(
"fxt_train_list",
"List all training jobs",
json!({"type": "object", "properties": {}}),
),
// ── Trading ─────────────────────────────────────────────────
tool(
"fxt_trade_positions",
"Get current positions",
json!({"type": "object", "properties": {}}),
),
tool(
"fxt_trade_orders",
"List active orders",
json!({"type": "object", "properties": {}}),
),
tool(
"fxt_trade_account",
"Get account state (balance, margin, P&L)",
json!({"type": "object", "properties": {}}),
),
tool(
"fxt_trade_submit",
"Place an order",
json!({
"type": "object",
"properties": {
"symbol": {"type": "string", "description": "Trading symbol (e.g. ES.FUT)"},
"side": {"type": "string", "enum": ["buy", "sell"], "description": "Order side"},
"quantity": {"type": "number", "description": "Order quantity"},
"order_type": {"type": "string", "enum": ["market", "limit"], "description": "Order type"},
"price": {"type": "number", "description": "Limit price (required for limit orders)"}
},
"required": ["symbol", "side", "quantity", "order_type"]
}),
),
// ── Model management ────────────────────────────────────────
tool(
"fxt_model_list",
"List available models",
json!({"type": "object", "properties": {}}),
),
tool(
"fxt_model_status",
"Get model health and performance metrics",
json!({
"type": "object",
"properties": {
"model": {"type": "string", "description": "Model name or identifier"}
},
"required": ["model"]
}),
),
tool(
"fxt_model_predict",
"Get model prediction for a symbol",
json!({
"type": "object",
"properties": {
"model": {"type": "string", "description": "Model name"},
"symbol": {"type": "string", "description": "Trading symbol"}
},
"required": ["model", "symbol"]
}),
),
tool(
"fxt_model_ensemble",
"Get ensemble vote (aggregated prediction from all models)",
json!({
"type": "object",
"properties": {
"symbol": {"type": "string", "description": "Trading symbol"}
},
"required": ["symbol"]
}),
),
// ── Broker ──────────────────────────────────────────────────
tool(
"fxt_broker_status",
"Broker session status (FIX, heartbeat RTT)",
json!({"type": "object", "properties": {}}),
),
tool(
"fxt_broker_connect",
"Establish broker session",
json!({"type": "object", "properties": {}}),
),
// ── Risk ────────────────────────────────────────────────────
tool(
"fxt_risk_status",
"Risk system state (kill switches, circuit breakers)",
json!({"type": "object", "properties": {}}),
),
tool(
"fxt_risk_limits",
"Current risk limits",
json!({"type": "object", "properties": {}}),
),
tool(
"fxt_risk_drawdown",
"Drawdown statistics",
json!({"type": "object", "properties": {}}),
),
tool(
"fxt_risk_emergency",
"Emergency controls (halt all trading or resume)",
json!({
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": ["halt", "resume"],
"description": "Emergency action"
}
},
"required": ["action"]
}),
),
// ── Data ────────────────────────────────────────────────────
tool(
"fxt_data_status",
"Data feed status and freshness",
json!({"type": "object", "properties": {}}),
),
tool(
"fxt_data_feeds",
"Active feed list",
json!({"type": "object", "properties": {}}),
),
// ── Cluster ─────────────────────────────────────────────────
tool(
"fxt_cluster_status",
"Node/pod status",
json!({"type": "object", "properties": {}}),
),
tool(
"fxt_cluster_resources",
"CPU/RAM/GPU utilization",
json!({"type": "object", "properties": {}}),
),
// ── Agent ───────────────────────────────────────────────────
tool(
"fxt_agent_start",
"Start trading agent",
json!({"type": "object", "properties": {}}),
),
tool(
"fxt_agent_stop",
"Stop trading agent",
json!({"type": "object", "properties": {}}),
),
tool(
"fxt_agent_status",
"Trading agent state",
json!({"type": "object", "properties": {}}),
),
// ── Config ──────────────────────────────────────────────────
tool(
"fxt_config_get",
"Get configuration value",
json!({
"type": "object",
"properties": {
"key": {"type": "string", "description": "Configuration key"}
},
"required": ["key"]
}),
),
tool(
"fxt_config_set",
"Set configuration value",
json!({
"type": "object",
"properties": {
"key": {"type": "string", "description": "Configuration key"},
"value": {"type": "string", "description": "New value"}
},
"required": ["key", "value"]
}),
),
// ── Tune ────────────────────────────────────────────────────
tool(
"fxt_tune_start",
"Start hyperparameter tuning (PSO/Argmin)",
json!({
"type": "object",
"properties": {
"model": {
"type": "string",
"description": "Model type to tune"
}
},
"required": ["model"]
}),
),
tool(
"fxt_tune_status",
"Tuning progress and best parameters so far",
json!({"type": "object", "properties": {}}),
),
]
}
/// Helper to build a [`ToolDefinition`].
fn tool(name: &str, description: &str, input_schema: Value) -> ToolDefinition {
ToolDefinition {
name: name.to_owned(),
description: description.to_owned(),
input_schema,
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn all_tools_non_empty() {
let tools = all_tools();
assert!(!tools.is_empty());
}
#[test]
fn all_tool_names_unique() {
let tools = all_tools();
let mut names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect();
let count = names.len();
names.sort_unstable();
names.dedup();
assert_eq!(names.len(), count, "duplicate tool names found");
}
#[test]
fn tool_names_follow_convention() {
let tools = all_tools();
for t in &tools {
assert!(
t.name.starts_with("fxt_"),
"tool name '{}' must start with 'fxt_'",
t.name
);
}
}
#[test]
fn tool_schemas_are_objects() {
let tools = all_tools();
for t in &tools {
let ty = t.input_schema.get("type");
assert_eq!(
ty,
Some(&json!("object")),
"tool '{}' input_schema.type must be 'object'",
t.name
);
}
}
#[test]
fn tool_definitions_serialize() {
let tools = all_tools();
let first = tools.first().unwrap();
let json_str = serde_json::to_string(first).unwrap();
assert!(json_str.contains("inputSchema"));
assert!(json_str.contains(&first.name));
}
#[test]
fn expected_tool_count() {
// 3 service + 4 training + 4 trading + 4 model + 2 broker
// + 4 risk + 2 data + 2 cluster + 3 agent + 2 config + 2 tune = 32
let tools = all_tools();
assert_eq!(tools.len(), 32);
}
}