coderlib 0.1.0

A Rust library for AI-powered code assistance and agentic system
Documentation
//! MCP configuration types and management

use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;

/// Main MCP configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpConfig {
    /// Whether MCP support is enabled
    pub enabled: bool,
    /// Map of server name to server configuration
    pub servers: HashMap<String, McpServerConfig>,
    /// Default timeout for MCP operations in seconds
    pub timeout_seconds: u64,
    /// Maximum number of concurrent MCP connections
    pub max_connections: usize,
    /// Whether to automatically start servers on initialization
    pub auto_start: bool,
    /// Global environment variables for all MCP servers
    pub global_env: HashMap<String, String>,
}

/// Configuration for a single MCP server
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpServerConfig {
    /// Human-readable name for the server
    pub name: String,
    /// Transport configuration
    pub transport: McpTransportConfig,
    /// Whether this server is enabled
    pub enabled: bool,
    /// Whether to automatically start this server
    pub auto_start: bool,
    /// Environment variables specific to this server
    pub env: HashMap<String, String>,
    /// Timeout for this server in seconds (overrides global)
    pub timeout_seconds: Option<u64>,
    /// Working directory for the server process
    pub working_directory: Option<PathBuf>,
    /// Whether to restart the server on failure
    pub auto_restart: bool,
    /// Maximum number of restart attempts
    pub max_restart_attempts: u32,
}

/// Transport configuration for MCP servers
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum McpTransportConfig {
    /// Standard I/O transport (for command-line tools)
    Stdio {
        /// Command to execute
        command: String,
        /// Command arguments
        args: Vec<String>,
    },
    /// Server-Sent Events transport (for web services)
    Sse {
        /// URL of the SSE endpoint
        url: String,
        /// HTTP headers to send
        headers: HashMap<String, String>,
    },
}

impl Default for McpConfig {
    fn default() -> Self {
        Self {
            enabled: false,
            servers: HashMap::new(),
            timeout_seconds: 30,
            max_connections: 10,
            auto_start: true,
            global_env: HashMap::new(),
        }
    }
}

impl Default for McpServerConfig {
    fn default() -> Self {
        Self {
            name: "Unnamed MCP Server".to_string(),
            transport: McpTransportConfig::Stdio {
                command: "echo".to_string(),
                args: vec!["Hello from MCP".to_string()],
            },
            enabled: true,
            auto_start: true,
            env: HashMap::new(),
            timeout_seconds: None,
            working_directory: None,
            auto_restart: true,
            max_restart_attempts: 3,
        }
    }
}

impl McpConfig {
    /// Create a new MCP configuration
    pub fn new() -> Self {
        Self::default()
    }
    
    /// Enable MCP support
    pub fn enable(&mut self) -> &mut Self {
        self.enabled = true;
        self
    }
    
    /// Disable MCP support
    pub fn disable(&mut self) -> &mut Self {
        self.enabled = false;
        self
    }
    
    /// Add a server configuration
    pub fn add_server(&mut self, name: String, config: McpServerConfig) -> &mut Self {
        self.servers.insert(name, config);
        self
    }
    
    /// Remove a server configuration
    pub fn remove_server(&mut self, name: &str) -> Option<McpServerConfig> {
        self.servers.remove(name)
    }
    
    /// Get a server configuration by name
    pub fn get_server(&self, name: &str) -> Option<&McpServerConfig> {
        self.servers.get(name)
    }
    
    /// Get a mutable server configuration by name
    pub fn get_server_mut(&mut self, name: &str) -> Option<&mut McpServerConfig> {
        self.servers.get_mut(name)
    }
    
    /// List all enabled servers
    pub fn enabled_servers(&self) -> impl Iterator<Item = (&String, &McpServerConfig)> {
        self.servers.iter().filter(|(_, config)| config.enabled)
    }
    
    /// List all auto-start servers
    pub fn auto_start_servers(&self) -> impl Iterator<Item = (&String, &McpServerConfig)> {
        self.servers
            .iter()
            .filter(|(_, config)| config.enabled && config.auto_start)
    }
    
    /// Validate the configuration
    pub fn validate(&self) -> Result<(), String> {
        if self.timeout_seconds == 0 {
            return Err("Timeout must be greater than 0".to_string());
        }
        
        if self.max_connections == 0 {
            return Err("Max connections must be greater than 0".to_string());
        }
        
        for (name, server) in &self.servers {
            server.validate().map_err(|e| format!("Server '{}': {}", name, e))?;
        }
        
        Ok(())
    }
    
    /// Merge environment variables (global + server-specific)
    pub fn merged_env(&self, server_name: &str) -> HashMap<String, String> {
        let mut env = self.global_env.clone();
        
        if let Some(server) = self.get_server(server_name) {
            env.extend(server.env.clone());
        }
        
        env
    }
}

