prodex 0.47.0

OpenAI profile pooling and safe auto-rotate for Codex CLI and Claude Code
Documentation
use super::*;

pub(crate) fn runtime_proxy_translate_anthropic_mcp_servers(
    value: &serde_json::Value,
) -> Result<BTreeMap<String, RuntimeAnthropicMcpServer>> {
    let Some(mcp_servers) = value.get("mcp_servers") else {
        return Ok(BTreeMap::new());
    };
    let mcp_servers = mcp_servers
        .as_array()
        .context("Anthropic mcp_servers must be an array when present")?;
    let mut translated = BTreeMap::new();
    for server in mcp_servers {
        let name = server
            .get("name")
            .and_then(serde_json::Value::as_str)
            .map(str::trim)
            .filter(|value| !value.is_empty())
            .context("Anthropic mcp_server requires a non-empty name")?;
        let mut headers = serde_json::Map::new();
        if let Some(server_headers) = server.get("headers").and_then(serde_json::Value::as_object) {
            for (header_name, header_value) in server_headers {
                let Some(header_value) = header_value
                    .as_str()
                    .map(str::trim)
                    .filter(|value| !value.is_empty())
                else {
                    continue;
                };
                headers.insert(
                    header_name.clone(),
                    serde_json::Value::String(header_value.to_string()),
                );
            }
        }
        translated.insert(
            name.to_string(),
            RuntimeAnthropicMcpServer {
                name: name.to_string(),
                url: server
                    .get("url")
                    .and_then(serde_json::Value::as_str)
                    .map(str::trim)
                    .filter(|value| !value.is_empty())
                    .map(str::to_string),
                authorization_token: server
                    .get("authorization_token")
                    .and_then(serde_json::Value::as_str)
                    .map(str::trim)
                    .filter(|value| !value.is_empty())
                    .map(str::to_string),
                headers,
                description: server
                    .get("description")
                    .and_then(serde_json::Value::as_str)
                    .map(str::trim)
                    .filter(|value| !value.is_empty())
                    .map(str::to_string),
            },
        );
    }
    Ok(translated)
}

pub(crate) fn runtime_proxy_translate_anthropic_mcp_tool(
    tool: &serde_json::Value,
    mcp_servers: &BTreeMap<String, RuntimeAnthropicMcpServer>,
) -> Result<Option<serde_json::Value>> {
    let Some(server_name) = tool
        .get("mcp_server_name")
        .and_then(serde_json::Value::as_str)
        .map(str::trim)
        .filter(|value| !value.is_empty())
    else {
        return Ok(None);
    };
    let Some(server) = mcp_servers.get(server_name) else {
        return Ok(None);
    };
    let Some(server_url) = server.url.as_deref() else {
        return Ok(None);
    };

    let default_config = tool
        .get("default_config")
        .and_then(serde_json::Value::as_object);
    let default_enabled = default_config
        .and_then(|config| config.get("enabled"))
        .and_then(serde_json::Value::as_bool)
        .unwrap_or(true);
    let default_defer_loading = default_config
        .and_then(|config| config.get("defer_loading"))
        .and_then(serde_json::Value::as_bool);

    let mut allowlisted_tools = Vec::new();
    let mut has_unrepresentable_denylist = false;
    if let Some(configs) = tool.get("configs") {
        let Some(configs) = configs.as_object() else {
            return Ok(None);
        };
        for (tool_name, config) in configs {
            let Some(config) = config.as_object() else {
                return Ok(None);
            };
            let enabled = config
                .get("enabled")
                .and_then(serde_json::Value::as_bool)
                .unwrap_or(default_enabled);
            if default_enabled {
                if !enabled {
                    has_unrepresentable_denylist = true;
                    break;
                }
            } else if enabled {
                allowlisted_tools.push(serde_json::Value::String(tool_name.clone()));
            }
        }
    }
    if has_unrepresentable_denylist {
        return Ok(None);
    }

    let mut translated = serde_json::Map::new();
    translated.insert(
        "type".to_string(),
        serde_json::Value::String("mcp".to_string()),
    );
    translated.insert(
        "server_label".to_string(),
        serde_json::Value::String(server.name.clone()),
    );
    translated.insert(
        "server_url".to_string(),
        serde_json::Value::String(server_url.to_string()),
    );
    translated.insert(
        "require_approval".to_string(),
        serde_json::Value::String("never".to_string()),
    );
    if !default_enabled {
        translated.insert(
            "allowed_tools".to_string(),
            serde_json::Value::Array(allowlisted_tools),
        );
    }
    if let Some(defer_loading) = default_defer_loading {
        translated.insert(
            "defer_loading".to_string(),
            serde_json::Value::Bool(defer_loading),
        );
    }
    if let Some(authorization) = server.authorization_token.as_deref() {
        translated.insert(
            "authorization".to_string(),
            serde_json::Value::String(authorization.to_string()),
        );
    }
    if !server.headers.is_empty() {
        translated.insert(
            "headers".to_string(),
            serde_json::Value::Object(server.headers.clone()),
        );
    }
    if let Some(server_description) = tool
        .get("description")
        .and_then(serde_json::Value::as_str)
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .or(server.description.as_deref())
    {
        translated.insert(
            "server_description".to_string(),
            serde_json::Value::String(server_description.to_string()),
        );
    }
    Ok(Some(serde_json::Value::Object(translated)))
}