All checks were successful
## 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
363 lines
19 KiB
Rust
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..." } },
|
|
}
|
|
}
|
|
}
|