Cookie preferences

We use cookies to improve your experience. See our Privacy Policy.

Manufact

How to Deploy FastMCP to Production

Enrico Toniato
Enrico ToniatoCTO
How to Deploy FastMCP to Production

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

  • 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 Depot's layer cache.

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/mcp-detect-fastmcp

Live /mcp: warm-wave-cqtmi.run.mcp-use.com/mcpserverInfo.name is mcp-detect-fastmcp (Manufact server fastmcp-demo, repo mcp-detect-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

  • 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. If you're not strictly Python, mcp-use 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.

Share