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.
//! Cliente MCP (Model Context Protocol) por stdio: JSON-RPC con un mensaje por línea.
//!
//! Un servidor es un proceso hijo propio de Aura. Se arranca solo cuando el agente lo necesita y se mata al soltarlo.
//! El transporte (procesos, líneas JSON, respuestas) es el de `rpc`, compartido con ACP.

use crate::ai::rpc::{Handled, RpcPeer};
use crate::ai::types::AiError;
use serde_json::{Value, json};
use std::collections::BTreeMap;
use std::time::Duration;

const REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
const PROTOCOL_VERSION: &str = "2025-06-18";

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct McpConfig {
    pub command: String,
    pub args: Vec<String>,
    pub env: BTreeMap<String, String>,
}

#[derive(Clone, Debug, PartialEq)]
pub struct McpTool {
    pub name: String,
    pub description: String,
    pub schema: Value,
}

pub struct McpServer {
    peer: RpcPeer,
    pub tools: Vec<McpTool>,
}

impl McpServer {
    pub fn start(config: &McpConfig) -> Result<Self, AiError> {
        // Un servidor MCP no nos pide nada (no soportamos muestreo ni raíces): todo lo suyo se rechaza.
        let peer = RpcPeer::spawn(&config.command, &config.args, &config.env, None, |_| Handled::Unsupported)?;
        let init = json!({ "protocolVersion": PROTOCOL_VERSION, "capabilities": {}, "clientInfo": { "name": "aura", "version": "0.1" } });
        peer.request("initialize", init, REQUEST_TIMEOUT)?;
        peer.notify("notifications/initialized", json!({}))?;
        let tools = Self::list_tools(&peer)?;
        Ok(Self { peer, tools })
    }

    fn list_tools(peer: &RpcPeer) -> Result<Vec<McpTool>, AiError> {
        let (mut tools, mut cursor) = (Vec::new(), None::<String>);
        loop {
            let params = cursor.as_ref().map_or_else(|| json!({}), |cursor| json!({ "cursor": cursor }));
            let page = peer.request("tools/list", params, REQUEST_TIMEOUT)?;
            for tool in page["tools"].as_array().into_iter().flatten() {
                let Some(name) = tool["name"].as_str() else { continue };
                tools.push(McpTool {
                    name: name.to_owned(),
                    description: tool["description"].as_str().unwrap_or_default().to_owned(),
                    schema: tool.get("inputSchema").cloned().unwrap_or_else(|| json!({ "type": "object" })),
                });
            }
            cursor = page["nextCursor"].as_str().map(str::to_owned);
            if cursor.is_none() {
                return Ok(tools);
            }
        }
    }

    /// Llama a una herramienta. `Ok((texto, es_error))`: un error de la herramienta no es un fallo del protocolo.
    pub fn call_tool(&self, name: &str, arguments: &Value) -> Result<(String, bool), AiError> {
        let result =
            self.peer.request("tools/call", json!({ "name": name, "arguments": arguments }), REQUEST_TIMEOUT)?;
        let text = result["content"]
            .as_array()
            .into_iter()
            .flatten()
            .filter_map(|part| part["text"].as_str())
            .collect::<Vec<_>>()
            .join("\n");
        Ok((text, result["isError"].as_bool().unwrap_or(false)))
    }
}