diff --git a/compliance-dashboard/src/infrastructure/mcp.rs b/compliance-dashboard/src/infrastructure/mcp.rs index 3844682..80fedb3 100644 --- a/compliance-dashboard/src/infrastructure/mcp.rs +++ b/compliance-dashboard/src/infrastructure/mcp.rs @@ -113,6 +113,64 @@ pub async fn add_mcp_server( Ok(()) } +/// Probe each MCP server's health endpoint and update status in MongoDB. +#[server] +pub async fn refresh_mcp_status() -> Result<(), ServerFnError> { + use chrono::Utc; + use compliance_core::models::McpServerStatus; + use mongodb::bson::doc; + + let state: super::server_state::ServerState = + dioxus_fullstack::FullstackContext::extract().await?; + + let mut cursor = state + .db + .mcp_servers() + .find(doc! {}) + .await + .map_err(|e| ServerFnError::new(e.to_string()))?; + + let client = reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(5)) + .build() + .map_err(|e| ServerFnError::new(e.to_string()))?; + + while cursor + .advance() + .await + .map_err(|e| ServerFnError::new(e.to_string()))? + { + let server: compliance_core::models::McpServerConfig = cursor + .deserialize_current() + .map_err(|e| ServerFnError::new(e.to_string()))?; + + let Some(oid) = server.id else { continue }; + + // Derive health URL from the endpoint (replace trailing /mcp with /health) + let health_url = if server.endpoint_url.ends_with("/mcp") { + format!("{}health", &server.endpoint_url[..server.endpoint_url.len() - 3]) + } else { + format!("{}/health", server.endpoint_url.trim_end_matches('/')) + }; + + let new_status = match client.get(&health_url).send().await { + Ok(resp) if resp.status().is_success() => McpServerStatus::Running, + _ => McpServerStatus::Stopped, + }; + + let _ = state + .db + .mcp_servers() + .update_one( + doc! { "_id": oid }, + doc! { "$set": { "status": bson::to_bson(&new_status).unwrap(), "updated_at": Utc::now().to_rfc3339() } }, + ) + .await; + } + + Ok(()) +} + #[server] pub async fn delete_mcp_server(server_id: String) -> Result<(), ServerFnError> { use mongodb::bson::doc; diff --git a/compliance-dashboard/src/pages/mcp_servers.rs b/compliance-dashboard/src/pages/mcp_servers.rs index 583dab1..2a47f37 100644 --- a/compliance-dashboard/src/pages/mcp_servers.rs +++ b/compliance-dashboard/src/pages/mcp_servers.rs @@ -5,7 +5,7 @@ use dioxus_free_icons::Icon; use crate::components::page_header::PageHeader; use crate::components::toast::{ToastType, Toasts}; use crate::infrastructure::mcp::{ - add_mcp_server, delete_mcp_server, fetch_mcp_servers, regenerate_mcp_token, + add_mcp_server, delete_mcp_server, fetch_mcp_servers, refresh_mcp_status, regenerate_mcp_token, }; #[component] @@ -22,6 +22,17 @@ pub fn McpServersPage() -> Element { let mut new_mongo_uri = use_signal(String::new); let mut new_mongo_db = use_signal(String::new); + // Probe health of all MCP servers on page load, then refresh the list + let mut refreshing = use_signal(|| true); + use_effect(move || { + spawn(async move { + refreshing.set(true); + let _ = refresh_mcp_status().await; + servers.restart(); + refreshing.set(false); + }); + }); + // Track which server's token is visible let mut visible_token: Signal> = use_signal(|| None); // Track which server is pending delete confirmation diff --git a/compliance-mcp/src/main.rs b/compliance-mcp/src/main.rs index df152c7..7bcfd96 100644 --- a/compliance-mcp/src/main.rs +++ b/compliance-mcp/src/main.rs @@ -41,7 +41,9 @@ async fn main() -> Result<(), Box> { StreamableHttpServerConfig::default(), ); - let router = axum::Router::new().nest_service("/mcp", service); + let router = axum::Router::new() + .route("/health", axum::routing::get(|| async { "ok" })) + .nest_service("/mcp", service); let listener = tokio::net::TcpListener::bind(("0.0.0.0", port)).await?; tracing::info!("MCP HTTP server listening on 0.0.0.0:{port}"); axum::serve(listener, router).await?;