diff --git a/docs/apps/development.mdx b/docs/apps/development.mdx
index fa8a8646a..7045cd939 100644
--- a/docs/apps/development.mdx
+++ b/docs/apps/development.mdx
@@ -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'
+
+
+
+
`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
diff --git a/docs/apps/images/dev-app.png b/docs/apps/images/dev-app.png
new file mode 100644
index 000000000..fdb05d69e
Binary files /dev/null and b/docs/apps/images/dev-app.png differ
diff --git a/docs/apps/interactive-apps.mdx b/docs/apps/interactive-apps.mdx
new file mode 100644
index 000000000..c8f9e2356
--- /dev/null
+++ b/docs/apps/interactive-apps.mdx
@@ -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'
+
+
+
+
+[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.
+
+
+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
diff --git a/docs/apps/overview.mdx b/docs/apps/overview.mdx
index d8dcfc3fd..760ca9c4f 100644
--- a/docs/apps/overview.mdx
+++ b/docs/apps/overview.mdx
@@ -10,67 +10,146 @@ import { VersionBadge } from '/snippets/version-badge.mdx'
-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)
+
+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.
+
+
+## Prefab Apps
-
-[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.
-
-
-[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
+
+
+
+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 "..."
+```bash
+fastmcp dev apps server.py
```
-
-See [Custom HTML Apps](/apps/low-level) for the full reference.
diff --git a/docs/apps/patterns.mdx b/docs/apps/patterns.mdx
index ffc699f89..cf48e0fb5 100644
--- a/docs/apps/patterns.mdx
+++ b/docs/apps/patterns.mdx
@@ -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'
-
-[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.
-
+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
diff --git a/docs/apps/prefab.mdx b/docs/apps/prefab.mdx
index 916f376ef..28dd95e9e 100644
--- a/docs/apps/prefab.mdx
+++ b/docs/apps/prefab.mdx
@@ -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'
-[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).
-[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
-
-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
-]
-```
-
-
-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
diff --git a/docs/docs.json b/docs/docs.json
index b391766d9..d08bdf89f 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -193,6 +193,7 @@
"pages": [
"apps/overview",
"apps/prefab",
+ "apps/interactive-apps",
"apps/patterns",
"apps/development",
"apps/low-level"
diff --git a/pyproject.toml b/pyproject.toml
index 904d7b81c..8fc39945c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -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"]
diff --git a/src/fastmcp/cli/apps_dev.py b/src/fastmcp/cli/apps_dev.py
index 8e7cc65fb..ac15b9c4c 100644
--- a/src/fastmcp/cli/apps_dev.py
+++ b/src/fastmcp/cli/apps_dev.py
@@ -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 ""
diff --git a/src/fastmcp/server/app.py b/src/fastmcp/server/app.py
index 9d786414a..440545e52 100644
--- a/src/fastmcp/server/app.py
+++ b/src/fastmcp/server/app.py
@@ -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
diff --git a/tests/test_fastmcp_app.py b/tests/test_fastmcp_app.py
index 1ef8023f4..4b4cd865c 100644
--- a/tests/test_fastmcp_app.py
+++ b/tests/test_fastmcp_app.py
@@ -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)
diff --git a/uv.lock b/uv.lock
index a59038b13..a898c84c6 100644
--- a/uv.lock
+++ b/uv.lock
@@ -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]]