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

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:
2026-03-09 08:21:04 +00:00
parent d13cef94cb
commit 32e5fc21e7
28 changed files with 1847 additions and 224 deletions

View 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..." } } },
}
}
}

View File

@@ -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;