# How to Deploy FastMCP to Production

Enrico Toniato·CTO·May 10, 2026

This guide is the **FastMCP** entry from our [seven-framework deploy comparison](/content/blog/deploying-seven-mcp-frameworks/index.html). We shipped the same example on Manufact Cloud: an `echo` tool and a `greet_widget` that returns a [MCP Apps](https://github.com/modelcontextprotocol/ext-apps) view. Below is the deploy path we used; the reference server code is in the second half if you want to reproduce the example.

If you want to run the same deploy pipeline on your repo, connect it in the dashboard. [Open the dashboard](/content/login/index.html).

## Deploy to Manufact

Manufact's Python pipeline detects `pyproject.toml`. With a committed `uv.lock` it runs `uv sync --frozen`; otherwise it falls back to `pip install .` (both honor the `[apps]` extra). The runtime launches uvicorn against the ASGI app you declared.

### Push the repo to GitHub

```
git init && git add . && git commit -m "Initial commit"
gh repo create my-org/fastmcp-greet --private --source=. --push
```

After you edit `pyproject.toml`, run `uv lock` (ideally on Python 3.12 to match Manufact's runtime) and commit **`pyproject.toml` + `uv.lock`**. The cloud uses `uv sync --frozen` when the lock is present.

If a starter shipped a **stale** `uv.lock` (for example fastmcp 3.0.1 without the `[apps]` extra), regenerate with `uv lock` before pushing. Removing the lock is only a quick one-off shortcut; for production, keep the lock in sync with `pyproject.toml`.

### Open the new-server flow

Go to [`manufact.com/cloud/<your-org>/servers/new`](/content/site-root.html) and pick **Deploy from GitHub**. The probe labels the repo **fastmcp**.

### Set port and start command

- **Port:** `8000`
- **Build command:** _(leave empty: the cloud auto-runs `uv sync --frozen` when `uv.lock` is present, otherwise `pip install .` from `pyproject.toml`)_
- **Start command:** `uvicorn src.server:app --host 0.0.0.0 --port 8000`

Manufact reads `requires-python` from your `pyproject.toml` and picks a matching base image (supported: 3.11, 3.12, 3.13). Pin a version in that range — `requires-python = ">=3.13"` deploys on Python 3.13. Anything above the supported range (for example `>=3.14`) isn't available yet, so commit a Dockerfile if you need it.

### Click Deploy

First install pulls FastMCP + Prefab UI + transitive deps (~60–120s). Subsequent deploys reuse cached image layers.

### Smoke-test the URL

```
curl -s -X POST -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"greet_widget","arguments":{"name":"World"}}}' \
  https://<your-slug>.run.mcp-use.com/mcp
```

The response includes `structuredContent.$prefab`: the serialized component tree the renderer interprets. The view URI in `_meta.ui.resourceUri` will be something like `ui://prefab/tool/<hash>/renderer.html`.

**Example repo:** [`manufacts/manufact-for-fastmcp`](https://github.com/manufacts/manufact-for-fastmcp)

**Live `/mcp`:** [`fast-zero-2g0gy.run.mcp-use.com/mcp`](https://fast-zero-2g0gy.run.mcp-use.com/mcp) — `serverInfo.name` is `manufact-for-fastmcp` (Manufact server `fastmcp-demo`, repo `manufact-for-fastmcp`; status: running).

Warning

Keep `uv.lock` in sync with `pyproject.toml`

The official FastMCP starter once shipped a `uv.lock` pinned to fastmcp 3.0.1
(without the `[apps]` extra). `uv sync --frozen` honours the lock no matter
what `pyproject.toml` says. The fix for production is `uv lock` on your target
Python, then commit both files. Deleting `uv.lock` only forces the pip fallback — we used that once to unblock the comparison deploy, not as a
long-term pattern.

The steps above are what we used for the live demo. Your repo can use the same GitHub deploy flow. [Open the dashboard](/content/login/index.html).

## Reference server

Use this section if you are following along with the same example. If you already have an MCP app to deploy, the GitHub steps above are enough.

### What we deployed

- A FastMCP server with the `[apps]` extra installed
- An `echo` tool (text-only)
- A `greet_widget` tool with `app=True` that returns a `PrefabApp` rendered as an interactive UI

## Project setup

```
mkdir my-server && cd my-server
python3 -m venv .venv
source .venv/bin/activate
pip install "fastmcp[apps]" pydantic-settings uvicorn
```

`pyproject.toml`:

```
[project]
name = "my-server"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [\
    "fastmcp[apps]>=3.2.4",\
    "pydantic-settings>=2.12.0",\
    "prefab-ui>=0.19.1,<0.20",\
]

[build-system]
requires = ["uv_build>=0.9.28,<0.10.0"]
build-backend = "uv_build"

[tool.uv.build-backend]
module-name = "src"
module-root = ""`

Project layout:

```
my-server/
├── pyproject.toml
└── src/
    ├── __init__.py
    ├── config.py
    └── server.py
```

`src/__init__.py`: empty.

`src/config.py`:

```
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    service_name: str = "my-server"

def get_settings() -> Settings:
    return Settings()
```

## The server

`src/server.py`:

```
"""FastMCP example: echo tool + Prefab UI greet widget."

from fastmcp import FastMCP
from prefab_ui.app import PrefabApp
from prefab_ui.components import Card, CardContent, Column, Heading, Muted
from starlette.requests import Request
from starlette.responses import JSONResponse

from src.config import get_settings

settings = get_settings()
mcp = FastMCP(name=settings.service_name)

@mcp.tool
def echo(text: str) -> str:
    """Echo the input back as text."""
    return text

@mcp.tool(app=True)
def greet_widget(name: str) -> PrefabApp:
    """Greet someone and render a Prefab UI greeting card."""
    with Column(gap=4, css_class="p-6") as view:
        with Card():
            with CardContent():
                Heading(f"Hello, {name}!")
                Muted("Greeting widget served by my-server.")
    return PrefabApp(view=view)

@mcp.custom_route("/health", methods=["GET"])
async def health_check(request: Request) -> JSONResponse:
    return JSONResponse({"status": "healthy"})

# Streamable-HTTP ASGI app. `stateless_http=True` so each request handles its
# own MCP session: matches the post-deploy /mcp probe in most cloud platforms.
app = mcp.http_app(stateless_http=True)
```

Three things happen when you write `@mcp.tool(app=True)`:

1. FastMCP infers the return-type annotation. `PrefabApp` triggers app-rendering automatically (you can omit `app=True` if the type is right, but explicit is clearer).
2. The framework registers a `text/html;profile=mcp-app` resource hosting the **Prefab renderer** at a URI like `ui://prefab/tool/<hash>/renderer.html`. The renderer is a static JS bundle from `cdn.jsdelivr.net` that interprets the JSON component tree the tool returns.
3. The tool's `_meta.ui.resourceUri` is set to that renderer URI. The host fetches both, calls the tool, sends the structured Prefab tree to the renderer, and the user sees the UI.

The model still sees text: by default, the placeholder string `[Rendered Prefab UI]`. To give the model a real summary, wrap your return in `ToolResult(content="...", structured_content=view)`.

## Run it

```
pip install -e .
MCP_ENV=production uvicorn 'src.server:app' --host 0.0.0.0 --port 8000
```

Hit it:

```
curl -s -X POST -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' \
  http://localhost:8000/mcp
```

You should see both tools and the auto-injected `_meta.ui.resourceUri` on `greet_widget`.

## When to reach for it

Pick this when your team is Python-only and the widget is data-shaped: charts, tables, dashboards, forms: so Prefab's component palette covers what you need. You'd rather write `BarChart(data=…, series=[…])` than wire up Recharts, and you're OK with the model seeing a placeholder string for the tool result unless you wrap it explicitly.

For raw protocol-level control without the Prefab DSL, see [mcp-python](/content/blog/mcp-app-with-mcp-python-sdk/index.html). If you're not strictly Python, [mcp-use](/content/blog/mcp-app-with-mcp-use/index.html) gives you the same widget-from-React-component pattern in TypeScript without baking you into a single component palette. The full comparison is in [Deploying Seven MCP Frameworks](/content/blog/deploying-seven-mcp-frameworks/index.html).
