lean-host-mcp 0.4.1

MCP server hosting Lean 4 via a supervised lean-rs-worker child
Documentation
//! Shared Lean source-file ingestion for proof-agent tools.

use std::path::{Path, PathBuf};

use crate::error::{Result, ServerError};

pub(crate) struct QueryFile {
    pub resolved: PathBuf,
    pub hash: [u8; 32],
    pub imports: Vec<String>,
    pub source: String,
}

pub(crate) fn read_query_file(root: &Path, path: &Path) -> Result<QueryFile> {
    let resolved = resolve_path(root, path).canonicalize().map_err(ServerError::Io)?;
    let bytes = std::fs::read(&resolved).map_err(ServerError::Io)?;
    let hash = crate::cache::hash_bytes(&bytes);
    let source = String::from_utf8(bytes).map_err(|e| ServerError::Internal(format!("file not UTF-8: {e}")))?;
    let imports = header_imports(&source);
    Ok(QueryFile {
        resolved,
        hash,
        imports,
        source,
    })
}

pub(crate) fn resolve_path(root: &Path, path: &Path) -> PathBuf {
    if path.is_absolute() {
        path.to_path_buf()
    } else {
        root.join(path)
    }
}

pub(crate) fn module_name_for_file(root: &Path, path: &Path) -> Option<String> {
    let relative = path.strip_prefix(root).ok()?;
    if relative.extension()? != "lean" {
        return None;
    }
    let stemmed = relative.with_extension("");
    let parts = stemmed
        .components()
        .map(|component| component.as_os_str().to_str())
        .collect::<Option<Vec<_>>>()?;
    if parts.is_empty() { None } else { Some(parts.join(".")) }
}

pub(crate) fn header_imports(source: &str) -> Vec<String> {
    source
        .lines()
        .filter_map(|line| {
            let line = line.split_once("--").map_or(line, |(before, _)| before);
            let mut words = line.split_whitespace();
            let mut token = words.next()?;
            if token == "public" {
                token = words.next()?;
            }
            if token == "meta" {
                token = words.next()?;
            }
            if token != "import" {
                return None;
            }
            if words.clone().next() == Some("all") {
                let _ = words.next();
            }
            words.next().map(str::to_owned)
        })
        .collect()
}