Keyboard shortcuts

Press ← or → to navigate between chapters

Press ? to show this help

Press Esc to hide this help

Introduction

component-shape lets Rust libraries describe a UI component once and expose the public contracts that frameworks, code generators, and Model Context Protocol (MCP) integrations need. Those contracts cover naming, value compatibility, runtime capabilities, render behavior, and structured model input.

A shape is a Rust type that describes a component. Its metadata lets a consumer choose compatible values and input formats; framework-specific traits add construction, rendering, and event handling.

Choose the entry point that owns your task:

  • Use component-shape to publish framework-neutral metadata or build a generator or framework adapter.
  • Use component-shape-gpui to declare GPUI components, construction, rendering, and value binding.
  • Use component-shape-mcp to turn coarse component metadata or typed Rust inputs into JSON Schema, MCP tools, composed servers, and stdio applications.

Start with Getting started to add a dependency. The remaining chapters explain how to combine the contracts without coupling the framework-neutral crate to GPUI or MCP protocol behavior.

For complete signatures and trait definitions, use the component-shape API documentation, component-shape-gpui API documentation, and component-shape-mcp API documentation.

Getting started

Use Rust 1.99 or newer, then add the narrowest crate that owns your integration. All crates use edition 2024.

Framework-neutral metadata

[dependencies]
component-shape = "0.3"

Implement ComponentShapeMetadata on a shape type. Add ComponentShapeFor<Value> for each value the shape supports. Continue with Framework-neutral shapes.

GPUI declarations

[dependencies]
component-shape-gpui = "0.3"
gpui-kit = "0.6.4"

The GPUI crate re-exports the shared framework-neutral traits and uses the gpui-kit facade for GPUI types and components. Choose the derive for an owned component or component_shape! for a local wrapper around external types. Continue with GPUI shapes.

MCP integrations

[dependencies]
component-shape-mcp = "0.3"

The default features export the McpJsonSchema and McpToolInput derives. Use default-features = false when an integration only consumes coarse McpInput metadata or implements schema contracts manually. Continue with MCP integration.

Check the integration

From your workspace root, replace <package-name> with the package that owns the new dependency:

cargo check -p <package-name>

A successful check confirms that the selected crate and its public contracts resolve in that package.

Framework-neutral shapes

Use a framework-neutral shape when a generator or adapter needs component metadata without depending on GPUI or another UI runtime. A shape can advertise value compatibility, capabilities, stable generated-name suffixes, and coarse MCP input metadata.

Declare metadata and value compatibility

#![allow(unused)]
fn main() {
extern crate component_shape;
use component_shape::{ComponentShapeFor, ComponentShapeMetadata, McpInput};

struct TextInputShape;

impl ComponentShapeMetadata for TextInputShape {
    const MCP_INPUT: McpInput = McpInput::string();
}

impl ComponentShapeFor<String> for TextInputShape {}
}

ComponentShapeFor<Value>::MCP_INPUT inherits the shape-level value. Override the associated constant on a specific shape/value pair when one shape supports values with different model-facing forms.

Choose the contract

  • Put shape-owned prototyping, capabilities, and coarse MCP input in ComponentShapeMetadata.
  • Use ComponentShapeFor<Value> to advertise a supported value and any value-specific MCP input.
  • Require DeclaredComponentShape only when a backend accepts shapes produced by its trusted declaration surface.
  • Record a selected source field and shape path with ComponentShapeUse.
  • Normalize component events into ValueChange::Unchanged, ValueChange::Set, or ValueChange::Clear.

Use ComponentSuffix for generated identifier suffixes: ASCII letters, digits, and underscores, starting with a letter or underscore. Empty strings and _ are rejected. Use RustPath, RustType, and RustExpr when a generator must preserve validated Rust syntax.

Keep MCP metadata coarse

Use McpInput for common scalar, collection, object, and range shapes. The default, McpInput::unsupported(), leaves model input unadvertised. Use McpInput::any() only when arbitrary JSON is intentional. Move precise JSON Schema, typed decoding, tool registration, and transport behavior to component-shape-mcp.

GPUI shapes

component-shape-gpui connects framework-neutral metadata to GPUI state, construction, optional rendering, configured builders, and value binding.

Declare an owned component

Derive GpuiComponentShape when your crate owns the rendered component and its backing state:

#![allow(unused)]
fn main() {
extern crate component_shape_gpui;
extern crate gpui_kit;
use component_shape_gpui::GpuiComponentShape;

pub struct TextInputState;

impl TextInputState {
    pub fn new(
        _window: &mut gpui_kit::Window,
        _cx: &mut gpui_kit::Context<'_, Self>,
    ) -> Self {
        Self
    }
}

#[derive(GpuiComponentShape)]
#[gpui_component_shape(value = String, field_suffix = "input")]
pub struct TextInput;

impl TextInput {
    pub fn new(_state: &gpui_kit::Entity<TextInputState>) -> impl gpui_kit::IntoElement {
        gpui_kit::div()
    }
}
}

The derive infers TextInputState from the component name. Set state = path::State for another name or location. By default, generated construction calls State::new(window, cx).

Wrap external component types

Use component_shape! when the state or rendered component belongs to another crate. The local wrapper owns the generated implementations and avoids orphan-rule conflicts.

#![allow(unused)]
fn main() {
extern crate component_shape_gpui;
extern crate gpui_kit;
component_shape_gpui::component_shape! {
    pub struct EmailInputShape {
        state = gpui_kit::component::input::InputState;
        component = gpui_kit::component::input::Input;
        value = String;
        field_suffix = "input";
    }
}
}

Omit component = ... when the wrapper only needs state, construction, and metadata. It then uses NoGpuiRenderComponent, whose RENDERS value is false.

Publish value and construction behavior

  • Add value = T or values(...) for supported values.
  • Add value_binding when the shape delegates through GpuiComponentStateValueBinding<T>.
  • Put a GpuiComponentValueBinding<T> implementation inside component_shape! when the wrapper owns the binding behavior.
  • Implement both ComponentShapeFor<T> and GpuiComponentShapeFor<T> for a hand-written compatibility pair.
  • Use GpuiComponentShapeBuilder<Shape> for configuration selected at the field-use site. Use DefaultGpuiComponentShapeBuilder<Shape> for the shape’s normal constructor.

The derive and function-like macro implement DeclaredGpuiComponentShape and the framework-neutral DeclaredComponentShape. Consumers can require those markers when they accept only macro-declared shapes.

Common Rust values infer coarse McpInput metadata. Add an explicit mcp_input = ... only for a known custom wire form; use component-shape-mcp for precise schema and decoding.

MCP integration

Use component-shape-mcp when an application must expose component metadata or typed Rust inputs through MCP. The crate keeps each published schema paired with the decoder and handler contract that accepts it.

Add the crate as shown in Getting started. Its default features include the schema and tool-input derives.

Choose the input contract

  • Use McpInput and schema_for_input for coarse metadata published by a component shape.
  • Use McpJsonSchema for a nested Rust value with a precise JSON Schema.
  • Use McpToolValue when one value needs both schema and strict decoding.
  • Use McpToolInput for a named top-level tool argument struct.
  • Use McpRange<T> for a typed { "min": ..., "max": ... } range.
  • Use McpAny only when unconstrained JSON is intentional.

Follow the integration path

  1. Define schemas and strict arguments in Schemas and decoding.
  2. Pair arguments with handlers and structured results in Tools and results.
  3. Compose registrations and choose a transport in Servers and stdio.

The crate checks protocol contracts and executes Koruma rules for validated typed registrations. Koruma is the ecosystem’s canonical domain validator. Your application selects the rules, authorizes calls, and decides what handlers may do.

Schemas and decoding

Define a named argument struct with McpToolInput when a tool should publish an object schema and decode the same contract into Rust.

#![allow(unused)]
fn main() {
extern crate component_shape_mcp;
#[derive(component_shape_mcp::McpToolInput)]
#[serde(rename_all = "camelCase")]
#[mcp(crate = component_shape_mcp)]
struct SearchArgs {
    #[serde(alias = "q")]
    query: String,
    page_size: Option<u32>,
}
}

The derive follows serde deserialize names and aliases, rejects unknown input fields, and decodes each field through McpToolValue. It also implements McpJsonSchema, so the argument type can be nested in another schema.

Describe nested values

Derive McpJsonSchema for application-owned named structs, transparent newtypes, and fieldless enums. Built-in implementations cover common primitives, options, collections, arrays, tuples, sets, string-keyed maps, references, Cow<T>, boxed values, McpRange<T>, and McpAny.

