codelens-mcp 1.9.9

Pure Rust MCP server for code intelligence — 101 tools (+6 semantic), 25 languages, tree-sitter-first, 50-87% fewer tokens
use super::{
    AppState, ToolResult, optional_bool, optional_string, optional_usize, required_string,
    success_meta,
};
use crate::protocol::BackendKind;
use crate::tools::symbols::flatten_symbols;
use codelens_engine::{
    find_circular_dependencies, find_dead_code_v2, find_scoped_references, get_blast_radius,
    get_callees, get_callers, get_change_coupling, get_changed_files, get_importance,
    get_importers, search_for_pattern,
};
use serde_json::json;

pub fn get_changed_files_tool(state: &AppState, arguments: &serde_json::Value) -> ToolResult {
    let git_ref = optional_string(arguments, "ref");
    let include_untracked = optional_bool(arguments, "include_untracked", true);
    let changed = get_changed_files(&state.project(), git_ref, include_untracked)?;
    let ref_label = git_ref.unwrap_or("HEAD");
    Ok((
        json!({ "ref": ref_label, "files": changed, "count": changed.len() }),
        success_meta(BackendKind::Git, 0.95),
    ))
}

pub fn get_impact_analysis(state: &AppState, arguments: &serde_json::Value) -> ToolResult {
    let file_path = required_string(arguments, "file_path")?;
    let max_depth = optional_usize(arguments, "max_depth", 3);

    let blast = get_blast_radius(&state.project(), file_path, max_depth, &state.graph_cache())
        .unwrap_or_default();
    let symbols = state
        .symbol_index()
        .get_symbols_overview_cached(file_path, 1)
        .unwrap_or_default();
    let symbol_names: Vec<_> = flatten_symbols(&symbols)
        .iter()
        .map(|s| json!({"name": s.name, "kind": s.kind.as_label(), "line": s.line}))
        .collect();
    let importers =
        get_importers(&state.project(), file_path, 20, &state.graph_cache()).unwrap_or_default();
    let affected: Vec<_> = blast
        .iter()
        .map(|b| {
            let sym_count = state
                .symbol_index()
                .get_symbols_overview_cached(&b.file, 1)
                .map(|s| s.len())
                .unwrap_or(0);
            json!({"file": b.file, "depth": b.depth, "symbol_count": sym_count})
        })
        .collect();

    Ok((
        json!({
            "file": file_path,
            "symbols": symbol_names,
            "symbol_count": symbol_names.len(),
            "direct_importers": importers,
            "blast_radius": affected,
            "total_affected_files": affected.len(),
        }),
        success_meta(BackendKind::Hybrid, 0.85),
    ))
}

pub fn find_importers_tool(state: &AppState, arguments: &serde_json::Value) -> ToolResult {
    let file_path = required_string(arguments, "file_path")?;
    let max_results = optional_usize(arguments, "max_results", 50);
    Ok(get_importers(
        &state.project(),
        file_path,
        max_results,
        &state.graph_cache(),
    )
    .map(|value| {
        (
            json!({ "file": file_path, "importers": value, "count": value.len() }),
            success_meta(BackendKind::Hybrid, 0.87),
        )
    })?)
}

pub fn get_symbol_importance(state: &AppState, arguments: &serde_json::Value) -> ToolResult {
    let top_n = optional_usize(arguments, "top_n", 20);
    Ok(
        get_importance(&state.project(), top_n, &state.graph_cache()).map(|value| {
            (
                json!({ "ranking": value, "count": value.len() }),
                success_meta(BackendKind::Hybrid, 0.84),
            )
        })?,
    )
}

pub fn find_dead_code_v2_tool(state: &AppState, arguments: &serde_json::Value) -> ToolResult {
    let max_results = optional_usize(arguments, "max_results", 50);
    Ok(
        find_dead_code_v2(&state.project(), max_results, &state.graph_cache()).map(|value| {
            (
                json!({ "dead_code": value, "count": value.len() }),
                success_meta(BackendKind::Hybrid, 0.82),
            )
        })?,
    )
}

