codebase-graph 1.1.5

Native codebaseGraph CLI and MCP server for local code knowledge graphs.
use super::{executable_in_path, McpInstallOptions, NativeMcpDescriptor};
use crate::cli::constants::LATEST_PROTOCOL_VERSION;
use serde_json::json;
use std::{
    collections::{BTreeMap, BTreeSet},
    io::Write,
    process::Command,
};

pub(in crate::cli) fn attach_install_verification(
    mut payload: serde_json::Value,
    descriptor: &NativeMcpDescriptor,
    options: &McpInstallOptions,
) -> Result<serde_json::Value, String> {
    if options.verify && !options.dry_run {
        payload["verification"] = verify_mcp_install(descriptor, &options.client);
    }
    Ok(payload)
}

pub(in crate::cli) fn verify_mcp_install(
    descriptor: &NativeMcpDescriptor,
    client: &str,
) -> serde_json::Value {
    let stdio = verify_stdio(descriptor);
    let visibility = verify_client_visibility(client, &descriptor.name);
    json!({
        "ok": stdio.get("ok").and_then(serde_json::Value::as_bool).unwrap_or(false)
            && visibility.get("ok").and_then(serde_json::Value::as_bool).unwrap_or(true),
        "stdio": stdio,
        "client_visibility": visibility,
    })
}

pub(in crate::cli) fn verify_stdio(descriptor: &NativeMcpDescriptor) -> serde_json::Value {
    let command = descriptor_command(descriptor);
    let payload = [
        stdio_json_rpc_message(
            "initialize",
            json!({"protocolVersion": LATEST_PROTOCOL_VERSION}),
            1,
        ),
        stdio_json_rpc_message("tools/list", json!({}), 2),
        stdio_json_rpc_message(
            "tools/call",
            json!({"name": "graph_health", "arguments": {"include_structured_content": true}}),
            3,
        ),
        stdio_json_rpc_message(
            "tools/call",
            json!({"name": "graph_search", "arguments": {"query": descriptor.name, "limit": 1}}),
            4,
        ),
        stdio_json_rpc_message(
            "tools/call",
            json!({"name": "graph_query", "arguments": {"statement": "MATCH (n) DELETE n"}}),
            5,
        ),
    ]
    .join("");
    let mut process = match Command::new(&command[0])
        .args(&command[1..])
        .stdin(std::process::Stdio::piped())
        .stdout(std::process::Stdio::piped())
        .stderr(std::process::Stdio::piped())
        .spawn()
    {
        Ok(process) => process,
        Err(error) => return json!({"ok": false, "command": command, "error": error.to_string()}),
    };
    if let Some(mut stdin) = process.stdin.take() {
        if let Err(error) = stdin.write_all(payload.as_bytes()) {
            return json!({"ok": false, "command": command, "error": error.to_string()});
        }
    }
    let output = match process.wait_with_output() {
        Ok(output) => output,
        Err(error) => return json!({"ok": false, "command": command, "error": error.to_string()}),
    };
    let responses = parse_stdio_json_lines(&output.stdout);
    if !output.status.success() {
        return json!({
            "ok": false,
            "command": command,
            "returncode": output.status.code().unwrap_or(1),
            "stderr": String::from_utf8_lossy(&output.stderr).to_string(),
            "responses": responses,
        });
    }
    let checks = stdio_checks(&responses);
    json!({
        "ok": checks.values().all(|value| *value),
        "command": command,
        "checks": checks,
        "responses": responses,
        "stderr": String::from_utf8_lossy(&output.stderr).to_string(),
    })
}

