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>, tool_handler: Arc, auto_verify_result: Arc>>, } 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, 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 })) } }