The derive honors deserialize-facing serde behavior:

  • Renames and aliases become accepted input names. serde(rename_all) follows Serde’s distinct field and enum-variant rules, including acronyms, digits, and underscores. When serialization and deserialization names differ, the schema uses the deserialization names.
  • mcp(rename) and mcp(rename_all) take precedence for MCP wire names and preserve MCP’s word-based case conversion. Decoding translates those names to Serde’s deserialization names.
  • Deserialization-skipped fields are omitted.
  • Defaulted and optional fields are not required.
  • #[serde(flatten)] fields are rejected; use an explicit nested field or implement a custom schema and decoder.
  • Rust doc comments become descriptions unless an mcp description is set.

Use #[mcp(crate = path::to::mcp)] only when the dependency is renamed or multiple MCP facade re-exports make the path ambiguous.

Build custom schemas

Use typed builders such as McpSchema::string(), McpSchema::integer().with_minimum(0_u64), and McpSchema::object().with_properties(...). Reserve McpSchema::new(...) for JSON Schema keywords that the typed builders do not cover.

Tool input schemas must describe a top-level object. Output schemas may describe any JSON value. Use McpInput::unsupported() for a coarse shape that should not advertise model input and McpInput::any() or McpAny only for intentionally arbitrary JSON.

Decode untyped calls strictly

Custom untyped executors receive McpToolCall. Convert the call with into_arguments(), consume known fields with take_required_tool_value::<T> or take_present_tool_value::<T>, and call finish()? to reject unknown fields. Prefer these schema-paired helpers over taking raw JSON.

Tools and results

Register a typed tool so its published input schema and Rust handler argument remain paired by type.

extern crate component_shape_mcp;
fn main() -> Result<(), component_shape_mcp::McpToolError> {
#[derive(component_shape_mcp::McpToolInput)]
#[serde(rename_all = "camelCase")]
#[mcp(crate = component_shape_mcp)]
struct SearchArgs {
    #[serde(alias = "q")]
    query: String,
    page_size: Option<u32>,
}

let mut tools = component_shape_mcp::McpToolRegistry::new();

let tool = component_shape_mcp::tool_definition_for_input::<SearchArgs>(
    "search",
    Some("Search".to_owned()),
    None,
    None,
)?;

tools.add_typed_tool(tool, |args: SearchArgs| {
    component_shape_mcp::tool_structured_result(
        component_shape_mcp::serde_json::json!({ "query": args.query }),
    )
})?;

let server = component_shape_mcp::McpServer::from_tool_registry(
    "search-server",
    "1.0.0",
    tools,
);
Ok(())
}

This creates a server with one registered tool. It can now serve requests using the transport described in Servers and stdio.

Use the async registration methods when the handler returns a future. Use an untyped tool only when the integration must decode a dynamic argument set.

Execute Koruma domain rules

Derive koruma::Koruma alongside McpToolInput and register domain inputs through add_koruma_tool or add_koruma_tool_async. Koruma is the ecosystem’s canonical domain validator. The registry strictly decodes arguments, runs ValidateExt, then invokes the handler only on success. Async validation also precedes creating the handler’s future.

use koruma_collection::numeric::RangeValidation;

#[derive(component_shape_mcp::McpToolInput, koruma::Koruma)]
struct BatchArgs {
    #[koruma(RangeValidation::<_>.min(1).max(5))]
    count: u32,
}

fn main() -> Result<(), component_shape_mcp::McpToolError> {
let mut tools = component_shape_mcp::McpToolRegistry::new();
let tool = component_shape_mcp::tool_definition_for_input::<BatchArgs>(
    "batch", None, None, None,
)?;
tools.add_koruma_tool(tool, |args: BatchArgs| {
    component_shape_mcp::tool_structured_result(
        component_shape_mcp::serde_json::json!({ "count": args.count }),
    )
})?;
Ok(())
}

Add the koruma facade and koruma-collection for derives and built-in rules. component-shape-mcp uses the framework-neutral koruma-core runtime contracts. Server builders expose koruma_tool and koruma_tool_async. Applications retain rule selection, authorization, and handler policy.

Failures return Koruma’s structured form, field, and element issues through MCP, including source field names, labels, indices, and typed runtime parameters. McpValidationIssue::from(&issue) shares that conversion with domain adapters. Use koruma_validation_error(&error) to convert an error or validate_koruma after constructing an untyped input. Failed custom errors that enumerate no issues still return a form-level failure. Static schema rule descriptors and hints describe constraints to clients; domain validation executes the Koruma rules.

Add metadata

Store application-owned names, titles, descriptions, icons, and MCP annotation hints in McpToolMetadata. Use tool_definition_for_input_with_metadata when the metadata and typed input should be constructed together.

