mur_common/agent/
mcp_status.rs1use serde::{Deserialize, Serialize};
10
11pub const MCP_STATUS_FILE: &str = "mcp_status.json";
14
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17pub struct McpServerStatus {
18 pub name: String,
19 pub tools: usize,
21 #[serde(default, skip_serializing_if = "Option::is_none")]
23 pub error: Option<String>,
24}
25
26#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
28pub struct McpStatus {
29 #[serde(default)]
32 pub pid: u32,
33 pub servers: Vec<McpServerStatus>,
34}
35
36impl McpStatus {
37 pub fn failed(&self) -> impl Iterator<Item = &McpServerStatus> {
39 self.servers.iter().filter(|s| s.error.is_some())
40 }
41
42 pub fn save(&self, path: &std::path::Path) -> std::io::Result<()> {
44 let tmp = path.with_extension("json.tmp");
45 std::fs::write(&tmp, serde_json::to_vec_pretty(self)?)?;
46 std::fs::rename(&tmp, path)
47 }
48
49 pub fn load(path: &std::path::Path) -> Option<Self> {
51 serde_json::from_slice(&std::fs::read(path).ok()?).ok()
52 }
53}
54
55#[cfg(test)]
56mod tests {
57 use super::*;
58
59 #[test]
60 fn round_trips_and_reports_only_failures() {
61 let d = tempfile::tempdir().unwrap();
62 let p = d.path().join(MCP_STATUS_FILE);
63 let s = McpStatus {
64 pid: 42,
65 servers: vec![
66 McpServerStatus {
67 name: "ok".into(),
68 tools: 3,
69 error: None,
70 },
71 McpServerStatus {
72 name: "bad".into(),
73 tools: 0,
74 error: Some("io: Operation not permitted".into()),
75 },
76 ],
77 };
78 s.save(&p).unwrap();
79 let back = McpStatus::load(&p).unwrap();
80 assert_eq!(back, s);
81 let failed: Vec<_> = back.failed().map(|f| f.name.as_str()).collect();
82 assert_eq!(failed, ["bad"]);
83 }
84
85 #[test]
86 fn missing_file_is_none() {
87 let d = tempfile::tempdir().unwrap();
88 assert!(McpStatus::load(&d.path().join(MCP_STATUS_FILE)).is_none());
89 }
90}