use serde::{Deserialize, Serialize};
pub const MCP_STATUS_FILE: &str = "mcp_status.json";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct McpServerStatus {
pub name: String,
pub tools: usize,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct McpStatus {
#[serde(default)]
pub pid: u32,
pub servers: Vec<McpServerStatus>,
}
impl McpStatus {
pub fn failed(&self) -> impl Iterator<Item = &McpServerStatus> {
self.servers.iter().filter(|s| s.error.is_some())
}
pub fn save(&self, path: &std::path::Path) -> std::io::Result<()> {
let tmp = path.with_extension("json.tmp");
std::fs::write(&tmp, serde_json::to_vec_pretty(self)?)?;
std::fs::rename(&tmp, path)
}
pub fn load(path: &std::path::Path) -> Option<Self> {
serde_json::from_slice(&std::fs::read(path).ok()?).ok()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trips_and_reports_only_failures() {
let d = tempfile::tempdir().unwrap();
let p = d.path().join(MCP_STATUS_FILE);
let s = McpStatus {
pid: 42,
servers: vec![
McpServerStatus {
name: "ok".into(),
tools: 3,
error: None,
},
McpServerStatus {
name: "bad".into(),
tools: 0,
error: Some("io: Operation not permitted".into()),
},
],
};
s.save(&p).unwrap();
let back = McpStatus::load(&p).unwrap();
assert_eq!(back, s);
let failed: Vec<_> = back.failed().map(|f| f.name.as_str()).collect();
assert_eq!(failed, ["bad"]);
}
#[test]
fn missing_file_is_none() {
let d = tempfile::tempdir().unwrap();
assert!(McpStatus::load(&d.path().join(MCP_STATUS_FILE)).is_none());
}
}