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 pub version: String,
75
76 pub description: Option<String>,
78
79 #[serde(default = "default_icons")]
81 pub icons: Vec<Icon>,
82
83 pub website_url: Option<String>,
85
86 pub host: String,
88
89 pub port: u16,
91
92 pub transport_mode: String,
94
95 pub enable_sse: bool,
97
98 pub enable_oauth: bool,
100
101 pub max_connections: usize,
103
104 pub request_timeout_secs: u64,
106
107 pub response_timeout_secs: u64,
109
110 pub allowed_hosts: Vec<String>,
112
113 pub allowed_origins: Vec<String>,
116}
117
118fn default_icons() -> Vec<Icon> {
120 vec![
121 Icon {
122 src: "https://docs.rs/static/favicon-32x32.png".to_string(),
123 mime_type: Some("image/png".to_string()),
124 sizes: vec!["32x32".to_string()],
125 theme: Some(IconTheme::Light),
126 },
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::Dark),
132 },
133 ]
134}
135
136#[derive(Debug, Clone, Deserialize, Serialize)]
138pub struct LoggingConfig {
139 pub level: String,
141
142 pub file_path: Option<String>,
144
145 pub enable_console: bool,
147
148 pub enable_file: bool,
150
151 pub max_file_size_mb: u64,
153
154 pub max_files: usize,
156}
157
158#[derive(Debug, Clone, Deserialize, Serialize)]
160pub struct PerformanceConfig {
161 pub http_client_pool_size: usize,
163
164 pub http_client_pool_idle_timeout_secs: u64,
166
167 pub http_client_connect_timeout_secs: u64,
169
170 pub http_client_timeout_secs: u64,
172
173 pub http_client_read_timeout_secs: u64,
175
176 pub http_client_max_retries: u32,
178
179 pub http_client_retry_initial_delay_ms: u64,
181
182 pub http_client_retry_max_delay_ms: u64,
184
185 pub cache_max_size: usize,
187
188 pub cache_default_ttl_secs: u64,
190
191 pub rate_limit_per_second: u32,
193
194 pub concurrent_request_limit: usize,
196
197 pub enable_response_compression: bool,
199
200 pub enable_metrics: bool,
202
203 pub metrics_port: u16,
205}
206
207impl Default for ServerConfig {
208 fn default() -> Self {
209 Self {
210 name: "crates-docs".to_string(),
211 version: crate::VERSION.to_string(),
212 description: Some(
213 "High-performance Rust crate documentation query MCP server".to_string(),
214 ),
215 icons: default_icons(),
216 website_url: Some("https://github.com/KingingWang/crates-docs".to_string()),
217 host: "127.0.0.1".to_string(),
218 port: 8080,
219 transport_mode: "hybrid".to_string(),
220 enable_sse: true,
221 enable_oauth: false,
222 max_connections: 100,
223 request_timeout_secs: 30,
224 response_timeout_secs: 60,
225 allowed_hosts: vec!["localhost".to_string(), "127.0.0.1".to_string()],
227 allowed_origins: vec!["http://localhost:*".to_string()],
228 }
229 }
230}
231
232impl Default for LoggingConfig {
233 fn default() -> Self {
234 Self {
235 level: "info".to_string(),
236 file_path: Some("./logs/crates-docs.log".to_string()),
237 enable_console: true,
238 enable_file: false, max_file_size_mb: 100,
240 max_files: 10,
241 }
242 }
243}
244
245impl Default for PerformanceConfig {
246 fn default() -> Self {
247 Self {
248 http_client_pool_size: 10,
249 http_client_pool_idle_timeout_secs: 90,
250 http_client_connect_timeout_secs: 10,
251 http_client_timeout_secs: 30,
252 http_client_read_timeout_secs: 30,
253 http_client_max_retries: 3,
254 http_client_retry_initial_delay_ms: 100,
255 http_client_retry_max_delay_ms: 10000,
256 cache_max_size: 1000,
257 cache_default_ttl_secs: 3600,
258 rate_limit_per_second: 100,
259 concurrent_request_limit: 50,
260 enable_response_compression: true,
261 enable_metrics: true,
262 metrics_port: 0,
263 }
264 }
265}
266
267impl AppConfig {
268 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, crate::error::Error> {
274 let content = fs::read_to_string(path).map_err(|e| {
275 crate::error::Error::config("file", format!("Failed to read config file: {e}"))
276 })?;
277
278 let config: Self = toml::from_str(&content).map_err(|e| {
279 crate::error::Error::parse("config", None, format!("Failed to parse config file: {e}"))
280 })?;
281
282 config.validate()?;
283 Ok(config)
284 }
285
286 pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), crate::error::Error> {
292 let content = toml::to_string_pretty(self).map_err(|e| {
293 crate::error::Error::config(
294 "serialization",
295 format!("Failed to serialize configuration: {e}"),
296 )
297 })?;
298
299 if let Some(parent) = path.as_ref().parent() {
301 fs::create_dir_all(parent).map_err(|e| {
302 crate::error::Error::config("directory", format!("Failed to create directory: {e}"))
303 })?;
304 }
305
306 fs::write(path, content).map_err(|e| {
307 crate::error::Error::config("file", format!("Failed to write config file: {e}"))
308 })?;
309
310 Ok(())
311 }
312
313 pub fn validate(&self) -> Result<(), crate::error::Error> {
319 if self.server.host.is_empty() {
321 return Err(crate::error::Error::config("host", "cannot be empty"));
322 }
323
324 if self.server.port == 0 {
325 return Err(crate::error::Error::config("port", "cannot be 0"));
326 }
327
328 if self.server.max_connections == 0 {
329 return Err(crate::error::Error::config(
330 "max_connections",
331 "cannot be 0",
332 ));
333 }
334
335 let valid_modes = ["stdio", "http", "sse", "hybrid"];
337 if !valid_modes.contains(&self.server.transport_mode.as_str()) {
338 return Err(crate::error::Error::config(
339 "transport_mode",
340 format!(
341 "Invalid transport mode: {}, valid values: {:?}",
342 self.server.transport_mode, valid_modes
343 ),
344 ));
345 }
346
347 let valid_levels = ["trace", "debug", "info", "warn", "error"];
349 if !valid_levels.contains(&self.logging.level.as_str()) {
350 return Err(crate::error::Error::config(
351 "log_level",
352 format!(
353 "Invalid log level: {}, valid values: {:?}",
354 self.logging.level, valid_levels
355 ),
356 ));
357 }
358
359 if self.performance.http_client_pool_size == 0 {
361 return Err(crate::error::Error::config(
362 "http_client_pool_size",
363 "cannot be 0",
364 ));
365 }
366
367 if self.performance.http_client_pool_idle_timeout_secs == 0 {
368 return Err(crate::error::Error::config(
369 "http_client_pool_idle_timeout_secs",
370 "cannot be 0",
371 ));
372 }
373
374 if self.performance.http_client_connect_timeout_secs == 0 {
375 return Err(crate::error::Error::config(
376 "http_client_connect_timeout_secs",
377 "cannot be 0",
378 ));
379 }
380
381 if self.performance.http_client_timeout_secs == 0 {
382 return Err(crate::error::Error::config(
383 "http_client_timeout_secs",
384 "cannot be 0",
385 ));
386 }
387
388 if self.performance.cache_max_size == 0 {
389 return Err(crate::error::Error::config("cache_max_size", "cannot be 0"));
390 }
391
392 if self.server.enable_oauth {
394 self.oauth.validate()?;
395 }
396
397 Ok(())
398 }
399
400 pub fn from_env() -> Result<Self, crate::error::Error> {
406 let mut config = Self::default();
407
408 if let Ok(name) = std::env::var("CRATES_DOCS_NAME") {
410 config.server.name = name;
411 }
412
413 if let Ok(host) = std::env::var("CRATES_DOCS_HOST") {
414 config.server.host = host;
415 }
416
417 if let Ok(port) = std::env::var("CRATES_DOCS_PORT") {
418 config.server.port = port
419 .parse()
420 .map_err(|e| crate::error::Error::config("port", format!("Invalid port: {e}")))?;
421 }
422
423 if let Ok(mode) = std::env::var("CRATES_DOCS_TRANSPORT_MODE") {
424 config.server.transport_mode = mode;
425 }
426
427 if let Ok(level) = std::env::var("CRATES_DOCS_LOG_LEVEL") {
428 config.logging.level = level;
429 }
430
431 if let Ok(enable_console) = std::env::var("CRATES_DOCS_ENABLE_CONSOLE") {
432 config.logging.enable_console = enable_console.parse().unwrap_or(true);
433 }
434
435 if let Ok(enable_file) = std::env::var("CRATES_DOCS_ENABLE_FILE") {
436 config.logging.enable_file = enable_file.parse().unwrap_or(true);
437 }
438
439 config.validate()?;
440 Ok(config)
441 }
442
443 #[must_use]
445 pub fn merge(file_config: Option<Self>, env_config: Option<Self>) -> Self {
446 let mut config = Self::default();
447
448 if let Some(file) = file_config {
450 config = file;
451 }
452
453 if let Some(env) = env_config {
455 if env.server.name != "crates-docs" {
457 config.server.name = env.server.name;
458 }
459 if env.server.host != "127.0.0.1" {
460 config.server.host = env.server.host;
461 }
462 if env.server.port != 8080 {
463 config.server.port = env.server.port;
464 }
465 if env.server.transport_mode != "hybrid" {
466 config.server.transport_mode = env.server.transport_mode;
467 }
468
469 if env.logging.level != "info" {
471 config.logging.level = env.logging.level;
472 }
473 }
474
475 config
476 }
477}