component-shape-mcp 0.3.0

Shared MCP protocol and schema helpers for component-shape integrations.
Documentation
use super::*;

#[test]
fn json_resource_specs_register_and_reuse_generated_resources() {
    let specs = vec![
        crate::McpJsonResourceSpec::new(
            "gpui-form://forms/contact/descriptor",
            "contact_descriptor",
            Some("Contact descriptor".to_string()),
            Some("Descriptor for the contact form.".to_string()),
            json!({ "fields": ["name", "email"] }),
        )
        .expect("resource spec should build"),
    ];
    let definitions = crate::json_resource_definitions(&specs).expect("definitions should build");
    assert_eq!(definitions[0].uri, "gpui-form://forms/contact/descriptor");
    assert_eq!(
        definitions[0].mime_type.as_deref(),
        Some("application/json")
    );

    let mut server = crate::McpServer::new("test", "0.0.0");
    crate::register_json_resource_specs_if_missing(&mut server, specs.clone())
        .expect("resources should register");
    crate::register_json_resource_specs_if_missing(&mut server, specs)
        .expect("existing complete resource set should be reused");

    assert_eq!(server.resource_count(), 1);
    assert!(server.contains_resource("gpui-form://forms/contact/descriptor"));
}

#[test]
fn json_resource_specs_reject_duplicate_uris() {
    let specs = vec![
        crate::McpJsonResourceSpec::new(
            "gpui-form://forms/contact/descriptor",
            "contact_descriptor",
            None,
            None,
            json!({}),
        )
        .expect("resource spec should build"),
        crate::McpJsonResourceSpec::new(
            "gpui-form://forms/contact/descriptor",
            "duplicate_contact_descriptor",
            None,
            None,
            json!({}),
        )
        .expect("resource spec should build"),
    ];

    assert_eq!(
        crate::ensure_json_resource_specs_distinct(&specs).expect_err("duplicate URI should fail"),
        crate::McpToolError::duplicate_resource("gpui-form://forms/contact/descriptor")
    );
}

#[test]
fn server_exposes_resources_and_prompts_through_rmcp_protocol() {
    let runtime = tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()
        .expect("runtime should start");

    runtime.block_on(async {
        use rmcp::{
            ClientLifecycleMode, ClientServiceExt as _, ServerHandler as _, ServiceExt as _,
            model::{
                CacheScope, GetPromptRequestParams, ProtocolVersion, ReadResourceRequestParams,
                ResultType,
            },
        };

        let descriptor_uri = "gpui-form://forms/contact/descriptor";
        let descriptor = json!({
            "tool": "form_contact",
            "fields": ["name", "email"]
        });
        let descriptor_resource = descriptor.clone();
        let mut server = McpServer::new("test-server", "0.0.0");
        server
            .add_resource(
                crate::resource_definition(
                    descriptor_uri,
                    "contact_descriptor",
                    Some("Contact descriptor".to_string()),
                    Some("Descriptor for the contact form.".to_string()),
                    Some("application/json".to_string()),
                )
                .expect("resource definition should build"),
                move || {
                    crate::json_resource_result(descriptor_uri, &descriptor_resource)
                        .expect("descriptor resource should encode")
                },
            )
            .expect("resource should register");
        server
            .add_resource_template(
                crate::resource_template_definition(
                    "gpui-form://forms/{form}/descriptor",
                    "gpui_form_descriptor",
                    Some("GPUI form descriptor".to_string()),
                    Some("Descriptor for a generated GPUI form.".to_string()),
                    Some("application/json".to_string()),
                )
                .expect("resource template should build"),
            )
            .expect("resource template should register");
        server
            .add_prompt(
                crate::prompt_definition(
                    "draft_contact",
                    Some("Draft contact".to_string()),
                    Some("Draft values for the contact form.".to_string()),
                    None,
                )
                .expect("prompt definition should build"),
                |_| {
                    crate::text_prompt_result(
                        Some("Draft values for the contact form.".to_string()),
                        "Use the contact descriptor resource and return valid form fields.",
                    )
                },
            )
            .expect("prompt should register");

        let info = server.get_info();
        assert!(info.capabilities.resources.is_some());
        assert!(info.capabilities.prompts.is_some());

        let (server_transport, client_transport) = tokio::io::duplex(4096);
        let server_handle = tokio::spawn(async move {
            let service = server
                .serve(server_transport)
                .await
                .expect("server should start");
            service.waiting().await.expect("server task should join");
        });

        let client = ()
            .serve_with_lifecycle(
                client_transport,
                ClientLifecycleMode::Discover {
                    preferred_versions: vec![ProtocolVersion::V_2026_07_28],
                },
            )
            .await
            .expect("client should start");

        let resources = client
            .peer()
            .list_resources(Default::default())
            .await
            .expect("resources/list should succeed");
        assert_eq!(resources.resources.len(), 1);
        assert_eq!(resources.result_type, Some(ResultType::COMPLETE));
        assert_eq!(resources.ttl_ms, Some(0));
        assert_eq!(resources.cache_scope, Some(CacheScope::Private));
        assert_eq!(resources.resources[0].uri, descriptor_uri);
        assert_eq!(
            resources.resources[0].title.as_deref(),
            Some("Contact descriptor")
        );

        let templates = client
            .peer()
            .list_resource_templates(Default::default())
            .await
            .expect("resources/templates/list should succeed");
        assert_eq!(templates.resource_templates.len(), 1);
        assert_eq!(templates.result_type, Some(ResultType::COMPLETE));
        assert_eq!(templates.ttl_ms, Some(0));
        assert_eq!(templates.cache_scope, Some(CacheScope::Private));
        assert_eq!(
            templates.resource_templates[0].uri_template,
            "gpui-form://forms/{form}/descriptor"
        );

        let resource = client
            .peer()
            .read_resource(ReadResourceRequestParams::new(descriptor_uri))
            .await
            .expect("resources/read should succeed");
        assert_eq!(resource.contents.len(), 1);
        assert_eq!(resource.result_type, Some(ResultType::COMPLETE));
        assert_eq!(resource.ttl_ms, Some(0));
        assert_eq!(resource.cache_scope, Some(CacheScope::Private));
        match &resource.contents[0] {
            crate::McpResourceContents::TextResourceContents {
                uri,
                mime_type,
                text,
                ..
            } => {
                assert_eq!(uri, descriptor_uri);
                assert_eq!(mime_type.as_deref(), Some("application/json"));
                let value: Value =
                    serde_json::from_str(text).expect("resource should contain JSON");
                assert_eq!(value, descriptor);
            },
            other => panic!("expected text resource contents, got {other:?}"),
        }

        let prompts = client
            .peer()
            .list_prompts(Default::default())
            .await
            .expect("prompts/list should succeed");
        assert_eq!(prompts.prompts.len(), 1);
        assert_eq!(prompts.result_type, Some(ResultType::COMPLETE));
        assert_eq!(prompts.ttl_ms, Some(0));
        assert_eq!(prompts.cache_scope, Some(CacheScope::Private));
        assert_eq!(prompts.prompts[0].name, "draft_contact");
        assert_eq!(prompts.prompts[0].title.as_deref(), Some("Draft contact"));

        let prompt = client
            .peer()
            .get_prompt(GetPromptRequestParams::new("draft_contact"))
            .await
            .expect("prompts/get should succeed");
        assert_eq!(
            prompt.description.as_deref(),
            Some("Draft values for the contact form.")
        );
        assert_eq!(prompt.result_type, Some(ResultType::COMPLETE));
        assert_eq!(prompt.messages.len(), 1);
        assert_eq!(prompt.messages[0].role, crate::McpRole::User);
        match &prompt.messages[0].content {
            crate::ContentBlock::Text(content) => {
                assert!(content.text.contains("Use the contact descriptor resource"))
            },
            other => panic!("expected text prompt message, got {other:?}"),
        }

        client.cancel().await.expect("client should close");
        server_handle.await.expect("server should finish");
    });
}

