codebase-graph 1.1.6

Native codebaseGraph CLI and MCP server for local code knowledge graphs.
use super::{
    attach_install_verification, build_mcp_descriptor, copilot_studio_metadata,
    default_client_config_path, executable_in_path, install_scope, native_client_command,
    render_client_config, subprocess_error, write_text_atomic, McpInstallOptions,
    NativeMcpDescriptor,
};
use serde_json::json;
use std::{fs, process::Command};

pub(in crate::cli) fn install_mcp_client(
    options: &McpInstallOptions,
) -> Result<serde_json::Value, String> {
    let descriptor = build_mcp_descriptor(options)?;
    if options.client == "copilot-studio" || options.client == "microsoft-copilot" {
        let metadata = copilot_studio_metadata(&descriptor);
        let payload = json!({
            "action": if options.dry_run { "dry_run" } else { "reported" },
            "client": options.client,
            "scope": options.scope,
            "server_name": descriptor.name,
            "method": "manual_metadata",
            "path": serde_json::Value::Null,
            "command": serde_json::Value::Null,
            "descriptor": descriptor.as_json(),
            "entry": metadata["stdio"].clone(),
            "payload": metadata,
        });
        return attach_install_verification(payload, &descriptor, options);
    }
    let native_command = native_client_command(&options.client, &descriptor, &options.scope);
    let native_available = native_command
        .as_ref()
        .and_then(|command| command.first())
        .is_some_and(|executable| executable_in_path(executable));
    if options.dry_run && options.client_config_path.is_none() && native_available {
        return attach_install_verification(
            json!({
                "action": "dry_run",
                "client": options.client,
                "scope": install_scope(&options.client, &options.scope),
                "server_name": descriptor.name,
                "method": "native_cli",
                "path": serde_json::Value::Null,
                "command": native_command,
                "descriptor": descriptor.as_json(),
                "entry": descriptor.stdio_entry(false, false),
            }),
            &descriptor,
            options,
        );
    }
    if !options.dry_run && options.client_config_path.is_none() && native_available {
        let Some(command) = native_command.clone() else {
            return file_adapter_result(options, &descriptor, native_command, None);
        };
        let completed = Command::new(&command[0])
            .args(&command[1..])
            .output()
            .map_err(|error| format!("failed to run native client installer: {error}"))?;
        if completed.status.success() {
            return attach_install_verification(
                json!({
                    "action": "updated",
                    "client": options.client,
                    "scope": install_scope(&options.client, &options.scope),
                    "server_name": descriptor.name,
                    "method": "native_cli",
                    "path": serde_json::Value::Null,
                    "command": command,
                    "descriptor": descriptor.as_json(),
                    "entry": descriptor.stdio_entry(false, false),
                }),
                &descriptor,
                options,
            );
        }
        let error = subprocess_error(&completed);
        return file_adapter_result(options, &descriptor, Some(command), Some(error));
    }
    let native_error = native_command.as_ref().and_then(|command| {
        command.first().and_then(|executable| {
            if executable_in_path(executable) {
                None
            } else {
                Some(format!("{executable} executable not found"))
            }
        })
    });
    file_adapter_result(options, &descriptor, native_command, native_error)
}

pub(in crate::cli) fn file_adapter_result(
    options: &McpInstallOptions,
    descriptor: &NativeMcpDescriptor,
    native_command: Option<Vec<String>>,
    native_error: Option<String>,
) -> Result<serde_json::Value, String> {
    let path = options.client_config_path.clone().unwrap_or_else(|| {
        default_client_config_path(
            &options.client,
            &install_scope(&options.client, &options.scope),
            descriptor,
        )
    });
    let existing = fs::read_to_string(&path).ok();
    let rendered = render_client_config(
        &options.client,
        &install_scope(&options.client, &options.scope),
        existing.as_deref(),
        descriptor,
    )?;
    let action = if options.dry_run {
        "dry_run".to_string()
    } else {
        rendered.action.clone()
    };
    if !options.dry_run {
        write_text_atomic(&path, &rendered.text)?;
    }
    let mut payload = json!({
        "action": action,
        "client": options.client,
        "scope": install_scope(&options.client, &options.scope),
        "server_name": descriptor.name,
        "method": "file_adapter",
        "path": path.to_string_lossy(),
        "command": serde_json::Value::Null,
        "descriptor": descriptor.as_json(),
        "entry": rendered.entry,
        "patch": rendered.patch,
        "payload": rendered.payload,
    });
    if let Some(command) = native_command {
        payload["native_command"] = json!(command);
    }
    if let Some(error) = native_error {
        payload["native_error"] = json!(error);
    }
    attach_install_verification(payload, descriptor, options)
}