greentic-interfaces 0.4.113

Greentic ABI: WIT contracts, generated bindings, thin mappers
Documentation
use std::path::PathBuf;

use wit_parser::Resolve;

#[test]
fn mcp_packages_are_staged_and_parse() {
    let staged = PathBuf::from(env!("WIT_STAGING_DIR"));
    for pkg in [
        "wasix-mcp-24.11.5",
        "wasix-mcp-25.3.26",
        "wasix-mcp-25.6.18",
    ] {
        let path = staged.join(pkg).join("package.wit");
        assert!(
            path.exists(),
            "staged WIT missing for package {pkg}: {}",
            path.display()
        );

        let mut resolve = Resolve::new();
        resolve
            .push_path(&path)
            .unwrap_or_else(|err| panic!("failed to parse {pkg} ({path:?}): {err}"));
    }
}

#[test]
fn mcp_tool_and_result_shapes_compile() {
    use greentic_interfaces::bindings::wasix_mcp_24_11_5_mcp_router::exports::wasix::mcp::router as mcp24;
    let _tool24 = mcp24::Tool {
        name: "echo".into(),
        description: "test tool".into(),
        input_schema: mcp24::Value { json: "{}".into() },
        output_schema: None,
        output: Some("text/plain".into()),
        config: Some(vec![mcp24::ConfigDescriptor {
            name: "endpoint".into(),
            description: "service endpoint".into(),
            required: true,
        }]),
        secrets: Some(vec![mcp24::SecretDescriptor {
            name: "token".into(),
            description: "auth token".into(),
            required: true,
        }]),
    };

    use greentic_interfaces::bindings::wasix_mcp_25_3_26_mcp_router::exports::wasix::mcp::router as mcp25_03;
    let _result25 = mcp25_03::CallToolResult {
        content: vec![mcp25_03::Content::Audio(mcp25_03::AudioContent {
            data: "base64audio".into(),
            mime_type: "audio/wav".into(),
            annotations: None,
        })],
        progress: Some(vec![mcp25_03::ProgressNotification {
            progress: Some(0.5),
            message: Some("halfway".into()),
            annotations: None,
        }]),
        meta: Some(vec![mcp25_03::MetaEntry {
            key: "output".into(),
            value: "text/plain".into(),
        }]),
        is_error: Some(false),
    };

    use greentic_interfaces::bindings::wasix_mcp_25_6_18_mcp_router::exports::wasix::mcp::router as mcp25_06;
    let content = vec![
        mcp25_06::ContentBlock::Audio(mcp25_06::AudioContent {
            data: "base64audio".into(),
            mime_type: "audio/wav".into(),
            annotations: None,
        }),
        mcp25_06::ContentBlock::ResourceLink(mcp25_06::ResourceLinkContent {
            uri: "https://example.com/resource".into(),
            title: Some("example".into()),
            description: Some("linked resource".into()),
            mime_type: Some("text/plain".into()),
            annotations: None,
        }),
        mcp25_06::ContentBlock::EmbeddedResource(mcp25_06::EmbeddedResource {
            uri: "memory://embedded".into(),
            title: Some("embedded".into()),
            description: Some("inline resource".into()),
            mime_type: Some("text/plain".into()),
            data: "hello".into(),
            annotations: None,
        }),
    ];
    let _result = mcp25_06::ToolResult {
        content,
        structured_content: Some("{\"ok\":true}".into()),
        progress: None,
        meta: None,
        is_error: Some(false),
    };
}

#[derive(Debug, serde::Serialize, serde::Deserialize, PartialEq)]
#[serde(rename_all = "kebab-case")]
struct ToolPayload {
    name: String,
    title: Option<String>,
    description: String,
    input_schema: String,
    output_schema: Option<String>,
    meta: Option<Vec<MetaEntryPayload>>,
}

#[derive(Debug, serde::Serialize, serde::Deserialize, PartialEq)]
#[serde(rename_all = "kebab-case")]
struct MetaEntryPayload {
    key: String,
    value: String,
}

