feat: add MCP server for exposing compliance data to LLMs #5
@@ -70,6 +70,8 @@ jobs:
|
||||
run: cargo clippy -p compliance-dashboard --features server --no-default-features -- -D warnings
|
||||
- name: Clippy (dashboard web)
|
||||
run: cargo clippy -p compliance-dashboard --features web --no-default-features -- -D warnings
|
||||
- name: Clippy (mcp)
|
||||
run: cargo clippy -p compliance-mcp -- -D warnings
|
||||
- name: Show sccache stats
|
||||
run: sccache --show-stats
|
||||
if: always()
|
||||
@@ -140,6 +142,7 @@ jobs:
|
||||
agent: ${{ steps.changes.outputs.agent }}
|
||||
dashboard: ${{ steps.changes.outputs.dashboard }}
|
||||
docs: ${{ steps.changes.outputs.docs }}
|
||||
mcp: ${{ steps.changes.outputs.mcp }}
|
||||
steps:
|
||||
- name: Install git
|
||||
run: apk add --no-cache git
|
||||
@@ -177,6 +180,13 @@ jobs:
|
||||
echo "docs=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
# MCP: core libs, mcp code, mcp Dockerfile
|
||||
if echo "$CHANGED" | grep -qE '^(compliance-core/|compliance-mcp/|Dockerfile\.mcp|Cargo\.(toml|lock))'; then
|
||||
echo "mcp=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "mcp=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
deploy-agent:
|
||||
name: Deploy Agent
|
||||
runs-on: docker
|
||||
@@ -218,3 +228,17 @@ jobs:
|
||||
apk add --no-cache curl
|
||||
curl -sf "${{ secrets.COOLIFY_WEBHOOK_DOCS }}" \
|
||||
-H "Authorization: Bearer ${{ secrets.COOLIFY_TOKEN }}"
|
||||
|
||||
deploy-mcp:
|
||||
name: Deploy MCP
|
||||
runs-on: docker
|
||||
needs: [detect-changes]
|
||||
if: needs.detect-changes.outputs.mcp == 'true'
|
||||
container:
|
||||
image: alpine:latest
|
||||
steps:
|
||||
- name: Trigger Coolify deploy
|
||||
run: |
|
||||
apk add --no-cache curl
|
||||
curl -sf "${{ secrets.COOLIFY_WEBHOOK_MCP }}" \
|
||||
-H "Authorization: Bearer ${{ secrets.COOLIFY_TOKEN }}"
|
||||
|
||||
235
Cargo.lock
generated
235
Cargo.lock
generated
@@ -167,7 +167,7 @@ dependencies = [
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-tungstenite 0.28.0",
|
||||
"tower 0.5.3",
|
||||
"tower",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
@@ -413,6 +413,17 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "chacha20"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.3.0",
|
||||
"rand_core 0.10.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "charset"
|
||||
version = "0.1.5"
|
||||
@@ -675,6 +686,27 @@ dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "compliance-mcp"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"bson",
|
||||
"chrono",
|
||||
"compliance-core",
|
||||
"dotenvy",
|
||||
"mongodb",
|
||||
"rmcp",
|
||||
"schemars 1.2.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tower-http",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "console_error_panic_hook"
|
||||
version = "0.1.7"
|
||||
@@ -869,6 +901,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.0"
|
||||
@@ -972,8 +1013,18 @@ version = "0.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_macro",
|
||||
"darling_core 0.21.3",
|
||||
"darling_macro 0.21.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
|
||||
dependencies = [
|
||||
"darling_core 0.23.0",
|
||||
"darling_macro 0.23.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -990,13 +1041,37 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0"
|
||||
dependencies = [
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_core 0.21.3",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
|
||||
dependencies = [
|
||||
"darling_core 0.23.0",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
@@ -1335,7 +1410,7 @@ dependencies = [
|
||||
"tokio-stream",
|
||||
"tokio-tungstenite 0.27.0",
|
||||
"tokio-util",
|
||||
"tower 0.5.3",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tower-layer",
|
||||
"tracing",
|
||||
@@ -1626,7 +1701,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-tungstenite 0.27.0",
|
||||
"tokio-util",
|
||||
"tower 0.5.3",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tracing",
|
||||
"tracing-futures",
|
||||
@@ -1827,7 +1902,7 @@ version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f43e744e4ea338060faee68ed933e46e722fb7f3617e722a5772d7e856d8b3ce"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"darling 0.21.3",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
@@ -2104,6 +2179,7 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"rand_core 0.10.0",
|
||||
"wasip2",
|
||||
"wasip3",
|
||||
]
|
||||
@@ -3497,7 +3573,7 @@ dependencies = [
|
||||
"serde_urlencoded",
|
||||
"snafu",
|
||||
"tokio",
|
||||
"tower 0.5.3",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tracing",
|
||||
"url",
|
||||
@@ -3599,8 +3675,6 @@ dependencies = [
|
||||
"prost",
|
||||
"reqwest",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tonic",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
@@ -3668,6 +3742,12 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pastey"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b867cad97c0791bbd3aaa6472142568c6c9e8f71937e98379f584cfb0cf35bec"
|
||||
|
||||
[[package]]
|
||||
name = "pbkdf2"
|
||||
version = "0.12.2"
|
||||
@@ -4005,6 +4085,17 @@ dependencies = [
|
||||
"rand_core 0.9.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8"
|
||||
dependencies = [
|
||||
"chacha20",
|
||||
"getrandom 0.4.1",
|
||||
"rand_core 0.10.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.3.1"
|
||||
@@ -4043,6 +4134,12 @@ dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba"
|
||||
|
||||
[[package]]
|
||||
name = "rand_distr"
|
||||
version = "0.4.3"
|
||||
@@ -4163,6 +4260,7 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
"rustls",
|
||||
"rustls-native-certs",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -4171,7 +4269,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-util",
|
||||
"tower 0.5.3",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tower-service",
|
||||
"url",
|
||||
@@ -4202,6 +4300,50 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rmcp"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc4c9c94680f75470ee8083a0667988b5d7b5beb70b9f998a8e51de7c682ce60"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"futures",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"pastey",
|
||||
"pin-project-lite",
|
||||
"rand 0.10.0",
|
||||
"rmcp-macros",
|
||||
"schemars 1.2.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sse-stream",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tokio-util",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rmcp-macros"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90c23c8f26cae4da838fbc3eadfaecf2d549d97c04b558e7bd90526a9c28b42a"
|
||||
dependencies = [
|
||||
"darling 0.23.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde_json",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-stemmers"
|
||||
version = "1.2.0"
|
||||
@@ -4365,12 +4507,26 @@ version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"dyn-clone",
|
||||
"ref-cast",
|
||||
"schemars_derive",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schemars_derive"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde_derive_internals",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
@@ -4510,6 +4666,17 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive_internals"
|
||||
version = "0.29.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.149"
|
||||
@@ -4594,7 +4761,7 @@ version = "3.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6d4e30573c8cb306ed6ab1dca8423eec9a463ea0e155f45399455e0368b27e0"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"darling 0.21.3",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
@@ -4616,7 +4783,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"cpufeatures 0.2.17",
|
||||
"digest",
|
||||
]
|
||||
|
||||
@@ -4627,7 +4794,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"cpufeatures 0.2.17",
|
||||
"digest",
|
||||
]
|
||||
|
||||
@@ -4781,6 +4948,19 @@ version = "0.9.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||
|
||||
[[package]]
|
||||
name = "sse-stream"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb4dc4d33c68ec1f27d386b5610a351922656e1fdf5c05bbaad930cd1519479a"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.1"
|
||||
@@ -5364,35 +5544,10 @@ dependencies = [
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-timeout",
|
||||
"hyper-util",
|
||||
"percent-encoding",
|
||||
"pin-project",
|
||||
"prost",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tower 0.4.13",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower"
|
||||
version = "0.4.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"indexmap 1.9.3",
|
||||
"pin-project",
|
||||
"pin-project-lite",
|
||||
"rand 0.8.5",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
@@ -5453,7 +5608,7 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tower 0.5.3",
|
||||
"tower",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
|
||||
@@ -5,6 +5,7 @@ members = [
|
||||
"compliance-dashboard",
|
||||
"compliance-graph",
|
||||
"compliance-dast",
|
||||
"compliance-mcp",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
|
||||
16
Dockerfile.mcp
Normal file
16
Dockerfile.mcp
Normal file
@@ -0,0 +1,16 @@
|
||||
FROM rust:1.89-bookworm AS builder
|
||||
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
RUN cargo build --release -p compliance-mcp
|
||||
|
||||
FROM debian:bookworm-slim
|
||||
RUN apt-get update && apt-get install -y ca-certificates libssl3 && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=builder /app/target/release/compliance-mcp /usr/local/bin/compliance-mcp
|
||||
|
||||
EXPOSE 8090
|
||||
|
||||
ENV MCP_PORT=8090
|
||||
|
||||
ENTRYPOINT ["compliance-mcp"]
|
||||
21
compliance-mcp/Cargo.toml
Normal file
21
compliance-mcp/Cargo.toml
Normal file
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "compliance-mcp"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
compliance-core = { workspace = true, features = ["mongodb"] }
|
||||
rmcp = { version = "0.16", features = ["server", "macros", "transport-io", "transport-streamable-http-server"] }
|
||||
tokio = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
mongodb = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
dotenvy = "0.15"
|
||||
thiserror = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
bson = { version = "2", features = ["chrono-0_4"] }
|
||||
schemars = "1.0"
|
||||
axum = "0.8"
|
||||
tower-http = { version = "0.6", features = ["cors"] }
|
||||
34
compliance-mcp/src/database.rs
Normal file
34
compliance-mcp/src/database.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
use mongodb::{Client, Collection};
|
||||
|
||||
use compliance_core::models::*;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Database {
|
||||
inner: mongodb::Database,
|
||||
}
|
||||
|
||||
impl Database {
|
||||
pub async fn connect(uri: &str, db_name: &str) -> Result<Self, mongodb::error::Error> {
|
||||
let client = Client::with_uri_str(uri).await?;
|
||||
let db = client.database(db_name);
|
||||
db.run_command(mongodb::bson::doc! { "ping": 1 }).await?;
|
||||
tracing::info!("MCP server connected to MongoDB '{db_name}'");
|
||||
Ok(Self { inner: db })
|
||||
}
|
||||
|
||||
pub fn findings(&self) -> Collection<Finding> {
|
||||
self.inner.collection("findings")
|
||||
}
|
||||
|
||||
pub fn sbom_entries(&self) -> Collection<SbomEntry> {
|
||||
self.inner.collection("sbom_entries")
|
||||
}
|
||||
|
||||
pub fn dast_findings(&self) -> Collection<DastFinding> {
|
||||
self.inner.collection("dast_findings")
|
||||
}
|
||||
|
||||
pub fn dast_scan_runs(&self) -> Collection<DastScanRun> {
|
||||
self.inner.collection("dast_scan_runs")
|
||||
}
|
||||
}
|
||||
58
compliance-mcp/src/main.rs
Normal file
58
compliance-mcp/src/main.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
mod database;
|
||||
mod server;
|
||||
mod tools;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use database::Database;
|
||||
use rmcp::transport::{
|
||||
streamable_http_server::session::local::LocalSessionManager, StreamableHttpServerConfig,
|
||||
StreamableHttpService,
|
||||
};
|
||||
use server::ComplianceMcpServer;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let _ = dotenvy::dotenv();
|
||||
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
tracing_subscriber::EnvFilter::from_default_env()
|
||||
.add_directive("compliance_mcp=info".parse()?),
|
||||
)
|
||||
.init();
|
||||
|
||||
let mongo_uri =
|
||||
std::env::var("MONGODB_URI").unwrap_or_else(|_| "mongodb://localhost:27017".to_string());
|
||||
let db_name =
|
||||
std::env::var("MONGODB_DATABASE").unwrap_or_else(|_| "compliance_scanner".to_string());
|
||||
|
||||
let db = Database::connect(&mongo_uri, &db_name).await?;
|
||||
|
||||
// If MCP_PORT is set, run as Streamable HTTP server; otherwise use stdio.
|
||||
if let Ok(port_str) = std::env::var("MCP_PORT") {
|
||||
let port: u16 = port_str.parse()?;
|
||||
tracing::info!("Starting MCP server on HTTP port {port}");
|
||||
|
||||
let db_clone = db.clone();
|
||||
let service = StreamableHttpService::new(
|
||||
move || Ok(ComplianceMcpServer::new(db_clone.clone())),
|
||||
Arc::new(LocalSessionManager::default()),
|
||||
StreamableHttpServerConfig::default(),
|
||||
);
|
||||
|
||||
let router = axum::Router::new().nest_service("/mcp", service);
|
||||
let listener = tokio::net::TcpListener::bind(("0.0.0.0", port)).await?;
|
||||
tracing::info!("MCP HTTP server listening on 0.0.0.0:{port}");
|
||||
axum::serve(listener, router).await?;
|
||||
} else {
|
||||
tracing::info!("Starting MCP server on stdio");
|
||||
let server = ComplianceMcpServer::new(db);
|
||||
let transport = rmcp::transport::stdio();
|
||||
use rmcp::ServiceExt;
|
||||
let handle = server.serve(transport).await?;
|
||||
handle.waiting().await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
109
compliance-mcp/src/server.rs
Normal file
109
compliance-mcp/src/server.rs
Normal file
@@ -0,0 +1,109 @@
|
||||
use rmcp::{
|
||||
handler::server::wrapper::Parameters, model::*, tool, tool_handler, tool_router, ServerHandler,
|
||||
};
|
||||
|
||||
use crate::database::Database;
|
||||
use crate::tools::{dast, findings, sbom};
|
||||
|
||||
pub struct ComplianceMcpServer {
|
||||
db: Database,
|
||||
#[allow(dead_code)]
|
||||
tool_router: rmcp::handler::server::router::tool::ToolRouter<Self>,
|
||||
}
|
||||
|
||||
#[tool_router]
|
||||
impl ComplianceMcpServer {
|
||||
pub fn new(db: Database) -> Self {
|
||||
Self {
|
||||
db,
|
||||
tool_router: Self::tool_router(),
|
||||
}
|
||||
}
|
||||
|
||||
// ── Findings ──────────────────────────────────────────
|
||||
|
||||
#[tool(
|
||||
description = "List security findings with optional filters for repo, severity, status, and scan type"
|
||||
)]
|
||||
async fn list_findings(
|
||||
&self,
|
||||
Parameters(params): Parameters<findings::ListFindingsParams>,
|
||||
) -> Result<CallToolResult, rmcp::ErrorData> {
|
||||
findings::list_findings(&self.db, params).await
|
||||
}
|
||||
|
||||
#[tool(description = "Get a single finding by its ID")]
|
||||
async fn get_finding(
|
||||
&self,
|
||||
Parameters(params): Parameters<findings::GetFindingParams>,
|
||||
) -> Result<CallToolResult, rmcp::ErrorData> {
|
||||
findings::get_finding(&self.db, params).await
|
||||
}
|
||||
|
||||
#[tool(description = "Get a summary of findings counts grouped by severity and status")]
|
||||
async fn findings_summary(
|
||||
&self,
|
||||
Parameters(params): Parameters<findings::FindingsSummaryParams>,
|
||||
) -> Result<CallToolResult, rmcp::ErrorData> {
|
||||
findings::findings_summary(&self.db, params).await
|
||||
}
|
||||
|
||||
// ── SBOM ──────────────────────────────────────────────
|
||||
|
||||
#[tool(
|
||||
description = "List SBOM packages with optional filters for repo, vulnerabilities, package manager, and license"
|
||||
)]
|
||||
async fn list_sbom_packages(
|
||||
&self,
|
||||
Parameters(params): Parameters<sbom::ListSbomPackagesParams>,
|
||||
) -> Result<CallToolResult, rmcp::ErrorData> {
|
||||
sbom::list_sbom_packages(&self.db, params).await
|
||||
}
|
||||
|
||||
#[tool(
|
||||
description = "Generate a vulnerability report for a repository showing all packages with known CVEs"
|
||||
)]
|
||||
async fn sbom_vuln_report(
|
||||
&self,
|
||||
Parameters(params): Parameters<sbom::SbomVulnReportParams>,
|
||||
) -> Result<CallToolResult, rmcp::ErrorData> {
|
||||
sbom::sbom_vuln_report(&self.db, params).await
|
||||
}
|
||||
|
||||
// ── DAST ──────────────────────────────────────────────
|
||||
|
||||
#[tool(
|
||||
description = "List DAST findings with optional filters for target, scan run, severity, exploitability, and vulnerability type"
|
||||
)]
|
||||
async fn list_dast_findings(
|
||||
&self,
|
||||
Parameters(params): Parameters<dast::ListDastFindingsParams>,
|
||||
) -> Result<CallToolResult, rmcp::ErrorData> {
|
||||
dast::list_dast_findings(&self.db, params).await
|
||||
}
|
||||
|
||||
#[tool(description = "Get a summary of recent DAST scan runs and finding counts")]
|
||||
async fn dast_scan_summary(
|
||||
&self,
|
||||
Parameters(params): Parameters<dast::DastScanSummaryParams>,
|
||||
) -> Result<CallToolResult, rmcp::ErrorData> {
|
||||
dast::dast_scan_summary(&self.db, params).await
|
||||
}
|
||||
}
|
||||
|
||||
#[tool_handler]
|
||||
impl ServerHandler for ComplianceMcpServer {
|
||||
fn get_info(&self) -> ServerInfo {
|
||||
ServerInfo {
|
||||
protocol_version: ProtocolVersion::V_2024_11_05,
|
||||
capabilities: ServerCapabilities::builder()
|
||||
.enable_tools()
|
||||
.build(),
|
||||
server_info: Implementation::from_build_env(),
|
||||
instructions: Some(
|
||||
"Compliance Scanner MCP server. Query security findings, SBOM data, and DAST results."
|
||||
.to_string(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
154
compliance-mcp/src/tools/dast.rs
Normal file
154
compliance-mcp/src/tools/dast.rs
Normal file
@@ -0,0 +1,154 @@
|
||||
use mongodb::bson::doc;
|
||||
use rmcp::{model::*, ErrorData as McpError};
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::database::Database;
|
||||
|
||||
const MAX_LIMIT: i64 = 200;
|
||||
const DEFAULT_LIMIT: i64 = 50;
|
||||
|
||||
fn cap_limit(limit: Option<i64>) -> i64 {
|
||||
limit.unwrap_or(DEFAULT_LIMIT).clamp(1, MAX_LIMIT)
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, JsonSchema)]
|
||||
pub struct ListDastFindingsParams {
|
||||
/// Filter by DAST target ID
|
||||
pub target_id: Option<String>,
|
||||
/// Filter by scan run ID
|
||||
pub scan_run_id: Option<String>,
|
||||
/// Filter by severity: info, low, medium, high, critical
|
||||
pub severity: Option<String>,
|
||||
/// Only show confirmed exploitable findings
|
||||
pub exploitable: Option<bool>,
|
||||
/// Filter by vulnerability type (e.g. sql_injection, xss, ssrf)
|
||||
pub vuln_type: Option<String>,
|
||||
/// Maximum number of results (default 50, max 200)
|
||||
pub limit: Option<i64>,
|
||||
}
|
||||
|
||||
pub async fn list_dast_findings(
|
||||
db: &Database,
|
||||
params: ListDastFindingsParams,
|
||||
) -> Result<CallToolResult, McpError> {
|
||||
let mut filter = doc! {};
|
||||
if let Some(ref target_id) = params.target_id {
|
||||
filter.insert("target_id", target_id);
|
||||
}
|
||||
if let Some(ref scan_run_id) = params.scan_run_id {
|
||||
filter.insert("scan_run_id", scan_run_id);
|
||||
}
|
||||
if let Some(ref severity) = params.severity {
|
||||
filter.insert("severity", severity);
|
||||
}
|
||||
if let Some(exploitable) = params.exploitable {
|
||||
filter.insert("exploitable", exploitable);
|
||||
}
|
||||
if let Some(ref vuln_type) = params.vuln_type {
|
||||
filter.insert("vuln_type", vuln_type);
|
||||
}
|
||||
|
||||
let limit = cap_limit(params.limit);
|
||||
|
||||
let mut cursor = db
|
||||
.dast_findings()
|
||||
.find(filter)
|
||||
.sort(doc! { "created_at": -1 })
|
||||
.limit(limit)
|
||||
.await
|
||||
.map_err(|e| McpError::internal_error(format!("DB error: {e}"), None))?;
|
||||
|
||||
let mut results = Vec::new();
|
||||
while cursor
|
||||
.advance()
|
||||
.await
|
||||
.map_err(|e| McpError::internal_error(format!("cursor error: {e}"), None))?
|
||||
{
|
||||
let finding = cursor
|
||||
.deserialize_current()
|
||||
.map_err(|e| McpError::internal_error(format!("deserialize error: {e}"), None))?;
|
||||
results.push(finding);
|
||||
}
|
||||
|
||||
let json = serde_json::to_string_pretty(&results)
|
||||
.map_err(|e| McpError::internal_error(format!("json error: {e}"), None))?;
|
||||
|
||||
Ok(CallToolResult::success(vec![Content::text(json)]))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, JsonSchema)]
|
||||
pub struct DastScanSummaryParams {
|
||||
/// Filter by DAST target ID
|
||||
pub target_id: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn dast_scan_summary(
|
||||
db: &Database,
|
||||
params: DastScanSummaryParams,
|
||||
) -> Result<CallToolResult, McpError> {
|
||||
let mut filter = doc! {};
|
||||
if let Some(ref target_id) = params.target_id {
|
||||
filter.insert("target_id", target_id);
|
||||
}
|
||||
|
||||
// Get recent scan runs
|
||||
let mut cursor = db
|
||||
.dast_scan_runs()
|
||||
.find(filter.clone())
|
||||
.sort(doc! { "started_at": -1 })
|
||||
.limit(10)
|
||||
.await
|
||||
.map_err(|e| McpError::internal_error(format!("DB error: {e}"), None))?;
|
||||
|
||||
let mut scan_runs = Vec::new();
|
||||
while cursor
|
||||
.advance()
|
||||
.await
|
||||
.map_err(|e| McpError::internal_error(format!("cursor error: {e}"), None))?
|
||||
{
|
||||
let run = cursor
|
||||
.deserialize_current()
|
||||
.map_err(|e| McpError::internal_error(format!("deserialize error: {e}"), None))?;
|
||||
scan_runs.push(serde_json::json!({
|
||||
"id": run.id.map(|id| id.to_hex()),
|
||||
"target_id": run.target_id,
|
||||
"status": run.status,
|
||||
"findings_count": run.findings_count,
|
||||
"exploitable_count": run.exploitable_count,
|
||||
"endpoints_discovered": run.endpoints_discovered,
|
||||
"started_at": run.started_at.to_rfc3339(),
|
||||
"completed_at": run.completed_at.map(|t| t.to_rfc3339()),
|
||||
}));
|
||||
}
|
||||
|
||||
// Count findings by severity
|
||||
let mut findings_filter = doc! {};
|
||||
if let Some(ref target_id) = params.target_id {
|
||||
findings_filter.insert("target_id", target_id);
|
||||
}
|
||||
let total_findings = db
|
||||
.dast_findings()
|
||||
.count_documents(findings_filter.clone())
|
||||
.await
|
||||
.map_err(|e| McpError::internal_error(format!("DB error: {e}"), None))?;
|
||||
|
||||
let mut exploitable_filter = findings_filter.clone();
|
||||
exploitable_filter.insert("exploitable", true);
|
||||
let exploitable_count = db
|
||||
.dast_findings()
|
||||
.count_documents(exploitable_filter)
|
||||
.await
|
||||
.map_err(|e| McpError::internal_error(format!("DB error: {e}"), None))?;
|
||||
|
||||
let summary = serde_json::json!({
|
||||
"total_findings": total_findings,
|
||||
"exploitable_findings": exploitable_count,
|
||||
"recent_scan_runs": scan_runs,
|
||||
});
|
||||
|
||||
let json = serde_json::to_string_pretty(&summary)
|
||||
.map_err(|e| McpError::internal_error(format!("json error: {e}"), None))?;
|
||||
|
||||
Ok(CallToolResult::success(vec![Content::text(json)]))
|
||||
}
|
||||
163
compliance-mcp/src/tools/findings.rs
Normal file
163
compliance-mcp/src/tools/findings.rs
Normal file
@@ -0,0 +1,163 @@
|
||||
use mongodb::bson::doc;
|
||||
use rmcp::{model::*, ErrorData as McpError};
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::database::Database;
|
||||
|
||||
const MAX_LIMIT: i64 = 200;
|
||||
const DEFAULT_LIMIT: i64 = 50;
|
||||
|
||||
fn cap_limit(limit: Option<i64>) -> i64 {
|
||||
limit.unwrap_or(DEFAULT_LIMIT).clamp(1, MAX_LIMIT)
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, JsonSchema)]
|
||||
pub struct ListFindingsParams {
|
||||
/// Filter by repository ID
|
||||
pub repo_id: Option<String>,
|
||||
/// Filter by severity: info, low, medium, high, critical
|
||||
pub severity: Option<String>,
|
||||
/// Filter by status: open, triaged, false_positive, resolved, ignored
|
||||
pub status: Option<String>,
|
||||
/// Filter by scan type: sast, sbom, cve, gdpr, oauth
|
||||
pub scan_type: Option<String>,
|
||||
/// Maximum number of results (default 50, max 200)
|
||||
pub limit: Option<i64>,
|
||||
}
|
||||
|
||||
pub async fn list_findings(
|
||||
db: &Database,
|
||||
params: ListFindingsParams,
|
||||
) -> Result<CallToolResult, McpError> {
|
||||
let mut filter = doc! {};
|
||||
if let Some(ref repo_id) = params.repo_id {
|
||||
filter.insert("repo_id", repo_id);
|
||||
}
|
||||
if let Some(ref severity) = params.severity {
|
||||
filter.insert("severity", severity);
|
||||
}
|
||||
if let Some(ref status) = params.status {
|
||||
filter.insert("status", status);
|
||||
}
|
||||
if let Some(ref scan_type) = params.scan_type {
|
||||
filter.insert("scan_type", scan_type);
|
||||
}
|
||||
|
||||
let limit = cap_limit(params.limit);
|
||||
|
||||
let mut cursor = db
|
||||
.findings()
|
||||
.find(filter)
|
||||
.sort(doc! { "created_at": -1 })
|
||||
.limit(limit)
|
||||
.await
|
||||
.map_err(|e| McpError::internal_error(format!("DB error: {e}"), None))?;
|
||||
|
||||
let mut results = Vec::new();
|
||||
while cursor
|
||||
.advance()
|
||||
.await
|
||||
.map_err(|e| McpError::internal_error(format!("cursor error: {e}"), None))?
|
||||
{
|
||||
let finding = cursor
|
||||
.deserialize_current()
|
||||
.map_err(|e| McpError::internal_error(format!("deserialize error: {e}"), None))?;
|
||||
results.push(finding);
|
||||
}
|
||||
|
||||
let json = serde_json::to_string_pretty(&results)
|
||||
.map_err(|e| McpError::internal_error(format!("json error: {e}"), None))?;
|
||||
|
||||
Ok(CallToolResult::success(vec![Content::text(json)]))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, JsonSchema)]
|
||||
pub struct GetFindingParams {
|
||||
/// Finding ID (MongoDB ObjectId hex string)
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
pub async fn get_finding(
|
||||
db: &Database,
|
||||
params: GetFindingParams,
|
||||
) -> Result<CallToolResult, McpError> {
|
||||
let oid = bson::oid::ObjectId::parse_str(¶ms.id)
|
||||
.map_err(|e| McpError::invalid_params(format!("invalid ObjectId: {e}"), None))?;
|
||||
|
||||
let finding = db
|
||||
.findings()
|
||||
.find_one(doc! { "_id": oid })
|
||||
.await
|
||||
.map_err(|e| McpError::internal_error(format!("DB error: {e}"), None))?
|
||||
.ok_or_else(|| McpError::invalid_params("finding not found", None))?;
|
||||
|
||||
let json = serde_json::to_string_pretty(&finding)
|
||||
.map_err(|e| McpError::internal_error(format!("json error: {e}"), None))?;
|
||||
|
||||
Ok(CallToolResult::success(vec![Content::text(json)]))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, JsonSchema)]
|
||||
pub struct FindingsSummaryParams {
|
||||
/// Filter by repository ID
|
||||
pub repo_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct SeverityCount {
|
||||
severity: String,
|
||||
count: u64,
|
||||
}
|
||||
|
||||
pub async fn findings_summary(
|
||||
db: &Database,
|
||||
params: FindingsSummaryParams,
|
||||
) -> Result<CallToolResult, McpError> {
|
||||
let mut base_filter = doc! {};
|
||||
if let Some(ref repo_id) = params.repo_id {
|
||||
base_filter.insert("repo_id", repo_id);
|
||||
}
|
||||
|
||||
let severities = ["critical", "high", "medium", "low", "info"];
|
||||
let mut counts = Vec::new();
|
||||
|
||||
for sev in &severities {
|
||||
let mut filter = base_filter.clone();
|
||||
filter.insert("severity", sev);
|
||||
let count = db
|
||||
.findings()
|
||||
.count_documents(filter)
|
||||
.await
|
||||
.map_err(|e| McpError::internal_error(format!("DB error: {e}"), None))?;
|
||||
counts.push(SeverityCount {
|
||||
severity: sev.to_string(),
|
||||
count,
|
||||
});
|
||||
}
|
||||
|
||||
let total: u64 = counts.iter().map(|c| c.count).sum();
|
||||
|
||||
let mut status_counts = Vec::new();
|
||||
for status in &["open", "triaged", "false_positive", "resolved", "ignored"] {
|
||||
let mut filter = base_filter.clone();
|
||||
filter.insert("status", status);
|
||||
let count = db
|
||||
.findings()
|
||||
.count_documents(filter)
|
||||
.await
|
||||
.map_err(|e| McpError::internal_error(format!("DB error: {e}"), None))?;
|
||||
status_counts.push(serde_json::json!({ "status": status, "count": count }));
|
||||
}
|
||||
|
||||
let summary = serde_json::json!({
|
||||
"total": total,
|
||||
"by_severity": counts,
|
||||
"by_status": status_counts,
|
||||
});
|
||||
|
||||
let json = serde_json::to_string_pretty(&summary)
|
||||
.map_err(|e| McpError::internal_error(format!("json error: {e}"), None))?;
|
||||
|
||||
Ok(CallToolResult::success(vec![Content::text(json)]))
|
||||
}
|
||||
3
compliance-mcp/src/tools/mod.rs
Normal file
3
compliance-mcp/src/tools/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod dast;
|
||||
pub mod findings;
|
||||
pub mod sbom;
|
||||
129
compliance-mcp/src/tools/sbom.rs
Normal file
129
compliance-mcp/src/tools/sbom.rs
Normal file
@@ -0,0 +1,129 @@
|
||||
use mongodb::bson::doc;
|
||||
use rmcp::{model::*, ErrorData as McpError};
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::database::Database;
|
||||
|
||||
const MAX_LIMIT: i64 = 200;
|
||||
const DEFAULT_LIMIT: i64 = 50;
|
||||
|
||||
fn cap_limit(limit: Option<i64>) -> i64 {
|
||||
limit.unwrap_or(DEFAULT_LIMIT).clamp(1, MAX_LIMIT)
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, JsonSchema)]
|
||||
pub struct ListSbomPackagesParams {
|
||||
/// Filter by repository ID
|
||||
pub repo_id: Option<String>,
|
||||
/// Only show packages with known vulnerabilities
|
||||
pub has_vulns: Option<bool>,
|
||||
/// Filter by package manager (e.g. npm, cargo, pip)
|
||||
pub package_manager: Option<String>,
|
||||
/// Filter by license (e.g. MIT, Apache-2.0)
|
||||
pub license: Option<String>,
|
||||
/// Maximum number of results (default 50, max 200)
|
||||
pub limit: Option<i64>,
|
||||
}
|
||||
|
||||
pub async fn list_sbom_packages(
|
||||
db: &Database,
|
||||
params: ListSbomPackagesParams,
|
||||
) -> Result<CallToolResult, McpError> {
|
||||
let mut filter = doc! {};
|
||||
if let Some(ref repo_id) = params.repo_id {
|
||||
filter.insert("repo_id", repo_id);
|
||||
}
|
||||
if let Some(ref pm) = params.package_manager {
|
||||
filter.insert("package_manager", pm);
|
||||
}
|
||||
if let Some(ref license) = params.license {
|
||||
filter.insert("license", license);
|
||||
}
|
||||
if params.has_vulns == Some(true) {
|
||||
filter.insert("known_vulnerabilities.0", doc! { "$exists": true });
|
||||
}
|
||||
|
||||
let limit = cap_limit(params.limit);
|
||||
|
||||
let mut cursor = db
|
||||
.sbom_entries()
|
||||
.find(filter)
|
||||
.sort(doc! { "name": 1 })
|
||||
.limit(limit)
|
||||
.await
|
||||
.map_err(|e| McpError::internal_error(format!("DB error: {e}"), None))?;
|
||||
|
||||
let mut results = Vec::new();
|
||||
while cursor
|
||||
.advance()
|
||||
.await
|
||||
.map_err(|e| McpError::internal_error(format!("cursor error: {e}"), None))?
|
||||
{
|
||||
let entry = cursor
|
||||
.deserialize_current()
|
||||
.map_err(|e| McpError::internal_error(format!("deserialize error: {e}"), None))?;
|
||||
results.push(entry);
|
||||
}
|
||||
|
||||
let json = serde_json::to_string_pretty(&results)
|
||||
.map_err(|e| McpError::internal_error(format!("json error: {e}"), None))?;
|
||||
|
||||
Ok(CallToolResult::success(vec![Content::text(json)]))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, JsonSchema)]
|
||||
pub struct SbomVulnReportParams {
|
||||
/// Repository ID to generate vulnerability report for
|
||||
pub repo_id: String,
|
||||
}
|
||||
|
||||
pub async fn sbom_vuln_report(
|
||||
db: &Database,
|
||||
params: SbomVulnReportParams,
|
||||
) -> Result<CallToolResult, McpError> {
|
||||
let filter = doc! {
|
||||
"repo_id": ¶ms.repo_id,
|
||||
"known_vulnerabilities.0": { "$exists": true },
|
||||
};
|
||||
|
||||
let mut cursor = db
|
||||
.sbom_entries()
|
||||
.find(filter)
|
||||
.sort(doc! { "name": 1 })
|
||||
.await
|
||||
.map_err(|e| McpError::internal_error(format!("DB error: {e}"), None))?;
|
||||
|
||||
let mut vulnerable_packages = Vec::new();
|
||||
let mut total_vulns = 0u64;
|
||||
|
||||
while cursor
|
||||
.advance()
|
||||
.await
|
||||
.map_err(|e| McpError::internal_error(format!("cursor error: {e}"), None))?
|
||||
{
|
||||
let entry = cursor
|
||||
.deserialize_current()
|
||||
.map_err(|e| McpError::internal_error(format!("deserialize error: {e}"), None))?;
|
||||
total_vulns += entry.known_vulnerabilities.len() as u64;
|
||||
vulnerable_packages.push(serde_json::json!({
|
||||
"name": entry.name,
|
||||
"version": entry.version,
|
||||
"package_manager": entry.package_manager,
|
||||
"license": entry.license,
|
||||
"vulnerabilities": entry.known_vulnerabilities,
|
||||
}));
|
||||
}
|
||||
|
||||
let report = serde_json::json!({
|
||||
"repo_id": params.repo_id,
|
||||
"vulnerable_packages_count": vulnerable_packages.len(),
|
||||
"total_vulnerabilities": total_vulns,
|
||||
"packages": vulnerable_packages,
|
||||
});
|
||||
|
||||
let json = serde_json::to_string_pretty(&report)
|
||||
.map_err(|e| McpError::internal_error(format!("json error: {e}"), None))?;
|
||||
|
||||
Ok(CallToolResult::success(vec![Content::text(json)]))
|
||||
}
|
||||
Reference in New Issue
Block a user