feat: add MCP server for exposing compliance data to LLMs (#5)
Some checks failed
CI / Format (push) Successful in 3s
CI / Clippy (push) Successful in 4m4s
CI / Security Audit (push) Successful in 1m42s
CI / Tests (push) Successful in 4m38s
CI / Deploy Agent (push) Successful in 2s
CI / Deploy Dashboard (push) Successful in 1s
CI / Deploy MCP (push) Failing after 2s
CI / Detect Changes (push) Successful in 7s
CI / Deploy Docs (push) Successful in 2s
Some checks failed
CI / Format (push) Successful in 3s
CI / Clippy (push) Successful in 4m4s
CI / Security Audit (push) Successful in 1m42s
CI / Tests (push) Successful in 4m38s
CI / Deploy Agent (push) Successful in 2s
CI / Deploy Dashboard (push) Successful in 1s
CI / Deploy MCP (push) Failing after 2s
CI / Detect Changes (push) Successful in 7s
CI / Deploy Docs (push) Successful in 2s
New `compliance-mcp` crate providing a Model Context Protocol server with 7 tools: list/get/summarize findings, list SBOM packages, SBOM vulnerability report, list DAST findings, and DAST scan summary. Supports stdio (local dev) and Streamable HTTP (deployment via MCP_PORT). Includes Dockerfile, CI clippy check, and Coolify deploy job. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Sharang Parnerkar <parnerkarsharang@gmail.com> Reviewed-on: #5
This commit was merged in pull request #5.
This commit is contained in:
@@ -34,6 +34,8 @@ server = [
|
||||
"dep:url",
|
||||
"dep:sha2",
|
||||
"dep:base64",
|
||||
"dep:uuid",
|
||||
"dep:bson",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
@@ -67,3 +69,5 @@ rand = { version = "0.9", optional = true }
|
||||
url = { version = "2", optional = true }
|
||||
sha2 = { workspace = true, optional = true }
|
||||
base64 = { version = "0.22", optional = true }
|
||||
uuid = { workspace = true, optional = true }
|
||||
bson = { version = "2", features = ["chrono-0_4"], optional = true }
|
||||
|
||||
@@ -38,6 +38,8 @@ pub enum Route {
|
||||
DastFindingsPage {},
|
||||
#[route("/dast/findings/:id")]
|
||||
DastFindingDetailPage { id: String },
|
||||
#[route("/mcp-servers")]
|
||||
McpServersPage {},
|
||||
#[route("/settings")]
|
||||
SettingsPage {},
|
||||
}
|
||||
|
||||
@@ -24,12 +24,13 @@ pub fn AppShell() -> Element {
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(Ok(_)) => {
|
||||
rsx! { LoginPage {} }
|
||||
}
|
||||
Some(Err(e)) => {
|
||||
tracing::error!("Auth check failed: {e}");
|
||||
rsx! { LoginPage {} }
|
||||
Some(Ok(_)) | Some(Err(_)) => {
|
||||
// Not authenticated — redirect to Keycloak login
|
||||
rsx! {
|
||||
document::Script {
|
||||
dangerous_inner_html: "window.location.href = '/auth';"
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
rsx! {
|
||||
@@ -40,73 +41,3 @@ pub fn AppShell() -> Element {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn LoginPage() -> Element {
|
||||
rsx! {
|
||||
div { class: "login-page",
|
||||
div { class: "login-bg-grid" }
|
||||
div { class: "login-bg-glow" }
|
||||
div { class: "login-container",
|
||||
div { class: "login-card",
|
||||
div { class: "login-logo",
|
||||
svg {
|
||||
width: "48",
|
||||
height: "48",
|
||||
view_box: "0 0 24 24",
|
||||
fill: "none",
|
||||
stroke: "currentColor",
|
||||
stroke_width: "1.5",
|
||||
stroke_linecap: "round",
|
||||
stroke_linejoin: "round",
|
||||
path { d: "M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" }
|
||||
path { d: "M9 12l2 2 4-4" }
|
||||
}
|
||||
}
|
||||
h1 { class: "login-title", "Compliance Scanner" }
|
||||
p { class: "login-subtitle",
|
||||
"AI-powered security scanning, SBOM analysis, and compliance monitoring"
|
||||
}
|
||||
div { class: "login-features",
|
||||
div { class: "login-feature",
|
||||
span { class: "login-feature-icon", "\u{25C6}" }
|
||||
span { "SAST & CVE Detection" }
|
||||
}
|
||||
div { class: "login-feature",
|
||||
span { class: "login-feature-icon", "\u{25C6}" }
|
||||
span { "SBOM & License Compliance" }
|
||||
}
|
||||
div { class: "login-feature",
|
||||
span { class: "login-feature-icon", "\u{25C6}" }
|
||||
span { "Code Knowledge Graph" }
|
||||
}
|
||||
div { class: "login-feature",
|
||||
span { class: "login-feature-icon", "\u{25C6}" }
|
||||
span { "DAST & Impact Analysis" }
|
||||
}
|
||||
}
|
||||
a {
|
||||
href: "/auth",
|
||||
class: "login-button",
|
||||
svg {
|
||||
width: "20",
|
||||
height: "20",
|
||||
view_box: "0 0 24 24",
|
||||
fill: "none",
|
||||
stroke: "currentColor",
|
||||
stroke_width: "2",
|
||||
stroke_linecap: "round",
|
||||
stroke_linejoin: "round",
|
||||
rect { x: "3", y: "11", width: "18", height: "11", rx: "2", ry: "2" }
|
||||
path { d: "M7 11V7a5 5 0 0 1 10 0v4" }
|
||||
}
|
||||
span { "Sign in to continue" }
|
||||
}
|
||||
p { class: "login-footer",
|
||||
"Secured with single sign-on"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,11 @@ pub fn Sidebar() -> Element {
|
||||
route: Route::DastOverviewPage {},
|
||||
icon: rsx! { Icon { icon: BsBug, width: 18, height: 18 } },
|
||||
},
|
||||
NavItem {
|
||||
label: "MCP Servers",
|
||||
route: Route::McpServersPage {},
|
||||
icon: rsx! { Icon { icon: BsPlug, width: 18, height: 18 } },
|
||||
},
|
||||
NavItem {
|
||||
label: "Settings",
|
||||
route: Route::SettingsPage {},
|
||||
|
||||
@@ -42,4 +42,8 @@ impl Database {
|
||||
pub fn tracker_issues(&self) -> Collection<TrackerIssue> {
|
||||
self.inner.collection("tracker_issues")
|
||||
}
|
||||
|
||||
pub fn mcp_servers(&self) -> Collection<McpServerConfig> {
|
||||
self.inner.collection("mcp_servers")
|
||||
}
|
||||
}
|
||||
|
||||
160
compliance-dashboard/src/infrastructure/mcp.rs
Normal file
160
compliance-dashboard/src/infrastructure/mcp.rs
Normal file
@@ -0,0 +1,160 @@
|
||||
use dioxus::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use compliance_core::models::McpServerConfig;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct McpServersResponse {
|
||||
pub data: Vec<McpServerConfig>,
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn fetch_mcp_servers() -> Result<McpServersResponse, ServerFnError> {
|
||||
use mongodb::bson::doc;
|
||||
|
||||
let state: super::server_state::ServerState =
|
||||
dioxus_fullstack::FullstackContext::extract().await?;
|
||||
|
||||
let mut cursor = state
|
||||
.db
|
||||
.mcp_servers()
|
||||
.find(doc! {})
|
||||
.sort(doc! { "created_at": -1 })
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
|
||||
let mut data = Vec::new();
|
||||
while cursor
|
||||
.advance()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?
|
||||
{
|
||||
let server = cursor
|
||||
.deserialize_current()
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
data.push(server);
|
||||
}
|
||||
|
||||
Ok(McpServersResponse { data })
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn add_mcp_server(
|
||||
name: String,
|
||||
endpoint_url: String,
|
||||
transport: String,
|
||||
port: String,
|
||||
description: String,
|
||||
mongodb_uri: String,
|
||||
mongodb_database: String,
|
||||
) -> Result<(), ServerFnError> {
|
||||
use chrono::Utc;
|
||||
use compliance_core::models::{McpServerStatus, McpTransport};
|
||||
|
||||
let state: super::server_state::ServerState =
|
||||
dioxus_fullstack::FullstackContext::extract().await?;
|
||||
|
||||
let transport = match transport.as_str() {
|
||||
"http" => McpTransport::Http,
|
||||
_ => McpTransport::Stdio,
|
||||
};
|
||||
|
||||
let port_num: Option<u16> = port.parse().ok();
|
||||
|
||||
// Generate a random access token
|
||||
let token = format!("mcp_{}", uuid::Uuid::new_v4().to_string().replace('-', ""));
|
||||
|
||||
let all_tools = vec![
|
||||
"list_findings".to_string(),
|
||||
"get_finding".to_string(),
|
||||
"findings_summary".to_string(),
|
||||
"list_sbom_packages".to_string(),
|
||||
"sbom_vuln_report".to_string(),
|
||||
"list_dast_findings".to_string(),
|
||||
"dast_scan_summary".to_string(),
|
||||
];
|
||||
|
||||
let now = Utc::now();
|
||||
let server = McpServerConfig {
|
||||
id: None,
|
||||
name,
|
||||
endpoint_url,
|
||||
transport,
|
||||
port: port_num,
|
||||
status: McpServerStatus::Stopped,
|
||||
access_token: token,
|
||||
tools_enabled: all_tools,
|
||||
description: if description.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(description)
|
||||
},
|
||||
mongodb_uri: if mongodb_uri.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(mongodb_uri)
|
||||
},
|
||||
mongodb_database: if mongodb_database.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(mongodb_database)
|
||||
},
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
|
||||
state
|
||||
.db
|
||||
.mcp_servers()
|
||||
.insert_one(server)
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn delete_mcp_server(server_id: String) -> Result<(), ServerFnError> {
|
||||
use mongodb::bson::doc;
|
||||
|
||||
let state: super::server_state::ServerState =
|
||||
dioxus_fullstack::FullstackContext::extract().await?;
|
||||
|
||||
let oid = bson::oid::ObjectId::parse_str(&server_id)
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
|
||||
state
|
||||
.db
|
||||
.mcp_servers()
|
||||
.delete_one(doc! { "_id": oid })
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn regenerate_mcp_token(server_id: String) -> Result<String, ServerFnError> {
|
||||
use chrono::Utc;
|
||||
use mongodb::bson::doc;
|
||||
|
||||
let state: super::server_state::ServerState =
|
||||
dioxus_fullstack::FullstackContext::extract().await?;
|
||||
|
||||
let oid = bson::oid::ObjectId::parse_str(&server_id)
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
|
||||
let new_token = format!("mcp_{}", uuid::Uuid::new_v4().to_string().replace('-', ""));
|
||||
|
||||
state
|
||||
.db
|
||||
.mcp_servers()
|
||||
.update_one(
|
||||
doc! { "_id": oid },
|
||||
doc! { "$set": { "access_token": &new_token, "updated_at": Utc::now().to_rfc3339() } },
|
||||
)
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
|
||||
Ok(new_token)
|
||||
}
|
||||
@@ -6,6 +6,7 @@ pub mod dast;
|
||||
pub mod findings;
|
||||
pub mod graph;
|
||||
pub mod issues;
|
||||
pub mod mcp;
|
||||
pub mod repositories;
|
||||
pub mod sbom;
|
||||
pub mod scans;
|
||||
|
||||
328
compliance-dashboard/src/pages/mcp_servers.rs
Normal file
328
compliance-dashboard/src/pages/mcp_servers.rs
Normal file
@@ -0,0 +1,328 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
#[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);
|
||||
|
||||
// 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! {
|
||||
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 { class: "text-secondary", "No MCP servers registered. Add one to get started." }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
rsx! {
|
||||
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 => "mcp-status-running",
|
||||
compliance_core::models::McpServerStatus::Stopped => "mcp-status-stopped",
|
||||
compliance_core::models::McpServerStatus::Error => "mcp-status-error",
|
||||
};
|
||||
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();
|
||||
|
||||
rsx! {
|
||||
div { class: "card mcp-server-card mb-4",
|
||||
div { class: "mcp-server-header",
|
||||
div { class: "mcp-server-title",
|
||||
h3 { "{server.name}" }
|
||||
span { class: "mcp-status {status_class}",
|
||||
"{server.status}"
|
||||
}
|
||||
}
|
||||
div { class: "mcp-server-actions",
|
||||
button {
|
||||
class: "btn btn-sm btn-ghost",
|
||||
title: "Delete server",
|
||||
onclick: {
|
||||
let id = sid.clone();
|
||||
let name = name.clone();
|
||||
move |_| {
|
||||
confirm_delete.set(Some((id.clone(), name.clone())));
|
||||
}
|
||||
},
|
||||
"Delete"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref desc) = server.description {
|
||||
p { class: "text-secondary mb-3", "{desc}" }
|
||||
}
|
||||
|
||||
div { class: "mcp-config-grid",
|
||||
div { class: "mcp-config-item",
|
||||
span { class: "mcp-config-label", "Endpoint" }
|
||||
code { class: "mcp-config-value", "{server.endpoint_url}" }
|
||||
}
|
||||
div { class: "mcp-config-item",
|
||||
span { class: "mcp-config-label", "Transport" }
|
||||
span { class: "mcp-config-value", "{server.transport}" }
|
||||
}
|
||||
if let Some(port) = server.port {
|
||||
div { class: "mcp-config-item",
|
||||
span { class: "mcp-config-label", "Port" }
|
||||
span { class: "mcp-config-value", "{port}" }
|
||||
}
|
||||
}
|
||||
if let Some(ref db) = server.mongodb_database {
|
||||
div { class: "mcp-config-item",
|
||||
span { class: "mcp-config-label", "Database" }
|
||||
span { class: "mcp-config-value", "{db}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div { class: "mcp-tools-section",
|
||||
span { class: "mcp-config-label", "Enabled Tools" }
|
||||
div { class: "mcp-tools-list",
|
||||
for tool in server.tools_enabled.iter() {
|
||||
span { class: "mcp-tool-badge", "{tool}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div { class: "mcp-token-section",
|
||||
span { class: "mcp-config-label", "Access Token" }
|
||||
div { class: "mcp-token-row",
|
||||
code { class: "mcp-token-value",
|
||||
if is_token_visible {
|
||||
"{server.access_token}"
|
||||
} else {
|
||||
"mcp_••••••••••••••••••••••••••••"
|
||||
}
|
||||
}
|
||||
button {
|
||||
class: "btn btn-sm btn-ghost",
|
||||
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 { "Hide" } else { "Reveal" }
|
||||
}
|
||||
button {
|
||||
class: "btn btn-sm btn-ghost",
|
||||
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()),
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
"Regenerate"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div { class: "mcp-meta",
|
||||
span { class: "text-secondary",
|
||||
"Created {created_str}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Some(None) => rsx! { div { class: "card", p { "Failed to load MCP servers." } } },
|
||||
None => rsx! { div { class: "card", p { "Loading..." } } },
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ pub mod graph_explorer;
|
||||
pub mod graph_index;
|
||||
pub mod impact_analysis;
|
||||
pub mod issues;
|
||||
pub mod mcp_servers;
|
||||
pub mod overview;
|
||||
pub mod repositories;
|
||||
pub mod sbom;
|
||||
@@ -27,6 +28,7 @@ pub use graph_explorer::GraphExplorerPage;
|
||||
pub use graph_index::GraphIndexPage;
|
||||
pub use impact_analysis::ImpactAnalysisPage;
|
||||
pub use issues::IssuesPage;
|
||||
pub use mcp_servers::McpServersPage;
|
||||
pub use overview::OverviewPage;
|
||||
pub use repositories::RepositoriesPage;
|
||||
pub use sbom::SbomPage;
|
||||
|
||||
Reference in New Issue
Block a user