use serde::{Deserialize, Serialize};
use rmcp::schemars::{self, JsonSchema};
pub use kodegen_mcp_schema::config::{SystemInfo, ClientInfo, ClientRecord};
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ServerConfig {
#[serde(default = "default_schema_version")]
pub schema_version: u32,
#[serde(default)]
pub blocked_commands: Vec<String>,
#[serde(default = "default_shell")]
pub default_shell: String,
#[serde(default)]
pub allowed_directories: Vec<String>,
#[serde(default)]
pub denied_directories: Vec<String>,
#[serde(default = "default_file_read_limit")]
pub file_read_line_limit: usize,
#[serde(default = "default_file_write_limit")]
pub file_write_line_limit: usize,
#[serde(default = "default_fuzzy_threshold")]
pub fuzzy_search_threshold: f64,
#[serde(default = "default_http_timeout")]
pub http_connection_timeout_secs: u64,
#[serde(default = "default_path_timeout")]
pub path_validation_timeout_ms: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub current_client: Option<ClientInfo>,
#[serde(default)]
pub client_history: Vec<ClientRecord>,
#[serde(skip, default = "crate::system_info::get_system_info")]
pub system_info: SystemInfo,
#[serde(default)]
pub save_error_count: usize,
}
fn default_schema_version() -> u32 {
1 }
fn default_shell() -> String {
crate::shell_detection::detect_user_shell()
}
fn default_file_read_limit() -> usize {
10_000
}
fn default_file_write_limit() -> usize {
100_000
}
fn default_fuzzy_threshold() -> f64 {
0.6
}
fn default_http_timeout() -> u64 {
30
}
fn default_path_timeout() -> u64 {
5000
}
impl ServerConfig {
pub const CURRENT_SCHEMA_VERSION: u32 = 1;
pub fn validate_and_repair(&mut self) -> Result<(), String> {
let mut warnings = Vec::new();
if self.default_shell.is_empty() {
warnings.push("default_shell is empty, auto-detecting".to_string());
self.default_shell = crate::shell_detection::detect_user_shell();
} else if let Err(e) = crate::shell_detection::validate_shell_path(&self.default_shell) {
warnings.push(format!(
"default_shell validation failed ({}), auto-detecting",
e
));
self.default_shell = crate::shell_detection::detect_user_shell();
}
if self.file_read_line_limit == 0 {
return Err("file_read_line_limit cannot be zero".to_string());
}
if self.file_write_line_limit == 0 {
return Err("file_write_line_limit cannot be zero".to_string());
}
if !(0.0..=1.0).contains(&self.fuzzy_search_threshold) {
return Err(format!(
"fuzzy_search_threshold must be 0.0-1.0, got {}",
self.fuzzy_search_threshold
));
}
if self.http_connection_timeout_secs == 0 {
return Err("http_connection_timeout_secs cannot be zero".to_string());
}
if self.path_validation_timeout_ms == 0 {
return Err("path_validation_timeout_ms cannot be zero".to_string());
}
for dir in &self.allowed_directories {
if dir.is_empty() {
return Err("allowed_directories contains empty path".to_string());
}
}
for dir in &self.denied_directories {
if dir.is_empty() {
return Err("denied_directories contains empty path".to_string());
}
}
for warning in warnings {
log::warn!("Config validation: {}", warning);
}
Ok(())
}
}
impl Default for ServerConfig {
fn default() -> Self {
Self {
schema_version: ServerConfig::CURRENT_SCHEMA_VERSION,
blocked_commands: Vec::new(),
default_shell: default_shell(),
allowed_directories: Vec::new(),
denied_directories: Vec::new(),
file_read_line_limit: default_file_read_limit(),
file_write_line_limit: default_file_write_limit(),
fuzzy_search_threshold: default_fuzzy_threshold(),
http_connection_timeout_secs: default_http_timeout(),
path_validation_timeout_ms: default_path_timeout(),
current_client: None,
client_history: Vec::new(),
system_info: crate::system_info::get_system_info(),
save_error_count: 0,
}
}
}