3.2.0
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.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. add_note is omitted from the model’s tool list.
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.
App tool visibility is not a security boundary. Backend tools remain callable through direct MCP requests by clients that know their names, subject to the server’s authentication and authorization checks. Use authorization and server-side validation to enforce access and approval requirements; visibility metadata only declares the intended audience.
Composition is handled at serialization. Inside a FastMCPApp, FastMCP resolves every CallTool reference, by name or by function, to whatever that tool is actually called when the UI reaches the host. 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. 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"], asking hosts to include them in the LLM tool list and reject calls to them from the UI.
@app.ui() accepts name, description, title, tags, icons, annotations, auth, and timeout.
@app.tool() — backend tools
Backend tools do the work. By default they declare visibility=["app"], asking hosts to expose them to the UI and omit them from the model’s tool list.
model=True:
name, description, auth, and timeout.
App tool names matching __<12 lowercase hex>_<name> (for example, __abcdef012345_save) are reserved for generated tool addresses. @app.tool(), @app.ui(), and app.add_tool() reject that pattern with a ValueError at registration. Other names containing double underscores, such as __save or save__contact, are allowed.
CallTool — UI → backend
CallTool is how the UI invokes a backend tool. Pass the tool’s name or the function itself; inside a FastMCPApp, both resolve to the tool’s current address when the UI is serialized:
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 a plain @mcp.tool(app=True) that calls other tools — is composition safety.
When you mount a server under a namespace, tool names get prefixed:
@mcp.tool(app=True), CallTool("save_contact") would now point at a tool that no longer exists. Inside a FastMCPApp, the same call is addressed by the app’s identity and rewritten to contacts_save_contact on the way out, so the 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