#[derive(Debug, serde::Serialize, serde::Deserialize, PartialEq)]
#[serde(rename_all = "kebab-case")]
struct ToolResultPayload {
    content: Vec<ContentBlockPayload>,
    structured_content: Option<String>,
    meta: Option<Vec<MetaEntryPayload>>,
    is_error: Option<bool>,
}

#[derive(Debug, serde::Serialize, serde::Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "kebab-case", content = "value")]
enum ContentBlockPayload {
    ResourceLink(ResourceLinkPayload),
}

#[derive(Debug, serde::Serialize, serde::Deserialize, PartialEq)]
#[serde(rename_all = "kebab-case")]
struct ResourceLinkPayload {
    uri: String,
    title: Option<String>,
    description: Option<String>,
    mime_type: Option<String>,
}

impl From<&greentic_interfaces::bindings::wasix_mcp_25_6_18_mcp_router::exports::wasix::mcp::router::Tool>
    for ToolPayload
{
    fn from(tool: &greentic_interfaces::bindings::wasix_mcp_25_6_18_mcp_router::exports::wasix::mcp::router::Tool) -> Self {
        Self {
            name: tool.name.clone(),
            title: tool.title.clone(),
            description: tool.description.clone(),
            input_schema: tool.input_schema.clone(),
            output_schema: tool.output_schema.clone(),
            meta: tool
                .meta
                .as_ref()
                .map(|entries| entries.iter().map(MetaEntryPayload::from).collect()),
        }
    }
}

impl From<ToolPayload>
    for greentic_interfaces::bindings::wasix_mcp_25_6_18_mcp_router::exports::wasix::mcp::router::Tool
{
    fn from(tool: ToolPayload) -> Self {
        greentic_interfaces::bindings::wasix_mcp_25_6_18_mcp_router::exports::wasix::mcp::router::Tool {
            name: tool.name,
            title: tool.title,
            description: tool.description,
            input_schema: tool.input_schema,
            output_schema: tool.output_schema,
            annotations: None,
            meta: tool
                .meta
                .map(|entries| entries.into_iter().map(Into::into).collect()),
        }
    }
}

impl From<&greentic_interfaces::bindings::wasix_mcp_25_6_18_mcp_router::exports::wasix::mcp::router::MetaEntry>
    for MetaEntryPayload
{
    fn from(
        entry: &greentic_interfaces::bindings::wasix_mcp_25_6_18_mcp_router::exports::wasix::mcp::router::MetaEntry,
    ) -> Self {
        Self {
            key: entry.key.clone(),
            value: entry.value.clone(),
        }
    }
}

impl From<MetaEntryPayload>
    for greentic_interfaces::bindings::wasix_mcp_25_6_18_mcp_router::exports::wasix::mcp::router::MetaEntry
{
    fn from(entry: MetaEntryPayload) -> Self {
        greentic_interfaces::bindings::wasix_mcp_25_6_18_mcp_router::exports::wasix::mcp::router::MetaEntry {
            key: entry.key,
            value: entry.value,
        }
    }
}

impl From<&greentic_interfaces::bindings::wasix_mcp_25_6_18_mcp_router::exports::wasix::mcp::router::ToolResult>
    for ToolResultPayload
{
    fn from(
        result: &greentic_interfaces::bindings::wasix_mcp_25_6_18_mcp_router::exports::wasix::mcp::router::ToolResult,
    ) -> Self {
        let mut content = Vec::new();
        for block in &result.content {
            match block {
                greentic_interfaces::bindings::wasix_mcp_25_6_18_mcp_router::exports::wasix::mcp::router::ContentBlock::ResourceLink(link) => {
                    content.push(ContentBlockPayload::ResourceLink(ResourceLinkPayload {
                        uri: link.uri.clone(),
                        title: link.title.clone(),
                        description: link.description.clone(),
                        mime_type: link.mime_type.clone(),
                    }));
                }
                other => panic!("unexpected content variant in test fixture: {other:?}"),
            }
        }

        Self {
            content,
            structured_content: result.structured_content.clone(),
            meta: result
                .meta
                .as_ref()
                .map(|entries| entries.iter().map(MetaEntryPayload::from).collect()),
            is_error: result.is_error,
        }
    }
}

