codelens-mcp 1.8.0

Pure Rust MCP server for code intelligence — 89 tools (+6 semantic), 25 languages, tree-sitter-first, 50-87% fewer tokens
use crate::AppState;
use crate::tool_runtime::{ToolResult, required_string};
use crate::tools::report_contract::make_handle_response;
use crate::tools::report_utils::{stable_cache_key, strings_from_array};
use serde_json::{Value, json};
use std::collections::BTreeMap;

pub fn verify_change_readiness(state: &AppState, arguments: &Value) -> ToolResult {
    let task = required_string(arguments, "task")?;
    let ranked = crate::tools::symbols::get_ranked_context(
        state,
        &json!({"query": task, "max_tokens": 1200, "include_body": false, "depth": 2}),
    )?
    .0;
    let requested_changed_files = strings_from_array(
        arguments
            .get("changed_files")
            .and_then(|value| value.as_array()),
        "file",
        8,
    );
    let changed = if requested_changed_files.is_empty() {
        crate::tools::graph::get_changed_files_tool(state, &json!({"include_untracked": true}))
            .map(|out| out.0)
            .unwrap_or_else(
                |_| json!({"files": [], "count": 0, "note": "git metadata unavailable"}),
            )
    } else {
        json!({
            "files": requested_changed_files
                .iter()
                .map(|path| json!({"path": path, "status": "provided"}))
                .collect::<Vec<_>>(),
            "count": requested_changed_files.len(),
            "source": "provided",
        })
    };
    let ranked_symbols = ranked
        .get("symbols")
        .and_then(|value| value.as_array())
        .cloned()
        .unwrap_or_default();
    let ranked_files = ranked_symbols
        .iter()
        .take(5)
        .map(|entry| {
            json!({
                "file": entry.get("file").or_else(|| entry.get("file_path")).and_then(|v| v.as_str()).unwrap_or_default(),
                "symbol": entry.get("name").and_then(|v| v.as_str()).unwrap_or_default(),
                "kind": entry.get("kind").and_then(|v| v.as_str()).unwrap_or_default(),
                "score": entry.get("relevance_score").cloned().unwrap_or(json!(0))
            })
        })
        .collect::<Vec<_>>();
    let top_findings = ranked_files
        .iter()
        .take(3)
        .filter_map(|entry| {
            Some(format!(
                "{}: verify {} first",
                entry.get("symbol")?.as_str()?,
                entry.get("file")?.as_str()?
            ))
        })
        .collect::<Vec<_>>();
    let mut sections = BTreeMap::new();
    sections.insert(
        "ranked_files".to_owned(),
        json!({
            "task": task,
            "ranked_files": ranked_files,
        }),
    );
    sections.insert("raw_ranked_context".to_owned(), ranked);
    sections.insert("changed_files".to_owned(), changed);
    let touched_files = strings_from_array(
        sections
            .get("changed_files")
            .and_then(|value| value.get("files"))
            .and_then(|value| value.as_array()),
        "path",
        6,
    );
    make_handle_response(
        state,
        "verify_change_readiness",
        stable_cache_key(
            "verify_change_readiness",
            arguments,
            &["task", "profile_hint", "changed_files"],
        ),
        format!("Verifier-first readiness report for `{task}` with blockers and preflight cues."),
        top_findings,
        0.91,
        vec![
            "Review blockers before starting edits".to_owned(),
            "Expand verifier evidence before enabling mutation tools".to_owned(),
        ],
        sections,
        touched_files,
        None,
    )
}

