Skip to main content
UI is data. MCP is the transport. This is the standard β€” not one option among several.

πŸ—οΈ The model in one picture

  • You author neutral JSON definitions and token files.
  • The SDK on each platform renders them natively. It hardcodes no feature, no style, no UI concept β€” it resolves tokens and moves your state keys, nothing else.
  • The engine runs workflows whose agents pick your components (by whenToUse) and stream data into them.

Why SDUI


🧱 The closed primitive set

Definitions compose ONLY these. The set is frozen β€” adding to it is an SDK change, and a build-failing guard test enforces that. ❌ Wrong instinct: β€œI need a Chart / Accordion / Carousel primitive.” βœ… Right instinct: compose it β€” bars are Box + Each over data; an accordion is visibleWhen on a dev-named state key. If it genuinely cannot be composed, that’s a platform conversation, not a definition.

πŸ”Œ MCP Apps β€” the standard

A template is an MCP App. This is not an integration detail; it is the contract every client and every tool in the ecosystem shares: The rule: a host must NEVER hand-roll its own transport β€” no bespoke REST send, no custom user_action message, no side-channel state push. The SDK owns the one interaction path; every consumer (Studio, a native app, an external MCP client) shares it. If your channel needs something the path doesn’t do, that’s a platform gap to raise β€” not a workaround to build.

The org endpoint + its default app (the front door)

Each org is its own MCP endpoint β€” a self-contained connector / β€œexperience”:
Within an org, exactly one app’s manifest.json sets "default": true β€” the org’s home (typically its chat template, e.g. acmechatlayout). The endpoint tags that tool with _meta["unoverse/default"], so a client knows which app to open first as the conversation’s entry point. A lint rule enforces at most one default per org. MCP is pull-based (no β€œauto-open on connect”): our SDK reads that flag and opens the home app immediately, rendering its arrival defaultState; a foreign host (ChatGPT) surfaces it when the user first engages. default only answers β€œwhich app is the front door” β€” the app’s own defaultState/autoTrigger/binding behave exactly as usual. (The old per-app expose flag is removed β€” org scoping is the boundary now.)

The two lanes (know which carries what)

Everything you author reads from the first lane. The second lane belongs to native services (like voice) β€” covered in 04 β€” State, because it’s where β€œlocked” state comes from.

🎨 The four ways a component renders

Every design component reaches the chat by one of four paths β€” two in the workflow world, two discovered natively from Spatial. Every path ends identically: the SDK renders your definition into the conversation. Knowing which path you’re on tells you where the data comes from and whether a workflow is involved. (Canonical: docs/unoverse/UNOVERSE_MCP_TEMPLATE_PROTOCOL.md Β§β€œThe four ways UI reaches the chat”.) Two worlds:
  • A + D β€” the workflow world. Assign a workflow; it drives the surface (A) and streams its pieces in (D). Streaming is the standard runtime paint path, not a legacy one.
  • B + C β€” the native-MCP-from-spatial world. The component is discovered, not pushed. B carries its own data; C is a reusable card shell a Spatial node fills.
The one rule that keeps B + C pure: a component is never a callable primitive. The discovered unit is a standard MCP app (a tool with a ui:// resource); the agent does an ordinary tools/call, the result carries the UI, and the SDK renders it. Nothing component-specific is invented. This axis is orthogonal to presentation. The four ways are only how the data arrives. Once a component is in the store β€” by any of the four β€” how it’s shown is the reaction contract: the component carries its own defaultState and owns its faces (inline, focused, product, …); a template reacts to that state via a ComponentSlot.select.where surface and frames the matching face β€” it never reaches into the component. So a streamed (D) product card can write defaultState: "product" and a chat template’s product surface frames it, exactly the same way a self-contained (B) one would. Which of the four delivered it, and how a template presents it, are independent choices β€” see 04 β€” State.

πŸ” One path, dev and prod

Studio is not a special harness. It is just another MCP client: it subscribes to the same definition resources, receives the same component stream, and runs the same native renderers as production channels. That’s why:
  • β€œWorks in Studio” provably means β€œworks in production.”
  • Hot reload in Studio is the same resources/subscribe mechanism that live-updates production channels.
  • Live mode in Studio is literally production, pointed at local renderers.

Next: coming from React or Flutter? Read the 02a translation table first. Then 03 β€” Components β€” composition, props, and reuse.