Skip to main content

codex_cli_sdk/
mcp.rs

1//! MCP (Model Context Protocol) server configuration.
2//!
3//! Defines the [`McpServerConfig`] type for configuring MCP servers that the
4//! Codex CLI can connect to. The configuration is passed to the CLI via
5//! command-line flags or config overrides.
6//!
7//! # Example
8//!
9//! ```rust
10//! use codex_cli_sdk::mcp::{McpServerConfig, McpServers};
11//!
12//! let mut servers = McpServers::new();
13//! servers.insert(
14//!     "filesystem".into(),
15//!     McpServerConfig::new("npx")
16//!         .with_args(["-y", "@modelcontextprotocol/server-filesystem", "/tmp"])
17//!         .with_env("DEBUG", "true"),
18//! );
19//! ```
20
21use serde::{Deserialize, Serialize};
22use std::collections::HashMap;
23use std::path::PathBuf;
24
25/// A map of MCP server name → configuration.
26pub type McpServers = HashMap<String, McpServerConfig>;
27
28/// Configuration for a single MCP server.
29#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
30pub struct McpServerConfig {
31    /// The command to spawn the MCP server.
32    pub command: String,
33    /// Arguments to pass to the command.
34    #[serde(default, skip_serializing_if = "Vec::is_empty")]
35    pub args: Vec<String>,
36    /// Environment variables for the server process.
37    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
38    pub env: HashMap<String, String>,
39    /// Working directory for the server process.
40    #[serde(default, skip_serializing_if = "Option::is_none")]
41    pub cwd: Option<PathBuf>,
42}
43
44impl McpServerConfig {
45    /// Create a new MCP server config with the given command.
46    pub fn new(command: impl Into<String>) -> Self {
47        Self {
48            command: command.into(),
49            args: Vec::new(),
50            env: HashMap::new(),
51            cwd: None,
52        }
53    }
54
55    /// Add arguments to the server command.
56    pub fn with_args<I, S>(mut self, args: I) -> Self
57    where
58        I: IntoIterator<Item = S>,
59        S: Into<String>,
60    {
61        self.args.extend(args.into_iter().map(Into::into));
62        self
63    }
64
65    /// Add a single environment variable.
66    pub fn with_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
67        self.env.insert(key.into(), value.into());
68        self
69    }
70
71    /// Set the working directory for the server process.
72    pub fn with_cwd(mut self, cwd: impl Into<PathBuf>) -> Self {
73        self.cwd = Some(cwd.into());
74        self
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    #[test]
83    fn builder_pattern() {
84        let config = McpServerConfig::new("npx")
85            .with_args(["-y", "@modelcontextprotocol/server-filesystem", "/tmp"])
86            .with_env("DEBUG", "true")
87            .with_cwd("/home/user");
88
89        assert_eq!(config.command, "npx");
90        assert_eq!(config.args.len(), 3);
91        assert_eq!(config.env.get("DEBUG"), Some(&"true".into()));
92        assert_eq!(config.cwd, Some(PathBuf::from("/home/user")));
93    }
94
95    #[test]
96    fn json_round_trip() {
97        let config = McpServerConfig::new("node")
98            .with_args(["server.js"])
99            .with_env("PORT", "3000");
100
101        let json = serde_json::to_string(&config).unwrap();
102        let parsed: McpServerConfig = serde_json::from_str(&json).unwrap();
103        assert_eq!(config, parsed);
104    }
105
106    #[test]
107    fn json_round_trip_minimal() {
108        let config = McpServerConfig::new("sqlite-mcp");
109        let json = serde_json::to_string(&config).unwrap();
110        let parsed: McpServerConfig = serde_json::from_str(&json).unwrap();
111        assert_eq!(config, parsed);
112        // Empty fields should be omitted
113        assert!(!json.contains("args"));
114        assert!(!json.contains("env"));
115        assert!(!json.contains("cwd"));
116    }
117
118    #[test]
119    fn servers_map() {
120        let mut servers = McpServers::new();
121        servers.insert(
122            "fs".into(),
123            McpServerConfig::new("npx").with_args(["-y", "fs-server"]),
124        );
125        servers.insert("db".into(), McpServerConfig::new("sqlite-mcp"));
126
127        assert_eq!(servers.len(), 2);
128        assert_eq!(servers["fs"].command, "npx");
129        assert_eq!(servers["db"].command, "sqlite-mcp");
130
131        // JSON round-trip
132        let json = serde_json::to_string(&servers).unwrap();
133        let parsed: McpServers = serde_json::from_str(&json).unwrap();
134        assert_eq!(servers, parsed);
135    }
136}