Skip to main content

claude_cli_sdk/
mcp.rs

1//! MCP (Model Context Protocol) server configuration types.
2//!
3//! These types define how MCP servers are configured for use with
4//! Claude Code sessions. They are passed via [`ClientConfig`](crate::config::ClientConfig)
5//! and serialized into the CLI invocation.
6
7use std::collections::HashMap;
8use std::path::PathBuf;
9
10use serde::{Deserialize, Serialize};
11
12/// A map of MCP server names to their configurations.
13pub type McpServers = HashMap<String, McpServerConfig>;
14
15/// Configuration for a single MCP server that the CLI should connect to.
16///
17/// This maps to the `--mcp-servers` JSON argument passed to the CLI.
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19pub struct McpServerConfig {
20    /// The command to start the MCP server (e.g., `"npx"`, `"uvx"`).
21    pub command: String,
22
23    /// Arguments passed to the command.
24    #[serde(default)]
25    pub args: Vec<String>,
26
27    /// Environment variables to set for the MCP server process.
28    #[serde(default)]
29    pub env: HashMap<String, String>,
30
31    /// Working directory for the MCP server process.
32    #[serde(default, skip_serializing_if = "Option::is_none")]
33    pub cwd: Option<PathBuf>,
34}
35
36impl McpServerConfig {
37    /// Create a new MCP server config with just a command.
38    #[must_use]
39    pub fn new(command: impl Into<String>) -> Self {
40        Self {
41            command: command.into(),
42            args: Vec::new(),
43            env: HashMap::new(),
44            cwd: None,
45        }
46    }
47
48    /// Add arguments to the server command.
49    #[must_use]
50    pub fn with_args(mut self, args: impl IntoIterator<Item = impl Into<String>>) -> Self {
51        self.args = args.into_iter().map(Into::into).collect();
52        self
53    }
54
55    /// Add environment variables for the server process.
56    #[must_use]
57    pub fn with_env(
58        mut self,
59        env: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
60    ) -> Self {
61        self.env = env.into_iter().map(|(k, v)| (k.into(), v.into())).collect();
62        self
63    }
64
65    /// Set the working directory for the server process.
66    #[must_use]
67    pub fn with_cwd(mut self, cwd: impl Into<PathBuf>) -> Self {
68        self.cwd = Some(cwd.into());
69        self
70    }
71}
72
73// ── Tests ────────────────────────────────────────────────────────────────────
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    #[test]
80    fn mcp_server_config_round_trip() {
81        let config = McpServerConfig {
82            command: "npx".into(),
83            args: vec![
84                "-y".into(),
85                "@modelcontextprotocol/server-filesystem".into(),
86            ],
87            env: HashMap::from([("HOME".into(), "/home/user".into())]),
88            cwd: Some(PathBuf::from("/workspace")),
89        };
90        let json = serde_json::to_string(&config).unwrap();
91        let decoded: McpServerConfig = serde_json::from_str(&json).unwrap();
92        assert_eq!(config, decoded);
93    }
94
95    #[test]
96    fn mcp_server_config_minimal() {
97        let json = r#"{"command":"npx"}"#;
98        let config: McpServerConfig = serde_json::from_str(json).unwrap();
99        assert_eq!(config.command, "npx");
100        assert!(config.args.is_empty());
101        assert!(config.env.is_empty());
102        assert!(config.cwd.is_none());
103    }
104
105    #[test]
106    fn mcp_server_config_builder_pattern() {
107        let config = McpServerConfig::new("uvx")
108            .with_args(["mcp-server-git"])
109            .with_env([("GIT_DIR", "/repo/.git")])
110            .with_cwd("/repo");
111
112        assert_eq!(config.command, "uvx");
113        assert_eq!(config.args, ["mcp-server-git"]);
114        assert_eq!(config.env["GIT_DIR"], "/repo/.git");
115        assert_eq!(config.cwd, Some(PathBuf::from("/repo")));
116    }
117
118    #[test]
119    fn mcp_servers_map() {
120        let mut servers = McpServers::new();
121        servers.insert(
122            "filesystem".into(),
123            McpServerConfig::new("npx")
124                .with_args(["-y", "@modelcontextprotocol/server-filesystem"]),
125        );
126        servers.insert(
127            "git".into(),
128            McpServerConfig::new("uvx").with_args(["mcp-server-git"]),
129        );
130
131        let json = serde_json::to_string(&servers).unwrap();
132        let decoded: McpServers = serde_json::from_str(&json).unwrap();
133        assert_eq!(servers, decoded);
134    }
135
136    #[test]
137    fn mcp_server_status_from_cli_output() {
138        // Re-export test — McpServerStatus lives in types::messages
139        let json = r#"{"name":"my-server","status":"connected"}"#;
140        let status: crate::types::messages::McpServerStatus = serde_json::from_str(json).unwrap();
141        assert_eq!(status.name, "my-server");
142        assert_eq!(status.status, "connected");
143    }
144}