pub(in crate::cli) fn verify_client_visibility(
    client: &str,
    server_name: &str,
) -> serde_json::Value {
    let Some(command) = visibility_command(client) else {
        return json!({"ok": true, "skipped": true, "reason": format!("{client} has no CLI visibility check")});
    };
    if !executable_in_path(&command[0]) {
        return json!({"ok": true, "skipped": true, "reason": format!("{} executable not found", command[0])});
    }
    match Command::new(&command[0]).args(&command[1..]).output() {
        Ok(output) => {
            let stdout = String::from_utf8_lossy(&output.stdout).to_string();
            let stderr = String::from_utf8_lossy(&output.stderr).to_string();
            let found = stdout.contains(server_name) || stderr.contains(server_name);
            json!({
                "ok": output.status.success() && found,
                "command": command,
                "returncode": output.status.code().unwrap_or(1),
                "found": found,
                "stdout": stdout,
                "stderr": stderr,
            })
        }
        Err(error) => json!({"ok": false, "command": command, "error": error.to_string()}),
    }
}

pub(in crate::cli) fn descriptor_command(descriptor: &NativeMcpDescriptor) -> Vec<String> {
    let mut command = vec![descriptor.command.clone()];
    command.extend(descriptor.args.clone());
    command
}

pub(in crate::cli) fn stdio_json_rpc_message(
    method: &str,
    params: serde_json::Value,
    id: u64,
) -> String {
    serde_json::to_string(&json!({
        "jsonrpc": "2.0",
        "id": id,
        "method": method,
        "params": params,
    }))
    .unwrap_or_else(|_| "{}".to_string())
        + "\n"
}

pub(in crate::cli) fn parse_stdio_json_lines(data: &[u8]) -> Vec<serde_json::Value> {
    String::from_utf8_lossy(data)
        .lines()
        .filter_map(|line| serde_json::from_str::<serde_json::Value>(line).ok())
        .collect()
}

pub(in crate::cli) fn stdio_checks(responses: &[serde_json::Value]) -> BTreeMap<String, bool> {
    let mut by_id = BTreeMap::new();
    for response in responses {
        if let Some(id) = response.get("id").and_then(serde_json::Value::as_u64) {
            by_id.insert(id, response);
        }
    }
    let tools = by_id
        .get(&2)
        .and_then(|value| value.pointer("/result/tools"))
        .and_then(serde_json::Value::as_array)
        .cloned()
        .unwrap_or_default();
    let tool_names = tools
        .iter()
        .filter_map(|tool| tool.get("name").and_then(serde_json::Value::as_str))
        .collect::<BTreeSet<_>>();
    let mut checks = BTreeMap::new();
    checks.insert(
        "initialize".to_string(),
        by_id
            .get(&1)
            .and_then(|value| value.pointer("/result/protocolVersion"))
            .and_then(serde_json::Value::as_str)
            == Some(LATEST_PROTOCOL_VERSION),
    );
    checks.insert(
        "tools_list".to_string(),
        tool_names.contains("graph_health") && tool_names.contains("graph_search"),
    );
    checks.insert(
        "graph_health".to_string(),
        by_id
            .get(&3)
            .and_then(|value| value.pointer("/result/structuredContent/ok"))
            .and_then(serde_json::Value::as_bool)
            == Some(true),
    );
    checks.insert(
        "graph_search".to_string(),
        by_id
            .get(&4)
            .is_some_and(|value| value.get("error").is_none()),
    );
    checks.insert(
        "tool_error_result".to_string(),
        by_id
            .get(&5)
            .and_then(|value| value.pointer("/result/isError"))
            .and_then(serde_json::Value::as_bool)
            == Some(true),
    );
    checks
}

pub(in crate::cli) fn visibility_command(client: &str) -> Option<Vec<String>> {
    match client {
        "codex" => Some(vec![
            "codex".to_string(),
            "mcp".to_string(),
            "list".to_string(),
        ]),
        "claude" | "claude-project" => Some(vec![
            "claude".to_string(),
            "mcp".to_string(),
            "list".to_string(),
        ]),
        "openclaw" => Some(vec![
            "openclaw".to_string(),
            "mcp".to_string(),
            "list".to_string(),
        ]),
        _ => None,
    }
}