magi-code 0.62.0

Repository-aware CLI coding agent for terminal work
Documentation
use crate::{
    config::{McPaths, McpServerConfig, McpServersSettings, read_settings},
    mcp::{
        McpClient,
        manager::{McpManager, McpServerStatus},
        oauth,
    },
};
use anyhow::Result;
use std::{
    io::Write,
    process::Command,
    sync::{Arc, atomic::AtomicBool},
    thread,
    time::Duration,
};

pub(crate) fn run_list(stdout: &mut impl Write, stderr: &mut impl Write) -> Result<()> {
    let paths = McPaths::resolve()?;
    let settings = read_settings(&paths)?;
    writeln!(stdout, "MCP servers")?;
    if settings.mcp_servers.is_empty() {
        writeln!(stdout, "  none configured")?;
        return Ok(());
    }

    let manager = McpManager::from_settings_with_paths(&settings.mcp_servers, Some(&paths.root));
    for (name, config) in &settings.mcp_servers {
        let server_type = match config {
            McpServerConfig::Stdio(_) => "stdio".to_string(),
            McpServerConfig::Http(http) => format!("http {}", sanitized_url(&http.url)),
        };
        let auth = oauth_status_label(&paths, name, config, &mut *stderr);
        if !config.enabled() {
            writeln!(stdout, "  {name} ({server_type}{auth}): disabled")?;
            continue;
        }
        match manager.statuses().get(name) {
            Some(McpServerStatus::Connected { tool_count, .. }) => {
                writeln!(
                    stdout,
                    "  {name} ({server_type}{auth}): connected ({tool_count} tools)"
                )?;
            }
            Some(McpServerStatus::Failed { phase, error }) => {
                writeln!(
                    stdout,
                    "  {name} ({server_type}{auth}): failed during {phase}"
                )?;
                writeln!(stderr, "mcp {name}: {phase} failed: {error}")?;
            }
            None => {
                writeln!(
                    stdout,
                    "  {name} ({server_type}{auth}): failed during startup"
                )?;
                writeln!(stderr, "mcp {name}: startup failed without status")?;
            }
        }
    }
    Ok(())
}

pub(crate) fn run_login(
    server: &str,
    stdout: &mut impl Write,
    stderr: &mut impl Write,
) -> Result<()> {
    let paths = McPaths::resolve()?;
    let settings = read_settings(&paths)?;
    let config = settings
        .mcp_servers
        .get(server)
        .ok_or_else(|| anyhow::anyhow!("MCP server '{server}' is not configured"))?;
    let McpServerConfig::Http(http) = config else {
        anyhow::bail!("MCP server '{server}' is not an HTTP server")
    };
    let oauth_config = http
        .oauth
        .as_ref()
        .ok_or_else(|| anyhow::anyhow!("server does not have OAuth configured"))?;
    let client = reqwest::blocking::Client::builder()
        .redirect(reqwest::redirect::Policy::none())
        .timeout(Duration::from_secs(
            http.timeout
                .unwrap_or(crate::config::DEFAULT_MCP_TIMEOUT_SECONDS),
        ))
        .build()?;
    let (listener, redirect_uri) =
        oauth::bind_callback_listener().map_err(|e| anyhow::anyhow!(e))?;
    let login = oauth::prepare_login(&http.url, oauth_config, &redirect_uri, &client)
        .map_err(|e| anyhow::anyhow!(e))?;
    writeln!(
        stderr,
        "Open this URL in your browser:\n{}",
        login.authorization_url
    )?;
    let _ = open_browser(&login.authorization_url);
    let cancel = Arc::new(AtomicBool::new(false));
    let (tx, rx) = std::sync::mpsc::channel();
    thread::spawn(move || {
        let mut line = String::new();
        if std::io::stdin().read_line(&mut line).is_ok() && !line.trim().is_empty() {
            let _ = tx.send(line);
        }
    });
    let code = oauth::capture_loopback_or_manual_code(
        Some(listener),
        &login.state,
        oauth::LOGIN_WAIT_TIMEOUT,
        &cancel,
        Some(&rx),
    )
    .map_err(|e| anyhow::anyhow!(e))?;
    let response = oauth::exchange_code(
        &login.metadata.token_endpoint,
        &code,
        &login.redirect_uri,
        &login.client_id,
        &login.verifier,
        &login.resource,
        login.client_secret.as_deref(),
        &client,
    )
    .map_err(|e| anyhow::anyhow!(e))?;
    let token = oauth::stored_token_from_response(
        response,
        &login.client_id,
        login.client_secret,
        &http.url,
        Some(login.authorization_server),
        Some(login.metadata.issuer),
        Some(login.metadata.token_endpoint),
        Some(login.resource),
    )
    .map_err(|e| anyhow::anyhow!(e))?;
    oauth::write_token(&paths.root, server, &token).map_err(|e| anyhow::anyhow!(e))?;
    writeln!(stdout, "Authenticated with {server}. Token stored.")?;
    Ok(())
}

