enact-mcp 0.0.2

MCP (Model Context Protocol) client for Enact
Documentation
use crate::config::load_default_mcp_config;
use crate::{McpHttpClient, McpStdioClient};
use async_trait::async_trait;
use enact_core::tool::{DynTool, Tool};
use serde_json::json;
use std::sync::Arc;
use tokio::sync::Mutex;

enum McpClient {
    Stdio(Box<McpStdioClient>),
    Http(McpHttpClient),
}

pub struct McpToolAdapter {
    name: String,
    description: String,
    parameters: serde_json::Value,
    tool_name: String,
    client: Arc<Mutex<McpClient>>,
}

impl McpToolAdapter {
    fn new(
        server_name: &str,
        tool_name: String,
        description: String,
        parameters: serde_json::Value,
        client: Arc<Mutex<McpClient>>,
    ) -> Self {
        Self {
            name: format!("mcp__{}__{}", server_name, tool_name),
            description,
            parameters,
            tool_name,
            client,
        }
    }
}

#[async_trait]
impl Tool for McpToolAdapter {
    fn name(&self) -> &str {
        &self.name
    }

    fn description(&self) -> &str {
        &self.description
    }

    fn parameters_schema(&self) -> serde_json::Value {
        self.parameters.clone()
    }

    async fn execute(&self, args: serde_json::Value) -> anyhow::Result<serde_json::Value> {
        let mut client = self.client.lock().await;
        let output = match &mut *client {
            McpClient::Stdio(c) => c.call_tool(&self.tool_name, args).await?,
            McpClient::Http(c) => c.call_tool(&self.tool_name, args).await?,
        };
        Ok(json!({ "success": true, "output": output }))
    }
}

pub async fn discover_mcp_tools() -> anyhow::Result<Vec<DynTool>> {
    let config = load_default_mcp_config()?;
    let mut tools = Vec::new();

    for server in config.servers {
        let transport = server.transport.to_lowercase();
        let (discovered, shared): (Vec<crate::McpTool>, Arc<Mutex<McpClient>>) = if transport
            == "http"
        {
            let Some(url) = server.url.clone() else {
                tracing::warn!(
                    "MCP server '{}' missing url for HTTP transport",
                    server.name
                );
                continue;
            };
            let client = McpHttpClient::new(url);
            let discovered = match client.list_tools().await {
                Ok(list) => list,
                Err(e) => {
                    tracing::warn!("Failed to list MCP tools for '{}': {}", server.name, e);
                    continue;
                }
            };
            (discovered, Arc::new(Mutex::new(McpClient::Http(client))))
        } else {
            let args: Vec<&str> = server.args.iter().map(|s| s.as_str()).collect();
            let mut client = match McpStdioClient::new(&server.command, &args, &server.env).await {
                Ok(client) => client,
                Err(e) => {
                    tracing::warn!("Failed to connect MCP server '{}': {}", server.name, e);
                    continue;
                }
            };
            let discovered = match client.list_tools().await {
                Ok(list) => list,
                Err(e) => {
                    tracing::warn!("Failed to list MCP tools for '{}': {}", server.name, e);
                    continue;
                }
            };
            (
                discovered,
                Arc::new(Mutex::new(McpClient::Stdio(Box::new(client)))),
            )
        };
        for tool in discovered {
            tools.push(Arc::new(McpToolAdapter::new(
                &server.name,
                tool.name,
                tool.description,
                tool.parameters,
                Arc::clone(&shared),
            )) as DynTool);
        }
    }

    Ok(tools)
}