--- title: FastMCPApp sidebarTitle: FastMCPApp description: Wire an interactive UI to backend tools with managed visibility and composition safety. icon: puzzle-piece tag: NEW --- import { VersionBadge } from '/snippets/version-badge.mdx' import PrefabPinWarning from '/snippets/prefab-pin-warning.mdx' 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 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 @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 @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 @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 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 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 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 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 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 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 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 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 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 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 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 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 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 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