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 })
172 }
173 }
174
175 pub fn write_to(&self, path: impl AsRef<Path>) -> Result<PathBuf> {
177 let path = path.as_ref().to_path_buf();
178 let json = self.to_json()?;
179
180 if let Some(parent) = path.parent() {
181 std::fs::create_dir_all(parent).map_err(|e| crate::error::Error::Io {
182 message: format!("failed to create directory: {}", parent.display()),
183 source: e,
184 working_dir: None,
185 })?;
186 }
187
188 std::fs::write(&path, json).map_err(|e| crate::error::Error::Io {
189 message: format!("failed to write MCP config to {}", path.display()),
190 source: e,
191 working_dir: None,
192 })?;
193
194 Ok(path)
195 }
196
197 #[cfg(feature = "tempfile")]
224 pub fn build_temp(&self) -> Result<TempMcpConfig> {
225 use std::io::Write;
226
227 let json = self.to_json()?;
228 let mut file = tempfile::Builder::new()
229 .suffix(".mcp.json")
230 .tempfile()
231 .map_err(|e| crate::error::Error::Io {
232 message: "failed to create temp MCP config file".to_string(),
233 source: e,
234 working_dir: None,
235 })?;
236
237 file.write_all(json.as_bytes())
238 .map_err(|e| crate::error::Error::Io {
239 message: "failed to write temp MCP config".to_string(),
240 source: e,
241 working_dir: None,
242 })?;
243
244 Ok(TempMcpConfig { file })
245 }
246}
247
248#[cfg(feature = "tempfile")]
253#[derive(Debug)]
254pub struct TempMcpConfig {
255 file: tempfile::NamedTempFile,
256}
257
258#[cfg(feature = "tempfile")]
259impl TempMcpConfig {
260 #[must_use]
264 pub fn path(&self) -> &str {
265 self.file
266 .path()
267 .to_str()
268 .expect("temp file path is valid UTF-8")
269 }
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275
276 #[test]
277 fn test_http_server_config() {
278 let config = McpConfigBuilder::new().http_server("my-hub", "http://127.0.0.1:9090");
279
280 let json = config.to_json().unwrap();
281 assert!(json.contains("my-hub"));
282 assert!(json.contains("http://127.0.0.1:9090"));
283 assert!(json.contains(r#""type": "http""#));
284 }
285
286 #[test]
287 fn test_stdio_server_config() {
288 let config = McpConfigBuilder::new().stdio_server(
289 "my-tool",
290 "npx",
291 ["my-mcp-server", "--port", "3000"],
292 );
293
294 let json = config.to_json().unwrap();
295 assert!(json.contains("my-tool"));
296 assert!(json.contains("npx"));
297 assert!(json.contains("my-mcp-server"));
298 assert!(json.contains(r#""type": "stdio""#));
299 }
300
301 #[test]
302 #[cfg(feature = "tempfile")]
303 fn test_build_temp() {
304 let config = McpConfigBuilder::new()
305 .http_server("hub", "http://localhost:9090")
306 .stdio_server("tool", "echo", ["hello"]);
307
308 let temp = config.build_temp().unwrap();
309 let path = temp.path();
310 assert!(path.ends_with(".mcp.json"));
311
312 let contents = std::fs::read_to_string(path).unwrap();
313 assert!(contents.contains("hub"));
314 assert!(contents.contains("localhost:9090"));
315 }
316
317 #[test]
318 fn test_multiple_servers() {
319 let config = McpConfigBuilder::new()
320 .http_server("hub", "http://localhost:9090")
321 .stdio_server("tool", "node", ["server.js"]);
322
323 let json = config.to_json().unwrap();
324 assert!(json.contains("hub"));
325 assert!(json.contains("tool"));
326 }
327}