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
38const DEFAULT_HTTP_CLIENT_POOL_SIZE: usize = 10;
42const DEFAULT_HTTP_CLIENT_POOL_IDLE_TIMEOUT_SECS: u64 = 90;
44const DEFAULT_HTTP_CLIENT_CONNECT_TIMEOUT_SECS: u64 = 10;
46const DEFAULT_HTTP_CLIENT_TIMEOUT_SECS: u64 = 30;
48const DEFAULT_HTTP_CLIENT_READ_TIMEOUT_SECS: u64 = 30;
50const DEFAULT_HTTP_CLIENT_MAX_RETRIES: u32 = 3;
52const DEFAULT_HTTP_CLIENT_RETRY_INITIAL_DELAY_MS: u64 = 100;
54const DEFAULT_HTTP_CLIENT_RETRY_MAX_DELAY_MS: u64 = 10_000;
56
57const DEFAULT_SERVER_PORT: u16 = 8080;
61const DEFAULT_SERVER_MAX_CONNECTIONS: usize = 100;
63const DEFAULT_REQUEST_TIMEOUT_SECS: u64 = 30;
65const DEFAULT_RESPONSE_TIMEOUT_SECS: u64 = 60;
67
68const DEFAULT_CACHE_MAX_SIZE: usize = 1000;
72const DEFAULT_CACHE_DEFAULT_TTL_SECS: u64 = 3600;
74const DEFAULT_RATE_LIMIT_PER_SECOND: u32 = 100;
76const DEFAULT_CONCURRENT_REQUEST_LIMIT: usize = 50;
78
79const DEFAULT_MAX_FILE_SIZE_MB: u64 = 100;
83const DEFAULT_MAX_FILES: usize = 10;
85
86#[derive(Debug, Clone, Deserialize, Serialize, Default)]
111pub struct AppConfig {
112 #[serde(default)]
114 pub server: ServerConfig,
115
116 #[serde(default)]
118 pub cache: CacheConfig,
119
120 #[serde(default)]
122 pub auth: AuthConfig,
123
124 #[serde(default)]
126 pub oauth: OAuthConfig,
127
128 #[serde(default)]
130 pub logging: LoggingConfig,
131
132 #[serde(default)]
134 pub performance: PerformanceConfig,
135}
136
137#[derive(Debug, Clone, Deserialize, Serialize)]
146pub struct ServerConfig {
147 #[serde(default = "default_server_name")]
149 pub name: String,
150
151 #[serde(default = "default_version")]
153 pub version: String,
154
155 #[serde(default = "default_server_description")]
157 pub description: Option<String>,
158
159 #[serde(default = "default_icons")]
161 pub icons: Vec<Icon>,
162
163 #[serde(default = "default_server_website_url")]
165 pub website_url: Option<String>,
166
167 #[serde(default = "default_server_host")]
169 pub host: String,
170
171 #[serde(default = "default_server_port")]
173 pub port: u16,
174
175 #[serde(default = "default_server_transport_mode")]
177 pub transport_mode: String,
178
179 #[serde(default = "default_server_enable_sse")]
181 pub enable_sse: bool,
182
183 #[serde(default = "default_server_enable_oauth")]
185 pub enable_oauth: bool,
186
187 #[serde(default = "default_server_max_connections")]
189 pub max_connections: usize,
190
191 #[serde(default = "default_server_request_timeout_secs")]
193 pub request_timeout_secs: u64,
194
195 #[serde(default = "default_server_response_timeout_secs")]
197 pub response_timeout_secs: u64,
198
199 #[serde(default = "default_server_allowed_hosts")]
206 pub allowed_hosts: Vec<String>,
207
208 #[serde(default = "default_server_allowed_origins")]
216 pub allowed_origins: Vec<String>,
217
218 #[serde(default = "default_server_dns_rebinding_protection")]
231 pub dns_rebinding_protection: bool,
232}
233
234fn default_version() -> String {
236 crate::VERSION.to_string()
237}
238
239fn default_icons() -> Vec<Icon> {
241 vec![
242 Icon {
243 src: "https://docs.rs/static/favicon-32x32.png".to_string(),
244 mime_type: Some("image/png".to_string()),
245 sizes: vec!["32x32".to_string()],
246 theme: Some(IconTheme::Light),
247 },
248 Icon {
249 src: "https://docs.rs/static/favicon-32x32.png".to_string(),
250 mime_type: Some("image/png".to_string()),
251 sizes: vec!["32x32".to_string()],
252 theme: Some(IconTheme::Dark),
253 },
254 ]
255}
256
257fn default_server_name() -> String {
259 ServerConfig::default().name
260}
261
262fn default_server_description() -> Option<String> {
263 ServerConfig::default().description
264}
265
266fn default_server_website_url() -> Option<String> {
267 ServerConfig::default().website_url
268}
269
270fn default_server_host() -> String {
271 ServerConfig::default().host
272}
273
274fn default_server_port() -> u16 {
275 ServerConfig::default().port
276}
277
278fn default_server_transport_mode() -> String {
279 ServerConfig::default().transport_mode
280}
281
282fn default_server_enable_sse() -> bool {
283 ServerConfig::default().enable_sse
284}
285
286fn default_server_enable_oauth() -> bool {
287 ServerConfig::default().enable_oauth
288}
289
290fn default_server_max_connections() -> usize {
291 ServerConfig::default().max_connections
292}
293
294fn default_server_request_timeout_secs() -> u64 {
295 ServerConfig::default().request_timeout_secs
296}
297
298fn default_server_response_timeout_secs() -> u64 {
299 ServerConfig::default().response_timeout_secs
300}
301
302fn default_server_allowed_hosts() -> Vec<String> {
303 ServerConfig::default().allowed_hosts
304}
305
306fn default_server_allowed_origins() -> Vec<String> {
307 ServerConfig::default().allowed_origins
308}
309
310fn default_server_dns_rebinding_protection() -> bool {
311 ServerConfig::default().dns_rebinding_protection
312}
313fn default_logging_level() -> String {
314 LoggingConfig::default().level
315}
316
317fn default_logging_file_path() -> Option<String> {
318 LoggingConfig::default().file_path
319}
320
321fn default_logging_enable_console() -> bool {
322 LoggingConfig::default().enable_console
323}
324
325fn default_logging_enable_file() -> bool {
326 LoggingConfig::default().enable_file
327}
328
329fn default_logging_max_file_size_mb() -> u64 {
330 LoggingConfig::default().max_file_size_mb
331}
332
333fn default_logging_max_files() -> usize {
334 LoggingConfig::default().max_files
335}
336fn default_perf_http_client_pool_size() -> usize {
337 PerformanceConfig::default().http_client_pool_size
338}
339
340fn default_perf_http_client_pool_idle_timeout_secs() -> u64 {
341 PerformanceConfig::default().http_client_pool_idle_timeout_secs
342}
343
344fn default_perf_http_client_connect_timeout_secs() -> u64 {
345 PerformanceConfig::default().http_client_connect_timeout_secs
346}
347
348fn default_perf_http_client_timeout_secs() -> u64 {
349 PerformanceConfig::default().http_client_timeout_secs
350}
351
352fn default_perf_http_client_read_timeout_secs() -> u64 {
353 PerformanceConfig::default().http_client_read_timeout_secs
354}
355
356fn default_perf_http_client_max_retries() -> u32 {
357 PerformanceConfig::default().http_client_max_retries
358}
359
360fn default_perf_http_client_retry_initial_delay_ms() -> u64 {
361 PerformanceConfig::default().http_client_retry_initial_delay_ms
362}
363
364fn default_perf_http_client_retry_max_delay_ms() -> u64 {
365 PerformanceConfig::default().http_client_retry_max_delay_ms
366}
367
368fn default_perf_cache_max_size() -> usize {
369 PerformanceConfig::default().cache_max_size
370}
371
372fn default_perf_cache_default_ttl_secs() -> u64 {
373 PerformanceConfig::default().cache_default_ttl_secs
374}
375
376fn default_perf_rate_limit_per_second() -> u32 {
377 PerformanceConfig::default().rate_limit_per_second
378}
379
380fn default_perf_concurrent_request_limit() -> usize {
381 PerformanceConfig::default().concurrent_request_limit
382}
383
384fn default_perf_enable_response_compression() -> bool {
385 PerformanceConfig::default().enable_response_compression
386}
387
388fn default_perf_enable_metrics() -> bool {
389 PerformanceConfig::default().enable_metrics
390}
391
392fn default_perf_metrics_port() -> u16 {
393 PerformanceConfig::default().metrics_port
394}
395
396#[derive(Debug, Clone, Deserialize, Serialize)]
412pub struct LoggingConfig {
413 #[serde(default = "default_logging_level")]
415 pub level: String,
416
417 #[serde(default = "default_logging_file_path")]
419 pub file_path: Option<String>,
420
421 #[serde(default = "default_logging_enable_console")]
423 pub enable_console: bool,
424
425 #[serde(default = "default_logging_enable_file")]
427 pub enable_file: bool,
428
429 #[serde(default = "default_logging_max_file_size_mb")]
431 pub max_file_size_mb: u64,
432
433 #[serde(default = "default_logging_max_files")]
435 pub max_files: usize,
436}
437
438#[derive(Debug, Clone, Deserialize, Serialize)]
460pub struct PerformanceConfig {
461 #[serde(default = "default_perf_http_client_pool_size")]
463 pub http_client_pool_size: usize,
464
465 #[serde(default = "default_perf_http_client_pool_idle_timeout_secs")]
467 pub http_client_pool_idle_timeout_secs: u64,
468
469 #[serde(default = "default_perf_http_client_connect_timeout_secs")]
471 pub http_client_connect_timeout_secs: u64,
472
473 #[serde(default = "default_perf_http_client_timeout_secs")]
475 pub http_client_timeout_secs: u64,
476
477 #[serde(default = "default_perf_http_client_read_timeout_secs")]
479 pub http_client_read_timeout_secs: u64,
480
481 #[serde(default = "default_perf_http_client_max_retries")]
483 pub http_client_max_retries: u32,
484
485 #[serde(default = "default_perf_http_client_retry_initial_delay_ms")]
487 pub http_client_retry_initial_delay_ms: u64,
488
489 #[serde(default = "default_perf_http_client_retry_max_delay_ms")]
491 pub http_client_retry_max_delay_ms: u64,
492
493 #[serde(default = "default_perf_cache_max_size")]
495 pub cache_max_size: usize,
496
497 #[serde(default = "default_perf_cache_default_ttl_secs")]
499 pub cache_default_ttl_secs: u64,
500
501 #[serde(default = "default_perf_rate_limit_per_second")]
503 pub rate_limit_per_second: u32,
504
505 #[serde(default = "default_perf_concurrent_request_limit")]
507 pub concurrent_request_limit: usize,
508
509 #[serde(default = "default_perf_enable_response_compression")]
511 pub enable_response_compression: bool,
512
513 #[serde(default = "default_perf_enable_metrics")]
519 pub enable_metrics: bool,
520
521 #[serde(default = "default_perf_metrics_port")]
523 pub metrics_port: u16,
524}
525
526impl Default for ServerConfig {
527 fn default() -> Self {
528 Self {
529 name: "crates-docs".to_string(),
530 version: crate::VERSION.to_string(),
531 description: Some(
532 "High-performance Rust crate documentation query MCP server".to_string(),
533 ),
534 icons: default_icons(),
535 website_url: Some("https://github.com/KingingWang/crates-docs".to_string()),
536 host: "127.0.0.1".to_string(),
537 port: DEFAULT_SERVER_PORT,
538 transport_mode: "hybrid".to_string(),
539 enable_sse: true,
540 enable_oauth: false,
541 max_connections: DEFAULT_SERVER_MAX_CONNECTIONS,
542 request_timeout_secs: DEFAULT_REQUEST_TIMEOUT_SECS,
543 response_timeout_secs: DEFAULT_RESPONSE_TIMEOUT_SECS,
544 allowed_hosts: vec!["localhost".to_string(), "127.0.0.1".to_string()],
546 allowed_origins: vec!["http://localhost:*".to_string()],
547 dns_rebinding_protection: false,
550 }
551 }
552}
553
554impl Default for LoggingConfig {
555 fn default() -> Self {
556 Self {
557 level: "info".to_string(),
558 file_path: Some("./logs/crates-docs.log".to_string()),
559 enable_console: true,
560 enable_file: false, max_file_size_mb: DEFAULT_MAX_FILE_SIZE_MB,
562 max_files: DEFAULT_MAX_FILES,
563 }
564 }
565}
566
567impl Default for PerformanceConfig {
568 fn default() -> Self {
569 Self {
570 http_client_pool_size: DEFAULT_HTTP_CLIENT_POOL_SIZE,
571 http_client_pool_idle_timeout_secs: DEFAULT_HTTP_CLIENT_POOL_IDLE_TIMEOUT_SECS,
572 http_client_connect_timeout_secs: DEFAULT_HTTP_CLIENT_CONNECT_TIMEOUT_SECS,
573 http_client_timeout_secs: DEFAULT_HTTP_CLIENT_TIMEOUT_SECS,
574 http_client_read_timeout_secs: DEFAULT_HTTP_CLIENT_READ_TIMEOUT_SECS,
575 http_client_max_retries: DEFAULT_HTTP_CLIENT_MAX_RETRIES,
576 http_client_retry_initial_delay_ms: DEFAULT_HTTP_CLIENT_RETRY_INITIAL_DELAY_MS,
577 http_client_retry_max_delay_ms: DEFAULT_HTTP_CLIENT_RETRY_MAX_DELAY_MS,
578 cache_max_size: DEFAULT_CACHE_MAX_SIZE,
579 cache_default_ttl_secs: DEFAULT_CACHE_DEFAULT_TTL_SECS,
580 rate_limit_per_second: DEFAULT_RATE_LIMIT_PER_SECOND,
581 concurrent_request_limit: DEFAULT_CONCURRENT_REQUEST_LIMIT,
582 enable_response_compression: true,
583 enable_metrics: false,
584 metrics_port: 0,
585 }
586 }
587}
588
589#[derive(Debug, Clone, Default)]
609pub struct EnvServerConfig {
610 pub name: Option<String>,
612 pub host: Option<String>,
614 pub port: Option<u16>,
616 pub transport_mode: Option<String>,
618}
619
620#[derive(Debug, Clone, Default)]
630pub struct EnvLoggingConfig {
631 pub level: Option<String>,
633 pub enable_console: Option<bool>,
635 pub enable_file: Option<bool>,
637}
638
639#[cfg(feature = "api-key")]
649#[derive(Debug, Clone, Default)]
650pub struct EnvApiKeyConfig {
651 pub enabled: Option<bool>,
653 pub keys: Option<Vec<String>>,
655 pub header_name: Option<String>,
657 pub query_param_name: Option<String>,
659 pub allow_query_param: Option<bool>,
661 pub key_prefix: Option<String>,
663}
664
665#[derive(Debug, Clone, Default)]
670pub struct EnvAppConfig {
671 pub server: EnvServerConfig,
673 pub logging: EnvLoggingConfig,
675 #[cfg(feature = "api-key")]
677 pub auth_api_key: EnvApiKeyConfig,
678}
679
680impl AppConfig {
681 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, crate::error::Error> {
687 let content = fs::read_to_string(path).map_err(|e| {
688 crate::error::Error::config("file", format!("Failed to read config file: {e}"))
689 })?;
690
691 let config: Self = toml::from_str(&content).map_err(|e| {
692 crate::error::Error::parse("config", None, format!("Failed to parse config file: {e}"))
693 })?;
694
695 config.validate()?;
696 Ok(config)
697 }
698
699 pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), crate::error::Error> {
705 let content = toml::to_string_pretty(self).map_err(|e| {
706 crate::error::Error::config(
707 "serialization",
708 format!("Failed to serialize configuration: {e}"),
709 )
710 })?;
711
712 if let Some(parent) = path.as_ref().parent() {
714 fs::create_dir_all(parent).map_err(|e| {
715 crate::error::Error::config("directory", format!("Failed to create directory: {e}"))
716 })?;
717 }
718
719 fs::write(path, content).map_err(|e| {
720 crate::error::Error::config("file", format!("Failed to write config file: {e}"))
721 })?;
722
723 Ok(())
724 }
725
726 pub fn validate(&self) -> Result<(), crate::error::Error> {
732 if self.server.host.is_empty() {
734 return Err(crate::error::Error::config("host", "cannot be empty"));
735 }
736
737 if self.server.port == 0 {
738 return Err(crate::error::Error::config("port", "cannot be 0"));
739 }
740
741 if self.server.max_connections == 0 {
742 return Err(crate::error::Error::config(
743 "max_connections",
744 "cannot be 0",
745 ));
746 }
747
748 let valid_modes = ["stdio", "http", "sse", "hybrid"];
753 if !valid_modes.contains(&self.server.transport_mode.to_lowercase().as_str()) {
754 return Err(crate::error::Error::config(
755 "transport_mode",
756 format!(
757 "Invalid transport mode: {}, valid values: {:?}",
758 self.server.transport_mode, valid_modes
759 ),
760 ));
761 }
762
763 let valid_levels = ["trace", "debug", "info", "warn", "error"];
765
766 if !valid_levels.contains(&self.logging.level.as_str()) {
767 return Err(crate::error::Error::config(
768 "log_level",
769 format!(
770 "Invalid log level: {}, valid values: {:?}",
771 self.logging.level, valid_levels
772 ),
773 ));
774 }
775
776 if self.performance.http_client_pool_size == 0 {
778 return Err(crate::error::Error::config(
779 "http_client_pool_size",
780 "cannot be 0",
781 ));
782 }
783
784 if self.performance.http_client_pool_idle_timeout_secs == 0 {
785 return Err(crate::error::Error::config(
786 "http_client_pool_idle_timeout_secs",
787 "cannot be 0",
788 ));
789 }
790
791 if self.performance.http_client_connect_timeout_secs == 0 {
792 return Err(crate::error::Error::config(
793 "http_client_connect_timeout_secs",
794 "cannot be 0",
795 ));
796 }
797
798 if self.performance.http_client_timeout_secs == 0 {
799 return Err(crate::error::Error::config(
800 "http_client_timeout_secs",
801 "cannot be 0",
802 ));
803 }
804
805 if self.performance.http_client_read_timeout_secs == 0 {
811 return Err(crate::error::Error::config(
812 "http_client_read_timeout_secs",
813 "cannot be 0",
814 ));
815 }
816
817 if self.performance.cache_max_size == 0 {
818 return Err(crate::error::Error::config("cache_max_size", "cannot be 0"));
819 }
820
821 let valid_cache_types = ["memory", "redis"];
828 if !valid_cache_types.contains(&self.cache.cache_type.as_str()) {
829 return Err(crate::error::Error::config(
830 "cache.cache_type",
831 format!(
832 "Invalid cache type: {}, valid values: {:?}",
833 self.cache.cache_type, valid_cache_types
834 ),
835 ));
836 }
837 if self.cache.cache_type == "memory" && self.cache.memory_size == Some(0) {
838 return Err(crate::error::Error::config(
839 "cache.memory_size",
840 "cannot be 0 (this would disable the cache); omit it to use the default",
841 ));
842 }
843
844 if self.server.enable_oauth {
846 self.oauth.validate()?;
847 }
848
849 self.auth.validate()?;
855
856 Ok(())
857 }
858
859 pub fn from_env() -> Result<EnvAppConfig, crate::error::Error> {
868 let mut config = EnvAppConfig::default();
869
870 if let Ok(name) = std::env::var("CRATES_DOCS_NAME") {
872 config.server.name = Some(name);
873 }
874
875 if let Ok(host) = std::env::var("CRATES_DOCS_HOST") {
876 config.server.host = Some(host);
877 }
878
879 if let Ok(port) = std::env::var("CRATES_DOCS_PORT") {
880 config.server.port =
881 Some(port.parse().map_err(|e| {
882 crate::error::Error::config("port", format!("Invalid port: {e}"))
883 })?);
884 }
885
886 if let Ok(mode) = std::env::var("CRATES_DOCS_TRANSPORT_MODE") {
887 config.server.transport_mode = Some(mode);
888 }
889
890 if let Ok(level) = std::env::var("CRATES_DOCS_LOG_LEVEL") {
892 config.logging.level = Some(level);
893 }
894
895 if let Ok(enable_console) = std::env::var("CRATES_DOCS_ENABLE_CONSOLE") {
896 config.logging.enable_console = enable_console.parse().ok();
897 }
898
899 if let Ok(enable_file) = std::env::var("CRATES_DOCS_ENABLE_FILE") {
900 config.logging.enable_file = enable_file.parse().ok();
901 }
902
903 #[cfg(feature = "api-key")]
904 {
905 if let Ok(enabled) = std::env::var("CRATES_DOCS_API_KEY_ENABLED") {
906 config.auth_api_key.enabled = enabled.parse().ok();
907 }
908
909 if let Ok(keys) = std::env::var("CRATES_DOCS_API_KEYS") {
910 config.auth_api_key.keys = Some(
911 keys.split(',')
912 .map(str::trim)
913 .filter(|s| !s.is_empty())
914 .map(ToOwned::to_owned)
915 .collect(),
916 );
917 }
918
919 if let Ok(header_name) = std::env::var("CRATES_DOCS_API_KEY_HEADER") {
920 config.auth_api_key.header_name = Some(header_name);
921 }
922
923 if let Ok(query_param_name) = std::env::var("CRATES_DOCS_API_KEY_QUERY_PARAM_NAME") {
924 config.auth_api_key.query_param_name = Some(query_param_name);
925 }
926
927 if let Ok(allow_query_param) = std::env::var("CRATES_DOCS_API_KEY_ALLOW_QUERY") {
928 config.auth_api_key.allow_query_param = allow_query_param.parse().ok();
929 }
930
931 if let Ok(key_prefix) = std::env::var("CRATES_DOCS_API_KEY_PREFIX") {
932 config.auth_api_key.key_prefix = Some(key_prefix);
933 }
934 }
935
936 Ok(config)
937 }
938
939 #[must_use]
945 pub fn merge(file_config: Option<Self>, env_config: Option<EnvAppConfig>) -> Self {
946 let mut config = Self::default();
947
948 if let Some(file) = file_config {
950 config = file;
951 }
952
953 if let Some(env) = env_config {
956 if let Some(name) = env.server.name {
958 config.server.name = name;
959 }
960 if let Some(host) = env.server.host {
961 config.server.host = host;
962 }
963 if let Some(port) = env.server.port {
964 config.server.port = port;
965 }
966 if let Some(transport_mode) = env.server.transport_mode {
967 config.server.transport_mode = transport_mode;
968 }
969
970 if let Some(level) = env.logging.level {
972 config.logging.level = level;
973 }
974 if let Some(enable_console) = env.logging.enable_console {
975 config.logging.enable_console = enable_console;
976 }
977 if let Some(enable_file) = env.logging.enable_file {
978 config.logging.enable_file = enable_file;
979 }
980
981 #[cfg(feature = "api-key")]
982 {
983 if let Some(enabled) = env.auth_api_key.enabled {
984 config.auth.api_key.enabled = enabled;
985 }
986 if let Some(keys) = env.auth_api_key.keys {
987 config.auth.api_key.keys = keys;
988 }
989 if let Some(header_name) = env.auth_api_key.header_name {
990 config.auth.api_key.header_name = header_name;
991 }
992 if let Some(query_param_name) = env.auth_api_key.query_param_name {
993 config.auth.api_key.query_param_name = query_param_name;
994 }
995 if let Some(allow_query_param) = env.auth_api_key.allow_query_param {
996 config.auth.api_key.allow_query_param = allow_query_param;
997 }
998 if let Some(key_prefix) = env.auth_api_key.key_prefix {
999 config.auth.api_key.key_prefix = key_prefix;
1000 }
1001 }
1002 }
1003
1004 config
1005 }
1006}