//! MCP server import from deep link //! //! Handles batch import of MCP server configurations via ccswitch:// URLs. use super::utils::decode_base64_param; use super::DeepLinkImportRequest; use crate::app_config::{McpApps, McpServer}; use crate::error::AppError; use crate::services::McpService; use crate::store::AppState; use serde::{Deserialize, Serialize}; use serde_json::Value; /// MCP import result #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct McpImportResult { /// Number of successfully imported MCP servers pub imported_count: usize, /// IDs of successfully imported MCP servers pub imported_ids: Vec, /// Failed imports with error messages pub failed: Vec, } /// MCP import error #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct McpImportError { /// MCP server ID pub id: String, /// Error message pub error: String, } /// Import MCP servers from deep link request /// /// This function handles batch import of MCP servers from standard MCP JSON format. /// If a server already exists, only the apps flags are merged (existing config preserved). pub fn import_mcp_from_deeplink( state: &AppState, request: DeepLinkImportRequest, ) -> Result { // Verify this is an MCP request if request.resource != "mcp" { return Err(AppError::InvalidInput(format!( "Expected mcp resource, got '{}'", request.resource ))); } // Extract and validate apps parameter let apps_str = request .apps .as_ref() .ok_or_else(|| AppError::InvalidInput("Missing 'apps' parameter for MCP".to_string()))?; // Parse apps into McpApps struct let target_apps = parse_mcp_apps(apps_str)?; // Extract config let config_b64 = request .config .as_ref() .ok_or_else(|| AppError::InvalidInput("Missing 'config' parameter for MCP".to_string()))?; // Decode Base64 config let decoded = decode_base64_param("config", config_b64)?; let config_str = String::from_utf8(decoded) .map_err(|e| AppError::InvalidInput(format!("Invalid UTF-8 in config: {e}")))?; // Parse JSON let config_json: Value = serde_json::from_str(&config_str) .map_err(|e| AppError::InvalidInput(format!("Invalid JSON in MCP config: {e}")))?; // Extract mcpServers object let mcp_servers = config_json .get("mcpServers") .and_then(|v| v.as_object()) .ok_or_else(|| { AppError::InvalidInput("MCP config must contain 'mcpServers' object".to_string()) })?; if mcp_servers.is_empty() { return Err(AppError::InvalidInput( "No MCP servers found in config".to_string(), )); } // Get existing servers to check for duplicates let existing_servers = state.db.get_all_mcp_servers()?; // Import each MCP server let mut imported_ids = Vec::new(); let mut failed = Vec::new(); for (id, server_spec) in mcp_servers.iter() { // Check if server already exists let server = if let Some(existing) = existing_servers.get(id) { // Server exists - merge apps only, keep other fields unchanged log::info!("MCP server '{id}' already exists, merging apps only"); let mut merged_apps = existing.apps.clone(); // Merge new apps into existing apps if target_apps.claude { merged_apps.claude = true; } if target_apps.codex { merged_apps.codex = true; } if target_apps.gemini { merged_apps.gemini = true; } McpServer { id: existing.id.clone(), name: existing.name.clone(), server: existing.server.clone(), // Keep existing server config apps: merged_apps, // Merged apps description: existing.description.clone(), homepage: existing.homepage.clone(), docs: existing.docs.clone(), tags: existing.tags.clone(), } } else { // New server - create with provided config log::info!("Creating new MCP server: {id}"); McpServer { id: id.clone(), name: id.clone(), server: server_spec.clone(), apps: target_apps.clone(), description: None, homepage: None, docs: None, tags: vec!["imported".to_string()], } }; match McpService::upsert_server(state, server) { Ok(_) => { imported_ids.push(id.clone()); log::info!("Successfully imported/updated MCP server: {id}"); } Err(e) => { failed.push(McpImportError { id: id.clone(), error: format!("{e}"), }); log::warn!("Failed to import MCP server '{id}': {e}"); } } } Ok(McpImportResult { imported_count: imported_ids.len(), imported_ids, failed, }) } /// Parse apps string into McpApps struct pub(crate) fn parse_mcp_apps(apps_str: &str) -> Result { let mut apps = McpApps { claude: false, codex: false, gemini: false, }; for app in apps_str.split(',') { match app.trim() { "claude" => apps.claude = true, "codex" => apps.codex = true, "gemini" => apps.gemini = true, other => { return Err(AppError::InvalidInput(format!( "Invalid app in 'apps': {other}" ))) } } } if apps.is_empty() { return Err(AppError::InvalidInput( "At least one app must be specified in 'apps'".to_string(), )); } Ok(apps) }