pub(crate) fn run_logout(
    server: &str,
    stdout: &mut impl Write,
    _stderr: &mut impl Write,
) -> Result<()> {
    let paths = McPaths::resolve()?;
    oauth::delete_token(&paths.root, server).map_err(|e| anyhow::anyhow!(e))?;
    writeln!(stdout, "Logged out from {server}")?;
    Ok(())
}

pub(crate) fn run_test(
    server: &str,
    stdout: &mut impl Write,
    stderr: &mut impl Write,
) -> Result<()> {
    let paths = McPaths::resolve()?;
    let settings = read_settings(&paths)?;
    let config = settings
        .mcp_servers
        .get(server)
        .ok_or_else(|| anyhow::anyhow!("MCP server '{server}' is not configured"))?;
    if !config.enabled() {
        anyhow::bail!("MCP server '{server}' is disabled");
    }

    writeln!(stdout, "Testing MCP server: {server}")?;
    if let McpServerConfig::Http(http) = config {
        writeln!(stdout, "  endpoint: {}", sanitized_url(&http.url))?;
    }
    let mut client = connect_selected(server, &settings.mcp_servers, &paths, stderr)?;
    let init = client.initialize().map_err(|error| {
        let _ = writeln!(stderr, "mcp {server}: initialize failed: {error}");
        anyhow::anyhow!(error)
    })?;
    let tools = client.list_tools().map_err(|error| {
        let _ = writeln!(stderr, "mcp {server}: tools/list failed: {error}");
        anyhow::anyhow!(error)
    })?;

    writeln!(stdout, "Connected")?;
    writeln!(stdout, "  server: {}", init.server_info.name)?;
    writeln!(stdout, "  version: {}", init.server_info.version)?;
    writeln!(stdout, "  protocol: {}", init.protocol_version)?;
    writeln!(stdout, "  tools: {}", tools.len())?;
    for tool in tools {
        let description = tool.description.as_deref().unwrap_or("");
        if description.is_empty() {
            writeln!(stdout, "    - {}", tool.name)?;
        } else {
            writeln!(stdout, "    - {}: {description}", tool.name)?;
        }
    }
    client.shutdown();
    Ok(())
}

fn connect_selected(
    server: &str,
    settings: &McpServersSettings,
    paths: &McPaths,
    stderr: &mut impl Write,
) -> Result<McpClient> {
    let config = settings
        .get(server)
        .expect("server presence validated before connect");
    McpClient::connect_named(Some(server), config, Some(&paths.root)).map_err(|error| {
        let phase = match config {
            McpServerConfig::Stdio(_) => "spawn",
            McpServerConfig::Http(_) => "connect",
        };
        let _ = writeln!(stderr, "mcp {server}: {phase} failed: {error}");
        anyhow::anyhow!(error)
    })
}

fn oauth_status_label(
    paths: &McPaths,
    name: &str,
    config: &McpServerConfig,
    stderr: &mut impl Write,
) -> String {
    let McpServerConfig::Http(http) = config else {
        return String::new();
    };
    if http.oauth.is_none() {
        return String::new();
    }
    match oauth::auth_status(&paths.root, name, &http.url) {
        Ok(status) => format!(", auth: {status}"),
        Err(error) => {
            let _ = writeln!(stderr, "mcp {name}: auth status failed: {error}");
            ", auth: invalid (needs login)".to_string()
        }
    }
}

fn open_browser(url: &str) -> std::io::Result<()> {
    #[cfg(target_os = "macos")]
    {
        Command::new("open").arg(url).spawn().map(|_| ())
    }
    #[cfg(target_os = "windows")]
    {
        Command::new("cmd")
            .args(["/C", "start", "", url])
            .spawn()
            .map(|_| ())
    }
    #[cfg(all(not(target_os = "macos"), not(target_os = "windows")))]
    {
        Command::new("xdg-open").arg(url).spawn().map(|_| ())
    }
}

