use dioxus::prelude::*; use dioxus_free_icons::icons::bs_icons::*; 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, refresh_mcp_status, regenerate_mcp_token, }; #[component] pub fn McpServersPage() -> Element { let mut servers = use_resource(|| async { fetch_mcp_servers().await.ok() }); let mut toasts = use_context::(); let mut show_form = use_signal(|| false); let mut new_name = use_signal(String::new); let mut new_endpoint = use_signal(String::new); let mut new_transport = use_signal(|| "http".to_string()); let mut new_port = use_signal(|| "8090".to_string()); let mut new_description = use_signal(String::new); 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 let mut confirm_delete: Signal> = use_signal(|| None); rsx! { div { class: "back-nav", button { class: "btn btn-ghost btn-back", onclick: move |_| { navigator().go_back(); }, Icon { icon: BsArrowLeft, width: 16, height: 16 } "Back" } } PageHeader { title: "MCP Servers", description: "Manage Model Context Protocol servers for LLM integrations", } div { class: "mb-4", button { class: "btn btn-primary", onclick: move |_| show_form.set(!show_form()), if show_form() { "Cancel" } else { "Register Server" } } } if show_form() { div { class: "card mb-4", div { class: "card-header", "Register MCP Server" } div { class: "mcp-form-grid", div { class: "form-group", label { "Name" } input { r#type: "text", placeholder: "Production MCP", value: "{new_name}", oninput: move |e| new_name.set(e.value()), } } div { class: "form-group", label { "Endpoint URL" } input { r#type: "text", placeholder: "https://mcp.example.com/mcp", value: "{new_endpoint}", oninput: move |e| new_endpoint.set(e.value()), } } div { class: "form-group", label { "Transport" } select { value: "{new_transport}", oninput: move |e| new_transport.set(e.value()), option { value: "http", "HTTP (Streamable)" } option { value: "stdio", "Stdio" } } } div { class: "form-group", label { "Port" } input { r#type: "text", placeholder: "8090", value: "{new_port}", oninput: move |e| new_port.set(e.value()), } } div { class: "form-group", label { "MongoDB URI" } input { r#type: "text", placeholder: "mongodb://localhost:27017", value: "{new_mongo_uri}", oninput: move |e| new_mongo_uri.set(e.value()), } } div { class: "form-group", label { "Database Name" } input { r#type: "text", placeholder: "compliance_scanner", value: "{new_mongo_db}", oninput: move |e| new_mongo_db.set(e.value()), } } } div { class: "form-group", label { "Description" } input { r#type: "text", placeholder: "Optional notes about this server", value: "{new_description}", oninput: move |e| new_description.set(e.value()), } } button { class: "btn btn-primary", onclick: move |_| { let name = new_name(); let endpoint = new_endpoint(); let transport = new_transport(); let port = new_port(); let desc = new_description(); let mongo_uri = new_mongo_uri(); let mongo_db = new_mongo_db(); spawn(async move { match add_mcp_server(name, endpoint, transport, port, desc, mongo_uri, mongo_db).await { Ok(_) => { toasts.push(ToastType::Success, "MCP server registered"); servers.restart(); } Err(e) => toasts.push(ToastType::Error, e.to_string()), } }); show_form.set(false); new_name.set(String::new()); new_endpoint.set(String::new()); new_transport.set("http".to_string()); new_port.set("8090".to_string()); new_description.set(String::new()); new_mongo_uri.set(String::new()); new_mongo_db.set(String::new()); }, "Register" } } } // Delete confirmation modal if let Some((ref del_id, ref del_name)) = *confirm_delete.read() { div { class: "modal-overlay", onclick: move |_| confirm_delete.set(None), div { class: "modal-dialog", onclick: move |e| e.stop_propagation(), h3 { "Delete MCP Server" } p { "Are you sure you want to remove " strong { "{del_name}" } "?" } p { class: "text-secondary", "Connected LLM clients will lose access." } div { class: "modal-actions", button { class: "btn btn-ghost", onclick: move |_| confirm_delete.set(None), "Cancel" } button { class: "btn btn-danger", onclick: { let id = del_id.clone(); move |_| { let id = id.clone(); spawn(async move { match delete_mcp_server(id).await { Ok(_) => { toasts.push(ToastType::Success, "Server removed"); servers.restart(); } Err(e) => toasts.push(ToastType::Error, e.to_string()), } }); confirm_delete.set(None); } }, "Delete" } } } } } match &*servers.read() { Some(Some(resp)) => { if resp.data.is_empty() { rsx! { div { class: "card", p { style: "padding: 1rem; color: var(--text-secondary);", "No MCP servers registered. Add one to get started." } } } } else { rsx! { div { class: "mcp-cards-grid", for server in resp.data.iter() { { let sid = server.id.map(|id| id.to_hex()).unwrap_or_default(); let name = server.name.clone(); let status_class = match server.status { compliance_core::models::McpServerStatus::Running => "running", compliance_core::models::McpServerStatus::Stopped => "stopped", compliance_core::models::McpServerStatus::Error => "error", }; let status_label = format!("{}", server.status); let is_token_visible = visible_token().as_deref() == Some(sid.as_str()); let created_str = server.created_at.format("%Y-%m-%d %H:%M").to_string(); let tools_count = server.tools_enabled.len(); rsx! { div { class: "mcp-card", // Header row: status dot + name + actions div { class: "mcp-card-header", div { class: "mcp-card-title", span { class: "mcp-status-dot {status_class}" } h3 { "{server.name}" } span { class: "mcp-card-status {status_class}", "{status_label}" } } button { class: "btn btn-sm btn-ghost btn-ghost-danger", title: "Delete server", onclick: { let id = sid.clone(); let name = name.clone(); move |_| { confirm_delete.set(Some((id.clone(), name.clone()))); } }, Icon { icon: BsTrash, width: 14, height: 14 } } } if let Some(ref desc) = server.description { p { class: "mcp-card-desc", "{desc}" } } // Config details div { class: "mcp-card-details", div { class: "mcp-detail-row", Icon { icon: BsGlobe, width: 13, height: 13 } span { class: "mcp-detail-label", "Endpoint" } code { class: "mcp-detail-value", "{server.endpoint_url}" } } div { class: "mcp-detail-row", Icon { icon: BsHddNetwork, width: 13, height: 13 } span { class: "mcp-detail-label", "Transport" } span { class: "mcp-detail-value", "{server.transport}" } } if let Some(port) = server.port { div { class: "mcp-detail-row", Icon { icon: BsPlug, width: 13, height: 13 } span { class: "mcp-detail-label", "Port" } span { class: "mcp-detail-value", "{port}" } } } } // Tools div { class: "mcp-card-tools", span { class: "mcp-detail-label", Icon { icon: BsTools, width: 13, height: 13 } " {tools_count} tools" } div { class: "mcp-tools-list", for tool in server.tools_enabled.iter() { span { class: "mcp-tool-chip", "{tool}" } } } } // Token section div { class: "mcp-card-token", div { class: "mcp-token-display", Icon { icon: BsKey, width: 13, height: 13 } code { class: "mcp-token-code", if is_token_visible { "{server.access_token}" } else { "mcp_••••••••••••••••••••" } } } div { class: "mcp-token-actions", button { class: "btn btn-sm btn-ghost", title: if is_token_visible { "Hide token" } else { "Reveal token" }, onclick: { let id = sid.clone(); move |_| { if visible_token().as_deref() == Some(id.as_str()) { visible_token.set(None); } else { visible_token.set(Some(id.clone())); } } }, if is_token_visible { Icon { icon: BsEyeSlash, width: 14, height: 14 } } else { Icon { icon: BsEye, width: 14, height: 14 } } } button { class: "btn btn-sm btn-ghost", title: "Regenerate token", onclick: { let id = sid.clone(); move |_| { let id = id.clone(); spawn(async move { match regenerate_mcp_token(id).await { Ok(_) => { toasts.push(ToastType::Success, "Token regenerated"); servers.restart(); } Err(e) => toasts.push(ToastType::Error, e.to_string()), } }); } }, Icon { icon: BsArrowRepeat, width: 14, height: 14 } } } } // Footer div { class: "mcp-card-footer", span { "Created {created_str}" } } } } } } } } } }, Some(None) => rsx! { div { class: "card", p { style: "padding: 1rem;", "Failed to load MCP servers." } } }, None => rsx! { div { class: "loading", "Loading..." } }, } } }