Skip to main content

mcp_execution_core/
server_config.rs

1//! MCP server configuration with command, arguments, and environment.
2//!
3//! This module provides type-safe server configuration for launching MCP servers
4//! with security validation of commands, arguments, and environment variables.
5//!
6//! # Transport Types
7//!
8//! Supports two transport types:
9//! - Stdio: Subprocess communication via stdin/stdout (default)
10//! - HTTP: Communication via HTTP/HTTPS API
11//!
12//! # Security
13//!
14//! The configuration enforces:
15//! - Command validation (absolute path or binary name)
16//! - Argument sanitization (no shell metacharacters)
17//! - Environment variable validation (block dangerous names)
18//! - Forbidden characters: `;`, `|`, `&`, `>`, `<`, `` ` ``, `$`, `(`, `)`, `\n`, `\r`
19//! - Forbidden env vars: `LD_PRELOAD`, `LD_LIBRARY_PATH`, `DYLD_*`, `PATH`
20//!
21//! # Examples
22//!
23//! ```
24//! use mcp_execution_core::ServerConfig;
25//! use std::collections::HashMap;
26//!
27//! // Simple configuration with just command
28//! let config = ServerConfig::builder()
29//!     .command("docker".to_string())
30//!     .build();
31//!
32//! // Full configuration with args and env
33//! let config = ServerConfig::builder()
34//!     .command("/usr/local/bin/mcp-server".to_string())
35//!     .arg("--port".to_string())
36//!     .arg("8080".to_string())
37//!     .env("LOG_LEVEL".to_string(), "debug".to_string())
38//!     .build();
39//!
40//! // HTTP transport configuration
41//! let config = ServerConfig::builder()
42//!     .http_transport("https://api.example.com/mcp".to_string())
43//!     .header("Authorization".to_string(), "Bearer token".to_string())
44//!     .build();
45//! ```
46
47use serde::{Deserialize, Serialize};
48use std::collections::HashMap;
49use std::path::PathBuf;
50use std::time::Duration;
51
52/// Default timeout for establishing an MCP server connection (handshake).
53const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_secs(30);
54
55/// Default timeout for the `list_all_tools` discovery call after connecting.
56const DEFAULT_DISCOVER_TIMEOUT: Duration = Duration::from_secs(30);
57
58const fn default_connect_timeout() -> Duration {
59    DEFAULT_CONNECT_TIMEOUT
60}
61
62const fn default_discover_timeout() -> Duration {
63    DEFAULT_DISCOVER_TIMEOUT
64}
65
66/// Transport type for MCP server communication.
67///
68/// Defines how the client communicates with the MCP server.
69///
70/// # Examples
71///
72/// ```
73/// use mcp_execution_core::TransportType;
74///
75/// // Default is stdio
76/// let transport = TransportType::default();
77/// assert_eq!(transport, TransportType::Stdio);
78/// ```
79#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
80#[serde(rename_all = "lowercase")]
81pub enum TransportType {
82    /// Stdio transport: subprocess communication via stdin/stdout.
83    #[default]
84    Stdio,
85    /// HTTP transport: communication via HTTP/HTTPS API.
86    Http,
87    /// SSE transport: Server-Sent Events for streaming communication.
88    Sse,
89}
90
91/// MCP server configuration with command, arguments, and environment.
92///
93/// Represents the configuration needed to communicate with an MCP server,
94/// supporting both stdio (subprocess) and HTTP transports.
95///
96/// # Transport Types
97///
98/// - **Stdio**: Launches a subprocess and communicates via stdin/stdout
99/// - **HTTP**: Connects to an HTTP/HTTPS API endpoint
100///
101/// # Security
102///
103/// This type is designed to be safe by construction. Use the builder pattern
104/// to construct instances, and call [`validate_server_config`] before execution
105/// to ensure security requirements are met.
106///
107/// # Examples
108///
109/// ```
110/// use mcp_execution_core::ServerConfig;
111///
112/// // Stdio transport
113/// let config = ServerConfig::builder()
114///     .command("docker".to_string())
115///     .arg("run".to_string())
116///     .arg("mcp-server".to_string())
117///     .build();
118///
119/// assert_eq!(config.command, "docker");
120/// assert_eq!(config.args.len(), 2);
121///
122/// // HTTP transport
123/// let config = ServerConfig::builder()
124///     .http_transport("https://api.example.com/mcp".to_string())
125///     .header("Authorization".to_string(), "Bearer token".to_string())
126///     .build();
127/// ```
128///
129/// [`validate_server_config`]: fn.validate_server_config.html
130#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
131pub struct ServerConfig {
132    /// Transport type (stdio or http).
133    ///
134    /// Determines how the client communicates with the MCP server.
135    #[serde(default)]
136    pub transport: TransportType,
137
138    /// Command to execute (binary name or absolute path).
139    ///
140    /// **Only used for stdio transport.**
141    ///
142    /// Can be either:
143    /// - Binary name (e.g., "docker", "python") - resolved via PATH
144    /// - Absolute path (e.g., "/usr/local/bin/mcp-server")
145    #[serde(default)]
146    pub command: String,
147
148    /// Arguments to pass to command.
149    ///
150    /// **Only used for stdio transport.**
151    ///
152    /// Each argument is passed separately to avoid shell interpretation.
153    /// Do not include the command itself in arguments.
154    #[serde(default)]
155    pub args: Vec<String>,
156
157    /// Environment variables to set for the subprocess.
158    ///
159    /// **Only used for stdio transport.**
160    ///
161    /// These are added to (or override) the parent process environment.
162    /// Security validation blocks dangerous variables like `LD_PRELOAD`.
163    #[serde(default)]
164    pub env: HashMap<String, String>,
165
166    /// Working directory for the subprocess (optional).
167    ///
168    /// **Only used for stdio transport.**
169    ///
170    /// If None, inherits the parent process working directory.
171    #[serde(default)]
172    pub cwd: Option<PathBuf>,
173
174    /// URL for HTTP transport.
175    ///
176    /// **Only used for HTTP transport.**
177    ///
178    /// Example: `https://api.example.com/mcp`
179    #[serde(default)]
180    pub url: Option<String>,
181
182    /// HTTP headers for HTTP transport.
183    ///
184    /// **Only used for HTTP transport.**
185    ///
186    /// Common headers include:
187    /// - `Authorization`: Authentication token
188    /// - `Content-Type`: Request content type
189    #[serde(default)]
190    pub headers: HashMap<String, String>,
191
192    /// Timeout for establishing a connection to the server (handshake).
193    ///
194    /// Bounds how long `Introspector::discover_server` waits for the initial
195    /// rmcp `serve` handshake before giving up. Defaults to 30 seconds.
196    #[serde(default = "default_connect_timeout")]
197    pub connect_timeout: Duration,
198
199    /// Timeout for the tool discovery call after a connection is established.
200    ///
201    /// Bounds how long `Introspector::discover_server` waits for
202    /// `list_all_tools` to respond. Defaults to 30 seconds.
203    #[serde(default = "default_discover_timeout")]
204    pub discover_timeout: Duration,
205}
206
207impl ServerConfig {
208    /// Creates a new builder for `ServerConfig`.
209    ///
210    /// # Examples
211    ///
212    /// ```
213    /// use mcp_execution_core::ServerConfig;
214    ///
215    /// let config = ServerConfig::builder()
216    ///     .command("docker".to_string())
217    ///     .build();
218    /// ```
219    #[must_use]
220    pub fn builder() -> ServerConfigBuilder {
221        ServerConfigBuilder::default()
222    }
223
224    /// Returns the transport type.
225    #[must_use]
226    pub const fn transport(&self) -> &TransportType {
227        &self.transport
228    }
229
230    /// Returns the command as a string slice.
231    #[must_use]
232    pub fn command(&self) -> &str {
233        &self.command
234    }
235
236    /// Returns a slice of arguments.
237    #[must_use]
238    pub fn args(&self) -> &[String] {
239        &self.args
240    }
241
242    /// Returns a reference to the environment variables map.
243    #[must_use]
244    pub const fn env(&self) -> &HashMap<String, String> {
245        &self.env
246    }
247
248    /// Returns the working directory, if set.
249    #[must_use]
250    pub const fn cwd(&self) -> Option<&PathBuf> {
251        self.cwd.as_ref()
252    }
253
254    /// Returns the URL for HTTP transport, if set.
255    #[must_use]
256    pub fn url(&self) -> Option<&str> {
257        self.url.as_deref()
258    }
259
260    /// Returns a reference to the HTTP headers map.
261    #[must_use]
262    pub const fn headers(&self) -> &HashMap<String, String> {
263        &self.headers
264    }
265
266    /// Returns the connection (handshake) timeout.
267    ///
268    /// # Examples
269    ///
270    /// ```
271    /// use mcp_execution_core::ServerConfig;
272    /// use std::time::Duration;
273    ///
274    /// let config = ServerConfig::builder()
275    ///     .command("docker".to_string())
276    ///     .build();
277    ///
278    /// assert_eq!(config.connect_timeout(), Duration::from_secs(30));
279    /// ```
280    #[must_use]
281    pub const fn connect_timeout(&self) -> Duration {
282        self.connect_timeout
283    }
284
285    /// Returns the tool discovery timeout.
286    ///
287    /// # Examples
288    ///
289    /// ```
290    /// use mcp_execution_core::ServerConfig;
291    /// use std::time::Duration;
292    ///
293    /// let config = ServerConfig::builder()
294    ///     .command("docker".to_string())
295    ///     .build();
296    ///
297    /// assert_eq!(config.discover_timeout(), Duration::from_secs(30));
298    /// ```
299    #[must_use]
300    pub const fn discover_timeout(&self) -> Duration {
301        self.discover_timeout
302    }
303}
304
305/// Builder for constructing `ServerConfig` instances.
306///
307/// Provides a fluent API for building server configurations with
308/// optional arguments, environment variables, and HTTP settings.
309///
310/// # Examples
311///
312/// ```
313/// use mcp_execution_core::ServerConfig;
314///
315/// // Stdio transport
316/// let config = ServerConfig::builder()
317///     .command("mcp-server".to_string())
318///     .arg("--verbose".to_string())
319///     .env("DEBUG".to_string(), "1".to_string())
320///     .build();
321///
322/// // HTTP transport
323/// let config = ServerConfig::builder()
324///     .http_transport("https://api.example.com/mcp".to_string())
325///     .header("Authorization".to_string(), "Bearer token".to_string())
326///     .build();
327/// ```
328#[derive(Debug, Clone)]
329pub struct ServerConfigBuilder {
330    transport: TransportType,
331    command: Option<String>,
332    args: Vec<String>,
333    env: HashMap<String, String>,
334    cwd: Option<PathBuf>,
335    url: Option<String>,
336    headers: HashMap<String, String>,
337    connect_timeout: Duration,
338    discover_timeout: Duration,
339}
340
341impl Default for ServerConfigBuilder {
342    fn default() -> Self {
343        Self {
344            transport: TransportType::default(),
345            command: None,
346            args: Vec::new(),
347            env: HashMap::new(),
348            cwd: None,
349            url: None,
350            headers: HashMap::new(),
351            connect_timeout: DEFAULT_CONNECT_TIMEOUT,
352            discover_timeout: DEFAULT_DISCOVER_TIMEOUT,
353        }
354    }
355}
356
357impl ServerConfigBuilder {
358    /// Sets the command to execute.
359    ///
360    /// # Examples
361    ///
362    /// ```
363    /// use mcp_execution_core::ServerConfig;
364    ///
365    /// let config = ServerConfig::builder()
366    ///     .command("docker".to_string())
367    ///     .build();
368    /// ```
369    #[must_use]
370    pub fn command(mut self, command: String) -> Self {
371        self.command = Some(command);
372        self
373    }
374
375    /// Adds a single argument.
376    ///
377    /// # Examples
378    ///
379    /// ```
380    /// use mcp_execution_core::ServerConfig;
381    ///
382    /// let config = ServerConfig::builder()
383    ///     .command("docker".to_string())
384    ///     .arg("run".to_string())
385    ///     .arg("--rm".to_string())
386    ///     .build();
387    /// ```
388    #[must_use]
389    pub fn arg(mut self, arg: String) -> Self {
390        self.args.push(arg);
391        self
392    }
393
394    /// Sets all arguments at once, replacing any previously added.
395    ///
396    /// # Examples
397    ///
398    /// ```
399    /// use mcp_execution_core::ServerConfig;
400    ///
401    /// let config = ServerConfig::builder()
402    ///     .command("docker".to_string())
403    ///     .args(vec!["run".to_string(), "--rm".to_string()])
404    ///     .build();
405    /// ```
406    #[must_use]
407    pub fn args(mut self, args: Vec<String>) -> Self {
408        self.args = args;
409        self
410    }
411
412    /// Adds a single environment variable.
413    ///
414    /// # Examples
415    ///
416    /// ```
417    /// use mcp_execution_core::ServerConfig;
418    ///
419    /// let config = ServerConfig::builder()
420    ///     .command("mcp-server".to_string())
421    ///     .env("LOG_LEVEL".to_string(), "debug".to_string())
422    ///     .build();
423    /// ```
424    #[must_use]
425    pub fn env(mut self, key: String, value: String) -> Self {
426        self.env.insert(key, value);
427        self
428    }
429
430    /// Sets all environment variables at once, replacing any previously added.
431    ///
432    /// # Examples
433    ///
434    /// ```
435    /// use mcp_execution_core::ServerConfig;
436    /// use std::collections::HashMap;
437    ///
438    /// let mut env_map = HashMap::new();
439    /// env_map.insert("DEBUG".to_string(), "1".to_string());
440    ///
441    /// let config = ServerConfig::builder()
442    ///     .command("mcp-server".to_string())
443    ///     .environment(env_map)
444    ///     .build();
445    /// ```
446    #[must_use]
447    pub fn environment(mut self, env: HashMap<String, String>) -> Self {
448        self.env = env;
449        self
450    }
451
452    /// Sets the working directory for the subprocess.
453    ///
454    /// # Examples
455    ///
456    /// ```
457    /// use mcp_execution_core::ServerConfig;
458    /// use std::path::PathBuf;
459    ///
460    /// let config = ServerConfig::builder()
461    ///     .command("mcp-server".to_string())
462    ///     .cwd(PathBuf::from("/tmp"))
463    ///     .build();
464    /// ```
465    #[must_use]
466    pub fn cwd(mut self, cwd: PathBuf) -> Self {
467        self.cwd = Some(cwd);
468        self
469    }
470
471    /// Configures HTTP transport with the given URL.
472    ///
473    /// This sets the transport type to HTTP and configures the endpoint URL.
474    ///
475    /// # Examples
476    ///
477    /// ```
478    /// use mcp_execution_core::ServerConfig;
479    ///
480    /// let config = ServerConfig::builder()
481    ///     .http_transport("https://api.example.com/mcp".to_string())
482    ///     .build();
483    /// ```
484    #[must_use]
485    pub fn http_transport(mut self, url: String) -> Self {
486        self.transport = TransportType::Http;
487        self.url = Some(url);
488        // Set a dummy command for HTTP transport so build() doesn't panic
489        if self.command.is_none() {
490            self.command = Some(String::new());
491        }
492        self
493    }
494
495    /// Configures SSE transport with the given URL.
496    ///
497    /// This sets the transport type to SSE (Server-Sent Events) and configures the endpoint URL.
498    ///
499    /// # Examples
500    ///
501    /// ```
502    /// use mcp_execution_core::ServerConfig;
503    ///
504    /// let config = ServerConfig::builder()
505    ///     .sse_transport("https://api.example.com/sse".to_string())
506    ///     .build();
507    /// ```
508    #[must_use]
509    pub fn sse_transport(mut self, url: String) -> Self {
510        self.transport = TransportType::Sse;
511        self.url = Some(url);
512        // Set a dummy command for SSE transport so build() doesn't panic
513        if self.command.is_none() {
514            self.command = Some(String::new());
515        }
516        self
517    }
518
519    /// Sets the URL for HTTP transport.
520    ///
521    /// # Examples
522    ///
523    /// ```
524    /// use mcp_execution_core::ServerConfig;
525    ///
526    /// let config = ServerConfig::builder()
527    ///     .http_transport("https://api.example.com/mcp".to_string())
528    ///     .url("https://api.example.com/mcp/v2".to_string())
529    ///     .build();
530    /// ```
531    #[must_use]
532    pub fn url(mut self, url: String) -> Self {
533        self.url = Some(url);
534        self
535    }
536
537    /// Adds a single HTTP header.
538    ///
539    /// # Examples
540    ///
541    /// ```
542    /// use mcp_execution_core::ServerConfig;
543    ///
544    /// let config = ServerConfig::builder()
545    ///     .http_transport("https://api.example.com/mcp".to_string())
546    ///     .header("Authorization".to_string(), "Bearer token".to_string())
547    ///     .build();
548    /// ```
549    #[must_use]
550    pub fn header(mut self, key: String, value: String) -> Self {
551        self.headers.insert(key, value);
552        self
553    }
554
555    /// Sets all HTTP headers at once, replacing any previously added.
556    ///
557    /// # Examples
558    ///
559    /// ```
560    /// use mcp_execution_core::ServerConfig;
561    /// use std::collections::HashMap;
562    ///
563    /// let mut headers = HashMap::new();
564    /// headers.insert("Authorization".to_string(), "Bearer token".to_string());
565    ///
566    /// let config = ServerConfig::builder()
567    ///     .http_transport("https://api.example.com/mcp".to_string())
568    ///     .headers(headers)
569    ///     .build();
570    /// ```
571    #[must_use]
572    pub fn headers(mut self, headers: HashMap<String, String>) -> Self {
573        self.headers = headers;
574        self
575    }
576
577    /// Sets the connection (handshake) timeout, overriding the 30-second default.
578    ///
579    /// # Examples
580    ///
581    /// ```
582    /// use mcp_execution_core::ServerConfig;
583    /// use std::time::Duration;
584    ///
585    /// let config = ServerConfig::builder()
586    ///     .command("docker".to_string())
587    ///     .connect_timeout(Duration::from_secs(5))
588    ///     .build();
589    ///
590    /// assert_eq!(config.connect_timeout(), Duration::from_secs(5));
591    /// ```
592    #[must_use]
593    pub const fn connect_timeout(mut self, timeout: Duration) -> Self {
594        self.connect_timeout = timeout;
595        self
596    }
597
598    /// Sets the tool discovery timeout, overriding the 30-second default.
599    ///
600    /// # Examples
601    ///
602    /// ```
603    /// use mcp_execution_core::ServerConfig;
604    /// use std::time::Duration;
605    ///
606    /// let config = ServerConfig::builder()
607    ///     .command("docker".to_string())
608    ///     .discover_timeout(Duration::from_secs(5))
609    ///     .build();
610    ///
611    /// assert_eq!(config.discover_timeout(), Duration::from_secs(5));
612    /// ```
613    #[must_use]
614    pub const fn discover_timeout(mut self, timeout: Duration) -> Self {
615        self.discover_timeout = timeout;
616        self
617    }
618
619    /// Builds the `ServerConfig`.
620    ///
621    /// # Panics
622    ///
623    /// Panics if:
624    /// - Command was not set for stdio transport
625    /// - URL was not set for HTTP transport
626    ///
627    /// Use `try_build()` for fallible construction.
628    ///
629    /// # Examples
630    ///
631    /// ```
632    /// use mcp_execution_core::ServerConfig;
633    ///
634    /// let config = ServerConfig::builder()
635    ///     .command("docker".to_string())
636    ///     .build();
637    /// ```
638    #[must_use]
639    pub fn build(self) -> ServerConfig {
640        self.try_build()
641            .expect("ServerConfig::build() failed validation")
642    }
643
644    /// Attempts to build the `ServerConfig`, returning an error if invalid.
645    ///
646    /// # Errors
647    ///
648    /// Returns an error if:
649    /// - Command is not set for stdio transport
650    /// - URL is not set for HTTP transport
651    ///
652    /// # Examples
653    ///
654    /// ```
655    /// use mcp_execution_core::ServerConfig;
656    ///
657    /// let result = ServerConfig::builder()
658    ///     .command("docker".to_string())
659    ///     .try_build();
660    ///
661    /// assert!(result.is_ok());
662    /// ```
663    pub fn try_build(self) -> Result<ServerConfig, String> {
664        match self.transport {
665            TransportType::Stdio => {
666                let command = self
667                    .command
668                    .ok_or_else(|| "command is required for stdio transport".to_string())?;
669
670                if command.trim().is_empty() {
671                    return Err("command cannot be empty for stdio transport".to_string());
672                }
673
674                Ok(ServerConfig {
675                    transport: TransportType::Stdio,
676                    command,
677                    args: self.args,
678                    env: self.env,
679                    cwd: self.cwd,
680                    url: None,
681                    headers: HashMap::new(),
682                    connect_timeout: self.connect_timeout,
683                    discover_timeout: self.discover_timeout,
684                })
685            }
686            TransportType::Http => {
687                let url = self
688                    .url
689                    .ok_or_else(|| "url is required for HTTP transport".to_string())?;
690
691                Ok(ServerConfig {
692                    transport: TransportType::Http,
693                    command: String::new(),
694                    args: Vec::new(),
695                    env: HashMap::new(),
696                    cwd: None,
697                    url: Some(url),
698                    headers: self.headers,
699                    connect_timeout: self.connect_timeout,
700                    discover_timeout: self.discover_timeout,
701                })
702            }
703            TransportType::Sse => {
704                let url = self
705                    .url
706                    .ok_or_else(|| "url is required for SSE transport".to_string())?;
707
708                Ok(ServerConfig {
709                    transport: TransportType::Sse,
710                    command: String::new(),
711                    args: Vec::new(),
712                    env: HashMap::new(),
713                    cwd: None,
714                    url: Some(url),
715                    headers: self.headers,
716                    connect_timeout: self.connect_timeout,
717                    discover_timeout: self.discover_timeout,
718                })
719            }
720        }
721    }
722}
723
724#[cfg(test)]
725mod tests {
726    use super::*;
727
728    #[test]
729    fn test_server_config_builder_minimal() {
730        let config = ServerConfig::builder()
731            .command("docker".to_string())
732            .build();
733
734        assert_eq!(config.command, "docker");
735        assert!(config.args.is_empty());
736        assert!(config.env.is_empty());
737        assert!(config.cwd.is_none());
738    }
739
740    #[test]
741    fn test_server_config_builder_with_args() {
742        let config = ServerConfig::builder()
743            .command("docker".to_string())
744            .arg("run".to_string())
745            .arg("--rm".to_string())
746            .arg("mcp-server".to_string())
747            .build();
748
749        assert_eq!(config.command, "docker");
750        assert_eq!(config.args, vec!["run", "--rm", "mcp-server"]);
751    }
752
753    #[test]
754    fn test_server_config_builder_with_args_vec() {
755        let config = ServerConfig::builder()
756            .command("docker".to_string())
757            .args(vec!["run".to_string(), "--rm".to_string()])
758            .build();
759
760        assert_eq!(config.args, vec!["run", "--rm"]);
761    }
762
763    #[test]
764    fn test_server_config_builder_with_env() {
765        let config = ServerConfig::builder()
766            .command("mcp-server".to_string())
767            .env("LOG_LEVEL".to_string(), "debug".to_string())
768            .env("DEBUG".to_string(), "1".to_string())
769            .build();
770
771        assert_eq!(config.env.len(), 2);
772        assert_eq!(config.env.get("LOG_LEVEL"), Some(&"debug".to_string()));
773        assert_eq!(config.env.get("DEBUG"), Some(&"1".to_string()));
774    }
775
776    #[test]
777    fn test_server_config_builder_with_environment_map() {
778        let mut env_map = HashMap::new();
779        env_map.insert("VAR1".to_string(), "value1".to_string());
780        env_map.insert("VAR2".to_string(), "value2".to_string());
781
782        let config = ServerConfig::builder()
783            .command("mcp-server".to_string())
784            .environment(env_map)
785            .build();
786
787        assert_eq!(config.env.len(), 2);
788    }
789
790    #[test]
791    fn test_server_config_builder_with_cwd() {
792        let config = ServerConfig::builder()
793            .command("mcp-server".to_string())
794            .cwd(PathBuf::from("/tmp"))
795            .build();
796
797        assert_eq!(config.cwd, Some(PathBuf::from("/tmp")));
798    }
799
800    #[test]
801    fn test_server_config_builder_full() {
802        let mut env_map = HashMap::new();
803        env_map.insert("LOG_LEVEL".to_string(), "debug".to_string());
804
805        let config = ServerConfig::builder()
806            .command("/usr/local/bin/mcp-server".to_string())
807            .args(vec!["--port".to_string(), "8080".to_string()])
808            .environment(env_map)
809            .cwd(PathBuf::from("/var/run"))
810            .build();
811
812        assert_eq!(config.command, "/usr/local/bin/mcp-server");
813        assert_eq!(config.args.len(), 2);
814        assert_eq!(config.env.len(), 1);
815        assert_eq!(config.cwd, Some(PathBuf::from("/var/run")));
816    }
817
818    #[test]
819    #[should_panic(expected = "command")]
820    fn test_server_config_builder_missing_command() {
821        let _ = ServerConfig::builder().build();
822    }
823
824    #[test]
825    fn test_server_config_builder_try_build_missing_command() {
826        let result = ServerConfig::builder().try_build();
827        assert!(result.is_err());
828        assert!(result.unwrap_err().contains("command"));
829    }
830
831    #[test]
832    fn test_server_config_accessors() {
833        let config = ServerConfig::builder()
834            .command("docker".to_string())
835            .arg("run".to_string())
836            .env("VAR".to_string(), "value".to_string())
837            .cwd(PathBuf::from("/tmp"))
838            .build();
839
840        assert_eq!(config.command(), "docker");
841        assert_eq!(config.args(), &["run".to_string()]);
842        assert_eq!(config.env().len(), 1);
843        assert_eq!(config.cwd(), Some(&PathBuf::from("/tmp")));
844    }
845
846    #[test]
847    fn test_server_config_serialize_deserialize() {
848        let config = ServerConfig::builder()
849            .command("mcp-server".to_string())
850            .arg("--verbose".to_string())
851            .env("DEBUG".to_string(), "1".to_string())
852            .build();
853
854        let json = serde_json::to_string(&config).unwrap();
855        let deserialized: ServerConfig = serde_json::from_str(&json).unwrap();
856
857        assert_eq!(config, deserialized);
858    }
859
860    #[test]
861    fn test_server_config_clone() {
862        let config = ServerConfig::builder()
863            .command("docker".to_string())
864            .build();
865
866        let cloned = config.clone();
867        assert_eq!(config, cloned);
868    }
869
870    #[test]
871    fn test_server_config_debug() {
872        let config = ServerConfig::builder()
873            .command("docker".to_string())
874            .build();
875
876        let debug_str = format!("{config:?}");
877        assert!(debug_str.contains("docker"));
878    }
879
880    #[test]
881    fn test_transport_type_default() {
882        let transport = TransportType::default();
883        assert_eq!(transport, TransportType::Stdio);
884    }
885
886    #[test]
887    fn test_server_config_http_transport() {
888        let config = ServerConfig::builder()
889            .http_transport("https://api.example.com/mcp".to_string())
890            .build();
891
892        assert_eq!(config.transport, TransportType::Http);
893        assert_eq!(config.url(), Some("https://api.example.com/mcp"));
894        assert!(config.headers.is_empty());
895        assert!(config.command.is_empty());
896    }
897
898    #[test]
899    fn test_server_config_http_with_headers() {
900        let config = ServerConfig::builder()
901            .http_transport("https://api.example.com/mcp".to_string())
902            .header("Authorization".to_string(), "Bearer token".to_string())
903            .header("Content-Type".to_string(), "application/json".to_string())
904            .build();
905
906        assert_eq!(config.transport, TransportType::Http);
907        assert_eq!(config.headers.len(), 2);
908        assert_eq!(
909            config.headers.get("Authorization"),
910            Some(&"Bearer token".to_string())
911        );
912        assert_eq!(
913            config.headers.get("Content-Type"),
914            Some(&"application/json".to_string())
915        );
916    }
917
918    #[test]
919    fn test_server_config_http_with_headers_map() {
920        let mut headers = HashMap::new();
921        headers.insert("Authorization".to_string(), "Bearer token".to_string());
922
923        let config = ServerConfig::builder()
924            .http_transport("https://api.example.com/mcp".to_string())
925            .headers(headers)
926            .build();
927
928        assert_eq!(config.headers.len(), 1);
929    }
930
931    #[test]
932    fn test_server_config_http_try_build_missing_url() {
933        let result = ServerConfig::builder().try_build();
934        assert!(result.is_err());
935        assert!(result.unwrap_err().contains("required"));
936    }
937
938    #[test]
939    fn test_server_config_http_accessors() {
940        let config = ServerConfig::builder()
941            .http_transport("https://api.example.com/mcp".to_string())
942            .header("Auth".to_string(), "token".to_string())
943            .build();
944
945        assert_eq!(config.transport(), &TransportType::Http);
946        assert_eq!(config.url(), Some("https://api.example.com/mcp"));
947        assert_eq!(config.headers().len(), 1);
948    }
949
950    #[test]
951    fn test_server_config_stdio_default_transport() {
952        let config = ServerConfig::builder()
953            .command("docker".to_string())
954            .build();
955
956        assert_eq!(config.transport, TransportType::Stdio);
957    }
958
959    #[test]
960    fn test_server_config_sse_transport() {
961        let config = ServerConfig::builder()
962            .sse_transport("https://api.example.com/sse".to_string())
963            .build();
964
965        assert_eq!(config.transport, TransportType::Sse);
966        assert_eq!(config.url(), Some("https://api.example.com/sse"));
967        assert!(config.headers.is_empty());
968        assert!(config.command.is_empty());
969    }
970
971    #[test]
972    fn test_server_config_sse_with_headers() {
973        let config = ServerConfig::builder()
974            .sse_transport("https://api.example.com/sse".to_string())
975            .header("Authorization".to_string(), "Bearer token".to_string())
976            .header("X-Custom".to_string(), "value".to_string())
977            .build();
978
979        assert_eq!(config.transport, TransportType::Sse);
980        assert_eq!(config.headers.len(), 2);
981        assert_eq!(
982            config.headers.get("Authorization"),
983            Some(&"Bearer token".to_string())
984        );
985        assert_eq!(config.headers.get("X-Custom"), Some(&"value".to_string()));
986    }
987
988    #[test]
989    fn test_server_config_sse_try_build_missing_url() {
990        let mut builder = ServerConfig::builder();
991        builder.transport = TransportType::Sse;
992
993        let result = builder.try_build();
994        assert!(result.is_err());
995        assert!(result.unwrap_err().contains("url is required"));
996    }
997
998    #[test]
999    fn test_transport_type_serialization() {
1000        let stdio = TransportType::Stdio;
1001        let http = TransportType::Http;
1002        let sse = TransportType::Sse;
1003
1004        assert_eq!(serde_json::to_string(&stdio).unwrap(), "\"stdio\"");
1005        assert_eq!(serde_json::to_string(&http).unwrap(), "\"http\"");
1006        assert_eq!(serde_json::to_string(&sse).unwrap(), "\"sse\"");
1007    }
1008
1009    #[test]
1010    fn test_transport_type_deserialization() {
1011        let stdio: TransportType = serde_json::from_str("\"stdio\"").unwrap();
1012        let http: TransportType = serde_json::from_str("\"http\"").unwrap();
1013        let sse: TransportType = serde_json::from_str("\"sse\"").unwrap();
1014
1015        assert_eq!(stdio, TransportType::Stdio);
1016        assert_eq!(http, TransportType::Http);
1017        assert_eq!(sse, TransportType::Sse);
1018    }
1019}