#[test]
fn server_builder_registers_resources_templates_and_prompts() {
    let resource = || {
        crate::resource_definition(
            "shape://sync",
            "sync_resource",
            None,
            None,
            Some("text/plain".to_string()),
        )
        .expect("resource should build")
    };
    let async_resource = crate::resource_definition(
        "shape://async",
        "async_resource",
        None,
        None,
        Some("text/plain".to_string()),
    )
    .expect("resource should build");
    let template = crate::resource_template_definition(
        "shape://{name}",
        "shape_template",
        None,
        None,
        Some("text/plain".to_string()),
    )
    .expect("template should build");
    let prompt =
        crate::prompt_definition("sync_prompt", None, None, None).expect("prompt should build");
    let async_prompt =
        crate::prompt_definition("async_prompt", None, None, None).expect("prompt should build");

    let server = crate::McpServer::builder("builder", "0.0.0")
        .resource(resource(), || {
            crate::text_resource_result("shape://sync", "sync", "text/plain")
        })
        .resource_async(async_resource, || async {
            Ok(crate::text_resource_result(
                "shape://async",
                "async",
                "text/plain",
            ))
        })
        .resource_template(template)
        .prompt(prompt, |_| crate::text_prompt_result(None, "sync"))
        .prompt_async(async_prompt, |_| async {
            Ok(crate::text_prompt_result(None, "async"))
        })
        .build()
        .expect("builder should register resources and prompts");

    assert_eq!(server.resource_count(), 2);
    assert_eq!(server.list_resource_templates().len(), 1);
    assert_eq!(server.prompt_count(), 2);
    assert!(server.contains_prompt("sync_prompt"));

    let mut duplicates = crate::McpServer::new("duplicates", "0.0.0");
    duplicates
        .add_resource(resource(), || {
            crate::text_resource_result("shape://sync", "sync", "text/plain")
        })
        .expect("first resource should register");
    assert!(matches!(
        duplicates.add_resource(resource(), || {
            crate::text_resource_result("shape://sync", "sync", "text/plain")
        }),
        Err(crate::McpToolError::DuplicateResource { .. })
    ));

    let prompt = || {
        crate::prompt_definition("duplicate_prompt", None, None, None).expect("prompt should build")
    };
    duplicates
        .add_prompt(prompt(), |_| crate::text_prompt_result(None, "prompt"))
        .expect("first prompt should register");
    assert!(matches!(
        duplicates.add_prompt(prompt(), |_| crate::text_prompt_result(None, "prompt")),
        Err(crate::McpToolError::DuplicatePrompt { .. })
    ));
}