Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.metabind.ai/llms.txt

Use this file to discover all available pages before exploring further.

In this tutorial you’ll build a small BindJS component from scratch — a configurable call-to-action card with a title, a color variant, a counter that increments on tap, and previews for the component gallery. By the end you’ll have touched every piece of a working component: metadata, properties, a body that returns a layout, state via useState, and Self previews.
The component you build here renders as React on the web, SwiftUI on iOS, and Jetpack Compose on Android from this single source. You don’t write a separate version per platform.

Prerequisites

Before you start, you need:
  • A Metabind project with access to MCP App Studio. If you haven’t created one yet, start with Your first MCP App, then come back here.
  • Familiarity with TypeScript or modern JavaScript. You don’t need React or SwiftUI experience.
You’ll author the component in MCP App Studio’s component editor. No local toolchain is required.

Build the component

1

Define metadata

Every component begins with metadata that describes how it’s surfaced in the studio’s component picker, in the gallery, and in any preview UI. The fields are short and human-readable.
const metadata = {
  title: "Action card",
  description: "A configurable card with a title, a color variant, and a tap counter",
  category: "Cards",
}
title is the name shown in the picker. description helps teammates understand what the component does. category groups the component alongside others — pick a name that fits your project’s organization (for example, "Cards", "Controls", "Layout").
2

Define properties

Properties make a component configurable. Each property is declared with a helper function — PropertyString, PropertyEnum, PropertyNumber, PropertyBoolean, and so on — and the schema you build does three things at once: it generates typed props for the body, it validates inputs at runtime, and it tells the studio which inspector control to render.For the action card, you need a title and a color variant.
const properties = {
  title: PropertyString({
    title: "Title",
    description: "The card's main label",
    required: true,
    defaultValue: "Get started",
  }),

  variant: PropertyEnum({
    title: "Variant",
    description: "Color treatment for the card",
    options: [
      { value: "blue", label: "Blue" },
      { value: "green", label: "Green" },
      { value: "red", label: "Red" },
    ],
    defaultValue: "blue",
    inspector: { control: "segmented" },
  }),
}
A few things to notice. required: true on title means the component won’t render without one. defaultValue makes the property safe to omit. options on PropertyEnum is a list of { value, label } pairs — the value is what your body receives; the label is what the inspector shows. The inspector.control: "segmented" hint asks the studio to render a segmented control instead of a dropdown.
The body’s props argument is typed automatically from this schema. props.title is string, props.variant is "blue" | "green" | "red". You don’t need to write a separate interface.
3

Write the body function

The body is a function that receives props and children and returns a component tree. You compose the tree from BindJS primitives (VStack, HStack, Text, Button, shapes) and apply modifiers like .padding(), .background(), and .cornerRadius() by chaining methods.Start with a simple layout — a vertical stack containing the title, on a colored background.
const body = (props, children) =>
  VStack({ spacing: 12, alignment: "leading" }, [
    Text(props.title)
      .font("title2")
      .fontWeight("semibold")
      .foregroundStyle(Color("white")),

    Text("Tap the button below").foregroundStyle(Color("white").opacity(0.85)),
  ])
    .padding(20)
    .frame({ maxWidth: ".infinity", alignment: "leading" })
    .background(Color(props.variant))
    .cornerRadius(16)
The flow reads top to bottom: build a VStack of two Text views, then apply padding, an expanded frame, a background color tied to the variant, and a corner radius — all by chaining modifiers off the stack itself.
4

Add interactivity with useState

