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.
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
| Parameter | Type | Description |
|---|---|---|
request | Model subclass or None | If provided, the AI passes parameters to the React component as props. If None, the UI takes no parameters from the AI. |
path | str | The 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. |
title | str or None | Human-readable title shown in tool listings. |
description | str or None | Description used by the AI to decide when to open this UI. Write it as instructions to the AI. |
display_modes | list 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 inRebootClientProvider.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:
- An MCP tool that the AI calls to open the UI. The tool returns metadata telling the AI client to display the visual interface.
- 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.