1use std::collections::HashMap;
2use std::path::{Path, PathBuf};
3
4use serde::Serialize;
5
6use crate::error::Result;
7
8#[derive(Debug, Clone, Default)]
27pub struct McpConfigBuilder {
28 servers: HashMap<String, McpServerConfig>,
29}
30
31#[derive(Debug, Clone, Serialize)]
33#[serde(tag = "type")]
34pub enum McpServerConfig {
35 #[serde(rename = "http")]
37 Http {
38 url: String,
39 #[serde(skip_serializing_if = "HashMap::is_empty")]
40 headers: HashMap<String, String>,
41 },
42
43 #[serde(rename = "stdio")]
45 Stdio {
46 command: String,
47 #[serde(skip_serializing_if = "Vec::is_empty")]
48 args: Vec<String>,
49 #[serde(skip_serializing_if = "HashMap::is_empty")]
50 env: HashMap<String, String>,
51 },
52}
53
54#[derive(Debug, Serialize)]
56struct McpConfigFile {
57 #[serde(rename = "mcpServers")]
58 mcp_servers: HashMap<String, McpServerConfig>,
59}
60
61impl McpConfigBuilder {
62 #[must_use]
64 pub fn new() -> Self {
65 Self::default()
66 }
67
68 #[must_use]
70 pub fn http_server(mut self, name: impl Into<String>, url: impl Into<String>) -> Self {
71 self.servers.insert(
72 name.into(),
73 McpServerConfig::Http {
74 url: url.into(),
75 headers: HashMap::new(),
76 },
77 );
78 self
79 }
80
81 #[must_use]
83 pub fn http_server_with_headers(
84 mut self,
85 name: impl Into<String>,
86 url: impl Into<String>,
87 headers: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
88 ) -> Self {
89 self.servers.insert(
90 name.into(),
91 McpServerConfig::Http {
92 url: url.into(),
93 headers: headers
94 .into_iter()
95 .map(|(k, v)| (k.into(), v.into()))
96 .collect(),
97 },
98 );
99 self
100 }
101
102 #[must_use]
104 pub fn stdio_server(
105 mut self,
106 name: impl Into<String>,
107 command: impl Into<String>,
108 args: impl IntoIterator<Item = impl Into<String>>,
109 ) -> Self {
110 self.servers.insert(
111 name.into(),
112 McpServerConfig::Stdio {
113 command: command.into(),
114 args: args.into_iter().map(Into::into).collect(),
115 env: HashMap::new(),
116 },
117 );
118 self
119 }
120
121 #[must_use]
123 pub fn stdio_server_with_env(
124 mut self,
125 name: impl Into<String>,
126 command: impl Into<String>,
127 args: impl IntoIterator<Item = impl Into<String>>,
128 env: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
129 ) -> Self {
130 self.servers.insert(
131 name.into(),
132 McpServerConfig::Stdio {
133 command: command.into(),
134 args: args.into_iter().map(Into::into).collect(),
135 env: env.into_iter().map(|(k, v)| (k.into(), v.into())).collect(),
136 },
137 );
138 self
139 }
140
141 #[must_use]
143 pub fn server(mut self, name: impl Into<String>, config: McpServerConfig) -> Self {
144 self.servers.insert(name.into(), config);
145 self
146 }
147
148 pub fn to_json(&self) -> Result<String> {
150 let file = McpConfigFile {
151 mcp_servers: self.servers.clone(),
152 };
153
154 #[cfg(feature = "json")]
155 {
156 serde_json::to_string_pretty(&file).map_err(|e| crate::error::Error::Json {
157 message: "failed to serialize MCP config".to_string(),
158 source: e,
159 })
160 }
161
162 #[cfg(not(feature = "json"))]
163 {
164 let _ = file;
165 Err(crate::error::Error::Io {
166 message: "json feature required for MCP config serialization".to_string(),
167 source: std::io::Error::new(
168 std::io::ErrorKind::Unsupported,
169 "json feature not enabled",
170 ),
171 working_dir: None,
172 })
173 }
174 }
175
176 pub fn write_to(&self, path: impl AsRef<Path>) -> Result<PathBuf> {
178 let path = path.as_ref().to_path_buf();
179 let json = self.to_json()?;
180
181 if let Some(parent) = path.parent() {
182 std::fs::create_dir_all(parent).map_err(|e| crate::error::Error::Io {
183 message: format!("failed to create directory: {}", parent.display()),
184 source: e,
185 working_dir: None,
186 })?;
187 }
188
189 std::fs::write(&path, json).map_err(|e| crate::error::Error::Io {
190 message: format!("failed to write MCP config to {}", path.display()),
191 source: e,
192 working_dir: None,
193 })?;
194
195 Ok(path)
196 }
197
198 #[cfg(feature = "tempfile")]
225 pub fn build_temp(&self) -> Result<TempMcpConfig> {
226 use std::io::Write;
227
228 let json = self.to_json()?;
229 let mut file = tempfile::Builder::new()
230 .suffix(".mcp.json")
231 .tempfile()
232 .map_err(|e| crate::error::Error::Io {
233 message: "failed to create temp MCP config file".to_string(),
234 source: e,
235 working_dir: None,
236 })?;
237
238 file.write_all(json.as_bytes())
239 .map_err(|e| crate::error::Error::Io {
240 message: "failed to write temp MCP config".to_string(),
241 source: e,
242 working_dir: None,
243 })?;
244
245 Ok(TempMcpConfig { file })
246 }
247}
248
249#[cfg(feature = "tempfile")]
254#[derive(Debug)]
255pub struct TempMcpConfig {
256 file: tempfile::NamedTempFile,
257}
258
259#[cfg(feature = "tempfile")]
260impl TempMcpConfig {
261 #[must_use]
265 pub fn path(&self) -> &str {
266 self.file
267 .path()
268 .to_str()
269 .expect("temp file path is valid UTF-8")
270 }
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276
277 #[test]
278 fn test_http_server_config() {
279 let config = McpConfigBuilder::new().http_server("my-hub", "http://127.0.0.1:9090");
280
281 let json = config.to_json().unwrap();
282 assert!(json.contains("my-hub"));
283 assert!(json.contains("http://127.0.0.1:9090"));
284 assert!(json.contains(r#""type": "http""#));
285 }
286
287 #[test]
288 fn test_stdio_server_config() {
289 let config = McpConfigBuilder::new().stdio_server(
290 "my-tool",
291 "npx",
292 ["my-mcp-server", "--port", "3000"],
293 );
294
295 let json = config.to_json().unwrap();
296 assert!(json.contains("my-tool"));
297 assert!(json.contains("npx"));
298 assert!(json.contains("my-mcp-server"));
299 assert!(json.contains(r#""type": "stdio""#));
300 }
301
302 #[test]
303 #[cfg(feature = "tempfile")]
304 fn test_build_temp() {
305 let config = McpConfigBuilder::new()
306 .http_server("hub", "http://localhost:9090")
307 .stdio_server("tool", "echo", ["hello"]);
308
309 let temp = config.build_temp().unwrap();
310 let path = temp.path();
311 assert!(path.ends_with(".mcp.json"));
312
313 let contents = std::fs::read_to_string(path).unwrap();
314 assert!(contents.contains("hub"));
315 assert!(contents.contains("localhost:9090"));
316 }
317
318 #[test]
319 fn test_multiple_servers() {
320 let config = McpConfigBuilder::new()
321 .http_server("hub", "http://localhost:9090")
322 .stdio_server("tool", "node", ["server.js"]);
323
324 let json = config.to_json().unwrap();
325 assert!(json.contains("hub"));
326 assert!(json.contains("tool"));
327 }
328}