Files
mt5-quant/src/mcp_server.rs
T

259 lines
9.6 KiB
Rust

use serde_json::{json, Value};
use std::sync::Arc;
use tokio::sync::Mutex;
use crate::{models::Config as ModelsConfig, tools::ToolHandler, McpError, McpRequest, McpResponse};
/// Auto-verify result stored after first initialization
#[derive(Debug, Clone)]
#[allow(dead_code)]
struct AutoVerifyResult {
all_ok: bool,
hint: String,
config_path: String,
}
#[derive(Debug)]
pub struct McpServer {
initialized: Arc<Mutex<bool>>,
tool_handler: Arc<ToolHandler>,
auto_verify_result: Arc<Mutex<Option<AutoVerifyResult>>>,
}
impl McpServer {
pub fn new() -> Self {
let config = ModelsConfig::load().unwrap_or_default();
Self {
initialized: Arc::new(Mutex::new(false)),
tool_handler: Arc::new(ToolHandler::new(config)),
auto_verify_result: Arc::new(Mutex::new(None)),
}
}
/// Run verify_setup in background - non blocking
fn spawn_auto_verify(&self) {
let result_arc = self.auto_verify_result.clone();
tokio::spawn(async move {
// Get config
let config = ModelsConfig::load().unwrap_or_default();
let config_path = ModelsConfig::writable_config_path();
// Quick async file checks
let config_exists = tokio::task::spawn_blocking({
let path = config_path.clone();
move || path.exists()
}).await.unwrap_or(false);
let wine_ok = if let Some(wine) = &config.wine_executable {
let wine = wine.clone();
tokio::task::spawn_blocking(move || {
std::path::Path::new(&wine).exists()
}).await.unwrap_or(false)
} else {
false
};
let term_ok = if let Some(term) = &config.terminal_dir {
let term = term.clone();
tokio::task::spawn_blocking(move || {
std::path::Path::new(&term).is_dir()
}).await.unwrap_or(false)
} else {
false
};
let all_ok = config_exists && wine_ok && term_ok;
let hint = if all_ok {
"Environment fully configured and ready".to_string()
} else if !config_exists {
format!("Auto-discovery will run on first request. Config will be written to {}", config_path.display())
} else if !wine_ok {
"Wine/CrossOver not found - required for MT5 execution".to_string()
} else if !term_ok {
"MT5 directory not found - check installation".to_string()
} else {
"Fix missing paths in config".to_string()
};
let result = AutoVerifyResult {
all_ok,
hint,
config_path: config_path.to_string_lossy().to_string(),
};
// Store result
let mut guard = result_arc.lock().await;
*guard = Some(result);
});
}
/// Get current verify status (may be loading if called immediately after init)
#[allow(dead_code)]
async fn get_verify_status(&self) -> (Option<bool>, String) {
let guard = self.auto_verify_result.lock().await;
match guard.as_ref() {
Some(result) => (Some(result.all_ok), result.hint.clone()),
None => (None, "Checking environment...".to_string()),
}
}
/// Handle a notification (no id — no response sent)
pub async fn handle_notification(&self, request: McpRequest) {
match request.method.as_str() {
"notifications/initialized" => {
// Client confirms initialization is complete — no action needed
}
_ => {
tracing::debug!("Unhandled notification: {}", request.method);
}
}
}
pub async fn handle_request(&self, request: McpRequest) -> McpResponse {
match request.method.as_str() {
"initialize" => {
// Protocol version negotiation: client sends desired version
let client_version = request.params.as_ref()
.and_then(|p| p.get("protocolVersion"))
.and_then(|v| v.as_str())
.unwrap_or("2024-11-05");
// Server selects version (latest supported that client also supports)
let negotiated_version = if client_version.starts_with("2025-") {
"2024-11-05" // Fall back to stable version
} else {
"2024-11-05"
};
// Start background verify (non-blocking)
self.spawn_auto_verify();
*self.initialized.lock().await = true;
// Return immediately with fast status
let server_info = json!({
"name": "MT5-Quant",
"version": env!("CARGO_PKG_VERSION"),
"setup": {
"hint": "Auto-verification running... Use verify_setup tool for detailed status",
}
});
McpResponse {
jsonrpc: "2.0".to_string(),
id: request.id,
result: Some(json!({
"protocolVersion": negotiated_version,
"capabilities": {
"experimental": {},
"tools": {
"listChanged": false,
},
},
"serverInfo": server_info,
})),
error: None,
}
}
"tools/list" => {
let initialized = *self.initialized.lock().await;
if !initialized {
return McpResponse {
jsonrpc: "2.0".to_string(),
id: request.id,
result: None,
error: Some(McpError {
code: -32600,
message: "Received request before initialization was complete".to_string(),
data: None,
}),
};
}
McpResponse {
jsonrpc: "2.0".to_string(),
id: request.id,
result: Some(json!({"tools": crate::tools::get_tools_list()})),
error: None,
}
}
"tools/call" => {
let initialized = *self.initialized.lock().await;
if !initialized {
return McpResponse {
jsonrpc: "2.0".to_string(),
id: request.id,
result: None,
error: Some(McpError {
code: -32600,
message: "Received request before initialization was complete".to_string(),
data: None,
}),
};
}
if let Some(params) = request.params {
if let (Some(tool_name), Some(arguments)) = (
params.get("name").and_then(|v| v.as_str()),
params.get("arguments")
) {
let result = self.handle_tool_call(tool_name, arguments).await;
McpResponse {
jsonrpc: "2.0".to_string(),
id: request.id,
result: Some(result),
error: None,
}
} else {
McpResponse {
jsonrpc: "2.0".to_string(),
id: request.id,
result: None,
error: Some(McpError {
code: -32602,
message: "Invalid request parameters".to_string(),
data: None,
}),
}
}
} else {
McpResponse {
jsonrpc: "2.0".to_string(),
id: request.id,
result: None,
error: Some(McpError {
code: -32602,
message: "Invalid request parameters".to_string(),
data: None,
}),
}
}
}
_ => {
McpResponse {
jsonrpc: "2.0".to_string(),
id: request.id,
result: None,
error: Some(McpError {
code: -32601,
message: format!("Method not found: {}", request.method),
data: None,
}),
}
}
}
}
async fn handle_tool_call(&self, tool_name: &str, arguments: &Value) -> Value {
self.tool_handler.handle(tool_name, arguments).await.unwrap_or_else(|e| json!({
"content": [{
"type": "text",
"text": format!("Tool execution failed: {}", e)
}],
"isError": true
}))
}
}