codex-cli-sdk 0.0.1

Rust SDK for the OpenAI Codex CLI
Documentation
//! MCP (Model Context Protocol) server configuration.
//!
//! Defines the [`McpServerConfig`] type for configuring MCP servers that the
//! Codex CLI can connect to. The configuration is passed to the CLI via
//! command-line flags or config overrides.
//!
//! # Example
//!
//! ```rust
//! use codex_cli_sdk::mcp::{McpServerConfig, McpServers};
//!
//! let mut servers = McpServers::new();
//! servers.insert(
//!     "filesystem".into(),
//!     McpServerConfig::new("npx")
//!         .with_args(["-y", "@modelcontextprotocol/server-filesystem", "/tmp"])
//!         .with_env("DEBUG", "true"),
//! );
//! ```

use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;

/// A map of MCP server name → configuration.
pub type McpServers = HashMap<String, McpServerConfig>;

/// Configuration for a single MCP server.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct McpServerConfig {
    /// The command to spawn the MCP server.
    pub command: String,
    /// Arguments to pass to the command.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub args: Vec<String>,
    /// Environment variables for the server process.
    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
    pub env: HashMap<String, String>,
    /// Working directory for the server process.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub cwd: Option<PathBuf>,
}

impl McpServerConfig {
    /// Create a new MCP server config with the given command.
    pub fn new(command: impl Into<String>) -> Self {
        Self {
            command: command.into(),
            args: Vec::new(),
            env: HashMap::new(),
            cwd: None,
        }
    }

    /// Add arguments to the server command.
    pub fn with_args<I, S>(mut self, args: I) -> Self
    where
        I: IntoIterator<Item = S>,
        S: Into<String>,
    {
        self.args.extend(args.into_iter().map(Into::into));
        self
    }

    /// Add a single environment variable.
    pub fn with_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
        self.env.insert(key.into(), value.into());
        self
    }

    /// Set the working directory for the server process.
    pub fn with_cwd(mut self, cwd: impl Into<PathBuf>) -> Self {
        self.cwd = Some(cwd.into());
        self
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn builder_pattern() {
        let config = McpServerConfig::new("npx")
            .with_args(["-y", "@modelcontextprotocol/server-filesystem", "/tmp"])
            .with_env("DEBUG", "true")
            .with_cwd("/home/user");

        assert_eq!(config.command, "npx");
        assert_eq!(config.args.len(), 3);
        assert_eq!(config.env.get("DEBUG"), Some(&"true".into()));
        assert_eq!(config.cwd, Some(PathBuf::from("/home/user")));
    }

    #[test]
    fn json_round_trip() {
        let config = McpServerConfig::new("node")
            .with_args(["server.js"])
            .with_env("PORT", "3000");

        let json = serde_json::to_string(&config).unwrap();
        let parsed: McpServerConfig = serde_json::from_str(&json).unwrap();
        assert_eq!(config, parsed);
    }

    #[test]
    fn json_round_trip_minimal() {
        let config = McpServerConfig::new("sqlite-mcp");
        let json = serde_json::to_string(&config).unwrap();
        let parsed: McpServerConfig = serde_json::from_str(&json).unwrap();
        assert_eq!(config, parsed);
        // Empty fields should be omitted
        assert!(!json.contains("args"));
        assert!(!json.contains("env"));
        assert!(!json.contains("cwd"));
    }

    #[test]
    fn servers_map() {
        let mut servers = McpServers::new();
        servers.insert(
            "fs".into(),
            McpServerConfig::new("npx").with_args(["-y", "fs-server"]),
        );
        servers.insert("db".into(), McpServerConfig::new("sqlite-mcp"));

        assert_eq!(servers.len(), 2);
        assert_eq!(servers["fs"].command, "npx");
        assert_eq!(servers["db"].command, "sqlite-mcp");

        // JSON round-trip
        let json = serde_json::to_string(&servers).unwrap();
        let parsed: McpServers = serde_json::from_str(&json).unwrap();
        assert_eq!(servers, parsed);
    }
}