aacode-rs 1.8.10

Rust port of the AACode ReAct coding agent, built on fastshell for mobile AI agents
Documentation
// Copyright (c) 2026 xiefujin <490021684@qq.com>
// Licensed under GPL-3.0, see LICENSE file for full license terms.

//! MCP tools — list_mcp_tools / call_mcp_tool / get_mcp_status.
//! All blocking pipe I/O is executed on a background thread via spawn_blocking.

use super::registry::Tool;
use super::schema::{ParamType, ToolParameter, ToolSchema};
use crate::error::Result;
use crate::mcp::McpManager;
use serde_json::Value;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;

pub struct ListMcpToolsTool {
    pub mgr: Arc<McpManager>,
}
#[async_trait::async_trait]
impl Tool for ListMcpToolsTool {
    fn schema(&self) -> ToolSchema {
        ToolSchema::new(
            "list_mcp_tools",
            "List all available MCP tools from configured servers.",
            vec![],
        )
    }
    async fn call(&self, _args: &Value, _c: &AtomicBool) -> Result<String> {
        let mgr = self.mgr.clone();
        tokio::task::spawn_blocking(move || Ok(mgr.list_tools().to_string()))
            .await
            .map_err(|e| crate::error::AacodeError::Other(format!("{e}")))?
    }
}

pub struct CallMcpToolTool {
    pub mgr: Arc<McpManager>,
}
#[async_trait::async_trait]
impl Tool for CallMcpToolTool {
    fn schema(&self) -> ToolSchema {
        ToolSchema::new(
            "call_mcp_tool",
            "Call an MCP tool. tool_name is 'server.tool' or just 'tool'.",
            vec![
                ToolParameter::new(
                    "tool_name",
                    ParamType::String,
                    true,
                    "server.tool or tool",
                    &["tool", "name", "function"],
                ),
                ToolParameter::new(
                    "arguments",
                    ParamType::Object,
                    false,
                    "Tool arguments",
                    &["args", "params", "input"],
                ),
            ],
        )
    }
    async fn call(&self, args: &Value, _c: &AtomicBool) -> Result<String> {
        let name = args
            .get("tool_name")
            .and_then(|v| v.as_str())
            .unwrap_or("")
            .to_string();
        let arguments = args
            .get("arguments")
            .cloned()
            .unwrap_or(serde_json::json!({}));
        let mgr = self.mgr.clone();
        tokio::task::spawn_blocking(move || Ok(mgr.call_tool(&name, arguments).to_string()))
            .await
            .map_err(|e| crate::error::AacodeError::Other(format!("{e}")))?
    }
}

pub struct McpStatusTool {
    pub mgr: Arc<McpManager>,
}
#[async_trait::async_trait]
impl Tool for McpStatusTool {
    fn schema(&self) -> ToolSchema {
        ToolSchema::new("get_mcp_status", "Get MCP server status.", vec![])
    }
    async fn call(&self, _args: &Value, _c: &AtomicBool) -> Result<String> {
        let mgr = self.mgr.clone();
        tokio::task::spawn_blocking(move || Ok(mgr.status().to_string()))
            .await
            .map_err(|e| crate::error::AacodeError::Other(format!("{e}")))?
    }
}

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

    #[tokio::test]
    async fn status_tool_reports_empty() {
        let mgr = Arc::new(McpManager::new(vec![], 5));
        let t = McpStatusTool { mgr };
        let cancel = AtomicBool::new(false);
        let out = t.call(&json!({}), &cancel).await.unwrap();
        let v: Value = serde_json::from_str(&out).unwrap();
        assert_eq!(v["total_count"], 0);
    }

    #[tokio::test]
    async fn list_tool_empty() {
        let mgr = Arc::new(McpManager::new(vec![], 5));
        let t = ListMcpToolsTool { mgr };
        let cancel = AtomicBool::new(false);
        let out = t.call(&json!({}), &cancel).await.unwrap();
        assert!(out.contains("\"count\":0"));
    }
}