turul_mcp_client/
config.rs

1//! Configuration types for MCP client
2
3use serde::{Deserialize, Serialize};
4use std::time::Duration;
5
6/// Main client configuration
7#[derive(Debug, Clone, Serialize, Deserialize, Default)]
8pub struct ClientConfig {
9    /// Client identification information
10    pub client_info: ClientInfo,
11
12    /// Timeout configurations
13    pub timeouts: TimeoutConfig,
14
15    /// Retry configurations
16    pub retry: RetryConfig,
17
18    /// Connection configurations
19    pub connection: ConnectionConfig,
20
21    /// Logging configuration
22    pub logging: LoggingConfig,
23}
24
25/// Client identification information
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct ClientInfo {
28    /// Client name
29    pub name: String,
30
31    /// Client version
32    pub version: String,
33
34    /// Client description
35    pub description: Option<String>,
36
37    /// Vendor information
38    pub vendor: Option<String>,
39
40    /// Additional metadata
41    pub metadata: Option<serde_json::Value>,
42}
43
44/// Timeout configuration
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct TimeoutConfig {
47    /// Connection timeout
48    #[serde(with = "duration_serde")]
49    pub connect: Duration,
50
51    /// Request timeout for individual operations
52    #[serde(with = "duration_serde")]
53    pub request: Duration,
54
55    /// Long operation timeout (for streaming, etc.)
56    #[serde(with = "duration_serde")]
57    pub long_operation: Duration,
58
59    /// Session initialization timeout
60    #[serde(with = "duration_serde")]
61    pub initialization: Duration,
62
63    /// Heartbeat interval for keep-alive
64    #[serde(with = "duration_serde")]
65    pub heartbeat: Duration,
66}
67
68/// Retry configuration
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct RetryConfig {
71    /// Maximum number of retry attempts
72    pub max_attempts: u32,
73
74    /// Initial retry delay
75    #[serde(with = "duration_serde")]
76    pub initial_delay: Duration,
77
78    /// Maximum retry delay
79    #[serde(with = "duration_serde")]
80    pub max_delay: Duration,
81
82    /// Exponential backoff multiplier
83    pub backoff_multiplier: f64,
84
85    /// Jitter factor (0.0 to 1.0)
86    pub jitter: f64,
87
88    /// Whether to enable exponential backoff
89    pub exponential_backoff: bool,
90}
91
92/// Connection configuration
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct ConnectionConfig {
95    /// User agent string
96    pub user_agent: Option<String>,
97
98    /// Custom headers to include in requests
99    pub headers: Option<std::collections::HashMap<String, String>>,
100
101    /// Whether to follow redirects
102    pub follow_redirects: bool,
103
104    /// Maximum number of redirects to follow
105    pub max_redirects: u32,
106
107    /// Keep-alive settings
108    pub keep_alive: bool,
109
110    /// Connection pool settings
111    pub pool_settings: PoolConfig,
112}
113
114/// Connection pool configuration
115#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct PoolConfig {
117    /// Maximum number of idle connections per host
118    pub max_idle_per_host: u32,
119
120    /// Idle connection timeout
121    #[serde(with = "duration_serde")]
122    pub idle_timeout: Duration,
123
124    /// Connection lifetime
125    #[serde(with = "duration_serde")]
126    pub max_lifetime: Duration,
127}
128
129/// Logging configuration
130#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct LoggingConfig {
132    /// Log level
133    pub level: String,
134
135    /// Whether to log requests
136    pub log_requests: bool,
137
138    /// Whether to log responses
139    pub log_responses: bool,
140
141    /// Whether to log transport events
142    pub log_transport: bool,
143
144    /// Whether to redact sensitive information
145    pub redact_sensitive: bool,
146}
147
148impl Default for ClientInfo {
149    fn default() -> Self {
150        Self {
151            name: "mcp-client".to_string(),
152            version: env!("CARGO_PKG_VERSION").to_string(),
153            description: Some("Rust MCP Client Library".to_string()),
154            vendor: Some("MCP Framework".to_string()),
155            metadata: None,
156        }
157    }
158}
159
160impl Default for TimeoutConfig {
161    fn default() -> Self {
162        Self {
163            connect: Duration::from_secs(10),
164            request: Duration::from_secs(30),
165            long_operation: Duration::from_secs(300), // 5 minutes
166            initialization: Duration::from_secs(15),
167            heartbeat: Duration::from_secs(30),
168        }
169    }
170}
171
172impl Default for RetryConfig {
173    fn default() -> Self {
174        Self {
175            max_attempts: 3,
176            initial_delay: Duration::from_millis(100),
177            max_delay: Duration::from_secs(10),
178            backoff_multiplier: 2.0,
179            jitter: 0.1,
180            exponential_backoff: true,
181        }
182    }
183}
184
185impl Default for ConnectionConfig {
186    fn default() -> Self {
187        Self {
188            user_agent: Some(format!("mcp-client/{}", env!("CARGO_PKG_VERSION"))),
189            headers: None,
190            follow_redirects: true,
191            max_redirects: 5,
192            keep_alive: true,
193            pool_settings: PoolConfig::default(),
194        }
195    }
196}
197
198impl Default for PoolConfig {
199    fn default() -> Self {
200        Self {
201            max_idle_per_host: 5,
202            idle_timeout: Duration::from_secs(90),
203            max_lifetime: Duration::from_secs(300),
204        }
205    }
206}
207
208impl Default for LoggingConfig {
209    fn default() -> Self {
210        Self {
211            level: "info".to_string(),
212            log_requests: true,
213            log_responses: true,
214            log_transport: false,
215            redact_sensitive: true,
216        }
217    }
218}
219
220impl RetryConfig {
221    /// Calculate the delay for a given attempt number
222    pub fn delay_for_attempt(&self, attempt: u32) -> Duration {
223        if attempt == 0 {
224            return Duration::from_millis(0);
225        }
226
227        let mut delay = self.initial_delay;
228
229        if self.exponential_backoff && attempt > 1 {
230            // Apply exponential backoff
231            let multiplier = self.backoff_multiplier.powi((attempt - 1) as i32);
232            delay = Duration::from_millis((delay.as_millis() as f64 * multiplier) as u64);
233        }
234
235        // Cap at max delay
236        if delay > self.max_delay {
237            delay = self.max_delay;
238        }
239
240        // Apply jitter
241        if self.jitter > 0.0 {
242            let jitter_ms = (delay.as_millis() as f64 * self.jitter) as u64;
243            let random_offset = rand::random::<f64>() * jitter_ms as f64;
244            delay = Duration::from_millis(delay.as_millis() as u64 + random_offset as u64);
245        }
246
247        // Ensure final delay never exceeds max_delay (even after jitter)
248        if delay > self.max_delay {
249            delay = self.max_delay;
250        }
251
252        delay
253    }
254
255    /// Check if an attempt should be retried
256    pub fn should_retry(&self, attempt: u32) -> bool {
257        attempt < self.max_attempts
258    }
259}
260
261// Helper module for Duration serialization
262mod duration_serde {
263    use serde::{Deserialize, Deserializer, Serializer};
264    use std::time::Duration;
265
266    pub fn serialize<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
267    where
268        S: Serializer,
269    {
270        serializer.serialize_u64(duration.as_millis() as u64)
271    }
272
273    pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
274    where
275        D: Deserializer<'de>,
276    {
277        let millis = u64::deserialize(deserializer)?;
278        Ok(Duration::from_millis(millis))
279    }
280}
281
282#[cfg(test)]
283mod tests {
284    use super::*;
285
286    #[test]
287    fn test_retry_delay_calculation() {
288        let config = RetryConfig::default();
289
290        // First attempt should have no delay
291        assert_eq!(config.delay_for_attempt(0), Duration::from_millis(0));
292
293        // Second attempt should have initial delay
294        let delay1 = config.delay_for_attempt(1);
295        assert!(delay1 >= config.initial_delay);
296
297        // Third attempt should be longer with exponential backoff
298        let delay2 = config.delay_for_attempt(2);
299        assert!(delay2 > delay1);
300
301        // Should not exceed max delay
302        let large_delay = config.delay_for_attempt(20);
303        assert!(large_delay <= config.max_delay);
304    }
305
306    #[test]
307    fn test_retry_attempts() {
308        let config = RetryConfig::default();
309
310        assert!(config.should_retry(0));
311        assert!(config.should_retry(1));
312        assert!(config.should_retry(2));
313        assert!(!config.should_retry(3)); // Default max is 3
314    }
315
316    #[test]
317    fn test_config_serialization() {
318        let config = ClientConfig::default();
319        let json = serde_json::to_string(&config).unwrap();
320        let _deserialized: ClientConfig = serde_json::from_str(&json).unwrap();
321    }
322}