pub fn safe_rename_report(state: &AppState, arguments: &Value) -> ToolResult {
    let file_path = required_string(arguments, "file_path")?;
    let symbol = required_string(arguments, "symbol")?;
    let symbol_matches = crate::tools::symbols::find_symbol(
        state,
        &json!({"name": symbol, "file_path": file_path, "include_body": false, "exact_match": true, "max_matches": 5}),
    )?
    .0;
    let references = crate::tools::graph::find_scoped_references_tool(
        state,
        &json!({"symbol_name": symbol, "file_path": file_path, "max_results": 50}),
    )?
    .0;
    let preview = if let Some(new_name) = arguments.get("new_name").and_then(|v| v.as_str()) {
        crate::tools::mutation::rename_symbol(
            state,
            &json!({"file_path": file_path, "symbol_name": symbol, "new_name": new_name, "dry_run": true}),
        )
        .map(|out| out.0)
        .unwrap_or_else(|error| json!({"preview_error": error.to_string()}))
    } else {
        json!({"preview_skipped": true, "reason": "Provide new_name to generate a dry-run preview."})
    };
    let ref_count = references
        .get("count")
        .and_then(|v| v.as_u64())
        .unwrap_or_default();
    let blockers = if symbol_matches
        .get("count")
        .and_then(|v| v.as_u64())
        .unwrap_or_default()
        == 0
    {
        vec!["No exact symbol match found in the requested file.".to_owned()]
    } else {
        Vec::new()
    };
    let mut top_findings = vec![format!(
        "{ref_count} classified reference(s) found for `{symbol}`."
    )];
    if !blockers.is_empty() {
        top_findings.extend(blockers.clone());
    }
    let mut sections = BTreeMap::new();
    sections.insert("symbol_matches".to_owned(), symbol_matches);
    sections.insert("references".to_owned(), references);
    sections.insert("rename_preview".to_owned(), preview);
    make_handle_response(
        state,
        "safe_rename_report",
        stable_cache_key(
            "safe_rename_report",
            arguments,
            &["file_path", "symbol", "new_name"],
        ),
        format!("Rename safety report for `{symbol}` in `{file_path}`."),
        top_findings,
        0.9,
        vec!["Review the preview before enabling mutation tools".to_owned()],
        sections,
        vec![file_path.to_owned()],
        Some(symbol.to_owned()),
    )
}

pub fn unresolved_reference_check(state: &AppState, arguments: &Value) -> ToolResult {
    let file_path = required_string(arguments, "file_path")?;
    let symbol = arguments.get("symbol").and_then(|value| value.as_str());
    let changed_files = strings_from_array(
        arguments
            .get("changed_files")
            .and_then(|value| value.as_array()),
        "file",
        8,
    );
    let symbol_matches = if let Some(symbol) = symbol {
        crate::tools::symbols::find_symbol(
            state,
            &json!({
                "name": symbol,
                "file_path": file_path,
                "include_body": false,
                "exact_match": true,
                "max_matches": 5
            }),
        )?
        .0
    } else {
        json!({
            "symbols": [],
            "count": 0,
            "note": "Provide symbol to run an exact unresolved-reference check."
        })
    };
    let references = if let Some(symbol) = symbol {
        crate::tools::graph::find_scoped_references_tool(
            state,
            &json!({"symbol_name": symbol, "file_path": file_path, "max_results": 50}),
        )?
        .0
    } else {
        json!({
            "references": [],
            "count": 0,
            "note": "Provide symbol to classify references."
        })
    };
    let mut sections = BTreeMap::new();
    sections.insert("symbol_matches".to_owned(), symbol_matches);
    sections.insert("references".to_owned(), references);
    if !changed_files.is_empty() {
        sections.insert(
            "changed_files".to_owned(),
            json!({
                "files": changed_files
                    .iter()
                    .map(|path| json!({"path": path, "status": "provided"}))
                    .collect::<Vec<_>>(),
                "count": changed_files.len(),
                "source": "provided",
            }),
        );
    }
    let mut top_findings = if let Some(symbol) = symbol {
        vec![format!(
            "Reference guard prepared for `{symbol}` in `{file_path}`."
        )]
    } else {
        vec![format!(
            "Symbol hint missing for `{file_path}`; unresolved-reference verdict will stay conservative."
        )]
    };
    if !changed_files.is_empty() {
        top_findings.push(format!(
            "{} changed file(s) supplied for context.",
            changed_files.len()
        ));
    }
    let mut touched_files = vec![file_path.to_owned()];
    for path in changed_files {
        if !touched_files.iter().any(|existing| existing == &path) {
            touched_files.push(path);
        }
    }
    make_handle_response(
        state,
        "unresolved_reference_check",
        stable_cache_key(
            "unresolved_reference_check",
            arguments,
            &["file_path", "symbol", "changed_files"],
        ),
        if let Some(symbol) = symbol {
            format!("Unresolved-reference check for `{symbol}` in `{file_path}`.")
        } else {
            format!(
                "Unresolved-reference check for `{file_path}` with conservative file-level guards."
            )
        },
        top_findings,
        0.87,
        vec!["Expand verifier_references before a rename or broad edit".to_owned()],
        sections,
        touched_files,
        symbol.map(ToOwned::to_owned),
    )
}