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-shapeto publish framework-neutral metadata or build a generator or framework adapter. - Use
component-shape-gpuito declare GPUI components, construction, rendering, and value binding. - Use
component-shape-mcpto 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
DeclaredComponentShapeonly 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, orValueChange::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 = Torvalues(...)for supported values. - Add
value_bindingwhen the shape delegates throughGpuiComponentStateValueBinding<T>. - Put a
GpuiComponentValueBinding<T>implementation insidecomponent_shape!when the wrapper owns the binding behavior. - Implement both
ComponentShapeFor<T>andGpuiComponentShapeFor<T>for a hand-written compatibility pair. - Use
GpuiComponentShapeBuilder<Shape>for configuration selected at the field-use site. UseDefaultGpuiComponentShapeBuilder<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
McpInputandschema_for_inputfor coarse metadata published by a component shape. - Use
McpJsonSchemafor a nested Rust value with a precise JSON Schema. - Use
McpToolValuewhen one value needs both schema and strict decoding. - Use
McpToolInputfor a named top-level tool argument struct. - Use
McpRange<T>for a typed{ "min": ..., "max": ... }range. - Use
McpAnyonly when unconstrained JSON is intentional.
Follow the integration path
- Define schemas and strict arguments in Schemas and decoding.
- Pair arguments with handlers and structured results in Tools and results.
- 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)andmcp(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
mcpdescription 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_definitionandjson_resource_resultfor JSON resources.resource_template_definitionfor resource templates.prompt_definitionandtext_prompt_resultfor 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 anMcpServerfor your transport. - Call
serve_stdio().awaiton 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.