greentic-mcp 0.5.3

MCP ToolMap + WASIX/WASI executor bridge
Documentation
use anyhow::{Context, Result, anyhow};
use std::fs;
use std::io::ErrorKind;
use std::path::{Path, PathBuf};
use std::process::Command;

const ADAPTER_COMPONENT: &[u8] = include_bytes!("../assets/mcp_adapter_25_06_18.component.wasm");

pub const ADAPTER_PROTOCOL: &str = "25.06.18";

pub fn compose_router_with_bundled_adapter(
    router: &Path,
    output: &Path,
    wasm_tools: Option<&Path>,
) -> Result<()> {
    if !router.exists() {
        return Err(anyhow!("router component not found: {}", router.display()));
    }

    if let Some(parent) = output.parent()
        && !parent.as_os_str().is_empty()
    {
        fs::create_dir_all(parent)
            .with_context(|| format!("creating output directory {}", parent.display()))?;
    }

    let wasm_tools = resolve_wasm_tools(wasm_tools)?;
    let adapter_path = write_adapter_component()?;

    let output = output.to_path_buf();
    if try_compose_with_wac(adapter_path.path(), router, &output)? {
        return Ok(());
    }

    let status = Command::new(&wasm_tools)
        .arg("compose")
        .arg(adapter_path.path())
        .arg("-d")
        .arg(router)
        .arg("-o")
        .arg(&output)
        .status()
        .with_context(|| format!("running {}", wasm_tools.display()))?;

    if !status.success() {
        return Err(anyhow!("wasm-tools compose failed with status {status}"));
    }

    Ok(())
}

fn try_compose_with_wac(adapter: &Path, router: &Path, output: &Path) -> Result<bool> {
    let Some(wac) = resolve_wac() else {
        return Ok(false);
    };

    let status = Command::new(&wac)
        .arg("plug")
        .arg(adapter)
        .arg("--plug")
        .arg(router)
        .arg("--output")
        .arg(output)
        .status();

    match status {
        Ok(status) => Ok(status.success()),
        Err(err) if err.kind() == ErrorKind::NotFound => Ok(false),
        Err(err) => Err(anyhow!("running {}: {err}", wac.display())),
    }
}

fn resolve_wasm_tools(wasm_tools: Option<&Path>) -> Result<PathBuf> {
    if let Some(path) = wasm_tools {
        return Ok(path.to_path_buf());
    }
    if let Ok(path) = std::env::var("GREENTIC_MCP_WASM_TOOLS")
        && !path.trim().is_empty()
    {
        return Ok(PathBuf::from(path));
    }
    Ok(PathBuf::from("wasm-tools"))
}

fn resolve_wac() -> Option<PathBuf> {
    if let Ok(path) = std::env::var("GREENTIC_MCP_WAC")
        && !path.trim().is_empty()
    {
        return Some(PathBuf::from(path));
    }
    Some(PathBuf::from("wac"))
}

fn write_adapter_component() -> Result<tempfile::NamedTempFile> {
    let mut file = tempfile::Builder::new()
        .prefix("mcp_adapter_")
        .suffix(".component.wasm")
        .tempfile()
        .context("creating temp adapter component")?;
    std::io::Write::write_all(&mut file, ADAPTER_COMPONENT)
        .context("writing bundled adapter component")?;
    Ok(file)
}