fn sanitized_url(url: &str) -> String {
    match reqwest::Url::parse(url) {
        Ok(parsed) => {
            let host = parsed.host_str().unwrap_or("<unknown>");
            let port = parsed
                .port()
                .map(|port| format!(":{port}"))
                .unwrap_or_default();
            format!("{}://{}{}{}", parsed.scheme(), host, port, parsed.path())
        }
        Err(_) => "<invalid-url>".to_string(),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn http_list_output_redacts_header_values_and_url_query() {
        let temp = tempfile::TempDir::new().unwrap();
        let env = crate::test_support::env::env_lock();
        env.set_var("MC_HOME", temp.path());
        std::fs::write(
            temp.path().join("settings.json"),
            r#"{
              "mcp_servers": {
                "remote": {
                  "type": "http",
                  "url": "http://127.0.0.1:9/mcp?token=url-secret#frag",
                  "headers": { "X-Team": "literal-secret-value" },
                  "enabled": true,
                  "timeout": 1
                }
              }
            }"#,
        )
        .unwrap();
        let mut stdout = Vec::new();
        let mut stderr = Vec::new();

        run_list(&mut stdout, &mut stderr).unwrap();

        let output = format!(
            "{}{}",
            String::from_utf8(stdout).unwrap(),
            String::from_utf8(stderr).unwrap()
        );
        assert!(output.contains("http http://127.0.0.1:9/mcp"), "{output}");
        assert!(!output.contains("literal-secret-value"), "{output}");
        assert!(!output.contains("url-secret"), "{output}");
        assert!(!output.contains("token="), "{output}");
        env.remove_var("MC_HOME");
    }

    #[test]
    fn list_reports_oauth_auth_status_and_logout_deletes_token() {
        let temp = tempfile::TempDir::new().unwrap();
        let env = crate::test_support::env::env_lock();
        env.set_var("MC_HOME", temp.path());
        std::fs::write(
            temp.path().join("settings.json"),
            r#"{
              "mcp_servers": {
                "remote": {
                  "type": "http",
                  "url": "http://127.0.0.1:9/mcp",
                  "oauth": {"client_id":"client-1"},
                  "enabled": true,
                  "timeout": 1
                }
              }
            }"#,
        )
        .unwrap();
        crate::mcp::oauth::write_token(
            temp.path(),
            "remote",
            &crate::mcp::oauth::StoredToken {
                client_id: "client-1".to_string(),
                access_token: "access-token".to_string(),
                refresh_token: Some("refresh-token".to_string()),
                expires_at: Some(chrono::Utc::now().timestamp() + 3600),
                granted_scopes: Vec::new(),
                client_secret: None,
                authorization_server: None,
                issuer: None,
                token_endpoint: None,
                resource: Some("http://127.0.0.1:9/mcp".to_string()),
                server_url: "http://127.0.0.1:9/mcp".to_string(),
                token_received_at: chrono::Utc::now().timestamp(),
            },
        )
        .unwrap();
        let mut stdout = Vec::new();
        let mut stderr = Vec::new();
        run_list(&mut stdout, &mut stderr).unwrap();
        let output = String::from_utf8(stdout).unwrap();
        assert!(output.contains("auth: authenticated"), "{output}");
        assert!(!output.contains("access-token"), "{output}");

        let mut logout_stdout = Vec::new();
        run_logout("remote", &mut logout_stdout, &mut stderr).unwrap();
        assert!(
            String::from_utf8(logout_stdout)
                .unwrap()
                .contains("Logged out from remote")
        );
        assert!(
            crate::mcp::oauth::read_token(temp.path(), "remote")
                .unwrap()
                .is_none()
        );
        env.remove_var("MC_HOME");
    }

    #[test]
    fn list_with_empty_settings_is_human_readable() {
        let temp = tempfile::TempDir::new().unwrap();
        let env = crate::test_support::env::env_lock();
        env.set_var("MC_HOME", temp.path());
        let mut stdout = Vec::new();
        let mut stderr = Vec::new();

        run_list(&mut stdout, &mut stderr).unwrap();

        assert!(
            String::from_utf8(stdout)
                .unwrap()
                .contains("none configured")
        );
        assert!(stderr.is_empty());
        env.remove_var("MC_HOME");
    }
}