1use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::path::PathBuf;
6use std::time::Duration;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct ServerConfig {
11 pub name: String,
13 pub version: String,
15 pub description: Option<String>,
17 pub bind_address: String,
19 pub port: u16,
21 pub enable_tls: bool,
23 pub tls: Option<TlsConfig>,
25 pub timeouts: TimeoutConfig,
27 pub rate_limiting: RateLimitingConfig,
29 pub logging: LoggingConfig,
31 pub additional: HashMap<String, serde_json::Value>,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct TlsConfig {
38 pub cert_file: PathBuf,
40 pub key_file: PathBuf,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct TimeoutConfig {
47 pub request_timeout: Duration,
49 pub connection_timeout: Duration,
51 pub keep_alive_timeout: Duration,
53 pub tool_execution_timeout: Duration,
55 pub tool_timeouts: HashMap<String, u64>,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct RateLimitingConfig {
62 pub enabled: bool,
64 pub requests_per_second: u32,
66 pub burst_capacity: u32,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct LoggingConfig {
73 pub level: String,
75 pub structured: bool,
77 pub file: Option<PathBuf>,
79}
80
81impl Default for ServerConfig {
82 fn default() -> Self {
83 Self {
84 name: crate::SERVER_NAME.to_string(),
85 version: crate::SERVER_VERSION.to_string(),
86 description: Some("Next generation MCP server".to_string()),
87 bind_address: "127.0.0.1".to_string(),
88 port: 8080,
89 enable_tls: false,
90 tls: None,
91 timeouts: TimeoutConfig::default(),
92 rate_limiting: RateLimitingConfig::default(),
93 logging: LoggingConfig::default(),
94 additional: HashMap::new(),
95 }
96 }
97}
98
99#[derive(Debug, thiserror::Error)]
101pub enum ConfigError {
102 #[error("Configuration file not found: {0}")]
104 FileNotFound(PathBuf),
105
106 #[error("Unsupported configuration file format. Use .toml, .yaml, .yml, or .json")]
108 UnsupportedFormat,
109
110 #[error("Failed to parse configuration: {0}")]
112 ParseError(#[from] config::ConfigError),
113
114 #[error("IO error: {0}")]
116 IoError(#[from] std::io::Error),
117}
118
119impl ServerConfig {
120 pub fn from_file(path: impl AsRef<std::path::Path>) -> Result<Self, ConfigError> {
145 use config::{Config, File, FileFormat};
146
147 let path = path.as_ref();
148
149 if !path.exists() {
151 return Err(ConfigError::FileNotFound(path.to_path_buf()));
152 }
153
154 let format = match path.extension().and_then(|s| s.to_str()) {
156 Some("toml") => FileFormat::Toml,
157 Some("yaml") | Some("yml") => FileFormat::Yaml,
158 Some("json") => FileFormat::Json,
159 _ => return Err(ConfigError::UnsupportedFormat),
160 };
161
162 let config = Config::builder()
164 .add_source(File::new(
165 path.to_str()
166 .ok_or_else(|| ConfigError::UnsupportedFormat)?,
167 format,
168 ))
169 .add_source(
171 config::Environment::with_prefix("TURBOMCP")
172 .separator("__") .try_parsing(true),
174 )
175 .build()?;
176
177 Ok(config.try_deserialize()?)
178 }
179
180 pub fn from_file_with_prefix(
192 path: impl AsRef<std::path::Path>,
193 env_prefix: &str,
194 ) -> Result<Self, ConfigError> {
195 use config::{Config, File, FileFormat};
196
197 let path = path.as_ref();
198
199 if !path.exists() {
200 return Err(ConfigError::FileNotFound(path.to_path_buf()));
201 }
202
203 let format = match path.extension().and_then(|s| s.to_str()) {
204 Some("toml") => FileFormat::Toml,
205 Some("yaml") | Some("yml") => FileFormat::Yaml,
206 Some("json") => FileFormat::Json,
207 _ => return Err(ConfigError::UnsupportedFormat),
208 };
209
210 let config = Config::builder()
211 .add_source(File::new(
212 path.to_str()
213 .ok_or_else(|| ConfigError::UnsupportedFormat)?,
214 format,
215 ))
216 .add_source(
217 config::Environment::with_prefix(env_prefix)
218 .separator("__")
219 .try_parsing(true),
220 )
221 .build()?;
222
223 Ok(config.try_deserialize()?)
224 }
225
226 pub fn builder() -> ConfigurationBuilder {
241 ConfigurationBuilder::new()
242 }
243}
244
245impl Default for TimeoutConfig {
246 fn default() -> Self {
247 Self {
248 request_timeout: Duration::from_secs(30),
249 connection_timeout: Duration::from_secs(10),
250 keep_alive_timeout: Duration::from_secs(60),
251 tool_execution_timeout: Duration::from_secs(120), tool_timeouts: HashMap::new(), }
254 }
255}
256
257impl Default for RateLimitingConfig {
258 fn default() -> Self {
259 Self {
260 enabled: true,
261 requests_per_second: 100,
262 burst_capacity: 200,
263 }
264 }
265}
266
267impl Default for LoggingConfig {
268 fn default() -> Self {
269 Self {
270 level: "info".to_string(),
271 structured: true,
272 file: None,
273 }
274 }
275}
276
277#[derive(Debug)]
279pub struct ConfigurationBuilder {
280 config: ServerConfig,
282}
283
284impl ConfigurationBuilder {
285 #[must_use]
287 pub fn new() -> Self {
288 Self {
289 config: ServerConfig::default(),
290 }
291 }
292
293 pub fn name(mut self, name: impl Into<String>) -> Self {
295 self.config.name = name.into();
296 self
297 }
298
299 pub fn version(mut self, version: impl Into<String>) -> Self {
301 self.config.version = version.into();
302 self
303 }
304
305 pub fn description(mut self, description: impl Into<String>) -> Self {
307 self.config.description = Some(description.into());
308 self
309 }
310
311 pub fn bind_address(mut self, address: impl Into<String>) -> Self {
313 self.config.bind_address = address.into();
314 self
315 }
316
317 #[must_use]
319 pub const fn port(mut self, port: u16) -> Self {
320 self.config.port = port;
321 self
322 }
323
324 #[must_use]
326 pub fn tls(mut self, cert_file: PathBuf, key_file: PathBuf) -> Self {
327 self.config.enable_tls = true;
328 self.config.tls = Some(TlsConfig {
329 cert_file,
330 key_file,
331 });
332 self
333 }
334
335 #[must_use]
337 pub const fn request_timeout(mut self, timeout: Duration) -> Self {
338 self.config.timeouts.request_timeout = timeout;
339 self
340 }
341
342 #[must_use]
344 pub const fn rate_limiting(mut self, requests_per_second: u32, burst_capacity: u32) -> Self {
345 self.config.rate_limiting.enabled = true;
346 self.config.rate_limiting.requests_per_second = requests_per_second;
347 self.config.rate_limiting.burst_capacity = burst_capacity;
348 self
349 }
350
351 pub fn log_level(mut self, level: impl Into<String>) -> Self {
353 self.config.logging.level = level.into();
354 self
355 }
356
357 #[must_use]
359 pub fn build(self) -> ServerConfig {
360 self.config
361 }
362}
363
364impl Default for ConfigurationBuilder {
365 fn default() -> Self {
366 Self::new()
367 }
368}
369
370pub type Configuration = ServerConfig;
372#[cfg(test)]
373mod inline_tests {
374 use super::*;
375
376 #[test]
377 fn test_default_config() {
378 let config = ServerConfig::default();
379 assert_eq!(config.name, crate::SERVER_NAME);
380 assert_eq!(config.version, crate::SERVER_VERSION);
381 assert_eq!(config.bind_address, "127.0.0.1");
382 assert_eq!(config.port, 8080);
383 assert!(!config.enable_tls);
384 }
385
386 #[test]
387 fn test_config_builder() {
388 let config = ConfigurationBuilder::new()
389 .name("test-server")
390 .port(9000)
391 .build();
392
393 assert_eq!(config.name, "test-server");
394 assert_eq!(config.port, 9000);
395 }
396
397 mod proptest_tests {
399 use super::*;
400 use proptest::prelude::*;
401
402 proptest! {
403 #[test]
405 fn test_config_port_roundtrip(port in 1024u16..65535u16) {
406 let config = ConfigurationBuilder::new()
407 .port(port)
408 .build();
409
410 prop_assert_eq!(config.port, port);
411 }
412
413 #[test]
415 fn test_config_name_preservation(name in "[a-zA-Z0-9_-]{1,50}") {
416 let config = ConfigurationBuilder::new()
417 .name(&name)
418 .build();
419
420 prop_assert_eq!(config.name, name);
421 }
422
423 #[test]
425 fn test_rate_limiting_config(
426 rps in 1u32..10000u32,
427 burst in 1u32..1000u32
428 ) {
429 let config = RateLimitingConfig {
430 enabled: true,
431 requests_per_second: rps,
432 burst_capacity: burst,
433 };
434
435 prop_assert!(config.requests_per_second >= 1);
437 prop_assert!(config.burst_capacity >= 1);
438 }
439 }
440 }
441}
442
443#[derive(Debug, Clone, Serialize, Deserialize)]
447#[cfg(feature = "websocket")]
448pub struct WebSocketServerConfig {
449 pub bind_addr: String,
451 pub endpoint_path: String,
453 pub max_concurrent_requests: usize,
455}
456
457#[cfg(feature = "websocket")]
458impl Default for WebSocketServerConfig {
459 fn default() -> Self {
460 Self {
461 bind_addr: "127.0.0.1:8080".to_string(),
462 endpoint_path: "/ws".to_string(),
463 max_concurrent_requests: 100,
464 }
465 }
466}
467
468#[cfg(test)]
470mod tests;