use anyhow::{anyhow, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use tracing::{debug, info};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct MCPConfig {
pub servers: Option<Vec<MCPServerConfig>>,
pub options: Option<MCPGlobalOptions>,
pub auth_headers: Option<HashMap<String, String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MCPServerConfig {
pub name: Option<String>,
pub url: String,
pub description: Option<String>,
pub auth_headers: Option<HashMap<String, String>>,
pub options: Option<MCPServerOptions>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MCPGlobalOptions {
pub timeout: Option<u64>,
pub http_timeout: Option<u64>,
pub format: Option<String>,
pub detailed: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MCPServerOptions {
pub timeout: Option<u64>,
pub http_timeout: Option<u64>,
pub format: Option<String>,
pub detailed: Option<bool>,
}
fn is_verbose_or_debug() -> bool {
std::env::var("MCP_DEBUG").ok().as_deref() == Some("1")
|| std::env::var("RUST_LOG")
.map(|v| v.contains("debug") || v.contains("info"))
.unwrap_or(false)
}
pub struct MCPConfigManager {
config_paths: Vec<PathBuf>,
}
impl MCPConfigManager {
pub fn new() -> Self {
let mut paths = Vec::new();
if let Some(home_dir) = dirs::home_dir() {
let cursor_config = home_dir.join(".cursor").join("mcp.json");
paths.push(cursor_config);
let codium_config = home_dir
.join(".codium")
.join("windsurf")
.join("mcp_config.json");
paths.push(codium_config);
let vscode_config = home_dir.join(".vscode").join("mcp.json");
paths.push(vscode_config);
let neovim_config = home_dir.join(".config").join("nvim").join("mcp.json");
paths.push(neovim_config);
let helix_config = home_dir.join(".config").join("helix").join("mcp.json");
paths.push(helix_config);
}
Self {
config_paths: paths,
}
}
pub fn load_config(&self) -> Result<MCPConfig> {
let mut merged_config = MCPConfig::default();
for path in &self.config_paths {
if let Ok(config) = self.load_config_from_path(path) {
self.merge_config(&mut merged_config, &config);
info!(
"Loaded MCP configuration from IDE config: {}",
path.display()
);
if !is_verbose_or_debug() {
println!("Loaded MCP server config from: {}", path.display());
}
} else {
debug!(
"No MCP configuration found at IDE config: {}",
path.display()
);
}
}
Ok(merged_config)
}
pub fn load_config_from_path(&self, path: &Path) -> Result<MCPConfig> {
if !path.exists() {
return Err(anyhow!(
"IDE configuration file does not exist: {}",
path.display()
));
}
let content = fs::read_to_string(path)
.map_err(|e| anyhow!("Failed to read IDE config file {}: {}", path.display(), e))?;
let config: MCPConfig = serde_json::from_str(&content)
.map_err(|e| anyhow!("Failed to parse IDE config file {}: {}", path.display(), e))?;
Ok(config)
}
fn merge_config(&self, base: &mut MCPConfig, other: &MCPConfig) {
if let Some(other_servers) = &other.servers {
match &mut base.servers {
Some(base_servers) => {
base_servers.extend(other_servers.clone());
}
None => {
base.servers = Some(other_servers.clone());
}
}
}
if let Some(other_options) = &other.options {
match &mut base.options {
Some(base_options) => {
if other_options.timeout.is_some() {
base_options.timeout = other_options.timeout;
}
if other_options.http_timeout.is_some() {
base_options.http_timeout = other_options.http_timeout;
}
if other_options.format.is_some() {
base_options.format = other_options.format.clone();
}
if other_options.detailed.is_some() {
base_options.detailed = other_options.detailed;
}
}
None => {
base.options = Some(other_options.clone());
}
}
}
if let Some(other_auth_headers) = &other.auth_headers {
match &mut base.auth_headers {
Some(base_auth_headers) => {
for (key, value) in other_auth_headers {
base_auth_headers.insert(key.clone(), value.clone());
}
}
None => {
base.auth_headers = Some(other_auth_headers.clone());
}
}
}
}
pub fn has_config_files(&self) -> bool {
self.config_paths.iter().any(|path| path.exists())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::tempdir;
#[test]
fn test_load_config_from_path() {
let temp_dir = tempdir().unwrap();
let config_path = temp_dir.path().join("test_config.json");
let config_content = r#"{
"servers": [
{
"name": "test-server",
"url": "http://localhost:3000",
"description": "Test server"
}
],
"options": {
"timeout": 60,
"format": "json"
}
}"#;
fs::write(&config_path, config_content).unwrap();
let manager = MCPConfigManager::new();
let config = manager.load_config_from_path(&config_path).unwrap();
assert!(config.servers.is_some());
assert_eq!(config.servers.unwrap().len(), 1);
assert!(config.options.is_some());
}
#[test]
fn test_merge_config() {
let mut base = MCPConfig::default();
let other = MCPConfig {
servers: Some(vec![MCPServerConfig {
name: Some("server1".to_string()),
url: "http://localhost:3000".to_string(),
description: None,
auth_headers: None,
options: None,
}]),
options: Some(MCPGlobalOptions {
timeout: Some(60),
http_timeout: None,
format: Some("json".to_string()),
detailed: None,
}),
auth_headers: None,
};
let manager = MCPConfigManager::new();
manager.merge_config(&mut base, &other);
assert!(base.servers.is_some());
assert_eq!(base.servers.unwrap().len(), 1);
assert!(base.options.is_some());
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScannerConfig {
pub llm: LLMConfig,
pub scanner: ScannerSettings,
pub security: SecurityConfig,
pub logging: LoggingConfig,
pub performance: PerformanceConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LLMConfig {
pub provider: String,
pub model: String,
pub base_url: String,
pub api_key: String,
pub timeout: u64,
pub max_tokens: u32,
pub temperature: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScannerSettings {
pub http_timeout: u64,
pub scan_timeout: u64,
pub detailed: bool,
pub format: String,
pub parallel: bool,
pub max_retries: u32,
pub retry_delay_ms: u64,
pub llm_batch_size: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SecurityConfig {
pub enabled: bool,
pub min_severity: String,
pub checks: SecurityChecks,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SecurityChecks {
pub tool_poisoning: bool,
pub secrets_leakage: bool,
pub sql_injection: bool,
pub command_injection: bool,
pub path_traversal: bool,
pub auth_bypass: bool,
pub prompt_injection: bool,
pub pii_leakage: bool,
pub jailbreak: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoggingConfig {
pub level: String,
pub colored: bool,
pub timestamps: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PerformanceConfig {
pub tracking: bool,
pub slow_threshold_ms: u64,
}
impl Default for ScannerConfig {
fn default() -> Self {
Self {
llm: LLMConfig {
provider: "openai".to_string(),
model: "gpt-4o".to_string(),
base_url: "https://api.openai.com/v1".to_string(),
api_key: "".to_string(),
timeout: 30,
max_tokens: 4000,
temperature: 0.1,
},
scanner: ScannerSettings {
http_timeout: 30,
scan_timeout: 60,
detailed: false,
format: "table".to_string(),
parallel: true,
max_retries: 3,
retry_delay_ms: 1000,
llm_batch_size: 10,
},
security: SecurityConfig {
enabled: true,
min_severity: "low".to_string(),
checks: SecurityChecks {
tool_poisoning: true,
secrets_leakage: true,
sql_injection: true,
command_injection: true,
path_traversal: true,
auth_bypass: true,
prompt_injection: true,
pii_leakage: true,
jailbreak: true,
},
},
logging: LoggingConfig {
level: "info".to_string(),
colored: true,
timestamps: true,
},
performance: PerformanceConfig {
tracking: true,
slow_threshold_ms: 5000,
},
}
}
}
pub struct ScannerConfigManager {
config_path: PathBuf,
}
impl ScannerConfigManager {
pub fn new() -> Self {
let config_path = PathBuf::from("config.yaml");
Self { config_path }
}
pub fn load_config(&self) -> Result<ScannerConfig> {
if !self.config_path.exists() {
info!("No config.yaml found, using default configuration");
return Ok(ScannerConfig::default());
}
let content = fs::read_to_string(&self.config_path)
.map_err(|e| anyhow!("Failed to read config.yaml: {}", e))?;
let config: ScannerConfig = serde_yaml::from_str(&content)
.map_err(|e| anyhow!("Failed to parse config.yaml: {}", e))?;
info!("Loaded configuration from config.yaml");
Ok(config)
}
pub fn save_config(&self, config: &ScannerConfig) -> Result<()> {
let content = serde_yaml::to_string(config)
.map_err(|e| anyhow!("Failed to serialize configuration: {}", e))?;
fs::write(&self.config_path, content)
.map_err(|e| anyhow!("Failed to write config.yaml: {}", e))?;
info!("Saved configuration to config.yaml");
Ok(())
}
pub fn has_config_file(&self) -> bool {
self.config_path.exists()
}
}