Files
compliance-scanner-agent/compliance-dashboard/src/pages/mcp_servers.rs
Sharang Parnerkar 745ad8a441
All checks were successful
CI / Check (push) Has been skipped
CI / Detect Changes (push) Successful in 3s
CI / Deploy Agent (push) Successful in 2s
CI / Deploy Dashboard (push) Successful in 2s
CI / Deploy Docs (push) Has been skipped
CI / Deploy MCP (push) Successful in 2s
fix: check Gitea API response status and fallback for PR reviews (#47)
## Summary
- Add HTTP response status checking to all Gitea tracker methods that were silently swallowing errors
- Add fallback in create_pr_review: if inline comments fail, retry as plain PR comment

## Test plan
- [ ] Deploy and trigger a PR review, check logs for actual error details
- [ ] Verify fallback posts summary comment when inline comments fail

Co-authored-by: Sharang Parnerkar <parnerkarsharang@gmail.com>
Co-authored-by: Sharang Parnerkar <30073382+mighty840@users.noreply.github.com>
Reviewed-on: #47
2026-03-25 16:26:09 +00:00

363 lines
19 KiB
Rust

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::<Toasts>();
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<Option<String>> = use_signal(|| None);
// Track which server is pending delete confirmation
let mut confirm_delete: Signal<Option<(String, String)>> = 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..." } },
}
}
}