From 426572670ecd26beb10e13c991aa8c91d9caac3d Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Fri, 27 Mar 2026 10:18:58 -0400 Subject: [PATCH] Docs: generative UI page, fix imports, add PrefabAppConfig (#3650) --- docs/apps/generative.mdx | 128 +++++++++++++++++++++++++++++++++++++++ docs/apps/low-level.mdx | 12 ++-- docs/apps/overview.mdx | 20 +++++- docs/apps/prefab.mdx | 24 +++++++- docs/docs.json | 1 + 5 files changed, 175 insertions(+), 10 deletions(-) create mode 100644 docs/apps/generative.mdx diff --git a/docs/apps/generative.mdx b/docs/apps/generative.mdx new file mode 100644 index 000000000..71d7e111d --- /dev/null +++ b/docs/apps/generative.mdx @@ -0,0 +1,128 @@ +--- +title: Generative UI +sidebarTitle: Generative UI +description: Let the LLM build custom Prefab UIs on the fly. +icon: wand-magic-sparkles +tag: NEW +--- + +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. + +```python +from fastmcp import FastMCP +from fastmcp.apps.generative import GenerativeUI + +mcp = FastMCP("Prefab Studio") +mcp.add_provider(GenerativeUI()) +``` + +That's it. The `GenerativeUI` provider registers everything: + +- **`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 + +## 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. + +As the LLM generates each token: + +1. The host forwards partial arguments to the app via `ontoolinputpartial` +2. The renderer extracts the growing `code` string +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. + +## What the LLM Writes + +The tool description includes code examples that teach the LLM the Prefab patterns. A typical generation looks like: + +```python +from prefab_ui.components import Column, Row, Heading, Text, Badge, Card, CardContent +from prefab_ui.components.charts import BarChart, ChartSeries +from prefab_ui.app import PrefabApp + +with PrefabApp() as app: + with Column(gap=6, css_class="p-6"): + Heading("Q3 Revenue Report") + + BarChart( + data=[ + {"month": "Jul", "revenue": 42000}, + {"month": "Aug", "revenue": 51000}, + {"month": "Sep", "revenue": 63000}, + ], + series=[ChartSeries(data_key="revenue", label="Revenue")], + x_axis="month", + ) + + with Row(gap=4): + with Card(): + with CardContent(): + Text("Total", css_class="text-sm text-muted-foreground") + Heading("$156,000") + with Card(): + with CardContent(): + Text("Growth", css_class="text-sm text-muted-foreground") + 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 Component Search Tool + +Before writing code, the LLM can call `search_prefab_components` to discover what's available: + +``` +search_prefab_components("Chart") +→ 7 components matching 'Chart': + AreaChart — from prefab_ui.components.charts import AreaChart + BarChart — from prefab_ui.components.charts import BarChart + ... +``` + +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 Data + +The `generate_prefab_ui` tool accepts a `data` parameter. Values passed here become global variables in the sandbox: + +```python +# The LLM can reference 'sales_data' directly in its code +result = await generate_prefab_ui( + code="...", + data={"sales_data": [{"month": "Jan", "revenue": 42000}, ...]} +) +``` + +This lets the model use real data from earlier in the conversation to build visualizations. + +## Configuration + +`GenerativeUI` accepts options for customizing tool names: + +```python +GenerativeUI( + tool_name="generate_prefab_ui", # default + components_tool_name="search_prefab_components", # default + include_components_tool=True, # default +) +``` + +## 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. + +The streaming renderer loads Pyodide from CDN in the browser. The CSP is configured automatically by the provider — no manual setup needed. + +## Next Steps + +- **[Prefab Apps](/apps/prefab)** — The component library and state system the LLM writes code against +- **[Components](/apps/components)** — Quick reference for the most-used components +- **[Development](/apps/development)** — Preview generative UI tools locally with `fastmcp dev apps` diff --git a/docs/apps/low-level.mdx b/docs/apps/low-level.mdx index 944e48498..9ce86e3dd 100644 --- a/docs/apps/low-level.mdx +++ b/docs/apps/low-level.mdx @@ -27,7 +27,7 @@ The tool declares which resource to use via `AppConfig`. When the host calls the import json from fastmcp import FastMCP -from fastmcp.server.apps import AppConfig, ResourceCSP +from fastmcp.apps import AppConfig, ResourceCSP mcp = FastMCP("My App Server") @@ -47,7 +47,7 @@ def chart_view() -> str: `AppConfig` controls how a tool or resource participates in the Apps extension. Import it from `fastmcp.server.apps`: ```python -from fastmcp.server.apps import AppConfig +from fastmcp.apps import AppConfig ``` On **tools**, you'll typically set `resource_uri` to point to the UI resource: @@ -158,7 +158,7 @@ Apps run in sandboxed iframes with a deny-by-default Content Security Policy. By If your app needs to load external resources (CDN scripts, API calls, embedded iframes), declare the allowed domains with `ResourceCSP`: ```python -from fastmcp.server.apps import AppConfig, ResourceCSP +from fastmcp.apps import AppConfig, ResourceCSP @mcp.resource( "ui://my-app/view.html", @@ -185,7 +185,7 @@ def my_view() -> str: If your app needs browser capabilities like camera or clipboard access, request them via `ResourcePermissions`: ```python -from fastmcp.server.apps import AppConfig, ResourcePermissions +from fastmcp.apps import AppConfig, ResourcePermissions @mcp.resource( "ui://my-app/view.html", @@ -214,7 +214,7 @@ import qrcode from mcp import types from fastmcp import FastMCP -from fastmcp.server.apps import AppConfig, ResourceCSP +from fastmcp.apps import AppConfig, ResourceCSP from fastmcp.tools import ToolResult mcp = FastMCP("QR Code Server") @@ -290,7 +290,7 @@ Not all hosts support the Apps extension. You can check at runtime using the too ```python from fastmcp import Context -from fastmcp.server.apps import AppConfig, UI_EXTENSION_ID +from fastmcp.apps import AppConfig, UI_EXTENSION_ID @mcp.tool(app=AppConfig(resource_uri="ui://my-app/view.html")) async def my_tool(ctx: Context) -> str: diff --git a/docs/apps/overview.mdx b/docs/apps/overview.mdx index 760ca9c4f..e694156be 100644 --- a/docs/apps/overview.mdx +++ b/docs/apps/overview.mdx @@ -32,7 +32,8 @@ The quickest way to give a tool a visual UI. You return a [Prefab](https://prefa ```python from prefab_ui.app import PrefabApp -from prefab_ui.components import Column, Heading, BarChart, ChartSeries +from prefab_ui.components import Column, Heading +from prefab_ui.components.charts import BarChart, ChartSeries from fastmcp import FastMCP mcp = FastMCP("Dashboard") @@ -138,10 +139,27 @@ See [FastMCPApp](/apps/interactive-apps) for the full guide. | Light server interaction — one or two tool calls | [Prefab app](/apps/prefab) — `CallTool("tool_name")` | | Heavy server interaction — forms, CRUD, search, multi-step | [FastMCPApp](/apps/interactive-apps) — managed tool binding | | Composed servers — apps mounted under namespaces | [FastMCPApp](/apps/interactive-apps) — stable global keys | +| LLM-generated UIs — bespoke visualizations per request | [Generative UI](/apps/generative) — `GenerativeUI` provider | | Custom rendering — maps, 3D, specific JS frameworks | [Custom HTML](/apps/low-level) — raw MCP Apps extension | The boundary isn't sharp. Start with a Prefab app; graduate to `FastMCPApp` when the tool-management complexity justifies it. +## Generative UI + + + +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. + ## Custom HTML Apps Both approaches above use [Prefab UI](https://prefab.prefect.io) to build UIs in pure Python. If you need full control — your own HTML, CSS, JavaScript, a specific framework — you can use the [MCP Apps extension directly](/apps/low-level). You write the HTML yourself and communicate with the host via the [`@modelcontextprotocol/ext-apps`](https://github.com/modelcontextprotocol/ext-apps) SDK. diff --git a/docs/apps/prefab.mdx b/docs/apps/prefab.mdx index 50a44a8c8..35181808b 100644 --- a/docs/apps/prefab.mdx +++ b/docs/apps/prefab.mdx @@ -24,7 +24,8 @@ Here's a tool that returns a bar chart: ```python from prefab_ui.app import PrefabApp -from prefab_ui.components import Column, Heading, BarChart, ChartSeries +from prefab_ui.components import Column, Heading +from prefab_ui.components.charts import BarChart, ChartSeries from fastmcp import FastMCP mcp = FastMCP("Dashboard") @@ -312,7 +313,8 @@ If the model needs to reason about the data — reference it in conversation, su ```python from prefab_ui.app import PrefabApp -from prefab_ui.components import Column, Heading, BarChart, ChartSeries +from prefab_ui.components import Column, Heading +from prefab_ui.components.charts import BarChart, ChartSeries from fastmcp import FastMCP from fastmcp.tools import ToolResult @@ -361,12 +363,28 @@ The **component tree** is serialized as `structuredContent` on the tool result. None of this requires configuration. The `app=True` flag (or type inference) is the only thing you need. +## 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 while keeping all the auto-wiring: + +```python +from fastmcp.apps import PrefabAppConfig, ResourceCSP + +@mcp.tool(app=PrefabAppConfig( + csp=ResourceCSP(frame_domains=["https://example.com"]), +)) +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. + ## Mixing with Custom HTML Prefab tools and [custom HTML tools](/apps/low-level) coexist in the same server. Prefab tools share a single renderer resource; custom tools point to their own: ```python -from fastmcp.server.apps import AppConfig +from fastmcp.apps import AppConfig @mcp.tool(app=True) def team_directory() -> PrefabApp: diff --git a/docs/docs.json b/docs/docs.json index 1770ff0a1..7231b40a0 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -196,6 +196,7 @@ "apps/interactive-apps", "apps/components", "apps/patterns", + "apps/generative", "apps/development", "apps/architecture", "apps/low-level"