component-shape-mcp 0.3.4

Shared MCP protocol and schema helpers for component-shape integrations.
Documentation
use super::*;
use crate::McpToolValue as _;
use std::collections::BTreeSet;

macro_rules! serde_rename_contract {
    ($name:ident, $rule:literal) => {
        #[test]
        fn $name() {
            #[derive(
                Debug, PartialEq, serde::Serialize, serde::Deserialize, crate::McpJsonSchema,
            )]
            #[serde(rename_all = $rule)]
            struct Record {
                first_name: String,
                http_server2: String,
                _leading: String,
                trailing_: String,
                r#type: String,
            }

            #[derive(Debug, PartialEq, serde::Deserialize, crate::McpToolInput)]
            #[serde(rename_all = $rule)]
            struct Input {
                first_name: String,
                http_server2: String,
                _leading: String,
                trailing_: String,
                r#type: String,
            }

            let record = Record {
                first_name: "one".into(),
                http_server2: "two".into(),
                _leading: "three".into(),
                trailing_: "four".into(),
                r#type: "five".into(),
            };
            let wire = serde_json::to_value(&record).expect("serialize field names");
            for schema in [Record::json_schema(), Input::input_schema()] {
                assert_eq!(
                    schema["properties"]
                        .as_object()
                        .unwrap()
                        .keys()
                        .collect::<BTreeSet<_>>(),
                    wire.as_object().unwrap().keys().collect::<BTreeSet<_>>(),
                    "schema fields must follow Serde for {}",
                    $rule,
                );
            }
            assert_eq!(
                Record::from_tool_value("record", wire.clone()).unwrap(),
                record
            );
            let expected: Input = serde_json::from_value(wire.clone()).unwrap();
            let call = crate::McpToolCall::from_value(Some(wire)).unwrap();
            assert_eq!(Input::from_tool_call(call).unwrap(), expected);

            #[derive(
                Debug, PartialEq, serde::Serialize, serde::Deserialize, crate::McpJsonSchema,
            )]
            #[allow(non_camel_case_types)]
            #[serde(rename_all = $rule)]
            enum Variant {
                HTTPServer2,
                V1Beta,
                XML_HTTP2,
                r#type,
            }

            let variants = [
                Variant::HTTPServer2,
                Variant::V1Beta,
                Variant::XML_HTTP2,
                Variant::r#type,
            ];
            let expected_names = variants
                .iter()
                .map(|variant| serde_json::to_value(variant).unwrap())
                .collect::<Vec<_>>();
            assert_eq!(Variant::json_schema()["enum"], json!(expected_names));
            for (variant, wire) in variants.into_iter().zip(expected_names) {
                assert_eq!(Variant::from_tool_value("variant", wire).unwrap(), variant);
            }
        }
    };
}

serde_rename_contract!(serde_rename_lowercase, "lowercase");
serde_rename_contract!(serde_rename_uppercase, "UPPERCASE");
serde_rename_contract!(serde_rename_pascalcase, "PascalCase");
serde_rename_contract!(serde_rename_camelcase, "camelCase");
serde_rename_contract!(serde_rename_snakecase, "snake_case");
serde_rename_contract!(serde_rename_screaming_snakecase, "SCREAMING_SNAKE_CASE");
serde_rename_contract!(serde_rename_kebabcase, "kebab-case");
serde_rename_contract!(serde_rename_screaming_kebabcase, "SCREAMING-KEBAB-CASE");

#[test]
fn serde_rename_deserialize_overrides_and_mcp_names_are_separate() {
    #[derive(Debug, PartialEq, serde::Deserialize, crate::McpJsonSchema)]
    #[serde(rename_all(serialize = "UPPERCASE", deserialize = "lowercase"))]
    #[mcp(rename_all = "camelCase")]
    struct Record {
        first_name: String,
        #[serde(
            rename(serialize = "OUTPUT", deserialize = "input_name"),
            alias = "old_name"
        )]
        #[mcp(rename = "mcp_name")]
        custom_name: String,
    }

    let schema = Record::json_schema();
    assert_eq!(
        schema["properties"]["firstName"]["x-mcpDecodeName"],
        "first_name"
    );
    assert_eq!(
        schema["properties"]["mcp_name"]["x-mcpDecodeName"],
        "input_name"
    );
    let wire = json!({"firstName": "one", "old_name": "two"});
    assert_eq!(
        Record::from_tool_value("record", wire).unwrap(),
        Record {
            first_name: "one".into(),
            custom_name: "two".into()
        }
    );

    #[derive(Debug, PartialEq, serde::Deserialize, crate::McpJsonSchema)]
    #[serde(rename_all(deserialize = "snake_case", serialize = "UPPERCASE"))]
    #[mcp(rename_all = "snake_case")]
    enum Variant {
        HTTPServer2,
        #[serde(
            rename(deserialize = "serde_name", serialize = "OUTPUT"),
            alias = "old_name"
        )]
        #[mcp(rename = "mcp_name")]
        CustomName,
    }

    let schema = Variant::json_schema();
    assert_eq!(
        schema["enum"],
        json!(["http_server_2", "mcp_name", "old_name"])
    );
    assert_eq!(
        schema["x-mcpEnumDecodeAliases"]["http_server_2"],
        "h_t_t_p_server2"
    );
    assert_eq!(
        Variant::from_tool_value("variant", json!("http_server_2")).unwrap(),
        Variant::HTTPServer2
    );
    assert_eq!(
        Variant::from_tool_value("variant", json!("mcp_name")).unwrap(),
        Variant::CustomName
    );
    assert_eq!(
        Variant::from_tool_value("variant", json!("old_name")).unwrap(),
        Variant::CustomName
    );
}