---
schema: rendered-html/v1
title: "Interactive Tools - FastMCP"
source_url: https://gofastmcp.com/apps/prefab
generated_at: 2026-06-11T19:35:03.433479+00:00
---




[FastMCP home page![light logo](images/gofastmcp__com__apps__prefab__wordmark.avif)![dark logo](images/gofastmcp__com__apps__prefab__wordmark-white.avif)](gofastmcp__com.md)







Apps

Interactive Tools






















- [Start with a table](#start-with-a-table)
- [Add charts](#add-charts)
- [Compose a dashboard](#compose-a-dashboard)
- [Make it reactive](#make-it-reactive)
- [Content Security Policy](#content-security-policy)
- [Giving the LLM context](#giving-the-llm-context)
- [Next steps](#next-steps)

Apps

# Interactive Tools


Turn your tools into interactive UIs with charts, tables, and dashboards.


New in version `3.1.0`

[Prefab](https://prefab.prefect.io) is under active development with frequent breaking changes. FastMCP sets a minimum `prefab-ui` version but does not pin an upper bound — **pin `prefab-ui` to a specific version in your own dependencies** before deploying.

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](https://prefab.prefect.io) components, and return it as a `PrefabApp`. Prefab has [100+ components](https://prefab.prefect.io/docs/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.

## [​](#start-with-a-table) Start with a table

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

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.

## [​](#add-charts) Add charts

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.

@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](https://prefab.prefect.io/docs/components) for stacking, curves, custom colors, and more.

## [​](#compose-a-dashboard) Compose a dashboard

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

See all 57 lines

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.

## [​](#make-it-reactive) Make it reactive

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.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

See all 51 lines

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](https://prefab.prefect.io/docs/concepts/state) and [expression docs](https://prefab.prefect.io/docs/concepts/expressions).

## [​](#content-security-policy) Content Security Policy

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`.

## [​](#giving-the-llm-context) Giving the LLM context

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.

## [​](#next-steps) Next steps

- **[FastMCPApp](gofastmcp__com__apps__fastmcp-app.md)** — when your UI needs to call backend tools (forms, search, CRUD)
- **[Generative UI](gofastmcp__com__apps__generative.md)** — let the LLM design the UI at runtime
- **[Custom HTML](gofastmcp__com__apps__low-level.md)** — when Prefab isn’t enough (maps, 3D, your own framework)
- **[Examples](gofastmcp__com__apps__examples.md)** — complete working servers you can run today
- **[Development](gofastmcp__com__apps__development.md)** — preview your tools locally with `fastmcp dev apps`
- **[Prefab UI](https://prefab.prefect.io)** — full component reference with 100+ components, theming, and advanced patterns

[FastMCPApp

Previous](gofastmcp__com__apps__fastmcp-app.md)[Generative UI

Next](gofastmcp__com__apps__generative.md)






