Skip to main content

gemini_cli_sdk/
mcp.rs

1//! MCP server configuration types.
2//!
3//! Defines the types used to configure MCP (Model Context Protocol) servers
4//! that are passed to the Gemini CLI during session creation via `session/new`.
5//! The CLI manages MCP server lifecycle; the SDK only passes configuration.
6
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use std::collections::BTreeMap;
10
11/// Collection of MCP server configurations, keyed by server name.
12///
13/// `BTreeMap` is used to ensure deterministic ordering when serializing to
14/// the wire format, which aids reproducibility in tests and debug output.
15pub type McpServers = BTreeMap<String, McpServerConfig>;
16
17/// Configuration for a single MCP server process.
18///
19/// The Gemini CLI spawns and manages the server process; this struct only
20/// carries the parameters required to launch it.
21///
22/// # Examples
23///
24/// ```rust
25/// use gemini_cli_sdk::mcp::McpServerConfig;
26///
27/// let cfg = McpServerConfig::new("npx");
28/// assert_eq!(cfg.command, "npx");
29/// assert!(cfg.args.is_empty());
30/// ```
31#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
32pub struct McpServerConfig {
33    /// Command to launch the MCP server.
34    pub command: String,
35
36    /// Arguments forwarded to the command.
37    #[serde(default)]
38    pub args: Vec<String>,
39
40    /// Environment variables injected into the server process.
41    #[serde(default)]
42    pub env: BTreeMap<String, String>,
43
44    /// Optional working directory for the server process.
45    #[serde(default)]
46    pub cwd: Option<String>,
47
48    /// Pass-through for any additional fields the Gemini CLI may support.
49    /// Stored as a JSON object so unknown keys survive a round-trip.
50    #[serde(flatten)]
51    pub extra: Value,
52}
53
54impl McpServerConfig {
55    /// Create a minimal config with only a command.
56    ///
57    /// # Examples
58    ///
59    /// ```rust
60    /// use gemini_cli_sdk::mcp::McpServerConfig;
61    ///
62    /// let cfg = McpServerConfig::new("uvx");
63    /// assert_eq!(cfg.command, "uvx");
64    /// assert!(cfg.args.is_empty());
65    /// assert!(cfg.env.is_empty());
66    /// assert!(cfg.cwd.is_none());
67    /// ```
68    pub fn new(command: impl Into<String>) -> Self {
69        Self {
70            command: command.into(),
71            args: Vec::new(),
72            env: BTreeMap::new(),
73            cwd: None,
74            extra: Value::Object(Default::default()),
75        }
76    }
77
78    /// Create a config with a command and an explicit argument list.
79    ///
80    /// # Examples
81    ///
82    /// ```rust
83    /// use gemini_cli_sdk::mcp::McpServerConfig;
84    ///
85    /// let cfg = McpServerConfig::with_args(
86    ///     "npx",
87    ///     vec!["-y".to_string(), "@modelcontextprotocol/server-filesystem".to_string()],
88    /// );
89    /// assert_eq!(cfg.args.len(), 2);
90    /// ```
91    pub fn with_args(command: impl Into<String>, args: Vec<String>) -> Self {
92        Self {
93            command: command.into(),
94            args,
95            env: BTreeMap::new(),
96            cwd: None,
97            extra: Value::Object(Default::default()),
98        }
99    }
100}
101
102/// Convert an [`McpServers`] map to the wire format expected by `session/new`.
103///
104/// Each server config object gains a `"name"` field whose value is the map key.
105/// The resulting array preserves `BTreeMap` order (alphabetical by name).
106pub(crate) fn mcp_servers_to_wire(servers: &McpServers) -> Vec<Value> {
107    servers
108        .iter()
109        .map(|(name, config)| {
110            let mut obj = serde_json::to_value(config).unwrap_or_default();
111            if let Some(map) = obj.as_object_mut() {
112                map.insert("name".to_string(), Value::String(name.clone()));
113            }
114            obj
115        })
116        .collect()
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn test_mcp_config_new() {
125        let cfg = McpServerConfig::new("uvx");
126
127        assert_eq!(cfg.command, "uvx");
128        assert!(cfg.args.is_empty(), "args should default to empty");
129        assert!(cfg.env.is_empty(), "env should default to empty");
130        assert!(cfg.cwd.is_none(), "cwd should default to None");
131        // extra must be an empty JSON object, not null or array.
132        assert_eq!(cfg.extra, Value::Object(Default::default()));
133    }
134
135    #[test]
136    fn test_mcp_config_with_args() {
137        let args = vec![
138            "-y".to_string(),
139            "@modelcontextprotocol/server-filesystem".to_string(),
140            "/tmp".to_string(),
141        ];
142        let cfg = McpServerConfig::with_args("npx", args.clone());
143
144        assert_eq!(cfg.command, "npx");
145        assert_eq!(cfg.args, args);
146        assert!(cfg.env.is_empty());
147        assert!(cfg.cwd.is_none());
148    }
149
150    #[test]
151    fn test_mcp_config_serde_roundtrip() {
152        let mut env = BTreeMap::new();
153        env.insert("API_KEY".to_string(), "secret".to_string());
154
155        let original = McpServerConfig {
156            command: "python".to_string(),
157            args: vec!["-m".to_string(), "mcp_server".to_string()],
158            env,
159            cwd: Some("/workspace".to_string()),
160            extra: Value::Object(Default::default()),
161        };
162
163        let json = serde_json::to_string(&original).expect("serialize failed");
164        let decoded: McpServerConfig =
165            serde_json::from_str(&json).expect("deserialize failed");
166
167        assert_eq!(original, decoded);
168    }
169
170    #[test]
171    fn test_mcp_servers_to_wire_with_entries() {
172        let mut servers: McpServers = BTreeMap::new();
173
174        servers.insert(
175            "alpha".to_string(),
176            McpServerConfig::new("cmd-alpha"),
177        );
178        servers.insert(
179            "beta".to_string(),
180            McpServerConfig::with_args(
181                "cmd-beta",
182                vec!["--flag".to_string()],
183            ),
184        );
185
186        let wire = mcp_servers_to_wire(&servers);
187
188        // BTreeMap iterates alphabetically: alpha, beta.
189        assert_eq!(wire.len(), 2);
190
191        let alpha = &wire[0];
192        assert_eq!(alpha["name"], Value::String("alpha".to_string()));
193        assert_eq!(alpha["command"], Value::String("cmd-alpha".to_string()));
194
195        let beta = &wire[1];
196        assert_eq!(beta["name"], Value::String("beta".to_string()));
197        assert_eq!(beta["command"], Value::String("cmd-beta".to_string()));
198        assert_eq!(
199            beta["args"],
200            serde_json::json!(["--flag"]),
201            "args must survive wire conversion"
202        );
203    }
204
205    #[test]
206    fn test_mcp_servers_to_wire_empty() {
207        let servers: McpServers = BTreeMap::new();
208        let wire = mcp_servers_to_wire(&servers);
209        assert!(wire.is_empty(), "empty map must produce empty vec");
210    }
211}