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

@@ -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")
}
}

View 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)
}

View File

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