impl From<ToolResultPayload>
    for greentic_interfaces::bindings::wasix_mcp_25_6_18_mcp_router::exports::wasix::mcp::router::ToolResult
{
    fn from(result: ToolResultPayload) -> Self {
        let content = result
            .content
            .into_iter()
            .map(|block| match block {
                ContentBlockPayload::ResourceLink(link) => {
                    greentic_interfaces::bindings::wasix_mcp_25_6_18_mcp_router::exports::wasix::mcp::router::ContentBlock::ResourceLink(
                        greentic_interfaces::bindings::wasix_mcp_25_6_18_mcp_router::exports::wasix::mcp::router::ResourceLinkContent {
                            uri: link.uri,
                            title: link.title,
                            description: link.description,
                            mime_type: link.mime_type,
                            annotations: None,
                        },
                    )
                }
            })
            .collect();

        greentic_interfaces::bindings::wasix_mcp_25_6_18_mcp_router::exports::wasix::mcp::router::ToolResult {
            content,
            structured_content: result.structured_content,
            progress: None,
            meta: result
                .meta
                .map(|entries| entries.into_iter().map(Into::into).collect()),
            is_error: result.is_error,
        }
    }
}

#[test]
fn mcp_25_06_18_tool_and_result_roundtrip_json() {
    use greentic_interfaces::bindings::wasix_mcp_25_6_18_mcp_router::exports::wasix::mcp::router as mcp25_06;

    let tool = mcp25_06::Tool {
        name: "summarize".into(),
        title: Some("Summarize Text".into()),
        description: "Summarize structured payloads".into(),
        input_schema: "{\"type\":\"object\"}".into(),
        output_schema: Some(
            "{\"type\":\"object\",\"properties\":{\"summary\":{\"type\":\"string\"}}}".into(),
        ),
        annotations: None,
        meta: Some(vec![mcp25_06::MetaEntry {
            key: "output-schema".into(),
            value: "application/json".into(),
        }]),
    };

    let tool_payload: ToolPayload = (&tool).into();
    let tool_json = serde_json::to_string(&tool_payload).expect("serialize tool payload");
    let parsed_tool: ToolPayload =
        serde_json::from_str(&tool_json).expect("round-trip tool payload json");
    let tool_round: mcp25_06::Tool = parsed_tool.into();

    assert_eq!(tool_round.name, tool.name);
    assert_eq!(tool_round.output_schema, tool.output_schema);
    assert_eq!(
        tool_round
            .meta
            .as_ref()
            .and_then(|m| m.first())
            .map(|m| &m.key),
        tool.meta.as_ref().and_then(|m| m.first()).map(|m| &m.key)
    );

    let result = mcp25_06::ToolResult {
        content: vec![mcp25_06::ContentBlock::ResourceLink(
            mcp25_06::ResourceLinkContent {
                uri: "https://example.com/report.json".into(),
                title: Some("report".into()),
                description: Some("Structured summary output".into()),
                mime_type: Some("application/json".into()),
                annotations: None,
            },
        )],
        structured_content: Some("{\"summary\":\"done\"}".into()),
        progress: None,
        meta: Some(vec![mcp25_06::MetaEntry {
            key: "trace-id".into(),
            value: "\"abc123\"".into(),
        }]),
        is_error: Some(false),
    };

    let result_payload: ToolResultPayload = (&result).into();
    let result_json = serde_json::to_string(&result_payload).expect("serialize result payload");
    let parsed_result: ToolResultPayload =
        serde_json::from_str(&result_json).expect("round-trip result payload json");
    let result_round: mcp25_06::ToolResult = parsed_result.into();

    assert_eq!(result_round.structured_content, result.structured_content);
    assert_eq!(
        result_round
            .meta
            .as_ref()
            .and_then(|m| m.first())
            .map(|m| &m.key),
        result.meta.as_ref().and_then(|m| m.first()).map(|m| &m.key)
    );
    match result_round.content.first().expect("result content exists") {
        mcp25_06::ContentBlock::ResourceLink(link) => {
            assert_eq!(link.uri, "https://example.com/report.json");
            assert_eq!(link.mime_type.as_deref(), Some("application/json"));
        }
        other => panic!("expected resource-link content, got {other:?}"),
    }
}