How to Deploy FastMCP to Production | Manufact Blog

How to Deploy FastMCP to Production

Enrico Toniato·CTO·May 10, 2026

This guide is the FastMCP entry from our seven-framework deploy comparison. We shipped the same example on Manufact Cloud: an echo tool and a greet_widget that returns a MCP 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.

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 and pick Deploy from GitHub. The probe labels the repo fastmcp.

Set port and start command

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

Live /mcp: 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.

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

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).