docs/apps/prefab.mdx
import { VersionBadge } from '/snippets/version-badge.mdx' import PrefabPinWarning from '/snippets/prefab-pin-warning.mdx'
<VersionBadge version="3.1.0" /> <PrefabPinWarning /> <iframe src="/apps/demos/dashboard.html" style={{width:"100%", height:"680px", border:"none", overflow:"hidden", borderRadius:"8px"}} frameBorder="0" scrolling="no"></iframe>Believe it or not, that dashboard is a FastMCP tool. The chart has tooltips. The table is sortable. The badges are styled by deal stage. The whole thing is about 40 lines of Python, and the user sees it right inside their conversation instead of a wall of JSON.
The pattern behind every example on this page is the same: add app=True to your tool, build a UI with Prefab components, and return it as a PrefabApp. Prefab has 100+ components, from data tables and charts to forms and progress bars. You compose them in Python; the host renders them as a live, interactive application.
Most tools return data the user wants to explore. A DataTable is often the smallest useful upgrade — your data goes from a JSON blob to a searchable, sortable table:
from prefab_ui.components import DataTable, DataTableColumn
from fastmcp import FastMCP
mcp = FastMCP("Directory")
@mcp.tool(app=True)
def team_directory() -> DataTable:
"""Browse the team directory."""
employees = [
{"name": "Alice Chen", "role": "Staff Engineer", "dept": "Platform"},
{"name": "Bob Martinez", "role": "Lead Designer", "dept": "Design"},
{"name": "Carol Johnson", "role": "Senior Engineer", "dept": "Platform"},
{"name": "David Kim", "role": "Product Manager", "dept": "Product"},
{"name": "Eva Mueller", "role": "Engineer", "dept": "Platform"},
{"name": "Frank Lee", "role": "Data Scientist", "dept": "ML"},
{"name": "Grace Park", "role": "Eng Manager", "dept": "Platform"},
]
return DataTable(
columns=[
DataTableColumn(key="name", header="Name", sortable=True),
DataTableColumn(key="role", header="Role", sortable=True),
DataTableColumn(key="dept", header="Dept", sortable=True),
],
rows=employees,
search=True,
)
That's it. Add app=True, return a Prefab component instead of raw dicts. FastMCP handles the rendering, sandboxing, and security. No wrapper class needed for simple cases like this.
When numbers tell a better story as a visual, swap in a chart. The API is the same: pass your data as a list of dicts, tell the chart which keys to plot.
<iframe src="/apps/demos/bar-chart.html" style={{width:"100%", height:"430px", border:"none", overflow:"hidden", borderRadius:"8px"}} frameBorder="0" scrolling="no"></iframe>@mcp.tool(app=True)
def quarterly_revenue(year: int) -> BarChart:
"""Show quarterly revenue as a bar chart."""
data = [
{"quarter": "Q1", "revenue": 42000, "costs": 28000},
{"quarter": "Q2", "revenue": 51000, "costs": 31000},
{"quarter": "Q3", "revenue": 47000, "costs": 29000},
{"quarter": "Q4", "revenue": 63000, "costs": 35000},
]
return BarChart(
data=data,
series=[
ChartSeries(data_key="revenue", label="Revenue"),
ChartSeries(data_key="costs", label="Costs"),
],
x_axis="quarter",
show_legend=True,
)
Each ChartSeries plots a different key from the data. BarChart, LineChart, AreaChart, PieChart, RadarChart, and RadialChart all follow the same pattern. Hover over the bars to see tooltips.
@mcp.tool(app=True)
def ticket_breakdown() -> PieChart:
"""Show open tickets by category."""
data = [
{"category": "Bug", "count": 42},
{"category": "Feature", "count": 28},
{"category": "Docs", "count": 15},
{"category": "Infra", "count": 10},
]
return PieChart(
data=data,
data_key="count",
name_key="category",
inner_radius=50,
show_legend=True,
)
See the Prefab chart docs for stacking, curves, custom colors, and more.
Tables and charts are useful on their own, but the real power comes from composing them. Column stacks children vertically, Row lays them out side by side, and with blocks establish nesting — the indentation is the layout.
@mcp.tool(app=True)
def sales_dashboard() -> PrefabApp:
"""Show sales KPIs, trends, and deals."""
monthly = [
{"month": "Jan", "revenue": 48200, "costs": 31000},
{"month": "Feb", "revenue": 52100, "costs": 32500},
{"month": "Mar", "revenue": 61800, "costs": 34200},
{"month": "Apr", "revenue": 58400, "costs": 33800},
]
deals = [
{"account": "Acme Corp", "value": "$84,000", "stage": "Won"},
{"account": "Globex Inc", "value": "$52,000", "stage": "Negotiation"},
{"account": "Initech", "value": "$31,500", "stage": "Proposal"},
{"account": "Wayne Enterprises", "value": "$45,000", "stage": "Lost"},
]
rows = [
{
"account": d["account"],
"value": d["value"],
"stage": Badge(
d["stage"],
variant="success" if d["stage"] == "Won"
else "destructive" if d["stage"] == "Lost"
else "secondary",
),
}
for d in deals
]
total = sum(m["revenue"] for m in monthly)
with PrefabApp() as app:
with Column(gap=4, css_class="p-6"):
with Row(gap=6):
Metric(label="Revenue (Q1-Q4)", value=f"${total:,}")
Metric(label="Deals", value=f"{len(deals)}")
BarChart(
data=monthly,
series=[
ChartSeries(data_key="revenue", label="Revenue"),
ChartSeries(data_key="costs", label="Costs"),
],
x_axis="month",
show_legend=True,
)
Separator()
DataTable(
columns=[
DataTableColumn(key="account", header="Account", sortable=True),
DataTableColumn(key="value", header="Value", sortable=True),
DataTableColumn(key="stage", header="Stage"),
],
rows=rows,
)
return app
Notice how Badge components can be placed inside table cells — any Prefab component works as a cell value, so you can put progress bars, icons, or buttons in your tables too.
Everything above renders once from the data your Python provides. But interactive tools can also respond to user input in real time, without any server round-trips. Prefab's state system lets components read and write client-side values, so the UI updates instantly as the user interacts with it.
<iframe src="/apps/demos/reactive.html" style={{width:"100%", height:"500px", border:"none", overflow:"hidden", borderRadius:"8px"}} frameBorder="0" scrolling="no"></iframe>Try switching regions in the dropdown, and toggling the switch on and off.
from prefab_ui.rx import Rx
@mcp.tool(app=True)
def regional_sales() -> PrefabApp:
"""Sales by region with a live filter."""
north = [
{"month": "Jan", "sales": 22000},
{"month": "Feb", "sales": 25500},
{"month": "Mar", "sales": 24200},
]
south = [
{"month": "Jan", "sales": 5800},
{"month": "Feb", "sales": 6400},
{"month": "Mar", "sales": 5600},
]
west = [
{"month": "Jan", "sales": 6000},
{"month": "Feb", "sales": 6000},
{"month": "Mar", "sales": 5600},
]
with PrefabApp(
state={
"region": "north",
"north": north, "south": south, "west": west,
"show_target": True,
},
) as app:
with Column(
gap=4,
css_class="p-6",
let={"data": "{{ region == 'south' ? south"
" : region == 'west' ? west"
" : north }}"},
):
with Row(gap=4, align="center"):
with Select(name="region", css_class="w-40"):
SelectOption(value="north", label="North")
SelectOption(value="south", label="South")
SelectOption(value="west", label="West")
Switch(name="show_target", css_class="ml-auto")
Text("Show target", css_class="text-sm text-muted-foreground")
BarChart(
data=Rx("data"),
series=[ChartSeries(data_key="sales", label="Sales")],
x_axis="month",
)
with If(Rx("show_target")):
Metric(label="Q1 Target", value="$75,000")
return app
The state dict on PrefabApp declares initial values. The Select writes to the region key on every change. A let binding picks the matching dataset, and the chart re-renders. The Switch toggles a Metric on and off through If(Rx("show_target")). All of this happens in the browser — no calls back to your server.
Rx is a reactive reference: Rx("region") compiles to an expression the renderer evaluates live. It supports arithmetic, comparisons, formatting pipes (.currency(), .percent()), and ternary conditionals (.then()). For the full state system, see the Prefab state docs and expression docs.
Interactive tools render in a sandboxed iframe with a strict CSP. If your tool loads external resources — embedding iframes, fetching from APIs, loading scripts — add the required domains:
from fastmcp.apps import PrefabAppConfig, ResourceCSP
@mcp.tool(app=PrefabAppConfig(
csp=ResourceCSP(frame_domains=["https://example.com"]),
))
def dashboard_with_embed() -> PrefabApp:
...
PrefabAppConfig() with no arguments is equivalent to app=True.
By default, the LLM sees "[Rendered Prefab UI]" as the tool result. If the model needs to reason about the data, return a ToolResult with a text summary alongside the UI:
from fastmcp.tools import ToolResult
@mcp.tool(app=True)
def sales_overview(year: int) -> ToolResult:
"""Show sales visually, summarize for the model."""
data = get_sales_data(year)
total = sum(row["revenue"] for row in data)
with Column(gap=4, css_class="p-6") as view:
BarChart(data=data, series=[ChartSeries(data_key="revenue")])
return ToolResult(
content=f"Total revenue for {year}: ${total:,} across {len(data)} quarters",
structured_content=view,
)
The user sees the chart. The model sees the summary.
fastmcp dev apps