siumai-bridge 0.11.0-beta.9

Protocol bridge helpers for siumai gateway and facade integrations
Documentation
use crate::BridgeReport;
use serde_json::{Map, Value, json};
use siumai_core::types::{ChatRequest, ProviderDefinedTool};

pub(crate) const ANTHROPIC_MCP_SERVER_CHOICE_ALIAS: &str = "__anthropic_mcp_server__";

pub(crate) fn openai_mcp_choice_aliases(provider_tool: &ProviderDefinedTool) -> Vec<String> {
    vec![provider_tool.name.clone(), "mcp".to_string()]
}

pub(crate) fn is_anthropic_mcp_server_choice_alias(alias: &str) -> bool {
    alias == ANTHROPIC_MCP_SERVER_CHOICE_ALIAS
}

pub(crate) fn map_openai_mcp_server(
    index: usize,
    provider_tool: &ProviderDefinedTool,
    report: &mut BridgeReport,
) -> Option<Value> {
    let Some(obj) = provider_tool.args.as_object() else {
        report.record_dropped_field(
            format!("tools[{index}]"),
            "OpenAI MCP tool is missing an args object and could not be mapped to Anthropic mcp_servers",
        );
        return None;
    };

    let url = obj
        .get("serverUrl")
        .or_else(|| obj.get("server_url"))
        .and_then(|value| value.as_str())
        .map(str::to_string);

    let Some(url) = url else {
        report.record_dropped_field(
            format!("tools[{index}].args.serverUrl"),
            "Anthropic mcp_servers requires a server URL",
        );
        return None;
    };

    let mut server = Map::new();
    server.insert(
        "serverName".to_string(),
        Value::String(
            obj.get("serverLabel")
                .or_else(|| obj.get("server_label"))
                .and_then(|value| value.as_str())
                .unwrap_or(provider_tool.name.as_str())
                .to_string(),
        ),
    );
    server.insert("serverUrl".to_string(), Value::String(url));

    if let Some(allowed_tools) = obj.get("allowedTools").or_else(|| obj.get("allowed_tools")) {
        server.insert(
            "toolConfiguration".to_string(),
            json!({
                "allowedTools": allowed_tools.clone(),
            }),
        );
    }

    if let Some(token) = extract_authorization_token(obj.get("headers")) {
        server.insert("authorizationToken".to_string(), Value::String(token));
    } else if obj.get("headers").is_some() {
        report.record_dropped_field(
            format!("tools[{index}].args.headers"),
            "only Authorization bearer headers are mapped into Anthropic authorization_token",
        );
    }

    if obj
        .get("requireApproval")
        .or_else(|| obj.get("require_approval"))
        .is_some()
    {
        report.record_dropped_field(
            format!("tools[{index}].args.requireApproval"),
            "Anthropic mcp_servers does not expose OpenAI require_approval",
        );
    }

    if obj
        .get("serverDescription")
        .or_else(|| obj.get("server_description"))
        .is_some()
    {
        report.record_dropped_field(
            format!("tools[{index}].args.serverDescription"),
            "Anthropic mcp_servers does not expose OpenAI server description directly",
        );
    }

    Some(Value::Object(server))
}

pub(crate) fn apply_anthropic_mcp_servers_option(request: &mut ChatRequest, servers: Vec<Value>) {
    if servers.is_empty() {
        return;
    }

    let mut anthropic = request
        .provider_options_map
        .get("anthropic")
        .and_then(|value| value.as_object().cloned())
        .unwrap_or_default();
    anthropic.insert("mcpServers".to_string(), Value::Array(servers));
    request
        .provider_options_map
        .insert("anthropic", Value::Object(anthropic));
}

pub(crate) fn apply_anthropic_mcp_servers_overlay(request: &ChatRequest, body: &mut Value) {
    let Some(servers) = request
        .provider_options_map
        .get("anthropic")
        .and_then(|value| value.as_object())
        .and_then(|value| {
            value
                .get("mcpServers")
                .or_else(|| value.get("mcp_servers"))
                .and_then(|value| value.as_array())
        })
    else {
        return;
    };

    let normalized: Vec<Value> = servers
        .iter()
        .filter_map(normalize_anthropic_mcp_server)
        .collect();

    if normalized.is_empty() {
        return;
    }

    body["mcp_servers"] = Value::Array(normalized);
}

fn normalize_anthropic_mcp_server(value: &Value) -> Option<Value> {
    let obj = value.as_object()?;
    let mut out = Map::new();

    for (key, value) in obj {
        let normalized_key = match key.as_str() {
            "serverName" => "name",
            "serverUrl" => "url",
            "authorizationToken" => "authorization_token",
            "toolConfiguration" => "tool_configuration",
            "allowedTools" => "allowed_tools",
            other => other,
        };

        if normalized_key == "tool_configuration" && value.is_object() {
            let mut tool_configuration = Map::new();
            if let Some(inner) = value.as_object() {
                for (inner_key, inner_value) in inner {
                    let normalized_inner = match inner_key.as_str() {
                        "allowedTools" => "allowed_tools",
                        other => other,
                    };
                    tool_configuration.insert(normalized_inner.to_string(), inner_value.clone());
                }
            }
            out.insert(
                "tool_configuration".to_string(),
                Value::Object(tool_configuration),
            );
            continue;
        }

        out.insert(normalized_key.to_string(), value.clone());
    }

    Some(Value::Object(out))
}

fn extract_authorization_token(headers: Option<&Value>) -> Option<String> {
    let headers = headers?.as_object()?;
    let value = headers
        .get("Authorization")
        .or_else(|| headers.get("authorization"))
        .and_then(|value| value.as_str())?;

    if let Some(token) = value.strip_prefix("Bearer ") {
        return Some(token.to_string());
    }

    Some(value.to_string())
}