diff --git a/.github/workflows/run-upgrade-checks.yml b/.github/workflows/run-upgrade-checks.yml index 180c25bc0..c94720ba7 100644 --- a/.github/workflows/run-upgrade-checks.yml +++ b/.github/workflows/run-upgrade-checks.yml @@ -121,7 +121,7 @@ jobs: - **ty (type checker)**: New ty releases frequently add stricter checks that flag previously-accepted code. Run `uv run ty check` locally with the latest ty to reproduce. Fix the type errors or bump the ty version floor in `pyproject.toml`. - **ruff**: New lint rules or stricter defaults in a ruff upgrade. - - **mcp SDK**: Breaking changes in the `mcp` package (new method signatures, renamed types). + - **MCP SDK**: Breaking changes in the `mcp` package (new method signatures, renamed types). ### What to do diff --git a/docs/apps/architecture.mdx b/docs/apps/architecture.mdx index 26588c2f8..7ecab2aa3 100644 --- a/docs/apps/architecture.mdx +++ b/docs/apps/architecture.mdx @@ -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' -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. diff --git a/docs/apps/demos/bar-chart.html b/docs/apps/demos/bar-chart.html new file mode 100644 index 000000000..81372d859 --- /dev/null +++ b/docs/apps/demos/bar-chart.html @@ -0,0 +1,76 @@ + + + + Prefab + + + + + + +
+ + + \ No newline at end of file diff --git a/docs/apps/demos/bar-chart.py b/docs/apps/demos/bar-chart.py new file mode 100644 index 000000000..e2430b981 --- /dev/null +++ b/docs/apps/demos/bar-chart.py @@ -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, + ) diff --git a/docs/apps/demos/contacts.html b/docs/apps/demos/contacts.html new file mode 100644 index 000000000..5831d639c --- /dev/null +++ b/docs/apps/demos/contacts.html @@ -0,0 +1,172 @@ + + + + Prefab + + + + + + +
+ + + \ No newline at end of file diff --git a/docs/apps/demos/contacts.py b/docs/apps/demos/contacts.py new file mode 100644 index 000000000..0cbe60c0b --- /dev/null +++ b/docs/apps/demos/contacts.py @@ -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") diff --git a/docs/apps/demos/dashboard.html b/docs/apps/demos/dashboard.html new file mode 100644 index 000000000..21c1c8924 --- /dev/null +++ b/docs/apps/demos/dashboard.html @@ -0,0 +1,157 @@ + + + + Prefab + + + + + + +
+ + + \ No newline at end of file diff --git a/docs/apps/demos/dashboard.py b/docs/apps/demos/dashboard.py new file mode 100644 index 000000000..06fe6285d --- /dev/null +++ b/docs/apps/demos/dashboard.py @@ -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, + ) diff --git a/docs/apps/demos/data-table.html b/docs/apps/demos/data-table.html new file mode 100644 index 000000000..fe84abcd4 --- /dev/null +++ b/docs/apps/demos/data-table.html @@ -0,0 +1,90 @@ + + + + Prefab + + + + + + +
+ + + \ No newline at end of file diff --git a/docs/apps/demos/data-table.py b/docs/apps/demos/data-table.py new file mode 100644 index 000000000..5100237bf --- /dev/null +++ b/docs/apps/demos/data-table.py @@ -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, + ) diff --git a/docs/apps/demos/hitchhikers.html b/docs/apps/demos/hitchhikers.html new file mode 100644 index 000000000..7f26f9796 --- /dev/null +++ b/docs/apps/demos/hitchhikers.html @@ -0,0 +1,1105 @@ + + + + Prefab Showcase + + + + + + +
+ + + \ No newline at end of file diff --git a/docs/apps/demos/hitchhikers.py b/docs/apps/demos/hitchhikers.py new file mode 100644 index 000000000..1554e5165 --- /dev/null +++ b/docs/apps/demos/hitchhikers.py @@ -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, + ) diff --git a/docs/apps/demos/pie-chart.html b/docs/apps/demos/pie-chart.html new file mode 100644 index 000000000..c712eeb33 --- /dev/null +++ b/docs/apps/demos/pie-chart.html @@ -0,0 +1,60 @@ + + + + Prefab + + + + + + +
+ + + \ No newline at end of file diff --git a/docs/apps/demos/pie-chart.py b/docs/apps/demos/pie-chart.py new file mode 100644 index 000000000..c1fb489e4 --- /dev/null +++ b/docs/apps/demos/pie-chart.py @@ -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, + ) diff --git a/docs/apps/demos/reactive.html b/docs/apps/demos/reactive.html new file mode 100644 index 000000000..29f05e2d3 --- /dev/null +++ b/docs/apps/demos/reactive.html @@ -0,0 +1,167 @@ + + + + Prefab + + + + + + +
+ + + \ No newline at end of file diff --git a/docs/apps/demos/reactive.py b/docs/apps/demos/reactive.py new file mode 100644 index 000000000..16f2f9829 --- /dev/null +++ b/docs/apps/demos/reactive.py @@ -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", + ) diff --git a/docs/apps/demos/team-directory-reactive.html b/docs/apps/demos/team-directory-reactive.html new file mode 100644 index 000000000..f2ab5bf7e --- /dev/null +++ b/docs/apps/demos/team-directory-reactive.html @@ -0,0 +1,237 @@ + + + + Prefab + + + + + + +
+ + + \ No newline at end of file diff --git a/docs/apps/demos/team-directory-reactive.py b/docs/apps/demos/team-directory-reactive.py new file mode 100644 index 000000000..b6aa004f7 --- /dev/null +++ b/docs/apps/demos/team-directory-reactive.py @@ -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")) diff --git a/docs/apps/demos/team-directory.html b/docs/apps/demos/team-directory.html new file mode 100644 index 000000000..be577ddde --- /dev/null +++ b/docs/apps/demos/team-directory.html @@ -0,0 +1,127 @@ + + + + Prefab + + + + + + +
+ + + \ No newline at end of file diff --git a/docs/apps/demos/team-directory.py b/docs/apps/demos/team-directory.py new file mode 100644 index 000000000..7cfe21bc9 --- /dev/null +++ b/docs/apps/demos/team-directory.py @@ -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, + ) diff --git a/docs/apps/development.mdx b/docs/apps/development.mdx index 7045cd939..0d3a71ac7 100644 --- a/docs/apps/development.mdx +++ b/docs/apps/development.mdx @@ -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' The dev UI showing a rendered Prefab app with the MCP inspector panel -`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. diff --git a/docs/apps/examples.mdx b/docs/apps/examples.mdx index 024808f5d..5078120e7 100644 --- a/docs/apps/examples.mdx +++ b/docs/apps/examples.mdx @@ -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' -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. @@ -44,7 +43,7 @@ Every example below is a working FastMCP server you can run with `fastmcp dev ap -## 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. diff --git a/docs/apps/generative.mdx b/docs/apps/generative.mdx index 86d306dd7..b6293d32b 100644 --- a/docs/apps/generative.mdx +++ b/docs/apps/generative.mdx @@ -10,7 +10,9 @@ import { VersionBadge } from '/snippets/version-badge.mdx' -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. +