use axum::{ body::Body, extract::State, http::{header, Request, StatusCode}, middleware::{self, Next}, response::{IntoResponse, Response}, routing::{get, post}, Json, Router, }; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::net::SocketAddr; use std::sync::atomic::{AtomicBool, AtomicU16, Ordering}; use std::sync::Arc; use tauri::AppHandle; use tokio::net::TcpListener; use tokio::sync::Mutex as AsyncMutex; use uuid::Uuid; use crate::browser::ProxySettings; use crate::cloud_auth::CLOUD_AUTH; use crate::group_manager::GROUP_MANAGER; use crate::profile::{BrowserProfile, ProfileManager}; use crate::proxy_manager::PROXY_MANAGER; use crate::settings_manager::SettingsManager; use crate::wayfern_terms::WayfernTermsManager; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct McpTool { pub name: String, pub description: String, pub input_schema: serde_json::Value, } #[derive(Debug, Deserialize)] #[allow(dead_code)] pub struct McpRequest { jsonrpc: String, id: Option, method: String, params: Option, } const PROTOCOL_VERSION: &str = "2025-03-26"; const SERVER_NAME: &str = "donut-browser"; const SERVER_VERSION: &str = env!("CARGO_PKG_VERSION"); #[derive(Debug, Serialize)] pub struct McpResponse { jsonrpc: String, #[serde(skip_serializing_if = "Option::is_none")] id: Option, #[serde(skip_serializing_if = "Option::is_none")] result: Option, #[serde(skip_serializing_if = "Option::is_none")] error: Option, } #[derive(Debug, Serialize)] pub struct McpError { code: i32, message: String, } const DEFAULT_MCP_PORT: u16 = 51080; struct McpSession { initialized: bool, } struct McpServerInner { app_handle: Option, token: Option, shutdown_tx: Option>, sessions: HashMap, } #[derive(Clone)] struct McpHttpState { server: &'static McpServer, token: String, } pub struct McpServer { inner: Arc>, is_running: AtomicBool, port: AtomicU16, } impl McpServer { fn new() -> Self { Self { inner: Arc::new(AsyncMutex::new(McpServerInner { app_handle: None, token: None, shutdown_tx: None, sessions: HashMap::new(), })), is_running: AtomicBool::new(false), port: AtomicU16::new(0), } } pub fn instance() -> &'static McpServer { &MCP_SERVER } pub fn is_running(&self) -> bool { self.is_running.load(Ordering::SeqCst) } async fn require_paid_subscription(feature: &str) -> Result<(), McpError> { if !CLOUD_AUTH.has_active_paid_subscription().await { return Err(McpError { code: -32000, message: format!("{feature} requires an active paid subscription"), }); } Ok(()) } pub fn get_port(&self) -> Option { let port = self.port.load(Ordering::SeqCst); if port > 0 { Some(port) } else { None } } pub async fn start(&self, app_handle: AppHandle) -> Result { if !WayfernTermsManager::instance().is_terms_accepted() { return Err( "Wayfern Terms and Conditions must be accepted before starting MCP server".to_string(), ); } if self.is_running() { return Err("MCP server is already running".to_string()); } let settings_manager = SettingsManager::instance(); let settings = settings_manager .load_settings() .map_err(|e| format!("Failed to load settings: {e}"))?; // Get or generate token let existing_token = settings_manager .get_mcp_token(&app_handle) .await .ok() .flatten(); let token = if let Some(t) = existing_token { t } else { settings_manager .generate_mcp_token(&app_handle) .await .map_err(|e| format!("Failed to generate MCP token: {e}"))? }; // Determine port (use saved port, or try default, or random) let preferred_port = settings.mcp_port.unwrap_or(DEFAULT_MCP_PORT); let actual_port = self.bind_to_available_port(preferred_port).await?; // Save port if it changed if settings.mcp_port != Some(actual_port) { let mut new_settings = settings; new_settings.mcp_port = Some(actual_port); settings_manager .save_settings(&new_settings) .map_err(|e| format!("Failed to save settings: {e}"))?; } // Store state let mut inner = self.inner.lock().await; inner.app_handle = Some(app_handle); inner.token = Some(token.clone()); // Create shutdown channel let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>(); inner.shutdown_tx = Some(shutdown_tx); self.port.store(actual_port, Ordering::SeqCst); self.is_running.store(true, Ordering::SeqCst); // Start HTTP server in background let http_state = McpHttpState { server: McpServer::instance(), token, }; tokio::spawn(Self::run_http_server(actual_port, http_state, shutdown_rx)); log::info!("[mcp] Server started on port {}", actual_port); Ok(actual_port) } async fn bind_to_available_port(&self, preferred: u16) -> Result { let addr = SocketAddr::from(([127, 0, 0, 1], preferred)); if TcpListener::bind(addr).await.is_ok() { return Ok(preferred); } // Try random ports in 51000-51999 range for _ in 0..10 { let port = 51000 + (rand::random::() % 1000); let addr = SocketAddr::from(([127, 0, 0, 1], port)); if TcpListener::bind(addr).await.is_ok() { return Ok(port); } } Err("Could not find available port for MCP server".to_string()) } async fn run_http_server( port: u16, state: McpHttpState, shutdown_rx: tokio::sync::oneshot::Receiver<()>, ) { let app = Router::new() .route( "/mcp", post(Self::handle_mcp_post) .get(Self::handle_mcp_get) .delete(Self::handle_mcp_delete), ) .route("/health", get(Self::handle_health)) .layer(middleware::from_fn_with_state( state.clone(), Self::auth_middleware, )) .with_state(state); let addr = SocketAddr::from(([127, 0, 0, 1], port)); let listener = match TcpListener::bind(addr).await { Ok(l) => l, Err(e) => { log::error!("[mcp] Failed to bind to port {}: {}", port, e); return; } }; log::info!( "[mcp] HTTP server listening on http://127.0.0.1:{}/mcp", port ); let server = axum::serve(listener, app).with_graceful_shutdown(async { let _ = shutdown_rx.await; log::info!("[mcp] HTTP server shutting down"); }); if let Err(e) = server.await { log::error!("[mcp] HTTP server error: {}", e); } } async fn auth_middleware( State(state): State, req: Request, next: Next, ) -> Result { // Health endpoint is public if req.uri().path() == "/health" { return Ok(next.run(req).await); } let auth_header = req .headers() .get(header::AUTHORIZATION) .and_then(|h| h.to_str().ok()); let token = auth_header.and_then(|h| h.strip_prefix("Bearer ")); if token != Some(&state.token) { return Err(StatusCode::UNAUTHORIZED); } Ok(next.run(req).await) } async fn handle_health() -> impl IntoResponse { Json(serde_json::json!({ "status": "ok", "server": SERVER_NAME, "version": SERVER_VERSION, "protocolVersion": PROTOCOL_VERSION, })) } async fn handle_mcp_get() -> impl IntoResponse { // We don't support server-initiated SSE streams StatusCode::METHOD_NOT_ALLOWED } async fn handle_mcp_delete( State(state): State, req: Request, ) -> impl IntoResponse { let session_id = req .headers() .get("mcp-session-id") .and_then(|h| h.to_str().ok()) .map(|s| s.to_string()); if let Some(sid) = session_id { let mut inner = state.server.inner.lock().await; inner.sessions.remove(&sid); log::info!("[mcp] Session terminated: {}", sid); } StatusCode::OK } async fn handle_mcp_post(State(state): State, req: Request) -> Response { let session_id = req .headers() .get("mcp-session-id") .and_then(|h| h.to_str().ok()) .map(|s| s.to_string()); let body_bytes = match axum::body::to_bytes(req.into_body(), 1024 * 1024).await { Ok(b) => b, Err(_) => { return (StatusCode::BAD_REQUEST, "Invalid request body").into_response(); } }; let request: McpRequest = match serde_json::from_slice(&body_bytes) { Ok(r) => r, Err(_) => { return (StatusCode::BAD_REQUEST, "Invalid JSON").into_response(); } }; let is_notification = request.id.is_none(); let method = request.method.clone(); // Handle initialize (no session required) if method == "initialize" { let response = state.server.handle_initialize(request).await; match response { Ok((session_id, result)) => { let body = McpResponse { jsonrpc: "2.0".to_string(), id: Some(result.0), result: Some(result.1), error: None, }; Response::builder() .status(StatusCode::OK) .header(header::CONTENT_TYPE, "application/json") .header("mcp-session-id", &session_id) .body(Body::from(serde_json::to_vec(&body).unwrap())) .unwrap() } Err((id, error)) => { let body = McpResponse { jsonrpc: "2.0".to_string(), id: Some(id), result: None, error: Some(error), }; Json(body).into_response() } } } else if is_notification { // Notifications (like notifications/initialized) -> 202 Accepted if method == "notifications/initialized" { if let Some(sid) = &session_id { let mut inner = state.server.inner.lock().await; if let Some(session) = inner.sessions.get_mut(sid) { session.initialized = true; } } } StatusCode::ACCEPTED.into_response() } else { // Validate session exists if let Some(sid) = &session_id { let inner = state.server.inner.lock().await; if !inner.sessions.contains_key(sid) { return StatusCode::NOT_FOUND.into_response(); } } let response = state.server.handle_request(request).await; Json(response).into_response() } } pub async fn stop(&self) -> Result<(), String> { if !self.is_running() { return Err("MCP server is not running".to_string()); } let mut inner = self.inner.lock().await; inner.app_handle = None; inner.token = None; inner.sessions.clear(); // Send shutdown signal if let Some(tx) = inner.shutdown_tx.take() { let _ = tx.send(()); } self.port.store(0, Ordering::SeqCst); self.is_running.store(false, Ordering::SeqCst); log::info!("[mcp] Server stopped"); Ok(()) } pub fn get_tools(&self) -> Vec { vec![ McpTool { name: "list_profiles".to_string(), description: "List all Wayfern and Camoufox browser profiles".to_string(), input_schema: serde_json::json!({ "type": "object", "properties": {}, "required": [] }), }, McpTool { name: "get_profile".to_string(), description: "Get details of a specific browser profile".to_string(), input_schema: serde_json::json!({ "type": "object", "properties": { "profile_id": { "type": "string", "description": "The UUID of the profile to retrieve" } }, "required": ["profile_id"] }), }, McpTool { name: "run_profile".to_string(), description: "Launch a browser profile with an optional URL".to_string(), input_schema: serde_json::json!({ "type": "object", "properties": { "profile_id": { "type": "string", "description": "The UUID of the profile to launch" }, "url": { "type": "string", "description": "Optional URL to open in the browser" }, "headless": { "type": "boolean", "description": "Run the browser in headless mode" } }, "required": ["profile_id"] }), }, McpTool { name: "kill_profile".to_string(), description: "Stop a running browser profile".to_string(), input_schema: serde_json::json!({ "type": "object", "properties": { "profile_id": { "type": "string", "description": "The UUID of the profile to stop" } }, "required": ["profile_id"] }), }, McpTool { name: "create_profile".to_string(), description: "Create a new browser profile".to_string(), input_schema: serde_json::json!({ "type": "object", "properties": { "name": { "type": "string", "description": "Name for the new profile" }, "browser": { "type": "string", "enum": ["wayfern", "camoufox"], "description": "Browser engine to use" }, "proxy_id": { "type": "string", "description": "Optional proxy UUID to assign" }, "group_id": { "type": "string", "description": "Optional group UUID to assign" }, "tags": { "type": "array", "items": { "type": "string" }, "description": "Optional tags for the profile" } }, "required": ["name", "browser"] }), }, McpTool { name: "update_profile".to_string(), description: "Update an existing browser profile's settings".to_string(), input_schema: serde_json::json!({ "type": "object", "properties": { "profile_id": { "type": "string", "description": "The UUID of the profile to update" }, "name": { "type": "string", "description": "New name for the profile" }, "proxy_id": { "type": "string", "description": "Proxy UUID to assign (empty string to remove)" }, "group_id": { "type": "string", "description": "Group UUID to assign (empty string to remove)" }, "tags": { "type": "array", "items": { "type": "string" }, "description": "Tags for the profile (replaces existing tags)" }, "extension_group_id": { "type": "string", "description": "Extension group UUID to assign (empty string to remove)" }, "proxy_bypass_rules": { "type": "array", "items": { "type": "string" }, "description": "Proxy bypass rules (replaces existing rules)" } }, "required": ["profile_id"] }), }, McpTool { name: "delete_profile".to_string(), description: "Delete a browser profile and all its data".to_string(), input_schema: serde_json::json!({ "type": "object", "properties": { "profile_id": { "type": "string", "description": "The UUID of the profile to delete" } }, "required": ["profile_id"] }), }, McpTool { name: "list_tags".to_string(), description: "List all tags used across profiles".to_string(), input_schema: serde_json::json!({ "type": "object", "properties": {}, "required": [] }), }, McpTool { name: "list_proxies".to_string(), description: "List all configured proxies".to_string(), input_schema: serde_json::json!({ "type": "object", "properties": {}, "required": [] }), }, McpTool { name: "get_profile_status".to_string(), description: "Check if a browser profile is currently running".to_string(), input_schema: serde_json::json!({ "type": "object", "properties": { "profile_id": { "type": "string", "description": "The UUID of the profile to check" } }, "required": ["profile_id"] }), }, // Group management tools McpTool { name: "list_groups".to_string(), description: "List all profile groups".to_string(), input_schema: serde_json::json!({ "type": "object", "properties": {}, "required": [] }), }, McpTool { name: "get_group".to_string(), description: "Get details of a specific group".to_string(), input_schema: serde_json::json!({ "type": "object", "properties": { "group_id": { "type": "string", "description": "The UUID of the group to retrieve" } }, "required": ["group_id"] }), }, McpTool { name: "create_group".to_string(), description: "Create a new profile group".to_string(), input_schema: serde_json::json!({ "type": "object", "properties": { "name": { "type": "string", "description": "The name for the new group" } }, "required": ["name"] }), }, McpTool { name: "update_group".to_string(), description: "Update an existing group's name".to_string(), input_schema: serde_json::json!({ "type": "object", "properties": { "group_id": { "type": "string", "description": "The UUID of the group to update" }, "name": { "type": "string", "description": "The new name for the group" } }, "required": ["group_id", "name"] }), }, McpTool { name: "delete_group".to_string(), description: "Delete a profile group".to_string(), input_schema: serde_json::json!({ "type": "object", "properties": { "group_id": { "type": "string", "description": "The UUID of the group to delete" } }, "required": ["group_id"] }), }, McpTool { name: "assign_profiles_to_group".to_string(), description: "Assign one or more profiles to a group".to_string(), input_schema: serde_json::json!({ "type": "object", "properties": { "profile_ids": { "type": "array", "items": { "type": "string" }, "description": "Array of profile UUIDs to assign" }, "group_id": { "type": "string", "description": "The UUID of the group to assign to (null to remove from group)" } }, "required": ["profile_ids"] }), }, // Full proxy management tools McpTool { name: "get_proxy".to_string(), description: "Get details of a specific proxy".to_string(), input_schema: serde_json::json!({ "type": "object", "properties": { "proxy_id": { "type": "string", "description": "The UUID of the proxy to retrieve" } }, "required": ["proxy_id"] }), }, McpTool { name: "create_proxy".to_string(), description: "Create a new proxy configuration. For regular proxies, provide proxy_type/host/port. For dynamic proxies, provide dynamic_proxy_url and dynamic_proxy_format instead.".to_string(), input_schema: serde_json::json!({ "type": "object", "properties": { "name": { "type": "string", "description": "The name for the new proxy" }, "proxy_type": { "type": "string", "enum": ["http", "https", "socks4", "socks5"], "description": "The type of proxy (for regular proxies)" }, "host": { "type": "string", "description": "The proxy host address (for regular proxies)" }, "port": { "type": "integer", "description": "The proxy port number (for regular proxies)" }, "username": { "type": "string", "description": "Optional username for authentication (for regular proxies)" }, "password": { "type": "string", "description": "Optional password for authentication (for regular proxies)" }, "dynamic_proxy_url": { "type": "string", "description": "URL to fetch proxy settings from (for dynamic proxies)" }, "dynamic_proxy_format": { "type": "string", "enum": ["json", "text"], "description": "Format of the dynamic proxy response: 'json' for JSON object or 'text' for text like host:port:user:pass (for dynamic proxies)" } }, "required": ["name"] }), }, McpTool { name: "update_proxy".to_string(), description: "Update an existing proxy configuration".to_string(), input_schema: serde_json::json!({ "type": "object", "properties": { "proxy_id": { "type": "string", "description": "The UUID of the proxy to update" }, "name": { "type": "string", "description": "New name for the proxy" }, "proxy_type": { "type": "string", "enum": ["http", "https", "socks4", "socks5"], "description": "The type of proxy (for regular proxies)" }, "host": { "type": "string", "description": "The proxy host address (for regular proxies)" }, "port": { "type": "integer", "description": "The proxy port number (for regular proxies)" }, "username": { "type": "string", "description": "Optional username for authentication (for regular proxies)" }, "password": { "type": "string", "description": "Optional password for authentication (for regular proxies)" }, "dynamic_proxy_url": { "type": "string", "description": "URL to fetch proxy settings from (for dynamic proxies)" }, "dynamic_proxy_format": { "type": "string", "enum": ["json", "text"], "description": "Format of the dynamic proxy response (for dynamic proxies)" } }, "required": ["proxy_id"] }), }, McpTool { name: "delete_proxy".to_string(), description: "Delete a proxy configuration".to_string(), input_schema: serde_json::json!({ "type": "object", "properties": { "proxy_id": { "type": "string", "description": "The UUID of the proxy to delete" } }, "required": ["proxy_id"] }), }, McpTool { name: "export_proxies".to_string(), description: "Export all proxy configurations".to_string(), input_schema: serde_json::json!({ "type": "object", "properties": { "format": { "type": "string", "enum": ["json", "txt"], "description": "Export format (json for structured data, txt for URL format)" } }, "required": ["format"] }), }, McpTool { name: "import_proxies".to_string(), description: "Import proxy configurations from JSON or TXT content".to_string(), input_schema: serde_json::json!({ "type": "object", "properties": { "content": { "type": "string", "description": "The proxy configuration content to import" }, "format": { "type": "string", "enum": ["json", "txt"], "description": "Import format (json or txt)" }, "name_prefix": { "type": "string", "description": "Optional prefix for imported proxy names (default: 'Imported')" } }, "required": ["content", "format"] }), }, // VPN management tools McpTool { name: "import_vpn".to_string(), description: "Import a WireGuard (.conf) or OpenVPN (.ovpn) configuration".to_string(), input_schema: serde_json::json!({ "type": "object", "properties": { "content": { "type": "string", "description": "Raw VPN config file content" }, "filename": { "type": "string", "description": "Original filename (.conf or .ovpn) for type detection" }, "name": { "type": "string", "description": "Optional display name for the VPN config" } }, "required": ["content", "filename"] }), }, McpTool { name: "list_vpn_configs".to_string(), description: "List all stored VPN configurations".to_string(), input_schema: serde_json::json!({ "type": "object", "properties": {}, "required": [] }), }, McpTool { name: "delete_vpn".to_string(), description: "Delete a VPN configuration".to_string(), input_schema: serde_json::json!({ "type": "object", "properties": { "vpn_id": { "type": "string", "description": "The UUID of the VPN config to delete" } }, "required": ["vpn_id"] }), }, McpTool { name: "connect_vpn".to_string(), description: "Connect to a VPN configuration".to_string(), input_schema: serde_json::json!({ "type": "object", "properties": { "vpn_id": { "type": "string", "description": "The UUID of the VPN config to connect" } }, "required": ["vpn_id"] }), }, McpTool { name: "disconnect_vpn".to_string(), description: "Disconnect from a VPN".to_string(), input_schema: serde_json::json!({ "type": "object", "properties": { "vpn_id": { "type": "string", "description": "The UUID of the VPN to disconnect" } }, "required": ["vpn_id"] }), }, McpTool { name: "get_vpn_status".to_string(), description: "Get the connection status of a VPN".to_string(), input_schema: serde_json::json!({ "type": "object", "properties": { "vpn_id": { "type": "string", "description": "The UUID of the VPN to check" } }, "required": ["vpn_id"] }), }, // Fingerprint management tools McpTool { name: "get_profile_fingerprint".to_string(), description: "Get the fingerprint configuration for a Wayfern or Camoufox profile" .to_string(), input_schema: serde_json::json!({ "type": "object", "properties": { "profile_id": { "type": "string", "description": "The UUID of the profile" } }, "required": ["profile_id"] }), }, McpTool { name: "update_profile_fingerprint".to_string(), description: "Update the fingerprint configuration for a Wayfern or Camoufox profile. Requires an active Pro subscription." .to_string(), input_schema: serde_json::json!({ "type": "object", "properties": { "profile_id": { "type": "string", "description": "The UUID of the profile to update" }, "fingerprint": { "type": "string", "description": "JSON string of the fingerprint configuration, or null to clear" }, "os": { "type": "string", "enum": ["windows", "macos", "linux"], "description": "Operating system for fingerprint generation" }, "randomize_fingerprint_on_launch": { "type": "boolean", "description": "Whether to generate a new fingerprint on every launch" } }, "required": ["profile_id"] }), }, McpTool { name: "update_profile_proxy_bypass_rules".to_string(), description: "Update proxy bypass rules for a profile. Requests matching these rules will connect directly, bypassing the proxy." .to_string(), input_schema: serde_json::json!({ "type": "object", "properties": { "profile_id": { "type": "string", "description": "The UUID of the profile to update" }, "rules": { "type": "array", "items": { "type": "string" }, "description": "Array of bypass rules. Supports hostnames (e.g. 'example.com'), IP addresses, and regex patterns." } }, "required": ["profile_id", "rules"] }), }, McpTool { name: "list_extensions".to_string(), description: "List all managed browser extensions. Requires Pro subscription.".to_string(), input_schema: serde_json::json!({ "type": "object", "properties": {}, "required": [] }), }, McpTool { name: "list_extension_groups".to_string(), description: "List all extension groups. Requires Pro subscription.".to_string(), input_schema: serde_json::json!({ "type": "object", "properties": {}, "required": [] }), }, McpTool { name: "create_extension_group".to_string(), description: "Create a new extension group. Requires Pro subscription.".to_string(), input_schema: serde_json::json!({ "type": "object", "properties": { "name": { "type": "string", "description": "Name for the extension group" } }, "required": ["name"] }), }, McpTool { name: "delete_extension".to_string(), description: "Delete a managed extension. Requires Pro subscription.".to_string(), input_schema: serde_json::json!({ "type": "object", "properties": { "extension_id": { "type": "string", "description": "The extension ID to delete" } }, "required": ["extension_id"] }), }, McpTool { name: "delete_extension_group".to_string(), description: "Delete an extension group. Requires Pro subscription.".to_string(), input_schema: serde_json::json!({ "type": "object", "properties": { "group_id": { "type": "string", "description": "The extension group ID to delete" } }, "required": ["group_id"] }), }, McpTool { name: "assign_extension_group_to_profile".to_string(), description: "Assign an extension group to a profile, or remove the assignment. Requires Pro subscription.".to_string(), input_schema: serde_json::json!({ "type": "object", "properties": { "profile_id": { "type": "string", "description": "The profile ID" }, "extension_group_id": { "type": "string", "description": "The extension group ID, or empty string to remove" } }, "required": ["profile_id"] }), }, // Team lock tools McpTool { name: "get_team_locks".to_string(), description: "List all active team profile locks. Requires team plan.".to_string(), input_schema: serde_json::json!({ "type": "object", "properties": {}, "required": [] }), }, McpTool { name: "get_team_lock_status".to_string(), description: "Check if a profile is locked by a team member. Requires team plan.".to_string(), input_schema: serde_json::json!({ "type": "object", "properties": { "profile_id": { "type": "string", "description": "The UUID of the profile to check" } }, "required": ["profile_id"] }), }, // Synchronizer tools McpTool { name: "start_sync_session".to_string(), description: "Start a synchronizer session. Launches a leader profile and follower profiles, then mirrors all actions from the leader to the followers in real time. Only Wayfern profiles are supported. Requires paid subscription.".to_string(), input_schema: serde_json::json!({ "type": "object", "properties": { "leader_profile_id": { "type": "string", "description": "The UUID of the leader profile" }, "follower_profile_ids": { "type": "array", "items": { "type": "string" }, "description": "UUIDs of follower profiles" } }, "required": ["leader_profile_id", "follower_profile_ids"] }), }, McpTool { name: "stop_sync_session".to_string(), description: "Stop an active synchronizer session. Kills all follower profiles and the leader.".to_string(), input_schema: serde_json::json!({ "type": "object", "properties": { "session_id": { "type": "string", "description": "The sync session ID" } }, "required": ["session_id"] }), }, McpTool { name: "get_sync_sessions".to_string(), description: "List all active synchronizer sessions.".to_string(), input_schema: serde_json::json!({ "type": "object", "properties": {} }), }, McpTool { name: "remove_sync_follower".to_string(), description: "Remove a follower from an active synchronizer session.".to_string(), input_schema: serde_json::json!({ "type": "object", "properties": { "session_id": { "type": "string", "description": "The sync session ID" }, "follower_profile_id": { "type": "string", "description": "The UUID of the follower to remove" } }, "required": ["session_id", "follower_profile_id"] }), }, // Browser interaction tools McpTool { name: "navigate".to_string(), description: "Navigate a running browser profile to a URL. Waits for the page to fully load before returning.".to_string(), input_schema: serde_json::json!({ "type": "object", "properties": { "profile_id": { "type": "string", "description": "The UUID of the running profile" }, "url": { "type": "string", "description": "The URL to navigate to" } }, "required": ["profile_id", "url"] }), }, McpTool { name: "screenshot".to_string(), description: "Take a screenshot of the current page in a running browser profile. Returns base64-encoded image." .to_string(), input_schema: serde_json::json!({ "type": "object", "properties": { "profile_id": { "type": "string", "description": "The UUID of the running profile" }, "format": { "type": "string", "enum": ["png", "jpeg", "webp"], "description": "Image format (default: png)" }, "quality": { "type": "integer", "description": "Image quality 0-100 for jpeg/webp (default: 80)" }, "full_page": { "type": "boolean", "description": "Capture the full scrollable page (default: false)" } }, "required": ["profile_id"] }), }, McpTool { name: "evaluate_javascript".to_string(), description: "Execute JavaScript in the context of the current page and return the result. Works with both static and dynamically-generated content. Set wait_for_load=true if the script triggers navigation (e.g., form.submit())." .to_string(), input_schema: serde_json::json!({ "type": "object", "properties": { "profile_id": { "type": "string", "description": "The UUID of the running profile" }, "expression": { "type": "string", "description": "JavaScript expression to evaluate" }, "await_promise": { "type": "boolean", "description": "Whether to await the result if it's a Promise (default: false)" }, "wait_for_load": { "type": "boolean", "description": "Wait for page load after execution, use when the script triggers navigation like form.submit() (default: false)" } }, "required": ["profile_id", "expression"] }), }, McpTool { name: "click_element".to_string(), description: "Click on an element identified by a CSS selector. If the click triggers a page navigation, waits for the new page to load before returning.".to_string(), input_schema: serde_json::json!({ "type": "object", "properties": { "profile_id": { "type": "string", "description": "The UUID of the running profile" }, "selector": { "type": "string", "description": "CSS selector for the element to click" } }, "required": ["profile_id", "selector"] }), }, McpTool { name: "type_text".to_string(), description: "Focus an element by CSS selector and type text into it. By default uses realistic human-like typing with variable speed, natural errors, and self-corrections. Only set instant=true when you are certain the target does not have bot detection (e.g. browser address bars, developer tools, internal apps) — using instant on public websites risks the profile being flagged as a bot.".to_string(), input_schema: serde_json::json!({ "type": "object", "properties": { "profile_id": { "type": "string", "description": "The UUID of the running profile" }, "selector": { "type": "string", "description": "CSS selector for the input element" }, "text": { "type": "string", "description": "Text to type into the element" }, "clear_first": { "type": "boolean", "description": "Clear the input before typing (default: true)" }, "instant": { "type": "boolean", "description": "Paste all text at once instead of human typing. WARNING: only use on targets without bot detection — using this on public websites risks the profile being flagged." }, "wpm": { "type": "number", "description": "Target words per minute for human typing (default: 80)" } }, "required": ["profile_id", "selector", "text"] }), }, McpTool { name: "get_page_content".to_string(), description: "Get the content of the current page. Works with both static HTML and JavaScript-rendered content." .to_string(), input_schema: serde_json::json!({ "type": "object", "properties": { "profile_id": { "type": "string", "description": "The UUID of the running profile" }, "format": { "type": "string", "enum": ["html", "text"], "description": "Content format: 'html' for full HTML, 'text' for visible text only (default: text)" }, "selector": { "type": "string", "description": "Optional CSS selector to get content of a specific element instead of the whole page" } }, "required": ["profile_id"] }), }, McpTool { name: "get_page_info".to_string(), description: "Get metadata about the current page including URL, title, and readiness state" .to_string(), input_schema: serde_json::json!({ "type": "object", "properties": { "profile_id": { "type": "string", "description": "The UUID of the running profile" } }, "required": ["profile_id"] }), }, ] } async fn handle_initialize( &self, request: McpRequest, ) -> Result<(String, (serde_json::Value, serde_json::Value)), (serde_json::Value, McpError)> { let id = request.id.clone().unwrap_or(serde_json::Value::Null); if !self.is_running() { return Err(( id, McpError { code: -32001, message: "MCP server is not running".to_string(), }, )); } // Create session let session_id = Uuid::new_v4().to_string(); { let mut inner = self.inner.lock().await; inner .sessions .insert(session_id.clone(), McpSession { initialized: false }); } let result = serde_json::json!({ "protocolVersion": PROTOCOL_VERSION, "capabilities": { "tools": { "listChanged": false } }, "serverInfo": { "name": SERVER_NAME, "version": SERVER_VERSION, }, "instructions": "Donut Browser MCP server. Use tools/list to discover available browser automation tools." }); log::info!("[mcp] New session initialized: {}", session_id); Ok((session_id, (id, result))) } pub async fn handle_request(&self, request: McpRequest) -> McpResponse { let id = request.id.clone().unwrap_or(serde_json::Value::Null); if !self.is_running() { return McpResponse { jsonrpc: "2.0".to_string(), id: Some(id), result: None, error: Some(McpError { code: -32001, message: "MCP server is not running".to_string(), }), }; } let result = match request.method.as_str() { "ping" => Ok(serde_json::json!({})), "tools/list" => self.handle_tools_list().await, "tools/call" => self.handle_tool_call(request.params).await, _ => Err(McpError { code: -32601, message: format!("Method not found: {}", request.method), }), }; match result { Ok(value) => McpResponse { jsonrpc: "2.0".to_string(), id: Some(id), result: Some(value), error: None, }, Err(error) => McpResponse { jsonrpc: "2.0".to_string(), id: Some(id), result: None, error: Some(error), }, } } async fn handle_tools_list(&self) -> Result { Ok(serde_json::json!({ "tools": self.get_tools() })) } async fn handle_tool_call( &self, params: Option, ) -> Result { let params = params.ok_or_else(|| McpError { code: -32602, message: "Missing parameters".to_string(), })?; let tool_name = params .get("name") .and_then(|v| v.as_str()) .ok_or_else(|| McpError { code: -32602, message: "Missing tool name".to_string(), })?; let arguments = params .get("arguments") .cloned() .unwrap_or(serde_json::json!({})); match tool_name { "list_profiles" => self.handle_list_profiles().await, "get_profile" => self.handle_get_profile(&arguments).await, "run_profile" => { Self::require_paid_subscription("Browser automation").await?; self.handle_run_profile(&arguments).await } "kill_profile" => self.handle_kill_profile(&arguments).await, "create_profile" => self.handle_create_profile(&arguments).await, "update_profile" => self.handle_update_profile(&arguments).await, "delete_profile" => self.handle_delete_profile(&arguments).await, "list_tags" => self.handle_list_tags().await, "list_proxies" => self.handle_list_proxies().await, "get_profile_status" => self.handle_get_profile_status(&arguments).await, // Group management "list_groups" => self.handle_list_groups().await, "get_group" => self.handle_get_group(&arguments).await, "create_group" => self.handle_create_group(&arguments).await, "update_group" => self.handle_update_group(&arguments).await, "delete_group" => self.handle_delete_group(&arguments).await, "assign_profiles_to_group" => self.handle_assign_profiles_to_group(&arguments).await, // Full proxy management "get_proxy" => self.handle_get_proxy(&arguments).await, "create_proxy" => self.handle_create_proxy(&arguments).await, "update_proxy" => self.handle_update_proxy(&arguments).await, "delete_proxy" => self.handle_delete_proxy(&arguments).await, // Proxy import/export "export_proxies" => self.handle_export_proxies(&arguments).await, "import_proxies" => self.handle_import_proxies(&arguments).await, // VPN management "import_vpn" => self.handle_import_vpn(&arguments).await, "list_vpn_configs" => self.handle_list_vpn_configs().await, "delete_vpn" => self.handle_delete_vpn(&arguments).await, "connect_vpn" => self.handle_connect_vpn(&arguments).await, "disconnect_vpn" => self.handle_disconnect_vpn(&arguments).await, "get_vpn_status" => self.handle_get_vpn_status(&arguments).await, // Fingerprint management "get_profile_fingerprint" => self.handle_get_profile_fingerprint(&arguments).await, "update_profile_fingerprint" => self.handle_update_profile_fingerprint(&arguments).await, "update_profile_proxy_bypass_rules" => { self .handle_update_profile_proxy_bypass_rules(&arguments) .await } // Extension management "list_extensions" => self.handle_list_extensions().await, "list_extension_groups" => self.handle_list_extension_groups().await, "create_extension_group" => self.handle_create_extension_group(&arguments).await, "delete_extension" => self.handle_delete_extension_mcp(&arguments).await, "delete_extension_group" => self.handle_delete_extension_group_mcp(&arguments).await, "assign_extension_group_to_profile" => { self .handle_assign_extension_group_to_profile(&arguments) .await } // Team lock tools "get_team_locks" => self.handle_get_team_locks().await, "get_team_lock_status" => self.handle_get_team_lock_status(&arguments).await, // Synchronizer tools "start_sync_session" => { Self::require_paid_subscription("Synchronizer").await?; self.handle_start_sync_session(&arguments).await } "stop_sync_session" => self.handle_stop_sync_session(&arguments).await, "get_sync_sessions" => self.handle_get_sync_sessions().await, "remove_sync_follower" => self.handle_remove_sync_follower(&arguments).await, // Browser interaction tools (require paid subscription) "navigate" => { Self::require_paid_subscription("Browser automation").await?; self.handle_navigate(&arguments).await } "screenshot" => { Self::require_paid_subscription("Browser automation").await?; self.handle_screenshot(&arguments).await } "evaluate_javascript" => { Self::require_paid_subscription("Browser automation").await?; self.handle_evaluate_javascript(&arguments).await } "click_element" => { Self::require_paid_subscription("Browser automation").await?; self.handle_click_element(&arguments).await } "type_text" => { Self::require_paid_subscription("Browser automation").await?; self.handle_type_text(&arguments).await } "get_page_content" => { Self::require_paid_subscription("Browser automation").await?; self.handle_get_page_content(&arguments).await } "get_page_info" => { Self::require_paid_subscription("Browser automation").await?; self.handle_get_page_info(&arguments).await } _ => Err(McpError { code: -32602, message: format!("Unknown tool: {tool_name}"), }), } } async fn handle_list_profiles(&self) -> Result { let profiles = ProfileManager::instance() .list_profiles() .map_err(|e| McpError { code: -32000, message: format!("Failed to list profiles: {e}"), })?; // Filter to only Wayfern and Camoufox profiles let filtered: Vec<&BrowserProfile> = profiles .iter() .filter(|p| p.browser == "wayfern" || p.browser == "camoufox") .collect(); Ok(serde_json::json!({ "content": [{ "type": "text", "text": serde_json::to_string_pretty(&filtered).unwrap_or_default() }] })) } async fn handle_get_profile( &self, arguments: &serde_json::Value, ) -> Result { let profile_id = arguments .get("profile_id") .and_then(|v| v.as_str()) .ok_or_else(|| McpError { code: -32602, message: "Missing profile_id".to_string(), })?; let profiles = ProfileManager::instance() .list_profiles() .map_err(|e| McpError { code: -32000, message: format!("Failed to list profiles: {e}"), })?; let profile = profiles .iter() .find(|p| p.id.to_string() == profile_id) .ok_or_else(|| McpError { code: -32000, message: format!("Profile not found: {profile_id}"), })?; // Check if it's a Wayfern or Camoufox profile if profile.browser != "wayfern" && profile.browser != "camoufox" { return Err(McpError { code: -32000, message: "MCP only supports Wayfern and Camoufox profiles".to_string(), }); } Ok(serde_json::json!({ "content": [{ "type": "text", "text": serde_json::to_string_pretty(&profile).unwrap_or_default() }] })) } async fn handle_run_profile( &self, arguments: &serde_json::Value, ) -> Result { let profile_id = arguments .get("profile_id") .and_then(|v| v.as_str()) .ok_or_else(|| McpError { code: -32602, message: "Missing profile_id".to_string(), })?; let url = arguments.get("url").and_then(|v| v.as_str()); let _headless = arguments .get("headless") .and_then(|v| v.as_bool()) .unwrap_or(false); // Get the profile let profiles = ProfileManager::instance() .list_profiles() .map_err(|e| McpError { code: -32000, message: format!("Failed to list profiles: {e}"), })?; let profile = profiles .iter() .find(|p| p.id.to_string() == profile_id) .ok_or_else(|| McpError { code: -32000, message: format!("Profile not found: {profile_id}"), })?; // Check if it's a Wayfern or Camoufox profile if profile.browser != "wayfern" && profile.browser != "camoufox" { return Err(McpError { code: -32000, message: "MCP only supports Wayfern and Camoufox profiles".to_string(), }); } // Team lock check crate::team_lock::acquire_team_lock_if_needed(profile) .await .map_err(|e| McpError { code: -32000, message: e, })?; // Get app handle to launch let inner = self.inner.lock().await; let app_handle = inner.app_handle.as_ref().ok_or_else(|| McpError { code: -32000, message: "MCP server not properly initialized".to_string(), })?; // Launch the browser crate::browser_runner::BrowserRunner::instance() .launch_browser( app_handle.clone(), profile, url.map(|s| s.to_string()), None, ) .await .map_err(|e| McpError { code: -32000, message: format!("Failed to launch browser: {e}"), })?; Ok(serde_json::json!({ "content": [{ "type": "text", "text": format!("Browser profile '{}' launched successfully", profile.name) }] })) } async fn handle_kill_profile( &self, arguments: &serde_json::Value, ) -> Result { let profile_id = arguments .get("profile_id") .and_then(|v| v.as_str()) .ok_or_else(|| McpError { code: -32602, message: "Missing profile_id".to_string(), })?; // Get the profile let profiles = ProfileManager::instance() .list_profiles() .map_err(|e| McpError { code: -32000, message: format!("Failed to list profiles: {e}"), })?; let profile = profiles .iter() .find(|p| p.id.to_string() == profile_id) .ok_or_else(|| McpError { code: -32000, message: format!("Profile not found: {profile_id}"), })?; // Check if it's a Wayfern or Camoufox profile if profile.browser != "wayfern" && profile.browser != "camoufox" { return Err(McpError { code: -32000, message: "MCP only supports Wayfern and Camoufox profiles".to_string(), }); } // Get app handle to kill let inner = self.inner.lock().await; let app_handle = inner.app_handle.as_ref().ok_or_else(|| McpError { code: -32000, message: "MCP server not properly initialized".to_string(), })?; // Kill the browser crate::browser_runner::BrowserRunner::instance() .kill_browser_process(app_handle.clone(), profile) .await .map_err(|e| McpError { code: -32000, message: format!("Failed to kill browser: {e}"), })?; crate::team_lock::release_team_lock_if_needed(profile).await; Ok(serde_json::json!({ "content": [{ "type": "text", "text": format!("Browser profile '{}' stopped successfully", profile.name) }] })) } async fn handle_create_profile( &self, arguments: &serde_json::Value, ) -> Result { let name = arguments .get("name") .and_then(|v| v.as_str()) .ok_or_else(|| McpError { code: -32602, message: "Missing name".to_string(), })?; let browser = arguments .get("browser") .and_then(|v| v.as_str()) .ok_or_else(|| McpError { code: -32602, message: "Missing browser".to_string(), })?; if browser != "wayfern" && browser != "camoufox" { return Err(McpError { code: -32602, message: "browser must be 'wayfern' or 'camoufox'".to_string(), }); } let proxy_id = arguments .get("proxy_id") .and_then(|v| v.as_str()) .map(|s| s.to_string()); let group_id = arguments .get("group_id") .and_then(|v| v.as_str()) .map(|s| s.to_string()); let tags: Option> = arguments.get("tags").and_then(|v| { v.as_array().map(|arr| { arr .iter() .filter_map(|item| item.as_str().map(|s| s.to_string())) .collect() }) }); // Pick the latest downloaded version for this browser let registry = crate::downloaded_browsers_registry::DownloadedBrowsersRegistry::instance(); let versions = registry.get_downloaded_versions(browser); let version = versions.first().ok_or_else(|| McpError { code: -32000, message: format!("No downloaded version found for {browser}. Download it first."), })?; let inner = self.inner.lock().await; let app_handle = inner.app_handle.as_ref().ok_or_else(|| McpError { code: -32000, message: "MCP server not properly initialized".to_string(), })?; let mut profile = ProfileManager::instance() .create_profile_with_group( app_handle, name, browser, version, "stable", proxy_id, None, None, None, group_id, false, ) .await .map_err(|e| McpError { code: -32000, message: format!("Failed to create profile: {e}"), })?; if let Some(tags) = tags { let _ = ProfileManager::instance().update_profile_tags(app_handle, &profile.name, tags.clone()); profile.tags = tags; if let Ok(profiles) = ProfileManager::instance().list_profiles() { let _ = crate::tag_manager::TAG_MANAGER .lock() .map(|manager| manager.rebuild_from_profiles(&profiles)); } } Ok(serde_json::json!({ "content": [{ "type": "text", "text": format!("Profile '{}' created (id: {})", profile.name, profile.id) }] })) } async fn handle_update_profile( &self, arguments: &serde_json::Value, ) -> Result { let profile_id = arguments .get("profile_id") .and_then(|v| v.as_str()) .ok_or_else(|| McpError { code: -32602, message: "Missing profile_id".to_string(), })?; let inner = self.inner.lock().await; let app_handle = inner.app_handle.as_ref().ok_or_else(|| McpError { code: -32000, message: "MCP server not properly initialized".to_string(), })?; let pm = ProfileManager::instance(); if let Some(new_name) = arguments.get("name").and_then(|v| v.as_str()) { pm.rename_profile(app_handle, profile_id, new_name) .map_err(|e| McpError { code: -32000, message: format!("Failed to rename profile: {e}"), })?; } if let Some(proxy_id) = arguments.get("proxy_id").and_then(|v| v.as_str()) { let pid = if proxy_id.is_empty() { None } else { Some(proxy_id.to_string()) }; pm.update_profile_proxy(app_handle.clone(), profile_id, pid) .await .map_err(|e| McpError { code: -32000, message: format!("Failed to update proxy: {e}"), })?; } if let Some(group_id) = arguments.get("group_id").and_then(|v| v.as_str()) { let gid = if group_id.is_empty() { None } else { Some(group_id.to_string()) }; pm.assign_profiles_to_group(app_handle, vec![profile_id.to_string()], gid) .map_err(|e| McpError { code: -32000, message: format!("Failed to update group: {e}"), })?; } if let Some(tags) = arguments.get("tags").and_then(|v| v.as_array()) { let tag_list: Vec = tags .iter() .filter_map(|item| item.as_str().map(|s| s.to_string())) .collect(); pm.update_profile_tags(app_handle, profile_id, tag_list) .map_err(|e| McpError { code: -32000, message: format!("Failed to update tags: {e}"), })?; if let Ok(profiles) = pm.list_profiles() { let _ = crate::tag_manager::TAG_MANAGER .lock() .map(|manager| manager.rebuild_from_profiles(&profiles)); } } if let Some(ext_group_id) = arguments.get("extension_group_id").and_then(|v| v.as_str()) { let eid = if ext_group_id.is_empty() { None } else { Some(ext_group_id.to_string()) }; pm.update_profile_extension_group(profile_id, eid) .map_err(|e| McpError { code: -32000, message: format!("Failed to update extension group: {e}"), })?; } if let Some(rules) = arguments .get("proxy_bypass_rules") .and_then(|v| v.as_array()) { let rule_list: Vec = rules .iter() .filter_map(|item| item.as_str().map(|s| s.to_string())) .collect(); pm.update_profile_proxy_bypass_rules(app_handle, profile_id, rule_list) .map_err(|e| McpError { code: -32000, message: format!("Failed to update proxy bypass rules: {e}"), })?; } Ok(serde_json::json!({ "content": [{ "type": "text", "text": format!("Profile '{profile_id}' updated successfully") }] })) } async fn handle_delete_profile( &self, arguments: &serde_json::Value, ) -> Result { let profile_id = arguments .get("profile_id") .and_then(|v| v.as_str()) .ok_or_else(|| McpError { code: -32602, message: "Missing profile_id".to_string(), })?; let inner = self.inner.lock().await; let app_handle = inner.app_handle.as_ref().ok_or_else(|| McpError { code: -32000, message: "MCP server not properly initialized".to_string(), })?; ProfileManager::instance() .delete_profile(app_handle, profile_id) .map_err(|e| McpError { code: -32000, message: format!("Failed to delete profile: {e}"), })?; Ok(serde_json::json!({ "content": [{ "type": "text", "text": format!("Profile '{profile_id}' deleted successfully") }] })) } async fn handle_list_tags(&self) -> Result { let tags = crate::tag_manager::TAG_MANAGER .lock() .map_err(|e| McpError { code: -32000, message: format!("Failed to access tag manager: {e}"), })? .get_all_tags() .map_err(|e| McpError { code: -32000, message: format!("Failed to get tags: {e}"), })?; Ok(serde_json::json!({ "content": [{ "type": "text", "text": serde_json::to_string_pretty(&tags).unwrap_or_default() }] })) } async fn handle_list_proxies(&self) -> Result { let proxies = PROXY_MANAGER.get_stored_proxies(); Ok(serde_json::json!({ "content": [{ "type": "text", "text": serde_json::to_string_pretty(&proxies).unwrap_or_default() }] })) } async fn handle_get_profile_status( &self, arguments: &serde_json::Value, ) -> Result { let profile_id = arguments .get("profile_id") .and_then(|v| v.as_str()) .ok_or_else(|| McpError { code: -32602, message: "Missing profile_id".to_string(), })?; // Get the profile let profiles = ProfileManager::instance() .list_profiles() .map_err(|e| McpError { code: -32000, message: format!("Failed to list profiles: {e}"), })?; let profile = profiles .iter() .find(|p| p.id.to_string() == profile_id) .ok_or_else(|| McpError { code: -32000, message: format!("Profile not found: {profile_id}"), })?; // Check if it's a Wayfern or Camoufox profile if profile.browser != "wayfern" && profile.browser != "camoufox" { return Err(McpError { code: -32000, message: "MCP only supports Wayfern and Camoufox profiles".to_string(), }); } let is_running = profile.process_id.is_some(); Ok(serde_json::json!({ "content": [{ "type": "text", "text": serde_json::json!({ "profile_id": profile_id, "is_running": is_running }).to_string() }] })) } // Group management handlers async fn handle_list_groups(&self) -> Result { let groups = GROUP_MANAGER .lock() .map_err(|e| McpError { code: -32000, message: format!("Failed to lock group manager: {e}"), })? .get_all_groups() .map_err(|e| McpError { code: -32000, message: format!("Failed to list groups: {e}"), })?; Ok(serde_json::json!({ "content": [{ "type": "text", "text": serde_json::to_string_pretty(&groups).unwrap_or_default() }] })) } async fn handle_get_group( &self, arguments: &serde_json::Value, ) -> Result { let group_id = arguments .get("group_id") .and_then(|v| v.as_str()) .ok_or_else(|| McpError { code: -32602, message: "Missing group_id".to_string(), })?; let groups = GROUP_MANAGER .lock() .map_err(|e| McpError { code: -32000, message: format!("Failed to lock group manager: {e}"), })? .get_all_groups() .map_err(|e| McpError { code: -32000, message: format!("Failed to list groups: {e}"), })?; let group = groups .iter() .find(|g| g.id == group_id) .ok_or_else(|| McpError { code: -32000, message: format!("Group not found: {group_id}"), })?; Ok(serde_json::json!({ "content": [{ "type": "text", "text": serde_json::to_string_pretty(&group).unwrap_or_default() }] })) } async fn handle_create_group( &self, arguments: &serde_json::Value, ) -> Result { let name = arguments .get("name") .and_then(|v| v.as_str()) .ok_or_else(|| McpError { code: -32602, message: "Missing name".to_string(), })?; let inner = self.inner.lock().await; let app_handle = inner.app_handle.as_ref().ok_or_else(|| McpError { code: -32000, message: "MCP server not properly initialized".to_string(), })?; let group = GROUP_MANAGER .lock() .map_err(|e| McpError { code: -32000, message: format!("Failed to lock group manager: {e}"), })? .create_group(app_handle, name.to_string()) .map_err(|e| McpError { code: -32000, message: format!("Failed to create group: {e}"), })?; Ok(serde_json::json!({ "content": [{ "type": "text", "text": format!("Group '{}' created successfully with ID: {}", group.name, group.id) }] })) } async fn handle_update_group( &self, arguments: &serde_json::Value, ) -> Result { let group_id = arguments .get("group_id") .and_then(|v| v.as_str()) .ok_or_else(|| McpError { code: -32602, message: "Missing group_id".to_string(), })?; let name = arguments .get("name") .and_then(|v| v.as_str()) .ok_or_else(|| McpError { code: -32602, message: "Missing name".to_string(), })?; let inner = self.inner.lock().await; let app_handle = inner.app_handle.as_ref().ok_or_else(|| McpError { code: -32000, message: "MCP server not properly initialized".to_string(), })?; let group = GROUP_MANAGER .lock() .map_err(|e| McpError { code: -32000, message: format!("Failed to lock group manager: {e}"), })? .update_group(app_handle, group_id.to_string(), name.to_string()) .map_err(|e| McpError { code: -32000, message: format!("Failed to update group: {e}"), })?; Ok(serde_json::json!({ "content": [{ "type": "text", "text": format!("Group '{}' updated successfully", group.name) }] })) } async fn handle_delete_group( &self, arguments: &serde_json::Value, ) -> Result { let group_id = arguments .get("group_id") .and_then(|v| v.as_str()) .ok_or_else(|| McpError { code: -32602, message: "Missing group_id".to_string(), })?; let inner = self.inner.lock().await; let app_handle = inner.app_handle.as_ref().ok_or_else(|| McpError { code: -32000, message: "MCP server not properly initialized".to_string(), })?; GROUP_MANAGER .lock() .map_err(|e| McpError { code: -32000, message: format!("Failed to lock group manager: {e}"), })? .delete_group(app_handle, group_id.to_string()) .map_err(|e| McpError { code: -32000, message: format!("Failed to delete group: {e}"), })?; Ok(serde_json::json!({ "content": [{ "type": "text", "text": format!("Group '{}' deleted successfully", group_id) }] })) } async fn handle_assign_profiles_to_group( &self, arguments: &serde_json::Value, ) -> Result { let profile_ids: Vec = arguments .get("profile_ids") .and_then(|v| v.as_array()) .ok_or_else(|| McpError { code: -32602, message: "Missing profile_ids".to_string(), })? .iter() .filter_map(|v| v.as_str().map(|s| s.to_string())) .collect(); let group_id = arguments .get("group_id") .and_then(|v| v.as_str()) .map(|s| s.to_string()); let inner = self.inner.lock().await; let app_handle = inner.app_handle.as_ref().ok_or_else(|| McpError { code: -32000, message: "MCP server not properly initialized".to_string(), })?; ProfileManager::instance() .assign_profiles_to_group(app_handle, profile_ids.clone(), group_id.clone()) .map_err(|e| McpError { code: -32000, message: format!("Failed to assign profiles to group: {e}"), })?; let group_name = group_id.as_deref().unwrap_or("default"); Ok(serde_json::json!({ "content": [{ "type": "text", "text": format!("{} profile(s) assigned to group '{}'", profile_ids.len(), group_name) }] })) } // Full proxy management handlers async fn handle_get_proxy( &self, arguments: &serde_json::Value, ) -> Result { let proxy_id = arguments .get("proxy_id") .and_then(|v| v.as_str()) .ok_or_else(|| McpError { code: -32602, message: "Missing proxy_id".to_string(), })?; let proxies = PROXY_MANAGER.get_stored_proxies(); let proxy = proxies .iter() .find(|p| p.id == proxy_id) .ok_or_else(|| McpError { code: -32000, message: format!("Proxy not found: {proxy_id}"), })?; Ok(serde_json::json!({ "content": [{ "type": "text", "text": serde_json::to_string_pretty(&proxy).unwrap_or_default() }] })) } async fn handle_create_proxy( &self, arguments: &serde_json::Value, ) -> Result { let name = arguments .get("name") .and_then(|v| v.as_str()) .ok_or_else(|| McpError { code: -32602, message: "Missing name".to_string(), })?; let inner = self.inner.lock().await; let app_handle = inner.app_handle.as_ref().ok_or_else(|| McpError { code: -32000, message: "MCP server not properly initialized".to_string(), })?; // Check if this is a dynamic proxy creation let dynamic_url = arguments.get("dynamic_proxy_url").and_then(|v| v.as_str()); let dynamic_format = arguments .get("dynamic_proxy_format") .and_then(|v| v.as_str()); let proxy = if let (Some(url), Some(format)) = (dynamic_url, dynamic_format) { PROXY_MANAGER .create_dynamic_proxy( app_handle, name.to_string(), url.to_string(), format.to_string(), ) .map_err(|e| McpError { code: -32000, message: format!("Failed to create dynamic proxy: {e}"), })? } else { let proxy_type = arguments .get("proxy_type") .and_then(|v| v.as_str()) .ok_or_else(|| McpError { code: -32602, message: "Missing proxy_type (required for regular proxies)".to_string(), })?; let host = arguments .get("host") .and_then(|v| v.as_str()) .ok_or_else(|| McpError { code: -32602, message: "Missing host (required for regular proxies)".to_string(), })?; let port = arguments .get("port") .and_then(|v| v.as_u64()) .ok_or_else(|| McpError { code: -32602, message: "Missing port (required for regular proxies)".to_string(), })? as u16; let username = arguments .get("username") .and_then(|v| v.as_str()) .map(|s| s.to_string()); let password = arguments .get("password") .and_then(|v| v.as_str()) .map(|s| s.to_string()); let proxy_settings = ProxySettings { proxy_type: proxy_type.to_string(), host: host.to_string(), port, username, password, }; PROXY_MANAGER .create_stored_proxy(app_handle, name.to_string(), proxy_settings) .map_err(|e| McpError { code: -32000, message: format!("Failed to create proxy: {e}"), })? }; Ok(serde_json::json!({ "content": [{ "type": "text", "text": format!("Proxy '{}' created successfully with ID: {}", proxy.name, proxy.id) }] })) } async fn handle_update_proxy( &self, arguments: &serde_json::Value, ) -> Result { let proxy_id = arguments .get("proxy_id") .and_then(|v| v.as_str()) .ok_or_else(|| McpError { code: -32602, message: "Missing proxy_id".to_string(), })?; let name = arguments .get("name") .and_then(|v| v.as_str()) .map(|s| s.to_string()); // Build proxy_settings if any settings fields are provided let has_settings = arguments.get("proxy_type").is_some() || arguments.get("host").is_some() || arguments.get("port").is_some(); let proxy_settings = if has_settings { // Get existing proxy to use as defaults let proxies = PROXY_MANAGER.get_stored_proxies(); let existing = proxies .iter() .find(|p| p.id == proxy_id) .ok_or_else(|| McpError { code: -32000, message: format!("Proxy not found: {proxy_id}"), })?; let proxy_type = arguments .get("proxy_type") .and_then(|v| v.as_str()) .map(|s| s.to_string()) .unwrap_or_else(|| existing.proxy_settings.proxy_type.clone()); let host = arguments .get("host") .and_then(|v| v.as_str()) .map(|s| s.to_string()) .unwrap_or_else(|| existing.proxy_settings.host.clone()); let port = arguments .get("port") .and_then(|v| v.as_u64()) .map(|p| p as u16) .unwrap_or(existing.proxy_settings.port); let username = arguments .get("username") .and_then(|v| v.as_str()) .map(|s| s.to_string()) .or_else(|| existing.proxy_settings.username.clone()); let password = arguments .get("password") .and_then(|v| v.as_str()) .map(|s| s.to_string()) .or_else(|| existing.proxy_settings.password.clone()); Some(ProxySettings { proxy_type, host, port, username, password, }) } else { None }; let inner = self.inner.lock().await; let app_handle = inner.app_handle.as_ref().ok_or_else(|| McpError { code: -32000, message: "MCP server not properly initialized".to_string(), })?; // Check for dynamic proxy fields let dynamic_url = arguments .get("dynamic_proxy_url") .and_then(|v| v.as_str()) .map(|s| s.to_string()); let dynamic_format = arguments .get("dynamic_proxy_format") .and_then(|v| v.as_str()) .map(|s| s.to_string()); let is_dynamic = PROXY_MANAGER.is_dynamic_proxy(proxy_id) || dynamic_url.is_some(); let proxy = if is_dynamic { PROXY_MANAGER .update_dynamic_proxy(app_handle, proxy_id, name, dynamic_url, dynamic_format) .map_err(|e| McpError { code: -32000, message: format!("Failed to update dynamic proxy: {e}"), })? } else { PROXY_MANAGER .update_stored_proxy(app_handle, proxy_id, name, proxy_settings) .map_err(|e| McpError { code: -32000, message: format!("Failed to update proxy: {e}"), })? }; Ok(serde_json::json!({ "content": [{ "type": "text", "text": format!("Proxy '{}' updated successfully", proxy.name) }] })) } async fn handle_delete_proxy( &self, arguments: &serde_json::Value, ) -> Result { let proxy_id = arguments .get("proxy_id") .and_then(|v| v.as_str()) .ok_or_else(|| McpError { code: -32602, message: "Missing proxy_id".to_string(), })?; let inner = self.inner.lock().await; let app_handle = inner.app_handle.as_ref().ok_or_else(|| McpError { code: -32000, message: "MCP server not properly initialized".to_string(), })?; PROXY_MANAGER .delete_stored_proxy(app_handle, proxy_id) .map_err(|e| McpError { code: -32000, message: format!("Failed to delete proxy: {e}"), })?; Ok(serde_json::json!({ "content": [{ "type": "text", "text": format!("Proxy '{}' deleted successfully", proxy_id) }] })) } async fn handle_export_proxies( &self, arguments: &serde_json::Value, ) -> Result { let format = arguments .get("format") .and_then(|v| v.as_str()) .ok_or_else(|| McpError { code: -32602, message: "Missing format".to_string(), })?; let content = match format { "json" => PROXY_MANAGER.export_proxies_json().map_err(|e| McpError { code: -32000, message: format!("Failed to export proxies: {e}"), })?, "txt" => PROXY_MANAGER.export_proxies_txt(), _ => { return Err(McpError { code: -32602, message: format!("Invalid format '{}', must be 'json' or 'txt'", format), }) } }; Ok(serde_json::json!({ "content": [{ "type": "text", "text": content }] })) } async fn handle_import_proxies( &self, arguments: &serde_json::Value, ) -> Result { let content = arguments .get("content") .and_then(|v| v.as_str()) .ok_or_else(|| McpError { code: -32602, message: "Missing content".to_string(), })?; let format = arguments .get("format") .and_then(|v| v.as_str()) .ok_or_else(|| McpError { code: -32602, message: "Missing format".to_string(), })?; let name_prefix = arguments .get("name_prefix") .and_then(|v| v.as_str()) .map(|s| s.to_string()); let inner = self.inner.lock().await; let app_handle = inner.app_handle.as_ref().ok_or_else(|| McpError { code: -32000, message: "MCP server not properly initialized".to_string(), })?; let result = match format { "json" => PROXY_MANAGER .import_proxies_json(app_handle, content) .map_err(|e| McpError { code: -32000, message: format!("Failed to import proxies: {e}"), })?, "txt" => { use crate::proxy_manager::{ProxyManager, ProxyParseResult}; let parse_results = ProxyManager::parse_txt_proxies(content); let parsed: Vec<_> = parse_results .into_iter() .filter_map(|r| { if let ProxyParseResult::Parsed(p) = r { Some(p) } else { None } }) .collect(); if parsed.is_empty() { return Err(McpError { code: -32000, message: "No valid proxies found in content".to_string(), }); } PROXY_MANAGER .import_proxies_from_parsed(app_handle, parsed, name_prefix) .map_err(|e| McpError { code: -32000, message: format!("Failed to import proxies: {e}"), })? } _ => { return Err(McpError { code: -32602, message: format!("Invalid format '{}', must be 'json' or 'txt'", format), }) } }; Ok(serde_json::json!({ "content": [{ "type": "text", "text": format!( "Import complete: {} imported, {} skipped, {} errors", result.imported_count, result.skipped_count, result.errors.len() ) }] })) } // VPN management handlers async fn handle_import_vpn( &self, arguments: &serde_json::Value, ) -> Result { let content = arguments .get("content") .and_then(|v| v.as_str()) .ok_or_else(|| McpError { code: -32602, message: "Missing content".to_string(), })?; let filename = arguments .get("filename") .and_then(|v| v.as_str()) .ok_or_else(|| McpError { code: -32602, message: "Missing filename".to_string(), })?; let name = arguments .get("name") .and_then(|v| v.as_str()) .map(|s| s.to_string()); let storage = crate::vpn::VPN_STORAGE.lock().map_err(|e| McpError { code: -32000, message: format!("Failed to lock VPN storage: {e}"), })?; let config = storage .import_config(content, filename, name) .map_err(|e| McpError { code: -32000, message: format!("Failed to import VPN config: {e}"), })?; Ok(serde_json::json!({ "content": [{ "type": "text", "text": format!( "VPN '{}' ({}) imported successfully with ID: {}", config.name, config.vpn_type, config.id ) }] })) } async fn handle_list_vpn_configs(&self) -> Result { let storage = crate::vpn::VPN_STORAGE.lock().map_err(|e| McpError { code: -32000, message: format!("Failed to lock VPN storage: {e}"), })?; let configs = storage.list_configs().map_err(|e| McpError { code: -32000, message: format!("Failed to list VPN configs: {e}"), })?; Ok(serde_json::json!({ "content": [{ "type": "text", "text": serde_json::to_string_pretty(&configs).unwrap_or_default() }] })) } async fn handle_delete_vpn( &self, arguments: &serde_json::Value, ) -> Result { let vpn_id = arguments .get("vpn_id") .and_then(|v| v.as_str()) .ok_or_else(|| McpError { code: -32602, message: "Missing vpn_id".to_string(), })?; // First disconnect if connected (stop VPN worker) let _ = crate::vpn_worker_runner::stop_vpn_worker_by_vpn_id(vpn_id).await; let storage = crate::vpn::VPN_STORAGE.lock().map_err(|e| McpError { code: -32000, message: format!("Failed to lock VPN storage: {e}"), })?; storage.delete_config(vpn_id).map_err(|e| McpError { code: -32000, message: format!("Failed to delete VPN config: {e}"), })?; Ok(serde_json::json!({ "content": [{ "type": "text", "text": format!("VPN '{}' deleted successfully", vpn_id) }] })) } async fn handle_connect_vpn( &self, arguments: &serde_json::Value, ) -> Result { let vpn_id = arguments .get("vpn_id") .and_then(|v| v.as_str()) .ok_or_else(|| McpError { code: -32602, message: "Missing vpn_id".to_string(), })?; // Start VPN worker process crate::vpn_worker_runner::start_vpn_worker(vpn_id) .await .map_err(|e| McpError { code: -32000, message: format!("Failed to connect VPN: {e}"), })?; // Update last_used timestamp { let storage = crate::vpn::VPN_STORAGE.lock().map_err(|e| McpError { code: -32000, message: format!("Failed to lock VPN storage: {e}"), })?; let _ = storage.update_last_used(vpn_id); } Ok(serde_json::json!({ "content": [{ "type": "text", "text": format!("VPN '{}' connected successfully", vpn_id) }] })) } async fn handle_disconnect_vpn( &self, arguments: &serde_json::Value, ) -> Result { let vpn_id = arguments .get("vpn_id") .and_then(|v| v.as_str()) .ok_or_else(|| McpError { code: -32602, message: "Missing vpn_id".to_string(), })?; crate::vpn_worker_runner::stop_vpn_worker_by_vpn_id(vpn_id) .await .map_err(|e| McpError { code: -32000, message: format!("Failed to disconnect VPN: {e}"), })?; Ok(serde_json::json!({ "content": [{ "type": "text", "text": format!("VPN '{}' disconnected successfully", vpn_id) }] })) } async fn handle_get_vpn_status( &self, arguments: &serde_json::Value, ) -> Result { let vpn_id = arguments .get("vpn_id") .and_then(|v| v.as_str()) .ok_or_else(|| McpError { code: -32602, message: "Missing vpn_id".to_string(), })?; let connected = if let Some(worker) = crate::vpn_worker_storage::find_vpn_worker_by_vpn_id(vpn_id) { worker .pid .map(crate::proxy_storage::is_process_running) .unwrap_or(false) } else { false }; let status = crate::vpn::VpnStatus { connected, vpn_id: vpn_id.to_string(), connected_at: None, bytes_sent: None, bytes_received: None, last_handshake: None, }; Ok(serde_json::json!({ "content": [{ "type": "text", "text": serde_json::to_string_pretty(&status).unwrap_or_default() }] })) } // Fingerprint management handlers async fn handle_get_profile_fingerprint( &self, arguments: &serde_json::Value, ) -> Result { let profile_id = arguments .get("profile_id") .and_then(|v| v.as_str()) .ok_or_else(|| McpError { code: -32602, message: "Missing profile_id".to_string(), })?; let profiles = ProfileManager::instance() .list_profiles() .map_err(|e| McpError { code: -32000, message: format!("Failed to list profiles: {e}"), })?; let profile = profiles .iter() .find(|p| p.id.to_string() == profile_id) .ok_or_else(|| McpError { code: -32000, message: format!("Profile not found: {profile_id}"), })?; let fingerprint_info = match profile.browser.as_str() { "camoufox" => { let config = profile .camoufox_config .as_ref() .cloned() .unwrap_or_default(); serde_json::json!({ "browser": "camoufox", "fingerprint": config.fingerprint, "os": config.os, "randomize_fingerprint_on_launch": config.randomize_fingerprint_on_launch, "screen_max_width": config.screen_max_width, "screen_max_height": config.screen_max_height, "screen_min_width": config.screen_min_width, "screen_min_height": config.screen_min_height, }) } "wayfern" => { let config = profile.wayfern_config.as_ref().cloned().unwrap_or_default(); serde_json::json!({ "browser": "wayfern", "fingerprint": config.fingerprint, "os": config.os, "randomize_fingerprint_on_launch": config.randomize_fingerprint_on_launch, "screen_max_width": config.screen_max_width, "screen_max_height": config.screen_max_height, "screen_min_width": config.screen_min_width, "screen_min_height": config.screen_min_height, }) } _ => { return Err(McpError { code: -32000, message: "MCP only supports Wayfern and Camoufox profiles".to_string(), }) } }; Ok(serde_json::json!({ "content": [{ "type": "text", "text": serde_json::to_string_pretty(&fingerprint_info).unwrap_or_default() }] })) } async fn handle_update_profile_fingerprint( &self, arguments: &serde_json::Value, ) -> Result { if !CLOUD_AUTH.has_active_paid_subscription().await { return Err(McpError { code: -32000, message: "Fingerprint editing requires an active Pro subscription".to_string(), }); } let profile_id = arguments .get("profile_id") .and_then(|v| v.as_str()) .ok_or_else(|| McpError { code: -32602, message: "Missing profile_id".to_string(), })?; let fingerprint = arguments.get("fingerprint").and_then(|v| v.as_str()); let os = arguments.get("os").and_then(|v| v.as_str()); let randomize = arguments .get("randomize_fingerprint_on_launch") .and_then(|v| v.as_bool()); if let Some(os_val) = os { if !CLOUD_AUTH.is_fingerprint_os_allowed(Some(os_val)).await { return Err(McpError { code: -32000, message: format!( "OS spoofing to '{}' requires an active Pro subscription", os_val ), }); } } let profiles = ProfileManager::instance() .list_profiles() .map_err(|e| McpError { code: -32000, message: format!("Failed to list profiles: {e}"), })?; let profile = profiles .iter() .find(|p| p.id.to_string() == profile_id) .ok_or_else(|| McpError { code: -32000, message: format!("Profile not found: {profile_id}"), })?; let inner = self.inner.lock().await; let app_handle = inner.app_handle.as_ref().ok_or_else(|| McpError { code: -32000, message: "MCP server not properly initialized".to_string(), })?; match profile.browser.as_str() { "camoufox" => { let mut config = profile .camoufox_config .as_ref() .cloned() .unwrap_or_default(); if let Some(fp) = fingerprint { config.fingerprint = Some(fp.to_string()); } if let Some(os_val) = os { config.os = Some(os_val.to_string()); } if let Some(r) = randomize { config.randomize_fingerprint_on_launch = Some(r); } ProfileManager::instance() .update_camoufox_config(app_handle.clone(), profile_id, config) .await .map_err(|e| McpError { code: -32000, message: format!("Failed to update camoufox config: {e}"), })?; } "wayfern" => { let mut config = profile.wayfern_config.as_ref().cloned().unwrap_or_default(); if let Some(fp) = fingerprint { config.fingerprint = Some(fp.to_string()); } if let Some(os_val) = os { config.os = Some(os_val.to_string()); } if let Some(r) = randomize { config.randomize_fingerprint_on_launch = Some(r); } ProfileManager::instance() .update_wayfern_config(app_handle.clone(), profile_id, config) .await .map_err(|e| McpError { code: -32000, message: format!("Failed to update wayfern config: {e}"), })?; } _ => { return Err(McpError { code: -32000, message: "MCP only supports Wayfern and Camoufox profiles".to_string(), }) } } Ok(serde_json::json!({ "content": [{ "type": "text", "text": format!("Fingerprint configuration updated for profile '{}'", profile.name) }] })) } async fn handle_update_profile_proxy_bypass_rules( &self, arguments: &serde_json::Value, ) -> Result { let profile_id = arguments .get("profile_id") .and_then(|v| v.as_str()) .ok_or_else(|| McpError { code: -32602, message: "Missing profile_id".to_string(), })?; let rules: Vec = arguments .get("rules") .and_then(|v| v.as_array()) .ok_or_else(|| McpError { code: -32602, message: "Missing rules array".to_string(), })? .iter() .filter_map(|v| v.as_str().map(|s| s.to_string())) .collect(); let inner = self.inner.lock().await; let app_handle = inner.app_handle.as_ref().ok_or_else(|| McpError { code: -32000, message: "MCP server not properly initialized".to_string(), })?; let profile = ProfileManager::instance() .update_profile_proxy_bypass_rules(app_handle, profile_id, rules.clone()) .map_err(|e| McpError { code: -32000, message: format!("Failed to update proxy bypass rules: {e}"), })?; Ok(serde_json::json!({ "content": [{ "type": "text", "text": format!( "Proxy bypass rules updated for profile '{}': {} rule(s) configured", profile.name, rules.len() ) }] })) } async fn handle_list_extensions(&self) -> Result { if !CLOUD_AUTH.has_active_paid_subscription().await { return Err(McpError { code: -32000, message: "Extension management requires an active Pro subscription".to_string(), }); } let mgr = crate::extension_manager::EXTENSION_MANAGER.lock().unwrap(); let extensions = mgr.list_extensions().map_err(|e| McpError { code: -32000, message: format!("Failed to list extensions: {e}"), })?; Ok(serde_json::to_value(extensions).unwrap()) } async fn handle_list_extension_groups(&self) -> Result { if !CLOUD_AUTH.has_active_paid_subscription().await { return Err(McpError { code: -32000, message: "Extension management requires an active Pro subscription".to_string(), }); } let mgr = crate::extension_manager::EXTENSION_MANAGER.lock().unwrap(); let groups = mgr.list_groups().map_err(|e| McpError { code: -32000, message: format!("Failed to list extension groups: {e}"), })?; Ok(serde_json::to_value(groups).unwrap()) } async fn handle_create_extension_group( &self, arguments: &serde_json::Value, ) -> Result { if !CLOUD_AUTH.has_active_paid_subscription().await { return Err(McpError { code: -32000, message: "Extension management requires an active Pro subscription".to_string(), }); } let name = arguments .get("name") .and_then(|v| v.as_str()) .ok_or_else(|| McpError { code: -32602, message: "Missing required parameter: name".to_string(), })?; let mgr = crate::extension_manager::EXTENSION_MANAGER.lock().unwrap(); let group = mgr.create_group(name.to_string()).map_err(|e| McpError { code: -32000, message: format!("Failed to create extension group: {e}"), })?; Ok(serde_json::to_value(group).unwrap()) } async fn handle_delete_extension_mcp( &self, arguments: &serde_json::Value, ) -> Result { if !CLOUD_AUTH.has_active_paid_subscription().await { return Err(McpError { code: -32000, message: "Extension management requires an active Pro subscription".to_string(), }); } let extension_id = arguments .get("extension_id") .and_then(|v| v.as_str()) .ok_or_else(|| McpError { code: -32602, message: "Missing required parameter: extension_id".to_string(), })?; let mgr = crate::extension_manager::EXTENSION_MANAGER.lock().unwrap(); mgr .delete_extension_internal(extension_id) .map_err(|e| McpError { code: -32000, message: format!("Failed to delete extension: {e}"), })?; Ok(serde_json::json!({"success": true})) } async fn handle_delete_extension_group_mcp( &self, arguments: &serde_json::Value, ) -> Result { if !CLOUD_AUTH.has_active_paid_subscription().await { return Err(McpError { code: -32000, message: "Extension management requires an active Pro subscription".to_string(), }); } let group_id = arguments .get("group_id") .and_then(|v| v.as_str()) .ok_or_else(|| McpError { code: -32602, message: "Missing required parameter: group_id".to_string(), })?; let mgr = crate::extension_manager::EXTENSION_MANAGER.lock().unwrap(); // For MCP, we don't have an app_handle, but we need one for sync deletion. // Use the delete_group_internal which skips sync remote deletion. mgr.delete_group_internal(group_id).map_err(|e| McpError { code: -32000, message: format!("Failed to delete extension group: {e}"), })?; if let Err(e) = crate::events::emit_empty("extensions-changed") { log::error!("Failed to emit extensions-changed event: {e}"); } Ok(serde_json::json!({"success": true})) } async fn handle_assign_extension_group_to_profile( &self, arguments: &serde_json::Value, ) -> Result { if !CLOUD_AUTH.has_active_paid_subscription().await { return Err(McpError { code: -32000, message: "Extension management requires an active Pro subscription".to_string(), }); } let profile_id = arguments .get("profile_id") .and_then(|v| v.as_str()) .ok_or_else(|| McpError { code: -32602, message: "Missing required parameter: profile_id".to_string(), })?; let extension_group_id = arguments .get("extension_group_id") .and_then(|v| v.as_str()) .map(|s| { if s.is_empty() { None } else { Some(s.to_string()) } }) .unwrap_or(None); // Validate compatibility if assigning if let Some(ref gid) = extension_group_id { let profile_manager = ProfileManager::instance(); let profiles = profile_manager.list_profiles().map_err(|e| McpError { code: -32000, message: format!("Failed to list profiles: {e}"), })?; let profile = profiles .iter() .find(|p| p.id.to_string() == profile_id) .ok_or_else(|| McpError { code: -32000, message: format!("Profile '{profile_id}' not found"), })?; let mgr = crate::extension_manager::EXTENSION_MANAGER.lock().unwrap(); mgr .validate_group_compatibility(gid, &profile.browser) .map_err(|e| McpError { code: -32000, message: format!("{e}"), })?; } let profile_manager = ProfileManager::instance(); let profile = profile_manager .update_profile_extension_group(profile_id, extension_group_id) .map_err(|e| McpError { code: -32000, message: format!("Failed to assign extension group: {e}"), })?; Ok(serde_json::to_value(profile).unwrap()) } async fn handle_get_team_locks(&self) -> Result { if !CLOUD_AUTH.is_on_team_plan().await { return Err(McpError { code: -32000, message: "Team features require an active team plan".to_string(), }); } let locks = crate::team_lock::TEAM_LOCK.get_locks().await; Ok(serde_json::json!({ "content": [{ "type": "text", "text": serde_json::to_string_pretty(&locks).unwrap_or_default() }] })) } async fn handle_get_team_lock_status( &self, arguments: &serde_json::Value, ) -> Result { if !CLOUD_AUTH.is_on_team_plan().await { return Err(McpError { code: -32000, message: "Team features require an active team plan".to_string(), }); } let profile_id = arguments .get("profile_id") .and_then(|v| v.as_str()) .ok_or_else(|| McpError { code: -32602, message: "Missing profile_id".to_string(), })?; let lock_status = crate::team_lock::TEAM_LOCK .get_lock_status(profile_id) .await; Ok(serde_json::json!({ "content": [{ "type": "text", "text": serde_json::to_string_pretty(&lock_status).unwrap_or_default() }] })) } // --- CDP utility methods for browser interaction --- async fn get_cdp_port_for_profile(&self, profile: &BrowserProfile) -> Result { let profiles_dir = ProfileManager::instance().get_profiles_dir(); let profile_path = profile.get_profile_data_path(&profiles_dir); let profile_path_str = profile_path.to_string_lossy(); // Retry a few times — port info may not be stored yet right after launch for attempt in 0..10 { if attempt > 0 { tokio::time::sleep(std::time::Duration::from_secs(1)).await; } let port = if profile.browser == "wayfern" { crate::wayfern_manager::WayfernManager::instance() .get_cdp_port(&profile_path_str) .await } else if profile.browser == "camoufox" { crate::camoufox_manager::CamoufoxManager::instance() .get_cdp_port(&profile_path_str) .await } else { None }; if let Some(p) = port { return Ok(p); } } Err(McpError { code: -32000, message: format!( "No CDP connection available for profile '{}'. Make sure the browser is running.", profile.name ), }) } async fn get_cdp_ws_url(&self, port: u16) -> Result { let url = format!("http://127.0.0.1:{port}/json"); let client = reqwest::Client::new(); // Retry connecting to CDP endpoint (browser may still be starting up) let max_attempts = 15; let mut last_err = String::new(); for attempt in 0..max_attempts { if attempt > 0 { tokio::time::sleep(std::time::Duration::from_secs(1)).await; } match client .get(&url) .timeout(std::time::Duration::from_secs(3)) .send() .await { Ok(resp) => match resp.json::>().await { Ok(targets) => { if let Some(ws_url) = targets .iter() .find(|t| t.get("type").and_then(|v| v.as_str()) == Some("page")) .and_then(|t| t.get("webSocketDebuggerUrl")) .and_then(|v| v.as_str()) { return Ok(ws_url.to_string()); } last_err = "No page target found in browser".to_string(); } Err(e) => { last_err = format!("Failed to parse CDP targets: {e}"); } }, Err(e) => { last_err = format!("Failed to connect to browser CDP endpoint: {e}"); } } } Err(McpError { code: -32000, message: last_err, }) } async fn send_cdp( &self, ws_url: &str, method: &str, params: serde_json::Value, ) -> Result { use futures_util::sink::SinkExt; use futures_util::stream::StreamExt; use tokio_tungstenite::connect_async; use tokio_tungstenite::tungstenite::Message; let (mut ws_stream, _) = connect_async(ws_url).await.map_err(|e| McpError { code: -32000, message: format!("Failed to connect to CDP WebSocket: {e}"), })?; let command = serde_json::json!({ "id": 1, "method": method, "params": params }); ws_stream .send(Message::Text(command.to_string().into())) .await .map_err(|e| McpError { code: -32000, message: format!("Failed to send CDP command: {e}"), })?; while let Some(msg) = ws_stream.next().await { let msg = msg.map_err(|e| McpError { code: -32000, message: format!("CDP WebSocket error: {e}"), })?; if let Message::Text(text) = msg { let response: serde_json::Value = serde_json::from_str(text.as_str()).map_err(|e| McpError { code: -32000, message: format!("Failed to parse CDP response: {e}"), })?; if response.get("id") == Some(&serde_json::json!(1)) { if let Some(error) = response.get("error") { return Err(McpError { code: -32000, message: format!("CDP error: {error}"), }); } return Ok( response .get("result") .cloned() .unwrap_or(serde_json::json!({})), ); } } } Err(McpError { code: -32000, message: "No response received from CDP".to_string(), }) } async fn send_human_keystrokes( &self, ws_url: &str, text: &str, wpm: Option, ) -> Result<(), McpError> { use crate::human_typing::{MarkovTyper, TypingAction}; use futures_util::sink::SinkExt; use futures_util::stream::StreamExt; use tokio_tungstenite::connect_async; use tokio_tungstenite::tungstenite::Message; let events = MarkovTyper::new(text, wpm).run(); let (mut ws_stream, _) = connect_async(ws_url).await.map_err(|e| McpError { code: -32000, message: format!("Failed to connect to CDP WebSocket: {e}"), })?; let mut cmd_id = 1u64; let mut last_time = 0.0; for event in &events { let delay = event.time - last_time; if delay > 0.0 { tokio::time::sleep(std::time::Duration::from_secs_f64(delay)).await; } last_time = event.time; match &event.action { TypingAction::Char(ch) => { let text_str = ch.to_string(); // keyDown let down = serde_json::json!({ "id": cmd_id, "method": "Input.dispatchKeyEvent", "params": { "type": "keyDown", "text": text_str, "key": text_str, "unmodifiedText": text_str, } }); cmd_id += 1; ws_stream .send(Message::Text(down.to_string().into())) .await .map_err(|e| McpError { code: -32000, message: format!("Failed to send key event: {e}"), })?; // Drain response let _ = ws_stream.next().await; // keyUp let up = serde_json::json!({ "id": cmd_id, "method": "Input.dispatchKeyEvent", "params": { "type": "keyUp", "key": text_str, } }); cmd_id += 1; ws_stream .send(Message::Text(up.to_string().into())) .await .map_err(|e| McpError { code: -32000, message: format!("Failed to send key event: {e}"), })?; let _ = ws_stream.next().await; } TypingAction::Backspace => { let down = serde_json::json!({ "id": cmd_id, "method": "Input.dispatchKeyEvent", "params": { "type": "keyDown", "key": "Backspace", "code": "Backspace", "windowsVirtualKeyCode": 8, "nativeVirtualKeyCode": 8, } }); cmd_id += 1; ws_stream .send(Message::Text(down.to_string().into())) .await .map_err(|e| McpError { code: -32000, message: format!("Failed to send key event: {e}"), })?; let _ = ws_stream.next().await; let up = serde_json::json!({ "id": cmd_id, "method": "Input.dispatchKeyEvent", "params": { "type": "keyUp", "key": "Backspace", "code": "Backspace", "windowsVirtualKeyCode": 8, "nativeVirtualKeyCode": 8, } }); cmd_id += 1; ws_stream .send(Message::Text(up.to_string().into())) .await .map_err(|e| McpError { code: -32000, message: format!("Failed to send key event: {e}"), })?; let _ = ws_stream.next().await; } } } Ok(()) } /// Send a CDP command and wait for the page to finish loading. /// Uses a single WebSocket connection to: enable Page events, send the command, /// wait for the command response, then wait for `Page.loadEventFired`. async fn send_cdp_and_wait_for_load( &self, ws_url: &str, method: &str, params: serde_json::Value, timeout_secs: u64, ) -> Result { use futures_util::sink::SinkExt; use futures_util::stream::StreamExt; use tokio_tungstenite::connect_async; use tokio_tungstenite::tungstenite::Message; let (mut ws_stream, _) = connect_async(ws_url).await.map_err(|e| McpError { code: -32000, message: format!("Failed to connect to CDP WebSocket: {e}"), })?; // Enable Page domain events so we receive loadEventFired let enable_cmd = serde_json::json!({ "id": 1, "method": "Page.enable", "params": {} }); ws_stream .send(Message::Text(enable_cmd.to_string().into())) .await .map_err(|e| McpError { code: -32000, message: format!("Failed to send Page.enable: {e}"), })?; // Wait for Page.enable response loop { let msg = ws_stream .next() .await .ok_or_else(|| McpError { code: -32000, message: "WebSocket closed waiting for Page.enable response".to_string(), })? .map_err(|e| McpError { code: -32000, message: format!("CDP WebSocket error: {e}"), })?; if let Message::Text(text) = msg { let resp: serde_json::Value = serde_json::from_str(text.as_str()).unwrap_or_default(); if resp.get("id") == Some(&serde_json::json!(1)) { break; } } } // Send the actual command (e.g., Page.navigate) let command = serde_json::json!({ "id": 2, "method": method, "params": params }); ws_stream .send(Message::Text(command.to_string().into())) .await .map_err(|e| McpError { code: -32000, message: format!("Failed to send CDP command: {e}"), })?; // Wait for command response and then for Page.loadEventFired let mut command_result = None; let deadline = tokio::time::Instant::now() + tokio::time::Duration::from_secs(timeout_secs); loop { let remaining = deadline.saturating_duration_since(tokio::time::Instant::now()); if remaining.is_zero() { // Timed out waiting for load — return the command result if we have it break; } let msg = match tokio::time::timeout(remaining, ws_stream.next()).await { Ok(Some(Ok(msg))) => msg, Ok(Some(Err(e))) => { return Err(McpError { code: -32000, message: format!("CDP WebSocket error: {e}"), }); } Ok(None) => break, // stream ended Err(_) => break, // timeout }; if let Message::Text(text) = msg { let response: serde_json::Value = serde_json::from_str(text.as_str()).unwrap_or_default(); // Check for command response if response.get("id") == Some(&serde_json::json!(2)) { if let Some(error) = response.get("error") { return Err(McpError { code: -32000, message: format!("CDP error: {error}"), }); } command_result = Some( response .get("result") .cloned() .unwrap_or(serde_json::json!({})), ); } // Check for Page.loadEventFired — page is fully loaded if response.get("method") == Some(&serde_json::json!("Page.loadEventFired")) { break; } } } // Disable Page domain events let disable_cmd = serde_json::json!({ "id": 3, "method": "Page.disable", "params": {} }); let _ = ws_stream .send(Message::Text(disable_cmd.to_string().into())) .await; command_result.ok_or_else(|| McpError { code: -32000, message: "No response received from CDP".to_string(), }) } fn get_running_profile(&self, profile_id: &str) -> Result { let profiles = ProfileManager::instance() .list_profiles() .map_err(|e| McpError { code: -32000, message: format!("Failed to list profiles: {e}"), })?; let profile = profiles .into_iter() .find(|p| p.id.to_string() == profile_id) .ok_or_else(|| McpError { code: -32000, message: format!("Profile not found: {profile_id}"), })?; if profile.browser != "wayfern" && profile.browser != "camoufox" { return Err(McpError { code: -32000, message: "MCP only supports Wayfern and Camoufox profiles".to_string(), }); } if profile.process_id.is_none() { return Err(McpError { code: -32000, message: format!("Profile '{}' is not running", profile.name), }); } Ok(profile) } // --- Browser interaction handlers --- async fn handle_navigate( &self, arguments: &serde_json::Value, ) -> Result { let profile_id = arguments .get("profile_id") .and_then(|v| v.as_str()) .ok_or_else(|| McpError { code: -32602, message: "Missing profile_id".to_string(), })?; let url = arguments .get("url") .and_then(|v| v.as_str()) .ok_or_else(|| McpError { code: -32602, message: "Missing url".to_string(), })?; let profile = self.get_running_profile(profile_id)?; let cdp_port = self.get_cdp_port_for_profile(&profile).await?; let ws_url = self.get_cdp_ws_url(cdp_port).await?; self .send_cdp_and_wait_for_load( &ws_url, "Page.navigate", serde_json::json!({ "url": url }), 30, ) .await?; Ok(serde_json::json!({ "content": [{ "type": "text", "text": format!("Navigated to {url}") }] })) } async fn handle_screenshot( &self, arguments: &serde_json::Value, ) -> Result { let profile_id = arguments .get("profile_id") .and_then(|v| v.as_str()) .ok_or_else(|| McpError { code: -32602, message: "Missing profile_id".to_string(), })?; let format = arguments .get("format") .and_then(|v| v.as_str()) .unwrap_or("png"); let quality = arguments.get("quality").and_then(|v| v.as_i64()); let full_page = arguments .get("full_page") .and_then(|v| v.as_bool()) .unwrap_or(false); let profile = self.get_running_profile(profile_id)?; let cdp_port = self.get_cdp_port_for_profile(&profile).await?; let ws_url = self.get_cdp_ws_url(cdp_port).await?; let mut params = serde_json::json!({ "format": format }); if let Some(q) = quality { params["quality"] = serde_json::json!(q); } if full_page { let layout = self .send_cdp(&ws_url, "Page.getLayoutMetrics", serde_json::json!({})) .await?; if let Some(content_size) = layout.get("contentSize") { params["clip"] = serde_json::json!({ "x": 0, "y": 0, "width": content_size.get("width").and_then(|v| v.as_f64()).unwrap_or(1920.0), "height": content_size.get("height").and_then(|v| v.as_f64()).unwrap_or(1080.0), "scale": 1 }); params["captureBeyondViewport"] = serde_json::json!(true); } } let result = self .send_cdp(&ws_url, "Page.captureScreenshot", params) .await?; let data = result .get("data") .and_then(|v| v.as_str()) .unwrap_or_default(); Ok(serde_json::json!({ "content": [{ "type": "image", "data": data, "mimeType": format!("image/{format}") }] })) } async fn handle_evaluate_javascript( &self, arguments: &serde_json::Value, ) -> Result { let profile_id = arguments .get("profile_id") .and_then(|v| v.as_str()) .ok_or_else(|| McpError { code: -32602, message: "Missing profile_id".to_string(), })?; let expression = arguments .get("expression") .and_then(|v| v.as_str()) .ok_or_else(|| McpError { code: -32602, message: "Missing expression".to_string(), })?; let await_promise = arguments .get("await_promise") .and_then(|v| v.as_bool()) .unwrap_or(false); let wait_for_load = arguments .get("wait_for_load") .and_then(|v| v.as_bool()) .unwrap_or(false); let profile = self.get_running_profile(profile_id)?; let cdp_port = self.get_cdp_port_for_profile(&profile).await?; let ws_url = self.get_cdp_ws_url(cdp_port).await?; let cdp_params = serde_json::json!({ "expression": expression, "returnByValue": true, "awaitPromise": await_promise, }); let result = if wait_for_load { self .send_cdp_and_wait_for_load(&ws_url, "Runtime.evaluate", cdp_params, 30) .await? } else { self .send_cdp(&ws_url, "Runtime.evaluate", cdp_params) .await? }; let value = if let Some(exception) = result.get("exceptionDetails") { let text = exception .get("text") .or_else(|| { exception .get("exception") .and_then(|e| e.get("description")) }) .and_then(|v| v.as_str()) .unwrap_or("Unknown error"); serde_json::json!({ "error": text }) } else if let Some(r) = result.get("result") { let val = r.get("value").cloned().unwrap_or(serde_json::json!(null)); serde_json::json!({ "value": val, "type": r.get("type") }) } else { serde_json::json!({ "value": null }) }; Ok(serde_json::json!({ "content": [{ "type": "text", "text": serde_json::to_string_pretty(&value).unwrap_or_default() }] })) } async fn handle_click_element( &self, arguments: &serde_json::Value, ) -> Result { let profile_id = arguments .get("profile_id") .and_then(|v| v.as_str()) .ok_or_else(|| McpError { code: -32602, message: "Missing profile_id".to_string(), })?; let selector = arguments .get("selector") .and_then(|v| v.as_str()) .ok_or_else(|| McpError { code: -32602, message: "Missing selector".to_string(), })?; let profile = self.get_running_profile(profile_id)?; let cdp_port = self.get_cdp_port_for_profile(&profile).await?; let ws_url = self.get_cdp_ws_url(cdp_port).await?; let selector_escaped = selector.replace('\\', "\\\\").replace('\'', "\\'"); let js = format!( r#"(() => {{ const el = document.querySelector('{}'); if (!el) throw new Error('Element not found: {}'); el.scrollIntoView({{block: 'center'}}); el.click(); return true; }})()"#, selector_escaped, selector_escaped ); // Use send_cdp_and_wait_for_load: if the click triggers navigation, // we wait for the new page to load. If not, the 10s timeout expires // and we return immediately. let result = self .send_cdp_and_wait_for_load( &ws_url, "Runtime.evaluate", serde_json::json!({ "expression": js, "returnByValue": true, }), 10, ) .await?; if let Some(exception) = result.get("exceptionDetails") { let msg = exception .get("exception") .and_then(|e| e.get("description")) .or_else(|| exception.get("text")) .and_then(|v| v.as_str()) .unwrap_or("Click failed"); return Err(McpError { code: -32000, message: msg.to_string(), }); } Ok(serde_json::json!({ "content": [{ "type": "text", "text": format!("Clicked element: {selector}") }] })) } async fn handle_type_text( &self, arguments: &serde_json::Value, ) -> Result { let profile_id = arguments .get("profile_id") .and_then(|v| v.as_str()) .ok_or_else(|| McpError { code: -32602, message: "Missing profile_id".to_string(), })?; let selector = arguments .get("selector") .and_then(|v| v.as_str()) .ok_or_else(|| McpError { code: -32602, message: "Missing selector".to_string(), })?; let text = arguments .get("text") .and_then(|v| v.as_str()) .ok_or_else(|| McpError { code: -32602, message: "Missing text".to_string(), })?; let clear_first = arguments .get("clear_first") .and_then(|v| v.as_bool()) .unwrap_or(true); let instant = arguments .get("instant") .and_then(|v| v.as_bool()) .unwrap_or(false); let wpm = arguments.get("wpm").and_then(|v| v.as_f64()); let profile = self.get_running_profile(profile_id)?; let cdp_port = self.get_cdp_port_for_profile(&profile).await?; let ws_url = self.get_cdp_ws_url(cdp_port).await?; let selector_escaped = selector.replace('\\', "\\\\").replace('\'', "\\'"); let focus_js = if clear_first { format!( r#"(() => {{ const el = document.querySelector('{}'); if (!el) throw new Error('Element not found: {}'); el.scrollIntoView({{block: 'center'}}); el.focus(); el.value = ''; el.dispatchEvent(new Event('input', {{bubbles: true}})); return true; }})()"#, selector_escaped, selector_escaped ) } else { format!( r#"(() => {{ const el = document.querySelector('{}'); if (!el) throw new Error('Element not found: {}'); el.scrollIntoView({{block: 'center'}}); el.focus(); return true; }})()"#, selector_escaped, selector_escaped ) }; let focus_result = self .send_cdp( &ws_url, "Runtime.evaluate", serde_json::json!({ "expression": focus_js, "returnByValue": true, }), ) .await?; if let Some(exception) = focus_result.get("exceptionDetails") { let msg = exception .get("exception") .and_then(|e| e.get("description")) .or_else(|| exception.get("text")) .and_then(|v| v.as_str()) .unwrap_or("Focus failed"); return Err(McpError { code: -32000, message: msg.to_string(), }); } if instant { self .send_cdp( &ws_url, "Input.insertText", serde_json::json!({ "text": text }), ) .await?; } else { self.send_human_keystrokes(&ws_url, text, wpm).await?; } Ok(serde_json::json!({ "content": [{ "type": "text", "text": format!("Typed text into element: {selector}") }] })) } async fn handle_get_page_content( &self, arguments: &serde_json::Value, ) -> Result { let profile_id = arguments .get("profile_id") .and_then(|v| v.as_str()) .ok_or_else(|| McpError { code: -32602, message: "Missing profile_id".to_string(), })?; let format = arguments .get("format") .and_then(|v| v.as_str()) .unwrap_or("text"); let selector = arguments.get("selector").and_then(|v| v.as_str()); let profile = self.get_running_profile(profile_id)?; let cdp_port = self.get_cdp_port_for_profile(&profile).await?; let ws_url = self.get_cdp_ws_url(cdp_port).await?; let js = if let Some(sel) = selector { let sel_escaped = sel.replace('\\', "\\\\").replace('\'', "\\'"); if format == "html" { format!( r#"(() => {{ const el = document.querySelector('{}'); return el ? el.outerHTML : null; }})()"#, sel_escaped ) } else { format!( r#"(() => {{ const el = document.querySelector('{}'); return el ? el.innerText : null; }})()"#, sel_escaped ) } } else if format == "html" { "document.documentElement.outerHTML".to_string() } else { "document.body.innerText".to_string() }; let result = self .send_cdp( &ws_url, "Runtime.evaluate", serde_json::json!({ "expression": js, "returnByValue": true, }), ) .await?; let content = result .get("result") .and_then(|r| r.get("value")) .and_then(|v| v.as_str()) .unwrap_or(""); Ok(serde_json::json!({ "content": [{ "type": "text", "text": content }] })) } async fn handle_get_page_info( &self, arguments: &serde_json::Value, ) -> Result { let profile_id = arguments .get("profile_id") .and_then(|v| v.as_str()) .ok_or_else(|| McpError { code: -32602, message: "Missing profile_id".to_string(), })?; let profile = self.get_running_profile(profile_id)?; let cdp_port = self.get_cdp_port_for_profile(&profile).await?; let ws_url = self.get_cdp_ws_url(cdp_port).await?; let result = self .send_cdp( &ws_url, "Runtime.evaluate", serde_json::json!({ "expression": "JSON.stringify({url: location.href, title: document.title, readyState: document.readyState})", "returnByValue": true, }), ) .await?; let info_str = result .get("result") .and_then(|r| r.get("value")) .and_then(|v| v.as_str()) .unwrap_or("{}"); let info: serde_json::Value = serde_json::from_str(info_str).unwrap_or(serde_json::json!({})); Ok(serde_json::json!({ "content": [{ "type": "text", "text": serde_json::to_string_pretty(&info).unwrap_or_default() }] })) } // --- Synchronizer handlers --- async fn handle_start_sync_session( &self, arguments: &serde_json::Value, ) -> Result { let leader_id = arguments .get("leader_profile_id") .and_then(|v| v.as_str()) .ok_or_else(|| McpError { code: -32602, message: "Missing leader_profile_id".to_string(), })?; let follower_ids: Vec = arguments .get("follower_profile_ids") .and_then(|v| v.as_array()) .ok_or_else(|| McpError { code: -32602, message: "Missing follower_profile_ids".to_string(), })? .iter() .filter_map(|v| v.as_str().map(|s| s.to_string())) .collect(); let app = { let inner = self.inner.lock().await; inner.app_handle.clone().ok_or_else(|| McpError { code: -32000, message: "MCP server not properly initialized".to_string(), })? }; let info = crate::synchronizer::SynchronizerManager::instance() .start_session(app, leader_id.to_string(), follower_ids) .await .map_err(|e| McpError { code: -32000, message: e, })?; Ok(serde_json::json!({ "content": [{ "type": "text", "text": serde_json::to_string_pretty(&info).unwrap_or_default() }] })) } async fn handle_stop_sync_session( &self, arguments: &serde_json::Value, ) -> Result { let session_id = arguments .get("session_id") .and_then(|v| v.as_str()) .ok_or_else(|| McpError { code: -32602, message: "Missing session_id".to_string(), })?; let app = { let inner = self.inner.lock().await; inner.app_handle.clone().ok_or_else(|| McpError { code: -32000, message: "MCP server not properly initialized".to_string(), })? }; crate::synchronizer::SynchronizerManager::instance() .stop_session(app, session_id) .await .map_err(|e| McpError { code: -32000, message: e, })?; Ok(serde_json::json!({ "content": [{ "type": "text", "text": "Sync session stopped" }] })) } async fn handle_get_sync_sessions(&self) -> Result { let sessions = crate::synchronizer::SynchronizerManager::instance() .get_sessions() .await; Ok(serde_json::json!({ "content": [{ "type": "text", "text": serde_json::to_string_pretty(&sessions).unwrap_or_default() }] })) } async fn handle_remove_sync_follower( &self, arguments: &serde_json::Value, ) -> Result { let session_id = arguments .get("session_id") .and_then(|v| v.as_str()) .ok_or_else(|| McpError { code: -32602, message: "Missing session_id".to_string(), })?; let follower_id = arguments .get("follower_profile_id") .and_then(|v| v.as_str()) .ok_or_else(|| McpError { code: -32602, message: "Missing follower_profile_id".to_string(), })?; let app = { let inner = self.inner.lock().await; inner.app_handle.clone().ok_or_else(|| McpError { code: -32000, message: "MCP server not properly initialized".to_string(), })? }; crate::synchronizer::SynchronizerManager::instance() .remove_follower(app, session_id, follower_id) .await .map_err(|e| McpError { code: -32000, message: e, })?; Ok(serde_json::json!({ "content": [{ "type": "text", "text": "Follower removed from sync session" }] })) } } lazy_static::lazy_static! { static ref MCP_SERVER: McpServer = McpServer::new(); } #[cfg(test)] mod tests { use super::*; #[test] fn test_mcp_tools_count() { let server = McpServer::new(); let tools = server.get_tools(); // Should have at least 41 tools (34 + 7 browser interaction tools) assert!(tools.len() >= 41); // Check tool names let tool_names: Vec<&str> = tools.iter().map(|t| t.name.as_str()).collect(); // Profile tools assert!(tool_names.contains(&"list_profiles")); assert!(tool_names.contains(&"get_profile")); assert!(tool_names.contains(&"run_profile")); assert!(tool_names.contains(&"kill_profile")); assert!(tool_names.contains(&"get_profile_status")); // Group tools assert!(tool_names.contains(&"list_groups")); assert!(tool_names.contains(&"get_group")); assert!(tool_names.contains(&"create_group")); assert!(tool_names.contains(&"update_group")); assert!(tool_names.contains(&"delete_group")); assert!(tool_names.contains(&"assign_profiles_to_group")); // Proxy tools assert!(tool_names.contains(&"list_proxies")); assert!(tool_names.contains(&"get_proxy")); assert!(tool_names.contains(&"create_proxy")); assert!(tool_names.contains(&"update_proxy")); assert!(tool_names.contains(&"delete_proxy")); // Proxy import/export tools assert!(tool_names.contains(&"export_proxies")); assert!(tool_names.contains(&"import_proxies")); // VPN tools assert!(tool_names.contains(&"import_vpn")); assert!(tool_names.contains(&"list_vpn_configs")); assert!(tool_names.contains(&"delete_vpn")); assert!(tool_names.contains(&"connect_vpn")); assert!(tool_names.contains(&"disconnect_vpn")); assert!(tool_names.contains(&"get_vpn_status")); // Fingerprint tools assert!(tool_names.contains(&"get_profile_fingerprint")); assert!(tool_names.contains(&"update_profile_fingerprint")); assert!(tool_names.contains(&"update_profile_proxy_bypass_rules")); // Extension tools assert!(tool_names.contains(&"list_extensions")); assert!(tool_names.contains(&"list_extension_groups")); assert!(tool_names.contains(&"create_extension_group")); assert!(tool_names.contains(&"delete_extension")); assert!(tool_names.contains(&"delete_extension_group")); assert!(tool_names.contains(&"assign_extension_group_to_profile")); // Team lock tools assert!(tool_names.contains(&"get_team_locks")); assert!(tool_names.contains(&"get_team_lock_status")); // Synchronizer tools assert!(tool_names.contains(&"start_sync_session")); assert!(tool_names.contains(&"stop_sync_session")); assert!(tool_names.contains(&"get_sync_sessions")); assert!(tool_names.contains(&"remove_sync_follower")); // Browser interaction tools assert!(tool_names.contains(&"navigate")); assert!(tool_names.contains(&"screenshot")); assert!(tool_names.contains(&"evaluate_javascript")); assert!(tool_names.contains(&"click_element")); assert!(tool_names.contains(&"type_text")); assert!(tool_names.contains(&"get_page_content")); assert!(tool_names.contains(&"get_page_info")); } #[test] fn test_mcp_server_initial_state() { let server = McpServer::new(); assert!(!server.is_running()); } }