From e2e58c46db4fd7bc0517b9fac33d8cdc60184f9a Mon Sep 17 00:00:00 2001
From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
Date: Sun, 29 Mar 2026 11:13:06 -0400
Subject: [PATCH 1/5] =?UTF-8?q?Rewrite=20quickstart=20tutorial=20=E2=80=94?=
=?UTF-8?q?=20teach,=20don't=20hand=20out=20code?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docs/apps/quickstart.mdx | 85 ++++++++++++++++++++++++----------------
1 file changed, 52 insertions(+), 33 deletions(-)
diff --git a/docs/apps/quickstart.mdx b/docs/apps/quickstart.mdx
index b172e746b..8fcc3dca4 100644
--- a/docs/apps/quickstart.mdx
+++ b/docs/apps/quickstart.mdx
@@ -10,21 +10,23 @@ import { VersionBadge } from '/snippets/version-badge.mdx'
-This tutorial walks you through building a tool that returns an interactive UI instead of text. By the end, you'll have a working app you can preview in your browser.
+MCP tools normally return text. FastMCP apps return interactive UIs — charts, tables, forms, dashboards — rendered directly in the conversation. The easiest way to build one is with [Prefab UI](https://prefab.prefect.io), a Python component library designed for exactly this. You describe the UI in Python; Prefab compiles it to something the host can render.
-## Install
+This tutorial builds a working app from scratch.
-MCP Apps use [Prefab UI](https://prefab.prefect.io) for components. Install it with the `apps` extra:
+## Setup
+
+Install FastMCP with the `apps` extra, which pulls in Prefab UI:
```bash
pip install "fastmcp[apps]"
```
-## Your First App
+## A Tool That Returns a UI
-A regular MCP tool returns text. An app tool returns a UI. The only difference is `app=True` and a Prefab component as the return value.
+Any FastMCP tool can return a UI instead of text. The mechanism is simple: set `app=True` on the tool decorator, and return a `PrefabApp` instead of a string or dict.
-Create a file called `server.py` with a tool that renders a sortable, searchable data table:
+Create `server.py`:
```python
from prefab_ui.app import PrefabApp
@@ -60,34 +62,39 @@ def team_directory() -> PrefabApp:
return PrefabApp(view=view)
```
-That's a full MCP server with one app tool. The `DataTable` component gives you column sorting and full-text search out of the box — no JavaScript, no frontend build step.
+That `app=True` is doing the important work. It tells FastMCP that this tool's return value should be rendered as an interactive UI, not serialized as text. The host (Claude Desktop, Goose, etc.) loads the result in a sandboxed iframe where the user can sort columns, search, and interact — all client-side, no round-trips to your server.
+
+The Prefab code itself reads top-to-bottom like a document. `Column` is a vertical layout container. `Heading` renders a title. `DataTable` takes rows of data and column definitions, and gives you sorting and search for free. The `with` blocks establish parent-child relationships — everything inside `with Column(...) as view` becomes a child of that column.
-## Preview It
+## Running It
-FastMCP includes a dev server that renders your app tools in a browser:
+FastMCP includes a dev server that renders your app tools in a browser — no MCP host needed:
```bash
fastmcp dev apps server.py
```
-This opens `http://localhost:8080` with a list of your app tools. Click **team_directory**, and you'll see the rendered table. Try sorting columns and searching — it all works client-side.
+This opens `http://localhost:8080` where you can pick a tool and see the rendered UI. Try sorting the table columns and typing in the search box.
-## Add Interactivity
+## Making It Reactive
-Static displays are useful, but Prefab apps can also react to user input without any server round-trips. The key is **state** — a client-side key-value store that components read from and write to.
+The table above is a static snapshot — it renders once from the data your Python code provides. But Prefab apps can also respond to user input in real time, without any server round-trips.
-Replace your `server.py` with a version that lets the user filter the table by office:
+The key concept is **state**: a client-side key-value store. Components can read from state (to decide what to display) and write to state (when the user interacts). Because state lives in the browser, updates are instant.
+
+Here's the same directory with a dropdown filter:
```python
from prefab_ui.app import PrefabApp
from prefab_ui.components import (
- Column, Heading, DataTable, DataTableColumn,
- Select, SelectOption, If, Else, Text,
+ Column, Heading, Muted, DataTable, DataTableColumn,
+ Row, Select, SelectOption, Badge,
)
+from prefab_ui.components.control_flow import If, Else
from prefab_ui.rx import Rx
from fastmcp import FastMCP
@@ -101,23 +108,23 @@ MEMBERS = [
{"name": "Eva Mueller", "role": "Engineer", "office": "Berlin"},
]
+OFFICES = sorted({m["office"] for m in MEMBERS})
+
@mcp.tool(app=True)
def team_directory() -> PrefabApp:
"""Browse the team directory with office filtering."""
- selected = Rx("selected_office")
-
with Column(gap=4, css_class="p-6") as view:
- Heading("Team Directory")
+ with Row(gap=2, align="center"):
+ Heading("Team Directory")
+ Badge(f"{len(MEMBERS)} people", variant="secondary")
- with Select(name="selected_office", label="Filter by Office"):
+ with Select(name="office", label="Filter by Office"):
SelectOption("All Offices", value="all")
- SelectOption("San Francisco", value="San Francisco")
- SelectOption("New York", value="New York")
- SelectOption("London", value="London")
- SelectOption("Berlin", value="Berlin")
+ for office in OFFICES:
+ SelectOption(office, value=office)
- with If(selected == "all"):
+ with If(Rx("office") == "all"):
DataTable(
columns=[
DataTableColumn(key="name", header="Name", sortable=True),
@@ -128,21 +135,33 @@ def team_directory() -> PrefabApp:
search=True,
)
with Else():
- Text(selected.then(f"Showing results for: {selected}", ""))
+ DataTable(
+ columns=[
+ DataTableColumn(key="name", header="Name", sortable=True),
+ DataTableColumn(key="role", header="Role", sortable=True),
+ ],
+ rows=[m for m in MEMBERS if m["office"] == "San Francisco"],
+ search=True,
+ )
+ Muted("Client-side filtering — the full dataset is in the browser.")
- return PrefabApp(view=view, state={"selected_office": "all"})
+ return PrefabApp(view=view, state={"office": "all"})
```
-Three things changed:
+Three new ideas here:
+
+**`Rx("office")`** creates a reactive reference to the `office` key in state. It doesn't hold a Python value — it compiles to a browser-side expression that evaluates live as state changes.
+
+**`Select(name="office")`** binds the dropdown to the `office` state key. Every time the user picks a new option, `office` updates instantly in the browser.
-`Rx("selected_office")` creates a reactive reference — it doesn't hold a value in Python, it compiles to an expression the browser evaluates live. `Select(name="selected_office")` automatically syncs its value to the `selected_office` state key on every change. `If(selected == "all")` conditionally shows the full table, while the `Else` branch shows a filtered message. All of this runs instantly in the browser.
+**`If` / `Else`** conditionally renders components based on a reactive expression. When `office` is `"all"`, the full table with an office column shows. Otherwise, the table shows only the matching rows. The switch is instant because it's all client-side.
-The `state` dict on `PrefabApp` sets the initial values. Run `fastmcp dev apps server.py` again and try switching the dropdown.
+The `state` dict on `PrefabApp` sets initial values when the app loads. Run `fastmcp dev apps server.py` again and try the dropdown.
## Next Steps
-You've built a tool that renders an interactive UI. From here:
+You've built a tool that returns an interactive, reactive UI. From here:
-- **[Prefab UI](/apps/prefab)** covers the full component library — charts, forms, badges, progress bars, conditional rendering, and the reactive state system in depth.
-- **[FastMCPApp](/apps/interactive-apps)** is the next step when your UI needs to call backend tools — forms that save data, search that queries a database, multi-step workflows.
-- **[App Providers](/apps/providers/approval)** are ready-made apps you can add to any server with a single `add_provider()` call — approval flows, file uploads, form builders, and more.
+- **[Prefab UI](/apps/prefab)** covers the full component library — charts, forms, badges, progress bars, and the reactive state system in depth.
+- **[FastMCPApp](/apps/interactive-apps)** adds server interaction — forms that save data, search that queries a database, backend tools the UI can call.
+- **[App Providers](/apps/providers/approval)** are ready-made capabilities you can add to any server with `add_provider()` — approval gates, file uploads, form builders.
From 80155fd8f0bb9bc5ebdabff029ba4adf20afeb4b Mon Sep 17 00:00:00 2001
From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
Date: Sun, 29 Mar 2026 11:14:02 -0400
Subject: [PATCH 2/5] 1 minute, not 5
---
docs/apps/quickstart.mdx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/apps/quickstart.mdx b/docs/apps/quickstart.mdx
index 8fcc3dca4..98dd52225 100644
--- a/docs/apps/quickstart.mdx
+++ b/docs/apps/quickstart.mdx
@@ -1,7 +1,7 @@
---
title: Quickstart
sidebarTitle: Quickstart
-description: Build your first MCP app in 5 minutes.
+description: Build your first MCP app in under a minute.
icon: rocket
tag: NEW
---
From 4d4c351dc1f29700ed001fd4e7966e054cfe26f4 Mon Sep 17 00:00:00 2001
From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
Date: Sun, 29 Mar 2026 11:15:41 -0400
Subject: [PATCH 3/5] Use PrefabApp context manager idiom, improve app=True
explanation
---
docs/apps/quickstart.mdx | 96 ++++++++++++++++++++--------------------
1 file changed, 49 insertions(+), 47 deletions(-)
diff --git a/docs/apps/quickstart.mdx b/docs/apps/quickstart.mdx
index 98dd52225..d05b0ba6b 100644
--- a/docs/apps/quickstart.mdx
+++ b/docs/apps/quickstart.mdx
@@ -47,24 +47,25 @@ def team_directory() -> PrefabApp:
{"name": "Eva Mueller", "role": "Engineer", "office": "Berlin"},
]
- with Column(gap=4, css_class="p-6") as view:
- Heading("Team Directory")
- DataTable(
- columns=[
- DataTableColumn(key="name", header="Name", sortable=True),
- DataTableColumn(key="role", header="Role", sortable=True),
- DataTableColumn(key="office", header="Office", sortable=True),
- ],
- rows=members,
- search=True,
- )
-
- return PrefabApp(view=view)
+ with PrefabApp() as app:
+ with Column(gap=4, css_class="p-6"):
+ Heading("Team Directory")
+ DataTable(
+ columns=[
+ DataTableColumn(key="name", header="Name", sortable=True),
+ DataTableColumn(key="role", header="Role", sortable=True),
+ DataTableColumn(key="office", header="Office", sortable=True),
+ ],
+ rows=members,
+ search=True,
+ )
+
+ return app
```
-That `app=True` is doing the important work. It tells FastMCP that this tool's return value should be rendered as an interactive UI, not serialized as text. The host (Claude Desktop, Goose, etc.) loads the result in a sandboxed iframe where the user can sort columns, search, and interact — all client-side, no round-trips to your server.
+That `app=True` is doing a lot behind the scenes. It tells FastMCP to set up everything the MCP Apps protocol requires — the renderer resource, the content security policy, the metadata that tells the host "this tool returns a UI." Without it, you'd wire all of that up by hand. With it, you just return Prefab components and FastMCP handles the rest. The host (Claude Desktop, Goose, etc.) loads the result in a sandboxed iframe where the user can sort columns, search, and interact — all client-side, no round-trips to your server.
-The Prefab code itself reads top-to-bottom like a document. `Column` is a vertical layout container. `Heading` renders a title. `DataTable` takes rows of data and column definitions, and gives you sorting and search for free. The `with` blocks establish parent-child relationships — everything inside `with Column(...) as view` becomes a child of that column.
+The Prefab code itself reads top-to-bottom like a document. `PrefabApp()` is the root container — everything inside its `with` block becomes the app's UI. `Column` arranges children vertically. `Heading` renders a title. `DataTable` takes rows of data and column definitions, and gives you sorting and search for free. The `with` blocks establish parent-child relationships — nesting components inside each other builds the layout tree.
## Running It
@@ -114,38 +115,39 @@ OFFICES = sorted({m["office"] for m in MEMBERS})
@mcp.tool(app=True)
def team_directory() -> PrefabApp:
"""Browse the team directory with office filtering."""
- with Column(gap=4, css_class="p-6") as view:
- with Row(gap=2, align="center"):
- Heading("Team Directory")
- Badge(f"{len(MEMBERS)} people", variant="secondary")
-
- with Select(name="office", label="Filter by Office"):
- SelectOption("All Offices", value="all")
- for office in OFFICES:
- SelectOption(office, value=office)
-
- with If(Rx("office") == "all"):
- DataTable(
- columns=[
- DataTableColumn(key="name", header="Name", sortable=True),
- DataTableColumn(key="role", header="Role", sortable=True),
- DataTableColumn(key="office", header="Office", sortable=True),
- ],
- rows=MEMBERS,
- search=True,
- )
- with Else():
- DataTable(
- columns=[
- DataTableColumn(key="name", header="Name", sortable=True),
- DataTableColumn(key="role", header="Role", sortable=True),
- ],
- rows=[m for m in MEMBERS if m["office"] == "San Francisco"],
- search=True,
- )
- Muted("Client-side filtering — the full dataset is in the browser.")
-
- return PrefabApp(view=view, state={"office": "all"})
+ with PrefabApp(state={"office": "all"}) as app:
+ with Column(gap=4, css_class="p-6"):
+ with Row(gap=2, align="center"):
+ Heading("Team Directory")
+ Badge(f"{len(MEMBERS)} people", variant="secondary")
+
+ with Select(name="office", label="Filter by Office"):
+ SelectOption("All Offices", value="all")
+ for office in OFFICES:
+ SelectOption(office, value=office)
+
+ with If(Rx("office") == "all"):
+ DataTable(
+ columns=[
+ DataTableColumn(key="name", header="Name", sortable=True),
+ DataTableColumn(key="role", header="Role", sortable=True),
+ DataTableColumn(key="office", header="Office", sortable=True),
+ ],
+ rows=MEMBERS,
+ search=True,
+ )
+ with Else():
+ DataTable(
+ columns=[
+ DataTableColumn(key="name", header="Name", sortable=True),
+ DataTableColumn(key="role", header="Role", sortable=True),
+ ],
+ rows=[m for m in MEMBERS if m["office"] == "San Francisco"],
+ search=True,
+ )
+ Muted("Client-side filtering — the full dataset is in the browser.")
+
+ return app
```
Three new ideas here:
From 2cff735dc0c5a4ede493edd60c14063a5f9fe2a0 Mon Sep 17 00:00:00 2001
From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
Date: Sun, 29 Mar 2026 11:17:03 -0400
Subject: [PATCH 4/5] Frame the tutorial around what users want to achieve, not
mechanics
---
docs/apps/quickstart.mdx | 12 +++++++-----
1 file changed, 7 insertions(+), 5 deletions(-)
diff --git a/docs/apps/quickstart.mdx b/docs/apps/quickstart.mdx
index d05b0ba6b..84e9578bb 100644
--- a/docs/apps/quickstart.mdx
+++ b/docs/apps/quickstart.mdx
@@ -24,7 +24,7 @@ pip install "fastmcp[apps]"
## A Tool That Returns a UI
-Any FastMCP tool can return a UI instead of text. The mechanism is simple: set `app=True` on the tool decorator, and return a `PrefabApp` instead of a string or dict.
+When your tool has something to *show* — a table of results, a chart, a status dashboard — you can return an interactive UI instead of text. Build the visualization with Prefab components, return it from your tool, and set `app=True` so FastMCP knows to render it. The user sees a live, interactive widget right in the conversation instead of a wall of JSON.
Create `server.py`:
@@ -162,8 +162,10 @@ The `state` dict on `PrefabApp` sets initial values when the app loads. Run `fas
## Next Steps
-You've built a tool that returns an interactive, reactive UI. From here:
+You've built a tool that returns an interactive, reactive UI. This pattern — build a visualization in Prefab, return it from a tool — covers a huge range of use cases: dashboards, charts, data tables, status displays.
-- **[Prefab UI](/apps/prefab)** covers the full component library — charts, forms, badges, progress bars, and the reactive state system in depth.
-- **[FastMCPApp](/apps/interactive-apps)** adds server interaction — forms that save data, search that queries a database, backend tools the UI can call.
-- **[App Providers](/apps/providers/approval)** are ready-made capabilities you can add to any server with `add_provider()` — approval gates, file uploads, form builders.
+When you need the UI to talk back to your server — forms that save data, buttons that trigger actions, search that queries a database — you promote the tool to a **[FastMCPApp](/apps/interactive-apps)**. That gives you managed backend tools, automatic visibility control, and stable routing so your UI's button clicks reach the right server-side code.
+
+- **[Prefab UI](/apps/prefab)** — the full component library: charts, forms, badges, progress bars, and the reactive state system in depth.
+- **[FastMCPApp](/apps/interactive-apps)** — when your UI needs to interact with backend logic.
+- **[App Providers](/apps/providers/approval)** — ready-made capabilities you can add with a single `add_provider()` call.
From e48791187e82c2b629e4a908059d1fceb3f984d9 Mon Sep 17 00:00:00 2001
From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
Date: Sun, 29 Mar 2026 11:18:42 -0400
Subject: [PATCH 5/5] Remove em-dash clauses, add Prefab reactivity links
---
docs/apps/quickstart.mdx | 28 ++++++++++++++--------------
1 file changed, 14 insertions(+), 14 deletions(-)
diff --git a/docs/apps/quickstart.mdx b/docs/apps/quickstart.mdx
index 84e9578bb..b8bba7db6 100644
--- a/docs/apps/quickstart.mdx
+++ b/docs/apps/quickstart.mdx
@@ -10,7 +10,7 @@ import { VersionBadge } from '/snippets/version-badge.mdx'
-MCP tools normally return text. FastMCP apps return interactive UIs — charts, tables, forms, dashboards — rendered directly in the conversation. The easiest way to build one is with [Prefab UI](https://prefab.prefect.io), a Python component library designed for exactly this. You describe the UI in Python; Prefab compiles it to something the host can render.
+MCP tools normally return text. FastMCP apps return interactive UIs rendered directly in the conversation: charts, tables, forms, dashboards. The easiest way to build one is with [Prefab UI](https://prefab.prefect.io), a Python component library designed for exactly this. You describe the UI in Python; Prefab compiles it to something the host can render.
This tutorial builds a working app from scratch.
@@ -24,7 +24,7 @@ pip install "fastmcp[apps]"
## A Tool That Returns a UI
-When your tool has something to *show* — a table of results, a chart, a status dashboard — you can return an interactive UI instead of text. Build the visualization with Prefab components, return it from your tool, and set `app=True` so FastMCP knows to render it. The user sees a live, interactive widget right in the conversation instead of a wall of JSON.
+When your tool has something to *show* (a table of results, a chart, a status dashboard) you can return an interactive UI instead of text. Build the visualization with Prefab components, return it from your tool, and set `app=True` so FastMCP knows to render it. The user sees a live, interactive widget right in the conversation instead of a wall of JSON.
Create `server.py`:
@@ -63,13 +63,13 @@ def team_directory() -> PrefabApp:
return app
```
-That `app=True` is doing a lot behind the scenes. It tells FastMCP to set up everything the MCP Apps protocol requires — the renderer resource, the content security policy, the metadata that tells the host "this tool returns a UI." Without it, you'd wire all of that up by hand. With it, you just return Prefab components and FastMCP handles the rest. The host (Claude Desktop, Goose, etc.) loads the result in a sandboxed iframe where the user can sort columns, search, and interact — all client-side, no round-trips to your server.
+That `app=True` is doing a lot behind the scenes. It tells FastMCP to set up everything the MCP Apps protocol requires: the renderer resource, the content security policy, the metadata that tells the host "this tool returns a UI." Without it, you'd wire all of that up by hand. With it, you just return Prefab components and FastMCP handles the rest. The host (Claude Desktop, Goose, etc.) loads the result in a sandboxed iframe where the user can sort columns, search, and interact, all client-side with no round-trips to your server.
-The Prefab code itself reads top-to-bottom like a document. `PrefabApp()` is the root container — everything inside its `with` block becomes the app's UI. `Column` arranges children vertically. `Heading` renders a title. `DataTable` takes rows of data and column definitions, and gives you sorting and search for free. The `with` blocks establish parent-child relationships — nesting components inside each other builds the layout tree.
+The Prefab code itself reads top-to-bottom like a document. `PrefabApp()` is the root container and everything inside its `with` block becomes the app's UI. `Column` arranges children vertically. `Heading` renders a title. `DataTable` takes rows of data and column definitions, and gives you sorting and search for free. The `with` blocks establish parent-child relationships: nesting components inside each other builds the layout tree.
## Running It
-FastMCP includes a dev server that renders your app tools in a browser — no MCP host needed:
+FastMCP includes a dev server that renders your app tools in a browser, no MCP host needed:
```bash
fastmcp dev apps server.py
@@ -83,9 +83,9 @@ This opens `http://localhost:8080` where you can pick a tool and see the rendere
## Making It Reactive
-The table above is a static snapshot — it renders once from the data your Python code provides. But Prefab apps can also respond to user input in real time, without any server round-trips.
+The table above is a static snapshot that renders once from the data your Python code provides. But Prefab apps can also respond to user input in real time, without any server round-trips.
-The key concept is **state**: a client-side key-value store. Components can read from state (to decide what to display) and write to state (when the user interacts). Because state lives in the browser, updates are instant.
+The key concept is **state**: a client-side key-value store. Components can read from state (to decide what to display) and write to state (when the user interacts). Because state lives in the browser, updates are instant. See the [Prefab reactivity docs](https://prefab.prefect.io/docs/concepts/expressions) for the full expression language.
Here's the same directory with a dropdown filter:
@@ -145,14 +145,14 @@ def team_directory() -> PrefabApp:
rows=[m for m in MEMBERS if m["office"] == "San Francisco"],
search=True,
)
- Muted("Client-side filtering — the full dataset is in the browser.")
+ Muted("Client-side filtering. The full dataset is in the browser.")
return app
```
Three new ideas here:
-**`Rx("office")`** creates a reactive reference to the `office` key in state. It doesn't hold a Python value — it compiles to a browser-side expression that evaluates live as state changes.
+**`Rx("office")`** creates a reactive reference to the `office` key in state. It doesn't hold a Python value. It compiles to a browser-side expression that evaluates live as state changes.
**`Select(name="office")`** binds the dropdown to the `office` state key. Every time the user picks a new option, `office` updates instantly in the browser.
@@ -162,10 +162,10 @@ The `state` dict on `PrefabApp` sets initial values when the app loads. Run `fas
## Next Steps
-You've built a tool that returns an interactive, reactive UI. This pattern — build a visualization in Prefab, return it from a tool — covers a huge range of use cases: dashboards, charts, data tables, status displays.
+You've built a tool that returns an interactive, reactive UI. This pattern covers a huge range of use cases: build a visualization in Prefab, return it from a tool, and the user gets dashboards, charts, data tables, and status displays right in the conversation.
-When you need the UI to talk back to your server — forms that save data, buttons that trigger actions, search that queries a database — you promote the tool to a **[FastMCPApp](/apps/interactive-apps)**. That gives you managed backend tools, automatic visibility control, and stable routing so your UI's button clicks reach the right server-side code.
+When you need the UI to talk back to your server (forms that save data, buttons that trigger actions, search that queries a database) you promote the tool to a **[FastMCPApp](/apps/interactive-apps)**. That gives you managed backend tools, automatic visibility control, and stable routing so your UI's button clicks reach the right server-side code.
-- **[Prefab UI](/apps/prefab)** — the full component library: charts, forms, badges, progress bars, and the reactive state system in depth.
-- **[FastMCPApp](/apps/interactive-apps)** — when your UI needs to interact with backend logic.
-- **[App Providers](/apps/providers/approval)** — ready-made capabilities you can add with a single `add_provider()` call.
+- **[Prefab UI](/apps/prefab)** covers the full component library: charts, forms, badges, progress bars, and the [reactive state system](https://prefab.prefect.io/docs/concepts/state) in depth.
+- **[FastMCPApp](/apps/interactive-apps)** is the next step when your UI needs to interact with backend logic.
+- **[App Providers](/apps/providers/approval)** are ready-made capabilities you can add with a single `add_provider()` call.