1use crate::error::{Result, ThingsError};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::path::{Path, PathBuf};
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct McpServerConfig {
14 pub server: ServerConfig,
16 pub database: DatabaseConfig,
18 pub logging: LoggingConfig,
20 pub performance: PerformanceConfig,
22 pub security: SecurityConfig,
24 pub cache: CacheConfig,
26 pub monitoring: MonitoringConfig,
28 pub features: FeatureFlags,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct ServerConfig {
35 pub name: String,
37 pub version: String,
39 pub description: String,
41 pub max_connections: u32,
43 pub connection_timeout: u64,
45 pub request_timeout: u64,
47 pub graceful_shutdown: bool,
49 pub shutdown_timeout: u64,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct DatabaseConfig {
56 pub path: PathBuf,
58 pub fallback_to_default: bool,
60 pub pool_size: u32,
62 pub connection_timeout: u64,
64 pub query_timeout: u64,
66 pub enable_query_logging: bool,
68 pub enable_query_metrics: bool,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct LoggingConfig {
75 pub level: String,
77 pub json_logs: bool,
79 pub log_file: Option<PathBuf>,
81 pub console_logs: bool,
83 pub structured_logs: bool,
85 pub rotation: LogRotationConfig,
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct LogRotationConfig {
92 pub enabled: bool,
94 pub max_file_size_mb: u64,
96 pub max_files: u32,
98 pub compress: bool,
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct PerformanceConfig {
105 pub enabled: bool,
107 pub slow_request_threshold_ms: u64,
109 pub enable_profiling: bool,
111 pub memory_monitoring: MemoryMonitoringConfig,
113 pub cpu_monitoring: CpuMonitoringConfig,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct MemoryMonitoringConfig {
120 pub enabled: bool,
122 pub threshold_percentage: f64,
124 pub check_interval: u64,
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct CpuMonitoringConfig {
131 pub enabled: bool,
133 pub threshold_percentage: f64,
135 pub check_interval: u64,
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct SecurityConfig {
142 pub authentication: AuthenticationConfig,
144 pub rate_limiting: RateLimitingConfig,
146 pub cors: CorsConfig,
148 pub validation: ValidationConfig,
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct AuthenticationConfig {
155 pub enabled: bool,
157 pub require_auth: bool,
159 pub jwt_secret: String,
161 pub jwt_expiration: u64,
163 pub api_keys: Vec<ApiKeyConfig>,
165 pub oauth: Option<OAuth2Config>,
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct ApiKeyConfig {
172 pub key: String,
174 pub key_id: String,
176 pub permissions: Vec<String>,
178 pub expires_at: Option<String>,
180}
181
182#[derive(Debug, Clone, Serialize, Deserialize)]
184pub struct OAuth2Config {
185 pub client_id: String,
187 pub client_secret: String,
189 pub token_endpoint: String,
191 pub scopes: Vec<String>,
193}
194
195#[derive(Debug, Clone, Serialize, Deserialize)]
197pub struct RateLimitingConfig {
198 pub enabled: bool,
200 pub requests_per_minute: u32,
202 pub burst_limit: u32,
204 pub custom_limits: Option<HashMap<String, u32>>,
206}
207
208#[derive(Debug, Clone, Serialize, Deserialize)]
210pub struct CorsConfig {
211 pub enabled: bool,
213 pub allowed_origins: Vec<String>,
215 pub allowed_methods: Vec<String>,
217 pub allowed_headers: Vec<String>,
219 pub exposed_headers: Vec<String>,
221 pub allow_credentials: bool,
223 pub max_age: u64,
225}
226
227#[derive(Debug, Clone, Serialize, Deserialize)]
229pub struct ValidationConfig {
230 pub enabled: bool,
232 pub strict_mode: bool,
234 pub max_request_size: u64,
236 pub max_field_length: usize,
238}
239
240#[derive(Debug, Clone, Serialize, Deserialize)]
242pub struct CacheConfig {
243 pub enabled: bool,
245 pub cache_type: String,
247 pub max_size_mb: u64,
249 pub ttl_seconds: u64,
251 pub compression: bool,
253 pub eviction_policy: String,
255}
256
257#[derive(Debug, Clone, Serialize, Deserialize)]
259pub struct MonitoringConfig {
260 pub enabled: bool,
262 pub metrics_port: u16,
264 pub health_port: u16,
266 pub health_checks: bool,
268 pub metrics_collection: bool,
270 pub metrics_path: String,
272 pub health_path: String,
274}
275
276#[derive(Debug, Clone, Serialize, Deserialize)]
278#[allow(clippy::struct_excessive_bools)]
279pub struct FeatureFlags {
280 pub real_time_updates: bool,
282 pub websocket_server: bool,
284 pub dashboard: bool,
286 pub bulk_operations: bool,
288 pub data_export: bool,
290 pub backup: bool,
292 pub hot_reloading: bool,
294}
295
296impl McpServerConfig {
297 #[must_use]
299 pub fn new() -> Self {
300 Self::default()
301 }
302
303 #[allow(clippy::too_many_lines)]
308 pub fn from_env() -> Result<Self> {
309 let mut config = Self::default();
310
311 if let Ok(name) = std::env::var("MCP_SERVER_NAME") {
313 config.server.name = name;
314 }
315 if let Ok(version) = std::env::var("MCP_SERVER_VERSION") {
316 config.server.version = version;
317 }
318 if let Ok(description) = std::env::var("MCP_SERVER_DESCRIPTION") {
319 config.server.description = description;
320 }
321 if let Ok(max_connections) = std::env::var("MCP_MAX_CONNECTIONS") {
322 config.server.max_connections = max_connections
323 .parse()
324 .map_err(|_| ThingsError::configuration("Invalid MCP_MAX_CONNECTIONS value"))?;
325 }
326 if let Ok(connection_timeout) = std::env::var("MCP_CONNECTION_TIMEOUT") {
327 config.server.connection_timeout = connection_timeout
328 .parse()
329 .map_err(|_| ThingsError::configuration("Invalid MCP_CONNECTION_TIMEOUT value"))?;
330 }
331 if let Ok(request_timeout) = std::env::var("MCP_REQUEST_TIMEOUT") {
332 config.server.request_timeout = request_timeout
333 .parse()
334 .map_err(|_| ThingsError::configuration("Invalid MCP_REQUEST_TIMEOUT value"))?;
335 }
336
337 if let Ok(db_path) = std::env::var("MCP_DATABASE_PATH") {
339 config.database.path = PathBuf::from(db_path);
340 }
341 if let Ok(fallback) = std::env::var("MCP_DATABASE_FALLBACK") {
342 config.database.fallback_to_default = parse_bool(&fallback);
343 }
344 if let Ok(pool_size) = std::env::var("MCP_DATABASE_POOL_SIZE") {
345 config.database.pool_size = pool_size
346 .parse()
347 .map_err(|_| ThingsError::configuration("Invalid MCP_DATABASE_POOL_SIZE value"))?;
348 }
349
350 if let Ok(level) = std::env::var("MCP_LOG_LEVEL") {
352 config.logging.level = level;
353 }
354 if let Ok(json_logs) = std::env::var("MCP_JSON_LOGS") {
355 config.logging.json_logs = parse_bool(&json_logs);
356 }
357 if let Ok(log_file) = std::env::var("MCP_LOG_FILE") {
358 config.logging.log_file = Some(PathBuf::from(log_file));
359 }
360 if let Ok(console_logs) = std::env::var("MCP_CONSOLE_LOGS") {
361 config.logging.console_logs = parse_bool(&console_logs);
362 }
363
364 if let Ok(enabled) = std::env::var("MCP_PERFORMANCE_ENABLED") {
366 config.performance.enabled = parse_bool(&enabled);
367 }
368 if let Ok(threshold) = std::env::var("MCP_SLOW_REQUEST_THRESHOLD") {
369 config.performance.slow_request_threshold_ms = threshold.parse().map_err(|_| {
370 ThingsError::configuration("Invalid MCP_SLOW_REQUEST_THRESHOLD value")
371 })?;
372 }
373
374 if let Ok(auth_enabled) = std::env::var("MCP_AUTH_ENABLED") {
376 config.security.authentication.enabled = parse_bool(&auth_enabled);
377 }
378 if let Ok(jwt_secret) = std::env::var("MCP_JWT_SECRET") {
379 config.security.authentication.jwt_secret = jwt_secret;
380 }
381 if let Ok(rate_limit_enabled) = std::env::var("MCP_RATE_LIMIT_ENABLED") {
382 config.security.rate_limiting.enabled = parse_bool(&rate_limit_enabled);
383 }
384 if let Ok(requests_per_minute) = std::env::var("MCP_REQUESTS_PER_MINUTE") {
385 config.security.rate_limiting.requests_per_minute = requests_per_minute
386 .parse()
387 .map_err(|_| ThingsError::configuration("Invalid MCP_REQUESTS_PER_MINUTE value"))?;
388 }
389
390 if let Ok(cache_enabled) = std::env::var("MCP_CACHE_ENABLED") {
392 config.cache.enabled = parse_bool(&cache_enabled);
393 }
394 if let Ok(cache_type) = std::env::var("MCP_CACHE_TYPE") {
395 config.cache.cache_type = cache_type;
396 }
397 if let Ok(max_size) = std::env::var("MCP_CACHE_MAX_SIZE_MB") {
398 config.cache.max_size_mb = max_size
399 .parse()
400 .map_err(|_| ThingsError::configuration("Invalid MCP_CACHE_MAX_SIZE_MB value"))?;
401 }
402
403 if let Ok(monitoring_enabled) = std::env::var("MCP_MONITORING_ENABLED") {
405 config.monitoring.enabled = parse_bool(&monitoring_enabled);
406 }
407 if let Ok(metrics_port) = std::env::var("MCP_METRICS_PORT") {
408 config.monitoring.metrics_port = metrics_port
409 .parse()
410 .map_err(|_| ThingsError::configuration("Invalid MCP_METRICS_PORT value"))?;
411 }
412 if let Ok(health_port) = std::env::var("MCP_HEALTH_PORT") {
413 config.monitoring.health_port = health_port
414 .parse()
415 .map_err(|_| ThingsError::configuration("Invalid MCP_HEALTH_PORT value"))?;
416 }
417
418 if let Ok(real_time) = std::env::var("MCP_REAL_TIME_UPDATES") {
420 config.features.real_time_updates = parse_bool(&real_time);
421 }
422 if let Ok(websocket) = std::env::var("MCP_WEBSOCKET_SERVER") {
423 config.features.websocket_server = parse_bool(&websocket);
424 }
425 if let Ok(dashboard) = std::env::var("MCP_DASHBOARD") {
426 config.features.dashboard = parse_bool(&dashboard);
427 }
428 if let Ok(bulk_ops) = std::env::var("MCP_BULK_OPERATIONS") {
429 config.features.bulk_operations = parse_bool(&bulk_ops);
430 }
431 if let Ok(data_export) = std::env::var("MCP_DATA_EXPORT") {
432 config.features.data_export = parse_bool(&data_export);
433 }
434 if let Ok(backup) = std::env::var("MCP_BACKUP") {
435 config.features.backup = parse_bool(&backup);
436 }
437 if let Ok(hot_reload) = std::env::var("MCP_HOT_RELOADING") {
438 config.features.hot_reloading = parse_bool(&hot_reload);
439 }
440
441 Ok(config)
442 }
443
444 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
452 let path = path.as_ref();
453 let content = std::fs::read_to_string(path).map_err(|e| {
454 ThingsError::Io(std::io::Error::other(format!(
455 "Failed to read config file {}: {}",
456 path.display(),
457 e
458 )))
459 })?;
460
461 let config = if path.extension().and_then(|s| s.to_str()) == Some("yaml")
462 || path.extension().and_then(|s| s.to_str()) == Some("yml")
463 {
464 serde_yaml::from_str(&content).map_err(|e| {
465 ThingsError::configuration(format!("Failed to parse YAML config: {e}"))
466 })?
467 } else {
468 serde_json::from_str(&content).map_err(|e| {
469 ThingsError::configuration(format!("Failed to parse JSON config: {e}"))
470 })?
471 };
472
473 Ok(config)
474 }
475
476 pub fn to_file<P: AsRef<Path>>(&self, path: P, format: &str) -> Result<()> {
485 let path = path.as_ref();
486 let content = match format {
487 "yaml" | "yml" => serde_yaml::to_string(self).map_err(|e| {
488 ThingsError::configuration(format!("Failed to serialize YAML: {e}"))
489 })?,
490 "json" => serde_json::to_string_pretty(self).map_err(|e| {
491 ThingsError::configuration(format!("Failed to serialize JSON: {e}"))
492 })?,
493 _ => {
494 return Err(ThingsError::configuration(format!(
495 "Unsupported format: {format}"
496 )))
497 }
498 };
499
500 std::fs::write(path, content).map_err(|e| {
501 ThingsError::Io(std::io::Error::other(format!(
502 "Failed to write config file {}: {}",
503 path.display(),
504 e
505 )))
506 })?;
507
508 Ok(())
509 }
510
511 pub fn validate(&self) -> Result<()> {
516 if self.server.name.is_empty() {
518 return Err(ThingsError::configuration("Server name cannot be empty"));
519 }
520 if self.server.version.is_empty() {
521 return Err(ThingsError::configuration("Server version cannot be empty"));
522 }
523 if self.server.max_connections == 0 {
524 return Err(ThingsError::configuration(
525 "Max connections must be greater than 0",
526 ));
527 }
528
529 if self.database.pool_size == 0 {
531 return Err(ThingsError::configuration(
532 "Database pool size must be greater than 0",
533 ));
534 }
535
536 let valid_levels = ["trace", "debug", "info", "warn", "error"];
538 if !valid_levels.contains(&self.logging.level.as_str()) {
539 return Err(ThingsError::configuration(format!(
540 "Invalid log level: {}. Must be one of: {}",
541 self.logging.level,
542 valid_levels.join(", ")
543 )));
544 }
545
546 if self.performance.enabled && self.performance.slow_request_threshold_ms == 0 {
548 return Err(ThingsError::configuration("Slow request threshold must be greater than 0 when performance monitoring is enabled"));
549 }
550
551 if self.security.authentication.enabled
553 && self.security.authentication.jwt_secret.is_empty()
554 {
555 return Err(ThingsError::configuration(
556 "JWT secret cannot be empty when authentication is enabled",
557 ));
558 }
559
560 if self.cache.enabled && self.cache.max_size_mb == 0 {
562 return Err(ThingsError::configuration(
563 "Cache max size must be greater than 0 when caching is enabled",
564 ));
565 }
566
567 if self.monitoring.enabled && self.monitoring.metrics_port == 0 {
569 return Err(ThingsError::configuration(
570 "Metrics port must be greater than 0 when monitoring is enabled",
571 ));
572 }
573 if self.monitoring.enabled && self.monitoring.health_port == 0 {
574 return Err(ThingsError::configuration(
575 "Health port must be greater than 0 when monitoring is enabled",
576 ));
577 }
578
579 Ok(())
580 }
581
582 pub fn merge_with(&mut self, other: &McpServerConfig) {
584 if !other.server.name.is_empty() {
586 self.server.name.clone_from(&other.server.name);
587 }
588 if !other.server.version.is_empty() {
589 self.server.version.clone_from(&other.server.version);
590 }
591 if !other.server.description.is_empty() {
592 self.server
593 .description
594 .clone_from(&other.server.description);
595 }
596 if other.server.max_connections > 0 {
597 self.server.max_connections = other.server.max_connections;
598 }
599 if other.server.connection_timeout > 0 {
600 self.server.connection_timeout = other.server.connection_timeout;
601 }
602 if other.server.request_timeout > 0 {
603 self.server.request_timeout = other.server.request_timeout;
604 }
605
606 if other.database.path != PathBuf::new() {
608 self.database.path.clone_from(&other.database.path);
609 }
610 if other.database.pool_size > 0 {
611 self.database.pool_size = other.database.pool_size;
612 }
613
614 if !other.logging.level.is_empty() {
616 self.logging.level.clone_from(&other.logging.level);
617 }
618 if other.logging.log_file.is_some() {
619 self.logging.log_file.clone_from(&other.logging.log_file);
620 }
621
622 self.performance.enabled = other.performance.enabled;
624 if other.performance.slow_request_threshold_ms > 0 {
625 self.performance.slow_request_threshold_ms =
626 other.performance.slow_request_threshold_ms;
627 }
628
629 self.security.authentication.enabled = other.security.authentication.enabled;
631 if !other.security.authentication.jwt_secret.is_empty() {
632 self.security
633 .authentication
634 .jwt_secret
635 .clone_from(&other.security.authentication.jwt_secret);
636 }
637 self.security.rate_limiting.enabled = other.security.rate_limiting.enabled;
638 if other.security.rate_limiting.requests_per_minute > 0 {
639 self.security.rate_limiting.requests_per_minute =
640 other.security.rate_limiting.requests_per_minute;
641 }
642
643 self.cache.enabled = other.cache.enabled;
645 if other.cache.max_size_mb > 0 {
646 self.cache.max_size_mb = other.cache.max_size_mb;
647 }
648
649 self.monitoring.enabled = other.monitoring.enabled;
651 if other.monitoring.metrics_port > 0 {
652 self.monitoring.metrics_port = other.monitoring.metrics_port;
653 }
654 if other.monitoring.health_port > 0 {
655 self.monitoring.health_port = other.monitoring.health_port;
656 }
657
658 self.features.real_time_updates = other.features.real_time_updates;
660 self.features.websocket_server = other.features.websocket_server;
661 self.features.dashboard = other.features.dashboard;
662 self.features.bulk_operations = other.features.bulk_operations;
663 self.features.data_export = other.features.data_export;
664 self.features.backup = other.features.backup;
665 self.features.hot_reloading = other.features.hot_reloading;
666 }
667
668 pub fn get_effective_database_path(&self) -> Result<PathBuf> {
673 if self.database.path.exists() {
675 return Ok(self.database.path.clone());
676 }
677
678 if self.database.fallback_to_default {
680 let default_path = Self::get_default_database_path();
681 if default_path.exists() {
682 return Ok(default_path);
683 }
684 }
685
686 Err(ThingsError::configuration(format!(
687 "Database not found at {} and fallback is {}",
688 self.database.path.display(),
689 if self.database.fallback_to_default {
690 "enabled but default path also not found"
691 } else {
692 "disabled"
693 }
694 )))
695 }
696
697 #[must_use]
699 pub fn get_default_database_path() -> PathBuf {
700 let home = std::env::var("HOME").unwrap_or_else(|_| "~".to_string());
701 PathBuf::from(format!(
702 "{home}/Library/Group Containers/JLMPQHK86H.com.culturedcode.ThingsMac/ThingsData-0Z0Z2/Things Database.thingsdatabase/main.sqlite"
703 ))
704 }
705}
706
707impl Default for McpServerConfig {
708 #[allow(clippy::too_many_lines)]
709 fn default() -> Self {
710 Self {
711 server: ServerConfig {
712 name: "things3-mcp-server".to_string(),
713 version: env!("CARGO_PKG_VERSION").to_string(),
714 description: "Things 3 MCP Server".to_string(),
715 max_connections: 100,
716 connection_timeout: 30,
717 request_timeout: 60,
718 graceful_shutdown: true,
719 shutdown_timeout: 30,
720 },
721 database: DatabaseConfig {
722 path: Self::get_default_database_path(),
723 fallback_to_default: true,
724 pool_size: 10,
725 connection_timeout: 30,
726 query_timeout: 60,
727 enable_query_logging: false,
728 enable_query_metrics: true,
729 },
730 logging: LoggingConfig {
731 level: "info".to_string(),
732 json_logs: false,
733 log_file: None,
734 console_logs: true,
735 structured_logs: true,
736 rotation: LogRotationConfig {
737 enabled: true,
738 max_file_size_mb: 100,
739 max_files: 5,
740 compress: true,
741 },
742 },
743 performance: PerformanceConfig {
744 enabled: true,
745 slow_request_threshold_ms: 1000,
746 enable_profiling: false,
747 memory_monitoring: MemoryMonitoringConfig {
748 enabled: true,
749 threshold_percentage: 80.0,
750 check_interval: 60,
751 },
752 cpu_monitoring: CpuMonitoringConfig {
753 enabled: true,
754 threshold_percentage: 80.0,
755 check_interval: 60,
756 },
757 },
758 security: SecurityConfig {
759 authentication: AuthenticationConfig {
760 enabled: false,
761 require_auth: false,
762 jwt_secret: "your-secret-key-change-this-in-production".to_string(),
763 jwt_expiration: 3600,
764 api_keys: vec![],
765 oauth: None,
766 },
767 rate_limiting: RateLimitingConfig {
768 enabled: true,
769 requests_per_minute: 60,
770 burst_limit: 10,
771 custom_limits: None,
772 },
773 cors: CorsConfig {
774 enabled: true,
775 allowed_origins: vec!["*".to_string()],
776 allowed_methods: vec![
777 "GET".to_string(),
778 "POST".to_string(),
779 "PUT".to_string(),
780 "DELETE".to_string(),
781 ],
782 allowed_headers: vec!["*".to_string()],
783 exposed_headers: vec![],
784 allow_credentials: false,
785 max_age: 86400,
786 },
787 validation: ValidationConfig {
788 enabled: true,
789 strict_mode: false,
790 max_request_size: 1024 * 1024, max_field_length: 1000,
792 },
793 },
794 cache: CacheConfig {
795 enabled: true,
796 cache_type: "memory".to_string(),
797 max_size_mb: 100,
798 ttl_seconds: 3600,
799 compression: true,
800 eviction_policy: "lru".to_string(),
801 },
802 monitoring: MonitoringConfig {
803 enabled: true,
804 metrics_port: 9090,
805 health_port: 8080,
806 health_checks: true,
807 metrics_collection: true,
808 metrics_path: "/metrics".to_string(),
809 health_path: "/health".to_string(),
810 },
811 features: FeatureFlags {
812 real_time_updates: true,
813 websocket_server: true,
814 dashboard: true,
815 bulk_operations: true,
816 data_export: true,
817 backup: true,
818 hot_reloading: false,
819 },
820 }
821 }
822}
823
824fn parse_bool(value: &str) -> bool {
826 let lower = value.to_lowercase();
827 matches!(lower.as_str(), "true" | "1" | "yes" | "on")
828}
829
830#[cfg(test)]
831mod tests {
832 use super::*;
833 use std::sync::Mutex;
834 use tempfile::NamedTempFile;
835
836 static ENV_MUTEX: Mutex<()> = Mutex::new(());
838
839 #[test]
840 fn test_default_config() {
841 let config = McpServerConfig::default();
842 assert_eq!(config.server.name, "things3-mcp-server");
843 assert!(config.database.fallback_to_default);
844 assert_eq!(config.logging.level, "info");
845 assert!(config.performance.enabled);
846 assert!(!config.security.authentication.enabled);
847 assert!(config.cache.enabled);
848 assert!(config.monitoring.enabled);
849 }
850
851 #[test]
852 fn test_config_validation() {
853 let config = McpServerConfig::default();
854 assert!(config.validate().is_ok());
855 }
856
857 #[test]
858 fn test_config_validation_invalid_server_name() {
859 let mut config = McpServerConfig::default();
860 config.server.name = String::new();
861 assert!(config.validate().is_err());
862 }
863
864 #[test]
865 fn test_config_validation_invalid_log_level() {
866 let mut config = McpServerConfig::default();
867 config.logging.level = "invalid".to_string();
868 assert!(config.validate().is_err());
869 }
870
871 #[test]
872 fn test_config_from_env() {
873 let _lock = ENV_MUTEX.lock().unwrap();
874
875 cleanup_env_vars();
877
878 std::env::set_var("MCP_SERVER_NAME", "test-server");
879 std::env::set_var("MCP_LOG_LEVEL", "debug");
880 std::env::set_var("MCP_CACHE_ENABLED", "false");
881
882 let config = McpServerConfig::from_env().unwrap();
883 assert_eq!(config.server.name, "test-server");
884 assert_eq!(config.logging.level, "debug");
885 assert!(!config.cache.enabled);
886
887 cleanup_env_vars();
889 }
890
891 #[test]
892 fn test_config_to_and_from_file_json() {
893 let config = McpServerConfig::default();
894 let temp_file = NamedTempFile::new().unwrap();
895 let path = temp_file.path().with_extension("json");
896
897 config.to_file(&path, "json").unwrap();
898 let loaded_config = McpServerConfig::from_file(&path).unwrap();
899
900 assert_eq!(config.server.name, loaded_config.server.name);
901 assert_eq!(config.logging.level, loaded_config.logging.level);
902 }
903
904 #[test]
905 fn test_config_merge() {
906 let mut config1 = McpServerConfig::default();
907 let mut config2 = McpServerConfig::default();
908 config2.server.name = "merged-server".to_string();
909 config2.logging.level = "debug".to_string();
910 config2.cache.enabled = false;
911
912 config1.merge_with(&config2);
913 assert_eq!(config1.server.name, "merged-server");
914 assert_eq!(config1.logging.level, "debug");
915 assert!(!config1.cache.enabled);
916 }
917
918 #[test]
919 fn test_parse_bool() {
920 assert!(parse_bool("true"));
921 assert!(parse_bool("TRUE"));
922 assert!(parse_bool("1"));
923 assert!(parse_bool("yes"));
924 assert!(parse_bool("on"));
925 assert!(!parse_bool("false"));
926 assert!(!parse_bool("0"));
927 assert!(!parse_bool("no"));
928 assert!(!parse_bool("off"));
929 assert!(!parse_bool("invalid"));
930 }
931
932 fn cleanup_env_vars() {
933 let env_vars = [
934 "MCP_SERVER_NAME",
935 "MCP_SERVER_VERSION",
936 "MCP_SERVER_DESCRIPTION",
937 "MCP_MAX_CONNECTIONS",
938 "MCP_CONNECTION_TIMEOUT",
939 "MCP_REQUEST_TIMEOUT",
940 "MCP_DATABASE_PATH",
941 "MCP_DATABASE_FALLBACK",
942 "MCP_DATABASE_POOL_SIZE",
943 "MCP_LOG_LEVEL",
944 "MCP_LOG_FILE",
945 "MCP_PERFORMANCE_ENABLED",
946 "MCP_SLOW_REQUEST_THRESHOLD",
947 "MCP_AUTH_ENABLED",
948 "MCP_JWT_SECRET",
949 "MCP_RATE_LIMIT_ENABLED",
950 "MCP_REQUESTS_PER_MINUTE",
951 "MCP_CACHE_ENABLED",
952 "MCP_CACHE_MAX_SIZE_MB",
953 "MCP_MONITORING_ENABLED",
954 "MCP_METRICS_PORT",
955 "MCP_HEALTH_PORT",
956 "MCP_REAL_TIME_UPDATES",
957 "MCP_WEBSOCKET_SERVER",
958 "MCP_DASHBOARD",
959 "MCP_BULK_OPERATIONS",
960 "MCP_DATA_EXPORT",
961 "MCP_BACKUP",
962 "MCP_HOT_RELOADING",
963 ];
964
965 for var in env_vars {
966 std::env::remove_var(var);
967 }
968 }
969
970 fn set_test_env_vars() {
971 std::env::set_var("MCP_SERVER_NAME", "test-server");
972 std::env::set_var("MCP_SERVER_VERSION", "1.2.3");
973 std::env::set_var("MCP_SERVER_DESCRIPTION", "Test Description");
974 std::env::set_var("MCP_MAX_CONNECTIONS", "150");
975 std::env::set_var("MCP_CONNECTION_TIMEOUT", "30");
976 std::env::set_var("MCP_REQUEST_TIMEOUT", "60");
977 std::env::set_var("MCP_DATABASE_PATH", "/test/db.sqlite");
978 std::env::set_var("MCP_DATABASE_FALLBACK", "false");
979 std::env::set_var("MCP_DATABASE_POOL_SIZE", "20");
980 std::env::set_var("MCP_LOG_LEVEL", "debug");
981 std::env::set_var("MCP_LOG_FILE", "/test/log.txt");
982 std::env::set_var("MCP_PERFORMANCE_ENABLED", "false");
983 std::env::set_var("MCP_SLOW_REQUEST_THRESHOLD", "5000");
984 std::env::set_var("MCP_AUTH_ENABLED", "true");
985 std::env::set_var("MCP_JWT_SECRET", "test-secret");
986 std::env::set_var("MCP_RATE_LIMIT_ENABLED", "true");
987 std::env::set_var("MCP_REQUESTS_PER_MINUTE", "120");
988 std::env::set_var("MCP_CACHE_ENABLED", "false");
989 std::env::set_var("MCP_CACHE_MAX_SIZE_MB", "500");
990 std::env::set_var("MCP_MONITORING_ENABLED", "false");
991 std::env::set_var("MCP_METRICS_PORT", "9090");
992 std::env::set_var("MCP_HEALTH_PORT", "8080");
993 std::env::set_var("MCP_REAL_TIME_UPDATES", "true");
994 std::env::set_var("MCP_WEBSOCKET_SERVER", "true");
995 std::env::set_var("MCP_DASHBOARD", "true");
996 std::env::set_var("MCP_BULK_OPERATIONS", "true");
997 std::env::set_var("MCP_DATA_EXPORT", "true");
998 std::env::set_var("MCP_BACKUP", "true");
999 std::env::set_var("MCP_HOT_RELOADING", "true");
1000 }
1001
1002 fn assert_config_values(config: &McpServerConfig) {
1003 assert_eq!(config.server.name, "test-server");
1004 assert_eq!(config.server.version, "1.2.3");
1005 assert_eq!(config.server.description, "Test Description");
1006 assert_eq!(config.server.max_connections, 150);
1007 assert_eq!(config.server.connection_timeout, 30);
1008 assert_eq!(config.server.request_timeout, 60);
1009 assert_eq!(config.database.path, PathBuf::from("/test/db.sqlite"));
1010 assert!(!config.database.fallback_to_default);
1011 assert_eq!(config.database.pool_size, 20);
1012 assert_eq!(config.logging.level, "debug");
1013 assert_eq!(
1014 config.logging.log_file,
1015 Some(PathBuf::from("/test/log.txt"))
1016 );
1017 assert!(!config.performance.enabled);
1018 assert_eq!(config.performance.slow_request_threshold_ms, 5000);
1019 assert!(config.security.authentication.enabled);
1020 assert_eq!(config.security.authentication.jwt_secret, "test-secret");
1021 assert!(config.security.rate_limiting.enabled);
1022 assert_eq!(config.security.rate_limiting.requests_per_minute, 120);
1023 assert!(!config.cache.enabled);
1024 assert_eq!(config.cache.max_size_mb, 500);
1025 assert!(!config.monitoring.enabled);
1026 assert_eq!(config.monitoring.metrics_port, 9090);
1027 assert_eq!(config.monitoring.health_port, 8080);
1028 assert!(config.features.real_time_updates);
1029 assert!(config.features.websocket_server);
1030 assert!(config.features.dashboard);
1031 assert!(config.features.bulk_operations);
1032 assert!(config.features.data_export);
1033 assert!(config.features.backup);
1034 assert!(config.features.hot_reloading);
1035 }
1036
1037 #[test]
1038 fn test_config_from_env_comprehensive() {
1039 let _lock = ENV_MUTEX.lock().unwrap();
1040
1041 cleanup_env_vars();
1043
1044 set_test_env_vars();
1046
1047 let config = McpServerConfig::from_env().unwrap();
1048 assert_config_values(&config);
1049
1050 cleanup_env_vars();
1052 }
1053
1054 #[test]
1055 fn test_config_from_env_invalid_values() {
1056 let _lock = ENV_MUTEX.lock().unwrap();
1057
1058 cleanup_env_vars();
1060
1061 std::env::set_var("MCP_MAX_CONNECTIONS", "invalid");
1063 let result = McpServerConfig::from_env();
1064 assert!(result.is_err());
1065 std::env::remove_var("MCP_MAX_CONNECTIONS");
1066
1067 std::env::set_var("MCP_CONNECTION_TIMEOUT", "not-a-number");
1068 let result = McpServerConfig::from_env();
1069 assert!(result.is_err());
1070 std::env::remove_var("MCP_CONNECTION_TIMEOUT");
1071
1072 std::env::set_var("MCP_REQUEST_TIMEOUT", "abc");
1073 let result = McpServerConfig::from_env();
1074 assert!(result.is_err());
1075 std::env::remove_var("MCP_REQUEST_TIMEOUT");
1076
1077 cleanup_env_vars();
1079 }
1080
1081 #[test]
1082 fn test_config_from_file_nonexistent() {
1083 let result = McpServerConfig::from_file("/nonexistent/file.json");
1084 assert!(result.is_err());
1085 }
1086
1087 #[test]
1088 fn test_config_from_file_invalid_yaml() {
1089 let temp_file = NamedTempFile::new().unwrap();
1090 let config_path = temp_file.path().with_extension("yaml");
1091
1092 std::fs::write(&config_path, "invalid: yaml: content: [").unwrap();
1093
1094 let result = McpServerConfig::from_file(&config_path);
1095 assert!(result.is_err());
1096 }
1097
1098 #[test]
1099 fn test_config_to_file_invalid_format() {
1100 let config = McpServerConfig::default();
1101 let temp_file = NamedTempFile::new().unwrap();
1102 let config_path = temp_file.path().with_extension("txt");
1103
1104 let result = config.to_file(&config_path, "invalid");
1105 assert!(result.is_err());
1106 }
1107
1108 #[test]
1109 fn test_config_merge_comprehensive() {
1110 let mut config1 = McpServerConfig::default();
1111 config1.server.name = "server1".to_string();
1112 config1.server.max_connections = 100;
1113 config1.cache.enabled = true;
1114 config1.cache.max_size_mb = 200;
1115 config1.performance.enabled = false;
1116 config1.security.authentication.enabled = true;
1117 config1.security.authentication.jwt_secret = "secret1".to_string();
1118
1119 let mut config2 = McpServerConfig::default();
1120 config2.server.name = "server2".to_string();
1121 config2.server.max_connections = 0; config2.cache.enabled = false;
1123 config2.cache.max_size_mb = 0; config2.performance.enabled = true;
1125 config2.security.authentication.enabled = false;
1126 config2.security.authentication.jwt_secret = "secret2".to_string();
1127
1128 config1.merge_with(&config2);
1129
1130 assert_eq!(config1.server.name, "server2");
1131 assert_eq!(config1.server.max_connections, 100); assert!(!config1.cache.enabled); assert_eq!(config1.cache.max_size_mb, 200); assert!(config1.performance.enabled); assert!(!config1.security.authentication.enabled); assert_eq!(config1.security.authentication.jwt_secret, "secret2");
1137 }
1138
1139 #[test]
1140 fn test_config_validation_comprehensive() {
1141 let mut config = McpServerConfig::default();
1142
1143 config.server.name = String::new();
1145 assert!(config.validate().is_err());
1146 config.server.name = "test".to_string();
1147
1148 config.server.version = String::new();
1150 assert!(config.validate().is_err());
1151 config.server.version = "1.0.0".to_string();
1152
1153 config.server.max_connections = 0;
1155 assert!(config.validate().is_err());
1156 config.server.max_connections = 100;
1157
1158 config.database.pool_size = 0;
1160 assert!(config.validate().is_err());
1161 config.database.pool_size = 10;
1162
1163 config.logging.level = "invalid".to_string();
1165 assert!(config.validate().is_err());
1166 config.logging.level = "info".to_string();
1167
1168 config.performance.enabled = true;
1170 config.performance.slow_request_threshold_ms = 0;
1171 assert!(config.validate().is_err());
1172 config.performance.slow_request_threshold_ms = 1000;
1173
1174 config.security.authentication.enabled = true;
1176 config.security.authentication.jwt_secret = String::new();
1177 assert!(config.validate().is_err());
1178 config.security.authentication.jwt_secret = "secret".to_string();
1179
1180 config.cache.enabled = true;
1182 config.cache.max_size_mb = 0;
1183 assert!(config.validate().is_err());
1184 config.cache.max_size_mb = 100;
1185
1186 assert!(config.validate().is_ok());
1188 }
1189
1190 #[test]
1191 fn test_effective_database_path() {
1192 let temp_file = NamedTempFile::new().unwrap();
1193 let db_path = temp_file.path();
1194 let mut config = McpServerConfig::default();
1195 config.database.path = db_path.to_path_buf();
1196 config.database.fallback_to_default = false;
1197
1198 let effective_path = config.get_effective_database_path().unwrap();
1199 assert_eq!(effective_path, db_path);
1200 }
1201
1202 #[test]
1203 fn test_effective_database_path_fallback() {
1204 let mut config = McpServerConfig::default();
1205 config.database.path = PathBuf::from("/nonexistent/path");
1206 config.database.fallback_to_default = true;
1207
1208 let _ = config.get_effective_database_path();
1210 }
1211}