use crate::utils::error::Error as GgenError;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
pub const PROJECT_MCP_CONFIG: &str = ".mcp.json";
pub const USER_MCP_CONFIG_DIR: &str = ".ggen/mcp";
pub const USER_MCP_CONFIG: &str = ".ggen/mcp/config.json";
pub const SYSTEM_MCP_CONFIG: &str = "/etc/ggen/mcp.json";
pub const PROJECT_A2A_CONFIG: &str = "a2a.toml";
pub const USER_A2A_CONFIG: &str = ".ggen/a2a.toml";
pub const SYSTEM_A2A_CONFIG: &str = "/etc/ggen/a2a.toml";
pub const MCP_SERVER_PID_FILE: &str = ".ggen/mcp/server.pid";
pub const MCP_SERVER_LOCK_FILE: &str = ".ggen/mcp/server.lock";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpConfigFile {
#[serde(default)]
pub mcp_servers: HashMap<String, McpServerConfig>,
#[serde(default)]
pub metadata: McpMetadata,
#[serde(default)]
pub description: Option<String>,
#[serde(default = "default_mcp_version")]
pub version: String,
}
impl Default for McpConfigFile {
fn default() -> Self {
Self {
mcp_servers: HashMap::new(),
metadata: McpMetadata::default(),
description: Some(
"MCP (Model Context Protocol) servers for enhanced Claude Code capabilities"
.to_string(),
),
version: default_mcp_version(),
}
}
}
fn default_mcp_version() -> String {
"1.0.0".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct McpMetadata {
#[serde(default)]
pub project: Option<String>,
#[serde(default)]
pub version: Option<String>,
#[serde(default)]
pub purpose: Option<String>,
#[serde(default)]
pub updated_at: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpServerConfig {
pub command: String,
#[serde(default)]
pub args: Vec<String>,
#[serde(default)]
pub env: HashMap<String, String>,
#[serde(default)]
pub cwd: Option<String>,
#[serde(default = "default_server_timeout")]
pub timeout: u64,
#[serde(default = "default_server_enabled")]
pub enabled: bool,
#[serde(default = "default_max_restarts")]
pub max_restarts: u32,
#[serde(default)]
pub server_type: Option<String>,
}
fn default_server_timeout() -> u64 {
30
}
fn default_server_enabled() -> bool {
true
}
fn default_max_restarts() -> u32 {
3
}
impl McpServerConfig {
pub fn new(command: impl Into<String>) -> Self {
Self {
command: command.into(),
args: Vec::new(),
env: HashMap::new(),
cwd: None,
timeout: default_server_timeout(),
enabled: default_server_enabled(),
max_restarts: default_max_restarts(),
server_type: None,
}
}
pub fn with_arg(mut self, arg: impl Into<String>) -> Self {
self.args.push(arg.into());
self
}
pub fn with_args(mut self, args: Vec<String>) -> Self {
self.args.extend(args);
self
}
pub fn with_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.env.insert(key.into(), value.into());
self
}
pub fn with_cwd(mut self, cwd: impl Into<String>) -> Self {
self.cwd = Some(cwd.into());
self
}
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout.as_secs();
self
}
pub fn disabled(mut self) -> Self {
self.enabled = false;
self
}
pub fn validate(&self) -> Result<(), McpValidationError> {
if self.command.is_empty() {
return Err(McpValidationError::EmptyCommand);
}
if self.timeout == 0 {
return Err(McpValidationError::InvalidTimeout(
"Timeout must be greater than 0".to_string(),
));
}
let dangerous_commands = ["rm -rf", "mkfs", "format", "del /f"];
let command_lower = self.command.to_lowercase();
for dangerous in dangerous_commands {
if command_lower.contains(dangerous) {
return Err(McpValidationError::DangerousCommand(format!(
"Command contains potentially dangerous pattern: {}",
dangerous
)));
}
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct A2aConfig {
#[serde(default)]
pub server: A2aServerConfig,
#[serde(default)]
pub agents: HashMap<String, A2aAgentConfig>,
#[serde(default)]
pub workflows: HashMap<String, A2aWorkflowConfig>,
#[serde(default)]
pub metadata: A2aMetadata,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct A2aServerConfig {
#[serde(default = "default_a2a_host")]
pub host: String,
#[serde(default = "default_a2a_port")]
pub port: u16,
#[serde(default)]
pub tls_enabled: bool,
#[serde(default)]
pub tls_cert_path: Option<String>,
#[serde(default)]
pub tls_key_path: Option<String>,
#[serde(default = "default_a2a_timeout")]
pub timeout: u64,
#[serde(default = "default_max_connections")]
pub max_connections: usize,
}
fn default_a2a_host() -> String {
"127.0.0.1".to_string()
}
fn default_a2a_port() -> u16 {
8080
}
fn default_a2a_timeout() -> u64 {
30
}
fn default_max_connections() -> usize {
100
}
impl Default for A2aServerConfig {
fn default() -> Self {
Self {
host: default_a2a_host(),
port: default_a2a_port(),
tls_enabled: false,
tls_cert_path: None,
tls_key_path: None,
timeout: default_a2a_timeout(),
max_connections: default_max_connections(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct A2aAgentConfig {
pub agent_type: String,
pub name: String,
#[serde(default)]
pub description: Option<String>,
#[serde(default = "default_agent_enabled")]
pub enabled: bool,
#[serde(default)]
pub config: HashMap<String, toml::Value>,
}
fn default_agent_enabled() -> bool {
true
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct A2aWorkflowConfig {
pub spec_file: String,
pub name: String,
#[serde(default)]
pub auto_start: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct A2aMetadata {
#[serde(default = "default_a2a_config_version")]
pub version: String,
#[serde(default)]
pub environment: Option<String>,
#[serde(default)]
pub updated_at: Option<String>,
}
fn default_a2a_config_version() -> String {
"1.0.0".to_string()
}
impl Default for A2aMetadata {
fn default() -> Self {
Self {
version: default_a2a_config_version(),
environment: None,
updated_at: None,
}
}
}
impl A2aConfig {
pub fn new() -> Self {
Self::default()
}
pub fn validate(&self) -> Result<(), A2aValidationError> {
if self.server.port == 0 {
return Err(A2aValidationError::InvalidPort(
"Port cannot be zero".to_string(),
));
}
if self.server.timeout == 0 {
return Err(A2aValidationError::InvalidTimeout(
"Timeout must be greater than 0".to_string(),
));
}
if self.server.tls_enabled
&& (self.server.tls_cert_path.is_none() || self.server.tls_key_path.is_none())
{
return Err(A2aValidationError::TlsMisconfigured(
"TLS is enabled but certificate or key path is missing".to_string(),
));
}
Ok(())
}
pub fn server_url(&self) -> String {
let scheme = if self.server.tls_enabled {
"https"
} else {
"http"
};
format!("{}://{}:{}", scheme, self.server.host, self.server.port)
}
}
#[derive(Debug, Clone, thiserror::Error)]
pub enum McpValidationError {
#[error("Empty command in server configuration")]
EmptyCommand,
#[error("Invalid timeout: {0}")]
InvalidTimeout(String),
#[error("Dangerous command detected: {0}")]
DangerousCommand(String),
#[error("Server not found: {0}")]
ServerNotFound(String),
#[error("Configuration file not found: {0}")]
ConfigNotFound(String),
#[error("Invalid JSON in configuration: {0}")]
InvalidJson(String),
#[error("File IO error: {0}")]
FileIo(String),
}
#[derive(Debug, Clone, thiserror::Error)]
pub enum A2aValidationError {
#[error("Invalid port: {0}")]
InvalidPort(String),
#[error("Invalid timeout: {0}")]
InvalidTimeout(String),
#[error("TLS misconfigured: {0}")]
TlsMisconfigured(String),
#[error("Agent not found: {0}")]
AgentNotFound(String),
#[error("Configuration file not found: {0}")]
ConfigNotFound(String),
#[error("Invalid TOML in configuration: {0}")]
InvalidToml(String),
#[error("File IO error: {0}")]
FileIo(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ConfigPriority {
CliArgs,
EnvVars,
Project,
User,
System,
Defaults,
}
#[derive(Debug, Clone)]
pub struct ResolvedConfig {
pub mcp: Option<McpConfigFile>,
pub a2a: Option<A2aConfig>,
pub sources: HashMap<String, ConfigPriority>,
}
pub fn load_config(
project_dir: Option<&Path>, cli_mcp_file: Option<&Path>, cli_a2a_file: Option<&Path>,
) -> Result<ResolvedConfig, GgenError> {
let mut sources = HashMap::new();
let mut mcp_config: Option<McpConfigFile> = None;
let mut a2a_config: Option<A2aConfig> = None;
let env_mcp_config = load_mcp_from_env()?;
if env_mcp_config.is_some() {
sources.insert("mcp".to_string(), ConfigPriority::EnvVars);
mcp_config = env_mcp_config;
}
let env_a2a_config = load_a2a_from_env()?;
if env_a2a_config.is_some() {
sources.insert("a2a".to_string(), ConfigPriority::EnvVars);
a2a_config = env_a2a_config;
}
if let Some(project_dir) = project_dir {
let project_mcp_path = project_dir.join(PROJECT_MCP_CONFIG);
if project_mcp_path.exists() {
let config = load_mcp_from_file(&project_mcp_path)?;
if sources
.get("mcp")
.is_none_or(|p| *p < ConfigPriority::Project)
{
sources.insert("mcp".to_string(), ConfigPriority::Project);
mcp_config = Some(config);
}
}
let project_a2a_path = project_dir.join(PROJECT_A2A_CONFIG);
if project_a2a_path.exists() {
let config = load_a2a_from_file(&project_a2a_path)?;
if sources
.get("a2a")
.is_none_or(|p| *p < ConfigPriority::Project)
{
sources.insert("a2a".to_string(), ConfigPriority::Project);
a2a_config = Some(config);
}
}
}
let user_mcp_path = dirs::home_dir().map(|p| p.join(USER_MCP_CONFIG));
if let Some(ref path) = user_mcp_path {
if path.exists() {
let config = load_mcp_from_file(path)?;
if sources.get("mcp").is_none_or(|p| *p < ConfigPriority::User) {
sources.insert("mcp".to_string(), ConfigPriority::User);
mcp_config = Some(config);
}
}
}
let user_a2a_path = dirs::home_dir().map(|p| p.join(USER_A2A_CONFIG));
if let Some(ref path) = user_a2a_path {
if path.exists() {
let config = load_a2a_from_file(path)?;
if sources.get("a2a").is_none_or(|p| *p < ConfigPriority::User) {
sources.insert("a2a".to_string(), ConfigPriority::User);
a2a_config = Some(config);
}
}
}
let system_mcp_path = PathBuf::from(SYSTEM_MCP_CONFIG);
if system_mcp_path.exists() {
let config = load_mcp_from_file(&system_mcp_path)?;
if sources
.get("mcp")
.is_none_or(|p| *p < ConfigPriority::System)
{
sources.insert("mcp".to_string(), ConfigPriority::System);
mcp_config = Some(config);
}
}
let system_a2a_path = PathBuf::from(SYSTEM_A2A_CONFIG);
if system_a2a_path.exists() {
let config = load_a2a_from_file(&system_a2a_path)?;
if sources
.get("a2a")
.is_none_or(|p| *p < ConfigPriority::System)
{
sources.insert("a2a".to_string(), ConfigPriority::System);
a2a_config = Some(config);
}
}
if let Some(cli_file) = cli_mcp_file {
let config = load_mcp_from_file(cli_file)?;
sources.insert("mcp".to_string(), ConfigPriority::CliArgs);
mcp_config = Some(config);
}
if let Some(cli_file) = cli_a2a_file {
let config = load_a2a_from_file(cli_file)?;
sources.insert("a2a".to_string(), ConfigPriority::CliArgs);
a2a_config = Some(config);
}
Ok(ResolvedConfig {
mcp: mcp_config,
a2a: a2a_config,
sources,
})
}
pub fn load_mcp_from_file(path: &Path) -> Result<McpConfigFile, GgenError> {
let content = fs::read_to_string(path).map_err(|e| {
GgenError::invalid_input(format!(
"Failed to read MCP config from {}: {}",
path.display(),
e
))
})?;
serde_json::from_str(&content).map_err(|e| {
GgenError::invalid_input(format!(
"Failed to parse MCP config from {}: {}\nSuggestion: Check JSON syntax and structure",
path.display(),
e
))
})
}
pub fn load_a2a_from_file(path: &Path) -> Result<A2aConfig, GgenError> {
let content = fs::read_to_string(path).map_err(|e| {
GgenError::invalid_input(format!(
"Failed to read A2A config from {}: {}",
path.display(),
e
))
})?;
toml::from_str(&content).map_err(|e| {
GgenError::invalid_input(format!(
"Failed to parse A2A config from {}: {}\nSuggestion: Check TOML syntax",
path.display(),
e
))
})
}
pub fn load_mcp_from_env() -> Result<Option<McpConfigFile>, GgenError> {
let config_file = env::var("GGEN_MCP_CONFIG").ok();
if let Some(path) = config_file {
let path_buf = PathBuf::from(&path);
if path_buf.exists() {
return load_mcp_from_file(&path_buf).map(Some);
}
}
let mut servers = HashMap::new();
for (key, value) in env::vars() {
if key.starts_with("GGEN_MCP_SERVER_") && key.ends_with("_COMMAND") {
let server_name = key
.strip_prefix("GGEN_MCP_SERVER_")
.and_then(|s| s.strip_suffix("_COMMAND"))
.map(|s| s.to_lowercase());
if let Some(name) = server_name {
let mut server = McpServerConfig::new(&value);
let args_key = format!("GGEN_MCP_SERVER_{}_ARGS", name.to_uppercase());
if let Ok(args_str) = env::var(&args_key) {
server.args = args_str.split_whitespace().map(|s| s.to_string()).collect();
}
let timeout_key = format!("GGEN_MCP_SERVER_{}_TIMEOUT", name.to_uppercase());
if let Ok(timeout_str) = env::var(&timeout_key) {
if let Ok(secs) = timeout_str.parse::<u64>() {
server.timeout = secs;
}
}
servers.insert(name, server);
}
}
}
if servers.is_empty() {
Ok(None)
} else {
Ok(Some(McpConfigFile {
mcp_servers: servers,
..Default::default()
}))
}
}
pub fn load_a2a_from_env() -> Result<Option<A2aConfig>, GgenError> {
let config_file = env::var("GGEN_A2A_CONFIG").ok();
if let Some(path) = config_file {
let path_buf = PathBuf::from(&path);
if path_buf.exists() {
return load_a2a_from_file(&path_buf).map(Some);
}
}
let mut config = A2aConfig::new();
if let Ok(host) = env::var("GGEN_A2A_HOST") {
config.server.host = host;
}
if let Ok(port) = env::var("GGEN_A2A_PORT") {
if let Ok(p) = port.parse::<u16>() {
config.server.port = p;
}
}
if let Ok(timeout) = env::var("GGEN_A2A_TIMEOUT") {
if let Ok(t) = timeout.parse::<u64>() {
config.server.timeout = t;
}
}
if env::var("GGEN_A2A_TLS_ENABLED").is_ok() {
config.server.tls_enabled = true;
}
if let Ok(cert_path) = env::var("GGEN_A2A_TLS_CERT") {
config.server.tls_cert_path = Some(cert_path);
}
if let Ok(key_path) = env::var("GGEN_A2A_TLS_KEY") {
config.server.tls_key_path = Some(key_path);
}
let has_custom_config = config.server.host != default_a2a_host()
|| config.server.port != default_a2a_port()
|| config.server.timeout != default_a2a_timeout()
|| config.server.tls_enabled;
Ok(if has_custom_config {
Some(config)
} else {
None
})
}
pub fn init_mcp_config(path: &Path, include_examples: bool) -> Result<McpConfigFile, GgenError> {
let mut config = McpConfigFile::default();
if include_examples {
config.mcp_servers.insert(
"claude-code-guide".to_string(),
McpServerConfig::new("npx")
.with_args(vec!["@anthropic-ai/claude-code-guide".to_string()]),
);
config.mcp_servers.insert(
"git".to_string(),
McpServerConfig::new("git").with_args(vec!["mcp-server".to_string()]),
);
config.mcp_servers.insert(
"bash".to_string(),
McpServerConfig::new("bash")
.with_args(vec![
"--init-file".to_string(),
".claude/helpers/bash-init.sh".to_string(),
])
.with_env("CARGO_TERM_COLOR".to_string(), "always".to_string()),
);
}
config.metadata.updated_at = Some(timestamp_now());
if let Some(project_name) = path
.parent()
.and_then(|p| p.file_name())
.and_then(|n| n.to_str())
{
config.metadata.project = Some(project_name.to_string());
}
write_mcp_config(path, &config)?;
Ok(config)
}
pub fn init_a2a_config(path: &Path) -> Result<A2aConfig, GgenError> {
let config = A2aConfig::default();
write_a2a_config(path, &config)?;
Ok(config)
}
pub fn write_mcp_config(path: &Path, config: &McpConfigFile) -> Result<(), GgenError> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|e| {
GgenError::invalid_input(format!(
"Failed to create directory {}: {}",
parent.display(),
e
))
})?;
}
let content = serde_json::to_string_pretty(config)
.map_err(|e| GgenError::invalid_input(format!("Failed to serialize MCP config: {}", e)))?;
fs::write(path, content).map_err(|e| {
GgenError::invalid_input(format!(
"Failed to write MCP config to {}: {}",
path.display(),
e
))
})?;
Ok(())
}
pub fn write_a2a_config(path: &Path, config: &A2aConfig) -> Result<(), GgenError> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|e| {
GgenError::invalid_input(format!(
"Failed to create directory {}: {}",
parent.display(),
e
))
})?;
}
let content = toml::to_string_pretty(config)
.map_err(|e| GgenError::invalid_input(format!("Failed to serialize A2A config: {}", e)))?;
fs::write(path, content).map_err(|e| {
GgenError::invalid_input(format!(
"Failed to write A2A config to {}: {}",
path.display(),
e
))
})?;
Ok(())
}
pub fn validate_mcp_config(config: &McpConfigFile) -> Result<Vec<ValidationResult>, GgenError> {
let mut results = Vec::new();
for (name, server) in &config.mcp_servers {
match server.validate() {
Ok(()) => {
results.push(ValidationResult {
server_name: name.clone(),
is_valid: true,
errors: Vec::new(),
warnings: Vec::new(),
});
}
Err(e) => {
results.push(ValidationResult {
server_name: name.clone(),
is_valid: false,
errors: vec![e.to_string()],
warnings: Vec::new(),
});
}
}
}
if config.mcp_servers.is_empty() {
results.push(ValidationResult {
server_name: "_config".to_string(),
is_valid: false,
errors: vec!["No MCP servers configured".to_string()],
warnings: vec!["Consider adding at least one MCP server".to_string()],
});
}
Ok(results)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationResult {
pub server_name: String,
pub is_valid: bool,
pub errors: Vec<String>,
pub warnings: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerStatus {
pub is_running: bool,
pub pid: Option<u32>,
pub uptime_secs: Option<u64>,
pub config_file: Option<String>,
pub address: Option<String>,
pub last_start_time: Option<String>,
}
pub fn get_server_status(project_dir: Option<&Path>) -> Result<ServerStatus, GgenError> {
let pid_file = project_dir
.unwrap_or_else(|| Path::new("."))
.join(MCP_SERVER_PID_FILE);
if !pid_file.exists() {
return Ok(ServerStatus {
is_running: false,
pid: None,
uptime_secs: None,
config_file: None,
address: None,
last_start_time: None,
});
}
let pid_str = fs::read_to_string(&pid_file).map_err(|e| {
GgenError::invalid_input(format!(
"Failed to read PID file {}: {}",
pid_file.display(),
e
))
})?;
let pid: u32 = pid_str
.trim()
.parse()
.map_err(|_| GgenError::invalid_input(format!("Invalid PID in file: {}", pid_str)))?;
let is_running = is_process_running(pid);
if !is_running {
let _ = fs::remove_file(&pid_file);
return Ok(ServerStatus {
is_running: false,
pid: None,
uptime_secs: None,
config_file: None,
address: None,
last_start_time: None,
});
}
let uptime_secs = get_process_uptime(pid).ok();
let last_start = uptime_secs.map(|u| {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
format_timestamp(now - u)
});
Ok(ServerStatus {
is_running: true,
pid: Some(pid),
uptime_secs,
config_file: pid_file.to_str().map(|s| s.to_string()),
address: Some("127.0.0.1:0".to_string()),
last_start_time: last_start,
})
}
#[cfg(unix)]
fn is_process_running(pid: u32) -> bool {
use std::process::Command;
Command::new("kill")
.arg("-0")
.arg(pid.to_string())
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
#[cfg(windows)]
fn is_process_running(pid: u32) -> bool {
use std::process::Command;
Command::new("tasklist")
.args(&["/FI", &format!("PID eq {}", pid)])
.output()
.map(|o| {
let output = String::from_utf8_lossy(&o.stdout);
output.contains(&pid.to_string())
})
.unwrap_or(false)
}
#[cfg(unix)]
fn get_process_uptime(pid: u32) -> Result<u64, GgenError> {
use std::process::Command;
let output = Command::new("ps")
.args(["-o", "etime=", "-p", &pid.to_string()])
.output()
.map_err(|e| GgenError::invalid_input(format!("Failed to query process: {}", e)))?;
let elapsed = String::from_utf8_lossy(&output.stdout).trim().to_string();
let parts: Vec<&str> = elapsed.split(':').collect();
let seconds = match parts.len() {
2 => {
let mins: u64 = parts[0].parse().unwrap_or(0);
let secs: u64 = parts[1].parse().unwrap_or(0);
mins * 60 + secs
}
3 => {
let hours: u64 = parts[0].parse().unwrap_or(0);
let mins: u64 = parts[1].parse().unwrap_or(0);
let secs: u64 = parts[2].parse().unwrap_or(0);
hours * 3600 + mins * 60 + secs
}
_ => 0,
};
Ok(seconds)
}
#[cfg(windows)]
fn get_process_uptime(_pid: u32) -> Result<u64, GgenError> {
Err(GgenError::feature_not_enabled(
"Process uptime not available on Windows",
"",
))
}
pub fn stop_server(project_dir: Option<&Path>, force: bool) -> Result<bool, GgenError> {
let status = get_server_status(project_dir)?;
if !status.is_running {
return Ok(false);
}
let pid = status
.pid
.ok_or_else(|| GgenError::invalid_input("Server is running but PID is unknown"))?;
terminate_process(pid, force)?;
let pid_file = project_dir
.unwrap_or_else(|| Path::new("."))
.join(MCP_SERVER_PID_FILE);
let _ = fs::remove_file(&pid_file);
Ok(true)
}
#[cfg(unix)]
fn terminate_process(pid: u32, force: bool) -> Result<(), GgenError> {
use std::process::Command;
let signal = if force { "9" } else { "15" };
let output = Command::new("kill")
.arg(format!("-{}", signal))
.arg(pid.to_string())
.output()
.map_err(|e| GgenError::invalid_input(format!("Failed to terminate process: {}", e)))?;
if !output.status.success() {
return Err(GgenError::invalid_input(format!(
"Failed to stop process {}: {}",
pid,
String::from_utf8_lossy(&output.stderr)
)));
}
Ok(())
}
#[cfg(windows)]
fn terminate_process(pid: u32, _force: bool) -> Result<(), GgenError> {
use std::process::Command;
let output = Command::new("taskkill")
.args(&["/PID", &pid.to_string(), "/F"])
.output()
.map_err(|e| GgenError::invalid_input(&format!("Failed to terminate process: {}", e)))?;
if !output.status.success() {
return Err(GgenError::invalid_input(&format!(
"Failed to stop process {}",
pid
)));
}
Ok(())
}
pub fn write_pid_file(project_dir: Option<&Path>, pid: u32) -> Result<(), GgenError> {
let pid_file = project_dir
.unwrap_or_else(|| Path::new("."))
.join(MCP_SERVER_PID_FILE);
if let Some(parent) = pid_file.parent() {
fs::create_dir_all(parent).map_err(|e| {
GgenError::invalid_input(format!(
"Failed to create directory {}: {}",
parent.display(),
e
))
})?;
}
fs::write(&pid_file, pid.to_string()).map_err(|e| {
GgenError::invalid_input(format!(
"Failed to write PID file {}: {}",
pid_file.display(),
e
))
})?;
Ok(())
}
fn timestamp_now() -> String {
format_timestamp(
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
)
}
fn format_timestamp(secs: u64) -> String {
use chrono::{DateTime, Utc};
let dt = DateTime::<Utc>::from_timestamp(secs as i64, 0).unwrap_or_default();
dt.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
}
impl std::fmt::Display for ConfigPriority {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ConfigPriority::CliArgs => write!(f, "CLI arguments"),
ConfigPriority::EnvVars => write!(f, "Environment variables"),
ConfigPriority::Project => write!(f, "Project configuration"),
ConfigPriority::User => write!(f, "User configuration"),
ConfigPriority::System => write!(f, "System configuration"),
ConfigPriority::Defaults => write!(f, "Default values"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mcp_server_config_builder() {
let config = McpServerConfig::new("test-command")
.with_arg("--verbose")
.with_arg("--output")
.with_env("TEST_VAR", "test_value")
.with_timeout(Duration::from_mins(1))
.with_cwd("/tmp");
assert_eq!(config.command, "test-command");
assert_eq!(config.args.len(), 2);
assert_eq!(config.env.get("TEST_VAR"), Some(&"test_value".to_string()));
assert_eq!(config.timeout, 60);
assert_eq!(config.cwd, Some("/tmp".to_string()));
}
#[test]
fn test_mcp_server_validation() {
let config = McpServerConfig::new("npx")
.with_args(vec!["@anthropic-ai/claude-code-guide".to_string()]);
assert!(config.validate().is_ok());
let invalid = McpServerConfig::new("");
assert!(invalid.validate().is_err());
let invalid_timeout = McpServerConfig::new("test").with_timeout(Duration::from_secs(0));
assert!(invalid_timeout.validate().is_err());
}
#[test]
fn test_a2a_config_default() {
let config = A2aConfig::default();
assert_eq!(config.server.host, "127.0.0.1");
assert_eq!(config.server.port, 8080);
assert_eq!(config.server.timeout, 30);
assert!(!config.server.tls_enabled);
}
#[test]
fn test_a2a_config_validation() {
let config = A2aConfig::default();
assert!(config.validate().is_ok());
let mut invalid = config.clone();
invalid.server.port = 0;
assert!(invalid.validate().is_err());
let mut tls_invalid = config.clone();
tls_invalid.server.tls_enabled = true;
assert!(tls_invalid.validate().is_err());
}
#[test]
fn test_a2a_server_url() {
let config = A2aConfig::default();
assert_eq!(config.server_url(), "http://127.0.0.1:8080");
let mut tls_config = A2aConfig::default();
tls_config.server.tls_enabled = true;
tls_config.server.host = "example.com".to_string();
tls_config.server.port = 8443;
assert_eq!(tls_config.server_url(), "https://example.com:8443");
}
#[test]
fn test_mcp_config_file_default() {
let config = McpConfigFile::default();
assert!(config.mcp_servers.is_empty());
assert_eq!(config.version, "1.0.0");
assert!(config.description.is_some());
}
#[test]
fn test_config_priority_display() {
assert_eq!(format!("{}", ConfigPriority::CliArgs), "CLI arguments");
assert_eq!(
format!("{}", ConfigPriority::EnvVars),
"Environment variables"
);
assert_eq!(
format!("{}", ConfigPriority::Project),
"Project configuration"
);
}
#[test]
fn test_validation_result_serialization() {
let result = ValidationResult {
server_name: "test-server".to_string(),
is_valid: true,
errors: vec![],
warnings: vec!["Warning message".to_string()],
};
let json = serde_json::to_string(&result).unwrap();
assert!(json.contains("test-server"));
assert!(json.contains("true"));
}
}