Use this file to discover all available pages before exploring further.
Prefab 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.
The smallest interactive app: a form that saves a note, and a list that updates when the user submits.
from prefab_ui.actions import SetState, ShowToastfrom prefab_ui.actions.mcp import CallToolfrom prefab_ui.app import PrefabAppfrom prefab_ui.components import ( Badge, Button, Column, ForEach, Form, Heading, Input, Row, Separator, Text,)from prefab_ui.rx import RESULTfrom fastmcp import FastMCP, FastMCPAppapp = 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.
A fair question. Any Interactive Tool 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.
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.
@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.
CallTool is how the UI invokes a backend tool. Pass the tool’s name (or a direct function reference):
from prefab_ui.actions.mcp import CallToolCallTool("save_contact", arguments={"name": "Alice", "email": "alice@example.com"})# Or a function reference — resolves to a stable global keyCallTool(save_contact, arguments={...})
Arguments can reference state with Rx:
from prefab_ui.rx import STATECallTool("search", arguments={"query": STATE.search_term})
Server calls are async. Use on_success and on_error callbacks:
from prefab_ui.actions import SetState, ShowToastfrom prefab_ui.rx import RESULTCallTool( "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.
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:
from prefab_ui.actions import SetState, ToggleState, AppendState, PopState, ShowToastSetState("count", 42)ToggleState("expanded")AppendState("items", {"name": "New Item"})PopState("items", 0)ShowToast("Done!", variant="success")
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:
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.