impl McpServerConfig {
    /// Create a new stdio server configuration
    pub fn stdio(name: String, command: String, args: Vec<String>) -> Self {
        Self {
            name,
            transport: McpTransportConfig::Stdio { command, args },
            ..Default::default()
        }
    }
    
    /// Create a new SSE server configuration
    pub fn sse(name: String, url: String) -> Self {
        Self {
            name,
            transport: McpTransportConfig::Sse {
                url,
                headers: HashMap::new(),
            },
            ..Default::default()
        }
    }
    
    /// Set environment variables
    pub fn with_env(mut self, env: HashMap<String, String>) -> Self {
        self.env = env;
        self
    }
    
    /// Add an environment variable
    pub fn add_env(mut self, key: String, value: String) -> Self {
        self.env.insert(key, value);
        self
    }
    
    /// Set timeout
    pub fn with_timeout(mut self, timeout_seconds: u64) -> Self {
        self.timeout_seconds = Some(timeout_seconds);
        self
    }
    
    /// Set working directory
    pub fn with_working_directory(mut self, dir: PathBuf) -> Self {
        self.working_directory = Some(dir);
        self
    }
    
    /// Enable or disable auto-restart
    pub fn with_auto_restart(mut self, auto_restart: bool, max_attempts: u32) -> Self {
        self.auto_restart = auto_restart;
        self.max_restart_attempts = max_attempts;
        self
    }
    
    /// Validate the server configuration
    pub fn validate(&self) -> Result<(), String> {
        if self.name.is_empty() {
            return Err("Server name cannot be empty".to_string());
        }
        
        match &self.transport {
            McpTransportConfig::Stdio { command, .. } => {
                if command.is_empty() {
                    return Err("Stdio command cannot be empty".to_string());
                }
            }
            McpTransportConfig::Sse { url, .. } => {
                if url.is_empty() {
                    return Err("SSE URL cannot be empty".to_string());
                }
                
                // Basic URL validation
                if !url.starts_with("http://") && !url.starts_with("https://") {
                    return Err("SSE URL must start with http:// or https://".to_string());
                }
            }
        }
        
        if let Some(timeout) = self.timeout_seconds {
            if timeout == 0 {
                return Err("Server timeout must be greater than 0".to_string());
            }
        }
        
        Ok(())
    }
    
    /// Get the effective timeout for this server
    pub fn effective_timeout(&self, default_timeout: u64) -> u64 {
        self.timeout_seconds.unwrap_or(default_timeout)
    }
}

impl McpTransportConfig {
    /// Get a human-readable description of the transport
    pub fn description(&self) -> String {
        match self {
            McpTransportConfig::Stdio { command, args } => {
                if args.is_empty() {
                    format!("stdio: {}", command)
                } else {
                    format!("stdio: {} {}", command, args.join(" "))
                }
            }
            McpTransportConfig::Sse { url, .. } => {
                format!("sse: {}", url)
            }
        }
    }
    
    /// Check if this is a stdio transport
    pub fn is_stdio(&self) -> bool {
        matches!(self, McpTransportConfig::Stdio { .. })
    }
    
    /// Check if this is an SSE transport
    pub fn is_sse(&self) -> bool {
        matches!(self, McpTransportConfig::Sse { .. })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_mcp_config_default() {
        let config = McpConfig::default();
        assert!(!config.enabled);
        assert_eq!(config.timeout_seconds, 30);
        assert_eq!(config.max_connections, 10);
        assert!(config.auto_start);
    }
    
    #[test]
    fn test_mcp_config_validation() {
        let mut config = McpConfig::default();
        config.timeout_seconds = 0;
        
        assert!(config.validate().is_err());
        
        config.timeout_seconds = 30;
        assert!(config.validate().is_ok());
    }
    
    #[test]
    fn test_server_config_stdio() {
        let config = McpServerConfig::stdio(
            "test".to_string(),
            "echo".to_string(),
            vec!["hello".to_string()],
        );
        
        assert_eq!(config.name, "test");
        assert!(config.transport.is_stdio());
        assert!(config.validate().is_ok());
    }
    
    #[test]
    fn test_server_config_sse() {
        let config = McpServerConfig::sse(
            "test".to_string(),
            "https://example.com/mcp".to_string(),
        );
        
        assert_eq!(config.name, "test");
        assert!(config.transport.is_sse());
        assert!(config.validate().is_ok());
    }
    
    #[test]
    fn test_transport_description() {
        let stdio = McpTransportConfig::Stdio {
            command: "echo".to_string(),
            args: vec!["hello".to_string()],
        };
        assert_eq!(stdio.description(), "stdio: echo hello");
        
        let sse = McpTransportConfig::Sse {
            url: "https://example.com".to_string(),
            headers: HashMap::new(),
        };
        assert_eq!(sse.description(), "sse: https://example.com");
    }
}