1use std::collections::HashMap;
8use std::path::PathBuf;
9
10use serde::{Deserialize, Serialize};
11
12pub type McpServers = HashMap<String, McpServerConfig>;
14
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19pub struct McpServerConfig {
20 pub command: String,
22
23 #[serde(default)]
25 pub args: Vec<String>,
26
27 #[serde(default)]
29 pub env: HashMap<String, String>,
30
31 #[serde(default, skip_serializing_if = "Option::is_none")]
33 pub cwd: Option<PathBuf>,
34}
35
36impl McpServerConfig {
37 #[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 #[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 #[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 #[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#[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 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}