1use crate::cache::CacheConfig;
4use crate::server::auth::OAuthConfig;
5use rust_mcp_sdk::schema::{Icon, IconTheme};
6use serde::{Deserialize, Serialize};
7use std::fs;
8use std::path::Path;
9
10#[derive(Debug, Clone, Deserialize, Serialize, Default)]
12pub struct AppConfig {
13 pub server: ServerConfig,
15
16 pub cache: CacheConfig,
18
19 pub oauth: OAuthConfig,
21
22 pub logging: LoggingConfig,
24
25 pub performance: PerformanceConfig,
27}
28
29#[derive(Debug, Clone, Deserialize, Serialize)]
31pub struct ServerConfig {
32 pub name: String,
34
35 pub version: String,
37
38 pub description: Option<String>,
40
41 #[serde(default = "default_icons")]
43 pub icons: Vec<Icon>,
44
45 pub website_url: Option<String>,
47
48 pub host: String,
50
51 pub port: u16,
53
54 pub transport_mode: String,
56
57 pub enable_sse: bool,
59
60 pub enable_oauth: bool,
62
63 pub max_connections: usize,
65
66 pub request_timeout_secs: u64,
68
69 pub response_timeout_secs: u64,
71
72 pub allowed_hosts: Vec<String>,
74
75 pub allowed_origins: Vec<String>,
78}
79
80fn default_icons() -> Vec<Icon> {
82 vec![
83 Icon {
84 src: "https://docs.rs/static/favicon-32x32.png".to_string(),
85 mime_type: Some("image/png".to_string()),
86 sizes: vec!["32x32".to_string()],
87 theme: Some(IconTheme::Light),
88 },
89 Icon {
90 src: "https://docs.rs/static/favicon-32x32.png".to_string(),
91 mime_type: Some("image/png".to_string()),
92 sizes: vec!["32x32".to_string()],
93 theme: Some(IconTheme::Dark),
94 },
95 ]
96}
97
98#[derive(Debug, Clone, Deserialize, Serialize)]
100pub struct LoggingConfig {
101 pub level: String,
103
104 pub file_path: Option<String>,
106
107 pub enable_console: bool,
109
110 pub enable_file: bool,
112
113 pub max_file_size_mb: u64,
115
116 pub max_files: usize,
118}
119
120#[derive(Debug, Clone, Deserialize, Serialize)]
122pub struct PerformanceConfig {
123 pub http_client_pool_size: usize,
125
126 pub http_client_pool_idle_timeout_secs: u64,
128
129 pub http_client_connect_timeout_secs: u64,
131
132 pub http_client_timeout_secs: u64,
134
135 pub http_client_read_timeout_secs: u64,
137
138 pub http_client_max_retries: u32,
140
141 pub http_client_retry_initial_delay_ms: u64,
143
144 pub http_client_retry_max_delay_ms: u64,
146
147 pub cache_max_size: usize,
149
150 pub cache_default_ttl_secs: u64,
152
153 pub rate_limit_per_second: u32,
155
156 pub concurrent_request_limit: usize,
158
159 pub enable_response_compression: bool,
161
162 pub enable_metrics: bool,
164
165 pub metrics_port: u16,
167}
168
169impl Default for ServerConfig {
170 fn default() -> Self {
171 Self {
172 name: "crates-docs".to_string(),
173 version: crate::VERSION.to_string(),
174 description: Some(
175 "High-performance Rust crate documentation query MCP server".to_string(),
176 ),
177 icons: default_icons(),
178 website_url: Some("https://github.com/KingingWang/crates-docs".to_string()),
179 host: "127.0.0.1".to_string(),
180 port: 8080,
181 transport_mode: "hybrid".to_string(),
182 enable_sse: true,
183 enable_oauth: false,
184 max_connections: 100,
185 request_timeout_secs: 30,
186 response_timeout_secs: 60,
187 allowed_hosts: vec!["localhost".to_string(), "127.0.0.1".to_string()],
189 allowed_origins: vec!["http://localhost:*".to_string()],
190 }
191 }
192}
193
194impl Default for LoggingConfig {
195 fn default() -> Self {
196 Self {
197 level: "info".to_string(),
198 file_path: Some("./logs/crates-docs.log".to_string()),
199 enable_console: true,
200 enable_file: false, max_file_size_mb: 100,
202 max_files: 10,
203 }
204 }
205}
206
207impl Default for PerformanceConfig {
208 fn default() -> Self {
209 Self {
210 http_client_pool_size: 10,
211 http_client_pool_idle_timeout_secs: 90,
212 http_client_connect_timeout_secs: 10,
213 http_client_timeout_secs: 30,
214 http_client_read_timeout_secs: 30,
215 http_client_max_retries: 3,
216 http_client_retry_initial_delay_ms: 100,
217 http_client_retry_max_delay_ms: 10000,
218 cache_max_size: 1000,
219 cache_default_ttl_secs: 3600,
220 rate_limit_per_second: 100,
221 concurrent_request_limit: 50,
222 enable_response_compression: true,
223 enable_metrics: true,
224 metrics_port: 0,
225 }
226 }
227}
228
229impl AppConfig {
230 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, crate::error::Error> {
236 let content = fs::read_to_string(path)
237 .map_err(|e| crate::error::Error::Config(format!("Failed to read config file: {e}")))?;
238
239 let config: Self = toml::from_str(&content).map_err(|e| {
240 crate::error::Error::Config(format!("Failed to parse config file: {e}"))
241 })?;
242
243 config.validate()?;
244 Ok(config)
245 }
246
247 pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), crate::error::Error> {
253 let content = toml::to_string_pretty(self).map_err(|e| {
254 crate::error::Error::Config(format!("Failed to serialize configuration: {e}"))
255 })?;
256
257 if let Some(parent) = path.as_ref().parent() {
259 fs::create_dir_all(parent).map_err(|e| {
260 crate::error::Error::Config(format!("Failed to create directory: {e}"))
261 })?;
262 }
263
264 fs::write(path, content).map_err(|e| {
265 crate::error::Error::Config(format!("Failed to write config file: {e}"))
266 })?;
267
268 Ok(())
269 }
270
271 pub fn validate(&self) -> Result<(), crate::error::Error> {
277 if self.server.host.is_empty() {
279 return Err(crate::error::Error::Config(
280 "Server host cannot be empty".to_string(),
281 ));
282 }
283
284 if self.server.port == 0 {
285 return Err(crate::error::Error::Config(
286 "Server port cannot be 0".to_string(),
287 ));
288 }
289
290 if self.server.max_connections == 0 {
291 return Err(crate::error::Error::Config(
292 "Maximum connections cannot be 0".to_string(),
293 ));
294 }
295
296 let valid_modes = ["stdio", "http", "sse", "hybrid"];
298 if !valid_modes.contains(&self.server.transport_mode.as_str()) {
299 return Err(crate::error::Error::Config(format!(
300 "Invalid transport mode: {}, valid values: {:?}",
301 self.server.transport_mode, valid_modes
302 )));
303 }
304
305 let valid_levels = ["trace", "debug", "info", "warn", "error"];
307 if !valid_levels.contains(&self.logging.level.as_str()) {
308 return Err(crate::error::Error::Config(format!(
309 "Invalid log level: {}, valid values: {:?}",
310 self.logging.level, valid_levels
311 )));
312 }
313
314 if self.performance.http_client_pool_size == 0 {
316 return Err(crate::error::Error::Config(
317 "HTTP client connection pool size cannot be 0".to_string(),
318 ));
319 }
320
321 if self.performance.http_client_pool_idle_timeout_secs == 0 {
322 return Err(crate::error::Error::Config(
323 "HTTP client pool idle timeout cannot be 0".to_string(),
324 ));
325 }
326
327 if self.performance.http_client_connect_timeout_secs == 0 {
328 return Err(crate::error::Error::Config(
329 "HTTP client connection timeout cannot be 0".to_string(),
330 ));
331 }
332
333 if self.performance.http_client_timeout_secs == 0 {
334 return Err(crate::error::Error::Config(
335 "HTTP client request timeout cannot be 0".to_string(),
336 ));
337 }
338
339 if self.performance.cache_max_size == 0 {
340 return Err(crate::error::Error::Config(
341 "Maximum cache size cannot be 0".to_string(),
342 ));
343 }
344
345 if self.server.enable_oauth {
347 self.oauth.validate()?;
348 }
349
350 Ok(())
351 }
352
353 pub fn from_env() -> Result<Self, crate::error::Error> {
359 let mut config = Self::default();
360
361 if let Ok(name) = std::env::var("CRATES_DOCS_NAME") {
363 config.server.name = name;
364 }
365
366 if let Ok(host) = std::env::var("CRATES_DOCS_HOST") {
367 config.server.host = host;
368 }
369
370 if let Ok(port) = std::env::var("CRATES_DOCS_PORT") {
371 config.server.port = port
372 .parse()
373 .map_err(|e| crate::error::Error::Config(format!("Invalid port: {e}")))?;
374 }
375
376 if let Ok(mode) = std::env::var("CRATES_DOCS_TRANSPORT_MODE") {
377 config.server.transport_mode = mode;
378 }
379
380 if let Ok(level) = std::env::var("CRATES_DOCS_LOG_LEVEL") {
381 config.logging.level = level;
382 }
383
384 if let Ok(enable_console) = std::env::var("CRATES_DOCS_ENABLE_CONSOLE") {
385 config.logging.enable_console = enable_console.parse().unwrap_or(true);
386 }
387
388 if let Ok(enable_file) = std::env::var("CRATES_DOCS_ENABLE_FILE") {
389 config.logging.enable_file = enable_file.parse().unwrap_or(true);
390 }
391
392 config.validate()?;
393 Ok(config)
394 }
395
396 #[must_use]
398 pub fn merge(file_config: Option<Self>, env_config: Option<Self>) -> Self {
399 let mut config = Self::default();
400
401 if let Some(file) = file_config {
403 config = file;
404 }
405
406 if let Some(env) = env_config {
408 if env.server.name != "crates-docs" {
410 config.server.name = env.server.name;
411 }
412 if env.server.host != "127.0.0.1" {
413 config.server.host = env.server.host;
414 }
415 if env.server.port != 8080 {
416 config.server.port = env.server.port;
417 }
418 if env.server.transport_mode != "hybrid" {
419 config.server.transport_mode = env.server.transport_mode;
420 }
421
422 if env.logging.level != "info" {
424 config.logging.level = env.logging.level;
425 }
426 }
427
428 config
429 }
430}