greentic-mcp-exec 0.4.24

Greentic executor for running wasix:mcp-compatible WebAssembly components with policy-driven verification and sandboxing.
Documentation
use base64::Engine;
use serde_json::Value;
use wasmtime::component::Linker;

use crate::runner::StoreState;

mod bindings {
    wasmtime::component::bindgen!({
        path: "wit/wasix-mcp-25.6.18",
        world: "mcp-router",
    });
}

pub use bindings::McpRouter;
pub use bindings::exports::wasix::mcp::router::{
    ContentBlock, Response, Tool, ToolError, ToolResult,
};

pub(crate) fn try_call_tool_router(
    component: &wasmtime::component::Component,
    linker: &mut Linker<StoreState>,
    store: &mut wasmtime::Store<StoreState>,
    tool: &str,
    arguments_json: &String,
) -> anyhow::Result<Option<Value>> {
    let router = match McpRouter::instantiate(&mut *store, component, linker) {
        Ok(router) => router,
        Err(err) => {
            let msg = err.to_string();
            if msg.contains("unknown export")
                || msg.contains("No such export")
                || msg.contains("no exported instance named")
            {
                return Ok(None);
            }
            return Err(anyhow::anyhow!(err.to_string()));
        }
    };

    let response = match router
        .wasix_mcp_router()
        .call_call_tool(&mut *store, tool, arguments_json)
    {
        Ok(Ok(resp)) => resp,
        Ok(Err(err)) => return Ok(Some(tool_error_to_value(tool, err))),
        Err(err) => return Err(anyhow::anyhow!(err.to_string())),
    };

    Ok(Some(render_response(&response)))
}

#[allow(dead_code)]
pub(crate) fn try_list_tools_router(
    component: &wasmtime::component::Component,
    linker: &mut Linker<StoreState>,
    store: &mut wasmtime::Store<StoreState>,
) -> anyhow::Result<Option<Vec<Tool>>> {
    let router = match McpRouter::instantiate(&mut *store, component, linker) {
        Ok(router) => router,
        Err(err) => {
            let msg = err.to_string();
            if msg.contains("unknown export")
                || msg.contains("No such export")
                || msg.contains("no exported instance named")
            {
                return Ok(None);
            }
            return Err(anyhow::anyhow!(err.to_string()));
        }
    };

    let tools = router
        .wasix_mcp_router()
        .call_list_tools(&mut *store)
        .map_err(|err| anyhow::anyhow!(err.to_string()))?;
    Ok(Some(tools))
}

pub fn render_response(response: &Response) -> Value {
    match response {
        Response::Completed(result) => render_tool_result(result),
        Response::Elicit(req) => serde_json::json!({
            "ok": true,
            "elicitation": {
                "title": req.title.clone(),
                "message": req.message.clone(),
                "schema": req.schema.clone(),
            }
        }),
    }
}

fn render_tool_result(result: &ToolResult) -> Value {
    let (content, structured_content) = result.content.iter().map(render_content_block).fold(
        (Vec::new(), Vec::new()),
        |mut acc, (c, s)| {
            acc.0.push(c);
            if let Some(s) = s {
                acc.1.push(s);
            }
            acc
        },
    );

    serde_json::json!({
        "ok": true,
        "result": {
            "content": content,
            "structured_content": if structured_content.is_empty() { None } else { Some(structured_content) },
        }
    })
}

fn render_content_block(block: &ContentBlock) -> (Value, Option<Value>) {
    match block {
        ContentBlock::Text(text) => (serde_json::json!({"type": "text", "text": text.text}), None),
        ContentBlock::Image(img) => (
            serde_json::json!({"type": "image", "data": base64::engine::general_purpose::STANDARD.encode(&img.data), "mime_type": img.mime_type}),
            None,
        ),
        ContentBlock::ResourceLink(link) => (
            serde_json::json!({"type": "resource", "uri": link.uri.clone()}),
            None,
        ),
        ContentBlock::EmbeddedResource(res) => (
            serde_json::json!({"type": "resource-embed", "uri": res.uri.clone(), "data": base64::engine::general_purpose::STANDARD.encode(&res.data)}),
            None,
        ),
        ContentBlock::Audio(audio) => (
            serde_json::json!({"type": "audio", "data": base64::engine::general_purpose::STANDARD.encode(&audio.data), "mime_type": audio.mime_type}),
            None,
        ),
    }
}

pub fn tool_error_to_value(tool: &str, err: ToolError) -> Value {
    let (code, status, message) = match err {
        ToolError::InvalidParameters(msg) => ("MCP_TOOL_ERROR", 400, msg),
        ToolError::ExecutionError(msg) => ("MCP_TOOL_ERROR", 500, msg),
        ToolError::SchemaError(msg) => ("MCP_TOOL_ERROR", 422, msg),
        ToolError::NotFound(msg) => ("MCP_TOOL_ERROR", 404, msg),
    };

    serde_json::json!({
        "ok": false,
        "error": {
            "code": code,
            "message": message,
            "status": status,
            "tool": tool,
            "protocol": "25.06.18",
        }
    })
}