Hooks let a component hold local state across renders. useState is the basic one, mirroring its React counterpart: it returns a value and a setter, and changing the value re-runs the body.Add a tap counter and a button that increments it.
const body = (props, children) => {
  const [taps, setTaps] = useState(0)

  return VStack({ spacing: 12, alignment: "leading" }, [
    Text(props.title)
      .font("title2")
      .fontWeight("semibold")
      .foregroundStyle(Color("white")),

    Text(`Tapped ${taps} ${taps === 1 ? "time" : "times"}`)
      .foregroundStyle(Color("white").opacity(0.85)),

    Button("Tap me", () => setTaps(taps + 1))
      .padding({ horizontal: 16, vertical: 8 })
      .background(Color("white").opacity(0.2))
      .foregroundStyle(Color("white"))
      .cornerRadius(8),
  ])
    .padding(20)
    .frame({ maxWidth: ".infinity", alignment: "leading" })
    .background(Color(props.variant))
    .cornerRadius(16)
}
Button takes a label and an action. When you call setTaps(taps + 1), the runtime re-runs the body with the new value, the third Text re-renders, and the AST diff is sent to the renderer.
Hooks can only be called inside a body function. If you ever see “hooks used outside a component” in development, check that you’re not calling useState at the module top level.
5

Add previews

Previews show how the component looks under different inputs. They appear in the component gallery, in design reviews, and anywhere the studio needs to render the component without real data. Use the Self helper to instantiate the current component with sample props, and .previewName() to label each preview.
const previews = [
  Self({ title: "Get started", variant: "blue" }).previewName("Default"),
  Self({ title: "Confirmed", variant: "green" }).previewName("Success"),
  Self({ title: "Action required", variant: "red" }).previewName("Warning"),
]
Aim for previews that cover the meaningful states of the component — variants, edge-case content (long titles, empty strings), and any combination a teammate might want to inspect at a glance.

Complete code

Here’s the full component, ready to paste into MCP App Studio.
const metadata = {
  title: "Action card",
  description: "A configurable card with a title, a color variant, and a tap counter",
  category: "Cards",
}

const properties = {
  title: PropertyString({
    title: "Title",
    description: "The card's main label",
    required: true,
    defaultValue: "Get started",
  }),

  variant: PropertyEnum({
    title: "Variant",
    description: "Color treatment for the card",
    options: [
      { value: "blue", label: "Blue" },
      { value: "green", label: "Green" },
      { value: "red", label: "Red" },
    ],
    defaultValue: "blue",
    inspector: { control: "segmented" },
  }),
}

const body = (props, children) => {
  const [taps, setTaps] = useState(0)

  return VStack({ spacing: 12, alignment: "leading" }, [
    Text(props.title)
      .font("title2")
      .fontWeight("semibold")
      .foregroundStyle(Color("white")),

    Text(`Tapped ${taps} ${taps === 1 ? "time" : "times"}`)
      .foregroundStyle(Color("white").opacity(0.85)),

    Button("Tap me", () => setTaps(taps + 1))
      .padding({ horizontal: 16, vertical: 8 })
      .background(Color("white").opacity(0.2))
      .foregroundStyle(Color("white"))
      .cornerRadius(8),
  ])
    .padding(20)
    .frame({ maxWidth: ".infinity", alignment: "leading" })
    .background(Color(props.variant))
    .cornerRadius(16)
}

const previews = [
  Self({ title: "Get started", variant: "blue" }).previewName("Default"),
  Self({ title: "Confirmed", variant: "green" }).previewName("Success"),
  Self({ title: "Action required", variant: "red" }).previewName("Warning"),
]

export default defineComponent({ metadata, properties, body, previews })
Save the file in MCP App Studio. The studio compiles the component, runs the body against each preview, and shows the results in the gallery. Edit any field and the preview updates.

Using the component elsewhere

Once a component is published, any other component in the same package can call it by name. The studio infers the function signature from the properties schema, so editor autocomplete shows you the available props.
ActionCard({ title: "Welcome back", variant: "green" })
You can also chain modifiers onto a custom component, the same way you would on a built-in.
ActionCard({ title: "Heads up", variant: "red" })
  .padding(8)
  .shadow({ radius: 6, y: 2 })

What to do next

Authoring components

Metadata, body, previews, and the full defineComponent shape.

Properties

Every property helper, validation, and inspector control.

MCP host integration

Turning a component into an Interactive Tool an AI host can call.

State and hooks

useState, useStore, and the runtime hook surface.