Skip to main content

crates_docs/config/
mod.rs

1//! Configuration module
2
3use crate::cache::CacheConfig;
4use crate::server::auth::OAuthConfig;
5use rust_mcp_sdk::schema::{Icon, IconTheme};
6use serde::{Deserialize, Serialize};
7use std::fs;
8use std::path::Path;
9
10/// Application configuration
11#[derive(Debug, Clone, Deserialize, Serialize, Default)]
12pub struct AppConfig {
13    /// Server configuration
14    pub server: ServerConfig,
15
16    /// Cache configuration
17    pub cache: CacheConfig,
18
19    /// OAuth configuration
20    pub oauth: OAuthConfig,
21
22    /// Logging configuration
23    pub logging: LoggingConfig,
24
25    /// Performance configuration
26    pub performance: PerformanceConfig,
27}
28
29/// Server configuration
30#[derive(Debug, Clone, Deserialize, Serialize)]
31pub struct ServerConfig {
32    /// Server name
33    pub name: String,
34
35    /// Server version
36    pub version: String,
37
38    /// Server description
39    pub description: Option<String>,
40
41    /// Server icons
42    #[serde(default = "default_icons")]
43    pub icons: Vec<Icon>,
44
45    /// Website URL
46    pub website_url: Option<String>,
47
48    /// Host address
49    pub host: String,
50
51    /// Port
52    pub port: u16,
53
54    /// Transport mode
55    pub transport_mode: String,
56
57    /// Enable SSE support
58    pub enable_sse: bool,
59
60    /// Enable OAuth authentication
61    pub enable_oauth: bool,
62
63    /// Maximum concurrent connections
64    pub max_connections: usize,
65
66    /// Request timeout (seconds)
67    pub request_timeout_secs: u64,
68
69    /// Response timeout (seconds)
70    pub response_timeout_secs: u64,
71
72    /// Allowed hosts for CORS (e.g., `["localhost", "127.0.0.1"]`)
73    pub allowed_hosts: Vec<String>,
74
75    /// Allowed origins for CORS (e.g., `["http://localhost:*"]`)
76    /// Use `"*"` only in development, specify exact origins in production
77    pub allowed_origins: Vec<String>,
78}
79
80/// Default icons for the server
81fn default_icons() -> Vec<Icon> {
82    vec![
83        Icon {
84            src: "https://docs.rs/static/favicon-32x32.png".to_string(),
85            mime_type: Some("image/png".to_string()),
86            sizes: vec!["32x32".to_string()],
87            theme: Some(IconTheme::Light),
88        },
89        Icon {
90            src: "https://docs.rs/static/favicon-32x32.png".to_string(),
91            mime_type: Some("image/png".to_string()),
92            sizes: vec!["32x32".to_string()],
93            theme: Some(IconTheme::Dark),
94        },
95    ]
96}
97
98/// Logging configuration
99#[derive(Debug, Clone, Deserialize, Serialize)]
100pub struct LoggingConfig {
101    /// Log level
102    pub level: String,
103
104    /// Log file path
105    pub file_path: Option<String>,
106
107    /// Whether to enable console logging
108    pub enable_console: bool,
109
110    /// Whether to enable file logging
111    pub enable_file: bool,
112
113    /// Maximum log file size (MB)
114    pub max_file_size_mb: u64,
115
116    /// Number of log files to retain
117    pub max_files: usize,
118}
119
120/// Performance configuration
121#[derive(Debug, Clone, Deserialize, Serialize)]
122pub struct PerformanceConfig {
123    /// HTTP client connection pool size
124    pub http_client_pool_size: usize,
125
126    /// HTTP client pool idle timeout (seconds)
127    pub http_client_pool_idle_timeout_secs: u64,
128
129    /// HTTP client connection timeout (seconds)
130    pub http_client_connect_timeout_secs: u64,
131
132    /// HTTP client request timeout (seconds)
133    pub http_client_timeout_secs: u64,
134
135    /// HTTP client read timeout (seconds)
136    pub http_client_read_timeout_secs: u64,
137
138    /// HTTP client max retry attempts
139    pub http_client_max_retries: u32,
140
141    /// HTTP client retry initial delay (milliseconds)
142    pub http_client_retry_initial_delay_ms: u64,
143
144    /// HTTP client retry max delay (milliseconds)
145    pub http_client_retry_max_delay_ms: u64,
146
147    /// Maximum cache size (number of entries)
148    pub cache_max_size: usize,
149
150    /// Default cache TTL (seconds)
151    pub cache_default_ttl_secs: u64,
152
153    /// Request rate limit (requests per second)
154    pub rate_limit_per_second: u32,
155
156    /// Concurrent request limit
157    pub concurrent_request_limit: usize,
158
159    /// Enable response compression
160    pub enable_response_compression: bool,
161
162    /// Enable Prometheus metrics
163    pub enable_metrics: bool,
164
165    /// Metrics endpoint port (0 = use server port)
166    pub metrics_port: u16,
167}
168
169impl Default for ServerConfig {
170    fn default() -> Self {
171        Self {
172            name: "crates-docs".to_string(),
173            version: crate::VERSION.to_string(),
174            description: Some(
175                "High-performance Rust crate documentation query MCP server".to_string(),
176            ),
177            icons: default_icons(),
178            website_url: Some("https://github.com/KingingWang/crates-docs".to_string()),
179            host: "127.0.0.1".to_string(),
180            port: 8080,
181            transport_mode: "hybrid".to_string(),
182            enable_sse: true,
183            enable_oauth: false,
184            max_connections: 100,
185            request_timeout_secs: 30,
186            response_timeout_secs: 60,
187            // Secure defaults: only allow localhost by default
188            allowed_hosts: vec!["localhost".to_string(), "127.0.0.1".to_string()],
189            allowed_origins: vec!["http://localhost:*".to_string()],
190        }
191    }
192}
193
194impl Default for LoggingConfig {
195    fn default() -> Self {
196        Self {
197            level: "info".to_string(),
198            file_path: Some("./logs/crates-docs.log".to_string()),
199            enable_console: true,
200            enable_file: false, // 默认仅输出到控制台
201            max_file_size_mb: 100,
202            max_files: 10,
203        }
204    }
205}
206
207impl Default for PerformanceConfig {
208    fn default() -> Self {
209        Self {
210            http_client_pool_size: 10,
211            http_client_pool_idle_timeout_secs: 90,
212            http_client_connect_timeout_secs: 10,
213            http_client_timeout_secs: 30,
214            http_client_read_timeout_secs: 30,
215            http_client_max_retries: 3,
216            http_client_retry_initial_delay_ms: 100,
217            http_client_retry_max_delay_ms: 10000,
218            cache_max_size: 1000,
219            cache_default_ttl_secs: 3600,
220            rate_limit_per_second: 100,
221            concurrent_request_limit: 50,
222            enable_response_compression: true,
223            enable_metrics: true,
224            metrics_port: 0,
225        }
226    }
227}
228
229impl AppConfig {
230    /// Load configuration from file
231    ///
232    /// # Errors
233    ///
234    /// Returns an error if file does not exist, cannot be read, or format is invalid
235    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, crate::error::Error> {
236        let content = fs::read_to_string(path)
237            .map_err(|e| crate::error::Error::Config(format!("Failed to read config file: {e}")))?;
238
239        let config: Self = toml::from_str(&content).map_err(|e| {
240            crate::error::Error::Config(format!("Failed to parse config file: {e}"))
241        })?;
242
243        config.validate()?;
244        Ok(config)
245    }
246
247    /// Save configuration to file
248    ///
249    /// # Errors
250    ///
251    /// Returns an error if configuration cannot be serialized, directory cannot be created, or file cannot be written
252    pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), crate::error::Error> {
253        let content = toml::to_string_pretty(self).map_err(|e| {
254            crate::error::Error::Config(format!("Failed to serialize configuration: {e}"))
255        })?;
256
257        // Ensure directory exists
258        if let Some(parent) = path.as_ref().parent() {
259            fs::create_dir_all(parent).map_err(|e| {
260                crate::error::Error::Config(format!("Failed to create directory: {e}"))
261            })?;
262        }
263
264        fs::write(path, content).map_err(|e| {
265            crate::error::Error::Config(format!("Failed to write config file: {e}"))
266        })?;
267
268        Ok(())
269    }
270
271    /// Validate configuration
272    ///
273    /// # Errors
274    ///
275    /// Returns an error if configuration is invalid (e.g., empty hostname, invalid port, etc.)
276    pub fn validate(&self) -> Result<(), crate::error::Error> {
277        // Validate server configuration
278        if self.server.host.is_empty() {
279            return Err(crate::error::Error::Config(
280                "Server host cannot be empty".to_string(),
281            ));
282        }
283
284        if self.server.port == 0 {
285            return Err(crate::error::Error::Config(
286                "Server port cannot be 0".to_string(),
287            ));
288        }
289
290        if self.server.max_connections == 0 {
291            return Err(crate::error::Error::Config(
292                "Maximum connections cannot be 0".to_string(),
293            ));
294        }
295
296        // Validate transport mode
297        let valid_modes = ["stdio", "http", "sse", "hybrid"];
298        if !valid_modes.contains(&self.server.transport_mode.as_str()) {
299            return Err(crate::error::Error::Config(format!(
300                "Invalid transport mode: {}, valid values: {:?}",
301                self.server.transport_mode, valid_modes
302            )));
303        }
304
305        // Validate log level
306        let valid_levels = ["trace", "debug", "info", "warn", "error"];
307        if !valid_levels.contains(&self.logging.level.as_str()) {
308            return Err(crate::error::Error::Config(format!(
309                "Invalid log level: {}, valid values: {:?}",
310                self.logging.level, valid_levels
311            )));
312        }
313
314        // Validate performance configuration
315        if self.performance.http_client_pool_size == 0 {
316            return Err(crate::error::Error::Config(
317                "HTTP client connection pool size cannot be 0".to_string(),
318            ));
319        }
320
321        if self.performance.http_client_pool_idle_timeout_secs == 0 {
322            return Err(crate::error::Error::Config(
323                "HTTP client pool idle timeout cannot be 0".to_string(),
324            ));
325        }
326
327        if self.performance.http_client_connect_timeout_secs == 0 {
328            return Err(crate::error::Error::Config(
329                "HTTP client connection timeout cannot be 0".to_string(),
330            ));
331        }
332
333        if self.performance.http_client_timeout_secs == 0 {
334            return Err(crate::error::Error::Config(
335                "HTTP client request timeout cannot be 0".to_string(),
336            ));
337        }
338
339        if self.performance.cache_max_size == 0 {
340            return Err(crate::error::Error::Config(
341                "Maximum cache size cannot be 0".to_string(),
342            ));
343        }
344
345        // Validate OAuth configuration
346        if self.server.enable_oauth {
347            self.oauth.validate()?;
348        }
349
350        Ok(())
351    }
352
353    /// Load configuration from environment variables
354    ///
355    /// # Errors
356    ///
357    /// Returns an error if environment variable format is invalid or configuration validation fails
358    pub fn from_env() -> Result<Self, crate::error::Error> {
359        let mut config = Self::default();
360
361        // Override configuration from environment variables
362        if let Ok(name) = std::env::var("CRATES_DOCS_NAME") {
363            config.server.name = name;
364        }
365
366        if let Ok(host) = std::env::var("CRATES_DOCS_HOST") {
367            config.server.host = host;
368        }
369
370        if let Ok(port) = std::env::var("CRATES_DOCS_PORT") {
371            config.server.port = port
372                .parse()
373                .map_err(|e| crate::error::Error::Config(format!("Invalid port: {e}")))?;
374        }
375
376        if let Ok(mode) = std::env::var("CRATES_DOCS_TRANSPORT_MODE") {
377            config.server.transport_mode = mode;
378        }
379
380        if let Ok(level) = std::env::var("CRATES_DOCS_LOG_LEVEL") {
381            config.logging.level = level;
382        }
383
384        if let Ok(enable_console) = std::env::var("CRATES_DOCS_ENABLE_CONSOLE") {
385            config.logging.enable_console = enable_console.parse().unwrap_or(true);
386        }
387
388        if let Ok(enable_file) = std::env::var("CRATES_DOCS_ENABLE_FILE") {
389            config.logging.enable_file = enable_file.parse().unwrap_or(true);
390        }
391
392        config.validate()?;
393        Ok(config)
394    }
395
396    /// Merge configuration (environment variables take precedence over file configuration)
397    #[must_use]
398    pub fn merge(file_config: Option<Self>, env_config: Option<Self>) -> Self {
399        let mut config = Self::default();
400
401        // First apply file configuration
402        if let Some(file) = file_config {
403            config = file;
404        }
405
406        // Then apply environment variable configuration (overrides file configuration)
407        if let Some(env) = env_config {
408            // Merge server configuration
409            if env.server.name != "crates-docs" {
410                config.server.name = env.server.name;
411            }
412            if env.server.host != "127.0.0.1" {
413                config.server.host = env.server.host;
414            }
415            if env.server.port != 8080 {
416                config.server.port = env.server.port;
417            }
418            if env.server.transport_mode != "hybrid" {
419                config.server.transport_mode = env.server.transport_mode;
420            }
421
422            // Merge logging configuration
423            if env.logging.level != "info" {
424                config.logging.level = env.logging.level;
425            }
426        }
427
428        config
429    }
430}