sloc-mcp 1.5.6

MCP stdio server exposing oxide-sloc analysis tools to AI agents
use crate::config::McpConfig;
use anyhow::Result;
use serde_json::{json, Value};
use std::path::PathBuf;
use tokio::process::Command;

pub async fn analyze_path(
    path: String,
    config_file: Option<String>,
    cfg: &McpConfig,
) -> Result<Value> {
    let tmp = tempfile_path();
    let mut cmd = Command::new(&cfg.bin_path);
    cmd.arg("analyze")
        .arg(&path)
        .arg("--json-out")
        .arg(&tmp)
        .arg("--quiet");
    if let Some(cf) = &config_file {
        cmd.arg("--config").arg(cf);
    }

    let output = cmd.output().await?;
    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        anyhow::bail!("oxide-sloc analyze failed: {stderr}");
    }

    let content = tokio::fs::read_to_string(&tmp).await?;
    let _ = tokio::fs::remove_file(&tmp).await;

    let run: Value = serde_json::from_str(&content)?;
    Ok(summarize_run(&run))
}

fn summarize_run(run: &Value) -> Value {
    let totals = &run["summary_totals"];
    let languages: Vec<Value> = run["totals_by_language"]
        .as_array()
        .map(|arr| {
            arr.iter()
                .map(|l| {
                    json!({
                        "language": l["language"],
                        "files": l["files"],
                        "code_lines": l["code_lines"],
                        "comment_lines": l["comment_lines"],
                        "blank_lines": l["blank_lines"],
                        "functions": l["functions"],
                        "test_count": l["test_count"],
                    })
                })
                .collect()
        })
        .unwrap_or_default();

    json!({
        "files_analyzed": totals["files_analyzed"],
        "files_skipped": totals["files_skipped"],
        "code_lines": totals["code_lines"],
        "comment_lines": totals["comment_lines"],
        "blank_lines": totals["blank_lines"],
        "total_physical_lines": totals["total_physical_lines"],
        "functions": totals["functions"],
        "classes": totals["classes"],
        "test_count": totals["test_count"],
        "test_assertion_count": totals["test_assertion_count"],
        "git_branch": run["git_branch"],
        "git_commit_short": run["git_commit_short"],
        "git_nearest_tag": run["git_nearest_tag"],
        "languages": languages,
    })
}

fn tempfile_path() -> PathBuf {
    let mut p = std::env::temp_dir();
    p.push(format!("sloc-mcp-{}.json", uuid_v4()));
    p
}

fn uuid_v4() -> String {
    use std::time::{SystemTime, UNIX_EPOCH};
    let t = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default();
    format!("{:x}-{:x}", t.as_secs(), t.subsec_nanos())
}