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    /// Maximum cache size (number of entries)
127    pub cache_max_size: usize,
128
129    /// Default cache TTL (seconds)
130    pub cache_default_ttl_secs: u64,
131
132    /// Request rate limit (requests per second)
133    pub rate_limit_per_second: u32,
134
135    /// Concurrent request limit
136    pub concurrent_request_limit: usize,
137
138    /// Enable response compression
139    pub enable_response_compression: bool,
140}
141
142impl Default for ServerConfig {
143    fn default() -> Self {
144        Self {
145            name: "crates-docs".to_string(),
146            version: crate::VERSION.to_string(),
147            description: Some(
148                "High-performance Rust crate documentation query MCP server".to_string(),
149            ),
150            icons: default_icons(),
151            website_url: Some("https://github.com/KingingWang/crates-docs".to_string()),
152            host: "127.0.0.1".to_string(),
153            port: 8080,
154            transport_mode: "hybrid".to_string(),
155            enable_sse: true,
156            enable_oauth: false,
157            max_connections: 100,
158            request_timeout_secs: 30,
159            response_timeout_secs: 60,
160            // Secure defaults: only allow localhost by default
161            allowed_hosts: vec!["localhost".to_string(), "127.0.0.1".to_string()],
162            allowed_origins: vec!["http://localhost:*".to_string()],
163        }
164    }
165}
166
167impl Default for LoggingConfig {
168    fn default() -> Self {
169        Self {
170            level: "info".to_string(),
171            file_path: Some("./logs/crates-docs.log".to_string()),
172            enable_console: true,
173            enable_file: false, // 默认仅输出到控制台
174            max_file_size_mb: 100,
175            max_files: 10,
176        }
177    }
178}
179
180impl Default for PerformanceConfig {
181    fn default() -> Self {
182        Self {
183            http_client_pool_size: 10,
184            cache_max_size: 1000,
185            cache_default_ttl_secs: 3600,
186            rate_limit_per_second: 100,
187            concurrent_request_limit: 50,
188            enable_response_compression: true,
189        }
190    }
191}
192
193impl AppConfig {
194    /// Load configuration from file
195    ///
196    /// # Errors
197    ///
198    /// Returns an error if file does not exist, cannot be read, or format is invalid
199    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, crate::error::Error> {
200        let content = fs::read_to_string(path)
201            .map_err(|e| crate::error::Error::Config(format!("Failed to read config file: {e}")))?;
202
203        let config: Self = toml::from_str(&content).map_err(|e| {
204            crate::error::Error::Config(format!("Failed to parse config file: {e}"))
205        })?;
206
207        config.validate()?;
208        Ok(config)
209    }
210
211    /// Save configuration to file
212    ///
213    /// # Errors
214    ///
215    /// Returns an error if configuration cannot be serialized, directory cannot be created, or file cannot be written
216    pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), crate::error::Error> {
217        let content = toml::to_string_pretty(self).map_err(|e| {
218            crate::error::Error::Config(format!("Failed to serialize configuration: {e}"))
219        })?;
220
221        // Ensure directory exists
222        if let Some(parent) = path.as_ref().parent() {
223            fs::create_dir_all(parent).map_err(|e| {
224                crate::error::Error::Config(format!("Failed to create directory: {e}"))
225            })?;
226        }
227
228        fs::write(path, content).map_err(|e| {
229            crate::error::Error::Config(format!("Failed to write config file: {e}"))
230        })?;
231
232        Ok(())
233    }
234
235    /// Validate configuration
236    ///
237    /// # Errors
238    ///
239    /// Returns an error if configuration is invalid (e.g., empty hostname, invalid port, etc.)
240    pub fn validate(&self) -> Result<(), crate::error::Error> {
241        // Validate server configuration
242        if self.server.host.is_empty() {
243            return Err(crate::error::Error::Config(
244                "Server host cannot be empty".to_string(),
245            ));
246        }
247
248        if self.server.port == 0 {
249            return Err(crate::error::Error::Config(
250                "Server port cannot be 0".to_string(),
251            ));
252        }
253
254        if self.server.max_connections == 0 {
255            return Err(crate::error::Error::Config(
256                "Maximum connections cannot be 0".to_string(),
257            ));
258        }
259
260        // Validate transport mode
261        let valid_modes = ["stdio", "http", "sse", "hybrid"];
262        if !valid_modes.contains(&self.server.transport_mode.as_str()) {
263            return Err(crate::error::Error::Config(format!(
264                "Invalid transport mode: {}, valid values: {:?}",
265                self.server.transport_mode, valid_modes
266            )));
267        }
268
269        // Validate log level
270        let valid_levels = ["trace", "debug", "info", "warn", "error"];
271        if !valid_levels.contains(&self.logging.level.as_str()) {
272            return Err(crate::error::Error::Config(format!(
273                "Invalid log level: {}, valid values: {:?}",
274                self.logging.level, valid_levels
275            )));
276        }
277
278        // Validate performance configuration
279        if self.performance.http_client_pool_size == 0 {
280            return Err(crate::error::Error::Config(
281                "HTTP client connection pool size cannot be 0".to_string(),
282            ));
283        }
284
285        if self.performance.cache_max_size == 0 {
286            return Err(crate::error::Error::Config(
287                "Maximum cache size cannot be 0".to_string(),
288            ));
289        }
290
291        // Validate OAuth configuration
292        if self.server.enable_oauth {
293            self.oauth.validate()?;
294        }
295
296        Ok(())
297    }
298
299    /// Load configuration from environment variables
300    ///
301    /// # Errors
302    ///
303    /// Returns an error if environment variable format is invalid or configuration validation fails
304    pub fn from_env() -> Result<Self, crate::error::Error> {
305        let mut config = Self::default();
306
307        // Override configuration from environment variables
308        if let Ok(name) = std::env::var("CRATES_DOCS_NAME") {
309            config.server.name = name;
310        }
311
312        if let Ok(host) = std::env::var("CRATES_DOCS_HOST") {
313            config.server.host = host;
314        }
315
316        if let Ok(port) = std::env::var("CRATES_DOCS_PORT") {
317            config.server.port = port
318                .parse()
319                .map_err(|e| crate::error::Error::Config(format!("Invalid port: {e}")))?;
320        }
321
322        if let Ok(mode) = std::env::var("CRATES_DOCS_TRANSPORT_MODE") {
323            config.server.transport_mode = mode;
324        }
325
326        if let Ok(level) = std::env::var("CRATES_DOCS_LOG_LEVEL") {
327            config.logging.level = level;
328        }
329
330        if let Ok(enable_console) = std::env::var("CRATES_DOCS_ENABLE_CONSOLE") {
331            config.logging.enable_console = enable_console.parse().unwrap_or(true);
332        }
333
334        if let Ok(enable_file) = std::env::var("CRATES_DOCS_ENABLE_FILE") {
335            config.logging.enable_file = enable_file.parse().unwrap_or(true);
336        }
337
338        config.validate()?;
339        Ok(config)
340    }
341
342    /// Merge configuration (environment variables take precedence over file configuration)
343    #[must_use]
344    pub fn merge(file_config: Option<Self>, env_config: Option<Self>) -> Self {
345        let mut config = Self::default();
346
347        // First apply file configuration
348        if let Some(file) = file_config {
349            config = file;
350        }
351
352        // Then apply environment variable configuration (overrides file configuration)
353        if let Some(env) = env_config {
354            // Merge server configuration
355            if env.server.name != "crates-docs" {
356                config.server.name = env.server.name;
357            }
358            if env.server.host != "127.0.0.1" {
359                config.server.host = env.server.host;
360            }
361            if env.server.port != 8080 {
362                config.server.port = env.server.port;
363            }
364            if env.server.transport_mode != "hybrid" {
365                config.server.transport_mode = env.server.transport_mode;
366            }
367
368            // Merge logging configuration
369            if env.logging.level != "info" {
370                config.logging.level = env.logging.level;
371            }
372        }
373
374        config
375    }
376}