1use crate::cache::CacheConfig;
32use crate::server::auth::{AuthConfig, OAuthConfig};
33use rust_mcp_sdk::schema::{Icon, IconTheme};
34use serde::{Deserialize, Serialize};
35use std::fs;
36use std::path::Path;
37
38#[derive(Debug, Clone, Deserialize, Serialize, Default)]
50pub struct AppConfig {
51 pub server: ServerConfig,
53
54 pub cache: CacheConfig,
56
57 #[serde(default)]
59 pub auth: AuthConfig,
60
61 #[serde(default)]
63 pub oauth: OAuthConfig,
64
65 pub logging: LoggingConfig,
67
68 pub performance: PerformanceConfig,
70}
71
72#[derive(Debug, Clone, Deserialize, Serialize)]
74pub struct ServerConfig {
75 pub name: String,
77
78 #[serde(default = "default_version")]
80 pub version: String,
81
82 pub description: Option<String>,
84
85 #[serde(default = "default_icons")]
87 pub icons: Vec<Icon>,
88
89 pub website_url: Option<String>,
91
92 pub host: String,
94
95 pub port: u16,
97
98 pub transport_mode: String,
100
101 pub enable_sse: bool,
103
104 pub enable_oauth: bool,
106
107 pub max_connections: usize,
109
110 pub request_timeout_secs: u64,
112
113 pub response_timeout_secs: u64,
115
116 pub allowed_hosts: Vec<String>,
118
119 pub allowed_origins: Vec<String>,
122}
123
124fn default_version() -> String {
126 crate::VERSION.to_string()
127}
128
129fn default_icons() -> Vec<Icon> {
131 vec![
132 Icon {
133 src: "https://docs.rs/static/favicon-32x32.png".to_string(),
134 mime_type: Some("image/png".to_string()),
135 sizes: vec!["32x32".to_string()],
136 theme: Some(IconTheme::Light),
137 },
138 Icon {
139 src: "https://docs.rs/static/favicon-32x32.png".to_string(),
140 mime_type: Some("image/png".to_string()),
141 sizes: vec!["32x32".to_string()],
142 theme: Some(IconTheme::Dark),
143 },
144 ]
145}
146
147#[derive(Debug, Clone, Deserialize, Serialize)]
149pub struct LoggingConfig {
150 pub level: String,
152
153 pub file_path: Option<String>,
155
156 pub enable_console: bool,
158
159 pub enable_file: bool,
161
162 pub max_file_size_mb: u64,
164
165 pub max_files: usize,
167}
168
169#[derive(Debug, Clone, Deserialize, Serialize)]
171pub struct PerformanceConfig {
172 pub http_client_pool_size: usize,
174
175 pub http_client_pool_idle_timeout_secs: u64,
177
178 pub http_client_connect_timeout_secs: u64,
180
181 pub http_client_timeout_secs: u64,
183
184 pub http_client_read_timeout_secs: u64,
186
187 pub http_client_max_retries: u32,
189
190 pub http_client_retry_initial_delay_ms: u64,
192
193 pub http_client_retry_max_delay_ms: u64,
195
196 pub cache_max_size: usize,
198
199 pub cache_default_ttl_secs: u64,
201
202 pub rate_limit_per_second: u32,
204
205 pub concurrent_request_limit: usize,
207
208 pub enable_response_compression: bool,
210
211 pub enable_metrics: bool,
213
214 pub metrics_port: u16,
216}
217
218impl Default for ServerConfig {
219 fn default() -> Self {
220 Self {
221 name: "crates-docs".to_string(),
222 version: crate::VERSION.to_string(),
223 description: Some(
224 "High-performance Rust crate documentation query MCP server".to_string(),
225 ),
226 icons: default_icons(),
227 website_url: Some("https://github.com/KingingWang/crates-docs".to_string()),
228 host: "127.0.0.1".to_string(),
229 port: 8080,
230 transport_mode: "hybrid".to_string(),
231 enable_sse: true,
232 enable_oauth: false,
233 max_connections: 100,
234 request_timeout_secs: 30,
235 response_timeout_secs: 60,
236 allowed_hosts: vec!["localhost".to_string(), "127.0.0.1".to_string()],
238 allowed_origins: vec!["http://localhost:*".to_string()],
239 }
240 }
241}
242
243impl Default for LoggingConfig {
244 fn default() -> Self {
245 Self {
246 level: "info".to_string(),
247 file_path: Some("./logs/crates-docs.log".to_string()),
248 enable_console: true,
249 enable_file: false, max_file_size_mb: 100,
251 max_files: 10,
252 }
253 }
254}
255
256impl Default for PerformanceConfig {
257 fn default() -> Self {
258 Self {
259 http_client_pool_size: 10,
260 http_client_pool_idle_timeout_secs: 90,
261 http_client_connect_timeout_secs: 10,
262 http_client_timeout_secs: 30,
263 http_client_read_timeout_secs: 30,
264 http_client_max_retries: 3,
265 http_client_retry_initial_delay_ms: 100,
266 http_client_retry_max_delay_ms: 10000,
267 cache_max_size: 1000,
268 cache_default_ttl_secs: 3600,
269 rate_limit_per_second: 100,
270 concurrent_request_limit: 50,
271 enable_response_compression: true,
272 enable_metrics: true,
273 metrics_port: 0,
274 }
275 }
276}
277
278impl AppConfig {
279 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, crate::error::Error> {
285 let content = fs::read_to_string(path).map_err(|e| {
286 crate::error::Error::config("file", format!("Failed to read config file: {e}"))
287 })?;
288
289 let config: Self = toml::from_str(&content).map_err(|e| {
290 crate::error::Error::parse("config", None, format!("Failed to parse config file: {e}"))
291 })?;
292
293 config.validate()?;
294 Ok(config)
295 }
296
297 pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), crate::error::Error> {
303 let content = toml::to_string_pretty(self).map_err(|e| {
304 crate::error::Error::config(
305 "serialization",
306 format!("Failed to serialize configuration: {e}"),
307 )
308 })?;
309
310 if let Some(parent) = path.as_ref().parent() {
312 fs::create_dir_all(parent).map_err(|e| {
313 crate::error::Error::config("directory", format!("Failed to create directory: {e}"))
314 })?;
315 }
316
317 fs::write(path, content).map_err(|e| {
318 crate::error::Error::config("file", format!("Failed to write config file: {e}"))
319 })?;
320
321 Ok(())
322 }
323
324 pub fn validate(&self) -> Result<(), crate::error::Error> {
330 if self.server.host.is_empty() {
332 return Err(crate::error::Error::config("host", "cannot be empty"));
333 }
334
335 if self.server.port == 0 {
336 return Err(crate::error::Error::config("port", "cannot be 0"));
337 }
338
339 if self.server.max_connections == 0 {
340 return Err(crate::error::Error::config(
341 "max_connections",
342 "cannot be 0",
343 ));
344 }
345
346 let valid_modes = ["stdio", "http", "sse", "hybrid"];
348 if !valid_modes.contains(&self.server.transport_mode.as_str()) {
349 return Err(crate::error::Error::config(
350 "transport_mode",
351 format!(
352 "Invalid transport mode: {}, valid values: {:?}",
353 self.server.transport_mode, valid_modes
354 ),
355 ));
356 }
357
358 let valid_levels = ["trace", "debug", "info", "warn", "error"];
360
361 if !valid_levels.contains(&self.logging.level.as_str()) {
362 return Err(crate::error::Error::config(
363 "log_level",
364 format!(
365 "Invalid log level: {}, valid values: {:?}",
366 self.logging.level, valid_levels
367 ),
368 ));
369 }
370
371 if self.performance.http_client_pool_size == 0 {
373 return Err(crate::error::Error::config(
374 "http_client_pool_size",
375 "cannot be 0",
376 ));
377 }
378
379 if self.performance.http_client_pool_idle_timeout_secs == 0 {
380 return Err(crate::error::Error::config(
381 "http_client_pool_idle_timeout_secs",
382 "cannot be 0",
383 ));
384 }
385
386 if self.performance.http_client_connect_timeout_secs == 0 {
387 return Err(crate::error::Error::config(
388 "http_client_connect_timeout_secs",
389 "cannot be 0",
390 ));
391 }
392
393 if self.performance.http_client_timeout_secs == 0 {
394 return Err(crate::error::Error::config(
395 "http_client_timeout_secs",
396 "cannot be 0",
397 ));
398 }
399
400 if self.performance.cache_max_size == 0 {
401 return Err(crate::error::Error::config("cache_max_size", "cannot be 0"));
402 }
403
404 if self.server.enable_oauth {
406 self.oauth.validate()?;
407 }
408
409 Ok(())
410 }
411
412 pub fn from_env() -> Result<Self, crate::error::Error> {
418 let mut config = Self::default();
419
420 if let Ok(name) = std::env::var("CRATES_DOCS_NAME") {
422 config.server.name = name;
423 }
424
425 if let Ok(host) = std::env::var("CRATES_DOCS_HOST") {
426 config.server.host = host;
427 }
428
429 if let Ok(port) = std::env::var("CRATES_DOCS_PORT") {
430 config.server.port = port
431 .parse()
432 .map_err(|e| crate::error::Error::config("port", format!("Invalid port: {e}")))?;
433 }
434
435 if let Ok(mode) = std::env::var("CRATES_DOCS_TRANSPORT_MODE") {
436 config.server.transport_mode = mode;
437 }
438
439 if let Ok(level) = std::env::var("CRATES_DOCS_LOG_LEVEL") {
440 config.logging.level = level;
441 }
442
443 if let Ok(enable_console) = std::env::var("CRATES_DOCS_ENABLE_CONSOLE") {
444 config.logging.enable_console = enable_console.parse().unwrap_or(true);
445 }
446
447 if let Ok(enable_file) = std::env::var("CRATES_DOCS_ENABLE_FILE") {
448 config.logging.enable_file = enable_file.parse().unwrap_or(true);
449 }
450
451 #[cfg(feature = "api-key")]
452 {
453 if let Ok(enabled) = std::env::var("CRATES_DOCS_API_KEY_ENABLED") {
454 config.auth.api_key.enabled = enabled.parse().unwrap_or(false);
455 }
456
457 if let Ok(keys) = std::env::var("CRATES_DOCS_API_KEYS") {
458 config.auth.api_key.keys = keys
459 .split(',')
460 .map(str::trim)
461 .filter(|s| !s.is_empty())
462 .map(ToOwned::to_owned)
463 .collect();
464 }
465
466 if let Ok(header_name) = std::env::var("CRATES_DOCS_API_KEY_HEADER") {
467 config.auth.api_key.header_name = header_name;
468 }
469
470 if let Ok(query_param_name) = std::env::var("CRATES_DOCS_API_KEY_QUERY_PARAM_NAME") {
471 config.auth.api_key.query_param_name = query_param_name;
472 }
473
474 if let Ok(allow_query_param) = std::env::var("CRATES_DOCS_API_KEY_ALLOW_QUERY") {
475 config.auth.api_key.allow_query_param = allow_query_param.parse().unwrap_or(false);
476 }
477
478 if let Ok(key_prefix) = std::env::var("CRATES_DOCS_API_KEY_PREFIX") {
479 config.auth.api_key.key_prefix = key_prefix;
480 }
481 }
482
483 config.validate()?;
484 Ok(config)
485 }
486
487 #[must_use]
489 pub fn merge(file_config: Option<Self>, env_config: Option<Self>) -> Self {
490 let mut config = Self::default();
491
492 if let Some(file) = file_config {
494 config = file;
495 }
496
497 if let Some(env) = env_config {
499 if env.server.name != "crates-docs" {
501 config.server.name = env.server.name;
502 }
503 if env.server.host != "127.0.0.1" {
504 config.server.host = env.server.host;
505 }
506 if env.server.port != 8080 {
507 config.server.port = env.server.port;
508 }
509 if env.server.transport_mode != "hybrid" {
510 config.server.transport_mode = env.server.transport_mode;
511 }
512
513 if env.logging.level != "info" {
515 config.logging.level = env.logging.level;
516 }
517
518 #[cfg(feature = "api-key")]
519 {
520 let default_api_key = crate::server::auth::ApiKeyConfig::default();
521
522 if env.auth.api_key.enabled != default_api_key.enabled {
523 config.auth.api_key.enabled = env.auth.api_key.enabled;
524 }
525
526 if env.auth.api_key.keys != default_api_key.keys {
527 config.auth.api_key.keys = env.auth.api_key.keys;
528 }
529
530 if env.auth.api_key.header_name != default_api_key.header_name {
531 config.auth.api_key.header_name = env.auth.api_key.header_name;
532 }
533
534 if env.auth.api_key.query_param_name != default_api_key.query_param_name {
535 config.auth.api_key.query_param_name = env.auth.api_key.query_param_name;
536 }
537
538 if env.auth.api_key.allow_query_param != default_api_key.allow_query_param {
539 config.auth.api_key.allow_query_param = env.auth.api_key.allow_query_param;
540 }
541
542 if env.auth.api_key.key_prefix != default_api_key.key_prefix {
543 config.auth.api_key.key_prefix = env.auth.api_key.key_prefix;
544 }
545 }
546 }
547
548 config
549 }
550}