magi-code 0.104.0

Repository-aware CLI coding agent for terminal work
Documentation
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;

pub type McpServersSettings = BTreeMap<String, McpServerConfig>;
pub const DEFAULT_MCP_TIMEOUT_SECONDS: u64 = 30;
pub const MAX_MCP_TIMEOUT_SECONDS: u64 = 300;

#[derive(Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum McpServerConfig {
    Stdio(McpStdioServerConfig),
    Http(McpHttpServerConfig),
}

impl std::fmt::Debug for McpServerConfig {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Stdio(config) => f.debug_tuple("Stdio").field(config).finish(),
            Self::Http(config) => f.debug_tuple("Http").field(config).finish(),
        }
    }
}

impl McpServerConfig {
    pub(crate) fn enabled(&self) -> bool {
        match self {
            Self::Stdio(config) => config.enabled,
            Self::Http(config) => config.enabled,
        }
    }

    pub(crate) fn set_enabled(&mut self, enabled: bool) {
        match self {
            Self::Stdio(config) => config.enabled = enabled,
            Self::Http(config) => config.enabled = enabled,
        }
    }
}

#[derive(Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct McpStdioServerConfig {
    pub command: String,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub args: Vec<String>,
    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
    pub env: BTreeMap<String, String>,
    #[serde(default)]
    pub enabled: bool,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub timeout: Option<u64>,
}

impl std::fmt::Debug for McpStdioServerConfig {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("McpStdioServerConfig")
            .field("command", &self.command)
            .field("args", &self.args)
            .field("env", &format_args!("<{} vars redacted>", self.env.len()))
            .field("enabled", &self.enabled)
            .field("timeout", &self.timeout)
            .finish()
    }
}

#[derive(Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct McpHttpServerConfig {
    pub url: String,
    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
    pub headers: BTreeMap<String, String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub oauth: Option<McpOAuthConfig>,
    #[serde(default)]
    pub enabled: bool,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub timeout: Option<u64>,
}

#[derive(Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
pub struct McpOAuthConfig {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub client_id: Option<String>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub scopes: Vec<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub authorization_server: Option<String>,
}

impl std::fmt::Debug for McpOAuthConfig {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("McpOAuthConfig")
            .field("client_id", &self.client_id.as_ref().map(|_| "[REDACTED]"))
            .field("scopes", &self.scopes)
            .field(
                "authorization_server",
                &self
                    .authorization_server
                    .as_ref()
                    .map(|url| sanitize_mcp_http_url_for_display(url)),
            )
            .finish()
    }
}

impl std::fmt::Debug for McpHttpServerConfig {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("McpHttpServerConfig")
            .field("url", &sanitize_mcp_http_url_for_display(&self.url))
            .field(
                "headers",
                &crate::mcp::headers::redact_headers(&self.headers),
            )
            .field("oauth", &self.oauth)
            .field("enabled", &self.enabled)
            .field("timeout", &self.timeout)
            .finish()
    }
}

fn sanitize_mcp_http_url_for_display(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(),
    }
}