# Architecture Source: https://gofastmcp.com/apps/architecture How FastMCP apps work under the hood — from Python to pixels. 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 An MCP app moves through five stages from Python to pixels: ``` Python components → JSON tree → structuredContent → Renderer iframe → Host UI ``` 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 sections below walk each stage. ## 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 `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 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 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` 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"]` (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 bare `Component` — becomes a JSON blob the renderer can interpret. ### `PrefabApp.to_json()` 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 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 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 (below). ### 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 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 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 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. 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 still applies. `get_app_tool` bypasses transforms but runs auth checks against the tool's `auth` config before executing. ### Provider delegation `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 Prefab renderer is a self-contained JavaScript application that interprets the JSON component tree and renders it as a React UI. ### The shared resource 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 the CDN domains the renderer needs. Hosts use this to configure the iframe's Content Security Policy. ### `postMessage` communication 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 (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 → 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 (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 `fastmcp dev apps` simulates the host-side behavior locally without a real MCP client. ### Proxy architecture 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 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 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 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. # Development Source: https://gofastmcp.com/apps/development Preview and test your app tools locally without a full MCP host. The dev UI showing a rendered Prefab app with the MCP inspector panel `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. Works with both [Interactive Tools](/apps/prefab) and [custom HTML apps](/apps/low-level). ## Quick start ```bash theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}} 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 The dev server does three things: The **picker page** connects to your MCP server, finds all tools with UI metadata, and renders a form for each one. The forms are auto-generated from the tool's input schema — text fields, dropdowns, checkboxes, all wired up. When you submit a form, the dev server **calls your tool** via the MCP protocol and opens the result in a new tab. The result page loads the tool's UI resource (the Prefab renderer or your custom HTML) inside an AppBridge — the same protocol that real MCP hosts use. A **reverse proxy** on `/mcp` forwards requests from the browser to your MCP server, avoiding CORS issues that would otherwise block the iframe-based renderer from talking to a different port. ## MCP inspector The dev UI includes an inspector panel on the left side that captures MCP traffic in real time. It shows JSON-RPC messages flowing between the browser and your server — requests, responses, and AppBridge `postMessage` traffic. Each entry shows direction, method, timing, and a smart summary. Click any entry to expand the full JSON-RPC body. The panel auto-scrolls to new messages unless you've scrolled up to inspect older ones. The inspector is useful for debugging: you can see exactly what arguments your tool received, what it returned, and how the AppBridge communicated with the renderer. ## Options ```bash theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}} fastmcp dev apps server.py:mcp --mcp-port 9000 --dev-port 9090 --no-reload ``` | Option | Flag | Default | Description | | ----------- | -------------------------- | ------- | --------------------------------------------- | | MCP Port | `--mcp-port` | `8000` | Port for your MCP server | | 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 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. ```bash theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}} # Server with multiple app tools fastmcp dev apps examples/apps/contacts/contacts_server.py ``` # Examples Source: https://gofastmcp.com/apps/examples Example apps you can run right now. 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.
## Running the examples Preview any example in your browser with the dev server: ```bash theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}} pip install "fastmcp[apps]" fastmcp dev apps examples/apps/sales_dashboard/sales_dashboard_server.py ``` 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 apps ### 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. ```bash theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}} fastmcp dev apps examples/apps/sales_dashboard/sales_dashboard_server.py ``` ### 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 up to 100 data points over time. ```bash theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}} pip install psutil fastmcp dev apps examples/apps/system_monitor/system_monitor_server.py ``` ### Quiz The LLM generates trivia questions and passes them to the tool. The user answers via buttons, sees correct/incorrect feedback, and tracks score across questions. Demonstrates multi-turn client-side state with FastMCPApp. ```bash theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}} fastmcp dev apps examples/apps/quiz/quiz_server.py ``` ### 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. A reminder that Prefab apps can break out of built-in components when they need to. ```bash theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}} fastmcp dev apps examples/apps/map/map_server.py ``` For ready-made building blocks like approvals, choice pickers, file uploads, and Pydantic forms, see the [Providers](/apps/providers/approval) group. # FastMCPApp Source: https://gofastmcp.com/apps/fastmcp-app Wire an interactive UI to backend tools with managed visibility and composition safety. [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. 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. You'll build up to the contacts app above by the end of this page. Let's start with something smaller. ## A minimal interactive app The smallest interactive app: a form that saves a note, and a list that updates when the user submits. ```python theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}} from prefab_ui.actions import SetState, ShowToast from prefab_ui.actions.mcp import CallTool from prefab_ui.app import PrefabApp from prefab_ui.components import ( Badge, Button, Column, ForEach, Form, Heading, Input, Row, Separator, Text, ) from prefab_ui.rx import RESULT from fastmcp import FastMCP, FastMCPApp app = FastMCPApp("Notes") notes_db: list[dict] = [] @app.tool() def add_note(title: str, body: str) -> list[dict]: """Save a note and return all notes.""" notes_db.append({"title": title, "body": body}) return list(notes_db) @app.ui() def notes_app() -> PrefabApp: """Open the notes app.""" with Column(gap=6, css_class="p-6") as view: Heading("Notes") with ForEach("notes") as note: with Row(gap=2, align="center"): Text(note.title, css_class="font-semibold") Badge(note.body) Separator() with Form( on_submit=CallTool( "add_note", on_success=[ SetState("notes", RESULT), ShowToast("Note saved!", variant="success"), ], on_error=ShowToast("Failed to save", variant="error"), ) ): Input(name="title", label="Title", required=True) Input(name="body", label="Body", required=True) Button("Add Note") return PrefabApp(view=view, state={"notes": list(notes_db)}) mcp = FastMCP("Notes Server", providers=[app]) ``` 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. ## Why not just `@mcp.tool(app=True)`? 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: * 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 theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}} @app.ui() def dashboard() -> PrefabApp: """The model calls this to open the dashboard.""" with Column(gap=4, css_class="p-6") as view: Heading("Dashboard") ... return PrefabApp(view=view) ``` `@app.ui()` supports the same options as `@mcp.tool`: `name`, `description`, `title`, `tags`, `icons`, `auth`, and `timeout`. ## `@app.tool()` — backend tools Backend tools do the work. By default they're visible only to the UI (`visibility=["app"]`), not the model. ```python theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}} @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) ``` If you want a tool callable by both the model and the UI, pass `model=True`: ```python theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}} @app.tool(model=True) def list_contacts() -> list[dict]: """Both the model and the UI can call this.""" return list(db) ``` Backend tools support `name`, `description`, `auth`, and `timeout`. ## `CallTool` — UI → backend `CallTool` is how the UI invokes a backend tool. Pass the tool's name (or a direct function reference): ```python theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}} from prefab_ui.actions.mcp import CallTool CallTool("save_contact", arguments={"name": "Alice", "email": "alice@example.com"}) # Or a function reference — resolves to a stable global key CallTool(save_contact, arguments={...}) ``` Arguments can reference state with `Rx`: ```python theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}} from prefab_ui.rx import STATE CallTool("search", arguments={"query": STATE.search_term}) ``` ### Handling results Server calls are async. Use `on_success` and `on_error` callbacks: ```python theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}} from prefab_ui.actions import SetState, ShowToast from prefab_ui.rx import RESULT CallTool( "save_contact", on_success=[ SetState("contacts", RESULT), ShowToast("Saved!", variant="success"), ], on_error=ShowToast("Something went wrong", variant="error"), ) ``` `RESULT` is a reactive reference to the 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. ### `result_key` shorthand When a tool's return value should replace a state key, use `result_key`: ```python theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}} CallTool("list_contacts", result_key="contacts") # same as: CallTool("list_contacts", on_success=SetState("contacts", RESULT)) ``` ## Actions `CallTool` is one of several actions. Actions attach to handlers like `on_click`, `on_submit`, and `on_change`. Client-side actions run instantly in the browser, no server round-trip: ```python theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}} from prefab_ui.actions import SetState, ToggleState, AppendState, PopState, ShowToast SetState("count", 42) ToggleState("expanded") AppendState("items", {"name": "New Item"}) PopState("items", 0) ShowToast("Done!", variant="success") ``` Pass a list to chain actions: ```python theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}} Button( "Reset", on_click=[ SetState("query", ""), SetState("results", []), ShowToast("Cleared"), ], ) ``` ### Loading states A common pattern: disable a button and show a spinner while a call is in flight. ```python theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}} from prefab_ui.rx import Rx saving = Rx("saving") Button( saving.then("Saving...", "Save"), disabled=saving, on_click=[ SetState("saving", True), CallTool( "save_data", on_success=[ SetState("saving", False), SetState("result", RESULT), ShowToast("Saved!", variant="success"), ], on_error=[ SetState("saving", False), ShowToast("Failed", variant="error"), ], ), ], ) # PrefabApp(view=view, state={"saving": False, ...}) ``` ## Forms Forms collect input and submit it to a tool. When submitted, named input values become the tool's arguments. ### Manual forms ```python theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}} from prefab_ui.components import Form, Input, Select, SelectOption, Textarea, Button with Form( on_submit=CallTool( "create_ticket", on_success=ShowToast("Ticket created!", variant="success"), ) ): Input(name="title", label="Title", required=True) with Select(name="priority", label="Priority"): SelectOption("Low", value="low") SelectOption("Medium", value="medium") SelectOption("High", value="high") Textarea(name="description", label="Description") Button("Create Ticket") ``` On submit, `CallTool` receives `{"title": ..., "priority": ..., "description": ...}`. ### Forms from Pydantic models For structured input, `Form.from_model()` generates the whole form — inputs, labels, validation: ```python theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}} from typing import Literal from pydantic import BaseModel, Field class BugReport(BaseModel): title: str = Field(title="Bug Title") severity: Literal["low", "medium", "high", "critical"] = Field( title="Severity", default="medium" ) description: str = Field(title="Description") @app.ui() def report_bug() -> PrefabApp: with Column(gap=4, css_class="p-6") as view: Heading("Report a Bug") Form.from_model( BugReport, on_submit=CallTool( "create_bug", on_success=ShowToast("Bug filed!", variant="success"), ), ) return PrefabApp(view=view) @app.tool() def create_bug(data: BugReport) -> str: return f"Created: {data.title}" ``` `str` becomes a text input, `Literal` becomes a select, `bool` becomes a checkbox. Field titles and defaults are respected. ## Composition and namespacing 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 theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}} platform = FastMCP("Platform") platform.mount("contacts", contacts_server) # "save_contact" becomes "contacts_save_contact" ``` `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. ### Mounting `FastMCPApp` is a Provider. Add it to a server with `providers=` or `add_provider`: ```python theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}} mcp = FastMCP("Platform", providers=[app]) # or mcp = FastMCP("Platform") mcp.add_provider(app) ``` 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 theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}} mcp = FastMCP("Platform", providers=[contacts_app, inventory_app, billing_app]) ``` ### Running standalone For development, `FastMCPApp` has a `run()` shortcut that wraps itself in a temporary `FastMCP` server: ```python theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}} app = FastMCPApp("Contacts") # ... register tools ... if __name__ == "__main__": app.run() ``` ## A full example: contact manager This brings everything together — entry point, backend tools, Pydantic form, manual form, state, actions, and multi-visibility. ```python expandable theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}} from __future__ import annotations from typing import Literal from prefab_ui.actions import SetState, ShowToast from prefab_ui.actions.mcp import CallTool from prefab_ui.app import PrefabApp from prefab_ui.components import ( Badge, Button, Column, ForEach, Form, Heading, Input, Muted, Row, Separator, Text, ) from prefab_ui.rx import RESULT, Rx from pydantic import BaseModel, Field from fastmcp import FastMCP, FastMCPApp contacts_db: list[dict] = [ {"name": "Arthur Dent", "email": "arthur@earth.com", "category": "Customer"}, {"name": "Ford Prefect", "email": "ford@betelgeuse.org", "category": "Partner"}, ] class ContactModel(BaseModel): name: str = Field(title="Full Name", min_length=1) email: str = Field(title="Email") category: Literal["Customer", "Vendor", "Partner", "Other"] = "Other" app = FastMCPApp("Contacts") @app.tool() def save_contact(data: ContactModel) -> list[dict]: """Save a new contact and return the updated list.""" contacts_db.append(data.model_dump()) return list(contacts_db) @app.tool() def search_contacts(query: str) -> list[dict]: """Filter contacts by name or email.""" q = query.lower() return [ c for c in contacts_db if q in c["name"].lower() or q in c["email"].lower() ] @app.tool(model=True) def list_contacts() -> list[dict]: """Return all contacts. Visible to both the model and the UI.""" return list(contacts_db) @app.ui() def contact_manager() -> PrefabApp: """Open the contact manager.""" with Column(gap=6, css_class="p-6") as view: Heading("Contacts") with ForEach("contacts") as contact: with Row(gap=2, align="center"): Text(contact.name, css_class="font-medium") Muted(contact.email) Badge(contact.category) Separator() Heading("Add Contact", level=3) Form.from_model( ContactModel, on_submit=CallTool( "save_contact", on_success=[ SetState("contacts", RESULT), ShowToast("Contact saved!", variant="success"), ], on_error=ShowToast("Failed to save", variant="error"), ), ) Separator() Heading("Search", level=3) with Form( on_submit=CallTool( "search_contacts", arguments={"query": Rx("query")}, on_success=SetState("contacts", RESULT), ) ): Input(name="query", placeholder="Search by name or email...") Button("Search") return PrefabApp(view=view, state={"contacts": list(contacts_db)}) mcp = FastMCP("Contacts Server", providers=[app]) if __name__ == "__main__": mcp.run() ``` Also available as a runnable server at `examples/apps/contacts/contacts_server.py`. ## Next steps * **[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 # Generative UI Source: https://gofastmcp.com/apps/generative Let the LLM build custom Prefab UIs on the fly.