use std::sync::Arc; use tauri::State; use tokio::sync::Mutex as TokioMutex; use tracing::{info, warn}; use crate::mcp::models::{ CreateMcpServerRequest, McpServer, McpServerStatus, UpdateMcpServerRequest, }; use crate::mcp::store::{ create_server, delete_server, get_resource_count, get_server, get_tool_count, list_servers, toggle_server, update_discovery_status, update_server, }; use crate::state::AppState; #[tauri::command] pub async fn list_mcp_servers(state: State<'_, AppState>) -> Result, String> { let db = state.db.lock().map_err(|e| e.to_string())?; let mut servers = list_servers(&db)?; // Never expose encrypted auth values to the frontend for s in &mut servers { s.auth_value = None; } Ok(servers) } #[tauri::command] pub async fn create_mcp_server( request: CreateMcpServerRequest, state: State<'_, AppState>, ) -> Result { let mut server = { let db = state.db.lock().map_err(|e| e.to_string())?; create_server(&db, &request)? }; server.auth_value = None; Ok(server) } #[tauri::command] pub async fn update_mcp_server( id: String, request: UpdateMcpServerRequest, state: State<'_, AppState>, ) -> Result { let mut server = { let db = state.db.lock().map_err(|e| e.to_string())?; update_server(&db, &id, &request)? }; server.auth_value = None; Ok(server) } #[tauri::command] pub async fn delete_mcp_server( id: String, state: State<'_, AppState>, app_handle: tauri::AppHandle, ) -> Result<(), String> { { let db = state.db.lock().map_err(|e| e.to_string())?; // Capture server name and tool count for the audit entry before cascade delete let server_name = get_server(&db, &id)? .map(|s| s.name) .unwrap_or_else(|| id.clone()); let tool_count = crate::mcp::store::get_tool_count(&db, &id).unwrap_or(0); let resource_count = crate::mcp::store::get_resource_count(&db, &id).unwrap_or(0); let details = serde_json::json!({ "server_name": server_name, "tools_deleted": tool_count, "resources_deleted": resource_count, }); crate::audit::log::write_audit_event( &db, "mcp_server_deleted", "mcp_server", &id, &details.to_string(), ) .map_err(|e| format!("Audit log failed: {e}"))?; delete_server(&db, &id)?; } // Remove live connection if present let mut connections = state.mcp_connections.lock().await; connections.remove(&id); drop(connections); info!(server_id = %id, "MCP server deleted"); let _ = app_handle; Ok(()) } #[tauri::command] pub async fn toggle_mcp_server( id: String, enabled: bool, state: State<'_, AppState>, ) -> Result<(), String> { let db = state.db.lock().map_err(|e| e.to_string())?; toggle_server(&db, &id, enabled)?; Ok(()) } #[tauri::command] pub async fn discover_mcp_server( id: String, app_handle: tauri::AppHandle, state: State<'_, AppState>, ) -> Result { let server = { let db = state.db.lock().map_err(|e| e.to_string())?; get_server(&db, &id)?.ok_or_else(|| format!("Server {id} not found"))? }; match crate::mcp::discovery::discover_server(&server, &app_handle).await { Ok(conn) => { let mut connections = state.mcp_connections.lock().await; connections.insert(id.clone(), Arc::new(TokioMutex::new(conn))); drop(connections); let (tool_count, resource_count, last_discovered_at) = { let db = state.db.lock().map_err(|e| e.to_string())?; let tc = get_tool_count(&db, &id)?; let rc = get_resource_count(&db, &id)?; let srv = get_server(&db, &id)?.unwrap(); (tc, rc, srv.last_discovered_at) }; Ok(McpServerStatus { server_id: id, status: "connected".to_string(), error: None, tool_count, resource_count, last_discovered_at, }) } Err(e) => { { let db = state.db.lock().map_err(|db_err| db_err.to_string())?; update_discovery_status(&db, &id, "error", Some(&e))?; } Ok(McpServerStatus { server_id: id, status: "error".to_string(), error: Some(e), tool_count: 0, resource_count: 0, last_discovered_at: None, }) } } } #[tauri::command] pub async fn get_mcp_server_status( id: String, state: State<'_, AppState>, ) -> Result { let (server, tool_count, resource_count) = { let db = state.db.lock().map_err(|e| e.to_string())?; let srv = get_server(&db, &id)?.ok_or_else(|| format!("Server {id} not found"))?; let tc = get_tool_count(&db, &id)?; let rc = get_resource_count(&db, &id)?; (srv, tc, rc) }; Ok(McpServerStatus { server_id: id, status: server.discovery_status, error: server.discovery_error, tool_count, resource_count, last_discovered_at: server.last_discovered_at, }) } #[tauri::command] pub async fn initiate_mcp_oauth( id: String, state: State<'_, AppState>, app_handle: tauri::AppHandle, ) -> Result<(), String> { let server = { let db = state.db.lock().map_err(|e| e.to_string())?; get_server(&db, &id)?.ok_or_else(|| format!("Server {id} not found"))? }; if server.auth_type != "oauth2" { return Err(format!( "Server {} uses auth_type '{}', not oauth2", id, server.auth_type )); } let config: serde_json::Value = serde_json::from_str(&server.transport_config).unwrap_or_default(); let auth_endpoint = config .get("auth_endpoint") .and_then(|v| v.as_str()) .ok_or("OAuth2 transport_config missing 'auth_endpoint'")? .to_string(); let client_id = config .get("client_id") .and_then(|v| v.as_str()) .ok_or("OAuth2 transport_config missing 'client_id'")? .to_string(); let token_endpoint = config .get("token_endpoint") .and_then(|v| v.as_str()) .ok_or("OAuth2 transport_config missing 'token_endpoint'")? .to_string(); let redirect_uri = "http://localhost:12345/mcp-oauth-callback".to_string(); let scope = config .get("scope") .and_then(|v| v.as_str()) .unwrap_or("") .to_string(); let pkce = crate::integrations::auth::generate_pkce(); let base_auth_url = crate::integrations::auth::build_auth_url( &auth_endpoint, &client_id, &redirect_uri, &scope, &pkce, ); // Append a cryptographically random state nonce for CSRF protection let state_nonce = { use rand::RngCore; let mut bytes = [0u8; 16]; rand::rng().fill_bytes(&mut bytes); hex::encode(bytes) }; let auth_url = format!( "{}&state={}", base_auth_url, urlencoding::encode(&state_nonce) ); // Open WebView window for OAuth let window_label = format!("mcp-oauth-{id}"); let _ = tauri::WebviewWindowBuilder::new( &app_handle, &window_label, tauri::WebviewUrl::External( url::Url::parse(&auth_url).map_err(|e| format!("Invalid OAuth URL: {e}"))?, ), ) .title(format!("Authenticate: {}", server.name)) .inner_size(800.0, 700.0) .build() .map_err(|e| format!("Failed to open OAuth window: {e}"))?; // Monitor URL changes for the redirect callback // For now, return Ok and let the user copy the code manually // Full implementation would poll the webview URL warn!(server_id = %id, "OAuth2 WebView opened — token exchange not yet automated"); // Exchange code → token // In production this would be driven by webview URL monitoring (see integrations::webview_auth) // This stub allows the UI to open the browser without crashing. let _ = (token_endpoint, pkce); Ok(()) }