use anyhow::{Context, Result, anyhow};
use serde::Deserialize;
use std::path::PathBuf;
use std::sync::RwLock;
use std::time::SystemTime;
#[derive(Debug, Clone, Deserialize)]
pub struct McpDiscovery {
pub url: String,
#[serde(default)]
pub pid: Option<u32>,
#[serde(default)]
pub started_at: Option<String>,
}
#[derive(Clone)]
struct Cached {
mtime: Option<SystemTime>,
value: McpDiscovery,
}
static CACHE: RwLock<Option<Cached>> = RwLock::new(None);
pub fn discovery_path() -> Option<PathBuf> {
directories::UserDirs::new().map(|u| u.home_dir().join(".construct").join("mcp.json"))
}
fn file_mtime(path: &std::path::Path) -> Option<SystemTime> {
std::fs::metadata(path).and_then(|m| m.modified()).ok()
}
pub fn read_construct_mcp() -> Result<McpDiscovery> {
let path = discovery_path()
.ok_or_else(|| anyhow!("could not resolve home directory for ~/.construct/mcp.json"))?;
let current_mtime = file_mtime(&path);
if let Some(cached) = CACHE.read().ok().and_then(|g| g.clone()) {
if cached.mtime == current_mtime {
return Ok(cached.value);
}
}
let bytes = std::fs::read(&path).with_context(|| format!("reading {}", path.display()))?;
let parsed: McpDiscovery =
serde_json::from_slice(&bytes).with_context(|| format!("parsing {}", path.display()))?;
if let Ok(mut guard) = CACHE.write() {
*guard = Some(Cached {
mtime: current_mtime,
value: parsed.clone(),
});
}
Ok(parsed)
}
#[cfg(test)]
pub fn parse_discovery(bytes: &[u8]) -> Result<McpDiscovery> {
Ok(serde_json::from_slice(bytes)?)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_valid_discovery() {
let payload =
br#"{"url":"http://127.0.0.1:54500/mcp","pid":1,"started_at":"2026-04-17T00:00:00Z"}"#;
let d = parse_discovery(payload).unwrap();
assert_eq!(d.url, "http://127.0.0.1:54500/mcp");
assert_eq!(d.pid, Some(1));
}
#[test]
fn parses_minimal_discovery() {
let payload = br#"{"url":"http://x/y"}"#;
let d = parse_discovery(payload).unwrap();
assert_eq!(d.url, "http://x/y");
assert_eq!(d.pid, None);
}
#[test]
fn rejects_bad_json() {
assert!(parse_discovery(b"not json").is_err());
}
}