# Schema-Driven UI (/docs/guides/ui-rendering)

## Overview

Schema-driven UI lets an LLM create apps on the fly. The model generates a JSON
schema describing the layout, components, and tool bindings based on what the
user asked, and `@xmcp-dev/ui` renders it into a working app at runtime. The
user never writes UI code.

There are three entry points, each giving you more control:

* `createRenderJsonTool()`: zero-config, handles everything
* `Rendered`: custom tool wrapper with the built-in preview engine
* `App`: full ownership of parsing, validation, and rendering

Start with `createRenderJsonTool()` and move down only when you need more
control.

<Callout variant="info">
  If you are building a handwritten React MCP App instead of rendering
  LLM-generated schemas, see [MCP Apps](/docs/core-concepts/mcp-apps).
</Callout>

## Quickstart

To scaffold a new project with the package, stylesheet, tools, and schema
reference resource already wired up:

```bash
npx create-xmcp-app@latest my-ui-server --ui-kit --yes
```

You can also run the interactive command and select **MCP App with UI kit**.
The starter puts `globals.css` at the project root, matching the Tailwind MCP
App template. xmcp automatically includes this stylesheet in tool UIs.

Import the package stylesheet once:

```css title="globals.css"
@import "tailwindcss";
@import "@xmcp-dev/ui/styles.css";

@theme {
  --font-sans: "Geist", ui-sans-serif, sans-serif;
}
```

Expose the `render-json` tool:

```tsx title="src/tools/render-json.tsx"
import { createRenderJsonTool } from "@xmcp-dev/ui";

const renderJsonTool = createRenderJsonTool();

export const metadata = renderJsonTool.metadata;
export const schema = renderJsonTool.schema;
export default renderJsonTool.handler;
```

Add the schema reference as a normal xmcp resource so the model can read the
component contract named in the tool description:

```ts title="src/resources/(skill)/xmcp-ui/schema-reference.ts"
export {
  schemaReferenceResourceHandler as default,
  schemaReferenceResourceMetadata as metadata,
} from "@xmcp-dev/ui";
```

Both `create-xmcp-app --ui-kit` and `npx @xmcp-dev/ui init` create this resource
for you.

The model supplies a `schemaJson` string and the package handles parsing,
validation, progressive preview, theme fallback, and rendering.

## Adding shadcn components

The `create-xmcp-app --ui-kit` starter is configured for the shadcn CLI. From
its project directory, add the components you want:

```bash
pnpm dlx shadcn@latest add button card dialog
```

You can use the generated components in handwritten React tools:

```tsx
import { AppShell, useMcpApp } from "@xmcp-dev/ui";
import { Button } from "#components/ui/button";

export default function MyApp() {
  const { openLink } = useMcpApp();
  return (
    <AppShell theme="light">
      <Button onClick={() => openLink("https://xmcp.dev/docs")}>
        Open docs
      </Button>
    </AppShell>
  );
}
```

`components.json` points to `src/components/ui`, with native `#components`,
`#lib`, and `#hooks` package imports. The starter enables TypeScript's bundler
resolution and includes `src/lib/utils.ts`, which re-exports the kit's `cn`.
The CLI installs dependencies required by each component.

The root stylesheet imports `@xmcp-dev/ui/shadcn.css` and `tw-animate-css`.
The optional shadcn stylesheet maps Tailwind v4's semantic utilities, such as
`bg-primary` and `text-foreground`, to the kit's HSL theme tokens. Keep this
stylesheet when adding components; there is no need to run `shadcn init` again.

`AppShell` supplies its theme tokens and dark-mode class to nested components.
For dialogs and other components that render portals under `document.body`,
keep the document's `dark` class in sync with the `AppShell` theme as well.
The stylesheet supplies light and dark defaults at the document level.

