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.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 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 becomesnotes_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, and hosts act on those declarations to decide what the model sees.
Composition is handled by never writing the name down. CallTool takes a function reference, and FastMCP resolves it when the UI is serialized — to whatever that tool is actually called by then. Mount the server under a namespace and the button calls notes_add_note; put a gateway in front and it calls whatever the gateway lists. Since you never wrote a name, renaming cannot break it. The architecture page covers how that resolution works.
The one rule that comes with this: an app name must be unique within a server. Composing the same app twice breaks its UI — two copies of FastMCPApp("notes") are indistinguishable no matter what namespaces you mount them under, so FastMCP declines to bind rather than picking one. Name apps for what they serve: FastMCPApp("notes-acme") and FastMCPApp("notes-globex"). The architecture page explains why identity works this way.
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.
@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.
model=True:
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):
Rx:
Handling results
Server calls are async. Useon_success and on_error callbacks:
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:
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:
Loading states
A common pattern: disable a button and show a spinner while a call is in flight.Forms
Forms collect input and submit it to a tool. When submitted, named input values become the tool’s arguments.Manual forms
CallTool receives {"title": ..., "priority": ..., "description": ...}.
Forms from Pydantic models
For structured input,Form.from_model() generates the whole form — inputs, labels, validation:
str becomes a text input, Literal becomes a select, bool becomes a checkbox. Field titles and defaults are respected.
Composition and namespacing
The reasonFastMCPApp 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.
Mounting
FastMCPApp is a Provider. Add it to a server with providers= or add_provider:
save.
Running standalone
For development,FastMCPApp has a run() shortcut that wraps itself in a temporary FastMCP server:
A full example: contact manager
This brings everything together — entry point, backend tools, Pydantic form, manual form, state, actions, and multi-visibility.examples/apps/contacts/contacts_server.py.
Next steps
- Interactive Tools — the building blocks: charts, tables, dashboards, reactive state
- Examples — complete working servers
- Development — preview and test app tools locally
- Prefab UI docs — full component reference

