1use crate::error::{ConfigError, McpResult};
31use serde::{Deserialize, Serialize};
32use std::collections::HashMap;
33use std::path::PathBuf;
34use std::time::Duration;
35use url::Url;
36
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
42#[serde(tag = "type", rename_all = "snake_case")]
43pub enum TransportConfig {
44    Stdio(StdioConfig),
46
47    HttpSse(HttpSseConfig),
49
50    HttpStream(HttpStreamConfig),
52}
53
54impl TransportConfig {
55    pub fn stdio(command: impl Into<String>, args: &[impl ToString]) -> Self {
65        Self::Stdio(StdioConfig {
66            command: command.into(),
67            args: args.iter().map(|s| s.to_string()).collect(),
68            working_dir: None,
69            timeout: Duration::from_secs(30),
70            environment: HashMap::new(),
71        })
72    }
73
74    pub fn http_sse(base_url: impl AsRef<str>) -> McpResult<Self> {
84        let url = base_url
85            .as_ref()
86            .parse()
87            .map_err(|e| ConfigError::InvalidValue {
88                parameter: "base_url".to_string(),
89                value: base_url.as_ref().to_string(),
90                reason: format!("Invalid URL: {}", e),
91            })?;
92
93        Ok(Self::HttpSse(HttpSseConfig {
94            base_url: url,
95            timeout: Duration::from_secs(60),
96            headers: HashMap::new(),
97            auth: None,
98        }))
99    }
100
101    pub fn http_stream(base_url: impl AsRef<str>) -> McpResult<Self> {
111        let url = base_url
112            .as_ref()
113            .parse()
114            .map_err(|e| ConfigError::InvalidValue {
115                parameter: "base_url".to_string(),
116                value: base_url.as_ref().to_string(),
117                reason: format!("Invalid URL: {}", e),
118            })?;
119
120        Ok(Self::HttpStream(HttpStreamConfig {
121            base_url: url,
122            timeout: Duration::from_secs(300),
123            headers: HashMap::new(),
124            auth: None,
125            compression: true,
126            flow_control_window: 65536,
127        }))
128    }
129
130    pub fn transport_type(&self) -> &'static str {
132        match self {
133            Self::Stdio(_) => "stdio",
134            Self::HttpSse(_) => "http-sse",
135            Self::HttpStream(_) => "http-stream",
136        }
137    }
138
139    pub fn validate(&self) -> McpResult<()> {
141        match self {
142            Self::Stdio(config) => config.validate(),
143            Self::HttpSse(config) => config.validate(),
144            Self::HttpStream(config) => config.validate(),
145        }
146    }
147
148    pub fn from_file(path: impl AsRef<std::path::Path>) -> McpResult<Self> {
161        let path = path.as_ref();
162        let content = std::fs::read_to_string(path).map_err(|_e| ConfigError::FileNotFound {
163            path: path.display().to_string(),
164        })?;
165
166        let config: Self = match path.extension().and_then(|ext| ext.to_str()) {
167            Some("json") => {
168                serde_json::from_str(&content).map_err(|e| ConfigError::InvalidFormat {
169                    path: path.display().to_string(),
170                    reason: e.to_string(),
171                })?
172            }
173            Some("yaml") | Some("yml") => {
174                serde_yaml::from_str(&content).map_err(|e| ConfigError::InvalidFormat {
175                    path: path.display().to_string(),
176                    reason: e.to_string(),
177                })?
178            }
179            Some("toml") => toml::from_str(&content).map_err(|e| ConfigError::InvalidFormat {
180                path: path.display().to_string(),
181                reason: e.to_string(),
182            })?,
183            _ => {
184                return Err(ConfigError::InvalidFormat {
185                    path: path.display().to_string(),
186                    reason: "Unsupported file format. Use .json, .yaml, or .toml".to_string(),
187                }
188                .into())
189            }
190        };
191
192        config.validate()?;
193        Ok(config)
194    }
195
196    pub fn to_file(&self, path: impl AsRef<std::path::Path>) -> McpResult<()> {
208        let path = path.as_ref();
209        let content = match path.extension().and_then(|ext| ext.to_str()) {
210            Some("json") => {
211                serde_json::to_string_pretty(self).map_err(|e| ConfigError::InvalidFormat {
212                    path: path.display().to_string(),
213                    reason: e.to_string(),
214                })?
215            }
216            Some("yaml") | Some("yml") => {
217                serde_yaml::to_string(self).map_err(|e| ConfigError::InvalidFormat {
218                    path: path.display().to_string(),
219                    reason: e.to_string(),
220                })?
221            }
222            Some("toml") => toml::to_string(self).map_err(|e| ConfigError::InvalidFormat {
223                path: path.display().to_string(),
224                reason: e.to_string(),
225            })?,
226            _ => {
227                return Err(ConfigError::InvalidFormat {
228                    path: path.display().to_string(),
229                    reason: "Unsupported file format. Use .json, .yaml, or .toml".to_string(),
230                }
231                .into())
232            }
233        };
234
235        std::fs::write(path, content)?;
236
237        Ok(())
238    }
239}
240
241#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
245pub struct StdioConfig {
246    pub command: String,
248
249    pub args: Vec<String>,
251
252    pub working_dir: Option<String>,
254
255    #[serde(with = "humantime_serde")]
257    pub timeout: Duration,
258
259    pub environment: HashMap<String, String>,
261}
262
263impl StdioConfig {
264    pub fn new(command: impl Into<String>) -> Self {
266        Self {
267            command: command.into(),
268            args: Vec::new(),
269            working_dir: None,
270            timeout: Duration::from_secs(30),
271            environment: HashMap::new(),
272        }
273    }
274
275    pub fn arg(mut self, arg: impl Into<String>) -> Self {
277        self.args.push(arg.into());
278        self
279    }
280
281    pub fn args(mut self, args: impl IntoIterator<Item = impl Into<String>>) -> Self {
283        self.args.extend(args.into_iter().map(|s| s.into()));
284        self
285    }
286
287    pub fn working_dir(mut self, dir: impl Into<String>) -> Self {
289        self.working_dir = Some(dir.into());
290        self
291    }
292
293    pub fn timeout(mut self, timeout: Duration) -> Self {
295        self.timeout = timeout;
296        self
297    }
298
299    pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
301        self.environment.insert(key.into(), value.into());
302        self
303    }
304
305    pub fn validate(&self) -> McpResult<()> {
307        if self.command.is_empty() {
308            return Err(ConfigError::MissingParameter {
309                parameter: "command".to_string(),
310            }
311            .into());
312        }
313
314        if let Some(ref dir) = self.working_dir {
315            if !PathBuf::from(dir).exists() {
316                return Err(ConfigError::InvalidValue {
317                    parameter: "working_dir".to_string(),
318                    value: dir.clone(),
319                    reason: "Directory does not exist".to_string(),
320                }
321                .into());
322            }
323        }
324
325        Ok(())
326    }
327}
328
329#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
334pub struct HttpSseConfig {
335    pub base_url: Url,
337
338    #[serde(with = "humantime_serde")]
340    pub timeout: Duration,
341
342    pub headers: HashMap<String, String>,
344
345    pub auth: Option<AuthConfig>,
347}
348
349impl HttpSseConfig {
350    pub fn new(base_url: Url) -> Self {
352        Self {
353            base_url,
354            timeout: Duration::from_secs(60),
355            headers: HashMap::new(),
356            auth: None,
357        }
358    }
359
360    pub fn timeout(mut self, timeout: Duration) -> Self {
362        self.timeout = timeout;
363        self
364    }
365
366    pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
368        self.headers.insert(key.into(), value.into());
369        self
370    }
371
372    pub fn auth(mut self, auth: AuthConfig) -> Self {
374        self.auth = Some(auth);
375        self
376    }
377
378    pub fn validate(&self) -> McpResult<()> {
380        if self.base_url.scheme() != "http" && self.base_url.scheme() != "https" {
381            return Err(ConfigError::InvalidValue {
382                parameter: "base_url".to_string(),
383                value: self.base_url.to_string(),
384                reason: "URL must use http or https scheme".to_string(),
385            }
386            .into());
387        }
388
389        if let Some(ref auth) = self.auth {
390            auth.validate()?;
391        }
392
393        Ok(())
394    }
395}
396
397#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
401pub struct HttpStreamConfig {
402    pub base_url: Url,
404
405    #[serde(with = "humantime_serde")]
407    pub timeout: Duration,
408
409    pub headers: HashMap<String, String>,
411
412    pub auth: Option<AuthConfig>,
414
415    pub compression: bool,
417
418    pub flow_control_window: u32,
420}
421
422impl HttpStreamConfig {
423    pub fn new(base_url: Url) -> Self {
425        Self {
426            base_url,
427            timeout: Duration::from_secs(300),
428            headers: HashMap::new(),
429            auth: None,
430            compression: true,
431            flow_control_window: 65536,
432        }
433    }
434
435    pub fn timeout(mut self, timeout: Duration) -> Self {
437        self.timeout = timeout;
438        self
439    }
440
441    pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
443        self.headers.insert(key.into(), value.into());
444        self
445    }
446
447    pub fn auth(mut self, auth: AuthConfig) -> Self {
449        self.auth = Some(auth);
450        self
451    }
452
453    pub fn compression(mut self, enabled: bool) -> Self {
455        self.compression = enabled;
456        self
457    }
458
459    pub fn flow_control_window(mut self, size: u32) -> Self {
461        self.flow_control_window = size;
462        self
463    }
464
465    pub fn validate(&self) -> McpResult<()> {
467        if self.base_url.scheme() != "http" && self.base_url.scheme() != "https" {
468            return Err(ConfigError::InvalidValue {
469                parameter: "base_url".to_string(),
470                value: self.base_url.to_string(),
471                reason: "URL must use http or https scheme".to_string(),
472            }
473            .into());
474        }
475
476        if self.flow_control_window == 0 {
477            return Err(ConfigError::InvalidValue {
478                parameter: "flow_control_window".to_string(),
479                value: self.flow_control_window.to_string(),
480                reason: "Flow control window must be greater than 0".to_string(),
481            }
482            .into());
483        }
484
485        if let Some(ref auth) = self.auth {
486            auth.validate()?;
487        }
488
489        Ok(())
490    }
491}
492
493#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
498#[serde(tag = "type", rename_all = "snake_case")]
499#[allow(missing_docs)]
500pub enum AuthConfig {
501    Basic { username: String, password: String },
503
504    Bearer { token: String },
506
507    OAuth {
509        client_id: String,
510        client_secret: String,
511        token_url: Url,
512        scope: Option<String>,
513    },
514
515    Header { name: String, value: String },
517}
518
519impl AuthConfig {
520    pub fn basic(username: impl Into<String>, password: impl Into<String>) -> Self {
522        Self::Basic {
523            username: username.into(),
524            password: password.into(),
525        }
526    }
527
528    pub fn bearer(token: impl Into<String>) -> Self {
530        Self::Bearer {
531            token: token.into(),
532        }
533    }
534
535    pub fn oauth(
537        client_id: impl Into<String>,
538        client_secret: impl Into<String>,
539        token_url: Url,
540        scope: Option<String>,
541    ) -> Self {
542        Self::OAuth {
543            client_id: client_id.into(),
544            client_secret: client_secret.into(),
545            token_url,
546            scope,
547        }
548    }
549
550    pub fn header(name: impl Into<String>, value: impl Into<String>) -> Self {
552        Self::Header {
553            name: name.into(),
554            value: value.into(),
555        }
556    }
557
558    pub fn validate(&self) -> McpResult<()> {
560        match self {
561            Self::Basic { username, password } => {
562                if username.is_empty() || password.is_empty() {
563                    return Err(ConfigError::InvalidValue {
564                        parameter: "auth".to_string(),
565                        value: "basic".to_string(),
566                        reason: "Username and password cannot be empty".to_string(),
567                    }
568                    .into());
569                }
570            }
571            Self::Bearer { token } => {
572                if token.is_empty() {
573                    return Err(ConfigError::InvalidValue {
574                        parameter: "auth".to_string(),
575                        value: "bearer".to_string(),
576                        reason: "Token cannot be empty".to_string(),
577                    }
578                    .into());
579                }
580            }
581            Self::OAuth {
582                client_id,
583                client_secret,
584                token_url,
585                ..
586            } => {
587                if client_id.is_empty() || client_secret.is_empty() {
588                    return Err(ConfigError::InvalidValue {
589                        parameter: "auth".to_string(),
590                        value: "oauth".to_string(),
591                        reason: "Client ID and secret cannot be empty".to_string(),
592                    }
593                    .into());
594                }
595                if token_url.scheme() != "https" {
596                    return Err(ConfigError::InvalidValue {
597                        parameter: "token_url".to_string(),
598                        value: token_url.to_string(),
599                        reason: "OAuth token URL must use HTTPS".to_string(),
600                    }
601                    .into());
602                }
603            }
604            Self::Header { name, value } => {
605                if name.is_empty() || value.is_empty() {
606                    return Err(ConfigError::InvalidValue {
607                        parameter: "auth".to_string(),
608                        value: "header".to_string(),
609                        reason: "Header name and value cannot be empty".to_string(),
610                    }
611                    .into());
612                }
613            }
614        }
615        Ok(())
616    }
617}