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}