Registration validates names, required text, icons, schemas, duplicates, and incompatible annotation hints. Raw custom tool definitions registered through add_tool or add_tool_async receive the same checks.

Return structured results

Use tool_structured_result for a successful structured response. If the tool publishes an output schema, its successful structured_content must match that schema.

The registry compiles each output schema at registration and shares the validator across calls and registry clones. Direct calls, async calls, and MCP protocol calls all validate successful results. Invalid schemas fail definition construction or registration with InvalidSchema; missing or invalid structured content produces InvalidToolOutput. Handler error results bypass output validation. Error kinds and structured fields are stable; diagnostic text may change with the validator.

Output schemas use JSON Schema Draft 2020-12 when $schema is absent. Explicit Draft 4, 6, 7, 2019-09, and 2020-12 dialects are supported; unknown dialects are rejected. format remains annotation-only, including application formats such as language-tag. References must resolve within the supplied schema, including bundled $defs and identifiers. External retrieval is disabled for every URI scheme, including HTTP and local files, even if another dependency enables the validator’s retrieval features.

Return the exact advertised property and enum names. The input decoder’s aliases and x-mcp* metadata do not rename or normalize output. If a type serializes with different names from its deserialize-facing McpJsonSchema, supply an output schema that describes the serialized representation. Application data containing keys such as $ref or $schema remains data and is never retrieved as a schema.

Handler failures remain error results and may include a structured error object. McpToolError supplies stable error kinds and relevant fields so clients can branch on failures without parsing display text. Use validation_issues_error when one call must report multiple domain validation issues.

Servers and stdio

Use one McpServer to compose generated registrars with application-owned tools, resources, resource templates, and prompts. Duplicate tool names, resource URIs, and prompt names fail registration instead of silently replacing an existing entry.

McpToolRegistry owns reusable MCP definitions and handlers. Use McpServer::from_tool_registry when the application builds that tool surface independently. Registry clones share handler allocations, while each server retains its own identity, resources, prompts, and transport lifecycle.

Compose registrations

Start with McpServer::builder(name, version). Chain generated registrars through register, or add custom entries through the matching builder or mutable-server methods.

The common definition helpers cover static integrations:

  • resource_definition and json_resource_result for JSON resources.
  • resource_template_definition for resource templates.
  • prompt_definition and text_prompt_result for text prompts.

Use async resource, prompt, or tool handlers when the application must await I/O.

Choose a serving boundary

  • Call build()? on a builder to obtain an McpServer for your transport.
  • Call serve_stdio().await on a server or builder from an async application.
  • Call serve_stdio_blocking() at a synchronous binary boundary; it creates its own Tokio runtime.

The stdio server uses newline-delimited JSON-RPC and delegates MCP lifecycle handling to rmcp. Treat it as a long-lived host: the registry and stateful handlers remain alive across calls until EOF, cancellation, or another application-owned shutdown signal ends the transport. Serving one call and then stopping is an explicit host policy.

Smoke-test a binary

Use McpStdioSmokeClient for process-level tests that launch a real server with piped stdin, stdout, and stderr. Exercise discovery, listing, resource reads, and tool calls through the client. Use tool_call_structured_content to read structured output without depending on the protocol field spelling.

Keep these tests at the application boundary. Use focused schema, decoding, and registration tests for library-level behavior.

Troubleshooting

A GPUI shape is not compatible with a value

Declare value = T or values(...), publish compatibility through value binding, or implement both ComponentShapeFor<T> and GpuiComponentShapeFor<T>.

A consumer rejects a hand-written GPUI shape

If the error names DeclaredGpuiComponentShape, the consumer requires a macro-declared shape. Use the derive or component_shape! so that marker is generated, then rerun the consumer’s Cargo check.

A derive cannot find component-shape-mcp

Use the normal crate name or an unambiguous facade re-export. Add #[mcp(crate = path::to::mcp)] for a renamed crate or when multiple facades make inference ambiguous.

A tool input schema is rejected

Tool inputs need a top-level object schema. Derive McpToolInput for a named argument struct. Use scalar, list, or other schemas only for nested fields and tool outputs.

An argument is reported as unknown

Match the deserialize-facing serde name or one of its aliases. In a custom untyped handler, consume every supported field before calling McpArguments::finish().

A successful output fails validation

Return structured_content that matches the declared output schema. Keep handler failures as error results with a structured error object.