pub fn find_referencing_code_snippets(
    state: &AppState,
    arguments: &serde_json::Value,
) -> ToolResult {
    let symbol_name = required_string(arguments, "symbol_name")?;
    let file_glob = optional_string(arguments, "file_glob");
    let context_lines = optional_usize(arguments, "context_lines", 2);
    let max_results = optional_usize(arguments, "max_results", 50);
    Ok(search_for_pattern(
        &state.project(),
        symbol_name,
        file_glob,
        max_results,
        context_lines,
        context_lines,
    )
    .map(|matches| {
        let snippets = matches
            .iter()
            .map(|m| {
                let mut obj = json!({
                    "file_path": m.file_path,
                    "line": m.line,
                    "column": m.column,
                    "matched_text": m.matched_text,
                    "line_content": m.line_content,
                });
                if !m.context_before.is_empty() {
                    obj["context_before"] = json!(m.context_before);
                }
                if !m.context_after.is_empty() {
                    obj["context_after"] = json!(m.context_after);
                }
                obj
            })
            .collect::<Vec<_>>();
        (
            json!({ "snippets": snippets, "count": snippets.len() }),
            success_meta(BackendKind::Filesystem, 0.92),
        )
    })?)
}

pub fn find_scoped_references_tool(state: &AppState, arguments: &serde_json::Value) -> ToolResult {
    let symbol_name = required_string(arguments, "symbol_name")?;
    let file_path = optional_string(arguments, "file_path");
    let max_results = optional_usize(arguments, "max_results", 50);
    Ok(
        find_scoped_references(&state.project(), symbol_name, file_path, max_results).map(
            |refs| {
                (
                    json!({ "references": refs, "count": refs.len() }),
                    success_meta(BackendKind::TreeSitter, 0.95),
                )
            },
        )?,
    )
}

pub fn get_callers_tool(state: &AppState, arguments: &serde_json::Value) -> ToolResult {
    let function_name = required_string(arguments, "function_name")?;
    let max_results = optional_usize(arguments, "max_results", 50);
    Ok(
        get_callers(&state.project(), function_name, max_results).map(|value| {
            (
                json!({ "function": function_name, "callers": value, "count": value.len() }),
                success_meta(BackendKind::Hybrid, 0.85),
            )
        })?,
    )
}

pub fn get_callees_tool(state: &AppState, arguments: &serde_json::Value) -> ToolResult {
    let function_name = required_string(arguments, "function_name")?;
    let file_path = optional_string(arguments, "file_path");
    let max_results = optional_usize(arguments, "max_results", 50);
    Ok(
        get_callees(&state.project(), function_name, file_path, max_results).map(|value| {
            (
                json!({ "function": function_name, "callees": value, "count": value.len() }),
                success_meta(BackendKind::Hybrid, 0.85),
            )
        })?,
    )
}

pub fn find_circular_dependencies_tool(
    state: &AppState,
    arguments: &serde_json::Value,
) -> ToolResult {
    let max_results = optional_usize(arguments, "max_results", 50);
    Ok(
        find_circular_dependencies(&state.project(), max_results, &state.graph_cache()).map(
            |value| {
                (
                    json!({ "cycles": value, "count": value.len() }),
                    success_meta(BackendKind::Hybrid, 0.88),
                )
            },
        )?,
    )
}

pub fn get_change_coupling_tool(state: &AppState, arguments: &serde_json::Value) -> ToolResult {
    let months = optional_usize(arguments, "months", 6);
    let min_strength = arguments
        .get("min_strength")
        .and_then(|v| v.as_f64())
        .unwrap_or(0.3);
    let min_commits = optional_usize(arguments, "min_commits", 3);
    let max_results = optional_usize(arguments, "max_results", 30);
    Ok(get_change_coupling(
        &state.project(),
        months,
        min_strength,
        min_commits,
        max_results,
    )
    .map(|value| {
        (
            json!({ "coupling": value, "count": value.len() }),
            success_meta(BackendKind::Git, 0.85),
        )
    })?)
}

pub fn get_architecture_tool(state: &AppState, arguments: &serde_json::Value) -> ToolResult {
    let min_size = arguments
        .get("min_community_size")
        .and_then(|v| v.as_u64())
        .unwrap_or(2) as usize;

    let graph = state.graph_cache().get_or_build(&state.project())?;
    let overview = codelens_engine::community::detect_communities(&graph, min_size)?;

    Ok((
        json!({
            "communities": overview.communities,
            "total_files": overview.total_files,
            "total_edges": overview.total_edges,
            "modularity": (overview.modularity * 1000.0).round() / 1000.0,
            "community_count": overview.communities.len(),
        }),
        success_meta(BackendKind::Hybrid, 0.88),
    ))
}