greentic-mcp-exec 1.1.2

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)))
}

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": result
                .structured_content
                .as_ref()
                .map(|raw| serde_json::from_str::<serde_json::Value>(raw).unwrap_or_else(|_| raw.clone().into())),
        }
    })
}

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",
        }
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::router::bindings::exports::wasix::mcp::router as generated_router_types;
    use crate::runner::StoreState;
    use wasmtime::Config;
    use wasmtime::Engine;
    use wasmtime::Store;
    use wasmtime::component::Component;
    use wasmtime::component::Linker;
    use wat::parse_str;

    fn empty_router_component() -> (Engine, Component) {
        let mut config = Config::new();
        config.wasm_component_model(true);
        let engine = Engine::new(&config).expect("engine");
        Component::from_binary(&engine, &parse_str("(component)").expect("wat"))
            .map(|component| (engine, component))
            .expect("component")
    }

    #[test]
    fn render_response_handles_completed_result() {
        let value = render_response(&Response::Completed(ToolResult {
            content: vec![
                ContentBlock::Text(generated_router_types::TextContent {
                    text: "hello".into(),
                    annotations: Some(generated_router_types::Annotations {
                        audience: Some(vec![generated_router_types::Role::Assistant]),
                        priority: Some(1.0),
                        timestamp: Some("ts".into()),
                    }),
                }),
                ContentBlock::Image(generated_router_types::ImageContent {
                    data: "aGVsbG8=".into(),
                    mime_type: "image/png".into(),
                    annotations: None,
                }),
            ],
            structured_content: Some(r#"{"a":1}"#.into()),
            progress: Some(vec![generated_router_types::ProgressNotification {
                progress: Some(0.5),
                message: Some("half".into()),
                annotations: Some(generated_router_types::Annotations {
                    audience: None,
                    priority: None,
                    timestamp: None,
                }),
            }]),
            meta: Some(vec![generated_router_types::MetaEntry {
                key: "source".into(),
                value: "\"router\"".into(),
            }]),
            is_error: Some(false),
        }));

        assert_eq!(value["ok"], serde_json::json!(true));
        assert_eq!(
            value["result"]["content"]
                .as_array()
                .expect("content")
                .len(),
            2
        );
        assert_eq!(
            value["result"]["structured_content"]["a"],
            serde_json::json!(1)
        );
    }

    #[test]
    fn render_content_maps_all_variants() {
        let image =
            render_content_block(&ContentBlock::Image(generated_router_types::ImageContent {
                data: "AA==".into(),
                mime_type: "image/png".into(),
                annotations: None,
            }));
        assert_eq!(image.0["type"], serde_json::json!("image"));
        assert_eq!(image.1, None);

        let resource = render_content_block(&ContentBlock::ResourceLink(
            generated_router_types::ResourceLinkContent {
                uri: "uri".into(),
                title: None,
                description: None,
                mime_type: Some("text/plain".into()),
                annotations: None,
            },
        ));
        assert_eq!(resource.0["type"], serde_json::json!("resource"));

        let embedded = render_content_block(&ContentBlock::EmbeddedResource(
            generated_router_types::EmbeddedResource {
                uri: "uri".into(),
                title: None,
                description: None,
                mime_type: Some("text/plain".into()),
                data: "".into(),
                annotations: None,
            },
        ));
        assert_eq!(embedded.0["type"], serde_json::json!("resource-embed"));

        let audio =
            render_content_block(&ContentBlock::Audio(generated_router_types::AudioContent {
                data: "AA==".into(),
                mime_type: "audio/wav".into(),
                annotations: None,
            }));
        assert_eq!(audio.0["type"], serde_json::json!("audio"));
    }

    #[test]
    fn maps_tool_errors_to_expected_status_codes() {
        assert_eq!(
            tool_error_to_value("tool", ToolError::InvalidParameters("bad".into()))["error"]["status"],
            400
        );
        assert_eq!(
            tool_error_to_value("tool", ToolError::ExecutionError("oops".into()))["error"]["status"],
            500
        );
        assert_eq!(
            tool_error_to_value("tool", ToolError::SchemaError("bad".into()))["error"]["status"],
            422
        );
        assert_eq!(
            tool_error_to_value("tool", ToolError::NotFound("missing".into()))["error"]["status"],
            404
        );
    }

    #[test]
    fn list_tooling_router_returns_none_when_export_missing() {
        let (engine, component) = empty_router_component();
        let mut linker = Linker::new(&engine);
        let mut store = Store::new(&engine, StoreState::new(false, None, None));
        let result = try_list_tools_router(&component, &mut linker, &mut store).expect("no export");
        assert!(result.is_none());
    }

    #[test]
    fn call_router_returns_none_when_export_missing() {
        let (engine, component) = empty_router_component();
        let mut linker = Linker::new(&engine);
        let mut store = Store::new(&engine, StoreState::new(false, None, None));
        let result = try_call_tool_router(
            &component,
            &mut linker,
            &mut store,
            "tool",
            &"{}".to_string(),
        )
        .expect("no export");
        assert!(result.is_none());
    }
}