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)]
63pub struct AppConfig {
64 pub server: ServerConfig,
66
67 pub cache: CacheConfig,
69
70 #[serde(default)]
72 pub auth: AuthConfig,
73
74 #[serde(default)]
76 pub oauth: OAuthConfig,
77
78 pub logging: LoggingConfig,
80
81 pub performance: PerformanceConfig,
83}
84
85#[derive(Debug, Clone, Deserialize, Serialize)]
94pub struct ServerConfig {
95 pub name: String,
97
98 #[serde(default = "default_version")]
100 pub version: String,
101
102 pub description: Option<String>,
104
105 #[serde(default = "default_icons")]
107 pub icons: Vec<Icon>,
108
109 pub website_url: Option<String>,
111
112 pub host: String,
114
115 pub port: u16,
117
118 pub transport_mode: String,
120
121 pub enable_sse: bool,
123
124 pub enable_oauth: bool,
126
127 pub max_connections: usize,
129
130 pub request_timeout_secs: u64,
132
133 pub response_timeout_secs: u64,
135
136 pub allowed_hosts: Vec<String>,
138
139 pub allowed_origins: Vec<String>,
142}
143
144fn default_version() -> String {
146 crate::VERSION.to_string()
147}
148
149fn default_icons() -> Vec<Icon> {
151 vec![
152 Icon {
153 src: "https://docs.rs/static/favicon-32x32.png".to_string(),
154 mime_type: Some("image/png".to_string()),
155 sizes: vec!["32x32".to_string()],
156 theme: Some(IconTheme::Light),
157 },
158 Icon {
159 src: "https://docs.rs/static/favicon-32x32.png".to_string(),
160 mime_type: Some("image/png".to_string()),
161 sizes: vec!["32x32".to_string()],
162 theme: Some(IconTheme::Dark),
163 },
164 ]
165}
166
167#[derive(Debug, Clone, Deserialize, Serialize)]
183pub struct LoggingConfig {
184 pub level: String,
186
187 pub file_path: Option<String>,
189
190 pub enable_console: bool,
192
193 pub enable_file: bool,
195
196 pub max_file_size_mb: u64,
198
199 pub max_files: usize,
201}
202
203#[derive(Debug, Clone, Deserialize, Serialize)]
225pub struct PerformanceConfig {
226 pub http_client_pool_size: usize,
228
229 pub http_client_pool_idle_timeout_secs: u64,
231
232 pub http_client_connect_timeout_secs: u64,
234
235 pub http_client_timeout_secs: u64,
237
238 pub http_client_read_timeout_secs: u64,
240
241 pub http_client_max_retries: u32,
243
244 pub http_client_retry_initial_delay_ms: u64,
246
247 pub http_client_retry_max_delay_ms: u64,
249
250 pub cache_max_size: usize,
252
253 pub cache_default_ttl_secs: u64,
255
256 pub rate_limit_per_second: u32,
258
259 pub concurrent_request_limit: usize,
261
262 pub enable_response_compression: bool,
264
265 pub enable_metrics: bool,
267
268 pub metrics_port: u16,
270}
271
272impl Default for ServerConfig {
273 fn default() -> Self {
274 Self {
275 name: "crates-docs".to_string(),
276 version: crate::VERSION.to_string(),
277 description: Some(
278 "High-performance Rust crate documentation query MCP server".to_string(),
279 ),
280 icons: default_icons(),
281 website_url: Some("https://github.com/KingingWang/crates-docs".to_string()),
282 host: "127.0.0.1".to_string(),
283 port: 8080,
284 transport_mode: "hybrid".to_string(),
285 enable_sse: true,
286 enable_oauth: false,
287 max_connections: 100,
288 request_timeout_secs: 30,
289 response_timeout_secs: 60,
290 allowed_hosts: vec!["localhost".to_string(), "127.0.0.1".to_string()],
292 allowed_origins: vec!["http://localhost:*".to_string()],
293 }
294 }
295}
296
297impl Default for LoggingConfig {
298 fn default() -> Self {
299 Self {
300 level: "info".to_string(),
301 file_path: Some("./logs/crates-docs.log".to_string()),
302 enable_console: true,
303 enable_file: false, max_file_size_mb: 100,
305 max_files: 10,
306 }
307 }
308}
309
310impl Default for PerformanceConfig {
311 fn default() -> Self {
312 Self {
313 http_client_pool_size: 10,
314 http_client_pool_idle_timeout_secs: 90,
315 http_client_connect_timeout_secs: 10,
316 http_client_timeout_secs: 30,
317 http_client_read_timeout_secs: 30,
318 http_client_max_retries: 3,
319 http_client_retry_initial_delay_ms: 100,
320 http_client_retry_max_delay_ms: 10000,
321 cache_max_size: 1000,
322 cache_default_ttl_secs: 3600,
323 rate_limit_per_second: 100,
324 concurrent_request_limit: 50,
325 enable_response_compression: true,
326 enable_metrics: true,
327 metrics_port: 0,
328 }
329 }
330}
331
332impl AppConfig {
333 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, crate::error::Error> {
339 let content = fs::read_to_string(path).map_err(|e| {
340 crate::error::Error::config("file", format!("Failed to read config file: {e}"))
341 })?;
342
343 let config: Self = toml::from_str(&content).map_err(|e| {
344 crate::error::Error::parse("config", None, format!("Failed to parse config file: {e}"))
345 })?;
346
347 config.validate()?;
348 Ok(config)
349 }
350
351 pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), crate::error::Error> {
357 let content = toml::to_string_pretty(self).map_err(|e| {
358 crate::error::Error::config(
359 "serialization",
360 format!("Failed to serialize configuration: {e}"),
361 )
362 })?;
363
364 if let Some(parent) = path.as_ref().parent() {
366 fs::create_dir_all(parent).map_err(|e| {
367 crate::error::Error::config("directory", format!("Failed to create directory: {e}"))
368 })?;
369 }
370
371 fs::write(path, content).map_err(|e| {
372 crate::error::Error::config("file", format!("Failed to write config file: {e}"))
373 })?;
374
375 Ok(())
376 }
377
378 pub fn validate(&self) -> Result<(), crate::error::Error> {
384 if self.server.host.is_empty() {
386 return Err(crate::error::Error::config("host", "cannot be empty"));
387 }
388
389 if self.server.port == 0 {
390 return Err(crate::error::Error::config("port", "cannot be 0"));
391 }
392
393 if self.server.max_connections == 0 {
394 return Err(crate::error::Error::config(
395 "max_connections",
396 "cannot be 0",
397 ));
398 }
399
400 let valid_modes = ["stdio", "http", "sse", "hybrid"];
402 if !valid_modes.contains(&self.server.transport_mode.as_str()) {
403 return Err(crate::error::Error::config(
404 "transport_mode",
405 format!(
406 "Invalid transport mode: {}, valid values: {:?}",
407 self.server.transport_mode, valid_modes
408 ),
409 ));
410 }
411
412 let valid_levels = ["trace", "debug", "info", "warn", "error"];
414
415 if !valid_levels.contains(&self.logging.level.as_str()) {
416 return Err(crate::error::Error::config(
417 "log_level",
418 format!(
419 "Invalid log level: {}, valid values: {:?}",
420 self.logging.level, valid_levels
421 ),
422 ));
423 }
424
425 if self.performance.http_client_pool_size == 0 {
427 return Err(crate::error::Error::config(
428 "http_client_pool_size",
429 "cannot be 0",
430 ));
431 }
432
433 if self.performance.http_client_pool_idle_timeout_secs == 0 {
434 return Err(crate::error::Error::config(
435 "http_client_pool_idle_timeout_secs",
436 "cannot be 0",
437 ));
438 }
439
440 if self.performance.http_client_connect_timeout_secs == 0 {
441 return Err(crate::error::Error::config(
442 "http_client_connect_timeout_secs",
443 "cannot be 0",
444 ));
445 }
446
447 if self.performance.http_client_timeout_secs == 0 {
448 return Err(crate::error::Error::config(
449 "http_client_timeout_secs",
450 "cannot be 0",
451 ));
452 }
453
454 if self.performance.cache_max_size == 0 {
455 return Err(crate::error::Error::config("cache_max_size", "cannot be 0"));
456 }
457
458 if self.server.enable_oauth {
460 self.oauth.validate()?;
461 }
462
463 Ok(())
464 }
465
466 pub fn from_env() -> Result<Self, crate::error::Error> {
472 let mut config = Self::default();
473
474 if let Ok(name) = std::env::var("CRATES_DOCS_NAME") {
476 config.server.name = name;
477 }
478
479 if let Ok(host) = std::env::var("CRATES_DOCS_HOST") {
480 config.server.host = host;
481 }
482
483 if let Ok(port) = std::env::var("CRATES_DOCS_PORT") {
484 config.server.port = port
485 .parse()
486 .map_err(|e| crate::error::Error::config("port", format!("Invalid port: {e}")))?;
487 }
488
489 if let Ok(mode) = std::env::var("CRATES_DOCS_TRANSPORT_MODE") {
490 config.server.transport_mode = mode;
491 }
492
493 if let Ok(level) = std::env::var("CRATES_DOCS_LOG_LEVEL") {
494 config.logging.level = level;
495 }
496
497 if let Ok(enable_console) = std::env::var("CRATES_DOCS_ENABLE_CONSOLE") {
498 config.logging.enable_console = enable_console.parse().unwrap_or(false);
499 }
500
501 if let Ok(enable_file) = std::env::var("CRATES_DOCS_ENABLE_FILE") {
502 config.logging.enable_file = enable_file.parse().unwrap_or(false);
503 }
504
505 #[cfg(feature = "api-key")]
506 {
507 if let Ok(enabled) = std::env::var("CRATES_DOCS_API_KEY_ENABLED") {
508 config.auth.api_key.enabled = enabled.parse().unwrap_or(false);
509 }
510
511 if let Ok(keys) = std::env::var("CRATES_DOCS_API_KEYS") {
512 config.auth.api_key.keys = keys
513 .split(',')
514 .map(str::trim)
515 .filter(|s| !s.is_empty())
516 .map(ToOwned::to_owned)
517 .collect();
518 }
519
520 if let Ok(header_name) = std::env::var("CRATES_DOCS_API_KEY_HEADER") {
521 config.auth.api_key.header_name = header_name;
522 }
523
524 if let Ok(query_param_name) = std::env::var("CRATES_DOCS_API_KEY_QUERY_PARAM_NAME") {
525 config.auth.api_key.query_param_name = query_param_name;
526 }
527
528 if let Ok(allow_query_param) = std::env::var("CRATES_DOCS_API_KEY_ALLOW_QUERY") {
529 config.auth.api_key.allow_query_param = allow_query_param.parse().unwrap_or(false);
530 }
531
532 if let Ok(key_prefix) = std::env::var("CRATES_DOCS_API_KEY_PREFIX") {
533 config.auth.api_key.key_prefix = key_prefix;
534 }
535 }
536
537 config.validate()?;
538 Ok(config)
539 }
540
541 #[must_use]
543 pub fn merge(file_config: Option<Self>, env_config: Option<Self>) -> Self {
544 let mut config = Self::default();
545
546 if let Some(file) = file_config {
548 config = file;
549 }
550
551 if let Some(env) = env_config {
553 if env.server.name != "crates-docs" {
555 config.server.name = env.server.name;
556 }
557 if env.server.host != "127.0.0.1" {
558 config.server.host = env.server.host;
559 }
560 if env.server.port != 8080 {
561 config.server.port = env.server.port;
562 }
563 if env.server.transport_mode != "hybrid" {
564 config.server.transport_mode = env.server.transport_mode;
565 }
566
567 if env.logging.level != "info" {
569 config.logging.level = env.logging.level;
570 }
571
572 #[cfg(feature = "api-key")]
573 {
574 let default_api_key = crate::server::auth::ApiKeyConfig::default();
575
576 if env.auth.api_key.enabled != default_api_key.enabled {
577 config.auth.api_key.enabled = env.auth.api_key.enabled;
578 }
579
580 if env.auth.api_key.keys != default_api_key.keys {
581 config.auth.api_key.keys = env.auth.api_key.keys;
582 }
583
584 if env.auth.api_key.header_name != default_api_key.header_name {
585 config.auth.api_key.header_name = env.auth.api_key.header_name;
586 }
587
588 if env.auth.api_key.query_param_name != default_api_key.query_param_name {
589 config.auth.api_key.query_param_name = env.auth.api_key.query_param_name;
590 }
591
592 if env.auth.api_key.allow_query_param != default_api_key.allow_query_param {
593 config.auth.api_key.allow_query_param = env.auth.api_key.allow_query_param;
594 }
595
596 if env.auth.api_key.key_prefix != default_api_key.key_prefix {
597 config.auth.api_key.key_prefix = env.auth.api_key.key_prefix;
598 }
599 }
600 }
601
602 config
603 }
604}