1use crate::cache::CacheConfig;
32use crate::server::auth::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 pub oauth: OAuthConfig,
59
60 pub logging: LoggingConfig,
62
63 pub performance: PerformanceConfig,
65}
66
67#[derive(Debug, Clone, Deserialize, Serialize)]
69pub struct ServerConfig {
70 pub name: String,
72
73 #[serde(default = "default_version")]
75 pub version: String,
76
77 pub description: Option<String>,
79
80 #[serde(default = "default_icons")]
82 pub icons: Vec<Icon>,
83
84 pub website_url: Option<String>,
86
87 pub host: String,
89
90 pub port: u16,
92
93 pub transport_mode: String,
95
96 pub enable_sse: bool,
98
99 pub enable_oauth: bool,
101
102 pub max_connections: usize,
104
105 pub request_timeout_secs: u64,
107
108 pub response_timeout_secs: u64,
110
111 pub allowed_hosts: Vec<String>,
113
114 pub allowed_origins: Vec<String>,
117}
118
119fn default_version() -> String {
121 crate::VERSION.to_string()
122}
123
124fn default_icons() -> Vec<Icon> {
126 vec![
127 Icon {
128 src: "https://docs.rs/static/favicon-32x32.png".to_string(),
129 mime_type: Some("image/png".to_string()),
130 sizes: vec!["32x32".to_string()],
131 theme: Some(IconTheme::Light),
132 },
133 Icon {
134 src: "https://docs.rs/static/favicon-32x32.png".to_string(),
135 mime_type: Some("image/png".to_string()),
136 sizes: vec!["32x32".to_string()],
137 theme: Some(IconTheme::Dark),
138 },
139 ]
140}
141
142#[derive(Debug, Clone, Deserialize, Serialize)]
144pub struct LoggingConfig {
145 pub level: String,
147
148 pub file_path: Option<String>,
150
151 pub enable_console: bool,
153
154 pub enable_file: bool,
156
157 pub max_file_size_mb: u64,
159
160 pub max_files: usize,
162}
163
164#[derive(Debug, Clone, Deserialize, Serialize)]
166pub struct PerformanceConfig {
167 pub http_client_pool_size: usize,
169
170 pub http_client_pool_idle_timeout_secs: u64,
172
173 pub http_client_connect_timeout_secs: u64,
175
176 pub http_client_timeout_secs: u64,
178
179 pub http_client_read_timeout_secs: u64,
181
182 pub http_client_max_retries: u32,
184
185 pub http_client_retry_initial_delay_ms: u64,
187
188 pub http_client_retry_max_delay_ms: u64,
190
191 pub cache_max_size: usize,
193
194 pub cache_default_ttl_secs: u64,
196
197 pub rate_limit_per_second: u32,
199
200 pub concurrent_request_limit: usize,
202
203 pub enable_response_compression: bool,
205
206 pub enable_metrics: bool,
208
209 pub metrics_port: u16,
211}
212
213impl Default for ServerConfig {
214 fn default() -> Self {
215 Self {
216 name: "crates-docs".to_string(),
217 version: crate::VERSION.to_string(),
218 description: Some(
219 "High-performance Rust crate documentation query MCP server".to_string(),
220 ),
221 icons: default_icons(),
222 website_url: Some("https://github.com/KingingWang/crates-docs".to_string()),
223 host: "127.0.0.1".to_string(),
224 port: 8080,
225 transport_mode: "hybrid".to_string(),
226 enable_sse: true,
227 enable_oauth: false,
228 max_connections: 100,
229 request_timeout_secs: 30,
230 response_timeout_secs: 60,
231 allowed_hosts: vec!["localhost".to_string(), "127.0.0.1".to_string()],
233 allowed_origins: vec!["http://localhost:*".to_string()],
234 }
235 }
236}
237
238impl Default for LoggingConfig {
239 fn default() -> Self {
240 Self {
241 level: "info".to_string(),
242 file_path: Some("./logs/crates-docs.log".to_string()),
243 enable_console: true,
244 enable_file: false, max_file_size_mb: 100,
246 max_files: 10,
247 }
248 }
249}
250
251impl Default for PerformanceConfig {
252 fn default() -> Self {
253 Self {
254 http_client_pool_size: 10,
255 http_client_pool_idle_timeout_secs: 90,
256 http_client_connect_timeout_secs: 10,
257 http_client_timeout_secs: 30,
258 http_client_read_timeout_secs: 30,
259 http_client_max_retries: 3,
260 http_client_retry_initial_delay_ms: 100,
261 http_client_retry_max_delay_ms: 10000,
262 cache_max_size: 1000,
263 cache_default_ttl_secs: 3600,
264 rate_limit_per_second: 100,
265 concurrent_request_limit: 50,
266 enable_response_compression: true,
267 enable_metrics: true,
268 metrics_port: 0,
269 }
270 }
271}
272
273impl AppConfig {
274 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, crate::error::Error> {
280 let content = fs::read_to_string(path).map_err(|e| {
281 crate::error::Error::config("file", format!("Failed to read config file: {e}"))
282 })?;
283
284 let config: Self = toml::from_str(&content).map_err(|e| {
285 crate::error::Error::parse("config", None, format!("Failed to parse config file: {e}"))
286 })?;
287
288 config.validate()?;
289 Ok(config)
290 }
291
292 pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), crate::error::Error> {
298 let content = toml::to_string_pretty(self).map_err(|e| {
299 crate::error::Error::config(
300 "serialization",
301 format!("Failed to serialize configuration: {e}"),
302 )
303 })?;
304
305 if let Some(parent) = path.as_ref().parent() {
307 fs::create_dir_all(parent).map_err(|e| {
308 crate::error::Error::config("directory", format!("Failed to create directory: {e}"))
309 })?;
310 }
311
312 fs::write(path, content).map_err(|e| {
313 crate::error::Error::config("file", format!("Failed to write config file: {e}"))
314 })?;
315
316 Ok(())
317 }
318
319 pub fn validate(&self) -> Result<(), crate::error::Error> {
325 if self.server.host.is_empty() {
327 return Err(crate::error::Error::config("host", "cannot be empty"));
328 }
329
330 if self.server.port == 0 {
331 return Err(crate::error::Error::config("port", "cannot be 0"));
332 }
333
334 if self.server.max_connections == 0 {
335 return Err(crate::error::Error::config(
336 "max_connections",
337 "cannot be 0",
338 ));
339 }
340
341 let valid_modes = ["stdio", "http", "sse", "hybrid"];
343 if !valid_modes.contains(&self.server.transport_mode.as_str()) {
344 return Err(crate::error::Error::config(
345 "transport_mode",
346 format!(
347 "Invalid transport mode: {}, valid values: {:?}",
348 self.server.transport_mode, valid_modes
349 ),
350 ));
351 }
352
353 let valid_levels = ["trace", "debug", "info", "warn", "error"];
355 if !valid_levels.contains(&self.logging.level.as_str()) {
356 return Err(crate::error::Error::config(
357 "log_level",
358 format!(
359 "Invalid log level: {}, valid values: {:?}",
360 self.logging.level, valid_levels
361 ),
362 ));
363 }
364
365 if self.performance.http_client_pool_size == 0 {
367 return Err(crate::error::Error::config(
368 "http_client_pool_size",
369 "cannot be 0",
370 ));
371 }
372
373 if self.performance.http_client_pool_idle_timeout_secs == 0 {
374 return Err(crate::error::Error::config(
375 "http_client_pool_idle_timeout_secs",
376 "cannot be 0",
377 ));
378 }
379
380 if self.performance.http_client_connect_timeout_secs == 0 {
381 return Err(crate::error::Error::config(
382 "http_client_connect_timeout_secs",
383 "cannot be 0",
384 ));
385 }
386
387 if self.performance.http_client_timeout_secs == 0 {
388 return Err(crate::error::Error::config(
389 "http_client_timeout_secs",
390 "cannot be 0",
391 ));
392 }
393
394 if self.performance.cache_max_size == 0 {
395 return Err(crate::error::Error::config("cache_max_size", "cannot be 0"));
396 }
397
398 if self.server.enable_oauth {
400 self.oauth.validate()?;
401 }
402
403 Ok(())
404 }
405
406 pub fn from_env() -> Result<Self, crate::error::Error> {
412 let mut config = Self::default();
413
414 if let Ok(name) = std::env::var("CRATES_DOCS_NAME") {
416 config.server.name = name;
417 }
418
419 if let Ok(host) = std::env::var("CRATES_DOCS_HOST") {
420 config.server.host = host;
421 }
422
423 if let Ok(port) = std::env::var("CRATES_DOCS_PORT") {
424 config.server.port = port
425 .parse()
426 .map_err(|e| crate::error::Error::config("port", format!("Invalid port: {e}")))?;
427 }
428
429 if let Ok(mode) = std::env::var("CRATES_DOCS_TRANSPORT_MODE") {
430 config.server.transport_mode = mode;
431 }
432
433 if let Ok(level) = std::env::var("CRATES_DOCS_LOG_LEVEL") {
434 config.logging.level = level;
435 }
436
437 if let Ok(enable_console) = std::env::var("CRATES_DOCS_ENABLE_CONSOLE") {
438 config.logging.enable_console = enable_console.parse().unwrap_or(true);
439 }
440
441 if let Ok(enable_file) = std::env::var("CRATES_DOCS_ENABLE_FILE") {
442 config.logging.enable_file = enable_file.parse().unwrap_or(true);
443 }
444
445 config.validate()?;
446 Ok(config)
447 }
448
449 #[must_use]
451 pub fn merge(file_config: Option<Self>, env_config: Option<Self>) -> Self {
452 let mut config = Self::default();
453
454 if let Some(file) = file_config {
456 config = file;
457 }
458
459 if let Some(env) = env_config {
461 if env.server.name != "crates-docs" {
463 config.server.name = env.server.name;
464 }
465 if env.server.host != "127.0.0.1" {
466 config.server.host = env.server.host;
467 }
468 if env.server.port != 8080 {
469 config.server.port = env.server.port;
470 }
471 if env.server.transport_mode != "hybrid" {
472 config.server.transport_mode = env.server.transport_mode;
473 }
474
475 if env.logging.level != "info" {
477 config.logging.level = env.logging.level;
478 }
479 }
480
481 config
482 }
483}