---
title: FastMCP
sidebarTitle: FastMCP
description: Build MCP Apps that render inside Claude Desktop, ChatGPT, and other hosts.
icon: rocket
---

Prefab integrates with [FastMCP](https://github.com/PrefectHQ/fastmcp) to build [MCP Apps](https://modelcontextprotocol.io/docs/extensions/apps) — interactive UIs that render directly inside the conversation. Mark any tool with `app=True` and return a component tree or a `PrefabApp`:

<Note>
When you install `fastmcp[apps]`, FastMCP pulls in `prefab-ui` with only a minimum version — no upper bound. For production, pin `prefab-ui` explicitly in your own dependencies (e.g. `prefab-ui==0.15.0`). See [Versioning Policy](/getting-started/installation#versioning-policy) for details.
</Note>

```python
from fastmcp import FastMCP
from prefab_ui.app import PrefabApp
from prefab_ui.components import Heading, Text
from prefab_ui.components.control_flow import ForEach
mcp = FastMCP("My Server")

ITEMS = [{"name": "Widget"}, {"name": "Gadget"}, {"name": "Gizmo"}]

@mcp.tool(app=True)
def browse() -> PrefabApp:
    """Show all items."""
    with PrefabApp(state={"items": ITEMS}, css_class="p-6") as app:
        Heading("Items")
        with ForEach("items"):
            Text("{{ name }}")
    return app
```

When a host like Claude Desktop calls this tool, the user sees a fully interactive UI instead of plain text. FastMCP handles the wiring automatically: it registers a shared Prefab renderer as a `ui://` resource, sets the MCP Apps metadata on your tool, and converts `PrefabApp` returns to `structuredContent` in the tool result.

## How It Works

The flow has three steps:

1. **Tool call** — the host calls your tool via MCP
2. **PrefabApp → structuredContent** — FastMCP serializes your return value into the Prefab JSON envelope (`version`, `view`, `state`, `defs`)
3. **Renderer** — the host loads the Prefab renderer (from `ui://prefab/renderer.html`) and passes it the structured content. The renderer builds the UI.

When the user interacts with the UI (clicks a button, submits a form), [`CallTool`](/actions/call-tool) sends a new tool call through MCP back to your server. The tool returns a fresh `PrefabApp`, and the renderer updates.

## Returning UI

You can return either a `PrefabApp` or a bare component. Returning a component is a shorthand — FastMCP wraps it in a `PrefabApp` automatically.

<CodeGroup>
```python PrefabApp
from prefab_ui.rx import Rx

name = Rx("name")

@mcp.tool(app=True)
def dashboard() -> PrefabApp:
    with Column(gap=4) as view:
        Heading("Dashboard")
        Text(f"Welcome, {name}")
    return PrefabApp(
        title="Dashboard",
        view=view,
        state={"name": "Alice"},
    )
```
```python Bare Component
@mcp.tool(app=True)
def dashboard() -> Column:
    with Column(gap=4) as view:
        Heading("Dashboard")
        Text("Hello!")
    return view
```
</CodeGroup>

Return a `PrefabApp` when you need initial state, a page title, or reusable definitions. Return a bare component for simple, stateless views.

## Patterns

### Calling Back to the Server

[`CallTool`](/actions/call-tool) is the MCP equivalent of [`Fetch`](/actions/fetch). It sends a tool call through MCP, and the response is available as `$result` (Python: `RESULT`) in the `on_success` callback:

```python
from prefab_ui.actions import SetState
from prefab_ui.actions.mcp import CallTool
from prefab_ui.rx import RESULT

@mcp.tool(app=True)
def browse() -> PrefabApp:
    with Column(gap=4) as view:
        Input(
            name="q",
            placeholder="Search...",
            on_change=[
                SetState("q", "{{ $event }}"),
                CallTool("search", arguments={"q": "{{ $event }}"}, on_success=SetState("results", RESULT)),
            ],
        )
        Slot("results")
    return PrefabApp(view=view, state={"q": "", "results": None})

@mcp.tool
def search(q: str = "") -> PrefabApp:
    matches = [i for i in ITEMS if q.lower() in i["name"].lower()] if q else ITEMS
    with ForEach("items") as view:
        Text("{{ name }}")
    return PrefabApp(view=view, state={"items": matches})
```

The first tool (`browse`) defines the layout with a `Slot`. The second tool (`search`) returns a component tree that fills that slot. This is the core MCP Apps pattern: the initial tool sets up the shell, and subsequent tool calls swap content in and out.

<Note>
`search` doesn't need `app=True` — it's a helper tool called from within the UI, not an entry point that a host would show directly. FastMCP still auto-wires it because its return type is `PrefabApp`.
</Note>

### Dynamic Component Results with Slot

When a `CallTool` writes its result into state via `SetState("key", RESULT)` in `on_success`, a [`Slot`](/components/slot) watching that key renders whatever component tree arrives:

```python {5,11}
# In the main view
with Slot("detail"):
    Text("Select an item", css_class="text-muted-foreground")

# A tool that returns component content for the slot
@mcp.tool
def get_detail(id: str) -> PrefabApp:
    item = lookup(id)
    with Card() as view:
        CardTitle(item["name"])
        Text(item["description"])
    return PrefabApp(view=view, state={"item": item})
```

The `Slot` shows its fallback children until a `CallTool` populates the state key. This is the same pattern as [Dynamic Component Routes](/running/api#dynamic-component-routes) in the API Server guide — the server decides what to render, not just what data to return.

### Error Handling

If a tool raises an exception, the MCP protocol surfaces it as an error. Use `on_error` to display it:

```python
from prefab_ui.rx import Rx

new_title = Rx("new_title")

CallTool(
    "add_entry",
    arguments={"title": new_title},
    on_error=ShowToast("{{ $error }}", variant="error"),
)
```

On the server side, just raise:

```python
@mcp.tool
def add_entry(title: str) -> PrefabApp:
    if not title.strip():
        raise ValueError("Title is required")
    # ...
```

### Communicating with the Host

Beyond `CallTool`, MCP Apps can interact with the host conversation using [`SendMessage`](/actions/send-message) and [`UpdateContext`](/actions/update-context).

[`SendMessage`](/actions/send-message) sends a message to the conversation as if the user typed it — useful for quick-action buttons that trigger follow-up questions:

```python
from prefab_ui.actions.mcp import SendMessage

Button("Explain this",
       on_click=SendMessage("Explain {{ title }} in more detail."))
```

[`UpdateContext`](/actions/update-context) silently updates what the model knows without creating a visible message. The context is attached to the next conversation turn, so the model can reference it without the user needing to re-explain:

```python
from prefab_ui.actions import ShowToast
from prefab_ui.actions.mcp import UpdateContext

Button(
    "Send to Chat",
    on_click=[
        UpdateContext(
            content="Selected item: {{ name }} ({{ category }})"
        ),
        ShowToast("Added to context", variant="success"),
    ],
)
```

<Note>
`UpdateContext` depends on host support. The context is delivered to the host, but whether the model sees it depends on the host's implementation of the MCP Apps `ui/update-model-context` method.
</Note>

### Tool Visibility

By default, all tools registered on your server are visible to the model — including helper tools meant only for UI interactions. When the model can see tools like `search` or `delete_item`, it may call them directly instead of letting the user interact through the UI.

Use `AppConfig(visibility=["app"])` to mark tools as app-only. These tools remain callable via `CallTool` from the UI, but the host should exclude them when presenting tools to the model:

```python {1,8}
from fastmcp.server.apps import AppConfig

@mcp.tool(app=True)
def browse() -> PrefabApp:
    """The entry point — visible to the model."""
    ...

@mcp.tool(app=AppConfig(visibility=["app"]))
def search(q: str = "") -> PrefabApp:
    """Called from the UI only — hidden from the model."""
    ...
```

The entry point tool uses `app=True` so the model can invoke it. Helper tools use `AppConfig(visibility=["app"])` so they're accessible to `CallTool` but won't appear in the model's tool list.

<Note>
Tool visibility is metadata that the host is responsible for enforcing. The server returns all tools from `tools/list` regardless of visibility — the host filters based on the `visibility` field in the tool's app metadata.
</Note>

## API Server vs FastMCP

Both use the same components and state model. The difference is transport:

| | FastMCP | API Server |
|---|---|---|
| **Transport** | MCP protocol | HTTP (fetch) |
| **Server action** | `CallTool` | `Fetch` |
| **Host actions** | `SendMessage`, `UpdateContext` | — |
| **Hosting** | Inside Claude Desktop, ChatGPT, etc. | Standalone web page |
| **Renderer** | Provided by the MCP host | Bundled in `PrefabApp.html()` |

If you're building an MCP server, use `CallTool`. If you're building a web app, use `Fetch`. `SendMessage` and `UpdateContext` are MCP-only — they communicate with the host's conversation, which doesn't exist in standalone mode. The component tree and client-side actions are identical either way.

## Example App

The [`examples/hitchhikers-guide`](https://github.com/PrefectHQ/prefab/tree/main/examples/hitchhikers-guide) directory contains a complete working MCP server — a Hitchhiker's Guide catalog with search, dialog-based entry creation, inline deletion, and error handling. The same directory also contains a [FastAPI version](/running/api) of the same app for comparison.

```bash
uv run examples/hitchhikers-guide/mcp_server.py
```
