Skip to main content
Glama
config.rs10.1 kB
//! Configuration types for Terminal MCP Server. use serde::{Deserialize, Serialize}; use std::path::Path; /// Server configuration loaded from YAML file. #[derive(Debug, Clone, Serialize, Deserialize, Default)] #[serde(default)] pub struct ServerConfig { /// Server settings pub server: ServerSettings, /// Security settings pub security: SecuritySettings, /// Detection settings pub detection: DetectionSettings, /// Terminal settings pub terminal: TerminalSettings, } impl ServerConfig { /// Load configuration from a YAML file. pub fn from_file<P: AsRef<Path>>(path: P) -> crate::Result<Self> { let content = std::fs::read_to_string(path)?; Self::from_yaml(&content) } /// Parse configuration from YAML string. pub fn from_yaml(yaml: &str) -> crate::Result<Self> { let config: ServerConfig = serde_yaml::from_str(yaml) .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; config.validate()?; Ok(config) } /// Validate configuration values. pub fn validate(&self) -> crate::Result<()> { // Validate max_sessions if self.server.max_sessions == 0 { return Err(std::io::Error::new( std::io::ErrorKind::InvalidInput, "server.max_sessions must be > 0", ) .into()); } // Validate terminal dimensions if self.terminal.default_rows == 0 || self.terminal.default_cols == 0 { return Err(std::io::Error::new( std::io::ErrorKind::InvalidInput, "terminal dimensions must be > 0", ) .into()); } // Validate custom patterns for pattern in &self.detection.custom_patterns { pattern.validate()?; } Ok(()) } } /// Server settings. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(default)] pub struct ServerSettings { /// Transport type (stdio, tcp, etc.) pub transport: String, /// Maximum number of concurrent sessions pub max_sessions: usize, /// Session timeout in seconds (0 = no timeout) pub session_timeout: u64, /// Log level (trace, debug, info, warn, error) pub log_level: String, } impl Default for ServerSettings { fn default() -> Self { Self { transport: "stdio".to_string(), max_sessions: 10, session_timeout: 3600, log_level: "info".to_string(), } } } /// Security settings. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(default)] pub struct SecuritySettings { /// List of allowed commands (empty = allow all) pub allowed_commands: Vec<String>, /// Sandbox mode: none, container, namespace pub sandbox_mode: String, } impl Default for SecuritySettings { fn default() -> Self { Self { allowed_commands: vec![], sandbox_mode: "none".to_string(), } } } impl SecuritySettings { /// Check if a command is allowed. /// /// Returns true if allowed_commands is empty (allow all) or if the command /// matches one of the allowed commands. pub fn is_command_allowed(&self, command: &str) -> bool { if self.allowed_commands.is_empty() { return true; } self.allowed_commands .iter() .any(|allowed| allowed == command) } } /// Detection settings. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(default)] pub struct DetectionSettings { /// Idle threshold in milliseconds pub idle_threshold_ms: u64, /// Maximum idle wait time in milliseconds pub max_idle_wait_ms: u64, /// Custom pattern definitions pub custom_patterns: Vec<CustomPatternConfig>, } impl Default for DetectionSettings { fn default() -> Self { Self { idle_threshold_ms: 100, max_idle_wait_ms: 5000, custom_patterns: vec![], } } } /// Custom pattern configuration for domain-specific element detection. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CustomPatternConfig { /// Pattern name (identifier) pub name: String, /// Regular expression pattern pub pattern: String, /// Element type to generate pub element_type: String, /// Named captures #[serde(default)] pub captures: Vec<CaptureConfig>, } impl CustomPatternConfig { /// Validate the pattern configuration. pub fn validate(&self) -> crate::Result<()> { // Validate name is not empty if self.name.trim().is_empty() { return Err(std::io::Error::new( std::io::ErrorKind::InvalidInput, "custom pattern name cannot be empty", ) .into()); } // Validate regex pattern regex::Regex::new(&self.pattern).map_err(|e| { std::io::Error::new( std::io::ErrorKind::InvalidInput, format!("Invalid regex pattern '{}': {}", self.name, e), ) })?; // Validate element_type if self.element_type.trim().is_empty() { return Err(std::io::Error::new( std::io::ErrorKind::InvalidInput, format!( "custom pattern '{}' element_type cannot be empty", self.name ), ) .into()); } Ok(()) } } /// Named capture configuration. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CaptureConfig { /// Capture name pub name: String, } /// Terminal settings. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(default)] pub struct TerminalSettings { /// Default terminal rows pub default_rows: u16, /// Default terminal columns pub default_cols: u16, /// Scrollback buffer lines pub scrollback_lines: usize, /// TERM environment variable value pub term: String, } impl Default for TerminalSettings { fn default() -> Self { Self { default_rows: 24, default_cols: 80, scrollback_lines: 10000, term: "xterm-256color".to_string(), } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_default_config() { let config = ServerConfig::default(); assert_eq!(config.server.transport, "stdio"); assert_eq!(config.server.max_sessions, 10); assert_eq!(config.terminal.default_rows, 24); assert_eq!(config.terminal.default_cols, 80); } #[test] fn test_config_validation() { let config = ServerConfig::default(); assert!(config.validate().is_ok()); } #[test] fn test_invalid_max_sessions() { let mut config = ServerConfig::default(); config.server.max_sessions = 0; assert!(config.validate().is_err()); } #[test] fn test_invalid_dimensions() { let mut config = ServerConfig::default(); config.terminal.default_rows = 0; assert!(config.validate().is_err()); } #[test] fn test_parse_yaml() { let yaml = r#" server: transport: stdio max_sessions: 5 session_timeout: 1800 log_level: debug security: allowed_commands: - /bin/bash - /usr/bin/htop sandbox_mode: none detection: idle_threshold_ms: 200 max_idle_wait_ms: 10000 custom_patterns: [] terminal: default_rows: 30 default_cols: 120 scrollback_lines: 5000 term: "xterm-256color" "#; let config = ServerConfig::from_yaml(yaml).unwrap(); assert_eq!(config.server.max_sessions, 5); assert_eq!(config.server.session_timeout, 1800); assert_eq!(config.server.log_level, "debug"); assert_eq!(config.security.allowed_commands.len(), 2); assert_eq!(config.detection.idle_threshold_ms, 200); assert_eq!(config.terminal.default_rows, 30); assert_eq!(config.terminal.default_cols, 120); } #[test] fn test_custom_patterns() { let yaml = r#" detection: custom_patterns: - name: "service_status" pattern: "^(\\w+)\\s+(Running|Stopped)$" element_type: "table_row" captures: - name: "service" - name: "status" "#; let config = ServerConfig::from_yaml(yaml).unwrap(); assert_eq!(config.detection.custom_patterns.len(), 1); assert_eq!(config.detection.custom_patterns[0].name, "service_status"); assert_eq!(config.detection.custom_patterns[0].captures.len(), 2); } #[test] fn test_invalid_regex_pattern() { let yaml = r#" detection: custom_patterns: - name: "bad_pattern" pattern: "([unclosed" element_type: "text" "#; let result = ServerConfig::from_yaml(yaml); assert!(result.is_err()); } #[test] fn test_security_command_allowed() { let mut settings = SecuritySettings::default(); // Empty list = allow all assert!(settings.is_command_allowed("/bin/bash")); assert!(settings.is_command_allowed("anything")); // With allowlist settings.allowed_commands = vec!["/bin/bash".to_string(), "/usr/bin/htop".to_string()]; assert!(settings.is_command_allowed("/bin/bash")); assert!(settings.is_command_allowed("/usr/bin/htop")); assert!(!settings.is_command_allowed("/bin/sh")); } #[test] fn test_empty_pattern_name() { let pattern = CustomPatternConfig { name: "".to_string(), pattern: "test".to_string(), element_type: "text".to_string(), captures: vec![], }; assert!(pattern.validate().is_err()); } #[test] fn test_empty_element_type() { let pattern = CustomPatternConfig { name: "test".to_string(), pattern: "test".to_string(), element_type: "".to_string(), captures: vec![], }; assert!(pattern.validate().is_err()); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/aybelatchane/mcp-server-terminal'

If you have feedback or need assistance with the MCP directory API, please join our Discord server