The existing-project `xmcp-ui init` command adds the UI kit files; configuring
shadcn in an existing project follows the [manual installation guide](https://ui.shadcn.com/docs/installation/manual).
Use the same optional stylesheet to keep the kit's theme tokens compatible.

## Transport

`transportMode` defaults to `"auto"`: tool calls use the MCP App host when the
renderer is embedded in one and fall back to direct HTTP for standalone
previews. Set `"host"` only when the app must require a host:

```tsx title="src/tools/render-json.tsx"
import { createRenderJsonTool } from "@xmcp-dev/ui";

const renderJsonTool = createRenderJsonTool({
  transportMode: "host",
});

export const metadata = renderJsonTool.metadata;
export const schema = renderJsonTool.schema;
export default renderJsonTool.handler;
```

Use `"http"` to require direct HTTP. The same option exists on `Rendered` and
`App`.

Direct HTTP uses the MCP SDK to negotiate modern or legacy servers and correlate
JSON and SSE responses. Connections are shared within an app and closed when
it unmounts or switches to the host. Stateless servers remain stateless; when a
server issues a session, the client reuses it and requests its deletion during
cleanup. If a session expires, the failed tool call is not replayed: the next
user action opens a new connection.

## Constrain model-provided endpoints

An AppSchema can include `mcpServerUrl` and `mcpHeaders`. When the schema comes
from a model, pin the endpoint in server-owned configuration:

```tsx title="src/tools/render-json.tsx"
import { createRenderJsonTool } from "@xmcp-dev/ui";

const renderJsonTool = createRenderJsonTool({
  serverUrl: "https://mcp.example.com",
  allowedOrigins: ["https://mcp.example.com"],
});

export const metadata = renderJsonTool.metadata;
export const schema = renderJsonTool.schema;
export default renderJsonTool.handler;
```

`serverUrl` ignores the schema's `mcpServerUrl`. `allowedOrigins` renders a
visible error if the effective origin is not allowed. The renderer always
rejects non-HTTP(S) endpoints, embedded credentials, and redirects. It drops
invalid, transport-reserved, or unsafe header entries before calling `fetch`.

## What the packaged path does for you

The packaged `renderJson` path renders through `Rendered` and handles common
model-generated mistakes:

* Progressive previewing while the model is still streaming
* Partial JSON recovery instead of waiting for the full payload
* Normalizes common safe sizing mistakes
* Rejects unreadable inline `themeTokens`, falling back to the preset theme
* Light mode and `zinc` preset by default

The server author controls defaults while the model only needs to supply
`schemaJson`.

## Using `Rendered`

Use this when you want your own tool definition but still want the built-in
preview engine.

```tsx title="src/tools/render-dashboard.tsx"
import { Rendered } from "@xmcp-dev/ui";
import { z } from "zod";

export const schema = {
  schemaJson: z.string().optional(),
};

export const metadata = {
  name: "renderDashboard",
  description: "Preview an AppSchema with server-owned defaults.",
};

export default function renderDashboard({
  schemaJson,
}: {
  schemaJson?: string;
}) {
  return (
    <Rendered
      schemaJson={schemaJson}
      previewMode="progressive"
      themeMode="light"
      themePreset="zinc"
      serverUrl="https://mcp.example.com"
      allowedOrigins={["https://mcp.example.com"]}
    />
  );
}
```

## Using `App` directly

Use this when you want to own the full preview pipeline: JSON parsing, schema
validation, loading states, error handling, and theme control.

```tsx title="src/tools/render-strict-app.tsx"
import { App, validateSchema } from "@xmcp-dev/ui";
import { z } from "zod";

export const schema = {
  schemaJson: z.string().optional(),
};

export const metadata = {
  name: "renderStrictApp",
  description: "Render a validated AppSchema with custom server-side behavior.",
};

export default function renderStrictApp({
  schemaJson,
}: {
  schemaJson?: string;
}) {
  if (!schemaJson?.trim()) {
    return <div>Waiting for schema JSON...</div>;
  }

  try {
    const parsed = JSON.parse(schemaJson);
    const appSchema = validateSchema(parsed);
    return (
      <App
        schema={appSchema}
        serverUrl="https://mcp.example.com"
        allowedOrigins={["https://mcp.example.com"]}
      />
    );
  } catch (error) {
    return <pre>{error instanceof Error ? error.message : String(error)}</pre>;
  }
}
```

## References

* [MCP Apps](/docs/core-concepts/mcp-apps)
* [CSS](/docs/core-concepts/css)
* [Tools](/docs/core-concepts/tools)
