Skip to main content

UI methods

A UI method declares a React component that an MCP client opens inside the conversation when the AI calls the associated tool. Unlike reader, writer, transaction, and workflow methods, a UI method has no backend servicer code — the React App.tsx component is the implementation.

MCP Apps only

UI methods exist for MCP, where they appear as tools. There is no way to invoke one from a browser or from your backend. Your web and React Native frontends render their own components and call ordinary methods.

Syntax​

You declare a UI method in your API definition, alongside your other methods:

show_clicker=UI(
request=None,
path="clicker",
title="Counter Clicker",
description="Interactive clicker UI "
"for the counter.",
),

Parameters​

ParameterTypeDescription
requestModel subclass or NoneIf provided, the AI passes parameters to the React component as props. If None, the UI takes no parameters from the AI.
pathstrThe UI's URL path under Reboot's /__/frontend/mcp/, such as "clicker" or "shop/cart". In development the frontend's dev server (--frontend-url) serves it at /__/frontend/mcp/clicker/, from mcp/clicker/ (the mcpAppPath of the Vite plugin); built, Reboot reads it from <--mcp-app-dist-path>/clicker/index.html. The method name, not the path, is the UI's identity.
titlestr or NoneHuman-readable title shown in tool listings.
descriptionstr or NoneDescription used by the AI to decide when to open this UI. Write it as instructions to the AI.
display_modeslist of "inline", "fullscreen", "pip"The modes the UI prefers, most preferred first, such as ["fullscreen", "inline"]. When the UI opens it asks the host for the first one the host supports. The host can refuse: the Claude desktop app's Code tab, for one, changes the mode only after a click inside the UI. With none of them supported, the request refused, or no display_modes, the UI stays inline. To offer fullscreen in such a host, give the UI a button whose click calls useMcpApp()?.requestDisplayMode({ mode: "fullscreen" }).

Examples​

Zero-config UI​

From the mcp-app-counter example:

show_clicker=UI(
request=None,
path="clicker",
title="Counter Clicker",
description="Interactive clicker UI "
"for the counter.",
),

Parameterized UI​

From the mcp-app-counter-dashboard example:

show_dashboard=UI(
request=DashboardConfig,
path="dashboard",
title="Counter Dashboard",
description="Dashboard UI. Use "
"`personalized_message` to impart wisdom "
"on the topic of counting things.",
),

The React component receives DashboardConfig fields as props:

import {
type DashboardConfig,
type UseCounterApi,
useCounter,
} from "@api/mcp_app_counter/v1/counter_rbt_react";

export const DashboardApp: React.FC<DashboardConfig> = ({
personalizedMessage,
}) => {
const { counter, isLoading } = useCounter();

if (isLoading) return <div>loading...</div>;
if (counter === undefined) return <div>No counter available.</div>;

return (
<Dashboard counter={counter} personalizedMessage={personalizedMessage} />
);
};

const Dashboard: React.FC<DashboardConfig & { counter: UseCounterApi }> = ({
counter,
personalizedMessage,
}) => {
const { response } = counter.useGet();
// ...
};

React implementation​

Each UI method points to a directory containing:

  • index.html — the Vite entry point.
  • main.tsx — renders the React component wrapped in RebootClientProvider.
  • App.tsx — the component that implements the UI.

The component uses generated hooks (like useCounter()) to interact with backend state. The state ID is automatically provided by the framework when the UI is opened — you do not need to pass it. Called without an id, the hook returns { counter, isLoading }, where counter is undefined until the ID resolves, so check both before using the handle:

import {
type UseCounterApi,
useCounter,
} from "@api/mcp_app_counter/v1/counter_rbt_react";

export const ClickerApp: FC = () => {
const { counter, isLoading } = useCounter();

if (isLoading) return <div>loading...</div>;
if (counter === undefined) return <div>No counter available.</div>;

return <Clicker counter={counter} />;
};

const Clicker: React.FC<{ counter: UseCounterApi }> = ({ counter }) => {
const { response } = counter.useGet();

const handleIncrement = async () => {
await counter.increment({ amount: 1 });
};

return (
<div>
<span>{response?.value ?? 0}</span>
<button onClick={handleIncrement}>+</button>
</div>
);
};

How it works​

When you define a UI method, Reboot generates two MCP artifacts:

  1. An MCP tool that the AI calls to open the UI. The tool returns metadata telling the AI client to display the visual interface.
  2. An MCP resource that serves the compiled HTML/JS/CSS. The AI client loads this resource in an embedded browser.

The React app talks to your backend through the same generated hooks as any other frontend, so its reads are reactive and its mutations retry safely.

Bundling UI methods​

Each UI method's React app must be bundled into a single HTML file so that the Reboot server can serve it as an MCP resource. Use Vite with vite-plugin-singlefile.

The agent skills write the vite.config.ts below for you. If you are setting a project up by hand, copy it from an example such as bank-pydantic.

Dependencies​

npm install -D @vitejs/plugin-react vite vite-plugin-singlefile

Vite configuration​

The config comes with @reboot-dev/reboot-react: the reboot() plugin from its vite entry point drives three jobs — the dev server, and two builds selected by the RBT_BUILD_TARGET env var (set per UI by the package's build(), which build.mjs calls) — a per-UI MCP build (RBT_BUILD_TARGET=mcp:<name>) and the standalone web/ SPA build (RBT_BUILD_TARGET=web). It takes the frontend's directories: mcpAppPath, whose <name>/index.html subdirectories are the MCP UIs; webAppPath, the web app, if there is one; and apiPath, the generated client, for the @api alias. frontend/vite.config.ts is the stock Vite file with the plugin added:

import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
import { reboot } from "@reboot-dev/reboot-react/vite";

export default defineConfig({
plugins: [
react(),
reboot({ mcpAppPath: "mcp", webAppPath: "web", apiPath: "api" }),
],
});

Anything else is ordinary Vite config, added to the literal: another plugin, build.sourcemap, and so on. The values the plugin sets (root, base, the output directories, the dev server's port and host, the @api alias) are the contract with the backend's --frontend-url and --mcp-app-dist-path; setting one of them in the config too is an error, which the plugin raises by name.

Building​

Build one UI by name by setting RBT_BUILD_TARGET (or build every UI and the web/ SPA at once with npm run build, whose build.mjs calls build() from @reboot-dev/reboot-react/vite):

cd frontend && RBT_BUILD_TARGET=mcp:clicker npx vite build

The bundled HTML file is written to frontend/dist/mcp/clicker/index.html, <--mcp-app-dist-path>/<path>/index.html for --mcp-app-dist-path=frontend/dist/mcp. The Reboot server serves it as an MCP resource.

No backend servicer​

UI methods are skipped in your servicer class. You only implement reader, writer, transaction, and workflow methods in the servicer.