scryer-mcp 0.2.1

Model Context Protocol (MCP) server for Scryer code intelligence
//! Identity of the running binary, so a long-lived daemon can tell when it is out of date.
//!
//! `CARGO_PKG_VERSION` doesn't change between rebuilds, so the build ID comes from the
//! executable file itself (size and modification time), read once at startup. Reinstalling
//! the binary (`cargo install`) changes the file on disk but not this process's ID.

use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use std::time::UNIX_EPOCH;

use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

struct Startup {
    exe: Option<PathBuf>,
    build_id: String,
}

fn startup() -> &'static Startup {
    static STARTUP: OnceLock<Startup> = OnceLock::new();
    STARTUP.get_or_init(|| {
        let exe = std::env::current_exe().ok();
        let build_id = exe
            .as_deref()
            .and_then(file_build_id)
            .unwrap_or_else(|| "unknown".to_string());
        Startup { exe, build_id }
    })
}

/// Build ID of the binary at `path`: its size and modification time, in hex.
fn file_build_id(path: &Path) -> Option<String> {
    let meta = std::fs::metadata(path).ok()?;
    let mtime = meta.modified().ok()?.duration_since(UNIX_EPOCH).ok()?;
    Some(format!("{:x}-{:x}", meta.len(), mtime.as_nanos()))
}

/// Build ID of this process's executable as it was at startup.
pub fn build_id() -> &'static str {
    &startup().build_id
}

/// Whether the executable this process started from has since been replaced or removed.
pub fn binary_replaced() -> bool {
    let startup = startup();
    let Some(exe) = &startup.exe else {
        return false;
    };
    file_build_id(exe).as_deref() != Some(startup.build_id.as_str())
}

/// Version and build of the server answering a request.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct ServerBuild {
    pub version: String,
    pub build_id: String,
    /// The binary was reinstalled after this server started: it is serving old code.
    /// Restart it (`scryer daemon stop`, then reconnect the MCP client).
    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
    pub stale: bool,
}

impl ServerBuild {
    pub fn current() -> Self {
        Self {
            version: crate::mcp_version().to_string(),
            build_id: build_id().to_string(),
            stale: binary_replaced(),
        }
    }
}