use super::{CliError, CliToolBridge};
use crate::core::{Confidence, Finding, Severity};
use std::path::Path;
const BINARY: &str = "uvx";
pub struct McpScanBridge {
bridge: CliToolBridge,
}
impl Default for McpScanBridge {
fn default() -> Self {
Self::new()
}
}
impl McpScanBridge {
pub fn new() -> Self {
Self {
bridge: CliToolBridge::new(BINARY),
}
}
pub fn is_available(&self) -> bool {
self.bridge.is_available()
}
pub async fn scan(&self, config_path: &Path) -> Result<Vec<Finding>, CliError> {
let path_str = config_path.to_string_lossy();
let (_exit_code, stdout, _stderr) = self
.bridge
.run(&["mcp-scan@latest", "scan", &path_str])
.await?;
Ok(parse_mcp_scan_output(&stdout, config_path))
}
}
pub fn parse_mcp_scan_output(json_text: &str, config_path: &Path) -> Vec<Finding> {
let value: serde_json::Value = match serde_json::from_str(json_text) {
Ok(v) => v,
Err(_) => return Vec::new(),
};
let findings_array = value
.get("findings")
.and_then(|v| v.as_array())
.or_else(|| value.as_array());
let Some(items) = findings_array else {
return Vec::new();
};
items
.iter()
.map(|item| {
let issue_type = item
.get("type")
.or_else(|| item.get("issue_type"))
.and_then(|v| v.as_str())
.unwrap_or("unknown");
let title = item
.get("title")
.or_else(|| item.get("message"))
.and_then(|v| v.as_str())
.unwrap_or(issue_type);
let severity_str = item
.get("severity")
.and_then(|v| v.as_str())
.unwrap_or("medium");
let severity = match severity_str.to_lowercase().as_str() {
"critical" => Severity::Critical,
"high" => Severity::High,
"medium" => Severity::Medium,
"low" => Severity::Low,
_ => Severity::Medium,
};
let mut finding = Finding::new(
format!("mcp-scan-{}", issue_type.to_lowercase().replace(' ', "-")),
format!("mcp-scan: {}", title),
severity,
);
finding.confidence = Confidence::Medium;
finding.file_path = Some(config_path.to_path_buf());
finding.tags.push("mcp-security".to_string());
finding.tags.push(issue_type.to_lowercase());
if let Some(desc) = item.get("description").and_then(|v| v.as_str()) {
finding.description = desc.to_string();
}
if let Some(rem) = item.get("remediation").and_then(|v| v.as_str()) {
finding.remediation = Some(rem.to_string());
}
if let Some(tool) = item.get("tool_name").and_then(|v| v.as_str()) {
finding.evidence.push(format!("Tool: {}", tool));
}
if let Some(server) = item.get("server_name").and_then(|v| v.as_str()) {
finding.evidence.push(format!("Server: {}", server));
}
finding
})
.collect()
}