aura-gui 0.2.2

UI nativa acelerada por GPU para escritorio, Android e iOS, en un solo crate: estilo tipo Tailwind, componentes, sombras, animaciones y agentes de IA. Reposo a cero. Licencia AGPL-3.0 o comercial.
//! Conjunto de servidores MCP del usuario: expone sus herramientas al modelo con un nombre único
//! (`mcp__servidor__herramienta`) y encamina cada llamada al servidor correcto.

use crate::ai::mcp::{McpConfig, McpServer};
use crate::ai::types::ToolSpec;
use crate::ai::wire;
use serde_json::Value;

const PREFIX: &str = "mcp__";

#[derive(Default)]
pub struct McpHub {
    servers: Vec<(String, McpServer)>,
}

/// Separa `mcp__servidor__herramienta`.
pub fn split_name(full: &str) -> Option<(&str, &str)> {
    full.strip_prefix(PREFIX)?.split_once("__")
}

impl McpHub {
    /// Arranca todos los servidores (bloqueante: llamar desde un hilo). Devuelve los errores de los que fallaron.
    pub fn start<'a>(configs: impl IntoIterator<Item = (&'a String, &'a McpConfig)>) -> (Self, Vec<String>) {
        let (mut servers, mut errors) = (Vec::new(), Vec::new());
        for (name, config) in configs {
            match McpServer::start(config) {
                Ok(server) => servers.push((name.clone(), server)),
                Err(error) => errors.push(format!("MCP «{name}»: {error}")),
            }
        }
        (Self { servers }, errors)
    }

    pub fn is_empty(&self) -> bool {
        self.servers.is_empty()
    }

    pub fn specs(&self) -> Vec<ToolSpec> {
        self.servers
            .iter()
            .flat_map(|(server, handle)| {
                handle.tools.iter().map(move |tool| ToolSpec {
                    name: wire::tool_name(&format!("{PREFIX}{server}__{}", tool.name)),
                    description: format!("[{server}] {}", tool.description),
                    schema: wire::object_schema(&tool.schema),
                })
            })
            .collect()
    }

    /// Ejecuta una herramienta por su nombre completo. El error es texto para devolvérselo al modelo.
    pub fn call(&self, full_name: &str, input: &Value) -> Result<String, String> {
        // Los nombres se limpiaron para la API (`wire::tool_name`): se busca la herramienta original que coincide.
        let found = self.servers.iter().find_map(|(server, handle)| {
            let tool = handle
                .tools
                .iter()
                .find(|tool| wire::tool_name(&format!("{PREFIX}{server}__{}", tool.name)) == full_name)?;
            Some((handle, tool.name.as_str()))
        });
        let (handle, tool) = found
            .ok_or_else(|| format!("«{full_name}»: su servidor MCP no está activo o no ofrece esa herramienta"))?;
        match handle.call_tool(tool, input) {
            Ok((text, false)) => Ok(text),
            Ok((text, true)) => Err(text),
            Err(error) => Err(error.to_string()),
        }
    }
}

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

    #[test]
    fn separa_servidor_y_herramienta() {
        assert_eq!(split_name("mcp__git__log"), Some(("git", "log")));
        assert_eq!(split_name("mcp__fs__read__file"), Some(("fs", "read__file")));
        assert_eq!(split_name("read_file"), None);
    }
}