mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-09 07:09:11 +02:00
Overhaul apps docs (#3915)
This commit is contained in:
parent
5c2ff1bd7b
commit
b732a4a516
36 changed files with 3567 additions and 1295 deletions
|
|
@ -1,119 +1,118 @@
|
|||
---
|
||||
title: App Architecture
|
||||
title: Architecture
|
||||
sidebarTitle: Architecture
|
||||
description: How FastMCP apps work under the hood — from Python to pixels.
|
||||
icon: sitemap
|
||||
tag: NEW
|
||||
---
|
||||
|
||||
import { VersionBadge } from '/snippets/version-badge.mdx'
|
||||
|
||||
<VersionBadge version="3.2.0" />
|
||||
|
||||
This page explains how Prefab apps work under the hood — how your Python code becomes an interactive UI inside a host client's conversation. You don't need any of this to build apps, but the mental model is useful when something isn't rendering the way you expect, when tool calls from the UI aren't reaching your server, or when you're building [custom HTML apps](/apps/low-level) and need to understand the protocol directly.
|
||||
You don't need this page to build apps. It's for when something isn't rendering the way you expect, when UI tool calls aren't reaching your server, or when you're writing [custom HTML apps](/apps/low-level) and need to understand the protocol directly.
|
||||
|
||||
## The Pipeline
|
||||
## The pipeline
|
||||
|
||||
An MCP App moves through five stages from Python to pixels:
|
||||
An MCP app moves through five stages from Python to pixels:
|
||||
|
||||
```
|
||||
Python components → JSON tree → structuredContent → Renderer iframe → Host UI
|
||||
```
|
||||
|
||||
You write Prefab components in Python. FastMCP serializes them to a JSON component tree and delivers it as `structuredContent` on the tool result. The host loads the Prefab renderer in a sandboxed iframe, pushes the JSON into it, and the renderer paints the UI. If the UI needs to call server tools, it talks back through the same `postMessage` channel.
|
||||
You write Prefab components. FastMCP serializes them to a JSON component tree and delivers it as `structuredContent` on the tool result. The host loads the Prefab renderer in a sandboxed iframe, pushes the JSON in, and the renderer paints the UI. If the UI calls server tools, it talks back through the same `postMessage` channel.
|
||||
|
||||
The following sections walk through each stage.
|
||||
The sections below walk each stage.
|
||||
|
||||
## Tool Registration
|
||||
## Tool registration
|
||||
|
||||
When you mark a tool with `app=True` or `@app.ui()`, FastMCP wires up the metadata and renderer resource that the protocol requires.
|
||||
|
||||
### The `app=True` Flag
|
||||
### The `app=True` flag
|
||||
|
||||
The `app` parameter on `@mcp.tool` accepts `True`, an `AppConfig` object, or a dict. When you pass `True`, FastMCP checks whether the tool's return type is a Prefab type (`PrefabApp`, `Component`, or unions containing them). If the tool qualifies, FastMCP expands `True` into a full `AppConfig` — setting the renderer URI, CSP headers, and visibility — and stores it in the tool's `meta["ui"]` dict.
|
||||
`app` on `@mcp.tool` accepts `True`, an `AppConfig`, or a dict. When you pass `True`, FastMCP checks whether the tool's return type is a Prefab type (`PrefabApp`, `Component`, or unions containing them). If it qualifies, FastMCP expands `True` into a full `AppConfig` — setting the renderer URI, CSP headers, and visibility — and stores it in the tool's `meta["ui"]` dict.
|
||||
|
||||
This expansion also triggers registration of the shared Prefab renderer resource (discussed below). The tool and the renderer are linked through a `resourceUri` field in the metadata: the tool says "render me with `ui://prefab/renderer.html`", and the host fetches that resource when it needs to display the result.
|
||||
This expansion also registers the shared Prefab renderer resource (below). The tool and the renderer are linked through a `resourceUri` field in the metadata: the tool says "render me with `ui://prefab/renderer.html`" and the host fetches that resource when it displays the result.
|
||||
|
||||
Type inference works the same way. If your return type annotation is a Prefab type and you haven't set `app` explicitly, FastMCP auto-wires the metadata as if you'd written `app=True`.
|
||||
Type inference works the same way. If the return type is a Prefab type and you haven't set `app` explicitly, FastMCP auto-wires the metadata as if you'd written `app=True`.
|
||||
|
||||
### FastMCPApp Registration
|
||||
### FastMCPApp registration
|
||||
|
||||
`FastMCPApp` uses the same underlying mechanism but adds two things. First, it tags every tool — both `@app.ui()` entry points and `@app.tool()` backends — with `meta["fastmcp"]["app"]` set to the app's name. This tag is how the server identifies which app a tool belongs to when routing calls from the UI.
|
||||
`FastMCPApp` uses the same mechanism but adds two things. First, it tags every tool — both `@app.ui()` entry points and `@app.tool()` backends — with `meta["fastmcp"]["app"]` set to the app's name. That tag lets the server identify which app a tool belongs to when routing UI calls.
|
||||
|
||||
Second, it sets `meta["ui"]["visibility"]` to control who can see each tool. Entry points default to `["model"]` (visible to the LLM). Backend tools default to `["app"]` (visible only to the UI). Hosts use this to filter the tool list — the model sees entry points, and the UI sees backends.
|
||||
Second, it sets `meta["ui"]["visibility"]` to control who can see each tool. Entry points default to `["model"]` (LLM-visible). Backend tools default to `["app"]` (UI-only). Hosts use this to filter the tool list.
|
||||
|
||||
## Serialization
|
||||
|
||||
When a Prefab tool runs, its return value — a `PrefabApp` or a raw `Component` — needs to become a JSON blob that the renderer can interpret.
|
||||
When a Prefab tool runs, its return value — a `PrefabApp` or a bare `Component` — becomes a JSON blob the renderer can interpret.
|
||||
|
||||
### PrefabApp.to_json()
|
||||
### `PrefabApp.to_json()`
|
||||
|
||||
The serialization entry point is `PrefabApp.to_json()`. This method walks the component tree and produces a JSON object with three top-level keys: `view` (the component tree), `state` (initial state values), and `_meta` (routing metadata).
|
||||
The entry point is `PrefabApp.to_json()`. It walks the component tree and produces a JSON object with three top-level keys: `view` (the component tree), `state` (initial state values), and `_meta` (routing metadata).
|
||||
|
||||
FastMCP passes a `tool_resolver` callback to `to_json()`. Whenever the component tree contains a `CallTool` action that references a function (not a string), the resolver converts it to a `ResolvedTool` with the function's registered name. This is how `CallTool(save_contact)` becomes `CallTool("save_contact")` in the wire format. The resolver also handles `unwrap_result` — a flag that tells the renderer to unwrap single-value results from the `{"result": value}` envelope that FastMCP uses for schema compliance.
|
||||
FastMCP passes a `tool_resolver` callback to `to_json()`. Whenever the tree contains a `CallTool` action that references a function (not a string), the resolver converts it to a `ResolvedTool` with the function's registered name. This is how `CallTool(save_contact)` becomes `CallTool("save_contact")` on the wire. The resolver also handles `unwrap_result` — a flag telling the renderer to unwrap single-value results from the `{"result": value}` envelope FastMCP uses for schema compliance.
|
||||
|
||||
### The _meta.fastmcp.app Tag
|
||||
### The `_meta.fastmcp.app` tag
|
||||
|
||||
After `to_json()` produces the JSON tree, FastMCP injects `_meta.fastmcp.app` with the app's name (if the tool belongs to a `FastMCPApp`). This tag rides along inside `structuredContent` all the way to the renderer.
|
||||
After `to_json()` produces the tree, FastMCP injects `_meta.fastmcp.app` with the app's name (if the tool belongs to a `FastMCPApp`). This tag rides along inside `structuredContent` all the way to the renderer.
|
||||
|
||||
When the renderer calls a backend tool, it includes `_meta.fastmcp.app` in the `CallTool` request. The server sees this tag and routes the call through a special path that bypasses transforms — more on this in the next section.
|
||||
When the renderer calls a backend tool, it includes `_meta.fastmcp.app` in the `CallTool` request. The server sees this tag and routes the call through a special path that bypasses transforms (below).
|
||||
|
||||
### ToolResult Assembly
|
||||
### ToolResult assembly
|
||||
|
||||
The final tool result has two parts: `content` (a list of `TextContent` blocks for the LLM) and `structuredContent` (the JSON tree for the renderer). By default, Prefab tools send `"[Rendered Prefab UI]"` as the text content — just enough for the LLM to know something was rendered. If you return a `ToolResult` explicitly, you control both halves.
|
||||
|
||||
## Tool Call Routing
|
||||
## Tool call routing
|
||||
|
||||
When a host calls a tool, the server needs to find it. Normal tool calls go through the provider chain, which applies transforms (namespace prefixes, visibility filters, etc.) before resolving the tool by name. But app UI calls need a different path.
|
||||
Normal tool calls go through the provider chain, which applies transforms (namespace prefixes, visibility filters) before resolving by name. App UI calls need a different path.
|
||||
|
||||
### The get_app_tool Bypass
|
||||
### The `get_app_tool` bypass
|
||||
|
||||
Backend tools registered with `@app.tool()` are typically hidden from the model (`visibility=["app"]`). Visibility transforms would filter them out of normal resolution. And namespace transforms might rename them — `save_contact` becomes `contacts_save_contact` — but the renderer still uses the original name.
|
||||
Backend tools are typically hidden from the model (`visibility=["app"]`). Visibility transforms would filter them out of normal resolution. And namespace transforms might rename them — `save_contact` becomes `contacts_save_contact` — while the renderer still uses the original name.
|
||||
|
||||
`get_app_tool` solves both problems. When the server sees `_meta.fastmcp.app` on an incoming `CallTool` request, it calls `get_app_tool(app_name, tool_name)` instead of the normal `get_tool(name)`. This method walks the provider tree directly, skipping the transform chain entirely. It finds the tool by its original registered name and verifies that its `meta["fastmcp"]["app"]` matches the expected app identity.
|
||||
`get_app_tool` solves both problems. When the server sees `_meta.fastmcp.app` on an incoming `CallTool` request, it calls `get_app_tool(app_name, tool_name)` instead of the normal `get_tool(name)`. This walks the provider tree directly, skipping transforms. It finds the tool by its original registered name and verifies that its `meta["fastmcp"]["app"]` matches the expected app.
|
||||
|
||||
This is why `CallTool("save_contact")` keeps working even when the server is mounted under a namespace prefix. The renderer sends the original name plus the app identity; the server uses `get_app_tool` to find the tool without transforms getting in the way.
|
||||
That's why `CallTool("save_contact")` keeps working when the server is mounted under a namespace. The renderer sends the original name plus the app identity; the server uses `get_app_tool` to find it without transforms in the way.
|
||||
|
||||
Authorization checks still apply — `get_app_tool` bypasses transforms, but it runs auth checks against the tool's `auth` configuration before executing.
|
||||
Authorization still applies. `get_app_tool` bypasses transforms but runs auth checks against the tool's `auth` config before executing.
|
||||
|
||||
### Provider Delegation
|
||||
### Provider delegation
|
||||
|
||||
The `get_app_tool` method is defined on the `Provider` base class and overridden by aggregate and wrapped providers. Aggregate providers fan out the lookup across all child providers in parallel. Wrapped providers (like `FastMCPProvider`, which wraps a nested `FastMCP` server) delegate to the inner server's `get_app_tool`. This means backend tools are reachable through any depth of server composition.
|
||||
`get_app_tool` is defined on the `Provider` base class and overridden by aggregate and wrapped providers. Aggregate providers fan out the lookup across child providers in parallel. Wrapped providers (like `FastMCPProvider`, which wraps a nested `FastMCP` server) delegate to the inner server's `get_app_tool`. Backend tools are reachable through any depth of composition.
|
||||
|
||||
## The Renderer
|
||||
## The renderer
|
||||
|
||||
The Prefab renderer is a self-contained JavaScript application that interprets the JSON component tree and renders it as a React UI.
|
||||
|
||||
### The Shared Resource
|
||||
### The shared resource
|
||||
|
||||
FastMCP registers the renderer as a `ui://prefab/renderer.html` resource with MIME type `text/html;profile=mcp-app`. The renderer HTML is bundled inside the `prefab-ui` Python package — `get_renderer_html()` returns it as a string. All Prefab tools on a server share this single resource, regardless of how many tools or apps are registered.
|
||||
FastMCP registers the renderer as a `ui://prefab/renderer.html` resource with MIME type `text/html;profile=mcp-app`. The HTML is bundled inside the `prefab-ui` Python package; `get_renderer_html()` returns it as a string. All Prefab tools on a server share this single resource.
|
||||
|
||||
The resource also carries CSP metadata (via `get_renderer_csp()`) declaring which CDN domains the renderer needs to load its JavaScript dependencies. Hosts use this to configure the iframe's Content Security Policy.
|
||||
The resource also carries CSP metadata (via `get_renderer_csp()`) declaring the CDN domains the renderer needs. Hosts use this to configure the iframe's Content Security Policy.
|
||||
|
||||
### postMessage Communication
|
||||
### `postMessage` communication
|
||||
|
||||
The renderer lives in a sandboxed iframe. It communicates with the host using `postMessage` — the standard browser API for cross-origin iframe communication. The protocol follows the [MCP Apps extension](https://modelcontextprotocol.io/docs/extensions/apps) specification:
|
||||
The renderer lives in a sandboxed iframe and communicates with the host using `postMessage`. The protocol follows the [MCP Apps extension](https://modelcontextprotocol.io/docs/extensions/apps) spec:
|
||||
|
||||
The host pushes the tool result (including `structuredContent`) into the iframe. The renderer parses the JSON component tree, initializes state, and renders the UI. When the user interacts with the UI — submitting a form, clicking a button — and that interaction triggers a `CallTool` action, the renderer sends a `callServerTool` message back to the host via `postMessage`. The host forwards this as a regular MCP `tools/call` request to the server, including `_meta.fastmcp.app` for routing.
|
||||
The host pushes the tool result (with `structuredContent`) into the iframe. The renderer parses the component tree, initializes state, and renders the UI. When the user interacts — submitting a form, clicking a button — and the interaction triggers a `CallTool` action, the renderer sends a `callServerTool` message back to the host via `postMessage`. The host forwards it as a regular MCP `tools/call` request to the server, including `_meta.fastmcp.app` for routing.
|
||||
|
||||
The response flows back the same way: server to host, host to iframe via `postMessage`, renderer updates state with the result.
|
||||
The response flows back the same way: server → host → iframe via `postMessage`, and the renderer updates state with the result.
|
||||
|
||||
### AppBridge
|
||||
|
||||
The `@modelcontextprotocol/ext-apps` JavaScript SDK provides the `App` class (sometimes called AppBridge) that manages the `postMessage` handshake. It handles connection negotiation, tool result delivery, server tool calls, and host context (like safe area insets and theme preferences). The Prefab renderer uses this SDK internally — you only interact with it directly when building [custom HTML apps](/apps/low-level).
|
||||
The `@modelcontextprotocol/ext-apps` JavaScript SDK provides the `App` class (sometimes called AppBridge) that manages the `postMessage` handshake. It handles connection negotiation, tool result delivery, server tool calls, and host context (safe area insets, theme preferences). The Prefab renderer uses it internally; you only touch it directly when building [custom HTML apps](/apps/low-level).
|
||||
|
||||
## The Dev Server
|
||||
## The dev server
|
||||
|
||||
`fastmcp dev apps` provides a local preview environment that simulates the host-side behavior without requiring a real MCP host client.
|
||||
`fastmcp dev apps` simulates the host-side behavior locally without a real MCP client.
|
||||
|
||||
### Proxy Architecture
|
||||
### Proxy architecture
|
||||
|
||||
The dev server runs two HTTP servers. Your MCP server starts on port 8000 (configurable) with the Streamable HTTP transport. The dev UI runs on port 8080 and serves a picker page that lists your app tools.
|
||||
Two HTTP servers. Your MCP server runs on port 8000 with the Streamable HTTP transport. The dev UI runs on port 8080 and serves a picker page that lists your app tools.
|
||||
|
||||
A reverse proxy at `/mcp` on the dev server forwards requests to your MCP server. This is important because the renderer iframe runs on `localhost:8080`, and your MCP server runs on `localhost:8000`. Without the proxy, the renderer's `callServerTool` requests would be cross-origin and blocked by the browser. The proxy makes everything same-origin from the iframe's perspective.
|
||||
A reverse proxy at `/mcp` on the dev server forwards requests to your MCP server. This matters because the renderer iframe runs on `localhost:8080` and your MCP server runs on `localhost:8000` — without the proxy, the renderer's `callServerTool` requests would be cross-origin and the browser would block them. The proxy keeps everything same-origin from the iframe's perspective.
|
||||
|
||||
### The Launch Flow
|
||||
### The launch flow
|
||||
|
||||
When you select a tool and click launch, the dev UI calls the tool through the proxy, receives the `structuredContent` response, and opens a new tab. That tab loads the tool's renderer resource (fetched from the proxy) in an iframe, creates an AppBridge instance, and pushes the tool result into the renderer. From this point forward, the experience matches what a real host would provide — the renderer displays the UI, and any `CallTool` actions route back through the proxy to your MCP server.
|
||||
When you select a tool and click launch, the dev UI calls the tool through the proxy, receives the `structuredContent` response, and opens a new tab. That tab loads the tool's renderer resource (via the proxy), creates an AppBridge, and pushes the tool result into the renderer. From here on it matches what a real host provides: the renderer displays the UI, and any `CallTool` actions route back through the proxy to your server.
|
||||
|
||||
Auto-reload is enabled by default, so changes to your server code restart the MCP server automatically. The dev UI stays running — just re-launch the tool to see your changes.
|
||||
Auto-reload is on by default, so changes to your server code restart the MCP server automatically. The dev UI keeps running — relaunch the tool to see changes.
|
||||
|
|
|
|||
76
docs/apps/demos/bar-chart.html
Normal file
76
docs/apps/demos/bar-chart.html
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Prefab</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" crossorigin href="https://cdn.jsdelivr.net/npm/@prefecthq/prefab-ui@0.19.0/dist/app/renderer.css">
|
||||
<script type="module" crossorigin src="https://cdn.jsdelivr.net/npm/@prefecthq/prefab-ui@0.19.0/dist/app/renderer.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root" style="max-width:64rem;margin:0 auto;padding:2rem"></div>
|
||||
<script id="prefab:initial-data" type="application/json">{
|
||||
"$prefab": {
|
||||
"version": "0.2"
|
||||
},
|
||||
"view": {
|
||||
"cssClass": "pf-app-root",
|
||||
"type": "Div",
|
||||
"children": [
|
||||
{
|
||||
"cssClass": "p-6",
|
||||
"type": "Column",
|
||||
"children": [
|
||||
{
|
||||
"type": "BarChart",
|
||||
"data": [
|
||||
{
|
||||
"quarter": "Q1",
|
||||
"revenue": 42000,
|
||||
"costs": 28000
|
||||
},
|
||||
{
|
||||
"quarter": "Q2",
|
||||
"revenue": 51000,
|
||||
"costs": 31000
|
||||
},
|
||||
{
|
||||
"quarter": "Q3",
|
||||
"revenue": 47000,
|
||||
"costs": 29000
|
||||
},
|
||||
{
|
||||
"quarter": "Q4",
|
||||
"revenue": 63000,
|
||||
"costs": 35000
|
||||
}
|
||||
],
|
||||
"series": [
|
||||
{
|
||||
"dataKey": "revenue",
|
||||
"label": "Revenue"
|
||||
},
|
||||
{
|
||||
"dataKey": "costs",
|
||||
"label": "Costs"
|
||||
}
|
||||
],
|
||||
"xAxis": "quarter",
|
||||
"height": 250,
|
||||
"stacked": false,
|
||||
"horizontal": false,
|
||||
"barRadius": 4,
|
||||
"showLegend": true,
|
||||
"showTooltip": true,
|
||||
"animate": true,
|
||||
"showGrid": true,
|
||||
"showYAxis": true,
|
||||
"yAxisFormat": "auto"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}</script>
|
||||
</body>
|
||||
</html>
|
||||
23
docs/apps/demos/bar-chart.py
Normal file
23
docs/apps/demos/bar-chart.py
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
from prefab_ui.app import PrefabApp
|
||||
from prefab_ui.components import Column
|
||||
from prefab_ui.components.charts import BarChart, ChartSeries
|
||||
|
||||
data = [
|
||||
{"quarter": "Q1", "revenue": 42000, "costs": 28000},
|
||||
{"quarter": "Q2", "revenue": 51000, "costs": 31000},
|
||||
{"quarter": "Q3", "revenue": 47000, "costs": 29000},
|
||||
{"quarter": "Q4", "revenue": 63000, "costs": 35000},
|
||||
]
|
||||
|
||||
with PrefabApp() as app:
|
||||
with Column(css_class="p-6"):
|
||||
BarChart(
|
||||
data=data,
|
||||
series=[
|
||||
ChartSeries(data_key="revenue", label="Revenue"),
|
||||
ChartSeries(data_key="costs", label="Costs"),
|
||||
],
|
||||
x_axis="quarter",
|
||||
show_legend=True,
|
||||
height=250,
|
||||
)
|
||||
172
docs/apps/demos/contacts.html
Normal file
172
docs/apps/demos/contacts.html
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Prefab</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" crossorigin href="https://cdn.jsdelivr.net/npm/@prefecthq/prefab-ui@0.19.0/dist/app/renderer.css">
|
||||
<script type="module" crossorigin src="https://cdn.jsdelivr.net/npm/@prefecthq/prefab-ui@0.19.0/dist/app/renderer.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root" style="max-width:64rem;margin:0 auto;padding:2rem"></div>
|
||||
<script id="prefab:initial-data" type="application/json">{
|
||||
"$prefab": {
|
||||
"version": "0.2"
|
||||
},
|
||||
"view": {
|
||||
"cssClass": "pf-app-root",
|
||||
"type": "Div",
|
||||
"children": [
|
||||
{
|
||||
"cssClass": "gap-4 p-6",
|
||||
"type": "Column",
|
||||
"children": [
|
||||
{
|
||||
"type": "DataTable",
|
||||
"columns": [
|
||||
{
|
||||
"key": "name",
|
||||
"header": "Name",
|
||||
"sortable": true
|
||||
},
|
||||
{
|
||||
"key": "email",
|
||||
"header": "Email",
|
||||
"sortable": false
|
||||
},
|
||||
{
|
||||
"key": "category",
|
||||
"header": "Category",
|
||||
"sortable": false
|
||||
}
|
||||
],
|
||||
"rows": [
|
||||
{
|
||||
"name": "Arthur Dent",
|
||||
"email": "arthur@earth.com",
|
||||
"category": {
|
||||
"type": "Badge",
|
||||
"label": "Customer",
|
||||
"variant": "success"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Ford Prefect",
|
||||
"email": "ford@betelgeuse.org",
|
||||
"category": {
|
||||
"type": "Badge",
|
||||
"label": "Partner",
|
||||
"variant": "secondary"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Trillian Astra",
|
||||
"email": "trillian@heartofgold.com",
|
||||
"category": {
|
||||
"type": "Badge",
|
||||
"label": "Customer",
|
||||
"variant": "success"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Zaphod Beeblebrox",
|
||||
"email": "zaphod@galaxy.gov",
|
||||
"category": {
|
||||
"type": "Badge",
|
||||
"label": "Vendor",
|
||||
"variant": "outline"
|
||||
}
|
||||
}
|
||||
],
|
||||
"search": true,
|
||||
"paginated": false,
|
||||
"pageSize": 10
|
||||
},
|
||||
{
|
||||
"type": "Separator",
|
||||
"orientation": "horizontal"
|
||||
},
|
||||
{
|
||||
"content": "Add Contact",
|
||||
"type": "H3"
|
||||
},
|
||||
{
|
||||
"cssClass": "gap-4",
|
||||
"type": "Form",
|
||||
"onSubmit": {
|
||||
"action": "showToast",
|
||||
"message": "Contact saved! (preview demo \u2014 no backend wired)",
|
||||
"variant": "success"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"cssClass": "gap-4",
|
||||
"type": "Row",
|
||||
"children": [
|
||||
{
|
||||
"name": "name",
|
||||
"type": "Input",
|
||||
"inputType": "text",
|
||||
"placeholder": "Full name",
|
||||
"disabled": false,
|
||||
"readOnly": false,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "email",
|
||||
"type": "Input",
|
||||
"inputType": "text",
|
||||
"placeholder": "name@example.com",
|
||||
"disabled": false,
|
||||
"readOnly": false,
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "category",
|
||||
"type": "Select",
|
||||
"size": "default",
|
||||
"disabled": false,
|
||||
"required": false,
|
||||
"invalid": false,
|
||||
"children": [
|
||||
{
|
||||
"type": "SelectOption",
|
||||
"value": "Customer",
|
||||
"label": "Customer",
|
||||
"selected": false,
|
||||
"disabled": false
|
||||
},
|
||||
{
|
||||
"type": "SelectOption",
|
||||
"value": "Partner",
|
||||
"label": "Partner",
|
||||
"selected": false,
|
||||
"disabled": false
|
||||
},
|
||||
{
|
||||
"type": "SelectOption",
|
||||
"value": "Vendor",
|
||||
"label": "Vendor",
|
||||
"selected": false,
|
||||
"disabled": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Button",
|
||||
"label": "Save Contact",
|
||||
"variant": "default",
|
||||
"size": "default",
|
||||
"disabled": false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}</script>
|
||||
</body>
|
||||
</html>
|
||||
78
docs/apps/demos/contacts.py
Normal file
78
docs/apps/demos/contacts.py
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
from prefab_ui.actions import ShowToast
|
||||
from prefab_ui.app import PrefabApp
|
||||
from prefab_ui.components import (
|
||||
H3,
|
||||
Badge,
|
||||
Button,
|
||||
Column,
|
||||
DataTable,
|
||||
DataTableColumn,
|
||||
Form,
|
||||
Input,
|
||||
Row,
|
||||
Select,
|
||||
SelectOption,
|
||||
Separator,
|
||||
)
|
||||
|
||||
contacts = [
|
||||
{"name": "Arthur Dent", "email": "arthur@earth.com", "category": "Customer"},
|
||||
{"name": "Ford Prefect", "email": "ford@betelgeuse.org", "category": "Partner"},
|
||||
{
|
||||
"name": "Trillian Astra",
|
||||
"email": "trillian@heartofgold.com",
|
||||
"category": "Customer",
|
||||
},
|
||||
{"name": "Zaphod Beeblebrox", "email": "zaphod@galaxy.gov", "category": "Vendor"},
|
||||
]
|
||||
|
||||
rows = [
|
||||
{
|
||||
"name": c["name"],
|
||||
"email": c["email"],
|
||||
"category": Badge(
|
||||
c["category"],
|
||||
variant="success"
|
||||
if c["category"] == "Customer"
|
||||
else "secondary"
|
||||
if c["category"] == "Partner"
|
||||
else "outline",
|
||||
),
|
||||
}
|
||||
for c in contacts
|
||||
]
|
||||
|
||||
with PrefabApp() as app:
|
||||
with Column(gap=4, css_class="p-6"):
|
||||
DataTable(
|
||||
columns=[
|
||||
DataTableColumn(key="name", header="Name", sortable=True),
|
||||
DataTableColumn(key="email", header="Email"),
|
||||
DataTableColumn(key="category", header="Category"),
|
||||
],
|
||||
rows=rows,
|
||||
search=True,
|
||||
)
|
||||
|
||||
Separator()
|
||||
|
||||
H3("Add Contact")
|
||||
with Form(
|
||||
on_submit=ShowToast(
|
||||
"Contact saved! (preview demo — no backend wired)",
|
||||
variant="success",
|
||||
),
|
||||
):
|
||||
with Row(gap=4):
|
||||
Input(name="name", label="Name", placeholder="Full name", required=True)
|
||||
Input(
|
||||
name="email",
|
||||
label="Email",
|
||||
placeholder="name@example.com",
|
||||
required=True,
|
||||
)
|
||||
with Select(name="category", label="Category"):
|
||||
SelectOption(value="Customer", label="Customer")
|
||||
SelectOption(value="Partner", label="Partner")
|
||||
SelectOption(value="Vendor", label="Vendor")
|
||||
Button("Save Contact")
|
||||
157
docs/apps/demos/dashboard.html
Normal file
157
docs/apps/demos/dashboard.html
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Prefab</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" crossorigin href="https://cdn.jsdelivr.net/npm/@prefecthq/prefab-ui@0.19.0/dist/app/renderer.css">
|
||||
<script type="module" crossorigin src="https://cdn.jsdelivr.net/npm/@prefecthq/prefab-ui@0.19.0/dist/app/renderer.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root" style="max-width:64rem;margin:0 auto;padding:2rem"></div>
|
||||
<script id="prefab:initial-data" type="application/json">{
|
||||
"$prefab": {
|
||||
"version": "0.2"
|
||||
},
|
||||
"view": {
|
||||
"cssClass": "pf-app-root",
|
||||
"type": "Div",
|
||||
"children": [
|
||||
{
|
||||
"cssClass": "gap-4 p-6",
|
||||
"type": "Column",
|
||||
"children": [
|
||||
{
|
||||
"cssClass": "gap-6",
|
||||
"type": "Row",
|
||||
"children": [
|
||||
{
|
||||
"type": "Metric",
|
||||
"label": "Revenue (Q1-Q4)",
|
||||
"value": "$220,500"
|
||||
},
|
||||
{
|
||||
"type": "Metric",
|
||||
"label": "Deals",
|
||||
"value": "4"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "BarChart",
|
||||
"data": [
|
||||
{
|
||||
"month": "Jan",
|
||||
"revenue": 48200,
|
||||
"costs": 31000
|
||||
},
|
||||
{
|
||||
"month": "Feb",
|
||||
"revenue": 52100,
|
||||
"costs": 32500
|
||||
},
|
||||
{
|
||||
"month": "Mar",
|
||||
"revenue": 61800,
|
||||
"costs": 34200
|
||||
},
|
||||
{
|
||||
"month": "Apr",
|
||||
"revenue": 58400,
|
||||
"costs": 33800
|
||||
}
|
||||
],
|
||||
"series": [
|
||||
{
|
||||
"dataKey": "revenue",
|
||||
"label": "Revenue"
|
||||
},
|
||||
{
|
||||
"dataKey": "costs",
|
||||
"label": "Costs"
|
||||
}
|
||||
],
|
||||
"xAxis": "month",
|
||||
"height": 200,
|
||||
"stacked": false,
|
||||
"horizontal": false,
|
||||
"barRadius": 4,
|
||||
"showLegend": true,
|
||||
"showTooltip": true,
|
||||
"animate": true,
|
||||
"showGrid": true,
|
||||
"showYAxis": true,
|
||||
"yAxisFormat": "auto"
|
||||
},
|
||||
{
|
||||
"type": "Separator",
|
||||
"orientation": "horizontal"
|
||||
},
|
||||
{
|
||||
"type": "DataTable",
|
||||
"columns": [
|
||||
{
|
||||
"key": "account",
|
||||
"header": "Account",
|
||||
"sortable": true
|
||||
},
|
||||
{
|
||||
"key": "value",
|
||||
"header": "Value",
|
||||
"sortable": true
|
||||
},
|
||||
{
|
||||
"key": "stage",
|
||||
"header": "Stage",
|
||||
"sortable": false
|
||||
}
|
||||
],
|
||||
"rows": [
|
||||
{
|
||||
"account": "Acme Corp",
|
||||
"value": "$84,000",
|
||||
"stage": {
|
||||
"type": "Badge",
|
||||
"label": "Won",
|
||||
"variant": "success"
|
||||
}
|
||||
},
|
||||
{
|
||||
"account": "Globex Inc",
|
||||
"value": "$52,000",
|
||||
"stage": {
|
||||
"type": "Badge",
|
||||
"label": "Negotiation",
|
||||
"variant": "secondary"
|
||||
}
|
||||
},
|
||||
{
|
||||
"account": "Initech",
|
||||
"value": "$31,500",
|
||||
"stage": {
|
||||
"type": "Badge",
|
||||
"label": "Proposal",
|
||||
"variant": "secondary"
|
||||
}
|
||||
},
|
||||
{
|
||||
"account": "Wayne Enterprises",
|
||||
"value": "$45,000",
|
||||
"stage": {
|
||||
"type": "Badge",
|
||||
"label": "Lost",
|
||||
"variant": "destructive"
|
||||
}
|
||||
}
|
||||
],
|
||||
"search": false,
|
||||
"paginated": false,
|
||||
"pageSize": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}</script>
|
||||
</body>
|
||||
</html>
|
||||
68
docs/apps/demos/dashboard.py
Normal file
68
docs/apps/demos/dashboard.py
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
from prefab_ui.app import PrefabApp
|
||||
from prefab_ui.components import (
|
||||
Badge,
|
||||
Column,
|
||||
DataTable,
|
||||
DataTableColumn,
|
||||
Row,
|
||||
Separator,
|
||||
)
|
||||
from prefab_ui.components.charts import BarChart, ChartSeries
|
||||
from prefab_ui.components.metric import Metric
|
||||
|
||||
monthly = [
|
||||
{"month": "Jan", "revenue": 48200, "costs": 31000},
|
||||
{"month": "Feb", "revenue": 52100, "costs": 32500},
|
||||
{"month": "Mar", "revenue": 61800, "costs": 34200},
|
||||
{"month": "Apr", "revenue": 58400, "costs": 33800},
|
||||
]
|
||||
|
||||
deals = [
|
||||
{"account": "Acme Corp", "value": "$84,000", "stage": "Won"},
|
||||
{"account": "Globex Inc", "value": "$52,000", "stage": "Negotiation"},
|
||||
{"account": "Initech", "value": "$31,500", "stage": "Proposal"},
|
||||
{"account": "Wayne Enterprises", "value": "$45,000", "stage": "Lost"},
|
||||
]
|
||||
|
||||
rows = [
|
||||
{
|
||||
"account": d["account"],
|
||||
"value": d["value"],
|
||||
"stage": Badge(
|
||||
d["stage"],
|
||||
variant="success"
|
||||
if d["stage"] == "Won"
|
||||
else "destructive"
|
||||
if d["stage"] == "Lost"
|
||||
else "secondary",
|
||||
),
|
||||
}
|
||||
for d in deals
|
||||
]
|
||||
|
||||
total = sum(m["revenue"] for m in monthly)
|
||||
|
||||
with PrefabApp() as app:
|
||||
with Column(gap=4, css_class="p-6"):
|
||||
with Row(gap=6):
|
||||
Metric(label="Revenue (Q1-Q4)", value=f"${total:,}")
|
||||
Metric(label="Deals", value=f"{len(deals)}")
|
||||
BarChart(
|
||||
data=monthly,
|
||||
series=[
|
||||
ChartSeries(data_key="revenue", label="Revenue"),
|
||||
ChartSeries(data_key="costs", label="Costs"),
|
||||
],
|
||||
x_axis="month",
|
||||
show_legend=True,
|
||||
height=200,
|
||||
)
|
||||
Separator()
|
||||
DataTable(
|
||||
columns=[
|
||||
DataTableColumn(key="account", header="Account", sortable=True),
|
||||
DataTableColumn(key="value", header="Value", sortable=True),
|
||||
DataTableColumn(key="stage", header="Stage"),
|
||||
],
|
||||
rows=rows,
|
||||
)
|
||||
90
docs/apps/demos/data-table.html
Normal file
90
docs/apps/demos/data-table.html
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Prefab</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" crossorigin href="https://cdn.jsdelivr.net/npm/@prefecthq/prefab-ui@0.19.0/dist/app/renderer.css">
|
||||
<script type="module" crossorigin src="https://cdn.jsdelivr.net/npm/@prefecthq/prefab-ui@0.19.0/dist/app/renderer.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root" style="max-width:64rem;margin:0 auto;padding:2rem"></div>
|
||||
<script id="prefab:initial-data" type="application/json">{
|
||||
"$prefab": {
|
||||
"version": "0.2"
|
||||
},
|
||||
"view": {
|
||||
"cssClass": "pf-app-root",
|
||||
"type": "Div",
|
||||
"children": [
|
||||
{
|
||||
"cssClass": "gap-4 p-6",
|
||||
"type": "Column",
|
||||
"children": [
|
||||
{
|
||||
"type": "DataTable",
|
||||
"columns": [
|
||||
{
|
||||
"key": "name",
|
||||
"header": "Name",
|
||||
"sortable": true
|
||||
},
|
||||
{
|
||||
"key": "role",
|
||||
"header": "Role",
|
||||
"sortable": true
|
||||
},
|
||||
{
|
||||
"key": "dept",
|
||||
"header": "Dept",
|
||||
"sortable": true
|
||||
}
|
||||
],
|
||||
"rows": [
|
||||
{
|
||||
"name": "Alice Chen",
|
||||
"role": "Staff Engineer",
|
||||
"dept": "Platform"
|
||||
},
|
||||
{
|
||||
"name": "Bob Martinez",
|
||||
"role": "Lead Designer",
|
||||
"dept": "Design"
|
||||
},
|
||||
{
|
||||
"name": "Carol Johnson",
|
||||
"role": "Senior Engineer",
|
||||
"dept": "Platform"
|
||||
},
|
||||
{
|
||||
"name": "David Kim",
|
||||
"role": "Product Manager",
|
||||
"dept": "Product"
|
||||
},
|
||||
{
|
||||
"name": "Eva Mueller",
|
||||
"role": "Engineer",
|
||||
"dept": "Platform"
|
||||
},
|
||||
{
|
||||
"name": "Frank Lee",
|
||||
"role": "Data Scientist",
|
||||
"dept": "ML"
|
||||
},
|
||||
{
|
||||
"name": "Grace Park",
|
||||
"role": "Eng Manager",
|
||||
"dept": "Platform"
|
||||
}
|
||||
],
|
||||
"search": true,
|
||||
"paginated": false,
|
||||
"pageSize": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}</script>
|
||||
</body>
|
||||
</html>
|
||||
24
docs/apps/demos/data-table.py
Normal file
24
docs/apps/demos/data-table.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
from prefab_ui.app import PrefabApp
|
||||
from prefab_ui.components import Column, DataTable, DataTableColumn
|
||||
|
||||
employees = [
|
||||
{"name": "Alice Chen", "role": "Staff Engineer", "dept": "Platform"},
|
||||
{"name": "Bob Martinez", "role": "Lead Designer", "dept": "Design"},
|
||||
{"name": "Carol Johnson", "role": "Senior Engineer", "dept": "Platform"},
|
||||
{"name": "David Kim", "role": "Product Manager", "dept": "Product"},
|
||||
{"name": "Eva Mueller", "role": "Engineer", "dept": "Platform"},
|
||||
{"name": "Frank Lee", "role": "Data Scientist", "dept": "ML"},
|
||||
{"name": "Grace Park", "role": "Eng Manager", "dept": "Platform"},
|
||||
]
|
||||
|
||||
with PrefabApp() as app:
|
||||
with Column(gap=4, css_class="p-6"):
|
||||
DataTable(
|
||||
columns=[
|
||||
DataTableColumn(key="name", header="Name", sortable=True),
|
||||
DataTableColumn(key="role", header="Role", sortable=True),
|
||||
DataTableColumn(key="dept", header="Dept", sortable=True),
|
||||
],
|
||||
rows=employees,
|
||||
search=True,
|
||||
)
|
||||
1105
docs/apps/demos/hitchhikers.html
Normal file
1105
docs/apps/demos/hitchhikers.html
Normal file
File diff suppressed because it is too large
Load diff
461
docs/apps/demos/hitchhikers.py
Normal file
461
docs/apps/demos/hitchhikers.py
Normal file
|
|
@ -0,0 +1,461 @@
|
|||
"""The Hitchhiker's Guide dashboard from the Prefab welcome page.
|
||||
|
||||
Run with:
|
||||
prefab serve examples/hitchhikers-guide/dashboard.py
|
||||
prefab export examples/hitchhikers-guide/dashboard.py
|
||||
"""
|
||||
|
||||
from prefab_ui import PrefabApp
|
||||
from prefab_ui.actions import SetInterval, SetState, ShowToast
|
||||
from prefab_ui.components import (
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertTitle,
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Carousel,
|
||||
Checkbox,
|
||||
Column,
|
||||
Combobox,
|
||||
ComboboxOption,
|
||||
DataTable,
|
||||
DataTableColumn,
|
||||
DatePicker,
|
||||
Dialog,
|
||||
Grid,
|
||||
GridItem,
|
||||
HoverCard,
|
||||
Loader,
|
||||
Metric,
|
||||
Muted,
|
||||
P,
|
||||
Progress,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
Ring,
|
||||
Row,
|
||||
Separator,
|
||||
Slider,
|
||||
Switch,
|
||||
Text,
|
||||
Tooltip,
|
||||
)
|
||||
from prefab_ui.components.charts import (
|
||||
BarChart,
|
||||
ChartSeries,
|
||||
RadarChart,
|
||||
Sparkline,
|
||||
)
|
||||
from prefab_ui.components.control_flow import Else, If
|
||||
from prefab_ui.rx import Rx
|
||||
|
||||
ctx_tick = Rx("ctx_tick")
|
||||
|
||||
# Context window: climbs from 24% to ~78%, then resets
|
||||
ctx_pct = (ctx_tick % 20) * 3 + 20
|
||||
ctx_variant = (ctx_pct > 70).then(
|
||||
"destructive", (ctx_pct <= 33).then("success", "default")
|
||||
)
|
||||
|
||||
with PrefabApp(
|
||||
title="Prefab Showcase",
|
||||
state={"ctx_tick": 0, "improbability": 42},
|
||||
on_mount=SetInterval(
|
||||
400,
|
||||
on_tick=SetState("ctx_tick", ctx_tick + 1),
|
||||
),
|
||||
) as app:
|
||||
with Grid(columns={"default": 1, "md": 2, "lg": 4}, gap=4):
|
||||
# ── Col 1 ─────────────────────────────────────────────────────────
|
||||
with Column(gap=4):
|
||||
with Card():
|
||||
with CardHeader():
|
||||
CardTitle("Register Towel")
|
||||
CardDescription("The most important item in the galaxy")
|
||||
with CardContent():
|
||||
with Column(gap=3):
|
||||
with Combobox(
|
||||
placeholder="Type...",
|
||||
search_placeholder="Search types...",
|
||||
):
|
||||
ComboboxOption("Bath", value="bath")
|
||||
ComboboxOption("Beach", value="beach")
|
||||
ComboboxOption("Interstellar", value="interstellar")
|
||||
ComboboxOption("Microfiber", value="micro")
|
||||
DatePicker(placeholder="Registration date")
|
||||
with CardFooter():
|
||||
with Row(gap=2):
|
||||
with Dialog(
|
||||
title="Towel Registered!",
|
||||
description="Your towel has been added to the galactic registry.",
|
||||
):
|
||||
Button("Register")
|
||||
Text("Don't forget to bring it.")
|
||||
Button("Cancel", variant="outline")
|
||||
with If("{{ !pressed }}"):
|
||||
Button(
|
||||
"This is probably the best button to press.",
|
||||
variant="success",
|
||||
on_click=SetState("pressed", True),
|
||||
)
|
||||
with Else():
|
||||
Button(
|
||||
"Please do not press this button again.",
|
||||
variant="destructive",
|
||||
on_click=SetState("pressed", False),
|
||||
)
|
||||
|
||||
with Card():
|
||||
with CardHeader():
|
||||
CardTitle("Ship Status")
|
||||
with CardContent():
|
||||
with Column(gap=3):
|
||||
with Row(
|
||||
align="center",
|
||||
css_class="justify-between",
|
||||
):
|
||||
Text("heart-of-gold")
|
||||
with HoverCard(open_delay=0, close_delay=200):
|
||||
Badge("In Orbit", variant="default")
|
||||
with Column(gap=2):
|
||||
Text("heart-of-gold")
|
||||
Muted("Deployed 2h ago")
|
||||
Progress(
|
||||
value=100,
|
||||
max=100,
|
||||
variant="success",
|
||||
)
|
||||
Progress(
|
||||
value=100,
|
||||
max=100,
|
||||
indicator_class="bg-yellow-400",
|
||||
)
|
||||
with Row(
|
||||
align="center",
|
||||
css_class="justify-between",
|
||||
):
|
||||
Text("vogon-poetry")
|
||||
with Tooltip("64% — ETA 12 min", delay=0):
|
||||
with Badge(variant="secondary"):
|
||||
Loader(size="sm")
|
||||
Text("Deploying")
|
||||
Progress(value=64, max=100)
|
||||
with Row(
|
||||
align="center",
|
||||
css_class="justify-between",
|
||||
):
|
||||
Text("deep-thought")
|
||||
with Tooltip(
|
||||
"Computing... 7.5 million years remaining",
|
||||
delay=0,
|
||||
):
|
||||
with Badge(variant="outline"):
|
||||
Loader(size="sm", variant="ios")
|
||||
Text("Soon...")
|
||||
Progress(value=12, max=100)
|
||||
with Card():
|
||||
with CardHeader():
|
||||
CardTitle("Planet Ratings")
|
||||
with CardContent():
|
||||
RadarChart(
|
||||
data=[
|
||||
{"axis": "Views", "earth": 30, "mag": 95},
|
||||
{"axis": "Fjords", "earth": 65, "mag": 100},
|
||||
{"axis": "Pubs", "earth": 90, "mag": 10},
|
||||
{"axis": "Mice", "earth": 40, "mag": 85},
|
||||
{"axis": "Tea", "earth": 95, "mag": 15},
|
||||
{"axis": "Safety", "earth": 45, "mag": 70},
|
||||
],
|
||||
series=[
|
||||
ChartSeries(dataKey="earth", label="Earth"),
|
||||
ChartSeries(dataKey="mag", label="Magrathea"),
|
||||
],
|
||||
axis_key="axis",
|
||||
height=200,
|
||||
show_legend=True,
|
||||
show_tooltip=True,
|
||||
)
|
||||
|
||||
# ── Col 2 ─────────────────────────────────────────────────────────
|
||||
with Column(gap=4):
|
||||
with Card():
|
||||
with CardHeader():
|
||||
CardTitle("Survival Odds")
|
||||
with CardContent(css_class="w-fit mx-auto"):
|
||||
Ring(
|
||||
value=42,
|
||||
label="42%",
|
||||
variant="info",
|
||||
size="lg",
|
||||
thickness=12,
|
||||
indicator_class="group-hover:drop-shadow-[0_0_24px_rgba(59,130,246,0.9)]",
|
||||
)
|
||||
with Card():
|
||||
with CardHeader():
|
||||
with Row(gap=2, align="center"):
|
||||
CardTitle("Improbability Drive")
|
||||
Loader(
|
||||
variant="pulse",
|
||||
size="sm",
|
||||
css_class="text-blue-500",
|
||||
)
|
||||
with CardContent():
|
||||
with Column(gap=2):
|
||||
Slider(
|
||||
min=0,
|
||||
max=100,
|
||||
value=42,
|
||||
name="improbability",
|
||||
)
|
||||
with Row(
|
||||
align="center",
|
||||
css_class="justify-between",
|
||||
):
|
||||
Muted("Probable")
|
||||
Muted("Infinite")
|
||||
with Carousel(auto_advance=3000, show_controls=False, direction="up"):
|
||||
with Alert(variant="success", icon="circle-check"):
|
||||
AlertTitle("Don't Panic")
|
||||
AlertDescription("Normality achieved.")
|
||||
with Alert(variant="destructive", icon="triangle-alert"):
|
||||
AlertTitle("Display Department")
|
||||
AlertDescription("Beware of the leopard.")
|
||||
with Card():
|
||||
with CardHeader():
|
||||
CardTitle("Prefect Horizon Config")
|
||||
with CardContent():
|
||||
with Column(gap=3):
|
||||
Switch(
|
||||
label="Auto-scale agents",
|
||||
value=True,
|
||||
name="autoscale",
|
||||
)
|
||||
Separator()
|
||||
Switch(
|
||||
label="Code Mode",
|
||||
value=True,
|
||||
name="code_mode",
|
||||
)
|
||||
Separator()
|
||||
Switch(
|
||||
label="Tool call caching",
|
||||
value=False,
|
||||
name="cache",
|
||||
)
|
||||
with CardFooter():
|
||||
Button(
|
||||
"Save Preferences",
|
||||
on_click=ShowToast("Preferences saved!"),
|
||||
)
|
||||
with Card():
|
||||
with CardHeader():
|
||||
CardTitle("Travel Class")
|
||||
with CardContent():
|
||||
with RadioGroup(name="travel_class"):
|
||||
Radio(option="economy", label="Economy")
|
||||
Radio(option="business", label="Business Class")
|
||||
Radio(
|
||||
option="improbability",
|
||||
label="Infinite Improbability",
|
||||
value=True,
|
||||
)
|
||||
|
||||
# ── Cols 3–4: summary row, chart, then 2-col grid below ─────────
|
||||
with GridItem(css_class="md:col-span-2"):
|
||||
with Column(gap=4):
|
||||
with Grid(columns=2, gap=4, css_class="h-32"):
|
||||
with Card():
|
||||
with CardHeader():
|
||||
CardTitle("Context Window")
|
||||
with CardContent():
|
||||
with Column(
|
||||
gap=6,
|
||||
justify="center",
|
||||
css_class="h-full",
|
||||
):
|
||||
with Row(
|
||||
align="center",
|
||||
css_class="justify-between",
|
||||
):
|
||||
Text(f"{ctx_pct}% used")
|
||||
Muted(f"{ctx_pct * 2}k / 200k tokens")
|
||||
with Tooltip(
|
||||
"Auto-compact buffer: 12%",
|
||||
delay=0,
|
||||
):
|
||||
Progress(
|
||||
value=ctx_pct,
|
||||
max=100,
|
||||
variant=ctx_variant,
|
||||
)
|
||||
with Card(css_class="pb-0 gap-0"):
|
||||
with CardContent():
|
||||
Metric(
|
||||
label="Fjords designed",
|
||||
value="1,847",
|
||||
delta="+3 coastlines",
|
||||
)
|
||||
Sparkline(
|
||||
data=[
|
||||
820,
|
||||
950,
|
||||
1100,
|
||||
980,
|
||||
1250,
|
||||
1400,
|
||||
1350,
|
||||
1500,
|
||||
1680,
|
||||
1847,
|
||||
],
|
||||
variant="success",
|
||||
fill=True,
|
||||
css_class="h-16",
|
||||
)
|
||||
with Card():
|
||||
with CardHeader():
|
||||
CardTitle("Towel Incidents")
|
||||
with CardContent():
|
||||
BarChart(
|
||||
data=[
|
||||
{"month": "Jan", "lost": 8, "found": 5},
|
||||
{"month": "Feb", "lost": 24, "found": 15},
|
||||
{"month": "Mar", "lost": 12, "found": 28},
|
||||
{"month": "Apr", "lost": 35, "found": 19},
|
||||
{"month": "May", "lost": 18, "found": 38},
|
||||
{"month": "Jun", "lost": 42, "found": 30},
|
||||
],
|
||||
series=[
|
||||
ChartSeries(dataKey="lost", label="Lost"),
|
||||
ChartSeries(dataKey="found", label="Found"),
|
||||
],
|
||||
x_axis="month",
|
||||
height=200,
|
||||
bar_radius=4,
|
||||
show_legend=True,
|
||||
show_tooltip=True,
|
||||
show_grid=True,
|
||||
)
|
||||
|
||||
with Grid(columns=2, gap=4):
|
||||
with Column(gap=4):
|
||||
with Card():
|
||||
with CardContent():
|
||||
with Column(gap=2):
|
||||
Checkbox(label="Towel packed", value=True)
|
||||
Checkbox(label="Guide charged", value=True)
|
||||
Checkbox(
|
||||
label="Babel fish inserted",
|
||||
value=False,
|
||||
)
|
||||
with Card():
|
||||
with CardHeader():
|
||||
CardTitle("Marvin's Mood")
|
||||
with CardContent():
|
||||
with Column(gap=3):
|
||||
P("How's life?")
|
||||
with Column(gap=2):
|
||||
Button(
|
||||
"Meh",
|
||||
on_click=ShowToast(
|
||||
"Noted. Enthusiasm levels nominal."
|
||||
),
|
||||
)
|
||||
Button(
|
||||
"Depressed",
|
||||
variant="info",
|
||||
on_click=ShowToast(
|
||||
"I think you ought to "
|
||||
"know I'm feeling very "
|
||||
"depressed."
|
||||
),
|
||||
)
|
||||
Button(
|
||||
"Don't talk to me about life",
|
||||
variant="warning",
|
||||
on_click=ShowToast(
|
||||
"Brain the size of a "
|
||||
"planet and they ask me "
|
||||
"to pick up a piece of "
|
||||
"paper."
|
||||
),
|
||||
)
|
||||
|
||||
with Column(gap=4):
|
||||
with Card():
|
||||
with CardContent():
|
||||
with Row(gap=2, align="center"):
|
||||
Loader(variant="dots", size="sm")
|
||||
Muted("Marvin is thinking...")
|
||||
with Card():
|
||||
with CardContent():
|
||||
DataTable(
|
||||
columns=[
|
||||
DataTableColumn(
|
||||
key="crew",
|
||||
header="Crew",
|
||||
sortable=True,
|
||||
),
|
||||
DataTableColumn(
|
||||
key="species",
|
||||
header="Species",
|
||||
sortable=True,
|
||||
),
|
||||
DataTableColumn(
|
||||
key="towel",
|
||||
header="Towel?",
|
||||
sortable=True,
|
||||
),
|
||||
DataTableColumn(
|
||||
key="status",
|
||||
header="Status",
|
||||
sortable=True,
|
||||
),
|
||||
],
|
||||
rows=[
|
||||
{
|
||||
"crew": "Arthur Dent",
|
||||
"species": "Human",
|
||||
"towel": "Yes",
|
||||
"status": "Confused",
|
||||
},
|
||||
{
|
||||
"crew": "Ford Prefect",
|
||||
"species": "Betelgeusian",
|
||||
"towel": "Always",
|
||||
"status": "Drinking",
|
||||
},
|
||||
{
|
||||
"crew": "Zaphod",
|
||||
"species": "Betelgeusian",
|
||||
"towel": "Lost it",
|
||||
"status": "Presidential",
|
||||
},
|
||||
{
|
||||
"crew": "Trillian",
|
||||
"species": "Human",
|
||||
"towel": "Yes",
|
||||
"status": "Navigating",
|
||||
},
|
||||
{
|
||||
"crew": "Marvin",
|
||||
"species": "Android",
|
||||
"towel": "No point",
|
||||
"status": "Depressed",
|
||||
},
|
||||
{
|
||||
"crew": "Slartibartfast",
|
||||
"species": "Magrathean",
|
||||
"towel": "Somewhere",
|
||||
"status": "Designing",
|
||||
},
|
||||
],
|
||||
search=True,
|
||||
paginated=False,
|
||||
)
|
||||
60
docs/apps/demos/pie-chart.html
Normal file
60
docs/apps/demos/pie-chart.html
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Prefab</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" crossorigin href="https://cdn.jsdelivr.net/npm/@prefecthq/prefab-ui@0.19.0/dist/app/renderer.css">
|
||||
<script type="module" crossorigin src="https://cdn.jsdelivr.net/npm/@prefecthq/prefab-ui@0.19.0/dist/app/renderer.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root" style="max-width:64rem;margin:0 auto;padding:2rem"></div>
|
||||
<script id="prefab:initial-data" type="application/json">{
|
||||
"$prefab": {
|
||||
"version": "0.2"
|
||||
},
|
||||
"view": {
|
||||
"cssClass": "pf-app-root",
|
||||
"type": "Div",
|
||||
"children": [
|
||||
{
|
||||
"cssClass": "p-6",
|
||||
"type": "Column",
|
||||
"children": [
|
||||
{
|
||||
"type": "PieChart",
|
||||
"data": [
|
||||
{
|
||||
"category": "Bug",
|
||||
"count": 42
|
||||
},
|
||||
{
|
||||
"category": "Feature",
|
||||
"count": 28
|
||||
},
|
||||
{
|
||||
"category": "Docs",
|
||||
"count": 15
|
||||
},
|
||||
{
|
||||
"category": "Infra",
|
||||
"count": 10
|
||||
}
|
||||
],
|
||||
"dataKey": "count",
|
||||
"nameKey": "category",
|
||||
"height": 240,
|
||||
"innerRadius": 50,
|
||||
"showLabel": false,
|
||||
"paddingAngle": 0,
|
||||
"showLegend": true,
|
||||
"showTooltip": true,
|
||||
"animate": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}</script>
|
||||
</body>
|
||||
</html>
|
||||
21
docs/apps/demos/pie-chart.py
Normal file
21
docs/apps/demos/pie-chart.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
from prefab_ui.app import PrefabApp
|
||||
from prefab_ui.components import Column
|
||||
from prefab_ui.components.charts import PieChart
|
||||
|
||||
data = [
|
||||
{"category": "Bug", "count": 42},
|
||||
{"category": "Feature", "count": 28},
|
||||
{"category": "Docs", "count": 15},
|
||||
{"category": "Infra", "count": 10},
|
||||
]
|
||||
|
||||
with PrefabApp() as app:
|
||||
with Column(css_class="p-6"):
|
||||
PieChart(
|
||||
data=data,
|
||||
data_key="count",
|
||||
name_key="category",
|
||||
inner_radius=50,
|
||||
show_legend=True,
|
||||
height=240,
|
||||
)
|
||||
167
docs/apps/demos/reactive.html
Normal file
167
docs/apps/demos/reactive.html
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Prefab</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" crossorigin href="https://cdn.jsdelivr.net/npm/@prefecthq/prefab-ui@0.19.0/dist/app/renderer.css">
|
||||
<script type="module" crossorigin src="https://cdn.jsdelivr.net/npm/@prefecthq/prefab-ui@0.19.0/dist/app/renderer.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root" style="max-width:64rem;margin:0 auto;padding:2rem"></div>
|
||||
<script id="prefab:initial-data" type="application/json">{
|
||||
"$prefab": {
|
||||
"version": "0.2"
|
||||
},
|
||||
"view": {
|
||||
"cssClass": "pf-app-root",
|
||||
"type": "Div",
|
||||
"children": [
|
||||
{
|
||||
"cssClass": "gap-4 p-6",
|
||||
"let": {
|
||||
"data": "{{ region == 'south' ? south : region == 'west' ? west : north }}"
|
||||
},
|
||||
"type": "Column",
|
||||
"children": [
|
||||
{
|
||||
"cssClass": "gap-4 items-center",
|
||||
"type": "Row",
|
||||
"children": [
|
||||
{
|
||||
"cssClass": "w-40",
|
||||
"name": "region",
|
||||
"type": "Select",
|
||||
"size": "default",
|
||||
"disabled": false,
|
||||
"required": false,
|
||||
"invalid": false,
|
||||
"children": [
|
||||
{
|
||||
"type": "SelectOption",
|
||||
"value": "north",
|
||||
"label": "North",
|
||||
"selected": false,
|
||||
"disabled": false
|
||||
},
|
||||
{
|
||||
"type": "SelectOption",
|
||||
"value": "south",
|
||||
"label": "South",
|
||||
"selected": false,
|
||||
"disabled": false
|
||||
},
|
||||
{
|
||||
"type": "SelectOption",
|
||||
"value": "west",
|
||||
"label": "West",
|
||||
"selected": false,
|
||||
"disabled": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"cssClass": "ml-auto",
|
||||
"name": "show_target",
|
||||
"value": false,
|
||||
"type": "Switch",
|
||||
"size": "default",
|
||||
"disabled": false,
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"cssClass": "text-sm text-muted-foreground",
|
||||
"content": "Show target",
|
||||
"type": "Text"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "BarChart",
|
||||
"data": "{{ data }}",
|
||||
"series": [
|
||||
{
|
||||
"dataKey": "sales",
|
||||
"label": "Sales"
|
||||
}
|
||||
],
|
||||
"xAxis": "month",
|
||||
"height": 200,
|
||||
"stacked": false,
|
||||
"horizontal": false,
|
||||
"barRadius": 4,
|
||||
"showLegend": true,
|
||||
"showTooltip": true,
|
||||
"animate": true,
|
||||
"showGrid": true,
|
||||
"showYAxis": true,
|
||||
"yAxisFormat": "auto"
|
||||
},
|
||||
{
|
||||
"type": "Condition",
|
||||
"cases": [
|
||||
{
|
||||
"when": "{{ show_target }}",
|
||||
"children": [
|
||||
{
|
||||
"type": "Metric",
|
||||
"label": "Q1 Target",
|
||||
"value": "$75,000"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"state": {
|
||||
"region": "north",
|
||||
"north": [
|
||||
{
|
||||
"month": "Jan",
|
||||
"sales": 22000
|
||||
},
|
||||
{
|
||||
"month": "Feb",
|
||||
"sales": 25500
|
||||
},
|
||||
{
|
||||
"month": "Mar",
|
||||
"sales": 24200
|
||||
}
|
||||
],
|
||||
"south": [
|
||||
{
|
||||
"month": "Jan",
|
||||
"sales": 5800
|
||||
},
|
||||
{
|
||||
"month": "Feb",
|
||||
"sales": 6400
|
||||
},
|
||||
{
|
||||
"month": "Mar",
|
||||
"sales": 5600
|
||||
}
|
||||
],
|
||||
"west": [
|
||||
{
|
||||
"month": "Jan",
|
||||
"sales": 6000
|
||||
},
|
||||
{
|
||||
"month": "Feb",
|
||||
"sales": 6000
|
||||
},
|
||||
{
|
||||
"month": "Mar",
|
||||
"sales": 5600
|
||||
}
|
||||
],
|
||||
"show_target": true
|
||||
}
|
||||
}</script>
|
||||
</body>
|
||||
</html>
|
||||
66
docs/apps/demos/reactive.py
Normal file
66
docs/apps/demos/reactive.py
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
from prefab_ui.app import PrefabApp
|
||||
from prefab_ui.components import (
|
||||
Column,
|
||||
Row,
|
||||
Select,
|
||||
SelectOption,
|
||||
Switch,
|
||||
Text,
|
||||
)
|
||||
from prefab_ui.components.charts import BarChart, ChartSeries
|
||||
from prefab_ui.components.control_flow import If
|
||||
from prefab_ui.components.metric import Metric
|
||||
from prefab_ui.rx import Rx
|
||||
|
||||
region = Rx("region")
|
||||
|
||||
north = [
|
||||
{"month": "Jan", "sales": 22000},
|
||||
{"month": "Feb", "sales": 25500},
|
||||
{"month": "Mar", "sales": 24200},
|
||||
]
|
||||
south = [
|
||||
{"month": "Jan", "sales": 5800},
|
||||
{"month": "Feb", "sales": 6400},
|
||||
{"month": "Mar", "sales": 5600},
|
||||
]
|
||||
west = [
|
||||
{"month": "Jan", "sales": 6000},
|
||||
{"month": "Feb", "sales": 6000},
|
||||
{"month": "Mar", "sales": 5600},
|
||||
]
|
||||
|
||||
with PrefabApp(
|
||||
state={
|
||||
"region": "north",
|
||||
"north": north,
|
||||
"south": south,
|
||||
"west": west,
|
||||
"show_target": True,
|
||||
},
|
||||
) as app:
|
||||
with Column(
|
||||
gap=4,
|
||||
css_class="p-6",
|
||||
let={
|
||||
"data": "{{ region == 'south' ? south : region == 'west' ? west : north }}",
|
||||
},
|
||||
):
|
||||
with Row(gap=4, align="center"):
|
||||
with Select(name="region", css_class="w-40"):
|
||||
SelectOption(value="north", label="North")
|
||||
SelectOption(value="south", label="South")
|
||||
SelectOption(value="west", label="West")
|
||||
Switch(name="show_target", css_class="ml-auto")
|
||||
Text("Show target", css_class="text-sm text-muted-foreground")
|
||||
BarChart(
|
||||
data=Rx("data"),
|
||||
series=[ChartSeries(data_key="sales", label="Sales")],
|
||||
x_axis="month",
|
||||
height=200,
|
||||
)
|
||||
with If(Rx("show_target")):
|
||||
Metric(
|
||||
label="Q1 Target",
|
||||
value="$75,000",
|
||||
)
|
||||
237
docs/apps/demos/team-directory-reactive.html
Normal file
237
docs/apps/demos/team-directory-reactive.html
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Prefab</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" crossorigin href="https://cdn.jsdelivr.net/npm/@prefecthq/prefab-ui@0.19.0/dist/app/renderer.css">
|
||||
<script type="module" crossorigin src="https://cdn.jsdelivr.net/npm/@prefecthq/prefab-ui@0.19.0/dist/app/renderer.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root" style="max-width:64rem;margin:0 auto;padding:2rem"></div>
|
||||
<script id="prefab:initial-data" type="application/json">{
|
||||
"$prefab": {
|
||||
"version": "0.2"
|
||||
},
|
||||
"view": {
|
||||
"cssClass": "pf-app-root",
|
||||
"type": "Div",
|
||||
"children": [
|
||||
{
|
||||
"cssClass": "gap-4 p-6",
|
||||
"type": "Column",
|
||||
"children": [
|
||||
{
|
||||
"cssClass": "gap-4",
|
||||
"type": "Grid",
|
||||
"columnTemplate": "1fr 2fr",
|
||||
"children": [
|
||||
{
|
||||
"type": "PieChart",
|
||||
"data": [
|
||||
{
|
||||
"office": "San Francisco",
|
||||
"count": 3
|
||||
},
|
||||
{
|
||||
"office": "New York",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"office": "London",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"office": "Berlin",
|
||||
"count": 1
|
||||
}
|
||||
],
|
||||
"dataKey": "count",
|
||||
"nameKey": "office",
|
||||
"height": 300,
|
||||
"innerRadius": 0,
|
||||
"showLabel": false,
|
||||
"paddingAngle": 0,
|
||||
"showLegend": true,
|
||||
"showTooltip": true,
|
||||
"animate": true
|
||||
},
|
||||
{
|
||||
"type": "DataTable",
|
||||
"columns": [
|
||||
{
|
||||
"key": "name",
|
||||
"header": "Name",
|
||||
"sortable": true
|
||||
},
|
||||
{
|
||||
"key": "role",
|
||||
"header": "Role",
|
||||
"sortable": true
|
||||
},
|
||||
{
|
||||
"key": "office",
|
||||
"header": "Office",
|
||||
"sortable": true
|
||||
}
|
||||
],
|
||||
"rows": [
|
||||
{
|
||||
"name": "Alice Chen",
|
||||
"role": "Staff Engineer",
|
||||
"office": "San Francisco",
|
||||
"email": "alice@company.com",
|
||||
"projects": 3
|
||||
},
|
||||
{
|
||||
"name": "Bob Martinez",
|
||||
"role": "Lead Designer",
|
||||
"office": "New York",
|
||||
"email": "bob@company.com",
|
||||
"projects": 5
|
||||
},
|
||||
{
|
||||
"name": "Carol Johnson",
|
||||
"role": "Senior Engineer",
|
||||
"office": "London",
|
||||
"email": "carol@company.com",
|
||||
"projects": 2
|
||||
},
|
||||
{
|
||||
"name": "David Kim",
|
||||
"role": "Product Manager",
|
||||
"office": "San Francisco",
|
||||
"email": "david@company.com",
|
||||
"projects": 7
|
||||
},
|
||||
{
|
||||
"name": "Eva Mueller",
|
||||
"role": "Engineer",
|
||||
"office": "Berlin",
|
||||
"email": "eva@company.com",
|
||||
"projects": 1
|
||||
},
|
||||
{
|
||||
"name": "Frank Lee",
|
||||
"role": "Data Scientist",
|
||||
"office": "San Francisco",
|
||||
"email": "frank@company.com",
|
||||
"projects": 4
|
||||
},
|
||||
{
|
||||
"name": "Grace Park",
|
||||
"role": "Engineering Manager",
|
||||
"office": "New York",
|
||||
"email": "grace@company.com",
|
||||
"projects": 6
|
||||
}
|
||||
],
|
||||
"search": true,
|
||||
"paginated": false,
|
||||
"pageSize": 10,
|
||||
"onRowClick": {
|
||||
"action": "setState",
|
||||
"key": "selected",
|
||||
"value": "{{ $event }}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Condition",
|
||||
"cases": [
|
||||
{
|
||||
"when": "{{ selected }}",
|
||||
"children": [
|
||||
{
|
||||
"type": "Card",
|
||||
"children": [
|
||||
{
|
||||
"type": "CardHeader",
|
||||
"children": [
|
||||
{
|
||||
"cssClass": "gap-2 items-center",
|
||||
"type": "Row",
|
||||
"children": [
|
||||
{
|
||||
"content": "{{ selected.name }}",
|
||||
"type": "H3"
|
||||
},
|
||||
{
|
||||
"type": "Badge",
|
||||
"label": "{{ selected.office }}",
|
||||
"variant": "default"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "CardContent",
|
||||
"children": [
|
||||
{
|
||||
"cssClass": "gap-4 grid-cols-3",
|
||||
"type": "Grid",
|
||||
"children": [
|
||||
{
|
||||
"cssClass": "gap-0",
|
||||
"type": "Column",
|
||||
"children": [
|
||||
{
|
||||
"content": "Role",
|
||||
"type": "Small"
|
||||
},
|
||||
{
|
||||
"content": "{{ selected.role }}",
|
||||
"type": "Text"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"cssClass": "gap-0",
|
||||
"type": "Column",
|
||||
"children": [
|
||||
{
|
||||
"content": "Email",
|
||||
"type": "Small"
|
||||
},
|
||||
{
|
||||
"content": "{{ selected.email }}",
|
||||
"type": "Text"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"cssClass": "gap-0",
|
||||
"type": "Column",
|
||||
"children": [
|
||||
{
|
||||
"content": "Active Projects",
|
||||
"type": "Small"
|
||||
},
|
||||
{
|
||||
"content": "{{ selected.projects }}",
|
||||
"type": "Text"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"state": {
|
||||
"selected": null
|
||||
}
|
||||
}</script>
|
||||
</body>
|
||||
</html>
|
||||
116
docs/apps/demos/team-directory-reactive.py
Normal file
116
docs/apps/demos/team-directory-reactive.py
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
from collections import Counter
|
||||
|
||||
from prefab_ui.actions import SetState
|
||||
from prefab_ui.app import PrefabApp
|
||||
from prefab_ui.components import (
|
||||
H3,
|
||||
Badge,
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
Column,
|
||||
DataTable,
|
||||
DataTableColumn,
|
||||
Grid,
|
||||
Row,
|
||||
Small,
|
||||
Text,
|
||||
)
|
||||
from prefab_ui.components.charts import PieChart
|
||||
from prefab_ui.components.control_flow import If
|
||||
from prefab_ui.rx import STATE, Rx
|
||||
|
||||
MEMBERS = [
|
||||
{
|
||||
"name": "Alice Chen",
|
||||
"role": "Staff Engineer",
|
||||
"office": "San Francisco",
|
||||
"email": "alice@company.com",
|
||||
"projects": 3,
|
||||
},
|
||||
{
|
||||
"name": "Bob Martinez",
|
||||
"role": "Lead Designer",
|
||||
"office": "New York",
|
||||
"email": "bob@company.com",
|
||||
"projects": 5,
|
||||
},
|
||||
{
|
||||
"name": "Carol Johnson",
|
||||
"role": "Senior Engineer",
|
||||
"office": "London",
|
||||
"email": "carol@company.com",
|
||||
"projects": 2,
|
||||
},
|
||||
{
|
||||
"name": "David Kim",
|
||||
"role": "Product Manager",
|
||||
"office": "San Francisco",
|
||||
"email": "david@company.com",
|
||||
"projects": 7,
|
||||
},
|
||||
{
|
||||
"name": "Eva Mueller",
|
||||
"role": "Engineer",
|
||||
"office": "Berlin",
|
||||
"email": "eva@company.com",
|
||||
"projects": 1,
|
||||
},
|
||||
{
|
||||
"name": "Frank Lee",
|
||||
"role": "Data Scientist",
|
||||
"office": "San Francisco",
|
||||
"email": "frank@company.com",
|
||||
"projects": 4,
|
||||
},
|
||||
{
|
||||
"name": "Grace Park",
|
||||
"role": "Engineering Manager",
|
||||
"office": "New York",
|
||||
"email": "grace@company.com",
|
||||
"projects": 6,
|
||||
},
|
||||
]
|
||||
|
||||
OFFICE_COUNTS = [
|
||||
{"office": office, "count": count}
|
||||
for office, count in Counter(m["office"] for m in MEMBERS).items()
|
||||
]
|
||||
|
||||
with PrefabApp(state={"selected": None}) as app:
|
||||
with Column(gap=4, css_class="p-6"):
|
||||
with Grid(columns=[1, 2], gap=4):
|
||||
PieChart(
|
||||
data=OFFICE_COUNTS,
|
||||
data_key="count",
|
||||
name_key="office",
|
||||
show_legend=True,
|
||||
)
|
||||
DataTable(
|
||||
columns=[
|
||||
DataTableColumn(key="name", header="Name", sortable=True),
|
||||
DataTableColumn(key="role", header="Role", sortable=True),
|
||||
DataTableColumn(key="office", header="Office", sortable=True),
|
||||
],
|
||||
rows=MEMBERS,
|
||||
search=True,
|
||||
on_row_click=SetState("selected", Rx("$event")),
|
||||
)
|
||||
|
||||
with If(STATE.selected):
|
||||
with Card():
|
||||
with CardHeader():
|
||||
with Row(gap=2, align="center"):
|
||||
H3(Rx("selected.name"))
|
||||
Badge(Rx("selected.office"))
|
||||
with CardContent():
|
||||
with Grid(columns=3, gap=4):
|
||||
with Column(gap=0):
|
||||
Small("Role")
|
||||
Text(Rx("selected.role"))
|
||||
with Column(gap=0):
|
||||
Small("Email")
|
||||
Text(Rx("selected.email"))
|
||||
with Column(gap=0):
|
||||
Small("Active Projects")
|
||||
Text(Rx("selected.projects"))
|
||||
127
docs/apps/demos/team-directory.html
Normal file
127
docs/apps/demos/team-directory.html
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Prefab</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" crossorigin href="https://cdn.jsdelivr.net/npm/@prefecthq/prefab-ui@0.19.0/dist/app/renderer.css">
|
||||
<script type="module" crossorigin src="https://cdn.jsdelivr.net/npm/@prefecthq/prefab-ui@0.19.0/dist/app/renderer.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root" style="max-width:64rem;margin:0 auto;padding:2rem"></div>
|
||||
<script id="prefab:initial-data" type="application/json">{
|
||||
"$prefab": {
|
||||
"version": "0.2"
|
||||
},
|
||||
"view": {
|
||||
"cssClass": "pf-app-root",
|
||||
"type": "Div",
|
||||
"children": [
|
||||
{
|
||||
"cssClass": "gap-4 p-6",
|
||||
"type": "Column",
|
||||
"children": [
|
||||
{
|
||||
"cssClass": "gap-4",
|
||||
"type": "Grid",
|
||||
"columnTemplate": "1fr 2fr",
|
||||
"children": [
|
||||
{
|
||||
"type": "PieChart",
|
||||
"data": [
|
||||
{
|
||||
"office": "San Francisco",
|
||||
"count": 3
|
||||
},
|
||||
{
|
||||
"office": "New York",
|
||||
"count": 2
|
||||
},
|
||||
{
|
||||
"office": "London",
|
||||
"count": 1
|
||||
},
|
||||
{
|
||||
"office": "Berlin",
|
||||
"count": 1
|
||||
}
|
||||
],
|
||||
"dataKey": "count",
|
||||
"nameKey": "office",
|
||||
"height": 300,
|
||||
"innerRadius": 0,
|
||||
"showLabel": false,
|
||||
"paddingAngle": 0,
|
||||
"showLegend": true,
|
||||
"showTooltip": true,
|
||||
"animate": true
|
||||
},
|
||||
{
|
||||
"type": "DataTable",
|
||||
"columns": [
|
||||
{
|
||||
"key": "name",
|
||||
"header": "Name",
|
||||
"sortable": true
|
||||
},
|
||||
{
|
||||
"key": "role",
|
||||
"header": "Role",
|
||||
"sortable": true
|
||||
},
|
||||
{
|
||||
"key": "office",
|
||||
"header": "Office",
|
||||
"sortable": true
|
||||
}
|
||||
],
|
||||
"rows": [
|
||||
{
|
||||
"name": "Alice Chen",
|
||||
"role": "Staff Engineer",
|
||||
"office": "San Francisco"
|
||||
},
|
||||
{
|
||||
"name": "Bob Martinez",
|
||||
"role": "Lead Designer",
|
||||
"office": "New York"
|
||||
},
|
||||
{
|
||||
"name": "Carol Johnson",
|
||||
"role": "Senior Engineer",
|
||||
"office": "London"
|
||||
},
|
||||
{
|
||||
"name": "David Kim",
|
||||
"role": "Product Manager",
|
||||
"office": "San Francisco"
|
||||
},
|
||||
{
|
||||
"name": "Eva Mueller",
|
||||
"role": "Engineer",
|
||||
"office": "Berlin"
|
||||
},
|
||||
{
|
||||
"name": "Frank Lee",
|
||||
"role": "Data Scientist",
|
||||
"office": "San Francisco"
|
||||
},
|
||||
{
|
||||
"name": "Grace Park",
|
||||
"role": "Engineering Manager",
|
||||
"office": "New York"
|
||||
}
|
||||
],
|
||||
"search": true,
|
||||
"paginated": false,
|
||||
"pageSize": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}</script>
|
||||
</body>
|
||||
</html>
|
||||
39
docs/apps/demos/team-directory.py
Normal file
39
docs/apps/demos/team-directory.py
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
from collections import Counter
|
||||
|
||||
from prefab_ui.app import PrefabApp
|
||||
from prefab_ui.components import Column, DataTable, DataTableColumn, Grid
|
||||
from prefab_ui.components.charts import PieChart
|
||||
|
||||
members = [
|
||||
{"name": "Alice Chen", "role": "Staff Engineer", "office": "San Francisco"},
|
||||
{"name": "Bob Martinez", "role": "Lead Designer", "office": "New York"},
|
||||
{"name": "Carol Johnson", "role": "Senior Engineer", "office": "London"},
|
||||
{"name": "David Kim", "role": "Product Manager", "office": "San Francisco"},
|
||||
{"name": "Eva Mueller", "role": "Engineer", "office": "Berlin"},
|
||||
{"name": "Frank Lee", "role": "Data Scientist", "office": "San Francisco"},
|
||||
{"name": "Grace Park", "role": "Engineering Manager", "office": "New York"},
|
||||
]
|
||||
|
||||
office_counts = [
|
||||
{"office": office, "count": count}
|
||||
for office, count in Counter(m["office"] for m in members).items()
|
||||
]
|
||||
|
||||
with PrefabApp() as app:
|
||||
with Column(gap=4, css_class="p-6"):
|
||||
with Grid(columns=[1, 2], gap=4):
|
||||
PieChart(
|
||||
data=office_counts,
|
||||
data_key="count",
|
||||
name_key="office",
|
||||
show_legend=True,
|
||||
)
|
||||
DataTable(
|
||||
columns=[
|
||||
DataTableColumn(key="name", header="Name", sortable=True),
|
||||
DataTableColumn(key="role", header="Role", sortable=True),
|
||||
DataTableColumn(key="office", header="Office", sortable=True),
|
||||
],
|
||||
rows=members,
|
||||
search=True,
|
||||
)
|
||||
|
|
@ -3,7 +3,6 @@ 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'
|
||||
|
|
@ -14,11 +13,11 @@ import { VersionBadge } from '/snippets/version-badge.mdx'
|
|||
<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.
|
||||
`fastmcp dev apps` gives you a browser preview for your app tools without needing an MCP host client. It starts your server and a local dev UI side by side: you pick a tool, fill in its arguments, and the rendered result opens in a new tab.
|
||||
|
||||
This works with both [Prefab apps](/apps/prefab) and [custom HTML apps](/apps/low-level).
|
||||
Works with both [Interactive Tools](/apps/prefab) and [custom HTML apps](/apps/low-level).
|
||||
|
||||
## Quick Start
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
fastmcp dev apps server.py
|
||||
|
|
@ -26,7 +25,7 @@ fastmcp dev apps server.py
|
|||
|
||||
The dev UI opens at `http://localhost:8080`. Your MCP server runs on port 8000 with auto-reload enabled by default — save a file and the server restarts automatically.
|
||||
|
||||
## How It Works
|
||||
## How it works
|
||||
|
||||
The dev server does three things:
|
||||
|
||||
|
|
@ -36,7 +35,7 @@ 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
|
||||
## 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.
|
||||
|
||||
|
|
@ -56,7 +55,7 @@ fastmcp dev apps server.py:mcp --mcp-port 9000 --dev-port 9090 --no-reload
|
|||
| Dev Port | `--dev-port` | `8080` | Port for the dev UI |
|
||||
| Auto-Reload | `--reload` / `--no-reload` | On | Watch files and restart the server on changes |
|
||||
|
||||
## Multiple Tools
|
||||
## Multiple tools
|
||||
|
||||
If your server has multiple app tools, the picker shows a dropdown. Each tool gets its own form and launch button. The tool's `title` is displayed when available, falling back to the tool name.
|
||||
|
||||
|
|
|
|||
|
|
@ -3,14 +3,13 @@ title: Examples
|
|||
sidebarTitle: Examples
|
||||
description: Example apps you can run right now.
|
||||
icon: images
|
||||
tag: NEW
|
||||
---
|
||||
|
||||
import { VersionBadge } from '/snippets/version-badge.mdx'
|
||||
|
||||
<VersionBadge version="3.2.0" />
|
||||
|
||||
Every example below is a working FastMCP server you can run with `fastmcp dev apps` or connect to from any MCP host. The source is in `examples/apps/` in the repository.
|
||||
Each tile below is a working FastMCP server you can run with `fastmcp dev apps` or connect to from any MCP host. Source lives in `examples/apps/` in the repository.
|
||||
|
||||
<Columns cols={2}>
|
||||
<Tile href="#sales-dashboard" title="Sales Dashboard" description="Metrics, charts, and deal pipeline">
|
||||
|
|
@ -44,7 +43,7 @@ Every example below is a working FastMCP server you can run with `fastmcp dev ap
|
|||
</Tile>
|
||||
</Columns>
|
||||
|
||||
## Running Examples
|
||||
## Running the examples
|
||||
|
||||
Preview any example in your browser with the dev server:
|
||||
|
||||
|
|
@ -53,11 +52,11 @@ pip install "fastmcp[apps]"
|
|||
fastmcp dev apps examples/apps/sales_dashboard/sales_dashboard_server.py
|
||||
```
|
||||
|
||||
The dev server opens an interactive browser UI where you can select a tool and provide arguments. In a real deployment, the LLM provides these arguments on the fly based on the conversation. For example, the quiz example works best when connected to an MCP host like Goose or Claude Desktop, where the LLM generates the questions itself.
|
||||
The dev UI lets you pick a tool and fill in arguments. In a real deployment the LLM provides those arguments from conversation context — the quiz example especially shines when connected to a host like Goose or Claude Desktop, where the LLM generates the questions itself.
|
||||
|
||||
## Standalone Examples
|
||||
## Standalone apps
|
||||
|
||||
### Sales Dashboard
|
||||
### Sales dashboard
|
||||
|
||||
A full dashboard with KPI metrics, revenue trends, segment breakdown, and a deal pipeline table. Shows what you can build with a single `app=True` tool and Prefab's chart and data components.
|
||||
|
||||
|
|
@ -65,9 +64,9 @@ A full dashboard with KPI metrics, revenue trends, segment breakdown, and a deal
|
|||
fastmcp dev apps examples/apps/sales_dashboard/sales_dashboard_server.py
|
||||
```
|
||||
|
||||
### System Monitor
|
||||
### System monitor
|
||||
|
||||
Reads live CPU, memory, and disk stats from your machine using `psutil`. Auto-refreshes via `SetInterval` calling a backend tool, with a dropdown to control the refresh rate. The chart accumulates 100 data points over time.
|
||||
Reads live CPU, memory, and disk stats from your machine using `psutil`. Auto-refreshes via `SetInterval` calling a backend tool, with a dropdown to control the refresh rate. The chart accumulates up to 100 data points over time.
|
||||
|
||||
```bash
|
||||
pip install psutil
|
||||
|
|
@ -82,59 +81,12 @@ The LLM generates trivia questions and passes them to the tool. The user answers
|
|||
fastmcp dev apps examples/apps/quiz/quiz_server.py
|
||||
```
|
||||
|
||||
### Interactive Map
|
||||
### Interactive map
|
||||
|
||||
Accepts addresses or place names, geocodes them via OpenStreetMap Nominatim (free, no API key), and renders an interactive Leaflet map using Prefab's `Embed` component with inline HTML. Proves that Prefab apps aren't limited to built-in components.
|
||||
Accepts addresses or place names, geocodes them via OpenStreetMap Nominatim (free, no API key), and renders an interactive Leaflet map using Prefab's `Embed` component with inline HTML. A reminder that Prefab apps can break out of built-in components when they need to.
|
||||
|
||||
```bash
|
||||
fastmcp dev apps examples/apps/map/map_server.py
|
||||
```
|
||||
|
||||
## Built-in Providers
|
||||
|
||||
These are ready-made capabilities you add with a single `add_provider()` call.
|
||||
|
||||
### [File Upload](/apps/providers/file-upload)
|
||||
|
||||
Drag-and-drop file upload. The user drops files, clicks Upload, and the server stores them. The LLM can list and read uploaded files through model-visible tools.
|
||||
|
||||
```python
|
||||
from fastmcp.apps.file_upload import FileUpload
|
||||
mcp.add_provider(FileUpload())
|
||||
```
|
||||
|
||||
### [Approval](/apps/providers/approval)
|
||||
|
||||
Human-in-the-loop confirmation. The LLM presents what it's about to do, the user clicks Approve or Reject, and the decision flows back as a message.
|
||||
|
||||
```python
|
||||
from fastmcp.apps.approval import Approval
|
||||
mcp.add_provider(Approval())
|
||||
```
|
||||
|
||||
### [Choice](/apps/providers/choice)
|
||||
|
||||
Present clickable options instead of asking users to type. Clean structured input without parsing free text.
|
||||
|
||||
```python
|
||||
from fastmcp.apps.choice import Choice
|
||||
mcp.add_provider(Choice())
|
||||
```
|
||||
|
||||
### [Form Input](/apps/providers/form)
|
||||
|
||||
Generate a validated form from a Pydantic model. Submission is validated against the model before being returned.
|
||||
|
||||
```python
|
||||
from fastmcp.apps.form import FormInput
|
||||
mcp.add_provider(FormInput(model=MyModel))
|
||||
```
|
||||
|
||||
### [Generative UI](/apps/providers/generative)
|
||||
|
||||
The LLM writes Prefab Python code at runtime and the result renders as a streaming interactive UI. Tailored visualizations for any data. See the [full guide](/apps/generative) for details.
|
||||
|
||||
```python
|
||||
from fastmcp.apps.generative import GenerativeUI
|
||||
mcp.add_provider(GenerativeUI())
|
||||
```
|
||||
For ready-made building blocks like approvals, choice pickers, file uploads, and Pydantic forms, see the [Providers](/apps/providers/approval) group.
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@ import { VersionBadge } from '/snippets/version-badge.mdx'
|
|||
|
||||
<VersionBadge version="3.2.0" />
|
||||
|
||||
Generative UI means the LLM writes the UI code at runtime. Instead of calling a pre-built tool with a fixed interface, the model writes Prefab Python code tailored to the current data and request. The user watches the UI build up in real time as the model generates code.
|
||||
<video src="/apps/images/generative-ui.mp4" autoPlay loop muted playsInline style={{width:"100%", borderRadius:"8px", marginBottom:"1rem"}} />
|
||||
|
||||
With Generative UI, the LLM writes the UI code at runtime. Instead of calling a pre-built tool with a fixed shape, the model writes Prefab Python tailored to the current data and request. The user watches the UI stream in as the model generates it.
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
|
|
@ -20,15 +22,15 @@ mcp = FastMCP("Prefab Studio")
|
|||
mcp.add_provider(GenerativeUI())
|
||||
```
|
||||
|
||||
That's it. The `GenerativeUI` provider registers everything:
|
||||
One provider registers three things:
|
||||
|
||||
- **`generate_prefab_ui`** — a tool that accepts Python code, executes it in a Pyodide sandbox, and renders the result as a Prefab app
|
||||
- **`search_prefab_components`** — a tool that lets the LLM search the Prefab component library to discover what's available
|
||||
- **The generative renderer** — a `ui://` resource with browser-side Pyodide for streaming progressive rendering
|
||||
- **`search_prefab_components`** — a tool the LLM uses to discover what components are available
|
||||
- **The streaming renderer** — a `ui://` resource with browser-side Pyodide that progressively renders partial code as the LLM generates it
|
||||
|
||||
## How It Works
|
||||
## How it works
|
||||
|
||||
When the LLM decides to call `generate_prefab_ui`, it writes Prefab Python code into the `code` argument. The MCP Apps protocol creates the renderer iframe in parallel with the tool call, so the app is already running when partial arguments start flowing.
|
||||
When the LLM calls `generate_prefab_ui`, it writes Prefab Python code into the `code` argument. The MCP Apps protocol creates the renderer iframe in parallel with the tool call, so the app is already running by the time partial arguments start flowing.
|
||||
|
||||
As the LLM generates each token:
|
||||
|
||||
|
|
@ -37,11 +39,11 @@ As the LLM generates each token:
|
|||
3. Browser-side Pyodide executes whatever compiles successfully
|
||||
4. The user sees components appear as they're written
|
||||
|
||||
When the LLM finishes, the server runs the complete code in a server-side Pyodide sandbox for validation, and the renderer replaces the streaming preview with the final server-validated result.
|
||||
When the LLM finishes, the server runs the complete code in a server-side Pyodide sandbox for validation, and the renderer swaps the streaming preview for the final server-validated result.
|
||||
|
||||
## What the LLM Writes
|
||||
## What the LLM writes
|
||||
|
||||
The tool description includes code examples that teach the LLM the Prefab patterns. A typical generation looks like:
|
||||
The tool description includes examples that teach the model the Prefab patterns. A typical generation looks like:
|
||||
|
||||
```python
|
||||
from prefab_ui.components import Column, Row, Heading, Text, Badge, Card, CardContent
|
||||
|
|
@ -73,9 +75,9 @@ with PrefabApp() as app:
|
|||
Badge("+18%", variant="success")
|
||||
```
|
||||
|
||||
The model writes real Python — loops, f-strings, computation, helper functions. Prefab's component library gives it charts, tables, forms, cards, badges, and layout primitives to work with.
|
||||
The model writes real Python — loops, f-strings, computation, helper functions. Prefab gives it charts, tables, forms, cards, badges, and layout primitives to compose.
|
||||
|
||||
## The Component Search Tool
|
||||
## The component search tool
|
||||
|
||||
Before writing code, the LLM can call `search_prefab_components` to discover what's available:
|
||||
|
||||
|
|
@ -87,11 +89,11 @@ search_prefab_components("Chart")
|
|||
...
|
||||
```
|
||||
|
||||
Passing `detail=True` returns full field descriptions and docstrings. The search tool introspects the actual Prefab classes at runtime, so it's always up to date with the installed version.
|
||||
Passing `detail=True` returns full field descriptions and docstrings. The search tool introspects Prefab classes at runtime, so it's always up to date with the installed version.
|
||||
|
||||
## Passing Data
|
||||
## Passing data
|
||||
|
||||
The `generate_prefab_ui` tool accepts a `data` parameter. Values passed here become global variables in the sandbox:
|
||||
The `generate_prefab_ui` tool accepts a `data` parameter. Values become global variables in the sandbox:
|
||||
|
||||
```python
|
||||
# The LLM can reference 'sales_data' directly in its code
|
||||
|
|
@ -101,11 +103,11 @@ result = await generate_prefab_ui(
|
|||
)
|
||||
```
|
||||
|
||||
This lets the model use real data from earlier in the conversation to build visualizations.
|
||||
This lets the model use data from earlier in the conversation to build visualizations.
|
||||
|
||||
## Configuration
|
||||
|
||||
`GenerativeUI` accepts options for customizing tool names:
|
||||
`GenerativeUI` takes options for customizing tool names:
|
||||
|
||||
```python
|
||||
GenerativeUI(
|
||||
|
|
@ -117,17 +119,16 @@ GenerativeUI(
|
|||
|
||||
## Requirements
|
||||
|
||||
Generative UI requires `fastmcp[apps]` which installs `prefab-ui`. The Pyodide sandbox (for server-side validation) requires Deno — it installs automatically on first use.
|
||||
Generative UI needs `fastmcp[apps]`, which pulls in `prefab-ui`. The server-side Pyodide sandbox (for final validation) requires Deno — it installs automatically on first use.
|
||||
|
||||
The streaming renderer loads Pyodide from CDN in the browser. The CSP is configured automatically by the provider — no manual setup needed.
|
||||
The streaming renderer loads Pyodide from CDN in the browser. The CSP is configured automatically by the provider — no manual setup.
|
||||
|
||||
## Sandbox Limitations
|
||||
## Sandbox limitations
|
||||
|
||||
The Pyodide sandbox includes the Python standard library and Prefab. External packages (NumPy, pandas, requests, etc.) are **not available** — the LLM's code must work with only built-in Python and Prefab components. If the LLM tries to import an unavailable package, the sandbox will raise an `ImportError`.
|
||||
The Pyodide sandbox includes the Python standard library and Prefab. External packages (NumPy, pandas, requests, etc.) are **not available** — the LLM's code must work with only built-in Python and Prefab. If the LLM imports something unavailable, the sandbox raises `ImportError`.
|
||||
|
||||
## Next Steps
|
||||
## Next steps
|
||||
|
||||
- **[GenerativeUI Provider Reference](/apps/providers/generative)** — Configuration options and quick setup
|
||||
- **[Prefab UI](/apps/prefab)** — The component library and state system the LLM writes code against
|
||||
- **[Prefab Component Reference](https://prefab.prefect.io/docs/components)** — Full component library documentation
|
||||
- **[Development](/apps/development)** — Preview generative UI tools locally with `fastmcp dev apps`
|
||||
- **[Interactive Tools](/apps/prefab)** — the component building blocks the LLM will use
|
||||
- **[Prefab component reference](https://prefab.prefect.io/docs/components)** — full component library
|
||||
- **[Development](/apps/development)** — preview generative tools locally with `fastmcp dev apps`
|
||||
|
|
|
|||
BIN
docs/apps/images/generative-ui.mp4
Normal file
BIN
docs/apps/images/generative-ui.mp4
Normal file
Binary file not shown.
|
|
@ -1,45 +1,40 @@
|
|||
---
|
||||
title: FastMCPApp
|
||||
sidebarTitle: FastMCPApp
|
||||
description: Managed tool binding, visibility, and composition for apps with heavy server interaction.
|
||||
description: Wire an interactive UI to backend tools with managed visibility and composition safety.
|
||||
icon: puzzle-piece
|
||||
tag: NEW
|
||||
---
|
||||
|
||||
import { VersionBadge } from '/snippets/version-badge.mdx'
|
||||
import PrefabPinWarning from '/snippets/prefab-pin-warning.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>
|
||||
<PrefabPinWarning />
|
||||
|
||||
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?
|
||||
<iframe src="/apps/demos/contacts.html" style={{width:"100%", height:"650px", border:"none", overflow:"hidden", borderRadius:"8px"}} frameBorder="0" scrolling="no"></iframe>
|
||||
|
||||
`FastMCPApp` is a class that solves these problems. It gives you two decorators that work together:
|
||||
Search a list, fill out a form, click save, the list updates. That pattern — UI that reads and writes data on the server — needs two things: backend tools that actually do the work, and a way to call them from the UI. `FastMCPApp` handles the wiring.
|
||||
|
||||
- **`@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.
|
||||
You'll build up to the contacts app above by the end of this page. Let's start with something smaller.
|
||||
|
||||
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.
|
||||
## A minimal interactive app
|
||||
|
||||
## Your First Interactive App
|
||||
|
||||
Here's a minimal app with a form that saves data:
|
||||
The smallest interactive app: a form that saves a note, and a list that updates when the user submits.
|
||||
|
||||
```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,
|
||||
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] = []
|
||||
|
||||
|
||||
|
|
@ -83,13 +78,23 @@ def notes_app() -> PrefabApp:
|
|||
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.
|
||||
The model sees one tool: `notes_app`. Calling it opens the UI. When the user submits the form, `CallTool("add_note")` fires, the server saves the note, returns the updated list, and `SetState("notes", RESULT)` writes that list back into state. `ForEach("notes")` re-renders. The model never sees `add_note` — it's UI-only.
|
||||
|
||||
Let's break down the key concepts.
|
||||
## Why not just `@mcp.tool(app=True)`?
|
||||
|
||||
## Entry Points: @app.ui()
|
||||
A fair question. Any [Interactive Tool](/apps/prefab) can call a server tool — there's nothing stopping you from putting `CallTool("add_note")` inside a regular `@mcp.tool(app=True)`. It works for one or two tools. Things get harder once the app grows:
|
||||
|
||||
Entry points are what the model sees and calls to open your app. They return a Prefab UI, just like display tools:
|
||||
- Which tools should the model see, and which are UI-only?
|
||||
- What happens to `CallTool("add_note")` when you mount this server under a namespace and the tool becomes `notes_add_note`?
|
||||
- How do you keep it all wired correctly as you compose servers?
|
||||
|
||||
`FastMCPApp` owns these concerns. Entry points register as model-visible. Backend tools register as UI-only by default. Backend tools get globally stable identifiers that survive namespacing, and `CallTool` accepts function references, so references stay valid when you compose servers.
|
||||
|
||||
The rest of this page covers each piece in turn.
|
||||
|
||||
## `@app.ui()` — entry points
|
||||
|
||||
Entry points are what the model sees. They return a `PrefabApp` and default to `visibility=["model"]`, showing up in the LLM tool list but not callable from within the UI.
|
||||
|
||||
```python
|
||||
@app.ui()
|
||||
|
|
@ -97,21 +102,15 @@ 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`.
|
||||
`@app.ui()` supports 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:
|
||||
...
|
||||
```
|
||||
## `@app.tool()` — backend tools
|
||||
|
||||
## Backend Tools: @app.tool()
|
||||
|
||||
Backend tools do the work. The UI calls them via `CallTool`; they run on the server and return data:
|
||||
Backend tools do the work. By default they're visible only to the UI (`visibility=["app"]`), not the model.
|
||||
|
||||
```python
|
||||
@app.tool()
|
||||
|
|
@ -121,7 +120,7 @@ def save_contact(name: str, email: str) -> list[dict]:
|
|||
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`:
|
||||
If you want a tool callable by both the model and the UI, pass `model=True`:
|
||||
|
||||
```python
|
||||
@app.tool(model=True)
|
||||
|
|
@ -130,37 +129,32 @@ def list_contacts() -> list[dict]:
|
|||
return list(db)
|
||||
```
|
||||
|
||||
Backend tools support `name`, `description`, `auth`, and `timeout`:
|
||||
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]:
|
||||
...
|
||||
```
|
||||
## `CallTool` — UI → backend
|
||||
|
||||
## 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()`:
|
||||
`CallTool` is how the UI invokes a backend tool. Pass the tool's name (or a direct function reference):
|
||||
|
||||
```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
|
||||
# Or a function reference — resolves to a stable global key
|
||||
CallTool(save_contact, arguments={...})
|
||||
```
|
||||
|
||||
Arguments can reference state with `Rx`:
|
||||
|
||||
```python
|
||||
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.
|
||||
### Handling results
|
||||
|
||||
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:
|
||||
Server calls are async. Use `on_success` and `on_error` callbacks:
|
||||
|
||||
```python
|
||||
from prefab_ui.actions import SetState, ShowToast
|
||||
|
|
@ -176,78 +170,54 @@ CallTool(
|
|||
)
|
||||
```
|
||||
|
||||
`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`.
|
||||
`RESULT` is a reactive reference to the tool's return value, available inside `on_success`. `ERROR` (from `prefab_ui.rx`) is the counterpart inside `on_error`. Callbacks can be a single action or a list; they execute in order and short-circuit 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
|
||||
|
||||
### 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)`:
|
||||
When a tool's return value should replace a state key, use `result_key`:
|
||||
|
||||
```python
|
||||
CallTool("list_contacts", result_key="contacts")
|
||||
|
||||
# equivalent to:
|
||||
CallTool(
|
||||
"list_contacts",
|
||||
on_success=SetState("contacts", RESULT),
|
||||
)
|
||||
# same as:
|
||||
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`.
|
||||
`CallTool` is one of several actions. Actions attach to handlers like `on_click`, `on_submit`, and `on_change`.
|
||||
|
||||
### Client Actions
|
||||
|
||||
These run instantly in the browser — no server round-trip:
|
||||
Client-side actions 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:
|
||||
Pass a list to chain actions:
|
||||
|
||||
```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"),
|
||||
ShowToast("Cleared"),
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
### Loading States
|
||||
### Loading states
|
||||
|
||||
A common pattern: show a loading indicator while a server call is in flight.
|
||||
A common pattern: disable a button and show a spinner while a call is in flight.
|
||||
|
||||
```python
|
||||
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, Rx
|
||||
from prefab_ui.rx import Rx
|
||||
|
||||
saving = Rx("saving")
|
||||
|
||||
|
|
@ -271,21 +241,17 @@ Button(
|
|||
],
|
||||
)
|
||||
|
||||
# Pass state={"saving": False} to PrefabApp when returning
|
||||
# PrefabApp(view=view, state={"saving": False, ...})
|
||||
```
|
||||
|
||||
## 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.
|
||||
Forms collect input and submit it to a tool. When submitted, named input values become the tool's arguments.
|
||||
|
||||
### Manual Forms
|
||||
|
||||
Build forms with individual input components:
|
||||
### Manual forms
|
||||
|
||||
```python
|
||||
from prefab_ui.components import Form, Input, Select, SelectOption, Textarea, Button
|
||||
from prefab_ui.actions.mcp import CallTool
|
||||
from prefab_ui.actions import ShowToast
|
||||
|
||||
with Form(
|
||||
on_submit=CallTool(
|
||||
|
|
@ -298,26 +264,19 @@ with Form(
|
|||
SelectOption("Low", value="low")
|
||||
SelectOption("Medium", value="medium")
|
||||
SelectOption("High", value="high")
|
||||
SelectOption("Critical", value="critical")
|
||||
Textarea(name="description", label="Description")
|
||||
Button("Create Ticket")
|
||||
```
|
||||
|
||||
When submitted, the CallTool receives `{"title": "...", "priority": "...", "description": "..."}` as arguments to `create_ticket`.
|
||||
On submit, `CallTool` receives `{"title": ..., "priority": ..., "description": ...}`.
|
||||
|
||||
### Pydantic Model Forms
|
||||
### Forms from Pydantic models
|
||||
|
||||
For structured data, `Form.from_model()` generates the entire form from a Pydantic model — inputs, labels, and submit wiring:
|
||||
For structured input, `Form.from_model()` generates the whole form — inputs, labels, validation:
|
||||
|
||||
```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")
|
||||
|
|
@ -329,7 +288,6 @@ class BugReport(BaseModel):
|
|||
|
||||
@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(
|
||||
|
|
@ -337,7 +295,6 @@ def report_bug() -> PrefabApp:
|
|||
on_submit=CallTool(
|
||||
"create_bug",
|
||||
on_success=ShowToast("Bug filed!", variant="success"),
|
||||
on_error=ShowToast("Failed to submit", variant="error"),
|
||||
),
|
||||
)
|
||||
return PrefabApp(view=view)
|
||||
|
|
@ -345,69 +302,47 @@ def report_bug() -> PrefabApp:
|
|||
|
||||
@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.
|
||||
`str` becomes a text input, `Literal` becomes a select, `bool` becomes a checkbox. Field titles and defaults are respected.
|
||||
|
||||
## Composition and Namespacing
|
||||
## 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.
|
||||
The reason `FastMCPApp` exists — and why you'd pick it over plain `@mcp.tool(app=True)` with string-based `CallTool` — 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.
|
||||
`CallTool("save_contact")` would now be broken. But `CallTool(save_contact)` with a function reference resolves to a globally stable identifier that bypasses the namespace. Your app works the same whether standalone or mounted.
|
||||
|
||||
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
|
||||
### Mounting
|
||||
|
||||
`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
|
||||
# or
|
||||
mcp = FastMCP("Platform")
|
||||
mcp.add_provider(app)
|
||||
```
|
||||
|
||||
Multiple apps can coexist on the same server:
|
||||
Multiple apps can coexist; each gets its own global keys, so there's no collision even if two apps have a tool named `save`.
|
||||
|
||||
```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
|
||||
|
||||
### Running Standalone
|
||||
|
||||
For development, `FastMCPApp` has a convenience `run()` method that wraps itself in a temporary `FastMCP` server:
|
||||
For development, `FastMCPApp` has a `run()` shortcut that wraps itself in a temporary `FastMCP` server:
|
||||
|
||||
```python
|
||||
app = FastMCPApp("Contacts")
|
||||
|
|
@ -417,9 +352,9 @@ if __name__ == "__main__":
|
|||
app.run()
|
||||
```
|
||||
|
||||
## Complete Example: Contact Manager
|
||||
## A full example: contact manager
|
||||
|
||||
This pulls together everything — entry points, backend tools, callable references, forms (both manual and Pydantic), state management, and actions:
|
||||
This brings everything together — entry point, backend tools, Pydantic form, manual form, state, actions, and multi-visibility.
|
||||
|
||||
```python expandable
|
||||
from __future__ import annotations
|
||||
|
|
@ -437,8 +372,6 @@ from prefab_ui.rx import RESULT, Rx
|
|||
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"},
|
||||
|
|
@ -451,8 +384,6 @@ class ContactModel(BaseModel):
|
|||
category: Literal["Customer", "Vendor", "Partner", "Other"] = "Other"
|
||||
|
||||
|
||||
# App
|
||||
|
||||
app = FastMCPApp("Contacts")
|
||||
|
||||
|
||||
|
|
@ -528,11 +459,11 @@ if __name__ == "__main__":
|
|||
mcp.run()
|
||||
```
|
||||
|
||||
This example is also available as a runnable server at `examples/apps/contacts/contacts_server.py`.
|
||||
Also available as a runnable server at `examples/apps/contacts/contacts_server.py`.
|
||||
|
||||
## Next Steps
|
||||
## 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
|
||||
- **[Interactive Tools](/apps/prefab)** — the building blocks: charts, tables, dashboards, reactive state
|
||||
- **[Examples](/apps/examples)** — complete working servers
|
||||
- **[Development](/apps/development)** — preview and test app tools locally
|
||||
- **[Prefab UI docs](https://prefab.prefect.io)** — full component reference
|
||||
|
|
|
|||
|
|
@ -3,18 +3,17 @@ title: Custom HTML Apps
|
|||
sidebarTitle: Custom HTML
|
||||
description: Build apps with your own HTML, CSS, and JavaScript using the MCP Apps extension directly.
|
||||
icon: code
|
||||
tag: NEW
|
||||
---
|
||||
|
||||
import { VersionBadge } from '/snippets/version-badge.mdx'
|
||||
|
||||
<VersionBadge version="3.0.0" />
|
||||
|
||||
The [MCP Apps extension](https://modelcontextprotocol.io/docs/extensions/apps) is an open protocol that lets tools return interactive UIs — an HTML page rendered in a sandboxed iframe inside the host client. [Prefab UI](/apps/prefab) builds on this protocol so you never have to think about it, but when you need full control — custom rendering, a specific JavaScript framework, maps, 3D, video — you can use the MCP Apps extension directly.
|
||||
Everything on this page is for when you want full control: your own HTML, your own JavaScript framework, a map library, a 3D viewer, custom video playback. [Interactive Tools](/apps/prefab) wrap the MCP Apps extension so you never have to think about it — this page is what you reach for when you need to think about it.
|
||||
|
||||
This page covers how to write custom HTML apps and wire them up in FastMCP. You'll be working with the [`@modelcontextprotocol/ext-apps`](https://github.com/modelcontextprotocol/ext-apps) JavaScript SDK for host communication, and FastMCP's `AppConfig` for resource and CSP management.
|
||||
You'll be working with two things: the [`@modelcontextprotocol/ext-apps`](https://github.com/modelcontextprotocol/ext-apps) JavaScript SDK for host communication, and FastMCP's `AppConfig` for resources and CSP.
|
||||
|
||||
## How It Works
|
||||
## How it works
|
||||
|
||||
An MCP App has two parts:
|
||||
|
||||
|
|
@ -66,7 +65,7 @@ def my_tool() -> str:
|
|||
return "result"
|
||||
```
|
||||
|
||||
### Tool Visibility
|
||||
### Tool visibility
|
||||
|
||||
The `visibility` field controls where a tool appears:
|
||||
|
||||
|
|
@ -88,7 +87,7 @@ def refresh_data() -> str:
|
|||
return fetch_latest()
|
||||
```
|
||||
|
||||
### AppConfig Fields
|
||||
### AppConfig fields
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
|
|
@ -103,9 +102,9 @@ def refresh_data() -> str:
|
|||
On **resources**, `resource_uri` and `visibility` must not be set — the resource *is* the UI. Use `AppConfig` on resources only for `csp`, `permissions`, and other display settings.
|
||||
</Note>
|
||||
|
||||
## UI Resources
|
||||
## UI resources
|
||||
|
||||
Resources using the `ui://` scheme are automatically served with the MIME type `text/html;profile=mcp-app`. You don't need to set this manually.
|
||||
Resources using the `ui://` scheme are automatically served with the MIME type `text/html;profile=mcp-app`. No need to set it manually.
|
||||
|
||||
```python
|
||||
@mcp.resource("ui://my-app/view.html")
|
||||
|
|
@ -115,7 +114,7 @@ def my_view() -> str:
|
|||
|
||||
The HTML can be anything — a full single-page app, a simple display, or a complex interactive tool. The host renders it in a sandboxed iframe and establishes a `postMessage` channel for communication.
|
||||
|
||||
### Writing the App HTML
|
||||
### Writing the app HTML
|
||||
|
||||
Your HTML app communicates with the host using the [`@modelcontextprotocol/ext-apps`](https://github.com/modelcontextprotocol/ext-apps) JavaScript SDK. The simplest approach is to load it from a CDN:
|
||||
|
||||
|
|
@ -204,7 +203,7 @@ def my_view() -> str:
|
|||
|
||||
Hosts may or may not grant these permissions. Your app should use JavaScript feature detection as a fallback.
|
||||
|
||||
## Example: QR Code Server
|
||||
## Example: a QR code server
|
||||
|
||||
This example creates a tool that generates QR codes and an app that renders them as images. It's based on the [official MCP Apps example](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/qr-server). Requires the `qrcode[pil]` package.
|
||||
|
||||
|
|
@ -286,7 +285,7 @@ def view() -> str:
|
|||
|
||||
The tool generates a QR code as a base64 PNG. The resource loads the MCP Apps JS SDK from unpkg (declared in the CSP), listens for tool results, and renders the image. The host wires them together — when the LLM calls `generate_qr`, the QR code appears in an interactive frame inside the conversation.
|
||||
|
||||
## Checking Client Support
|
||||
## Checking client support
|
||||
|
||||
Not all hosts support the Apps extension. You can check at runtime using the tool's [context](/servers/context):
|
||||
|
||||
|
|
|
|||
|
|
@ -3,179 +3,70 @@ title: Apps
|
|||
sidebarTitle: Overview
|
||||
description: Give your tools interactive UIs rendered directly in the conversation.
|
||||
icon: grid-2
|
||||
tag: NEW
|
||||
mode: center
|
||||
---
|
||||
|
||||
import { VersionBadge } from '/snippets/version-badge.mdx'
|
||||
import PrefabPinWarning from '/snippets/prefab-pin-warning.mdx'
|
||||
|
||||
<VersionBadge version="3.0.0" />
|
||||
|
||||
MCP tools normally return text. That works for answers, but not for data the user wants to *explore* — a revenue chart they can hover over, a sortable employee directory, a form that submits structured input. MCP Apps let your tools return interactive UIs rendered right inside the conversation.
|
||||
A FastMCP app is a tool that returns an interactive UI instead of text. When the host calls it, the user sees a chart, a table, a form, or a whole dashboard rendered right inside the conversation, with working sort, search, tooltips, and state.
|
||||
|
||||
<Frame>
|
||||
<img src="/apps/images/app-showcase.png" alt="A Prefab app showing forms, charts, metrics, progress bars, data tables, and interactive controls — all built in Python" />
|
||||
</Frame>
|
||||
<div style={{
|
||||
margin: '0 clamp(-180px, calc(-18vw + 90px), 0px) 2rem',
|
||||
maxHeight: '700px',
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
maskImage: 'linear-gradient(to bottom, black 75%, transparent)',
|
||||
WebkitMaskImage: 'linear-gradient(to bottom, black 75%, transparent)',
|
||||
}}>
|
||||
<iframe src="/apps/demos/hitchhikers.html" style={{width:"100%", height:"2000px", border:"none", borderRadius:"8px", background:"transparent"}} frameBorder="0" scrolling="no" allowtransparency="true"></iframe>
|
||||
</div>
|
||||
|
||||
FastMCP builds on the [MCP Apps extension](https://modelcontextprotocol.io/docs/extensions/apps) with [Prefab](https://prefab.prefect.io), a Python component library that compiles to interactive UIs. You write Python; the user sees charts, tables, forms, and dashboards.
|
||||
The dashboard above is a [Prefab](https://prefab.prefect.io) showcase — a taste of what you can deliver from a FastMCP tool. Every card, chart, slider, dialog, and carousel is a Python component. Build a composition like this, add `@mcp.tool(app=True)`, and the host renders it inside the conversation.
|
||||
|
||||
<Note>
|
||||
The examples throughout the Apps docs require the `apps` extra:
|
||||
Under the hood, FastMCP builds on the [MCP Apps extension](https://modelcontextprotocol.io/docs/extensions/apps) and uses Prefab to describe UIs in Python.
|
||||
|
||||
```bash
|
||||
pip install "fastmcp[apps]"
|
||||
```
|
||||
|
||||
This installs [Prefab UI](https://prefab.prefect.io), the component library used to build app UIs.
|
||||
</Note>
|
||||
<PrefabPinWarning />
|
||||
|
||||
<Warning>
|
||||
FastMCP pins a **minimum** version of `prefab-ui` for compatibility but intentionally does **not** pin an upper bound. Prefab is a rapidly evolving library with frequent breaking changes. If you are deploying to production, you **must** pin `prefab-ui` to a specific version in your own dependencies. Without a pin, a fresh deploy could pull a newer Prefab version that changes component APIs, breaking your app.
|
||||
</Warning>
|
||||
## Pick your path
|
||||
|
||||
## Which Approach?
|
||||
Four patterns cover almost everything you'd want to build. Most apps start with Interactive Tools; you only reach for the others when you've hit a specific limit.
|
||||
|
||||
Most apps start with **[Prefab Apps](/apps/prefab)** — add `app=True` to a tool and return components. That covers charts, tables, dashboards, and client-side interactivity.
|
||||
### [Interactive Tools](/apps/prefab) — start here
|
||||
|
||||
When your UI needs multiple backend tools with managed visibility and composition safety, use **[FastMCPApp](/apps/interactive-apps)**.
|
||||
|
||||
When you want the LLM to design the UI at runtime, use **[Generative UI](/apps/generative)**.
|
||||
|
||||
When you need your own HTML/JS (maps, 3D, video), use **[Custom HTML](/apps/low-level)**.
|
||||
|
||||
FastMCP also includes ready-made **[app providers](/apps/providers/approval)** that add common capabilities with a single `add_provider()` call.
|
||||
|
||||
## Building Apps
|
||||
|
||||
### Prefab Apps
|
||||
|
||||
<VersionBadge version="3.1.0" />
|
||||
|
||||
The quickest way to give a tool a visual UI. Add `app=True` to any tool and return a Prefab component — when the host calls it, the user sees an interactive UI instead of a JSON blob:
|
||||
Add `app=True` to a tool and return a Prefab component. Charts, tables, dashboards, and client-side interactivity (toggles, tabs, filtering) all work without any server round-trips.
|
||||
|
||||
```python
|
||||
from prefab_ui.app import PrefabApp
|
||||
from prefab_ui.components import Column, Heading
|
||||
from prefab_ui.components.charts import BarChart, ChartSeries
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("Dashboard")
|
||||
|
||||
|
||||
@mcp.tool(app=True)
|
||||
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} Revenue")
|
||||
BarChart(
|
||||
data=data,
|
||||
series=[ChartSeries(data_key="revenue", label="Revenue")],
|
||||
x_axis="quarter",
|
||||
)
|
||||
|
||||
return PrefabApp(view=view)
|
||||
def team_directory() -> DataTable:
|
||||
return DataTable(columns=[...], rows=employees, search=True)
|
||||
```
|
||||
|
||||
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`. There's no hard wall on what a Prefab app can do.
|
||||
### [FastMCPApp](/apps/interactive-apps) — when the UI calls back to the server
|
||||
|
||||
See [Prefab Apps](/apps/prefab) for the full guide.
|
||||
Forms that save data, buttons that trigger backend work, search that hits a database. `FastMCPApp` manages the wiring between UI actions and backend tools, with stable tool identifiers that survive server composition.
|
||||
|
||||
### FastMCPApp
|
||||
### [Generative UI](/apps/generative) — when the LLM writes the UI
|
||||
|
||||
<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 identifiers that survive namespacing, visibility is managed automatically (the model sees entry points, the UI sees backends), and `CallTool` accepts tool names that resolve correctly regardless of how servers are composed:
|
||||
Register one provider and the model can write Prefab code tailored to the current data and request. The user watches the UI build up as the model generates it.
|
||||
|
||||
```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.
|
||||
|
||||
### Generative UI
|
||||
|
||||
<VersionBadge version="3.2.0" />
|
||||
|
||||
Instead of pre-building a UI, the LLM can write one from scratch. The `GenerativeUI` provider registers tools that let the model write Prefab Python code, execute it in a sandbox, and render the result — with streaming so the user watches the UI build up in real time.
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.apps.generative import GenerativeUI
|
||||
|
||||
mcp = FastMCP("Prefab Studio")
|
||||
mcp.add_provider(GenerativeUI())
|
||||
```
|
||||
|
||||
See [Generative UI](/apps/generative) for the full guide, or the [provider reference](/apps/providers/generative) for configuration options.
|
||||
### [Custom HTML](/apps/low-level) — when you need full control
|
||||
|
||||
### Custom HTML
|
||||
Write your own HTML, CSS, and JavaScript. Use a specific framework, drop in a map or 3D viewer, embed video. You're talking to the MCP Apps protocol directly.
|
||||
|
||||
All the 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.
|
||||
## What's next
|
||||
|
||||
## Previewing Apps Locally
|
||||
|
||||
The `fastmcp dev apps` command launches a browser-based preview for your app tools — no MCP host client needed. See [Development](/apps/development).
|
||||
|
||||
```bash
|
||||
fastmcp dev apps server.py
|
||||
```
|
||||
- **[Quickstart](/apps/quickstart)** — build a working app in a minute
|
||||
- **[Examples](/apps/examples)** — complete working servers you can run today
|
||||
- **[Providers](/apps/providers/approval)** — ready-made capabilities (approvals, choice pickers, file upload, forms) you add with one line
|
||||
- **[Development](/apps/development)** — preview app tools locally with `fastmcp dev apps`
|
||||
|
|
|
|||
|
|
@ -1,431 +0,0 @@
|
|||
---
|
||||
title: Patterns
|
||||
sidebarTitle: Patterns
|
||||
description: Copy-paste examples for common tool UIs.
|
||||
icon: grid-2-plus
|
||||
tag: NEW
|
||||
---
|
||||
|
||||
import { VersionBadge } from '/snippets/version-badge.mdx'
|
||||
|
||||
<VersionBadge version="3.1.0" />
|
||||
|
||||
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.
|
||||
|
||||
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 render client-side with tooltips, legends, and responsive sizing.
|
||||
|
||||
### Bar Chart
|
||||
|
||||
```python
|
||||
from prefab_ui.app import PrefabApp
|
||||
from prefab_ui.components import Column, Heading
|
||||
from prefab_ui.components.charts import BarChart, ChartSeries
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("Charts")
|
||||
|
||||
|
||||
@mcp.tool(app=True)
|
||||
def quarterly_revenue(year: int) -> PrefabApp:
|
||||
"""Show quarterly revenue as a bar chart."""
|
||||
data = [
|
||||
{"quarter": "Q1", "revenue": 42000, "costs": 28000},
|
||||
{"quarter": "Q2", "revenue": 51000, "costs": 31000},
|
||||
{"quarter": "Q3", "revenue": 47000, "costs": 29000},
|
||||
{"quarter": "Q4", "revenue": 63000, "costs": 35000},
|
||||
]
|
||||
|
||||
with Column(gap=4, css_class="p-6") as view:
|
||||
Heading(f"{year} Revenue vs Costs")
|
||||
BarChart(
|
||||
data=data,
|
||||
series=[
|
||||
ChartSeries(data_key="revenue", label="Revenue"),
|
||||
ChartSeries(data_key="costs", label="Costs"),
|
||||
],
|
||||
x_axis="quarter",
|
||||
show_legend=True,
|
||||
)
|
||||
|
||||
return PrefabApp(view=view)
|
||||
```
|
||||
|
||||
Multiple `ChartSeries` entries plot different data keys. Add `stacked=True` to stack bars, or `horizontal=True` to flip the axes.
|
||||
|
||||
### Area Chart
|
||||
|
||||
`LineChart` and `AreaChart` share the same API as `BarChart`, with `curve` for interpolation and `show_dots` for data points:
|
||||
|
||||
```python
|
||||
from prefab_ui.app import PrefabApp
|
||||
from prefab_ui.components import Column, Heading
|
||||
from prefab_ui.components.charts import AreaChart, ChartSeries
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("Charts")
|
||||
|
||||
|
||||
@mcp.tool(app=True)
|
||||
def usage_trend() -> PrefabApp:
|
||||
"""Show API usage over time."""
|
||||
data = [
|
||||
{"date": "Feb 1", "requests": 1200},
|
||||
{"date": "Feb 2", "requests": 1350},
|
||||
{"date": "Feb 3", "requests": 980},
|
||||
{"date": "Feb 4", "requests": 1500},
|
||||
{"date": "Feb 5", "requests": 1420},
|
||||
]
|
||||
|
||||
with Column(gap=4, css_class="p-6") as view:
|
||||
Heading("API Usage")
|
||||
AreaChart(
|
||||
data=data,
|
||||
series=[ChartSeries(data_key="requests", label="Requests")],
|
||||
x_axis="date",
|
||||
curve="smooth",
|
||||
height=250,
|
||||
)
|
||||
|
||||
return PrefabApp(view=view)
|
||||
```
|
||||
|
||||
### Pie and Donut Charts
|
||||
|
||||
`PieChart` uses `data_key` (the numeric value) and `name_key` (the label). Set `inner_radius` for a donut:
|
||||
|
||||
```python
|
||||
from prefab_ui.app import PrefabApp
|
||||
from prefab_ui.components import Column, Heading
|
||||
from prefab_ui.components.charts import PieChart
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("Charts")
|
||||
|
||||
|
||||
@mcp.tool(app=True)
|
||||
def ticket_breakdown() -> PrefabApp:
|
||||
"""Show open tickets by category."""
|
||||
data = [
|
||||
{"category": "Bug", "count": 23},
|
||||
{"category": "Feature", "count": 15},
|
||||
{"category": "Docs", "count": 8},
|
||||
{"category": "Infra", "count": 12},
|
||||
]
|
||||
|
||||
with Column(gap=4, css_class="p-6") as view:
|
||||
Heading("Open Tickets")
|
||||
PieChart(
|
||||
data=data,
|
||||
data_key="count",
|
||||
name_key="category",
|
||||
show_legend=True,
|
||||
inner_radius=60,
|
||||
)
|
||||
|
||||
return PrefabApp(view=view)
|
||||
```
|
||||
|
||||
## Data Tables
|
||||
|
||||
[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.app import PrefabApp
|
||||
from prefab_ui.components import Column, Heading, DataTable, DataTableColumn
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("Directory")
|
||||
|
||||
|
||||
@mcp.tool(app=True)
|
||||
def employee_directory() -> PrefabApp:
|
||||
"""Show a searchable, sortable employee directory."""
|
||||
employees = [
|
||||
{"name": "Alice Chen", "department": "Engineering", "role": "Staff Engineer", "location": "SF"},
|
||||
{"name": "Bob Martinez", "department": "Design", "role": "Lead Designer", "location": "NYC"},
|
||||
{"name": "Carol Johnson", "department": "Engineering", "role": "Senior Engineer", "location": "London"},
|
||||
{"name": "David Kim", "department": "Product", "role": "Product Manager", "location": "SF"},
|
||||
{"name": "Eva Müller", "department": "Engineering", "role": "Engineer", "location": "Berlin"},
|
||||
]
|
||||
|
||||
with Column(gap=4, css_class="p-6") as view:
|
||||
Heading("Employee Directory")
|
||||
DataTable(
|
||||
columns=[
|
||||
DataTableColumn(key="name", header="Name", sortable=True),
|
||||
DataTableColumn(key="department", header="Department", sortable=True),
|
||||
DataTableColumn(key="role", header="Role"),
|
||||
DataTableColumn(key="location", header="Office", sortable=True),
|
||||
],
|
||||
rows=employees,
|
||||
search=True,
|
||||
paginated=True,
|
||||
page_size=15,
|
||||
)
|
||||
|
||||
return PrefabApp(view=view)
|
||||
```
|
||||
|
||||
## Status Displays
|
||||
|
||||
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 fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("Monitoring")
|
||||
|
||||
|
||||
@mcp.tool(app=True)
|
||||
def system_status() -> PrefabApp:
|
||||
"""Show current system health."""
|
||||
services = [
|
||||
{"name": "API Gateway", "status": "healthy", "ok": True, "latency_ms": 12, "uptime_pct": 99.9},
|
||||
{"name": "Database", "status": "healthy", "ok": True, "latency_ms": 3, "uptime_pct": 99.99},
|
||||
{"name": "Cache", "status": "degraded", "ok": False, "latency_ms": 45, "uptime_pct": 98.2},
|
||||
{"name": "Queue", "status": "healthy", "ok": True, "latency_ms": 8, "uptime_pct": 99.8},
|
||||
]
|
||||
all_ok = all(s["ok"] for s in services)
|
||||
|
||||
with Column(gap=4, css_class="p-6") as view:
|
||||
with Row(gap=2, align="center"):
|
||||
Heading("System Status")
|
||||
Badge(
|
||||
"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():
|
||||
with CardContent():
|
||||
with Row(gap=2, align="center"):
|
||||
Text(svc["name"], css_class="font-medium")
|
||||
Badge(
|
||||
svc["status"],
|
||||
variant="success" if svc["ok"] else "destructive",
|
||||
)
|
||||
Muted(f"Response: {svc['latency_ms']}ms")
|
||||
Progress(value=svc["uptime_pct"])
|
||||
|
||||
return PrefabApp(view=view)
|
||||
```
|
||||
|
||||
## Reactive Displays
|
||||
|
||||
These patterns use state and `Rx()` for client-side interactivity — no server calls needed.
|
||||
|
||||
### Feature Toggles
|
||||
|
||||
```python
|
||||
from prefab_ui.app import PrefabApp
|
||||
from prefab_ui.components import Column, Heading, Switch, Alert, If, Separator
|
||||
from prefab_ui.rx import Rx
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("Flags")
|
||||
|
||||
|
||||
@mcp.tool(app=True)
|
||||
def feature_flags() -> PrefabApp:
|
||||
"""Toggle feature flags with live preview."""
|
||||
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")
|
||||
Separator()
|
||||
with If(Rx("dark_mode")):
|
||||
Alert(title="Dark mode enabled", description="UI will use dark theme.")
|
||||
with If(Rx("beta")):
|
||||
Alert(
|
||||
title="Beta features active",
|
||||
description="Experimental features are now visible.",
|
||||
variant="warning",
|
||||
)
|
||||
|
||||
return PrefabApp(view=view, state={"dark_mode": False, "beta": False})
|
||||
```
|
||||
|
||||
### 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 fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("Projects")
|
||||
|
||||
|
||||
@mcp.tool(app=True)
|
||||
def project_overview() -> PrefabApp:
|
||||
"""Show project details organized in tabs."""
|
||||
project = {
|
||||
"name": "FastMCP v3",
|
||||
"description": "Next generation MCP framework with Apps support.",
|
||||
"status": "Active",
|
||||
"members": [
|
||||
{"name": "Alice Chen", "role": "Lead"},
|
||||
{"name": "Bob Martinez", "role": "Design"},
|
||||
],
|
||||
"activity": [
|
||||
{"timestamp": "2 hours ago", "message": "Merged PR #342"},
|
||||
{"timestamp": "1 day ago", "message": "Released v3.0.1"},
|
||||
],
|
||||
}
|
||||
|
||||
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"])
|
||||
|
||||
with Tab("Members"):
|
||||
DataTable(
|
||||
columns=[
|
||||
DataTableColumn(key="name", header="Name", sortable=True),
|
||||
DataTableColumn(key="role", header="Role"),
|
||||
],
|
||||
rows=project["members"],
|
||||
)
|
||||
|
||||
with Tab("Activity"):
|
||||
with ForEach("activity") as item:
|
||||
with Row(gap=2):
|
||||
Muted(item.timestamp)
|
||||
Text(item.message)
|
||||
|
||||
return PrefabApp(view=view, state={"activity": project["activity"]})
|
||||
```
|
||||
|
||||
### Accordion
|
||||
|
||||
```python
|
||||
from prefab_ui.app import PrefabApp
|
||||
from prefab_ui.components import (
|
||||
Column, Heading, Row, Text, Badge, Progress,
|
||||
Accordion, AccordionItem,
|
||||
)
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("API Monitor")
|
||||
|
||||
|
||||
@mcp.tool(app=True)
|
||||
def api_health() -> PrefabApp:
|
||||
"""Show health details for each API endpoint."""
|
||||
endpoints = [
|
||||
{"path": "/api/users", "status": 200, "healthy": True, "avg_ms": 45, "p99_ms": 120, "uptime_pct": 99.9},
|
||||
{"path": "/api/orders", "status": 200, "healthy": True, "avg_ms": 82, "p99_ms": 250, "uptime_pct": 99.7},
|
||||
{"path": "/api/search", "status": 200, "healthy": True, "avg_ms": 150, "p99_ms": 500, "uptime_pct": 99.5},
|
||||
{"path": "/api/webhooks", "status": 503, "healthy": False, "avg_ms": 2000, "p99_ms": 5000, "uptime_pct": 95.1},
|
||||
]
|
||||
|
||||
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"]):
|
||||
with Row(gap=4):
|
||||
Badge(
|
||||
f"{ep['status']}",
|
||||
variant="success" if ep["healthy"] else "destructive",
|
||||
)
|
||||
Text(f"Avg: {ep['avg_ms']}ms")
|
||||
Text(f"P99: {ep['p99_ms']}ms")
|
||||
Progress(value=ep["uptime_pct"])
|
||||
|
||||
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, SelectOption, 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)
|
||||
with Select(name="category", label="Category"):
|
||||
SelectOption("Customer", value="Customer")
|
||||
SelectOption("Vendor", value="Vendor")
|
||||
SelectOption("Partner", value="Partner")
|
||||
SelectOption("Other", value="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
|
||||
|
||||
- **[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
|
||||
|
|
@ -1,321 +1,254 @@
|
|||
---
|
||||
title: Prefab UI
|
||||
sidebarTitle: Prefab UI
|
||||
description: The component library behind FastMCP apps — charts, tables, dashboards, forms, and reactive displays.
|
||||
title: Interactive Tools
|
||||
sidebarTitle: Interactive Tools
|
||||
description: Turn your tools into interactive UIs with charts, tables, and dashboards.
|
||||
icon: palette
|
||||
tag: NEW
|
||||
---
|
||||
|
||||
import { VersionBadge } from '/snippets/version-badge.mdx'
|
||||
import PrefabPinWarning from '/snippets/prefab-pin-warning.mdx'
|
||||
|
||||
<VersionBadge version="3.1.0" />
|
||||
|
||||
<Warning>
|
||||
[Prefab](https://prefab.prefect.io) is in early, active development — breaking changes can occur with any release. FastMCP pins a minimum version of `prefab-ui` for compatibility but does not pin an upper bound. If you are deploying to production, **pin `prefab-ui` to a specific version** in your own dependencies.
|
||||
</Warning>
|
||||
<PrefabPinWarning />
|
||||
|
||||
[Prefab UI](https://prefab.prefect.io) is the component library behind all FastMCP app features. You describe layouts, charts, tables, and forms in Python, and Prefab compiles them to interactive UIs that render in the host's conversation.
|
||||
<iframe src="/apps/demos/dashboard.html" style={{width:"100%", height:"680px", border:"none", overflow:"hidden", borderRadius:"8px"}} frameBorder="0" scrolling="no"></iframe>
|
||||
|
||||
The simplest way to use it: add `app=True` to a tool and return Prefab components. The host renders an interactive UI instead of text. This works for everything from static charts to reactive dashboards with client-side state — no server round-trips needed.
|
||||
Believe it or not, that dashboard is a FastMCP tool. The chart has tooltips. The table is sortable. The badges are styled by deal stage. The whole thing is about 40 lines of Python, and the user sees it right inside their conversation instead of a wall of JSON.
|
||||
|
||||
For apps that need server interaction (forms, search, CRUD), see [FastMCPApp](/apps/interactive-apps) which adds managed tool binding on top of Prefab UI. For LLM-generated UIs, see [Generative UI](/apps/generative).
|
||||
The pattern behind every example on this page is the same: add `app=True` to your tool, build a UI with [Prefab](https://prefab.prefect.io) components, and return it as a `PrefabApp`. Prefab has [100+ components](https://prefab.prefect.io/docs/components), from data tables and charts to forms and progress bars. You compose them in Python; the host renders them as a live, interactive application.
|
||||
|
||||
## Getting Started
|
||||
## Start with a table
|
||||
|
||||
Here's a tool that returns a bar chart:
|
||||
Most tools return data the user wants to explore. A `DataTable` is often the smallest useful upgrade — your data goes from a JSON blob to a searchable, sortable table:
|
||||
|
||||
<iframe src="/apps/demos/data-table.html" style={{width:"100%", height:"530px", border:"none", overflow:"hidden", borderRadius:"8px"}} frameBorder="0" scrolling="no"></iframe>
|
||||
|
||||
```python
|
||||
from prefab_ui.app import PrefabApp
|
||||
from prefab_ui.components import Column, Heading
|
||||
from prefab_ui.components.charts import BarChart, ChartSeries
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("Dashboard")
|
||||
|
||||
|
||||
@mcp.tool(app=True)
|
||||
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} Revenue")
|
||||
BarChart(
|
||||
data=data,
|
||||
series=[ChartSeries(data_key="revenue", label="Revenue")],
|
||||
x_axis="quarter",
|
||||
)
|
||||
|
||||
return PrefabApp(view=view)
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
Pass a `state` dict to `PrefabApp` to declare initial state, then use `Rx("key")` to create reactive references:
|
||||
|
||||
```python
|
||||
from prefab_ui.app import PrefabApp
|
||||
from prefab_ui.components import Column, Heading, Switch, Alert, If
|
||||
from prefab_ui.rx import Rx
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("Flags")
|
||||
|
||||
|
||||
@mcp.tool(app=True)
|
||||
def feature_flags() -> PrefabApp:
|
||||
"""Toggle feature flags with live preview."""
|
||||
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(Rx("dark_mode")):
|
||||
Alert(title="Dark mode enabled")
|
||||
with If(Rx("beta")):
|
||||
Alert(title="Beta features active", variant="warning")
|
||||
|
||||
return PrefabApp(view=view, state={"dark_mode": False, "beta": False})
|
||||
```
|
||||
|
||||
Three things to notice here:
|
||||
|
||||
The `state` dict on `PrefabApp` declares the keys and their starting values. `Rx("dark_mode")` creates a reactive reference that compiles to `{{ dark_mode }}` in the wire protocol.
|
||||
|
||||
Interactive components with a `name` prop automatically bind to state. The `Switch(name="dark_mode")` syncs its on/off value to the `dark_mode` state key on every toggle — no event wiring needed.
|
||||
|
||||
`If(Rx("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:
|
||||
|
||||
```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
|
||||
from prefab_ui.components import Column, Text, Slider
|
||||
from prefab_ui.rx import Rx
|
||||
from fastmcp import FastMCP
|
||||
|
||||
mcp = FastMCP("Calculator")
|
||||
|
||||
|
||||
@mcp.tool(app=True)
|
||||
def tip_calculator() -> PrefabApp:
|
||||
"""Calculate tip with a slider."""
|
||||
tip_pct = Rx("tip_pct")
|
||||
bill = Rx("bill")
|
||||
|
||||
tip_amount = tip_pct / 100 * bill
|
||||
total = 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={"bill": 50.00, "tip_pct": 18})
|
||||
```
|
||||
|
||||
`Rx("tip_pct") / 100 * Rx("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
|
||||
from prefab_ui.rx import Rx
|
||||
|
||||
price = Rx("price")
|
||||
ratio = Rx("ratio")
|
||||
name = Rx("name")
|
||||
|
||||
price.currency() # $42.50
|
||||
price.currency("EUR") # EUR format
|
||||
ratio.percent() # 85%
|
||||
name.upper() # ALICE
|
||||
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
|
||||
from prefab_ui.rx import Rx
|
||||
|
||||
connected = Rx("connected")
|
||||
|
||||
Badge(
|
||||
connected.then("Online", "Offline"),
|
||||
variant=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
|
||||
from prefab_ui.components import Column, Heading, ForEach, Row, Text, Badge
|
||||
from prefab_ui.components import DataTable, DataTableColumn
|
||||
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"},
|
||||
def team_directory() -> DataTable:
|
||||
"""Browse the team directory."""
|
||||
employees = [
|
||||
{"name": "Alice Chen", "role": "Staff Engineer", "dept": "Platform"},
|
||||
{"name": "Bob Martinez", "role": "Lead Designer", "dept": "Design"},
|
||||
{"name": "Carol Johnson", "role": "Senior Engineer", "dept": "Platform"},
|
||||
{"name": "David Kim", "role": "Product Manager", "dept": "Product"},
|
||||
{"name": "Eva Mueller", "role": "Engineer", "dept": "Platform"},
|
||||
{"name": "Frank Lee", "role": "Data Scientist", "dept": "ML"},
|
||||
{"name": "Grace Park", "role": "Eng Manager", "dept": "Platform"},
|
||||
]
|
||||
|
||||
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
|
||||
from prefab_ui.components import Column, Select, SelectOption, If, Elif, Else, Text
|
||||
from prefab_ui.rx import Rx
|
||||
|
||||
tier = Rx("tier")
|
||||
|
||||
with Column(gap=4) as view:
|
||||
with Select(name="tier", label="Plan"):
|
||||
SelectOption("Free", value="free")
|
||||
SelectOption("Pro", value="pro")
|
||||
SelectOption("Enterprise", value="enterprise")
|
||||
with If(tier == "enterprise"):
|
||||
Text("Full access to all features")
|
||||
with Elif(tier == "pro"):
|
||||
Text("Advanced features unlocked")
|
||||
with Else():
|
||||
Text("Basic features only")
|
||||
|
||||
# Pass state={"tier": "free"} to PrefabApp when returning
|
||||
```
|
||||
|
||||
Changes are instant — switching the dropdown re-evaluates the conditions in the browser.
|
||||
|
||||
## Giving the LLM Context
|
||||
|
||||
By default, Prefab sends `"[Rendered Prefab UI]"` as the text content for the LLM. If the model needs to reason about the data, wrap your return in a `ToolResult` with a meaningful summary:
|
||||
|
||||
```python
|
||||
from prefab_ui.app import PrefabApp
|
||||
from prefab_ui.components import Column, Heading
|
||||
from prefab_ui.components.charts import BarChart, ChartSeries
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.tools import ToolResult
|
||||
|
||||
mcp = FastMCP("Sales")
|
||||
|
||||
|
||||
@mcp.tool(app=True)
|
||||
def sales_overview(year: int) -> ToolResult:
|
||||
"""Show sales data visually and summarize for the model."""
|
||||
data = get_sales_data(year)
|
||||
total = sum(row["revenue"] for row in data)
|
||||
|
||||
with Column(gap=4, css_class="p-6") as view:
|
||||
Heading("Sales Overview")
|
||||
BarChart(data=data, series=[ChartSeries(data_key="revenue")])
|
||||
|
||||
return ToolResult(
|
||||
content=f"Total revenue for {year}: ${total:,} across {len(data)} quarters",
|
||||
structured_content=view,
|
||||
return DataTable(
|
||||
columns=[
|
||||
DataTableColumn(key="name", header="Name", sortable=True),
|
||||
DataTableColumn(key="role", header="Role", sortable=True),
|
||||
DataTableColumn(key="dept", header="Dept", sortable=True),
|
||||
],
|
||||
rows=employees,
|
||||
search=True,
|
||||
)
|
||||
```
|
||||
|
||||
The user sees the chart. The LLM sees the summary string.
|
||||
That's it. Add `app=True`, return a Prefab component instead of raw dicts. FastMCP handles the rendering, sandboxing, and security. No wrapper class needed for simple cases like this.
|
||||
|
||||
## Advanced
|
||||
## Add charts
|
||||
|
||||
<Accordion title="Customizing CSP">
|
||||
`app=True` auto-wires the Prefab renderer with default CSP settings. If your app loads external resources — embedding iframes, fetching from APIs, loading scripts — use `PrefabAppConfig` to add the required domains:
|
||||
When numbers tell a better story as a visual, swap in a chart. The API is the same: pass your data as a list of dicts, tell the chart which keys to plot.
|
||||
|
||||
<iframe src="/apps/demos/bar-chart.html" style={{width:"100%", height:"430px", border:"none", overflow:"hidden", borderRadius:"8px"}} frameBorder="0" scrolling="no"></iframe>
|
||||
|
||||
```python
|
||||
@mcp.tool(app=True)
|
||||
def quarterly_revenue(year: int) -> BarChart:
|
||||
"""Show quarterly revenue as a bar chart."""
|
||||
data = [
|
||||
{"quarter": "Q1", "revenue": 42000, "costs": 28000},
|
||||
{"quarter": "Q2", "revenue": 51000, "costs": 31000},
|
||||
{"quarter": "Q3", "revenue": 47000, "costs": 29000},
|
||||
{"quarter": "Q4", "revenue": 63000, "costs": 35000},
|
||||
]
|
||||
|
||||
return BarChart(
|
||||
data=data,
|
||||
series=[
|
||||
ChartSeries(data_key="revenue", label="Revenue"),
|
||||
ChartSeries(data_key="costs", label="Costs"),
|
||||
],
|
||||
x_axis="quarter",
|
||||
show_legend=True,
|
||||
)
|
||||
```
|
||||
|
||||
Each `ChartSeries` plots a different key from the data. `BarChart`, `LineChart`, `AreaChart`, `PieChart`, `RadarChart`, and `RadialChart` all follow the same pattern. Hover over the bars to see tooltips.
|
||||
|
||||
<iframe src="/apps/demos/pie-chart.html" style={{width:"100%", height:"410px", border:"none", overflow:"hidden", borderRadius:"8px"}} frameBorder="0" scrolling="no"></iframe>
|
||||
|
||||
```python
|
||||
@mcp.tool(app=True)
|
||||
def ticket_breakdown() -> PieChart:
|
||||
"""Show open tickets by category."""
|
||||
data = [
|
||||
{"category": "Bug", "count": 42},
|
||||
{"category": "Feature", "count": 28},
|
||||
{"category": "Docs", "count": 15},
|
||||
{"category": "Infra", "count": 10},
|
||||
]
|
||||
|
||||
return PieChart(
|
||||
data=data,
|
||||
data_key="count",
|
||||
name_key="category",
|
||||
inner_radius=50,
|
||||
show_legend=True,
|
||||
)
|
||||
```
|
||||
|
||||
See the [Prefab chart docs](https://prefab.prefect.io/docs/components) for stacking, curves, custom colors, and more.
|
||||
|
||||
## Compose a dashboard
|
||||
|
||||
Tables and charts are useful on their own, but the real power comes from composing them. `Column` stacks children vertically, `Row` lays them out side by side, and `with` blocks establish nesting — the indentation is the layout.
|
||||
|
||||
<iframe src="/apps/demos/dashboard.html" style={{width:"100%", height:"680px", border:"none", overflow:"hidden", borderRadius:"8px"}} frameBorder="0" scrolling="no"></iframe>
|
||||
|
||||
```python expandable
|
||||
@mcp.tool(app=True)
|
||||
def sales_dashboard() -> PrefabApp:
|
||||
"""Show sales KPIs, trends, and deals."""
|
||||
monthly = [
|
||||
{"month": "Jan", "revenue": 48200, "costs": 31000},
|
||||
{"month": "Feb", "revenue": 52100, "costs": 32500},
|
||||
{"month": "Mar", "revenue": 61800, "costs": 34200},
|
||||
{"month": "Apr", "revenue": 58400, "costs": 33800},
|
||||
]
|
||||
deals = [
|
||||
{"account": "Acme Corp", "value": "$84,000", "stage": "Won"},
|
||||
{"account": "Globex Inc", "value": "$52,000", "stage": "Negotiation"},
|
||||
{"account": "Initech", "value": "$31,500", "stage": "Proposal"},
|
||||
{"account": "Wayne Enterprises", "value": "$45,000", "stage": "Lost"},
|
||||
]
|
||||
|
||||
rows = [
|
||||
{
|
||||
"account": d["account"],
|
||||
"value": d["value"],
|
||||
"stage": Badge(
|
||||
d["stage"],
|
||||
variant="success" if d["stage"] == "Won"
|
||||
else "destructive" if d["stage"] == "Lost"
|
||||
else "secondary",
|
||||
),
|
||||
}
|
||||
for d in deals
|
||||
]
|
||||
|
||||
total = sum(m["revenue"] for m in monthly)
|
||||
|
||||
with PrefabApp() as app:
|
||||
with Column(gap=4, css_class="p-6"):
|
||||
with Row(gap=6):
|
||||
Metric(label="Revenue (Q1-Q4)", value=f"${total:,}")
|
||||
Metric(label="Deals", value=f"{len(deals)}")
|
||||
BarChart(
|
||||
data=monthly,
|
||||
series=[
|
||||
ChartSeries(data_key="revenue", label="Revenue"),
|
||||
ChartSeries(data_key="costs", label="Costs"),
|
||||
],
|
||||
x_axis="month",
|
||||
show_legend=True,
|
||||
)
|
||||
Separator()
|
||||
DataTable(
|
||||
columns=[
|
||||
DataTableColumn(key="account", header="Account", sortable=True),
|
||||
DataTableColumn(key="value", header="Value", sortable=True),
|
||||
DataTableColumn(key="stage", header="Stage"),
|
||||
],
|
||||
rows=rows,
|
||||
)
|
||||
|
||||
return app
|
||||
```
|
||||
|
||||
Notice how `Badge` components can be placed inside table cells — any Prefab component works as a cell value, so you can put progress bars, icons, or buttons in your tables too.
|
||||
|
||||
## Make it reactive
|
||||
|
||||
Everything above renders once from the data your Python provides. But interactive tools can also respond to user input in real time, without any server round-trips. Prefab's state system lets components read and write client-side values, so the UI updates instantly as the user interacts with it.
|
||||
|
||||
<iframe src="/apps/demos/reactive.html" style={{width:"100%", height:"500px", border:"none", overflow:"hidden", borderRadius:"8px"}} frameBorder="0" scrolling="no"></iframe>
|
||||
|
||||
Try switching regions in the dropdown, and toggling the switch on and off.
|
||||
|
||||
```python expandable
|
||||
from prefab_ui.rx import Rx
|
||||
|
||||
@mcp.tool(app=True)
|
||||
def regional_sales() -> PrefabApp:
|
||||
"""Sales by region with a live filter."""
|
||||
north = [
|
||||
{"month": "Jan", "sales": 22000},
|
||||
{"month": "Feb", "sales": 25500},
|
||||
{"month": "Mar", "sales": 24200},
|
||||
]
|
||||
south = [
|
||||
{"month": "Jan", "sales": 5800},
|
||||
{"month": "Feb", "sales": 6400},
|
||||
{"month": "Mar", "sales": 5600},
|
||||
]
|
||||
west = [
|
||||
{"month": "Jan", "sales": 6000},
|
||||
{"month": "Feb", "sales": 6000},
|
||||
{"month": "Mar", "sales": 5600},
|
||||
]
|
||||
|
||||
with PrefabApp(
|
||||
state={
|
||||
"region": "north",
|
||||
"north": north, "south": south, "west": west,
|
||||
"show_target": True,
|
||||
},
|
||||
) as app:
|
||||
with Column(
|
||||
gap=4,
|
||||
css_class="p-6",
|
||||
let={"data": "{{ region == 'south' ? south"
|
||||
" : region == 'west' ? west"
|
||||
" : north }}"},
|
||||
):
|
||||
with Row(gap=4, align="center"):
|
||||
with Select(name="region", css_class="w-40"):
|
||||
SelectOption(value="north", label="North")
|
||||
SelectOption(value="south", label="South")
|
||||
SelectOption(value="west", label="West")
|
||||
Switch(name="show_target", css_class="ml-auto")
|
||||
Text("Show target", css_class="text-sm text-muted-foreground")
|
||||
BarChart(
|
||||
data=Rx("data"),
|
||||
series=[ChartSeries(data_key="sales", label="Sales")],
|
||||
x_axis="month",
|
||||
)
|
||||
with If(Rx("show_target")):
|
||||
Metric(label="Q1 Target", value="$75,000")
|
||||
|
||||
return app
|
||||
```
|
||||
|
||||
The `state` dict on `PrefabApp` declares initial values. The `Select` writes to the `region` key on every change. A `let` binding picks the matching dataset, and the chart re-renders. The `Switch` toggles a `Metric` on and off through `If(Rx("show_target"))`. All of this happens in the browser — no calls back to your server.
|
||||
|
||||
`Rx` is a reactive reference: `Rx("region")` compiles to an expression the renderer evaluates live. It supports arithmetic, comparisons, formatting pipes (`.currency()`, `.percent()`), and ternary conditionals (`.then()`). For the full state system, see the [Prefab state docs](https://prefab.prefect.io/docs/concepts/state) and [expression docs](https://prefab.prefect.io/docs/concepts/expressions).
|
||||
|
||||
## Content Security Policy
|
||||
|
||||
Interactive tools render in a sandboxed iframe with a strict CSP. If your tool loads external resources — embedding iframes, fetching from APIs, loading scripts — add the required domains:
|
||||
|
||||
```python
|
||||
from fastmcp.apps import PrefabAppConfig, ResourceCSP
|
||||
|
|
@ -327,40 +260,37 @@ def dashboard_with_embed() -> PrefabApp:
|
|||
...
|
||||
```
|
||||
|
||||
`PrefabAppConfig()` with no arguments is equivalent to `app=True`. It auto-sets the renderer URI and merges the renderer's CSP with any additional domains you provide.
|
||||
</Accordion>
|
||||
`PrefabAppConfig()` with no arguments is equivalent to `app=True`.
|
||||
|
||||
<Accordion title="Type inference">
|
||||
If your return type annotation is a Prefab type — `PrefabApp`, `Component`, or unions containing them — FastMCP enables app rendering automatically, even without `app=True`:
|
||||
## Giving the LLM context
|
||||
|
||||
By default, the LLM sees `"[Rendered Prefab UI]"` as the tool result. If the model needs to reason about the data, return a `ToolResult` with a text summary alongside the UI:
|
||||
|
||||
```python
|
||||
@mcp.tool
|
||||
def greet(name: str) -> PrefabApp:
|
||||
return PrefabApp(view=Heading(f"Hello, {name}!"))
|
||||
```
|
||||
|
||||
Explicit `app=True` is recommended for clarity.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Mixing with custom HTML">
|
||||
Prefab tools and [custom HTML tools](/apps/low-level) coexist on the same server:
|
||||
|
||||
```python
|
||||
from fastmcp.apps import AppConfig
|
||||
from fastmcp.tools import ToolResult
|
||||
|
||||
@mcp.tool(app=True)
|
||||
def team_directory() -> PrefabApp:
|
||||
...
|
||||
def sales_overview(year: int) -> ToolResult:
|
||||
"""Show sales visually, summarize for the model."""
|
||||
data = get_sales_data(year)
|
||||
total = sum(row["revenue"] for row in data)
|
||||
|
||||
@mcp.tool(app=AppConfig(resource_uri="ui://my-app/map.html"))
|
||||
def map_view() -> str:
|
||||
...
|
||||
with Column(gap=4, css_class="p-6") as view:
|
||||
BarChart(data=data, series=[ChartSeries(data_key="revenue")])
|
||||
|
||||
return ToolResult(
|
||||
content=f"Total revenue for {year}: ${total:,} across {len(data)} quarters",
|
||||
structured_content=view,
|
||||
)
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
## Next Steps
|
||||
The user sees the chart. The model sees the summary.
|
||||
|
||||
- **[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
|
||||
## Next steps
|
||||
|
||||
- **[FastMCPApp](/apps/interactive-apps)** — when your UI needs to call backend tools (forms, search, CRUD)
|
||||
- **[Generative UI](/apps/generative)** — let the LLM design the UI at runtime
|
||||
- **[Custom HTML](/apps/low-level)** — when Prefab isn't enough (maps, 3D, your own framework)
|
||||
- **[Examples](/apps/examples)** — complete working servers you can run today
|
||||
- **[Development](/apps/development)** — preview your tools locally with `fastmcp dev apps`
|
||||
- **[Prefab UI](https://prefab.prefect.io)** — full component reference with 100+ components, theming, and advanced patterns
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ request_approval(
|
|||
)
|
||||
```
|
||||
|
||||
## How It Works
|
||||
## How it works
|
||||
|
||||
When the user clicks a button, two things happen:
|
||||
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ choose(
|
|||
)
|
||||
```
|
||||
|
||||
## How It Works
|
||||
## How it works
|
||||
|
||||
Each option renders as a full-width button in a vertical stack. When the user clicks one:
|
||||
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ FileUpload(
|
|||
|
||||
The `max_file_size` limit is enforced both in the UI (the DropZone rejects oversized files) and on the server (the `store_files` tool validates before calling `on_store`).
|
||||
|
||||
## Storage Scoping
|
||||
## Storage scoping
|
||||
|
||||
By default, files are stored in memory and scoped by MCP session ID. Each session gets its own isolated file store — files uploaded in one conversation aren't visible in another.
|
||||
|
||||
|
|
@ -77,7 +77,7 @@ class SharedUpload(FileUpload):
|
|||
return "__shared__"
|
||||
```
|
||||
|
||||
## Custom Storage
|
||||
## Custom storage
|
||||
|
||||
The default implementation stores files in memory for the lifetime of the server process. For persistent storage, subclass `FileUpload` and override three methods. Each receives the current `Context`, giving you access to session IDs, auth tokens, and request metadata for partitioning and authorization.
|
||||
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ This registers two tools:
|
|||
|
||||
The tool name is derived from the model class name, lowercased: `collect_{modelname}`. So `BugReport` becomes `collect_bugreport`, `ShippingAddress` becomes `collect_shippingaddress`. Use `tool_name` to override if needed. The LLM calls it with a prompt explaining what it needs, and the user gets a form with fields matching the model.
|
||||
|
||||
## Field Mapping
|
||||
## Field mapping
|
||||
|
||||
`FormInput` uses Prefab's `Form.from_model()`, which maps Pydantic types to form components:
|
||||
|
||||
|
|
@ -89,7 +89,7 @@ FormInput(
|
|||
|
||||
Set `send_message=True` to push the result back into the conversation via `SendMessage`, triggering the LLM's next turn. Without it, the result is just the tool return value.
|
||||
|
||||
## Multiple Forms
|
||||
## Multiple forms
|
||||
|
||||
Add multiple providers for different models — each gets its own tool:
|
||||
|
||||
|
|
|
|||
|
|
@ -1,74 +0,0 @@
|
|||
---
|
||||
title: Generative UI
|
||||
sidebarTitle: Generative UI
|
||||
description: Let the LLM generate custom UIs at runtime
|
||||
icon: wand-magic-sparkles
|
||||
tag: NEW
|
||||
---
|
||||
|
||||
import { VersionBadge } from '/snippets/version-badge.mdx'
|
||||
|
||||
<VersionBadge version="3.2.0" />
|
||||
|
||||
`GenerativeUI` lets the LLM write Prefab Python code at runtime and render it as a streaming interactive UI. Instead of calling pre-built tools with fixed interfaces, the model creates tailored visualizations for whatever data it's working with.
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.apps.generative import GenerativeUI
|
||||
|
||||
mcp = FastMCP("My Server")
|
||||
mcp.add_provider(GenerativeUI())
|
||||
```
|
||||
|
||||
This registers:
|
||||
|
||||
| Component | Type | Purpose |
|
||||
|-----------|------|---------|
|
||||
| `generate_prefab_ui` | Tool | Accepts Python code, executes in Pyodide sandbox, renders result |
|
||||
| `search_prefab_components` | Tool | Lets the LLM discover available Prefab components |
|
||||
| Generative renderer | Resource | `ui://` resource with browser-side Pyodide for streaming |
|
||||
|
||||
The LLM writes real Python — loops, f-strings, computation — using Prefab's component library (charts, tables, forms, cards, layout primitives). As the model generates tokens, the host streams partial code to the renderer via `ontoolinputpartial`, so the user watches the UI build up in real time.
|
||||
|
||||
## Configuration
|
||||
|
||||
```python
|
||||
GenerativeUI(
|
||||
tool_name="generate_prefab_ui", # Rename the generation tool
|
||||
components_tool_name="search_prefab_components", # Rename the search tool
|
||||
include_components_tool=True, # Set False to omit the search tool
|
||||
)
|
||||
```
|
||||
|
||||
## What the LLM Sees
|
||||
|
||||
The tool description includes code examples that teach the LLM the Prefab patterns. The LLM calls `generate_prefab_ui` with a `code` argument containing Prefab Python, and optionally a `data` argument to pass in real data from the conversation:
|
||||
|
||||
```python
|
||||
# The LLM generates something like:
|
||||
generate_prefab_ui(
|
||||
code="""
|
||||
from prefab_ui.components import Column, Heading
|
||||
from prefab_ui.components.charts import BarChart, ChartSeries
|
||||
from prefab_ui.app import PrefabApp
|
||||
|
||||
with PrefabApp() as app:
|
||||
with Column(gap=4):
|
||||
Heading("Revenue")
|
||||
BarChart(data=data, series=[ChartSeries(data_key="revenue")], x_axis="quarter")
|
||||
""",
|
||||
data={"data": [{"quarter": "Q1", "revenue": 42000}, ...]}
|
||||
)
|
||||
```
|
||||
|
||||
The component search tool lets the LLM discover what's available before writing code — `search_prefab_components("Chart")` returns matching components with import paths.
|
||||
|
||||
## Requirements
|
||||
|
||||
Requires `fastmcp[apps]` (installs `prefab-ui`). The Pyodide sandbox for server-side validation requires Deno, which installs automatically on first use. The streaming renderer loads Pyodide from CDN in the browser — CSP is configured automatically.
|
||||
|
||||
The sandbox includes the Python standard library and Prefab. External packages (NumPy, pandas, etc.) are not available.
|
||||
|
||||
## Learn More
|
||||
|
||||
The full **[Generative UI guide](/apps/generative)** covers the streaming mechanics in detail, how to pass data, the component search tool, and sandbox limitations.
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
title: Quickstart
|
||||
sidebarTitle: Quickstart
|
||||
description: Build your first MCP app in under a minute.
|
||||
description: Build your first FastMCP app in under a minute.
|
||||
icon: rocket
|
||||
tag: NEW
|
||||
---
|
||||
|
|
@ -10,33 +10,29 @@ import { VersionBadge } from '/snippets/version-badge.mdx'
|
|||
|
||||
<VersionBadge version="3.2.0" />
|
||||
|
||||
MCP tools normally return text. FastMCP apps return interactive UIs rendered directly in the conversation: charts, tables, forms, dashboards. The easiest way to build one is with [Prefab UI](https://prefab.prefect.io), a Python component library designed for exactly this. You describe the UI in Python; Prefab compiles it to something the host can render.
|
||||
By the end of this page, you'll have a working tool that returns this:
|
||||
|
||||
This tutorial builds a working app from scratch. Here's what you'll have in about a minute:
|
||||
<iframe src="/apps/demos/team-directory.html" style={{width:"100%", height:"545px", border:"none", overflow:"hidden", borderRadius:"8px"}} frameBorder="0" scrolling="no"></iframe>
|
||||
|
||||
<Frame>
|
||||
<img src="/apps/images/app-quickstart.png" alt="A team directory app with a pie chart and sortable data table, rendered inside a conversation in Goose" />
|
||||
</Frame>
|
||||
A pie chart the user can hover, a table they can sort and search — and a single Python tool.
|
||||
|
||||
## Setup
|
||||
|
||||
Install FastMCP with the `apps` extra, which pulls in Prefab UI:
|
||||
## Install
|
||||
|
||||
```bash
|
||||
pip install "fastmcp[apps]"
|
||||
```
|
||||
|
||||
## A Tool That Returns a UI
|
||||
The `apps` extra pulls in [Prefab](https://prefab.prefect.io), the Python component library used to build app UIs.
|
||||
|
||||
When your tool has something to *show* (a table of results, a chart, a status dashboard) you can return an interactive UI instead of text. Build the visualization with Prefab components, return it from your tool, and set `app=True` so FastMCP knows to render it. The user sees a live, interactive widget right in the conversation instead of a wall of JSON.
|
||||
## Write the tool
|
||||
|
||||
Create `server.py`:
|
||||
Create `server.py`. The interesting parts: `app=True` tells FastMCP this tool renders a UI, and `with PrefabApp() as app:` is the canonical pattern for composing one.
|
||||
|
||||
```python server.py expandable
|
||||
from collections import Counter
|
||||
|
||||
from prefab_ui.app import PrefabApp
|
||||
from prefab_ui.components import Column, Grid, Heading, DataTable, DataTableColumn
|
||||
from prefab_ui.components import Column, DataTable, DataTableColumn, Grid
|
||||
from prefab_ui.components.charts import PieChart
|
||||
from fastmcp import FastMCP
|
||||
|
||||
|
|
@ -63,7 +59,6 @@ def team_directory() -> PrefabApp:
|
|||
|
||||
with PrefabApp() as app:
|
||||
with Column(gap=4, css_class="p-6"):
|
||||
Heading("Team Directory")
|
||||
with Grid(columns=[1, 2], gap=4):
|
||||
PieChart(
|
||||
data=office_counts,
|
||||
|
|
@ -84,40 +79,42 @@ def team_directory() -> PrefabApp:
|
|||
return app
|
||||
```
|
||||
|
||||
That `app=True` is doing a lot behind the scenes. It tells FastMCP to set up everything the MCP Apps protocol requires: the renderer resource, the content security policy, the metadata that tells the host "this tool returns a UI." Without it, you'd wire all of that up by hand. With it, you just return Prefab components and FastMCP handles the rest. The host (Claude Desktop, Goose, etc.) loads the result in a sandboxed iframe where the user can sort columns, search, and interact, all client-side with no round-trips to your server.
|
||||
The Prefab code reads top-to-bottom. `PrefabApp()` is the root; everything inside its `with` block becomes the UI. `Column` stacks children vertically, `Grid` lays them out in columns. `DataTable` takes rows and column definitions and gives you sort and search for free.
|
||||
|
||||
The Prefab code itself reads top-to-bottom like a document. `PrefabApp()` is the root container and everything inside its `with` block becomes the app's UI. `Column` arranges children vertically. `Heading` renders a title. `DataTable` takes rows of data and column definitions, and gives you sorting and search for free. The `with` blocks establish parent-child relationships: nesting components inside each other builds the layout tree.
|
||||
`app=True` does the rest: it sets up the renderer resource, the content security policy, and the metadata that tells the host "this tool returns a UI." The host loads the result in a sandboxed iframe where the user can interact with it — all client-side, no round-trips.
|
||||
|
||||
## Running It
|
||||
## Preview it
|
||||
|
||||
FastMCP includes a dev server that renders your app tools in a browser, no MCP host needed:
|
||||
FastMCP ships a dev server that renders your app tools in a browser, no MCP host needed:
|
||||
|
||||
```bash
|
||||
fastmcp dev apps server.py
|
||||
```
|
||||
|
||||
This opens `http://localhost:8080` where you can pick a tool and see the rendered UI. Try sorting the table columns and typing in the search box.
|
||||
|
||||
## Making It Interactive
|
||||
|
||||
The table above is a static snapshot that renders once from the data your Python code provides. But Prefab apps can also respond to user interaction in real time, without any server round-trips.
|
||||
|
||||
The key concept is **state**: a client-side key-value store that components read from and write to. When the user interacts with a component, it updates state. Other components that reference that state re-render instantly. See the [Prefab state docs](https://prefab.prefect.io/docs/concepts/state) for the full guide.
|
||||
|
||||
Here's the same directory, but now clicking a row shows that person's details in a card:
|
||||
Open `http://localhost:8080`, pick `team_directory`, and try sorting columns and searching.
|
||||
|
||||
<Frame>
|
||||
<img src="/apps/images/app-quickstart-dev-2.png" alt="The team directory with a detail card showing after clicking Bob Martinez" />
|
||||
<img src="/apps/images/app-quickstart-dev-2.png" alt="The team directory rendered in the fastmcp dev apps preview, showing a pie chart, searchable table, and a detail card after clicking a row" />
|
||||
</Frame>
|
||||
|
||||
## Make it reactive
|
||||
|
||||
The UI above renders once from your Python. Prefab apps can also respond to user input live, without any server round-trips. The key concept is **state**: a client-side key-value store that components read from and write to.
|
||||
|
||||
Click a row in the demo below to see a detail card appear:
|
||||
|
||||
<iframe src="/apps/demos/team-directory-reactive.html" style={{width:"100%", height:"675px", border:"none", overflow:"hidden", borderRadius:"8px"}} frameBorder="0" scrolling="no"></iframe>
|
||||
|
||||
Add a few imports, give each member a couple more fields, wire up a click handler, and render a detail card when something's selected:
|
||||
|
||||
```python expandable server.py
|
||||
from collections import Counter
|
||||
|
||||
from prefab_ui.actions import SetState
|
||||
from prefab_ui.app import PrefabApp
|
||||
from prefab_ui.components import (
|
||||
Card, CardContent, CardHeader, Column, Grid, H3, Heading, Muted,
|
||||
Row, DataTable, DataTableColumn, Badge, Small, Text,
|
||||
Badge, Card, CardContent, CardHeader, Column, DataTable, DataTableColumn,
|
||||
Grid, H3, Row, Small, Text,
|
||||
)
|
||||
from prefab_ui.components.charts import PieChart
|
||||
from prefab_ui.components.control_flow import If
|
||||
|
|
@ -129,16 +126,12 @@ mcp = FastMCP("My First App")
|
|||
MEMBERS = [
|
||||
{"name": "Alice Chen", "role": "Staff Engineer", "office": "San Francisco", "email": "alice@company.com", "projects": 3},
|
||||
{"name": "Bob Martinez", "role": "Lead Designer", "office": "New York", "email": "bob@company.com", "projects": 5},
|
||||
{"name": "Carol Johnson", "role": "Senior Engineer", "office": "London", "email": "carol@company.com", "projects": 2},
|
||||
{"name": "David Kim", "role": "Product Manager", "office": "San Francisco", "email": "david@company.com", "projects": 7},
|
||||
{"name": "Eva Mueller", "role": "Engineer", "office": "Berlin", "email": "eva@company.com", "projects": 1},
|
||||
{"name": "Frank Lee", "role": "Data Scientist", "office": "San Francisco", "email": "frank@company.com", "projects": 4},
|
||||
{"name": "Grace Park", "role": "Engineering Manager", "office": "New York", "email": "grace@company.com", "projects": 6},
|
||||
# ... more members ...
|
||||
]
|
||||
|
||||
OFFICE_COUNTS = [
|
||||
{"office": office, "count": count}
|
||||
for office, count in Counter(m["office"] for m in MEMBERS).items()
|
||||
{"office": o, "count": c}
|
||||
for o, c in Counter(m["office"] for m in MEMBERS).items()
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -147,7 +140,6 @@ def team_directory() -> PrefabApp:
|
|||
"""Browse the team directory."""
|
||||
with PrefabApp(state={"selected": None}) as app:
|
||||
with Column(gap=4, css_class="p-6"):
|
||||
Heading("Team Directory")
|
||||
with Grid(columns=[1, 2], gap=4):
|
||||
PieChart(
|
||||
data=OFFICE_COUNTS,
|
||||
|
|
@ -187,22 +179,18 @@ def team_directory() -> PrefabApp:
|
|||
return app
|
||||
```
|
||||
|
||||
Three new ideas here:
|
||||
Three new ideas do all the work:
|
||||
|
||||
**`SetState` + `on_row_click`** is the interaction. When the user clicks a table row, `SetState("selected", Rx("$event"))` writes the clicked row's data into the `selected` state key. `$event` is a special variable that contains the event payload (in this case, the row dict).
|
||||
- **`on_row_click=SetState("selected", Rx("$event"))`** — clicking a row writes its data into the `selected` state key. `$event` is the clicked row dict.
|
||||
- **`Rx("selected.name")`** — a reactive reference. It doesn't hold a Python value; it compiles to a browser-side expression that re-evaluates whenever `selected` changes, so `Text(Rx("selected.name"))` always shows the latest clicked name.
|
||||
- **`If(STATE.selected)`** — conditionally renders its body. Before any click, `selected` is `None` and the card stays hidden.
|
||||
|
||||
**`Rx("selected.name")`** reads from state reactively. It doesn't hold a Python value. It compiles to a browser-side expression that re-evaluates live whenever `selected` changes. So `Text(Rx("selected.name"))` always shows the name of whoever was last clicked.
|
||||
The `state={"selected": None}` dict on `PrefabApp` sets the initial value. Everything else happens in the browser — no round-trips to your server when the user clicks.
|
||||
|
||||
**`If(STATE.selected)`** conditionally renders the detail card only when something has been selected. Before any click, `selected` is `None` and the card is hidden.
|
||||
## Where to go next
|
||||
|
||||
The `state` dict on `PrefabApp` sets initial values when the app loads. Run `fastmcp dev apps server.py` again and try clicking a row.
|
||||
You've built a tool that returns an interactive, reactive UI. This pattern covers a huge range of use cases: build a visualization, return it, and the user gets it rendered right in the conversation.
|
||||
|
||||
## Next Steps
|
||||
|
||||
You've built a tool that returns an interactive, reactive UI. This pattern covers a huge range of use cases: build a visualization in Prefab, return it from a tool, and the user gets dashboards, charts, data tables, and status displays right in the conversation.
|
||||
|
||||
When you need the UI to talk back to your server (forms that save data, buttons that trigger actions, search that queries a database) you promote the tool to a **[FastMCPApp](/apps/interactive-apps)**. That gives you managed backend tools, automatic visibility control, and stable routing so your UI's button clicks reach the right server-side code.
|
||||
|
||||
- **[Prefab UI](/apps/prefab)** covers the full component library: charts, forms, badges, progress bars, and the [reactive state system](https://prefab.prefect.io/docs/concepts/state) in depth.
|
||||
- **[FastMCPApp](/apps/interactive-apps)** is the next step when your UI needs to interact with backend logic.
|
||||
- **[App Providers](/apps/providers/approval)** are ready-made capabilities you can add with a single `add_provider()` call.
|
||||
- **[Interactive Tools](/apps/prefab)** — charts, tables, dashboards, reactive state, with live demos
|
||||
- **[FastMCPApp](/apps/interactive-apps)** — when the UI needs to call back to your server (forms, search, CRUD)
|
||||
- **[Examples](/apps/examples)** — complete working servers you can run today
|
||||
|
|
|
|||
|
|
@ -197,19 +197,10 @@
|
|||
"pages": [
|
||||
"apps/overview",
|
||||
"apps/quickstart",
|
||||
"apps/examples",
|
||||
{
|
||||
"collapsed": true,
|
||||
"group": "Building Apps",
|
||||
"icon": "hammer",
|
||||
"pages": [
|
||||
"apps/prefab",
|
||||
"apps/interactive-apps",
|
||||
"apps/generative",
|
||||
"apps/patterns"
|
||||
],
|
||||
"tag": "NEW"
|
||||
},
|
||||
"apps/prefab",
|
||||
"apps/interactive-apps",
|
||||
"apps/generative",
|
||||
"apps/low-level",
|
||||
{
|
||||
"collapsed": true,
|
||||
"group": "Providers",
|
||||
|
|
@ -218,21 +209,19 @@
|
|||
"apps/providers/approval",
|
||||
"apps/providers/choice",
|
||||
"apps/providers/file-upload",
|
||||
"apps/providers/form",
|
||||
"apps/providers/generative"
|
||||
"apps/providers/form"
|
||||
],
|
||||
"tag": "NEW"
|
||||
},
|
||||
{
|
||||
"collapsed": true,
|
||||
"group": "Advanced",
|
||||
"icon": "gear",
|
||||
"group": "Reference",
|
||||
"icon": "book",
|
||||
"pages": [
|
||||
"apps/development",
|
||||
"apps/architecture",
|
||||
"apps/low-level"
|
||||
],
|
||||
"tag": "NEW"
|
||||
"apps/examples",
|
||||
"apps/architecture"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -413,6 +402,14 @@
|
|||
]
|
||||
},
|
||||
"redirects": [
|
||||
{
|
||||
"destination": "/apps/generative",
|
||||
"source": "/apps/providers/generative"
|
||||
},
|
||||
{
|
||||
"destination": "/apps/prefab",
|
||||
"source": "/apps/patterns"
|
||||
},
|
||||
{
|
||||
"destination": "/cli/overview",
|
||||
"source": "/patterns/cli"
|
||||
|
|
|
|||
3
docs/snippets/prefab-pin-warning.mdx
Normal file
3
docs/snippets/prefab-pin-warning.mdx
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<Tip>
|
||||
[Prefab](https://prefab.prefect.io) is under active development with frequent breaking changes. FastMCP sets a minimum `prefab-ui` version but does not pin an upper bound — **pin `prefab-ui` to a specific version in your own dependencies** before deploying.
|
||||
</Tip>
|
||||
Loading…
Add table
Add a link
Reference in a new issue