1use serde::de;
2use serde::ser::SerializeMap;
3use serde::{Deserialize, Deserializer, Serialize, Serializer};
4use serde_json::Value;
5use std::collections::HashMap;
6
7#[derive(Debug, Clone)]
9pub struct McpConfig {
10 pub version: u32,
11 pub servers: Vec<McpServerConfig>,
12}
13
14fn default_version() -> u32 {
15 1
16}
17
18impl Default for McpConfig {
19 fn default() -> Self {
20 Self {
21 version: 1,
22 servers: Vec::new(),
23 }
24 }
25}
26
27#[derive(Debug, Clone, Deserialize)]
45struct McpConfigLegacyDisk {
46 #[serde(default = "default_version")]
47 version: u32,
48 #[serde(default)]
49 servers: Vec<McpServerConfig>,
50}
51
52#[derive(Debug, Clone, Deserialize)]
53struct McpServerConfigFlatDisk {
54 id: String,
55 #[serde(default)]
56 name: Option<String>,
57 #[serde(default)]
58 enabled: Option<bool>,
59 #[serde(default)]
60 disabled: bool,
61
62 #[serde(default)]
64 command: Option<String>,
65 #[serde(default)]
66 args: Vec<String>,
67 #[serde(default)]
68 cwd: Option<String>,
69 #[serde(default)]
70 env: HashMap<String, String>,
71 #[serde(default)]
72 env_encrypted: HashMap<String, String>,
73 #[serde(default)]
74 startup_timeout_ms: Option<u64>,
75
76 #[serde(default)]
78 url: Option<String>,
79 #[serde(default, deserialize_with = "deserialize_headers")]
80 headers: Vec<HeaderConfig>,
81 #[serde(default)]
82 connect_timeout_ms: Option<u64>,
83
84 #[serde(default)]
86 request_timeout_ms: Option<u64>,
87 #[serde(default)]
88 healthcheck_interval_ms: Option<u64>,
89 #[serde(default)]
90 reconnect: Option<ReconnectConfig>,
91 #[serde(default)]
92 allowed_tools: Vec<String>,
93 #[serde(default)]
94 denied_tools: Vec<String>,
95}
96
97fn deserialize_headers<'de, D>(deserializer: D) -> Result<Vec<HeaderConfig>, D::Error>
98where
99 D: Deserializer<'de>,
100{
101 let value = Value::deserialize(deserializer)?;
102
103 if value.is_null() {
104 return Ok(Vec::new());
105 }
106
107 if let Some(map) = value.as_object() {
109 let mut headers = Vec::with_capacity(map.len());
110 for (name, raw) in map.iter() {
111 let value = raw.as_str().unwrap_or("").to_string();
112 headers.push(HeaderConfig {
113 name: name.clone(),
114 value,
115 value_encrypted: None,
116 });
117 }
118 return Ok(headers);
119 }
120
121 if value.is_array() {
123 return serde_json::from_value::<Vec<HeaderConfig>>(value).map_err(de::Error::custom);
124 }
125
126 Err(de::Error::custom(
127 "MCP SSE headers must be an object map or an array",
128 ))
129}
130
131impl McpServerConfigFlatDisk {
132 fn into_internal(self) -> Result<McpServerConfig, String> {
133 let enabled = self.enabled.unwrap_or(!self.disabled);
134
135 let request_timeout_ms = self
136 .request_timeout_ms
137 .unwrap_or_else(default_request_timeout);
138 let healthcheck_interval_ms = self
139 .healthcheck_interval_ms
140 .unwrap_or_else(default_healthcheck_interval);
141 let reconnect = self.reconnect.unwrap_or_default();
142
143 let transport = match (self.command, self.url) {
144 (Some(command), None) => TransportConfig::Stdio(StdioConfig {
145 command,
146 args: self.args,
147 cwd: self.cwd,
148 env: self.env,
149 env_encrypted: self.env_encrypted,
150 startup_timeout_ms: self
151 .startup_timeout_ms
152 .unwrap_or_else(default_startup_timeout),
153 }),
154 (None, Some(url)) => TransportConfig::Sse(SseConfig {
155 url,
156 headers: self.headers,
157 connect_timeout_ms: self
158 .connect_timeout_ms
159 .unwrap_or_else(default_connect_timeout),
160 }),
161 (Some(_), Some(_)) => {
162 return Err("MCP server config cannot contain both 'command' and 'url'".to_string())
163 }
164 (None, None) => {
165 return Err(
166 "MCP server config must contain either 'command' (stdio) or 'url' (sse)"
167 .to_string(),
168 )
169 }
170 };
171
172 Ok(McpServerConfig {
173 id: self.id,
174 name: self.name,
175 enabled,
176 transport,
177 request_timeout_ms,
178 healthcheck_interval_ms,
179 reconnect,
180 allowed_tools: self.allowed_tools,
181 denied_tools: self.denied_tools,
182 })
183 }
184}
185
186#[derive(Debug, Clone, Serialize)]
187struct McpServerDiskOut {
188 #[serde(default, skip_serializing_if = "is_false")]
192 disabled: bool,
193
194 #[serde(skip_serializing_if = "Option::is_none")]
196 command: Option<String>,
197 #[serde(default, skip_serializing_if = "Vec::is_empty")]
198 args: Vec<String>,
199 #[serde(skip_serializing_if = "Option::is_none")]
200 cwd: Option<String>,
201 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
202 env: HashMap<String, String>,
203
204 #[serde(skip_serializing_if = "Option::is_none")]
206 url: Option<String>,
207 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
208 headers: HashMap<String, String>,
209
210 #[serde(skip_serializing_if = "Option::is_none")]
213 transport: Option<TransportConfig>,
214}
215
216fn is_false(value: &bool) -> bool {
217 !*value
218}
219
220impl From<&McpServerConfig> for McpServerDiskOut {
221 fn from(server: &McpServerConfig) -> Self {
222 let mut out = Self {
223 disabled: !server.enabled,
224 command: None,
225 args: Vec::new(),
226 cwd: None,
227 env: HashMap::new(),
228 url: None,
229 headers: HashMap::new(),
230 transport: None,
231 };
232
233 match &server.transport {
234 TransportConfig::Stdio(stdio) => {
235 out.command = Some(stdio.command.clone());
236 out.args = stdio.args.clone();
237 out.cwd = stdio.cwd.clone();
238 out.env = stdio.env.clone();
239 }
240 TransportConfig::Sse(sse) => {
241 out.url = Some(sse.url.clone());
242 out.headers = sse
243 .headers
244 .iter()
245 .filter(|h| !h.name.trim().is_empty())
246 .map(|h| (h.name.clone(), h.value.clone()))
247 .collect();
248 }
249 TransportConfig::StreamableHttp(config) => {
250 out.transport = Some(TransportConfig::StreamableHttp(config.clone()));
251 }
252 }
253
254 out
255 }
256}
257
258impl Serialize for McpConfig {
259 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
260 where
261 S: Serializer,
262 {
263 let mut map = serializer.serialize_map(Some(self.servers.len()))?;
264 for server in &self.servers {
265 let entry = McpServerDiskOut::from(server);
266 map.serialize_entry(&server.id, &entry)?;
267 }
268 map.end()
269 }
270}
271
272impl<'de> Deserialize<'de> for McpConfig {
273 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
274 where
275 D: Deserializer<'de>,
276 {
277 let value = Value::deserialize(deserializer)?;
278
279 if value.get("servers").is_some() {
281 let legacy: McpConfigLegacyDisk =
282 serde_json::from_value(value).map_err(de::Error::custom)?;
283 return Ok(Self {
284 version: legacy.version,
285 servers: legacy.servers,
286 });
287 }
288
289 let Some(obj) = value.as_object() else {
291 return Err(de::Error::custom(
292 "MCP config must be an object (legacy {version,servers} or a server map)",
293 ));
294 };
295
296 let mut servers = Vec::with_capacity(obj.len());
297 for (id, raw_entry) in obj.iter() {
298 let mut entry = raw_entry.clone();
299 let entry_obj = entry
300 .as_object_mut()
301 .ok_or_else(|| de::Error::custom("MCP server entry must be an object"))?;
302 entry_obj.insert("id".to_string(), Value::String(id.clone()));
304
305 if let Ok(server) = serde_json::from_value::<McpServerConfig>(entry.clone()) {
307 servers.push(server);
308 continue;
309 }
310
311 let flat: McpServerConfigFlatDisk =
312 serde_json::from_value(entry).map_err(de::Error::custom)?;
313 let server = flat.into_internal().map_err(de::Error::custom)?;
314 servers.push(server);
315 }
316
317 Ok(Self {
318 version: default_version(),
319 servers,
320 })
321 }
322}
323
324#[derive(Debug, Clone, Serialize, Deserialize)]
326pub struct McpServerConfig {
327 pub id: String,
329 #[serde(skip_serializing_if = "Option::is_none")]
331 pub name: Option<String>,
332 #[serde(default = "default_true")]
334 pub enabled: bool,
335 pub transport: TransportConfig,
337 #[serde(default = "default_request_timeout")]
339 pub request_timeout_ms: u64,
340 #[serde(default = "default_healthcheck_interval")]
342 pub healthcheck_interval_ms: u64,
343 #[serde(default)]
345 pub reconnect: ReconnectConfig,
346 #[serde(default)]
348 pub allowed_tools: Vec<String>,
349 #[serde(default)]
351 pub denied_tools: Vec<String>,
352}
353
354fn default_true() -> bool {
355 true
356}
357
358pub fn default_request_timeout() -> u64 {
359 60000 }
361
362pub fn default_healthcheck_interval() -> u64 {
363 30000 }
365
366#[derive(Debug, Clone, Serialize, Deserialize)]
368#[serde(tag = "type", rename_all = "lowercase")]
369pub enum TransportConfig {
370 Stdio(StdioConfig),
371 Sse(SseConfig),
372 #[serde(rename = "streamable_http")]
373 StreamableHttp(StreamableHttpConfig),
374}
375
376#[derive(Debug, Clone, Serialize, Deserialize)]
378pub struct StdioConfig {
379 pub command: String,
381 #[serde(default)]
383 pub args: Vec<String>,
384 #[serde(skip_serializing_if = "Option::is_none")]
386 pub cwd: Option<String>,
387 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
389 pub env: HashMap<String, String>,
390 #[serde(default, skip_serializing)]
396 pub env_encrypted: HashMap<String, String>,
397 #[serde(default = "default_startup_timeout")]
399 pub startup_timeout_ms: u64,
400}
401
402pub fn default_startup_timeout() -> u64 {
403 20000 }
405
406#[derive(Debug, Clone, Serialize, Deserialize)]
408pub struct SseConfig {
409 pub url: String,
411 #[serde(default)]
413 pub headers: Vec<HeaderConfig>,
414 #[serde(default = "default_connect_timeout")]
416 pub connect_timeout_ms: u64,
417}
418
419pub fn default_connect_timeout() -> u64 {
420 10000 }
422
423#[derive(Debug, Clone, Serialize, Deserialize)]
428pub struct StreamableHttpConfig {
429 pub url: String,
431 #[serde(default)]
433 pub headers: Vec<HeaderConfig>,
434 #[serde(default = "default_connect_timeout")]
436 pub connect_timeout_ms: u64,
437}
438
439#[derive(Debug, Clone, Serialize, Deserialize)]
441pub struct HeaderConfig {
442 pub name: String,
443 #[serde(default)]
445 pub value: String,
446 #[serde(default, skip_serializing)]
452 pub value_encrypted: Option<String>,
453}
454
455#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
457pub struct ReconnectConfig {
458 #[serde(default = "default_true")]
459 pub enabled: bool,
460 #[serde(default = "default_initial_backoff")]
462 pub initial_backoff_ms: u64,
463 #[serde(default = "default_max_backoff")]
465 pub max_backoff_ms: u64,
466 #[serde(default)]
468 pub max_attempts: u32,
469}
470
471impl Default for ReconnectConfig {
472 fn default() -> Self {
473 Self {
474 enabled: true,
475 initial_backoff_ms: 1000,
476 max_backoff_ms: 30000,
477 max_attempts: 0,
478 }
479 }
480}
481
482fn default_initial_backoff() -> u64 {
483 1000
484}
485
486fn default_max_backoff() -> u64 {
487 30000
488}
489
490#[cfg(test)]
491mod tests {
492 use super::*;
493
494 #[test]
495 fn test_mcp_config_default() {
496 let config = McpConfig::default();
497 assert_eq!(config.version, 1);
498 assert!(config.servers.is_empty());
499 }
500
501 #[test]
502 fn test_mcp_config_deserialization() {
503 let json = r#"{"version": 2, "servers": []}"#;
504 let config: McpConfig = serde_json::from_str(json).unwrap();
505 assert_eq!(config.version, 2);
506 assert!(config.servers.is_empty());
507 }
508
509 #[test]
510 fn test_mcp_config_default_version() {
511 let json = r#"{"servers": []}"#;
512 let config: McpConfig = serde_json::from_str(json).unwrap();
513 assert_eq!(config.version, 1);
514 }
515
516 #[test]
517 fn test_mcp_server_config_minimal() {
518 let json = r#"{
519 "id": "test-server",
520 "transport": {
521 "type": "stdio",
522 "command": "node"
523 }
524 }"#;
525 let config: McpServerConfig = serde_json::from_str(json).unwrap();
526 assert_eq!(config.id, "test-server");
527 assert!(config.enabled); assert_eq!(config.request_timeout_ms, 60000); assert_eq!(config.healthcheck_interval_ms, 30000); assert!(config.reconnect.enabled); assert!(config.allowed_tools.is_empty());
532 assert!(config.denied_tools.is_empty());
533 }
534
535 #[test]
536 fn test_mcp_server_config_full() {
537 let json = r#"{
538 "id": "test-server",
539 "name": "Test Server",
540 "enabled": false,
541 "transport": {
542 "type": "stdio",
543 "command": "node",
544 "args": ["server.js"],
545 "cwd": "/app",
546 "env": {"NODE_ENV": "production"},
547 "startup_timeout_ms": 30000
548 },
549 "request_timeout_ms": 120000,
550 "healthcheck_interval_ms": 60000,
551 "reconnect": {
552 "enabled": true,
553 "initial_backoff_ms": 2000,
554 "max_backoff_ms": 60000,
555 "max_attempts": 5
556 },
557 "allowed_tools": ["tool1", "tool2"],
558 "denied_tools": ["tool3"]
559 }"#;
560 let config: McpServerConfig = serde_json::from_str(json).unwrap();
561 assert_eq!(config.id, "test-server");
562 assert_eq!(config.name, Some("Test Server".to_string()));
563 assert!(!config.enabled);
564 assert_eq!(config.request_timeout_ms, 120000);
565 assert_eq!(config.healthcheck_interval_ms, 60000);
566 assert!(config.reconnect.enabled);
567 assert_eq!(config.reconnect.initial_backoff_ms, 2000);
568 assert_eq!(config.reconnect.max_backoff_ms, 60000);
569 assert_eq!(config.reconnect.max_attempts, 5);
570 assert_eq!(config.allowed_tools, vec!["tool1", "tool2"]);
571 assert_eq!(config.denied_tools, vec!["tool3"]);
572 }
573
574 #[test]
575 fn test_stdio_config() {
576 let json = r#"{
577 "type": "stdio",
578 "command": "python",
579 "args": ["-m", "server"],
580 "cwd": "/home/user",
581 "env": {"DEBUG": "1"},
582 "startup_timeout_ms": 15000
583 }"#;
584 let config: TransportConfig = serde_json::from_str(json).unwrap();
585 match config {
586 TransportConfig::Stdio(stdio) => {
587 assert_eq!(stdio.command, "python");
588 assert_eq!(stdio.args, vec!["-m", "server"]);
589 assert_eq!(stdio.cwd, Some("/home/user".to_string()));
590 assert_eq!(stdio.env.get("DEBUG"), Some(&"1".to_string()));
591 assert_eq!(stdio.startup_timeout_ms, 15000);
592 }
593 _ => panic!("Expected Stdio transport"),
594 }
595 }
596
597 #[test]
598 fn test_stdio_config_minimal() {
599 let json = r#"{
600 "type": "stdio",
601 "command": "node"
602 }"#;
603 let config: TransportConfig = serde_json::from_str(json).unwrap();
604 match config {
605 TransportConfig::Stdio(stdio) => {
606 assert_eq!(stdio.command, "node");
607 assert!(stdio.args.is_empty());
608 assert!(stdio.cwd.is_none());
609 assert!(stdio.env.is_empty());
610 assert_eq!(stdio.startup_timeout_ms, 20000); }
612 _ => panic!("Expected Stdio transport"),
613 }
614 }
615
616 #[test]
617 fn test_sse_config() {
618 let json = r#"{
619 "type": "sse",
620 "url": "http://localhost:8080/sse",
621 "headers": [
622 {"name": "Authorization", "value": "Bearer token123"}
623 ],
624 "connect_timeout_ms": 5000
625 }"#;
626 let config: TransportConfig = serde_json::from_str(json).unwrap();
627 match config {
628 TransportConfig::Sse(sse) => {
629 assert_eq!(sse.url, "http://localhost:8080/sse");
630 assert_eq!(sse.headers.len(), 1);
631 assert_eq!(sse.headers[0].name, "Authorization");
632 assert_eq!(sse.headers[0].value, "Bearer token123");
633 assert_eq!(sse.connect_timeout_ms, 5000);
634 }
635 _ => panic!("Expected SSE transport"),
636 }
637 }
638
639 #[test]
640 fn test_sse_config_minimal() {
641 let json = r#"{
642 "type": "sse",
643 "url": "http://localhost:8080/sse"
644 }"#;
645 let config: TransportConfig = serde_json::from_str(json).unwrap();
646 match config {
647 TransportConfig::Sse(sse) => {
648 assert_eq!(sse.url, "http://localhost:8080/sse");
649 assert!(sse.headers.is_empty());
650 assert_eq!(sse.connect_timeout_ms, 10000); }
652 _ => panic!("Expected SSE transport"),
653 }
654 }
655
656 #[test]
657 fn test_streamable_http_config() {
658 let json = r#"{
659 "type": "streamable_http",
660 "url": "http://localhost:3000/mcp",
661 "headers": [
662 {"name": "Authorization", "value": "Bearer token123"}
663 ],
664 "connect_timeout_ms": 5000
665 }"#;
666 let config: TransportConfig = serde_json::from_str(json).unwrap();
667 match config {
668 TransportConfig::StreamableHttp(cfg) => {
669 assert_eq!(cfg.url, "http://localhost:3000/mcp");
670 assert_eq!(cfg.headers.len(), 1);
671 assert_eq!(cfg.headers[0].name, "Authorization");
672 assert_eq!(cfg.connect_timeout_ms, 5000);
673 }
674 _ => panic!("Expected StreamableHttp transport"),
675 }
676 }
677
678 #[test]
679 fn test_streamable_http_config_minimal() {
680 let json = r#"{
681 "type": "streamable_http",
682 "url": "http://localhost:3000/mcp"
683 }"#;
684 let config: TransportConfig = serde_json::from_str(json).unwrap();
685 match config {
686 TransportConfig::StreamableHttp(cfg) => {
687 assert_eq!(cfg.url, "http://localhost:3000/mcp");
688 assert!(cfg.headers.is_empty());
689 assert_eq!(cfg.connect_timeout_ms, 10000); }
691 _ => panic!("Expected StreamableHttp transport"),
692 }
693 }
694
695 #[test]
696 fn test_streamable_http_round_trip() {
697 let cfg = McpConfig {
698 version: 1,
699 servers: vec![McpServerConfig {
700 id: "test-sh".to_string(),
701 name: None,
702 enabled: true,
703 transport: TransportConfig::StreamableHttp(StreamableHttpConfig {
704 url: "http://localhost:3000/mcp".to_string(),
705 headers: vec![HeaderConfig {
706 name: "Authorization".to_string(),
707 value: "Bearer token".to_string(),
708 value_encrypted: None,
709 }],
710 connect_timeout_ms: 5000,
711 }),
712 request_timeout_ms: default_request_timeout(),
713 healthcheck_interval_ms: default_healthcheck_interval(),
714 reconnect: ReconnectConfig::default(),
715 allowed_tools: vec![],
716 denied_tools: vec![],
717 }],
718 };
719
720 let json = serde_json::to_string(&cfg).unwrap();
721 let parsed: McpConfig = serde_json::from_str(&json).unwrap();
722 assert_eq!(parsed.servers.len(), 1);
723 match &parsed.servers[0].transport {
724 TransportConfig::StreamableHttp(sh) => {
725 assert_eq!(sh.url, "http://localhost:3000/mcp");
726 assert_eq!(sh.connect_timeout_ms, 5000);
727 }
728 _ => panic!("Expected StreamableHttp transport"),
729 }
730 }
731
732 #[test]
733 fn test_reconnect_config_default() {
734 let config = ReconnectConfig::default();
735 assert!(config.enabled);
736 assert_eq!(config.initial_backoff_ms, 1000);
737 assert_eq!(config.max_backoff_ms, 30000);
738 assert_eq!(config.max_attempts, 0); }
740
741 #[test]
742 fn test_reconnect_config_unlimited_attempts() {
743 let json = r#"{
744 "enabled": true,
745 "initial_backoff_ms": 500,
746 "max_backoff_ms": 10000
747 }"#;
748 let config: ReconnectConfig = serde_json::from_str(json).unwrap();
749 assert!(config.enabled);
750 assert_eq!(config.initial_backoff_ms, 500);
751 assert_eq!(config.max_backoff_ms, 10000);
752 assert_eq!(config.max_attempts, 0);
753 }
754
755 #[test]
756 fn test_reconnect_config_disabled() {
757 let json = r#"{"enabled": false}"#;
758 let config: ReconnectConfig = serde_json::from_str(json).unwrap();
759 assert!(!config.enabled);
760 }
761
762 #[test]
763 fn test_header_config() {
764 let header = HeaderConfig {
765 name: "Content-Type".to_string(),
766 value: "application/json".to_string(),
767 value_encrypted: None,
768 };
769 assert_eq!(header.name, "Content-Type");
770 assert_eq!(header.value, "application/json");
771 }
772
773 #[test]
774 fn test_full_mcp_config() {
775 let json = r#"{
776 "version": 1,
777 "servers": [
778 {
779 "id": "fs-server",
780 "transport": {
781 "type": "stdio",
782 "command": "mcp-server-filesystem"
783 }
784 },
785 {
786 "id": "web-server",
787 "transport": {
788 "type": "sse",
789 "url": "http://localhost:3000/sse"
790 }
791 }
792 ]
793 }"#;
794 let config: McpConfig = serde_json::from_str(json).unwrap();
795 assert_eq!(config.servers.len(), 2);
796 assert_eq!(config.servers[0].id, "fs-server");
797 assert_eq!(config.servers[1].id, "web-server");
798 }
799
800 #[test]
801 fn test_mcp_config_deserialization_mainstream_map_stdio() {
802 let json = r#"{
803 "filesystem": {
804 "command": "node",
805 "args": ["server.js"],
806 "env": {"MCP_ROOT": "/tmp"}
807 }
808 }"#;
809
810 let config: McpConfig = serde_json::from_str(json).unwrap();
811 assert_eq!(config.version, 1);
812 assert_eq!(config.servers.len(), 1);
813 assert_eq!(config.servers[0].id, "filesystem");
814
815 match &config.servers[0].transport {
816 TransportConfig::Stdio(stdio) => {
817 assert_eq!(stdio.command, "node");
818 assert_eq!(stdio.args, vec!["server.js"]);
819 assert_eq!(stdio.env.get("MCP_ROOT").map(|s| s.as_str()), Some("/tmp"));
820 }
821 _ => panic!("Expected stdio transport"),
822 }
823 }
824
825 #[test]
826 fn test_mcp_config_serialization_is_map() {
827 let mut env_encrypted = HashMap::new();
828 env_encrypted.insert("TOKEN".to_string(), "nonce:ciphertext".to_string());
829
830 let cfg = McpConfig {
831 version: 1,
832 servers: vec![McpServerConfig {
833 id: "demo".to_string(),
834 name: Some("Demo".to_string()),
835 enabled: true,
836 transport: TransportConfig::Stdio(StdioConfig {
837 command: "node".to_string(),
838 args: vec!["server.js".to_string()],
839 cwd: None,
840 env: HashMap::new(),
841 env_encrypted,
842 startup_timeout_ms: default_startup_timeout(),
843 }),
844 request_timeout_ms: default_request_timeout(),
845 healthcheck_interval_ms: default_healthcheck_interval(),
846 reconnect: ReconnectConfig::default(),
847 allowed_tools: vec![],
848 denied_tools: vec![],
849 }],
850 };
851
852 let value = serde_json::to_value(&cfg).unwrap();
853 assert!(value.get("servers").is_none());
854 assert!(value.get("demo").is_some());
855 }
856
857 #[test]
858 fn test_server_config_disabled() {
859 let json = r#"{
860 "id": "disabled-server",
861 "enabled": false,
862 "transport": {"type": "stdio", "command": "node"}
863 }"#;
864 let config: McpServerConfig = serde_json::from_str(json).unwrap();
865 assert!(!config.enabled);
866 }
867}