if let Some(idx) = gates.iter().position(|g| { g.action == q.action && g.target == q.target && g.namespace == q.namespace && g.params == q.params }) { found = Some(gates[idx].clone()); if q.consume { to_remove = Some(idx); } } if let Some(idx) = to_remove { gates.remove(idx); } }); match found { Some(record) => { if record.status == "authorized" { (axum::http::StatusCode::OK, "Authorized").into_response() } else { let msg = if let Some(r) = record.reason { format!("Action blocked. Reason: {}", r) } else { "Action blocked.".to_string() }; (axum::http::StatusCode::FORBIDDEN, msg).into_response() } } None => ( axum::http::StatusCode::NOT_FOUND, "Action not yet authorized (no gate record found).", ) .into_response(), } } async fn gate_set_handler( State(app_state): State>, Json(body): Json, ) -> axum::response::Response { let status = if body.block.unwrap_or(false) { "blocked".to_string() } else if body.authorize.unwrap_or(false) { "authorized".to_string() } else { "pending".to_string() }; let record = GateRecord { id: uuid::Uuid::new_v4().to_string(), action: body.action.clone(), target: body.target.clone(), namespace: body.namespace.clone(), params: body.params.clone(), status, reason: body.reason.clone(), timestamp: crate::handlers::utils::now_secs(), }; app_state.handler.state.gates.modify(|gates| { gates.retain(|g| !(g.action == record.action && g.target == record.target)); gates.push(record); }); (axum::http::StatusCode::OK, "Gate state updated.").into_response() } async fn run_server(state: Arc) -> Result<(), Box> { state.rebuild_index().await; tokio::spawn(index_committer_worker(Arc::clone(&state))); let app_state = Arc::new(AppState { handler: Arc::new(MemoryHandler::new(Arc::clone(&state))), clients: RwLock::new(HashMap::new()), next_id: AtomicUsize::new(1), }); let app_state_clone = Arc::clone(&app_state); let mut rx = state.activity_tx.subscribe(); tokio::spawn(async move { while let Ok(msg) = rx.recv().await { let senders: Vec<_> = app_state_clone .clients .read() .unwrap_or_else(|e| e.into_inner()) .values() .cloned() .collect(); for client_tx in senders { let _ = client_tx.try_send(msg.clone()); } } }); let app = Router::new() .route( "/api/version", get(|| async move { axum::Json(serde_json::json!({ "version": env!("APP_VERSION"), "git_hash": option_env!("GIT_HASH").unwrap_or("unknown") })) }), ) .route("/ws", get(ws_handler)) .route("/health", get(health_handler)) .route("/nvim/telemetry", post(nvim_telemetry_handler)) .route("/gate/verify", get(gate_verify_handler)) .route("/gate/set", post(gate_set_handler)) .route( "/shutdown", post( |headers: axum::http::HeaderMap, State(state): State>| async move { let token_path = state.handler.state.base_dir.join("admin.token"); let expected_token = tokio::fs::read_to_string(&token_path) .await .unwrap_or_default() .trim() .to_string(); let auth_header = headers .get(axum::http::header::AUTHORIZATION) .and_then(|h| h.to_str().ok()) .unwrap_or_default(); if expected_token.is_empty() || auth_header != format!("Bearer {}", expected_token) { return (axum::http::StatusCode::UNAUTHORIZED, "Unauthorized").into_response(); } std::thread::spawn(|| { tracing::info!( "Received shutdown request via /shutdown endpoint. Exiting process cleanly." ); std::thread::sleep(std::time::Duration::from_millis(100)); std::process::exit(0); }); (axum::http::StatusCode::OK, "Shutting down...").into_response() }, ), ) .route( "/", get(|| async move { axum::response::Html(include_str!("dashboard.html")) }), ) .route( "/api/graph", get({ let state_clone = app_state.handler.state.clone(); move || async move { let graph_json = state_clone.read_graph(|g| serde_json::to_string(g).unwrap_or_else(|_| "{}".to_string())); ([(axum::http::header::CONTENT_TYPE, "application/json")], graph_json) } }), ) .route( "/api/tasks/{id}/complete", post({ let state_clone = app_state.handler.state.clone(); move |axum::extract::Path(id): axum::extract::Path| async move { state_clone.tasks.modify(|tasks| { for t in tasks.iter_mut() { if t.id == id { t.status = "completed".to_string(); break; } } }); axum::Json(serde_json::json!({"status": "success"})) } }), ) .route( "/api/tasks", get({ let state_clone = app_state.handler.state.clone(); move || async move { let tasks_json = state_clone.tasks.read_with(|t| serde_json::to_string(t).unwrap_or_else(|_| "[]".to_string())); ([(axum::http::header::CONTENT_TYPE, "application/json")], tasks_json) } }), ) .route( "/api/sticky", get({ let state_clone = app_state.handler.state.clone(); move || async move { let sticky_json = state_clone.sticky.read_with(|s| serde_json::to_string(s).unwrap_or_else(|_| "[]".to_string())); ([(axum::http::header::CONTENT_TYPE, "application/json")], sticky_json) } }), ) .route( "/api/search", get({ let state_clone = app_state.handler.state.clone(); move |axum::extract::Query(params): axum::extract::Query< std::collections::HashMap, >| async move { if let Some(q) = params.get("q") && let Ok(idx) = state_clone.search_index.read() && let Ok(results) = idx.search(q, None) { let mut formatted_results = Vec::new(); for (id, doc_type, title, body, score) in results { formatted_results.push(serde_json::json!({ "id": id, "type_name": doc_type, "title": title, "content": body, "score": score })); } return axum::Json( serde_json::json!({ "results": formatted_results }), ); } axum::Json(serde_json::json!({ "results": [] })) } }), ) .route( "/api/activity", get({ let state_clone = app_state.handler.state.clone(); move || async move { let activities_json = state_clone.recent_activities.read_with(|a| serde_json::to_string(a).unwrap_or_else(|_| "[]".to_string())); ([(axum::http::header::CONTENT_TYPE, "application/json")], activities_json) } }), ) .route( "/api/stats", get({ let state_clone = app_state.handler.state.clone(); move || async move { let (entities, relations) = state_clone.read_graph(|g| (g.entities.len(), g.relations.len())); let tasks = state_clone.tasks.read_with(|items| items.len()); let snippets = state_clone.snippets.read_with(|items| items.len()); let tech_debts = state_clone.tech_debts.read_with(|items| items.len()); let adrs = state_clone.adrs.read_with(|items| items.len()); let ledger = state_clone.ledger.read_with(|items| items.len()); let error_fixes = state_clone.error_fixes.read_with(|items| items.len()); let session_summaries = state_clone.session_summaries.read_with(|items| items.len()); let handoff_memos = state_clone.handoff_memos.read_with(|items| items.len()); let env_fingerprints = state_clone.env_fingerprints.read_with(|items| items.len()); let env_requirements = state_clone.env_requirements.read_with(|items| items.len()); let milestones = state_clone.milestones.read_with(|items| items.len()); let environments = state_clone.environments.read_with(|items| items.len()); let gates = state_clone.gates.read_with(|items| items.len()); axum::Json(serde_json::json!({ "entities": entities, "relations": relations, "tasks": tasks, "snippets": snippets, "tech_debts": tech_debts, "adrs": adrs, "ledger": ledger, "error_fixes": error_fixes, "session_summaries": session_summaries, "handoff_memos": handoff_memos, "env_fingerprints": env_fingerprints, "env_requirements": env_requirements, "milestones": milestones, "environments": environments, "gates": gates })) } }), ) .with_state(app_state); tracing::info!("MCP Memory Server running on http://127.0.0.1:3000/sse"); let addr = std::env::var("MCP_PORT").unwrap_or_else(|_| "3000".to_string()); let addr: std::net::SocketAddr = format!("127.0.0.1:{}", addr) .parse() .expect("Invalid bind address"); let listener = match tokio::net::TcpListener::bind(&addr).await { Ok(l) => l, Err(e) => { let log_path = dirs::home_dir() .unwrap_or_default() .join(".gemini/mcp_memory/daemon_error.log"); let _ = tokio::fs::write(&log_path, format!("Failed to bind to {}: {}\n", addr, e)).await; return Ok(()); } }; if let Err(e) = axum::serve(listener, app.into_make_service()).await { let log_path = dirs::home_dir() .unwrap_or_default() .join(".gemini/mcp_memory/daemon_error.log"); let _ = tokio::fs::write(&log_path, format!("Server crashed: {}\n", e)).await; }