Comprehensive MCP Apps docs, string CallTool resolution (#3575)

* Comprehensive MCP Apps docs, string CallTool resolution, bump prefab-ui >=0.13.0

Rewrites the apps documentation as a learning journey: overview → Prefab apps
→ FastMCPApp → patterns → dev tools → custom HTML. Adds a new FastMCPApp page
covering composable apps with @app.tool()/@app.ui(), CallTool, forms, actions,
and composition. Teaches Rx() and set_initial_state() as the primary state API.

Adds string-based CallTool resolution so CallTool("save_contact") resolves to
the tool's global key, matching callable ref behavior. Requires prefab-ui 0.13.0
which passes strings through the tool resolver.

* Detect ambiguous string CallTool resolution across apps

* Simplify string name registry to plain dict (last-write-wins)
This commit is contained in:
Jeremiah Lowin 2026-03-21 19:44:11 -04:00 committed by GitHub
commit 1be9b4d39c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 1088 additions and 270 deletions

View file

@ -3,12 +3,17 @@ title: Development
sidebarTitle: Development
description: Preview and test your app tools locally without a full MCP host.
icon: flask
tag: NEW
---
import { VersionBadge } from '/snippets/version-badge.mdx'
<VersionBadge version="3.2.0" />
<Frame>
<img src="/apps/images/dev-app.png" alt="The dev UI showing a rendered Prefab app with the MCP inspector panel" />
</Frame>
`fastmcp dev apps` launches a browser-based preview for your app tools. It starts your MCP server and a local dev UI side by side — you pick a tool, fill in its arguments, and see the rendered result in a new tab. No MCP host client needed.
This works with both [Prefab apps](/apps/prefab) and [custom HTML apps](/apps/low-level).
@ -16,7 +21,6 @@ This works with both [Prefab apps](/apps/prefab) and [custom HTML apps](/apps/lo
## Quick Start
```bash
pip install "fastmcp[apps]"
fastmcp dev apps server.py
```
@ -32,6 +36,14 @@ When you submit a form, the dev server **calls your tool** via the MCP protocol
A **reverse proxy** on `/mcp` forwards requests from the browser to your MCP server, avoiding CORS issues that would otherwise block the iframe-based renderer from talking to a different port.
## MCP Inspector
The dev UI includes an inspector panel on the left side that captures MCP traffic in real time. It shows JSON-RPC messages flowing between the browser and your server — requests, responses, and AppBridge `postMessage` traffic.
Each entry shows direction, method, timing, and a smart summary. Click any entry to expand the full JSON-RPC body. The panel auto-scrolls to new messages unless you've scrolled up to inspect older ones.
The inspector is useful for debugging: you can see exactly what arguments your tool received, what it returned, and how the AppBridge communicated with the renderer.
## Options
```bash

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 KiB

View file

@ -0,0 +1,537 @@
---
title: FastMCPApp
sidebarTitle: FastMCPApp
description: Managed tool binding, visibility, and composition for apps with heavy server interaction.
icon: puzzle-piece
tag: NEW
---
import { VersionBadge } from '/snippets/version-badge.mdx'
<VersionBadge version="3.2.0" />
<Tip>
[Prefab](https://prefab.prefect.io) is in early, active development — its API changes frequently and breaking changes can occur with any release. Always pin `prefab-ui` to a specific version in your dependencies.
</Tip>
Any [Prefab app](/apps/prefab) can call server tools — there's nothing stopping you from using `CallTool("tool_name")` in a regular `@mcp.tool(app=True)`. But once you have multiple backend tools, the management overhead adds up: Which tools should the model see vs. only the UI? What happens to string-based tool references when servers are composed under namespaces? How do you keep things wired correctly as the app grows?
`FastMCPApp` is a class that solves these problems. It gives you two decorators that work together:
- **`@app.ui()`** — entry-point tools the model calls to open the app. These return a Prefab UI.
- **`@app.tool()`** — backend tools the UI calls via `CallTool`. These do the work.
Backend tools get globally stable identifiers that survive namespacing. Visibility is managed automatically — the model sees entry points, the UI sees backends. And `CallTool` accepts function references instead of strings, so references are refactorable and composition-safe.
## Your First Interactive App
Here's a minimal app with a form that saves data:
```python
from prefab_ui.actions import SetState, ShowToast
from prefab_ui.actions.mcp import CallTool
from prefab_ui.app import PrefabApp
from prefab_ui.components import (
Badge, Button, Column, ForEach, Form,
Heading, Input, Row, Separator, Text,
)
from prefab_ui.rx import RESULT
from fastmcp import FastMCP, FastMCPApp
app = FastMCPApp("Notes")
notes_db: list[dict] = []
@app.tool()
def add_note(title: str, body: str) -> list[dict]:
"""Save a note and return all notes."""
notes_db.append({"title": title, "body": body})
return list(notes_db)
@app.ui()
def notes_app() -> PrefabApp:
"""Open the notes app."""
with Column(gap=6, css_class="p-6") as view:
Heading("Notes")
with ForEach("notes") as note:
with Row(gap=2, align="center"):
Text(note.title, css_class="font-semibold")
Badge(note.body)
Separator()
with Form(
on_submit=CallTool(
"add_note",
on_success=[
SetState("notes", RESULT),
ShowToast("Note saved!", variant="success"),
],
on_error=ShowToast("Failed to save", variant="error"),
)
):
Input(name="title", label="Title", required=True)
Input(name="body", label="Body", required=True)
Button("Add Note")
return PrefabApp(view=view, state={"notes": list(notes_db)})
mcp = FastMCP("Notes Server", providers=[app])
```
When the model calls `notes_app`, the user sees a form. Submitting it calls `add_note` on the server, updates the state with the result, and shows a toast — all without leaving the UI.
Let's break down the key concepts.
## Entry Points: @app.ui()
Entry points are what the model sees and calls to open your app. They return a Prefab UI, just like display tools:
```python
@app.ui()
def dashboard() -> PrefabApp:
"""The model calls this to open the dashboard."""
with Column(gap=4, css_class="p-6") as view:
Heading("Dashboard")
# ... build UI ...
return PrefabApp(view=view)
```
Entry points default to `visibility=["model"]` — they show up in the tool list for the LLM but aren't callable from within the app UI. They support the same options as `@mcp.tool`: `name`, `description`, `title`, `tags`, `icons`, `auth`, and `timeout`.
```python
@app.ui(title="Contact Manager", description="Open the contact management interface")
def contact_manager() -> PrefabApp:
...
```
## Backend Tools: @app.tool()
Backend tools do the work. The UI calls them via `CallTool`; they run on the server and return data:
```python
@app.tool()
def save_contact(name: str, email: str) -> list[dict]:
"""Save a contact and return the updated list."""
db.append({"name": name, "email": email})
return list(db)
```
By default, backend tools are only visible to the app UI (`visibility=["app"]`). The model doesn't see them in the tool list. If you want a tool callable by both the model and the UI, pass `model=True`:
```python
@app.tool(model=True)
def list_contacts() -> list[dict]:
"""Both the model and the UI can call this."""
return list(db)
```
Backend tools support `name`, `description`, `auth`, and `timeout`:
```python
@app.tool(description="Search contacts by name or email", timeout=10.0)
def search(query: str) -> list[dict]:
...
```
## Connecting UI to Backend: CallTool
`CallTool` is the bridge between the UI and the server. Pass the name of a backend tool registered with `@app.tool()`:
```python
from prefab_ui.actions.mcp import CallTool
# Reference a backend tool by name
CallTool("save_contact", arguments={"name": "Alice", "email": "alice@example.com"})
# Arguments can reference state with Rx
from prefab_ui.rx import STATE
CallTool("search", arguments={"query": STATE.search_term})
```
FastMCPApp resolves the name to the tool's stable global key automatically, so `CallTool("save_contact")` keeps working even when the server is mounted under a namespace.
You can also pass the function directly — `CallTool(save_contact)` — which can be convenient when the tool is defined in the same file. Both forms resolve identically.
### Handling Results
Server calls are asynchronous. Use `on_success` and `on_error` callbacks to handle outcomes:
```python
from prefab_ui.actions import SetState, ShowToast
from prefab_ui.rx import RESULT
CallTool(
"save_contact",
on_success=[
SetState("contacts", RESULT),
ShowToast("Saved!", variant="success"),
],
on_error=ShowToast("Something went wrong", variant="error"),
)
```
`RESULT` is a reactive reference to the value the tool returned — available inside `on_success` callbacks. Similarly, `ERROR` (from `prefab_ui.rx`) is available inside `on_error`.
Callbacks can be a single action or a list of actions. They execute in order, and an error in any action short-circuits the rest.
### result_key Shorthand
When a tool returns data that should replace a state key, `result_key` is a convenient shorthand for `on_success=SetState(key, RESULT)`:
```python
CallTool("list_contacts", result_key="contacts")
# equivalent to:
CallTool(
"list_contacts",
on_success=SetState("contacts", RESULT),
)
```
## Actions
`CallTool` is one of several actions available in Prefab. Actions are events attached to component handlers like `on_click`, `on_submit`, and `on_change`.
### Client Actions
These run instantly in the browser — no server round-trip:
```python
from prefab_ui.actions import SetState, ToggleState, AppendState, PopState, ShowToast
# Set a value
SetState("count", 42)
# Toggle a boolean
ToggleState("expanded")
# Append to a list
AppendState("items", {"name": "New Item"})
# Remove by index
PopState("items", 0)
# Show a notification
ShowToast("Done!", variant="success")
```
### Chaining Actions
Pass a list to execute multiple actions in sequence:
```python
from prefab_ui.components import Button
from prefab_ui.actions import SetState, ShowToast
Button(
"Reset",
on_click=[
SetState("query", ""),
SetState("results", []),
ShowToast("Cleared", variant="default"),
],
)
```
### Loading States
A common pattern: show a loading indicator while a server call is in flight.
```python
from prefab_ui.app import set_initial_state
from prefab_ui.actions import SetState, ShowToast
from prefab_ui.actions.mcp import CallTool
from prefab_ui.components import Button
from prefab_ui.rx import RESULT
state = set_initial_state(saving=False)
Button(
state.saving.then("Saving...", "Save"),
disabled=state.saving,
on_click=[
SetState("saving", True),
CallTool(
"save_data",
on_success=[
SetState("saving", False),
SetState("result", RESULT),
ShowToast("Saved!", variant="success"),
],
on_error=[
SetState("saving", False),
ShowToast("Failed", variant="error"),
],
),
],
)
```
## Forms
Forms are the most common way to collect input and send it to the server. When a form submits, all named input values are gathered and passed as arguments to the `CallTool` action.
### Manual Forms
Build forms with individual input components:
```python
from prefab_ui.components import Form, Input, Select, Textarea, Button
from prefab_ui.actions.mcp import CallTool
from prefab_ui.actions import ShowToast
with Form(
on_submit=CallTool(
"create_ticket",
on_success=ShowToast("Ticket created!", variant="success"),
)
):
Input(name="title", label="Title", required=True)
Select(
name="priority",
label="Priority",
options=["low", "medium", "high", "critical"],
)
Textarea(name="description", label="Description")
Button("Create Ticket")
```
When submitted, the CallTool receives `{"title": "...", "priority": "...", "description": "..."}` as arguments to `create_ticket`.
### Pydantic Model Forms
For structured data, `Form.from_model()` generates the entire form from a Pydantic model — inputs, labels, and submit wiring:
```python
from typing import Literal
from pydantic import BaseModel, Field
from prefab_ui.components import Column, Heading, Form
from prefab_ui.actions.mcp import CallTool
from prefab_ui.actions import SetState, ShowToast
from prefab_ui.app import PrefabApp
from prefab_ui.rx import RESULT
class BugReport(BaseModel):
title: str = Field(title="Bug Title")
severity: Literal["low", "medium", "high", "critical"] = Field(
title="Severity", default="medium"
)
description: str = Field(title="Description")
@app.ui()
def report_bug() -> PrefabApp:
"""File a bug report."""
with Column(gap=4, css_class="p-6") as view:
Heading("Report a Bug")
Form.from_model(
BugReport,
on_submit=CallTool(
"create_bug",
on_success=ShowToast("Bug filed!", variant="success"),
on_error=ShowToast("Failed to submit", variant="error"),
),
)
return PrefabApp(view=view)
@app.tool()
def create_bug(data: BugReport) -> str:
"""Create a bug report."""
# save to database...
return f"Created: {data.title}"
```
`str` fields become text inputs, `Literal` becomes a select dropdown, `bool` becomes a checkbox. Field titles and defaults are respected.
## Composition and Namespacing
The reason `FastMCPApp` exists — and why you'd use it instead of plain `@mcp.tool(app=True)` with `CallTool("tool_name")` — is composition safety.
When you mount a server under a namespace, tool names get prefixed:
```python
from fastmcp import FastMCP
platform = FastMCP("Platform")
platform.mount("contacts", contacts_server)
# "save_contact" becomes "contacts_save_contact"
```
If your UI used `CallTool("save_contact")`, it would break — the tool is now named `contacts_save_contact`. But `CallTool(save_contact)` with a function reference resolves to a globally stable key (like `save_contact-a1b2c3d4`) that bypasses the namespace entirely.
This is why `FastMCPApp` assigns global keys to backend tools, and why `CallTool` accepts function references. Your app works the same whether it's running standalone or mounted inside a larger platform.
### Mounting an App
`FastMCPApp` is a Provider. Add it to a server with `providers=` or `add_provider`:
```python
from fastmcp import FastMCP, FastMCPApp
app = FastMCPApp("Contacts")
@app.ui()
def contact_manager() -> PrefabApp:
...
@app.tool()
def save_contact(name: str, email: str) -> dict:
...
# Option 1: providers list
mcp = FastMCP("Platform", providers=[app])
# Option 2: add_provider
mcp = FastMCP("Platform")
mcp.add_provider(app)
```
Multiple apps can coexist on the same server:
```python
mcp = FastMCP("Platform", providers=[contacts_app, inventory_app, billing_app])
```
Each app's backend tools have their own global keys, so there's no collision even if two apps have a tool named `save`.
### Running Standalone
For development, `FastMCPApp` has a convenience `run()` method that wraps itself in a temporary `FastMCP` server:
```python
app = FastMCPApp("Contacts")
# ... register tools ...
if __name__ == "__main__":
app.run()
```
## Complete Example: Contact Manager
This pulls together everything — entry points, backend tools, callable references, forms (both manual and Pydantic), state management, and actions:
```python expandable
from __future__ import annotations
from typing import Literal
from prefab_ui.actions import SetState, ShowToast
from prefab_ui.actions.mcp import CallTool
from prefab_ui.app import PrefabApp
from prefab_ui.components import (
Badge, Button, Column, ForEach, Form,
Heading, Input, Muted, Row, Separator, Text,
)
from prefab_ui.rx import RESULT
from pydantic import BaseModel, Field
from fastmcp import FastMCP, FastMCPApp
# Data
contacts_db: list[dict] = [
{"name": "Arthur Dent", "email": "arthur@earth.com", "category": "Customer"},
{"name": "Ford Prefect", "email": "ford@betelgeuse.org", "category": "Partner"},
]
class ContactModel(BaseModel):
name: str = Field(title="Full Name", min_length=1)
email: str = Field(title="Email")
category: Literal["Customer", "Vendor", "Partner", "Other"] = "Other"
# App
app = FastMCPApp("Contacts")
@app.tool()
def save_contact(data: ContactModel) -> list[dict]:
"""Save a new contact and return the updated list."""
contacts_db.append(data.model_dump())
return list(contacts_db)
@app.tool()
def search_contacts(query: str) -> list[dict]:
"""Filter contacts by name or email."""
q = query.lower()
return [
c for c in contacts_db
if q in c["name"].lower() or q in c["email"].lower()
]
@app.tool(model=True)
def list_contacts() -> list[dict]:
"""Return all contacts. Visible to both the model and the UI."""
return list(contacts_db)
@app.ui()
def contact_manager() -> PrefabApp:
"""Open the contact manager."""
with Column(gap=6, css_class="p-6") as view:
Heading("Contacts")
with ForEach("contacts") as contact:
with Row(gap=2, align="center"):
Text(contact.name, css_class="font-medium")
Muted(contact.email)
Badge(contact.category)
Separator()
Heading("Add Contact", level=3)
Form.from_model(
ContactModel,
on_submit=CallTool(
"save_contact",
on_success=[
SetState("contacts", RESULT),
ShowToast("Contact saved!", variant="success"),
],
on_error=ShowToast("Failed to save", variant="error"),
),
)
Separator()
Heading("Search", level=3)
with Form(
on_submit=CallTool(
"search_contacts",
arguments={"query": "{{ query }}"},
on_success=SetState("contacts", RESULT),
)
):
Input(name="query", placeholder="Search by name or email...")
Button("Search")
return PrefabApp(view=view, state={"contacts": list(contacts_db)})
mcp = FastMCP("Contacts Server", providers=[app])
if __name__ == "__main__":
mcp.run()
```
This example is also available as a runnable server at `examples/apps/contacts/contacts_server.py`.
## Next Steps
- **[Prefab Apps](/apps/prefab)** — Components, state, and reactive displays (the building blocks)
- **[Patterns](/apps/patterns)** — Copy-paste examples for common UIs
- **[Development](/apps/development)** — Preview and test app tools locally
- **[Prefab UI Docs](https://prefab.prefect.io)** — Full component reference and advanced patterns

View file

@ -10,67 +10,146 @@ import { VersionBadge } from '/snippets/version-badge.mdx'
<VersionBadge version="3.0.0" />
MCP Apps let your tools return interactive UIs — rendered in a sandboxed iframe right inside the host client's conversation. Instead of returning plain text, a tool can show a chart, a sortable table, a form, or anything you can build with HTML.
MCP Apps let tools return interactive UIs — charts, sortable tables, forms, dashboards — rendered in a sandboxed iframe inside the host client's conversation.
FastMCP implements the [MCP Apps extension](https://modelcontextprotocol.io/docs/extensions/apps) and provides two approaches:
FastMCP implements the [MCP Apps extension](https://modelcontextprotocol.io/docs/extensions/apps) and gives you two ways to build interactive UIs with [Prefab](https://prefab.prefect.io), depending on how much server-side interaction you need.
## Prefab Apps (Recommended)
<Note>
The examples throughout the Apps docs require the `apps` extra:
```bash
pip install "fastmcp[apps]"
```
This installs [Prefab UI](https://prefab.prefect.io), the component library used to build app UIs. Pin `prefab-ui` to a specific version in production — it's in early development and its API changes frequently.
</Note>
## Prefab Apps
<VersionBadge version="3.1.0" />
<Tip>
[Prefab](https://prefab.prefect.io) is in extremely early, active development — its API changes frequently and breaking changes can occur with any release. The FastMCP integration is equally new and under rapid development. These docs are included for users who want to work on the cutting edge; production use is not recommended. Always [pin `prefab-ui` to a specific version](/apps/prefab#getting-started) in your dependencies.
</Tip>
[Prefab UI](https://prefab.prefect.io) is a declarative UI framework for Python. You describe layouts, charts, tables, forms, and interactive behaviors using a Python DSL — and the framework compiles them to a JSON protocol that a shared renderer interprets. It started as a component library inside FastMCP and grew into its own framework with [comprehensive documentation](https://prefab.prefect.io).
The quickest way to give a tool a visual UI. You return a [Prefab](https://prefab.prefect.io) component or `PrefabApp` from an otherwise standard MCP tool, and when the host calls it, the app is rendered instead of plain text:
```python
from prefab_ui.components import Column, Heading, BarChart, ChartSeries
from prefab_ui.app import PrefabApp
from prefab_ui.components import Column, Heading, BarChart, ChartSeries
from fastmcp import FastMCP
mcp = FastMCP("Dashboard")
@mcp.tool(app=True)
def sales_chart(year: int) -> PrefabApp:
"""Show sales data as an interactive chart."""
data = get_sales_data(year)
def revenue_chart(year: int) -> PrefabApp:
"""Show annual revenue as an interactive bar chart."""
data = [
{"quarter": "Q1", "revenue": 42000},
{"quarter": "Q2", "revenue": 51000},
{"quarter": "Q3", "revenue": 47000},
{"quarter": "Q4", "revenue": 63000},
]
with Column(gap=4, css_class="p-6") as view:
Heading(f"{year} Sales")
Heading(f"{year} Revenue")
BarChart(
data=data,
series=[ChartSeries(data_key="revenue", label="Revenue")],
x_axis="month",
x_axis="quarter",
)
return PrefabApp(view=view)
```
Install with `pip install "fastmcp[apps]"` and see [Prefab Apps](/apps/prefab) for the integration guide.
Prefab apps aren't limited to static displays. Prefab's state system and client-side actions (toggles, tabs, conditionals) all work. You can even call other tools from the UI using `CallTool` with string names. There's no hard wall on what a Prefab app can do.
See [Prefab Apps](/apps/prefab) for the full guide.
## FastMCPApp
<VersionBadge version="3.2.0" />
When your app has a lot of server-side interaction — forms that save data, search that queries a database, multi-step workflows — managing the connection between UI and backend tools gets complicated fast. Which tools should the model see vs. only the UI? What happens to tool references when servers are composed under namespaces? How do you keep `CallTool("save_contact")` working when the tool name changes?
`FastMCPApp` is a class that solves these problems. It gives you two decorators that work together:
- **`@app.ui()`** — entry-point tools the model calls to open the app
- **`@app.tool()`** — backend tools the UI calls via `CallTool`
Backend tools get stable global identifiers that survive namespacing, visibility is managed automatically (the model sees entry points, the UI sees backends), and `CallTool` accepts function references instead of string names:
```python
from prefab_ui.actions import SetState, ShowToast
from prefab_ui.actions.mcp import CallTool
from prefab_ui.app import PrefabApp
from prefab_ui.components import (
Column, Heading, Form, Input, Button, ForEach, Row, Text, Badge, Separator,
)
from prefab_ui.rx import RESULT
from fastmcp import FastMCP, FastMCPApp
app = FastMCPApp("Contacts")
@app.tool()
def save_contact(name: str, email: str) -> list[dict]:
"""Save a contact and return the updated list."""
db.append({"name": name, "email": email})
return list(db)
@app.ui()
def contact_manager() -> PrefabApp:
"""Open the contact manager."""
with Column(gap=6, css_class="p-6") as view:
Heading("Contacts")
with ForEach("contacts") as contact:
with Row(gap=2):
Text(contact.name)
Badge(contact.email)
Separator()
with Form(
on_submit=CallTool(
"save_contact",
on_success=[
SetState("contacts", RESULT),
ShowToast("Saved!", variant="success"),
],
)
):
Input(name="name", label="Name", required=True)
Input(name="email", label="Email", required=True)
Button("Save")
return PrefabApp(view=view, state={"contacts": list(db)})
mcp = FastMCP("Server", providers=[app])
```
You *can* build server-interactive UIs without `FastMCPApp` — it's all the same protocol underneath. But once you have multiple tools, composition concerns, or visibility requirements, `FastMCPApp` handles the complexity so you don't have to.
See [FastMCPApp](/apps/interactive-apps) for the full guide.
## Which Approach?
| Scenario | Approach |
| -------- | -------- |
| Visual output — charts, tables, status dashboards | [Prefab app](/apps/prefab) — `@mcp.tool(app=True)` |
| Client-side interactivity — toggles, tabs, conditionals | [Prefab app](/apps/prefab) — state + `Rx()` |
| Light server interaction — one or two tool calls | [Prefab app](/apps/prefab) — `CallTool("tool_name")` |
| Heavy server interaction — forms, CRUD, search, multi-step | [FastMCPApp](/apps/interactive-apps) — managed tool binding |
| Composed servers — apps mounted under namespaces | [FastMCPApp](/apps/interactive-apps) — stable global keys |
| Custom rendering — maps, 3D, specific JS frameworks | [Custom HTML](/apps/low-level) — raw MCP Apps extension |
The boundary isn't sharp. Start with a Prefab app; graduate to `FastMCPApp` when the tool-management complexity justifies it.
## Custom HTML Apps
The [MCP Apps extension](https://modelcontextprotocol.io/docs/extensions/apps) is an open protocol, and you can use it directly when you need full control. You write your own HTML/CSS/JavaScript and communicate with the host via the [`@modelcontextprotocol/ext-apps`](https://github.com/modelcontextprotocol/ext-apps) SDK.
Both approaches above use [Prefab UI](https://prefab.prefect.io) to build UIs in pure Python. If you need full control — your own HTML, CSS, JavaScript, a specific framework — you can use the [MCP Apps extension directly](/apps/low-level). You write the HTML yourself and communicate with the host via the [`@modelcontextprotocol/ext-apps`](https://github.com/modelcontextprotocol/ext-apps) SDK.
This is the right choice for custom rendering (maps, 3D, video), specific JavaScript frameworks, or capabilities beyond what the component library offers.
## Previewing Apps Locally
```python
from fastmcp import FastMCP
from fastmcp.server.apps import AppConfig, ResourceCSP
The `fastmcp dev apps` command launches a browser-based preview for your app tools — no MCP host client needed. See [Development](/apps/development).
mcp = FastMCP("Custom App")
@mcp.tool(app=AppConfig(resource_uri="ui://my-app/view.html"))
def my_tool() -> str:
return '{"values": [1, 2, 3]}'
@mcp.resource(
"ui://my-app/view.html",
app=AppConfig(csp=ResourceCSP(resource_domains=["https://unpkg.com"])),
)
def view() -> str:
return "<html>...</html>"
```bash
fastmcp dev apps server.py
```
See [Custom HTML Apps](/apps/low-level) for the full reference.

View file

@ -1,30 +1,28 @@
---
title: Patterns
sidebarTitle: Patterns
description: Charts, tables, forms, and other common tool UIs.
description: Copy-paste examples for common tool UIs.
icon: grid-2-plus
tag: SOON
tag: NEW
---
import { VersionBadge } from '/snippets/version-badge.mdx'
<VersionBadge version="3.1.0" />
<Tip>
[Prefab](https://prefab.prefect.io) is in extremely early, active development — its API changes frequently and breaking changes can occur with any release. The FastMCP integration is equally new and under rapid development. These docs are included for users who want to work on the cutting edge; production use is not recommended. Always pin `prefab-ui` to a specific version in your dependencies.
</Tip>
Each pattern below is a complete, copy-pasteable tool. They're organized by what you're building — pick the one closest to your use case, paste it, and adapt.
The most common use of Prefab is giving your tools a visual representation — a chart instead of raw numbers, a sortable table instead of a text dump, a status dashboard instead of a list of booleans. Each pattern below is a complete, copy-pasteable tool.
For the full set of available components — layout containers, form controls, overlays, and more — see the [Prefab component reference](https://prefab.prefect.io/docs/components).
## Charts
Prefab includes [bar, line, area, pie, radar, and radial charts](https://prefab.prefect.io/docs/components/charts). They all render client-side with tooltips, legends, and responsive sizing.
Prefab includes [bar, line, area, pie, radar, and radial charts](https://prefab.prefect.io/docs/components/charts). They render client-side with tooltips, legends, and responsive sizing.
### Bar Chart
```python
from prefab_ui.components import Column, Heading, BarChart, ChartSeries
from prefab_ui.app import PrefabApp
from prefab_ui.components import Column, Heading, BarChart, ChartSeries
from fastmcp import FastMCP
mcp = FastMCP("Charts")
@ -59,11 +57,11 @@ Multiple `ChartSeries` entries plot different data keys. Add `stacked=True` to s
### Area Chart
`LineChart` and `AreaChart` share the same API as `BarChart`, with `curve` for interpolation (`"linear"`, `"smooth"`, `"step"`) and `show_dots` for data points:
`LineChart` and `AreaChart` share the same API as `BarChart`, with `curve` for interpolation and `show_dots` for data points:
```python
from prefab_ui.components import Column, Heading, AreaChart, ChartSeries
from prefab_ui.app import PrefabApp
from prefab_ui.components import Column, Heading, AreaChart, ChartSeries
from fastmcp import FastMCP
mcp = FastMCP("Charts")
@ -95,11 +93,11 @@ def usage_trend() -> PrefabApp:
### Pie and Donut Charts
`PieChart` uses `data_key` (the numeric value) and `name_key` (the label) instead of series. Set `inner_radius` for a donut:
`PieChart` uses `data_key` (the numeric value) and `name_key` (the label). Set `inner_radius` for a donut:
```python
from prefab_ui.components import Column, Heading, PieChart
from prefab_ui.app import PrefabApp
from prefab_ui.components import Column, Heading, PieChart
from fastmcp import FastMCP
mcp = FastMCP("Charts")
@ -130,11 +128,11 @@ def ticket_breakdown() -> PrefabApp:
## Data Tables
[DataTable](https://prefab.prefect.io/docs/components/data-display/data-table) provides sortable columns, full-text search, and pagination — all running client-side in the browser.
[DataTable](https://prefab.prefect.io/docs/components/data-display/data-table) provides sortable columns, full-text search, and pagination — all client-side:
```python
from prefab_ui.components import Column, Heading, DataTable, DataTableColumn
from prefab_ui.app import PrefabApp
from prefab_ui.components import Column, Heading, DataTable, DataTableColumn
from fastmcp import FastMCP
mcp = FastMCP("Directory")
@ -169,133 +167,16 @@ def employee_directory() -> PrefabApp:
return PrefabApp(view=view)
```
## Forms
A form collects input, but it needs somewhere to send that input. The [`CallTool`](https://prefab.prefect.io/docs/concepts/actions) action connects a form to a tool on your MCP server — so you need two tools: one that renders the form, and one that handles the submission.
```python
from prefab_ui.components import (
Column, Heading, Row, Muted, Badge, Input, Select,
Textarea, Button, Form, ForEach, Separator,
)
from prefab_ui.actions import ShowToast
from prefab_ui.actions.mcp import CallTool
from prefab_ui.app import PrefabApp
from fastmcp import FastMCP
mcp = FastMCP("Contacts")
contacts_db: list[dict] = [
{"name": "Zaphod Beeblebrox", "email": "zaphod@galaxy.gov", "category": "Partner"},
]
@mcp.tool(app=True)
def contact_form() -> PrefabApp:
"""Show a contact list with a form to add new contacts."""
with Column(gap=6, css_class="p-6") as view:
Heading("Contacts")
with ForEach("contacts"):
with Row(gap=2, align="center"):
Muted("{{ name }}")
Muted("{{ email }}")
Badge("{{ category }}")
Separator()
with Form(
on_submit=CallTool(
"save_contact",
result_key="contacts",
on_success=ShowToast("Contact saved!", variant="success"),
on_error=ShowToast("{{ $error }}", variant="error"),
)
):
Input(name="name", label="Full Name", required=True)
Input(name="email", label="Email", input_type="email", required=True)
Select(
name="category",
label="Category",
options=["Customer", "Vendor", "Partner", "Other"],
)
Textarea(name="notes", label="Notes", placeholder="Optional notes...")
Button("Save Contact")
return PrefabApp(view=view, state={"contacts": list(contacts_db)})
@mcp.tool
def save_contact(
name: str,
email: str,
category: str = "Other",
notes: str = "",
) -> list[dict]:
"""Save a new contact and return the updated list."""
contacts_db.append({"name": name, "email": email, "category": category, "notes": notes})
return list(contacts_db)
```
When the user submits the form, the renderer calls `save_contact` on the server with all named input values as arguments. Because `result_key="contacts"` is set, the returned list replaces the `contacts` state — and the `ForEach` re-renders with the new data automatically.
The `save_contact` tool is a regular MCP tool. The LLM can also call it directly in conversation. Your UI actions and your conversational tools are the same thing.
### Pydantic Model Forms
For complex forms, `Form.from_model()` generates the entire form from a Pydantic model — inputs, labels, validation, and submit wiring:
```python
from typing import Literal
from pydantic import BaseModel, Field
from prefab_ui.components import Column, Heading, Form
from prefab_ui.actions.mcp import CallTool
from prefab_ui.app import PrefabApp
from fastmcp import FastMCP
mcp = FastMCP("Bug Tracker")
class BugReport(BaseModel):
title: str = Field(title="Bug Title")
severity: Literal["low", "medium", "high", "critical"] = Field(
title="Severity", default="medium"
)
description: str = Field(title="Description")
steps_to_reproduce: str = Field(title="Steps to Reproduce")
@mcp.tool(app=True)
def report_bug() -> PrefabApp:
"""Show a bug report form."""
with Column(gap=4, css_class="p-6") as view:
Heading("Report a Bug")
Form.from_model(BugReport, on_submit=CallTool("create_bug_report"))
return PrefabApp(view=view)
@mcp.tool
def create_bug_report(data: dict) -> str:
"""Create a bug report from the form submission."""
report = BugReport(**data)
# save to database...
return f"Created bug report: {report.title}"
```
`str` fields become text inputs, `Literal` becomes a select, `bool` becomes a checkbox. The `on_submit` CallTool receives all field values under a `data` key.
## Status Displays
Cards, badges, progress bars, and grids combine naturally for dashboards. See the [Prefab layout](https://prefab.prefect.io/docs/concepts/composition) and [container](https://prefab.prefect.io/docs/components/containers) docs for the full set of layout and display components.
Cards, badges, progress bars, and grids combine naturally for dashboards:
```python
from prefab_ui.app import PrefabApp
from prefab_ui.components import (
Column, Row, Grid, Heading, Text, Muted, Badge,
Card, CardContent, Progress, Separator,
)
from prefab_ui.app import PrefabApp
from fastmcp import FastMCP
mcp = FastMCP("Monitoring")
@ -319,9 +200,7 @@ def system_status() -> PrefabApp:
"All Healthy" if all_ok else "Degraded",
variant="success" if all_ok else "destructive",
)
Separator()
with Grid(columns=2, gap=4):
for svc in services:
with Card():
@ -338,13 +217,15 @@ def system_status() -> PrefabApp:
return PrefabApp(view=view)
```
## Conditional Content
## Reactive Displays
[`If`, `Elif`, and `Else`](https://prefab.prefect.io/docs/concepts/composition#conditional-rendering) show or hide content based on state. Changes are instant — no server round-trip.
These patterns use state and `Rx()` for client-side interactivity — no server calls needed.
### Feature Toggles
```python
from prefab_ui.components import Column, Heading, Switch, Separator, Alert, If
from prefab_ui.app import PrefabApp
from prefab_ui.app import PrefabApp, set_initial_state
from prefab_ui.components import Column, Heading, Switch, Alert, If, Separator
from fastmcp import FastMCP
mcp = FastMCP("Flags")
@ -353,49 +234,45 @@ mcp = FastMCP("Flags")
@mcp.tool(app=True)
def feature_flags() -> PrefabApp:
"""Toggle feature flags with live preview."""
state = set_initial_state(dark_mode=False, beta=False)
with Column(gap=4, css_class="p-6") as view:
Heading("Feature Flags")
Switch(name="dark_mode", label="Dark Mode")
Switch(name="beta_features", label="Beta Features")
Switch(name="beta", label="Beta Features")
Separator()
with If("{{ dark_mode }}"):
with If(state.dark_mode):
Alert(title="Dark mode enabled", description="UI will use dark theme.")
with If("{{ beta_features }}"):
with If(state.beta):
Alert(
title="Beta features active",
description="Experimental features are now visible.",
variant="warning",
)
return PrefabApp(view=view, state={"dark_mode": False, "beta_features": False})
return PrefabApp(view=view)
```
## Tabs
[Tabs](https://prefab.prefect.io/docs/components/containers/tabs) organize content into switchable views. Switching is client-side — no server round-trip.
### Tabs
```python
from prefab_ui.app import PrefabApp
from prefab_ui.components import (
Column, Heading, Text, Muted, Badge, Row,
DataTable, DataTableColumn, Tabs, Tab, ForEach,
)
from prefab_ui.app import PrefabApp
from fastmcp import FastMCP
mcp = FastMCP("Projects")
@mcp.tool(app=True)
def project_overview(project_id: str) -> PrefabApp:
def project_overview() -> PrefabApp:
"""Show project details organized in tabs."""
project = {
"name": "FastMCP v3",
"description": "Next generation MCP framework with Apps support.",
"status": "Active",
"created_at": "2025-01-15",
"members": [
{"name": "Alice Chen", "role": "Lead"},
{"name": "Bob Martinez", "role": "Design"},
@ -408,13 +285,11 @@ def project_overview(project_id: str) -> PrefabApp:
with Column(gap=4, css_class="p-6") as view:
Heading(project["name"])
with Tabs():
with Tab("Overview"):
Text(project["description"])
with Row(gap=4):
Badge(project["status"])
Muted(f"Created: {project['created_at']}")
with Tab("Members"):
DataTable(
@ -426,24 +301,22 @@ def project_overview(project_id: str) -> PrefabApp:
)
with Tab("Activity"):
with ForEach("activity"):
with ForEach("activity") as item:
with Row(gap=2):
Muted("{{ timestamp }}")
Text("{{ message }}")
Muted(item.timestamp)
Text(item.message)
return PrefabApp(view=view, state={"activity": project["activity"]})
```
## Accordion
[Accordion](https://prefab.prefect.io/docs/components/containers/accordion) collapses sections to save space. `multiple=True` lets users expand several items at once:
### Accordion
```python
from prefab_ui.app import PrefabApp
from prefab_ui.components import (
Column, Heading, Row, Text, Badge, Progress,
Accordion, AccordionItem,
)
from prefab_ui.app import PrefabApp
from fastmcp import FastMCP
mcp = FastMCP("API Monitor")
@ -461,7 +334,6 @@ def api_health() -> PrefabApp:
with Column(gap=4, css_class="p-6") as view:
Heading("API Health")
with Accordion(multiple=True):
for ep in endpoints:
with AccordionItem(ep["path"]):
@ -477,7 +349,81 @@ def api_health() -> PrefabApp:
return PrefabApp(view=view)
```
## Interactive Patterns
These patterns call server tools. For context on `FastMCPApp`, `@app.tool()`, and `CallTool`, see [FastMCPApp](/apps/interactive-apps).
### Contact Form
```python
from prefab_ui.actions import SetState, ShowToast
from prefab_ui.actions.mcp import CallTool
from prefab_ui.app import PrefabApp
from prefab_ui.components import (
Badge, Button, Column, ForEach, Form, Heading,
Input, Muted, Row, Select, Separator, Text, Textarea,
)
from prefab_ui.rx import RESULT
from fastmcp import FastMCP, FastMCPApp
app = FastMCPApp("Contacts")
contacts_db: list[dict] = [
{"name": "Zaphod Beeblebrox", "email": "zaphod@galaxy.gov", "category": "Partner"},
]
@app.tool()
def save_contact(
name: str, email: str, category: str = "Other", notes: str = "",
) -> list[dict]:
"""Save a new contact and return the updated list."""
contacts_db.append({"name": name, "email": email, "category": category})
return list(contacts_db)
@app.ui()
def contact_form() -> PrefabApp:
"""Contact list with an add form."""
with Column(gap=6, css_class="p-6") as view:
Heading("Contacts")
with ForEach("contacts") as contact:
with Row(gap=2, align="center"):
Text(contact.name, css_class="font-medium")
Muted(contact.email)
Badge(contact.category)
Separator()
with Form(
on_submit=CallTool(
"save_contact",
on_success=[
SetState("contacts", RESULT),
ShowToast("Contact saved!", variant="success"),
],
on_error=ShowToast("Failed to save", variant="error"),
)
):
Input(name="name", label="Full Name", required=True)
Input(name="email", label="Email", input_type="email", required=True)
Select(
name="category",
label="Category",
options=["Customer", "Vendor", "Partner", "Other"],
)
Textarea(name="notes", label="Notes", placeholder="Optional notes...")
Button("Save Contact")
return PrefabApp(view=view, state={"contacts": list(contacts_db)})
mcp = FastMCP("Server", providers=[app])
```
## Next Steps
- **[Custom HTML Apps](/apps/low-level)** — When you need your own HTML, CSS, and JavaScript
- **[Prefab UI Docs](https://prefab.prefect.io)** — Components, state, expressions, and actions
- **[FastMCPApp](/apps/interactive-apps)** — Managed tool binding for server-connected UIs
- **[Development](/apps/development)** — Preview app tools locally with `fastmcp dev apps`
- **[Prefab UI Docs](https://prefab.prefect.io)** — Full component reference, layout guides, and more

View file

@ -1,9 +1,9 @@
---
title: Prefab Apps
sidebarTitle: Prefab Apps
description: Build interactive tool UIs in pure Python — no HTML or JavaScript required.
description: Build interactive tool UIs in pure Python — charts, tables, dashboards, forms, and reactive displays.
icon: palette
tag: SOON
tag: NEW
---
import { VersionBadge } from '/snippets/version-badge.mdx'
@ -11,34 +11,20 @@ import { VersionBadge } from '/snippets/version-badge.mdx'
<VersionBadge version="3.1.0" />
<Tip>
[Prefab](https://prefab.prefect.io) is in extremely early, active development — its API changes frequently and breaking changes can occur with any release. The FastMCP integration is equally new and under rapid development. These docs are included for users who want to work on the cutting edge; production use is not recommended. Always pin `prefab-ui` to a specific version in your dependencies (see below).
[Prefab](https://prefab.prefect.io) is in early, active development — its API changes frequently and breaking changes can occur with any release. Always pin `prefab-ui` to a specific version in your dependencies (see below).
</Tip>
[Prefab UI](https://prefab.prefect.io) is a declarative UI framework for Python. You describe what your interface should look like — a chart, a table, a form — and return it from your tool. FastMCP takes care of everything else: registering the renderer, wiring the protocol metadata, and delivering the component tree to the host.
The fastest way to give a tool a visual UI: return a [Prefab](https://prefab.prefect.io) component or `PrefabApp` from an otherwise standard MCP tool. FastMCP registers the rendering engine, wires the protocol metadata, and delivers the component tree to the host. You write Python; the user sees an interactive UI.
Prefab started as a component library inside FastMCP and grew into a full framework for building interactive applications — with its own state management, reactive expression system, and action model. The [Prefab documentation](https://prefab.prefect.io) covers all of this in depth. This page focuses on the FastMCP integration: what you return from a tool, and what FastMCP does with it.
This works for everything from static charts to reactive dashboards with client-side state. When your app has heavy server-side interaction — multiple backend tools, forms, search, CRUD — consider [FastMCPApp](/apps/interactive-apps), which manages tool binding, visibility, and composition safety for you.
```bash
pip install "fastmcp[apps]"
```
## Getting Started
<Tip>
Prefab UI is in active early development and its API changes frequently. We strongly recommend pinning `prefab-ui` to a specific version in your project's dependencies. Installing `fastmcp[apps]` pulls in `prefab-ui` but won't pin it — so a routine `pip install --upgrade` could introduce breaking changes.
```toml
# pyproject.toml
dependencies = [
"fastmcp[apps]",
"prefab-ui==0.8.0", # pin to a known working version
]
```
</Tip>
Here's the simplest possible Prefab App — a tool that returns a bar chart:
Here's a tool that returns a bar chart:
```python
from prefab_ui.components import Column, Heading, BarChart, ChartSeries
from prefab_ui.app import PrefabApp
from prefab_ui.components import Column, Heading, BarChart, ChartSeries
from fastmcp import FastMCP
mcp = FastMCP("Dashboard")
@ -65,67 +51,265 @@ def revenue_chart(year: int) -> PrefabApp:
return PrefabApp(view=view)
```
That's it — you declare a layout using Python's `with` statement, and return it. When the host calls this tool, the user sees an interactive bar chart instead of a JSON blob. The [Patterns](/apps/patterns) page has more examples: area charts, data tables, forms, status dashboards, and more.
The `app=True` flag tells FastMCP this tool returns a UI. When a host calls the tool, the user sees an interactive chart instead of a JSON blob. The [Patterns](/apps/patterns) page has more examples.
## Layout and Components
Prefab uses Python's `with` statement to express nesting. Containers like `Column`, `Row`, and `Grid` collect their children automatically:
```python
from prefab_ui.components import (
Column, Row, Grid, Heading, Text, Muted, Badge,
Card, CardContent, Separator,
)
with Column(gap=4, css_class="p-6") as view:
Heading("Team Status")
Separator()
with Grid(columns=2, gap=4):
with Card():
with CardContent():
Text("API Gateway", css_class="font-medium")
Badge("healthy", variant="success")
with Card():
with CardContent():
Text("Cache", css_class="font-medium")
Badge("degraded", variant="destructive")
```
You can also use Python loops to generate components at build time:
```python
services = [
{"name": "API", "status": "healthy", "ok": True},
{"name": "Cache", "status": "degraded", "ok": False},
]
with Grid(columns=2, gap=4):
for svc in services:
with Card():
with CardContent():
Text(svc["name"])
Badge(
svc["status"],
variant="success" if svc["ok"] else "destructive",
)
```
Build-time loops produce static content — the data is baked into the component tree at construction time. For dynamic iteration over state that changes at render time, use `ForEach` (covered below).
The full component library — layout containers, data display, charts, forms, overlays — is documented in the [Prefab component reference](https://prefab.prefect.io/docs/components).
## State and Reactivity
Display tools can be interactive without calling the server. The key is **state** — a client-side key-value store that lives in the browser. Components read from state, actions mutate it, and the UI re-renders automatically.
### Declaring State
Use `set_initial_state()` to declare state and get a typed proxy for referencing it:
```python
from prefab_ui.app import PrefabApp, set_initial_state
from prefab_ui.components import Column, Heading, Switch, Alert, If
from fastmcp import FastMCP
mcp = FastMCP("Flags")
@mcp.tool(app=True)
def feature_flags() -> PrefabApp:
"""Toggle feature flags with live preview."""
state = set_initial_state(dark_mode=False, beta=False)
with Column(gap=4, css_class="p-6") as view:
Heading("Feature Flags")
Switch(name="dark_mode", label="Dark Mode")
Switch(name="beta", label="Beta Features")
with If(state.dark_mode):
Alert(title="Dark mode enabled")
with If(state.beta):
Alert(title="Beta features active", variant="warning")
return PrefabApp(view=view)
```
Three things to notice here:
`set_initial_state()` declares the keys and their starting values, and returns a proxy object. Accessing `state.dark_mode` gives you a reactive reference (an `Rx` object) that compiles to `{{ dark_mode }}` in the wire protocol. A typo like `state.drk_mode` raises an `AttributeError` immediately.
Interactive components with a `name` prop automatically bind to state. The `Switch(name="dark_mode")` syncs its on/off value to `state["dark_mode"]` on every toggle — no event wiring needed.
`If(state.dark_mode)` shows its children only when the state key is truthy. When the switch flips, the condition re-evaluates instantly in the browser.
### Reactive References with Rx
The `Rx` class is how you reference state in component props. When you write `state.dark_mode`, you get an `Rx("dark_mode")` object. You can also create them directly:
```python
from prefab_ui.rx import Rx
count = Rx("count")
```
Rx objects support arithmetic, comparisons, and formatting — they compile to expressions the renderer evaluates at render time:
```python
from prefab_ui.app import PrefabApp, set_initial_state
from prefab_ui.components import Column, Text, Slider
from fastmcp import FastMCP
mcp = FastMCP("Calculator")
@mcp.tool(app=True)
def tip_calculator() -> PrefabApp:
"""Calculate tip with a slider."""
state = set_initial_state(bill=50.00, tip_pct=18)
tip_amount = state.tip_pct / 100 * state.bill
total = state.bill + tip_amount
with Column(gap=4, css_class="p-6") as view:
Slider(name="bill", label="Bill Amount", min=0, max=500, step=0.5)
Slider(name="tip_pct", label="Tip %", min=0, max=50)
Text(f"Tip: {tip_amount.currency()}")
Text(f"Total: {total.currency()}")
return PrefabApp(view=view)
```
`state.tip_pct / 100 * state.bill` builds a compound expression — it doesn't do the math in Python. The renderer evaluates it live as the sliders move. The `.currency()` pipe formats the result as currency.
#### Pipes
Rx objects support formatting pipes that transform values at render time:
```python
state = set_initial_state(price=42.50, ratio=0.85, name="alice")
state.price.currency() # $42.50
state.price.currency("EUR") # EUR format
state.ratio.percent() # 85%
state.name.upper() # ALICE
state.name.truncate(10) # alice (or truncated if longer)
```
Number pipes include `currency`, `percent`, `number`, `compact`, `round`, and `abs`. String pipes include `upper`, `lower`, and `truncate`. See the [Prefab expression docs](https://prefab.prefect.io/docs/concepts/expressions) for the full list.
#### Conditionals
The `.then()` method creates ternary expressions:
```python
state = set_initial_state(connected=True)
Badge(
state.connected.then("Online", "Offline"),
variant=state.connected.then("success", "destructive"),
)
```
### Dynamic Iteration with ForEach
Python `for` loops generate static content at build time. When you need to iterate over state that can change — a list that grows, items that get filtered — use `ForEach`:
```python
from prefab_ui.app import PrefabApp, set_initial_state
from prefab_ui.components import Column, Heading, ForEach, Row, Text, Badge
from fastmcp import FastMCP
mcp = FastMCP("Directory")
@mcp.tool(app=True)
def team_list() -> PrefabApp:
"""Show the current team."""
members = [
{"name": "Alice", "role": "Engineering"},
{"name": "Bob", "role": "Design"},
]
with Column(gap=4, css_class="p-6") as view:
Heading("Team")
with ForEach("members") as member:
with Row(gap=2, align="center"):
Text(member.name, css_class="font-medium")
Badge(member.role)
return PrefabApp(view=view, state={"members": members})
```
`ForEach("members")` iterates over the `members` state key. The `as member` gives you an Rx proxy scoped to each item, so `member.name` resolves to `{{ $item.name }}` in the wire protocol. If the `members` state changes (e.g., through an action), the list re-renders automatically.
### Conditional Rendering
`If`, `Elif`, and `Else` control what's visible based on state:
```python
from prefab_ui.app import PrefabApp, set_initial_state
from prefab_ui.components import Column, Select, If, Elif, Else, Text
state = set_initial_state(tier="free")
with Column(gap=4) as view:
Select(
name="tier",
label="Plan",
options=["free", "pro", "enterprise"],
)
with If(state.tier == "enterprise"):
Text("Full access to all features")
with Elif(state.tier == "pro"):
Text("Advanced features unlocked")
with Else():
Text("Basic features only")
```
Changes are instant — switching the dropdown re-evaluates the conditions in the browser.
## What You Return
### Components
The simplest way to get started. If you're returning a visual representation of data and don't need Prefab's more advanced features like initial state or stylesheets, just return the components directly. FastMCP wraps them in a `PrefabApp` automatically:
The simplest approach. Return a component directly and FastMCP wraps it in a `PrefabApp` automatically:
```python
from prefab_ui.components import Column, Heading, Badge
from fastmcp import FastMCP
mcp = FastMCP("Status")
@mcp.tool(app=True)
def status_badge() -> Column:
"""Show system status."""
def status() -> Column:
with Column(gap=2) as view:
Heading("All Systems Operational")
Badge("Healthy", variant="success")
return view
```
Want a chart? Return a chart. Want a table? Return a table. FastMCP handles the wiring.
### PrefabApp
When you need more control — setting initial state values that components can read and react to, or configuring the rendering engine — return a `PrefabApp` explicitly:
When you need initial state or other configuration, return a `PrefabApp` explicitly. If you've called `set_initial_state()` during the tool, PrefabApp picks up that state automatically:
```python
from prefab_ui.components import Column, Heading, Text, Button, If, Badge
from prefab_ui.actions import ToggleState
from prefab_ui.app import PrefabApp
from fastmcp import FastMCP
mcp = FastMCP("Demo")
@mcp.tool(app=True)
def toggle_demo() -> PrefabApp:
"""Interactive toggle with state."""
with Column(gap=4, css_class="p-6") as view:
Button("Toggle", on_click=ToggleState("show"))
with If("{{ show }}"):
Badge("Visible!", variant="success")
return PrefabApp(view=view, state={"show": False})
def dashboard() -> PrefabApp:
state = set_initial_state(tab="overview")
# ... build view ...
return PrefabApp(view=view)
```
The `state` dict provides the initial values. Components reference state with `{{ expression }}` templates. State mutations like `ToggleState` happen entirely in the browser — no server round-trip. The [Prefab state guide](https://prefab.prefect.io/docs/concepts/state) covers this in detail.
You can also pass state directly:
```python
return PrefabApp(view=view, state={"tab": "overview"})
```
### ToolResult
Every tool result has two audiences: the renderer (which displays the UI) and the LLM (which reads the text content to understand what happened). By default, Prefab Apps send `"[Rendered Prefab UI]"` as the text content, which tells the LLM almost nothing.
Every tool result has two audiences: the renderer (which displays the UI) and the LLM (which reads text content to understand what happened). By default, Prefab sends `"[Rendered Prefab UI]"` as the text — which tells the LLM nothing useful.
If you want the LLM to understand the result — so it can reference the data in conversation, summarize it, or decide what to do next — wrap your return in a `ToolResult` with a meaningful `content` string:
If the model needs to reason about the data — reference it in conversation, summarize it, decide what to do next — wrap your return in a `ToolResult`:
```python
from prefab_ui.components import Column, Heading, BarChart, ChartSeries
from prefab_ui.app import PrefabApp
from prefab_ui.components import Column, Heading, BarChart, ChartSeries
from fastmcp import FastMCP
from fastmcp.tools import ToolResult
@ -148,11 +332,11 @@ def sales_overview(year: int) -> ToolResult:
)
```
The user sees the chart. The LLM sees `"Total revenue for 2025: $203,000 across 4 quarters"` and can reason about it.
The user sees the chart. The LLM sees the summary string and can reason about it.
## Type Inference
If your tool's return type annotation is a Prefab type — `PrefabApp`, `Component`, or their `Optional` variants — FastMCP detects this and enables app rendering automatically:
If your tool's return type annotation is a Prefab type — `PrefabApp`, `Component`, or unions/optionals containing them — FastMCP detects this and enables app rendering automatically:
```python
@mcp.tool
@ -160,21 +344,23 @@ def greet(name: str) -> PrefabApp:
return PrefabApp(view=Heading(f"Hello, {name}!"))
```
This is equivalent to `@mcp.tool(app=True)`. Explicit `app=True` is recommended for clarity, and is required when the return type doesn't reveal a Prefab type (e.g., `-> ToolResult`).
This is equivalent to `@mcp.tool(app=True)`. Explicit `app=True` is recommended for clarity, and required when the return type doesn't reveal a Prefab type (e.g., `-> ToolResult`).
## How It Works
Behind the scenes, when a tool returns a Prefab component or `PrefabApp`, FastMCP:
When a tool returns a Prefab component or `PrefabApp`, three things happen behind the scenes:
1. **Registers a shared renderer** — a `ui://prefab/renderer.html` resource containing the JavaScript rendering engine, fetched once by the host and reused across all your Prefab tools.
2. **Wires the tool metadata** — so the host knows to load the renderer iframe when displaying the tool result.
3. **Serializes the component tree** — your Python components become `structuredContent` on the tool result, which the renderer interprets and displays.
A **shared renderer** is registered as a `ui://prefab/renderer.html` resource. This is a JavaScript rendering engine that interprets Prefab's JSON component protocol. The host fetches it once and reuses it for all Prefab tools on the server.
None of this requires any configuration. The `app=True` flag (or type inference) is the only thing you need.
The **tool metadata** is wired so the host knows to load the renderer iframe when displaying the result. This includes Content Security Policy headers that the renderer needs for its dependencies.
## Mixing with Custom HTML Apps
The **component tree** is serialized as `structuredContent` on the tool result. The renderer receives this JSON and renders the UI.
Prefab tools and [custom HTML tools](/apps/low-level) coexist in the same server. Prefab tools share a single renderer resource; custom tools point to their own. Both use the same MCP Apps protocol:
None of this requires configuration. The `app=True` flag (or type inference) is the only thing you need.
## Mixing with Custom HTML
Prefab tools and [custom HTML tools](/apps/low-level) coexist in the same server. Prefab tools share a single renderer resource; custom tools point to their own:
```python
from fastmcp.server.apps import AppConfig
@ -190,7 +376,7 @@ def map_view() -> str:
## Next Steps
- **[Patterns](/apps/patterns)** — Charts, tables, forms, and other common tool UIs
- **[Development](/apps/development)** — Preview and test app tools locally with `fastmcp dev apps`
- **[Custom HTML Apps](/apps/low-level)** — When you need your own HTML, CSS, and JavaScript
- **[Prefab UI Docs](https://prefab.prefect.io)** — Components, state, expressions, and actions
- **[FastMCPApp](/apps/interactive-apps)** — Managed tool binding for apps with heavy server interaction
- **[Patterns](/apps/patterns)** — Charts, tables, dashboards, and other common examples
- **[Development](/apps/development)** — Preview app tools locally with `fastmcp dev apps`
- **[Prefab UI Docs](https://prefab.prefect.io)** — Full component reference, advanced state patterns, and more

View file

@ -193,6 +193,7 @@
"pages": [
"apps/overview",
"apps/prefab",
"apps/interactive-apps",
"apps/patterns",
"apps/development",
"apps/low-level"

View file

@ -53,7 +53,7 @@ classifiers = [
[project.optional-dependencies]
anthropic = ["anthropic>=0.48.0"]
apps = ["prefab-ui>=0.11.2"]
apps = ["prefab-ui>=0.13.0"]
# PyJWT floor: transitive via msal; CVE-2026-32597 affects <= 2.11.0
azure = ["azure-identity>=1.16.0", "PyJWT>=2.12.0"]
code-mode = ["pydantic-monty==0.0.8"]

View file

@ -967,7 +967,7 @@ def _build_picker_html(tools: list[dict[str, Any]]) -> str:
else:
Heading(_tool_title(tools[0]), level=3)
with Pages(name="activeTool", default_value=first_name):
with Pages(name="activeTool", value=first_name):
for tool in tools:
name: str = tool["name"]
desc: str = tool.get("description") or ""

View file

@ -57,6 +57,10 @@ _APP_TOOL_REGISTRY: dict[str, Tool] = {}
# translate ``CallTool(save_contact)`` → ``"save_contact-a1b2c3d4"``.
_FN_TO_GLOBAL_KEY: dict[int, str] = {}
# tool name → global key. Used by the CallTool resolver to translate
# ``CallTool("save_contact")`` → ``"save_contact-a1b2c3d4"``.
_NAME_TO_GLOBAL_KEY: dict[str, str] = {}
def get_global_tool(name: str) -> Tool | None:
"""Look up a tool by its global key, or return None."""
@ -74,9 +78,10 @@ def _make_global_key(name: str) -> str:
def _register_global_key(tool: Tool, fn: Any, global_key: str) -> None:
"""Register a tool in both process-level registries."""
"""Register a tool in all process-level registries."""
_APP_TOOL_REGISTRY[global_key] = tool
_FN_TO_GLOBAL_KEY[id(fn)] = global_key
_NAME_TO_GLOBAL_KEY[tool.name] = global_key
def _stamp_global_key(tool: Tool, global_key: str) -> None:
@ -94,18 +99,34 @@ def _stamp_global_key(tool: Tool, global_key: str) -> None:
def _resolve_tool_ref(fn: Any) -> Any:
"""Resolve a callable to a ``ResolvedTool`` for CallTool serialization.
"""Resolve a callable or string to a ``ResolvedTool`` for CallTool serialization.
Always returns a ``ResolvedTool`` with the resolved name and any
metadata the renderer needs (e.g. ``unwrap_result``).
Resolution order:
Resolution order for callables:
1. Global key registry (FastMCPApp tools) includes metadata
2. ``__fastmcp__`` metadata (decorated but not on a FastMCPApp)
3. ``fn.__name__`` (bare function works for standalone servers)
Resolution for strings:
1. Name registry (FastMCPApp tools registered by name)
2. Pass through as-is (plain tool name)
"""
from prefab_ui.app import ResolvedTool
if isinstance(fn, str):
global_key = _NAME_TO_GLOBAL_KEY.get(fn)
if global_key is not None:
tool = _APP_TOOL_REGISTRY.get(global_key)
unwrap = bool(
tool is not None
and tool.output_schema
and tool.output_schema.get("x-fastmcp-wrap-result")
)
return ResolvedTool(name=global_key, unwrap_result=unwrap)
return ResolvedTool(name=fn)
global_key = _FN_TO_GLOBAL_KEY.get(id(fn))
if global_key is not None:
tool = _APP_TOOL_REGISTRY.get(global_key)
@ -416,6 +437,7 @@ class FastMCPApp(Provider):
self._local._add_component(tool)
_APP_TOOL_REGISTRY[global_key] = tool
_NAME_TO_GLOBAL_KEY[tool.name] = global_key
if fn is not None:
_FN_TO_GLOBAL_KEY[id(fn)] = global_key

View file

@ -21,6 +21,7 @@ from fastmcp import Client, FastMCP
from fastmcp.server.app import (
_APP_TOOL_REGISTRY,
_FN_TO_GLOBAL_KEY,
_NAME_TO_GLOBAL_KEY,
FastMCPApp,
_make_global_key,
_resolve_tool_ref,
@ -38,6 +39,7 @@ def _clear_registries() -> None:
"""Clear process-level registries between tests."""
_APP_TOOL_REGISTRY.clear()
_FN_TO_GLOBAL_KEY.clear()
_NAME_TO_GLOBAL_KEY.clear()
# ---------------------------------------------------------------------------
@ -384,6 +386,39 @@ class TestResolveToolRef:
assert isinstance(result, ResolvedTool)
assert result.name == "my_tool"
def test_resolve_string_name(self):
"""CallTool("save") resolves to the global key."""
app = FastMCPApp("test")
@app.tool()
def save(name: str) -> str:
return name
result = _resolve_tool_ref("save")
assert isinstance(result, ResolvedTool)
assert GLOBAL_KEY_PATTERN.match(result.name)
assert result.name.startswith("save-")
assert result.unwrap_result is True
def test_resolve_string_name_object_return(self):
"""String resolution also sets unwrap_result correctly."""
app = FastMCPApp("test")
@app.tool()
def save(name: str) -> dict:
return {"name": name}
result = _resolve_tool_ref("save")
assert isinstance(result, ResolvedTool)
assert result.name.startswith("save-")
assert result.unwrap_result is False
def test_resolve_string_unknown_passes_through(self):
"""Unknown string names pass through as-is."""
result = _resolve_tool_ref("unknown_tool")
assert isinstance(result, ResolvedTool)
assert result.name == "unknown_tool"
def test_resolve_unresolvable_raises(self):
with pytest.raises(ValueError):
_resolve_tool_ref(42)

8
uv.lock generated
View file

@ -862,7 +862,7 @@ requires-dist = [
{ name = "opentelemetry-api", specifier = ">=1.20.0" },
{ name = "packaging", specifier = ">=24.0" },
{ name = "platformdirs", specifier = ">=4.0.0" },
{ name = "prefab-ui", marker = "extra == 'apps'", specifier = ">=0.11.2" },
{ name = "prefab-ui", marker = "extra == 'apps'", specifier = ">=0.13.0" },
{ name = "py-key-value-aio", extras = ["filetree", "keyring", "memory"], specifier = ">=0.4.4,<0.5.0" },
{ name = "pydantic", extras = ["email"], specifier = ">=2.11.7" },
{ name = "pydantic-monty", marker = "extra == 'code-mode'", specifier = "==0.0.8" },
@ -1795,16 +1795,16 @@ wheels = [
[[package]]
name = "prefab-ui"
version = "0.11.2"
version = "0.13.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cyclopts" },
{ name = "pydantic" },
{ name = "rich" },
]
sdist = { url = "https://files.pythonhosted.org/packages/de/b4/2b90754a66f96711b8023158e47037a9fa99ecd98e0f7dbdff1cd67f7bcc/prefab_ui-0.11.2.tar.gz", hash = "sha256:3295d154b97ab570885ee5fb356252708a279d448adadc3a7be1f01317b94fdd", size = 2923681, upload-time = "2026-03-14T15:51:20.527Z" }
sdist = { url = "https://files.pythonhosted.org/packages/fe/7d/3d52298187d5771e56fd43757308cba91472403dac62a9c8e399f43b19e5/prefab_ui-0.13.0.tar.gz", hash = "sha256:9525f52c776a58a9b803638fbf83c136d573eb3885fb9027897698272b0e0a97", size = 3018292, upload-time = "2026-03-21T21:37:12.559Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/31/1c/c8587d93090c33d3ec3de10a3641aab100c23854d25cdba2e4be438f1a49/prefab_ui-0.11.2-py3-none-any.whl", hash = "sha256:70731df7c4bd02c9d09c23aca2c6577d88e68c9e59469ee570af0935bd8a2073", size = 872953, upload-time = "2026-03-14T15:51:19.006Z" },
{ url = "https://files.pythonhosted.org/packages/56/d5/ed87b6fd3e4bf873abc3a964779afe04a584adfc045f157448b24273250c/prefab_ui-0.13.0-py3-none-any.whl", hash = "sha256:81d71c6fc86de2c26a8088c7cfb4a352b7e43c2aad59b6919bcf926933d4678d", size = 888550, upload-time = "2026-03-21T21:37:11.201Z" },
]
[[package]]