fastmcp/docs/apps/quickstart.mdx
Jeremiah Lowin 3a9717e6be
Publish docs for v3.2.0 (#3713)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Jeremiah Lowin <jlowin@users.noreply.github.com>
Co-authored-by: Marvin Context Protocol <41898282+Marvin Context Protocol@users.noreply.github.com>
Co-authored-by: voidborne-d <voidborne-d@users.noreply.github.com>
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: d 🔹 <258577966+voidborne-d@users.noreply.github.com>
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
Co-authored-by: nightcityblade <nightcityblade@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Bill Easton <strawgate@users.noreply.github.com>
Co-authored-by: Sumanshu Nankana <sumanshunankana@gmail.com>
Co-authored-by: Eric Robinson <ericrobinson@indeed.com>
Co-authored-by: Martim Santos <martimfasantos@gmail.com>
Co-authored-by: d 🔹 <liusway405@gmail.com>
Co-authored-by: Matthieu B <66959271+mtthidoteu@users.noreply.github.com>
Co-authored-by: Sascha Buehrle <47737812+saschabuehrle@users.noreply.github.com>
Co-authored-by: Hakancan <142545736+hkc5@users.noreply.github.com>
Co-authored-by: nightcityblade <jackchen@haloailabs.com>
Co-authored-by: Matt Hallowell <17804673+mhallo@users.noreply.github.com>
Co-authored-by: nate nowack <thrast36@gmail.com>
Co-authored-by: Bill Easton <williamseaston@gmail.com>
Co-authored-by: Marcus Shu <46469249+shulkx@users.noreply.github.com>
Co-authored-by: Rushabh Doshi <radoshi@gmail.com>
Co-authored-by: AIKAWA Shigechika <shige@aikawa.jp>
Co-authored-by: Jeremy Simon <simonjer805@gmail.com>
Co-authored-by: Miguel Miranda Dias <7780875+pandego@users.noreply.github.com>
Co-authored-by: Anthony James Padavano <padavano.anthony@gmail.com>
Co-authored-by: Mostafa Kamal <hiremostafa@gmail.com>
Fix auto-close MRE script posting comment without closing (#3386)
Fix WorkOS token scope verification bypass 🤖 Generated with Codex (#3407)
Fix initialize McpError fallthrough 🤖 Generated with Codex (#3413)
Fix transform arg collisions with passthrough params (#3431)
Fix get_* returning None when latest version is disabled (#3439)
Fix get_* returning None when latest version is disabled (#3421)
Fix server lifespan overlap teardown (#3415)
Fix $ref output schema object detection regression (#3420)
resolved annotations (#3429)
Fix async partial callables rejected by iscoroutinefunction (#3438)
Fix async partial callables rejected by iscoroutinefunction (#3423)
fix: add version to components (#3458)
fix: use intent-based flag for OIDC scope patch in load_access_token (#3465)
Fixes #3461
fix: normalize Google scope shorthands and surface valid_scopes (#3477)
fix: resolve ty 0.0.23 type-checking errors and bump pin (#3481)
fix: shield lifespan teardown from cancellation (#3480)
fix: forward custom_route endpoints from mounted servers (#3462)
fix updates _get_additional_http_routes() to traverse providers,
Fixes #3457
fix: remove hardcoded version from CLI help text (#3456)
fix: monty 0.0.8 compatibility, drop external_functions from constructor (#3468)
fix: task test teardown hanging 5s per test (#3499)
Closes #3498
fix: validate workspace path is a directory before cursor install (#3440)
Fixes #3426
fix: handle re.error from malformed URI templates in build_regex (#3501)
fix: reject empty/OIDC-only required_scopes in AzureProvider (#3503)
fix: restrict $ref resolution to local refs only (SSRF/LFI) (#3502)
fix warnings and timeouts (#3504)
close upgrade check issue when build passes (#3505)
Closes #3484
fix: URL-encode path params to prevent SSRF/path traversal (GHSA-vv7q-7jx5-f767) (#3507)
fix: prevent path traversal in skill download (#3493)
fix: prefer IdP-granted scopes over client-requested scopes in OAuthProxy (#3492)
fix: remove unrelated transform and http.py changes from PR scope
fix: remove forced follow_redirects from httpx_client_factory calls (#3496)
fix: stop passing follow_redirects to httpx_client_factory
fix: restore follow_redirects=True for custom httpx client factories
Closes #3509
fix: CSRF double-submit cookie check in consent flow (#3519)
fix: validate server names in install commands (#3522)
fix: use raw strings for regex in pytest.raises match (#3523)
fix: reject refresh tokens used as Bearer access tokens (#3524)
fix: route ResourcesAsTools/PromptsAsTools through server middleware (#3495)
fix: resolve Pyright "Module is not callable" on @tool, @resource, @prompt decorators (#3540)
fix: filter warnings by message in KEY_PREFIX test (#3549)
fix: suppress output schema for ToolResult subclass annotations (#3548)
fix: increase sleep duration in proxy cache tests (#3567)
fix: store absolute token expiry to prevent stale expires_in on reload (#3572)
fix: preserve tool properties named 'title' during schema compression (#3582)
Fix loopback redirect URI port matching per RFC 8252 §7.3 (#3589)
Fix app tool routing: visibility check and middleware propagation (#3591)
Fix query parameter serialization to respect OpenAPI explode/style settings (#3595)
Fix dev apps form: union types, textarea support, JSON parsing (#3597)
fix(google): replace deprecated /oauth2/v1/tokeninfo with /oauth2/v3/userinfo (#3603)
fix: resolve EntraOBOToken dependency injection through MultiAuth (#3609)
fix(docs): correct misleading stateless_http header (#3622)
fix: filesystem provider import machinery (#3626)
Closes #3625 (issues 2, 3, 6)
fix: recover StdioTransport after subprocess exits (#3630)
fix(server): preserve mounted tool task metadata (#3632)
fix: scope deprecation warning filter to FastMCPDeprecationWarning (#3649)
fix imports, add PrefabAppConfig (#3650)
fix: resolve CurrentFastMCP/ctx.fastmcp to child server in mounted background tasks (#3651)
Fix blocking docs issues: chart imports, Select API, Rx consistency (#3652)
closed by default (#3657)
Fix prompt caching middleware missing wrap/unwrap round-trip (#3666)
fix: serialize object query params per OpenAPI style/explode rules (#3662)
Fixes #2857
fix: HTTP request headers not accessible in background task workers (#3631)
fix: restore HTTP headers in worker execution path for background tasks (#3681)
fix: strip discriminator after dereferencing schemas (#3682)
fix: remove stale ty:ignore directives for ty 0.0.26 (#3684)
Fix docs gaps in app provider pages (#3690)
fix: dev apps log panel UX improvements (#3698)
fix dev server empty string args (#3700)
2026-03-30 16:48:30 -04:00

208 lines
10 KiB
Text

---
title: Quickstart
sidebarTitle: Quickstart
description: Build your first MCP app in under a minute.
icon: rocket
tag: NEW
---
import { VersionBadge } from '/snippets/version-badge.mdx'
<VersionBadge version="3.2.0" />
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. Here's what you'll have in about a minute:
<Frame>
<img src="/apps/images/app-quickstart.png" alt="A team directory app with a pie chart and sortable data table, rendered inside a conversation in Goose" />
</Frame>
## Setup
Install FastMCP with the `apps` extra, which pulls in Prefab UI:
```bash
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.
Create `server.py`:
```python server.py expandable
from collections import Counter
from prefab_ui.app import PrefabApp
from prefab_ui.components import Column, Grid, Heading, DataTable, DataTableColumn
from prefab_ui.components.charts import PieChart
from fastmcp import FastMCP
mcp = FastMCP("My First App")
@mcp.tool(app=True)
def team_directory() -> PrefabApp:
"""Browse the team directory."""
members = [
{"name": "Alice Chen", "role": "Staff Engineer", "office": "San Francisco"},
{"name": "Bob Martinez", "role": "Lead Designer", "office": "New York"},
{"name": "Carol Johnson", "role": "Senior Engineer", "office": "London"},
{"name": "David Kim", "role": "Product Manager", "office": "San Francisco"},
{"name": "Eva Mueller", "role": "Engineer", "office": "Berlin"},
{"name": "Frank Lee", "role": "Data Scientist", "office": "San Francisco"},
{"name": "Grace Park", "role": "Engineering Manager", "office": "New York"},
]
office_counts = [
{"office": office, "count": count}
for office, count in Counter(m["office"] for m in members).items()
]
with PrefabApp() as app:
with Column(gap=4, css_class="p-6"):
Heading("Team Directory")
with Grid(columns=[1, 2], gap=4):
PieChart(
data=office_counts,
data_key="count",
name_key="office",
show_legend=True,
)
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 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 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:
```bash
fastmcp dev apps server.py
```
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.
## Making It Interactive
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 interaction in real time, without any server round-trips.
The key concept is **state**: a client-side key-value store that components read from and write to. When the user interacts with a component, it updates state. Other components that reference that state re-render instantly. See the [Prefab state docs](https://prefab.prefect.io/docs/concepts/state) for the full guide.
Here's the same directory, but now clicking a row shows that person's details in a card:
<Frame>
<img src="/apps/images/app-quickstart-dev-2.png" alt="The team directory with a detail card showing after clicking Bob Martinez" />
</Frame>
```python expandable server.py
from collections import Counter
from prefab_ui.actions import SetState
from prefab_ui.app import PrefabApp
from prefab_ui.components import (
Card, CardContent, CardHeader, Column, Grid, H3, Heading, Muted,
Row, DataTable, DataTableColumn, Badge, Small, Text,
)
from prefab_ui.components.charts import PieChart
from prefab_ui.components.control_flow import If
from prefab_ui.rx import Rx, STATE
from fastmcp import FastMCP
mcp = FastMCP("My First App")
MEMBERS = [
{"name": "Alice Chen", "role": "Staff Engineer", "office": "San Francisco", "email": "alice@company.com", "projects": 3},
{"name": "Bob Martinez", "role": "Lead Designer", "office": "New York", "email": "bob@company.com", "projects": 5},
{"name": "Carol Johnson", "role": "Senior Engineer", "office": "London", "email": "carol@company.com", "projects": 2},
{"name": "David Kim", "role": "Product Manager", "office": "San Francisco", "email": "david@company.com", "projects": 7},
{"name": "Eva Mueller", "role": "Engineer", "office": "Berlin", "email": "eva@company.com", "projects": 1},
{"name": "Frank Lee", "role": "Data Scientist", "office": "San Francisco", "email": "frank@company.com", "projects": 4},
{"name": "Grace Park", "role": "Engineering Manager", "office": "New York", "email": "grace@company.com", "projects": 6},
]
OFFICE_COUNTS = [
{"office": office, "count": count}
for office, count in Counter(m["office"] for m in MEMBERS).items()
]
@mcp.tool(app=True)
def team_directory() -> PrefabApp:
"""Browse the team directory."""
with PrefabApp(state={"selected": None}) as app:
with Column(gap=4, css_class="p-6"):
Heading("Team Directory")
with Grid(columns=[1, 2], gap=4):
PieChart(
data=OFFICE_COUNTS,
data_key="count",
name_key="office",
show_legend=True,
)
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,
on_row_click=SetState("selected", Rx("$event")),
)
with If(STATE.selected):
with Card():
with CardHeader():
with Row(gap=2, align="center"):
H3(Rx("selected.name"))
Badge(Rx("selected.office"))
with CardContent():
with Grid(columns=3, gap=4):
with Column(gap=0):
Small("Role")
Text(Rx("selected.role"))
with Column(gap=0):
Small("Email")
Text(Rx("selected.email"))
with Column(gap=0):
Small("Active Projects")
Text(Rx("selected.projects"))
return app
```
Three new ideas here:
**`SetState` + `on_row_click`** is the interaction. When the user clicks a table row, `SetState("selected", Rx("$event"))` writes the clicked row's data into the `selected` state key. `$event` is a special variable that contains the event payload (in this case, the row dict).
**`Rx("selected.name")`** reads from state reactively. It doesn't hold a Python value. It compiles to a browser-side expression that re-evaluates live whenever `selected` changes. So `Text(Rx("selected.name"))` always shows the name of whoever was last clicked.
**`If(STATE.selected)`** conditionally renders the detail card only when something has been selected. Before any click, `selected` is `None` and the card is hidden.
The `state` dict on `PrefabApp` sets initial values when the app loads. Run `fastmcp dev apps server.py` again and try clicking a row.
## Next Steps
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.
- **[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.