diff --git a/src-tauri/src/api_server.rs b/src-tauri/src/api_server.rs index a80b0fb..1d710db 100644 --- a/src-tauri/src/api_server.rs +++ b/src-tauri/src/api_server.rs @@ -401,6 +401,10 @@ impl ApiServer { .merge(v1_routes) .nest("/ws", ws_routes) .route("/openapi.json", get(move || async move { Json(api) })) + // Outermost layer: logs every request so customer reports show what + // their automation is actually calling, what the response status was, + // and how long it took. Never logs request bodies or auth headers. + .layer(middleware::from_fn(request_logging_middleware)) .layer(CorsLayer::permissive()) .with_state(state); @@ -454,6 +458,8 @@ async fn auth_middleware( request: axum::extract::Request, next: Next, ) -> Result { + let path = request.uri().path().to_string(); + // Get the Authorization header let auth_header = headers .get("Authorization") @@ -462,19 +468,31 @@ async fn auth_middleware( let token = match auth_header { Some(token) => token, - None => return Err(StatusCode::UNAUTHORIZED), + None => { + log::warn!("[api] Rejected {path}: missing Authorization header"); + return Err(StatusCode::UNAUTHORIZED); + } }; // Get the stored token let settings_manager = crate::settings_manager::SettingsManager::instance(); let stored_token = match settings_manager.get_api_token(&state.app_handle).await { Ok(Some(stored_token)) => stored_token, - Ok(None) => return Err(StatusCode::UNAUTHORIZED), - Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR), + Ok(None) => { + log::warn!( + "[api] Rejected {path}: API server has no stored token (was the API toggled off?)" + ); + return Err(StatusCode::UNAUTHORIZED); + } + Err(e) => { + log::error!("[api] Failed to read stored API token: {e}"); + return Err(StatusCode::INTERNAL_SERVER_ERROR); + } }; // Compare tokens if token != stored_token { + log::warn!("[api] Rejected {path}: token mismatch"); return Err(StatusCode::UNAUTHORIZED); } @@ -482,6 +500,38 @@ async fn auth_middleware( Ok(next.run(request).await) } +/// Logs every request: method, path, query, response status, duration. +/// Skips Authorization header and request bodies entirely. +async fn request_logging_middleware(request: axum::extract::Request, next: Next) -> Response { + let method = request.method().clone(); + let path = request.uri().path().to_string(); + let query = request.uri().query().map(|q| q.to_string()); + let started = std::time::Instant::now(); + + let response = next.run(request).await; + + let status = response.status(); + let elapsed_ms = started.elapsed().as_millis(); + + let level = if status.is_server_error() { + log::Level::Error + } else if status.is_client_error() { + log::Level::Warn + } else { + log::Level::Info + }; + + match query { + Some(q) => log::log!( + level, + "[api] {method} {path}?{q} -> {status} ({elapsed_ms} ms)" + ), + None => log::log!(level, "[api] {method} {path} -> {status} ({elapsed_ms} ms)"), + } + + response +} + // Global API server instance lazy_static! { pub static ref API_SERVER: Arc> = Arc::new(Mutex::new(ApiServer::new())); diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index b954931..82183d3 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1350,18 +1350,31 @@ pub fn run() { version_updater::VersionUpdater::run_background_task().await; }); - // Auto-start MCP server if it was previously enabled + // Auto-start MCP server if it was previously enabled. Always log the + // decision so customer logs reveal whether MCP is actually running — + // "automation features don't work" is otherwise indistinguishable from + // "MCP server isn't enabled" without this line. { let mcp_handle = app.handle().clone(); let settings_mgr = settings_manager::SettingsManager::instance(); - if let Ok(settings) = settings_mgr.load_settings() { - if settings.mcp_enabled { - tauri::async_runtime::spawn(async move { - match mcp_server::McpServer::instance().start(mcp_handle).await { - Ok(port) => log::info!("MCP server auto-started on port {port}"), - Err(e) => log::warn!("Failed to auto-start MCP server: {e}"), - } - }); + match settings_mgr.load_settings() { + Ok(settings) => { + if settings.mcp_enabled { + log::info!("MCP server is enabled in settings, attempting auto-start"); + tauri::async_runtime::spawn(async move { + match mcp_server::McpServer::instance().start(mcp_handle).await { + Ok(port) => log::info!("MCP server auto-started on port {port}"), + Err(e) => log::warn!("Failed to auto-start MCP server: {e}"), + } + }); + } else { + log::info!( + "MCP server is DISABLED in settings (mcp_enabled=false). Browser automation tools will not be available until it's enabled in Settings → Integrations." + ); + } + } + Err(e) => { + log::warn!("Could not read settings to determine MCP state: {e}"); } } } diff --git a/src-tauri/src/mcp_server.rs b/src-tauri/src/mcp_server.rs index 2d0b1b4..f237e5e 100644 --- a/src-tauri/src/mcp_server.rs +++ b/src-tauri/src/mcp_server.rs @@ -112,6 +112,17 @@ impl McpServer { async fn require_paid_subscription(feature: &str) -> Result<(), McpError> { if !CLOUD_AUTH.has_active_paid_subscription().await { + // Log the failed gate so customer logs explain why an MCP tool returned + // an error. Include enough state (logged-in vs not, plan, status) for + // support to diagnose without leaking secrets. + let summary = match CLOUD_AUTH.get_user().await { + Some(state) => format!( + "logged_in=true plan={} status={} period={:?}", + state.user.plan, state.user.subscription_status, state.user.plan_period, + ), + None => "logged_in=false".to_string(), + }; + log::warn!("[mcp] Rejected '{feature}' — paid subscription gate failed ({summary})"); return Err(McpError { code: -32000, message: format!("{feature} requires an active paid subscription"), @@ -1458,6 +1469,16 @@ impl McpServer { .cloned() .unwrap_or(serde_json::json!({})); + // Surface the call in logs so customer reports show which tools the MCP + // client is actually invoking (and therefore which gate any subsequent + // error came from). Log only the tool name and the profile_id arg — + // arbitrary URLs / JS / selectors can be sensitive. + let profile_id = arguments + .get("profile_id") + .and_then(|v| v.as_str()) + .unwrap_or(""); + log::info!("[mcp] tools/call name={tool_name} profile_id={profile_id}"); + match tool_name { "list_profiles" => self.handle_list_profiles().await, "get_profile" => self.handle_get_profile(&arguments).await,