turbomcp_server/
config.rs

1//! Server configuration management
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::path::PathBuf;
6use std::time::Duration;
7
8/// Protocol version configuration for MCP version negotiation
9///
10/// # Version Negotiation Behavior
11///
12/// When a client connects, the server negotiates protocol version as follows:
13///
14/// 1. If client requests a version in `supported` list → use client's version
15/// 2. If `allow_fallback` is true and client's version not supported → offer `preferred`
16/// 3. If `allow_fallback` is false → reject connection if versions don't match
17///
18/// # Examples
19///
20/// ```rust
21/// use turbomcp_server::ProtocolVersionConfig;
22///
23/// // Default: Latest spec with fallback enabled
24/// let config = ProtocolVersionConfig::default();
25///
26/// // Strict: Only accept latest spec, no fallback
27/// let config = ProtocolVersionConfig::strict("2025-11-25");
28///
29/// // Compatible: Prefer older version for maximum compatibility
30/// let config = ProtocolVersionConfig::compatible();
31/// ```
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct ProtocolVersionConfig {
34    /// Preferred protocol version (default: "2025-11-25" - latest official spec)
35    pub preferred: String,
36
37    /// Supported versions in fallback order (first = most preferred)
38    /// Server will accept any of these versions from clients
39    pub supported: Vec<String>,
40
41    /// Allow fallback negotiation when client requests unsupported version
42    /// - true: Offer preferred version as fallback (client can accept or disconnect)
43    /// - false: Reject connection immediately if client version not in supported list
44    pub allow_fallback: bool,
45}
46
47impl ProtocolVersionConfig {
48    /// Create config with latest spec as preferred, fallback enabled
49    pub fn latest() -> Self {
50        Self {
51            preferred: "2025-11-25".to_string(),
52            supported: turbomcp_protocol::SUPPORTED_VERSIONS
53                .iter()
54                .map(|s| (*s).to_string())
55                .collect(),
56            allow_fallback: true,
57        }
58    }
59
60    /// Create config optimized for Claude Code compatibility
61    /// Prefers 2025-06-18 but supports all versions
62    pub fn compatible() -> Self {
63        Self {
64            preferred: "2025-06-18".to_string(),
65            supported: vec![
66                "2025-06-18".to_string(),
67                "2025-11-25".to_string(),
68                "2025-03-26".to_string(),
69                "2024-11-05".to_string(),
70            ],
71            allow_fallback: true,
72        }
73    }
74
75    /// Create strict config - only accept the specified version, no fallback
76    pub fn strict(version: impl Into<String>) -> Self {
77        let version = version.into();
78        Self {
79            preferred: version.clone(),
80            supported: vec![version],
81            allow_fallback: false,
82        }
83    }
84
85    /// Create custom config with specified versions
86    pub fn custom(preferred: impl Into<String>, supported: Vec<impl Into<String>>) -> Self {
87        Self {
88            preferred: preferred.into(),
89            supported: supported.into_iter().map(Into::into).collect(),
90            allow_fallback: true,
91        }
92    }
93}
94
95impl Default for ProtocolVersionConfig {
96    fn default() -> Self {
97        Self::latest()
98    }
99}
100
101/// Server configuration
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct ServerConfig {
104    /// Server name
105    pub name: String,
106    /// Server version
107    pub version: String,
108    /// Server description
109    pub description: Option<String>,
110    /// Bind address
111    pub bind_address: String,
112    /// Bind port
113    pub port: u16,
114    /// Enable TLS
115    pub enable_tls: bool,
116    /// TLS configuration
117    pub tls: Option<TlsConfig>,
118    /// Protocol version configuration
119    pub protocol_version: ProtocolVersionConfig,
120    /// Timeout configuration
121    pub timeouts: TimeoutConfig,
122    /// Rate limiting configuration
123    pub rate_limiting: RateLimitingConfig,
124    /// Logging configuration
125    pub logging: LoggingConfig,
126    /// Additional configuration
127    pub additional: HashMap<String, serde_json::Value>,
128}
129
130/// TLS configuration
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct TlsConfig {
133    /// Certificate file path
134    pub cert_file: PathBuf,
135    /// Private key file path
136    pub key_file: PathBuf,
137}
138
139/// Timeout configuration
140#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct TimeoutConfig {
142    /// Request timeout
143    pub request_timeout: Duration,
144    /// Connection timeout
145    pub connection_timeout: Duration,
146    /// Keep-alive timeout
147    pub keep_alive_timeout: Duration,
148    /// Tool execution timeout (default for all tools)
149    pub tool_execution_timeout: Duration,
150    /// Per-tool timeout overrides (tool_name -> duration_seconds)
151    pub tool_timeouts: HashMap<String, u64>,
152}
153
154/// Rate limiting configuration
155#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct RateLimitingConfig {
157    /// Enable rate limiting
158    pub enabled: bool,
159    /// Requests per second
160    pub requests_per_second: u32,
161    /// Burst capacity
162    pub burst_capacity: u32,
163}
164
165/// Logging configuration
166#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct LoggingConfig {
168    /// Log level
169    pub level: String,
170    /// Enable structured logging
171    pub structured: bool,
172    /// Log file path
173    pub file: Option<PathBuf>,
174}
175
176impl Default for ServerConfig {
177    fn default() -> Self {
178        Self {
179            name: crate::SERVER_NAME.to_string(),
180            version: crate::SERVER_VERSION.to_string(),
181            description: Some("Next generation MCP server".to_string()),
182            bind_address: "127.0.0.1".to_string(),
183            port: 8080,
184            enable_tls: false,
185            tls: None,
186            protocol_version: ProtocolVersionConfig::default(),
187            timeouts: TimeoutConfig::default(),
188            rate_limiting: RateLimitingConfig::default(),
189            logging: LoggingConfig::default(),
190            additional: HashMap::new(),
191        }
192    }
193}
194
195/// Configuration error types
196#[derive(Debug, thiserror::Error)]
197pub enum ConfigError {
198    /// Config file not found
199    #[error("Configuration file not found: {0}")]
200    FileNotFound(PathBuf),
201
202    /// Unsupported file format
203    #[error("Unsupported configuration file format. Use .toml, .yaml, .yml, or .json")]
204    UnsupportedFormat,
205
206    /// Configuration parsing error
207    #[error("Failed to parse configuration: {0}")]
208    ParseError(#[from] config::ConfigError),
209
210    /// IO error
211    #[error("IO error: {0}")]
212    IoError(#[from] std::io::Error),
213}
214
215impl ServerConfig {
216    /// Load configuration from a file (TOML, YAML, or JSON)
217    ///
218    /// The file format is auto-detected from the file extension:
219    /// - `.toml` → TOML format
220    /// - `.yaml` or `.yml` → YAML format
221    /// - `.json` → JSON format
222    ///
223    /// Environment variables with the `TURBOMCP_` prefix will override file settings.
224    /// For example, `TURBOMCP_PORT=9000` will override the `port` setting.
225    ///
226    /// # Example
227    ///
228    /// ```rust,no_run
229    /// use turbomcp_server::ServerConfig;
230    ///
231    /// let config = ServerConfig::from_file("config.toml").expect("Failed to load config");
232    /// ```
233    ///
234    /// # Errors
235    ///
236    /// Returns an error if:
237    /// - The file doesn't exist
238    /// - The file format is unsupported
239    /// - The file contains invalid configuration
240    pub fn from_file(path: impl AsRef<std::path::Path>) -> Result<Self, ConfigError> {
241        use config::{Config, File, FileFormat};
242
243        let path = path.as_ref();
244
245        // Check if file exists
246        if !path.exists() {
247            return Err(ConfigError::FileNotFound(path.to_path_buf()));
248        }
249
250        // Determine file format from extension
251        let format = match path.extension().and_then(|s| s.to_str()) {
252            Some("toml") => FileFormat::Toml,
253            Some("yaml") | Some("yml") => FileFormat::Yaml,
254            Some("json") => FileFormat::Json,
255            _ => return Err(ConfigError::UnsupportedFormat),
256        };
257
258        // Build configuration with file + environment variables
259        let config = Config::builder()
260            .add_source(File::new(
261                path.to_str()
262                    .ok_or_else(|| ConfigError::UnsupportedFormat)?,
263                format,
264            ))
265            // Environment variables override file settings (12-factor app pattern)
266            .add_source(
267                config::Environment::with_prefix("TURBOMCP")
268                    .separator("__") // Use __ for nested config (e.g., TURBOMCP_TIMEOUTS__REQUEST_TIMEOUT)
269                    .try_parsing(true),
270            )
271            .build()?;
272
273        Ok(config.try_deserialize()?)
274    }
275
276    /// Load configuration from a file with custom environment prefix
277    ///
278    /// # Example
279    ///
280    /// ```rust,no_run
281    /// use turbomcp_server::ServerConfig;
282    ///
283    /// // Use MYAPP_PORT instead of TURBOMCP_PORT
284    /// let config = ServerConfig::from_file_with_prefix("config.toml", "MYAPP")
285    ///     .expect("Failed to load config");
286    /// ```
287    pub fn from_file_with_prefix(
288        path: impl AsRef<std::path::Path>,
289        env_prefix: &str,
290    ) -> Result<Self, ConfigError> {
291        use config::{Config, File, FileFormat};
292
293        let path = path.as_ref();
294
295        if !path.exists() {
296            return Err(ConfigError::FileNotFound(path.to_path_buf()));
297        }
298
299        let format = match path.extension().and_then(|s| s.to_str()) {
300            Some("toml") => FileFormat::Toml,
301            Some("yaml") | Some("yml") => FileFormat::Yaml,
302            Some("json") => FileFormat::Json,
303            _ => return Err(ConfigError::UnsupportedFormat),
304        };
305
306        let config = Config::builder()
307            .add_source(File::new(
308                path.to_str()
309                    .ok_or_else(|| ConfigError::UnsupportedFormat)?,
310                format,
311            ))
312            .add_source(
313                config::Environment::with_prefix(env_prefix)
314                    .separator("__")
315                    .try_parsing(true),
316            )
317            .build()?;
318
319        Ok(config.try_deserialize()?)
320    }
321
322    /// Create a configuration builder
323    ///
324    /// Use this for programmatic configuration without files.
325    ///
326    /// # Example
327    ///
328    /// ```rust
329    /// use turbomcp_server::ServerConfig;
330    ///
331    /// let config = ServerConfig::builder()
332    ///     .name("my-server")
333    ///     .port(9000)
334    ///     .build();
335    /// ```
336    pub fn builder() -> ConfigurationBuilder {
337        ConfigurationBuilder::new()
338    }
339}
340
341impl Default for TimeoutConfig {
342    fn default() -> Self {
343        Self {
344            request_timeout: Duration::from_secs(30),
345            connection_timeout: Duration::from_secs(10),
346            keep_alive_timeout: Duration::from_secs(60),
347            tool_execution_timeout: Duration::from_secs(120), // 2 minutes default for tools
348            tool_timeouts: HashMap::new(),                    // No per-tool overrides by default
349        }
350    }
351}
352
353impl Default for RateLimitingConfig {
354    fn default() -> Self {
355        Self {
356            enabled: true,
357            requests_per_second: 100,
358            burst_capacity: 200,
359        }
360    }
361}
362
363impl Default for LoggingConfig {
364    fn default() -> Self {
365        Self {
366            level: "info".to_string(),
367            structured: true,
368            file: None,
369        }
370    }
371}
372
373/// Configuration builder
374#[derive(Debug)]
375pub struct ConfigurationBuilder {
376    /// Configuration being built
377    config: ServerConfig,
378}
379
380impl ConfigurationBuilder {
381    /// Create a new configuration builder
382    #[must_use]
383    pub fn new() -> Self {
384        Self {
385            config: ServerConfig::default(),
386        }
387    }
388
389    /// Set server name
390    pub fn name(mut self, name: impl Into<String>) -> Self {
391        self.config.name = name.into();
392        self
393    }
394
395    /// Set server version
396    pub fn version(mut self, version: impl Into<String>) -> Self {
397        self.config.version = version.into();
398        self
399    }
400
401    /// Set server description
402    pub fn description(mut self, description: impl Into<String>) -> Self {
403        self.config.description = Some(description.into());
404        self
405    }
406
407    /// Set bind address
408    pub fn bind_address(mut self, address: impl Into<String>) -> Self {
409        self.config.bind_address = address.into();
410        self
411    }
412
413    /// Set port
414    #[must_use]
415    pub const fn port(mut self, port: u16) -> Self {
416        self.config.port = port;
417        self
418    }
419
420    /// Enable TLS with configuration
421    #[must_use]
422    pub fn tls(mut self, cert_file: PathBuf, key_file: PathBuf) -> Self {
423        self.config.enable_tls = true;
424        self.config.tls = Some(TlsConfig {
425            cert_file,
426            key_file,
427        });
428        self
429    }
430
431    /// Set request timeout
432    #[must_use]
433    pub const fn request_timeout(mut self, timeout: Duration) -> Self {
434        self.config.timeouts.request_timeout = timeout;
435        self
436    }
437
438    /// Enable rate limiting
439    #[must_use]
440    pub const fn rate_limiting(mut self, requests_per_second: u32, burst_capacity: u32) -> Self {
441        self.config.rate_limiting.enabled = true;
442        self.config.rate_limiting.requests_per_second = requests_per_second;
443        self.config.rate_limiting.burst_capacity = burst_capacity;
444        self
445    }
446
447    /// Set log level
448    pub fn log_level(mut self, level: impl Into<String>) -> Self {
449        self.config.logging.level = level.into();
450        self
451    }
452
453    /// Set preferred MCP protocol version
454    ///
455    /// Default is "2025-11-25" (latest official MCP spec).
456    /// Use "2025-06-18" for Claude Code compatibility.
457    ///
458    /// # Example
459    ///
460    /// ```rust
461    /// use turbomcp_server::ServerConfig;
462    ///
463    /// // Use older spec for compatibility
464    /// let config = ServerConfig::builder()
465    ///     .protocol_version("2025-06-18")
466    ///     .build();
467    /// ```
468    pub fn protocol_version(mut self, version: impl Into<String>) -> Self {
469        self.config.protocol_version.preferred = version.into();
470        self
471    }
472
473    /// Set supported MCP protocol versions in fallback order
474    ///
475    /// The server will accept any of these versions from clients.
476    /// Order matters: first version is most preferred for fallback.
477    ///
478    /// # Example
479    ///
480    /// ```rust
481    /// use turbomcp_server::ServerConfig;
482    ///
483    /// // Support specific versions with custom fallback order
484    /// let config = ServerConfig::builder()
485    ///     .supported_protocol_versions(vec!["2025-11-25", "2025-06-18"])
486    ///     .build();
487    /// ```
488    pub fn supported_protocol_versions(mut self, versions: Vec<impl Into<String>>) -> Self {
489        self.config.protocol_version.supported = versions.into_iter().map(Into::into).collect();
490        self
491    }
492
493    /// Enable/disable protocol version fallback negotiation
494    ///
495    /// - `true` (default): If client requests unsupported version, offer preferred version
496    /// - `false`: Reject connection if client version not in supported list
497    ///
498    /// # Example
499    ///
500    /// ```rust
501    /// use turbomcp_server::ServerConfig;
502    ///
503    /// // Disable fallback - strict version matching only
504    /// let config = ServerConfig::builder()
505    ///     .protocol_version("2025-11-25")
506    ///     .allow_protocol_fallback(false)
507    ///     .build();
508    /// ```
509    #[must_use]
510    pub const fn allow_protocol_fallback(mut self, allow: bool) -> Self {
511        self.config.protocol_version.allow_fallback = allow;
512        self
513    }
514
515    /// Use pre-configured protocol version settings
516    ///
517    /// # Example
518    ///
519    /// ```rust
520    /// use turbomcp_server::{ServerConfig, ProtocolVersionConfig};
521    ///
522    /// // Use Claude Code compatible settings
523    /// let config = ServerConfig::builder()
524    ///     .protocol_version_config(ProtocolVersionConfig::compatible())
525    ///     .build();
526    ///
527    /// // Use strict mode for specific version
528    /// let config = ServerConfig::builder()
529    ///     .protocol_version_config(ProtocolVersionConfig::strict("2025-11-25"))
530    ///     .build();
531    /// ```
532    #[must_use]
533    pub fn protocol_version_config(mut self, config: ProtocolVersionConfig) -> Self {
534        self.config.protocol_version = config;
535        self
536    }
537
538    /// Build the configuration
539    #[must_use]
540    pub fn build(self) -> ServerConfig {
541        self.config
542    }
543}
544
545impl Default for ConfigurationBuilder {
546    fn default() -> Self {
547        Self::new()
548    }
549}
550
551/// Configuration alias for convenience
552pub type Configuration = ServerConfig;
553#[cfg(test)]
554mod inline_tests {
555    use super::*;
556
557    #[test]
558    fn test_default_config() {
559        let config = ServerConfig::default();
560        assert_eq!(config.name, crate::SERVER_NAME);
561        assert_eq!(config.version, crate::SERVER_VERSION);
562        assert_eq!(config.bind_address, "127.0.0.1");
563        assert_eq!(config.port, 8080);
564        assert!(!config.enable_tls);
565    }
566
567    #[test]
568    fn test_config_builder() {
569        let config = ConfigurationBuilder::new()
570            .name("test-server")
571            .port(9000)
572            .build();
573
574        assert_eq!(config.name, "test-server");
575        assert_eq!(config.port, 9000);
576    }
577
578    // Property-based tests
579    mod proptest_tests {
580        use super::*;
581        use proptest::prelude::*;
582
583        proptest! {
584            /// Test that config serialization roundtrips correctly for any valid port
585            #[test]
586            fn test_config_port_roundtrip(port in 1024u16..65535u16) {
587                let config = ConfigurationBuilder::new()
588                    .port(port)
589                    .build();
590
591                prop_assert_eq!(config.port, port);
592            }
593
594            /// Test that server name is preserved through builder
595            #[test]
596            fn test_config_name_preservation(name in "[a-zA-Z0-9_-]{1,50}") {
597                let config = ConfigurationBuilder::new()
598                    .name(&name)
599                    .build();
600
601                prop_assert_eq!(config.name, name);
602            }
603
604            /// Test rate limiting configuration validity
605            #[test]
606            fn test_rate_limiting_config(
607                rps in 1u32..10000u32,
608                burst in 1u32..1000u32
609            ) {
610                let config = RateLimitingConfig {
611                    enabled: true,
612                    requests_per_second: rps,
613                    burst_capacity: burst,
614                };
615
616                // Verify values are within bounds
617                prop_assert!(config.requests_per_second >= 1);
618                prop_assert!(config.burst_capacity >= 1);
619            }
620        }
621    }
622}
623
624/// WebSocket server configuration
625///
626/// Configuration for WebSocket transport when using `run_websocket_with_config()`.
627#[derive(Debug, Clone, Serialize, Deserialize)]
628#[cfg(feature = "websocket")]
629pub struct WebSocketServerConfig {
630    /// Bind address (e.g., "127.0.0.1:8080")
631    pub bind_addr: String,
632    /// WebSocket endpoint path (default: "/ws")
633    pub endpoint_path: String,
634    /// Maximum concurrent request handlers per connection (default: 100)
635    pub max_concurrent_requests: usize,
636}
637
638#[cfg(feature = "websocket")]
639impl Default for WebSocketServerConfig {
640    fn default() -> Self {
641        Self {
642            bind_addr: "127.0.0.1:8080".to_string(),
643            endpoint_path: "/ws".to_string(),
644            max_concurrent_requests: 100,
645        }
646    }
647}
648
649// Multi-tenancy configuration (opt-in feature)
650#[cfg(feature = "multi-tenancy")]
651pub mod multi_tenant;
652#[cfg(feature = "multi-tenancy")]
653pub use multi_tenant::{
654    NoOpTenantConfigProvider, StaticTenantConfigProvider, TenantConfig, TenantConfigProvider,
655};
656
657// Additional comprehensive tests in separate file
658#[cfg(test)]
659mod tests;