This guide is the the official Python SDK 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
This repo has no uv.lock, so Manufact installs via pip install .. For your own app, add and commit uv.lock after uv lock if you use uv — the cloud will run uv sync --frozen instead. Either way, the runtime launches uvicorn against the ASGI app you exported as app.
Push the repo to GitHub
git init && git add . && git commit -m "Initial commit"
gh repo create my-org/mcp-python-greet --private --source=. --pushOpen the new-server flow
Go to manufact.com/cloud/<your-org>/servers/new and pick Deploy from GitHub. The probe labels the repo mcp-python.
Set port and start command
- Port:
8000 - Build command: (leave empty: this repo has no
uv.lock, so the cloud auto-runspip install .; with a committeduv.lockit usesuv sync --frozeninstead) - Start command:
uvicorn 'my_server:app' --host 0.0.0.0 --port 8000
The single quotes around the module name are important if your package has dashes: Python module names use underscores even when the package name has dashes.
Click Deploy
First install pulls mcp[cli], uvicorn, starlette, and transitives (~30–60s). Subsequent deploys reuse layer cache.
Smoke-test the URL
curl -s -X POST -H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'Host: my-server.fly.dev' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' \
https://<your-slug>.run.mcp-use.com/mcpThe non-localhost Host header is what tripped earlier deployments. With enable_dns_rebinding_protection=False, you should now see both tools and the resource URI.
Example repo: manufacts/manufact-for-mcp-python
Live /mcp: wild-pulse-opk28.run.mcp-use.com/mcp — serverInfo.name is manufact-for-mcp-python (Manufact server status: running).
The say-server pattern
The same meta={"ui": {"resourceUri": URI}} + @server.resource(mime_type="text/html;profile=mcp-app") pattern shows up in the official ext-apps say-server: a streaming TTS demo with a richer view. Worth reading once you're past the "Hello, World" stage.
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 from the official
mcp[cli]package - An
echotool (text-only) - A
greet_widgettool withmeta={"ui": {"resourceUri": ...}}linking to atext/html;profile=mcp-appresource
Project setup
mkdir my-server && cd my-server
python3 -m venv .venv
source .venv/bin/activate
pip install "mcp[cli]" uvicornpyproject.toml:
Project layout:
my-server/
├── pyproject.toml
└── src/
└── my_server/
└── __init__.pyThe server
src/my_server/__init__.py:
"""Greet widget example using the official mcp Python SDK."""
from typing import TypedDict
from mcp.server.fastmcp import FastMCP
from mcp.server.transport_security import TransportSecuritySettings
class GreetProps(TypedDict):
"""Structured payload sent to the View as ``structuredContent``."""
name: str
VIEW_URI = "ui://my-server/greet.html"
RESOURCE_MIME_TYPE = "text/html;profile=mcp-app"
VIEW_HTML = """<!doctype html>
<html><body style="font:16px/1.4 system-ui;padding:24px">
<h1 id="greeting">Greeting view loaded.</h1>
<script type="module">
import { App } from "https://esm.sh/@modelcontextprotocol/ext-apps@1";
const app = new App({ name: "greet", version: "0.1.0" });
app.ontoolresult = (r) => {
const text = (r?.content ?? []).find(c => c.type === "text")?.text;
const struct = r?.structuredContent;
if (text) document.getElementById("greeting").textContent = text;
if (struct?.name) document.title = "Hello, " + struct.name;
};
app.connect();
</script>
</body></html>"""
# Disable DNS-rebinding protection so the server is reachable behind any proxy
# host (Fly, Cloudflare, etc.). FastMCP's default Host check only allows
# localhost, returning 421 Misdirected Request from any other origin.
server = FastMCP(
"my-server",
transport_security=TransportSecuritySettings(
enable_dns_rebinding_protection=False
),
stateless_http=True,
)
@server.tool(name="echo", description="Echo the input back as text.")
def echo(text: str) -> str:
return text
@server.tool(
name="greet_widget",
description="Greet someone and render an MCP App view.",
meta={
"ui": {"resourceUri": VIEW_URI},
"ui/resourceUri": VIEW_URI,
},
)
def greet_widget(name: str) -> GreetProps:
"""Return structuredContent the MCP Apps view renders."""
return {"name": name}
@server.resource(
VIEW_URI,
name="Greet view",
description="MCP Apps view for the greet_widget tool.",
mime_type=RESOURCE_MIME_TYPE,
)
def greet_view() -> str:
return VIEW_HTML
# Streamable-HTTP ASGI app (mounted at /mcp).
app = server.streamable_http_app()Two protocol-level patterns to notice:
meta=on@server.toolis how you set MCP_metaon the tool definition. Bothmeta={"ui": {"resourceUri": …}}andmeta={"ui/resourceUri": …}are common: modern hosts read the first, older hosts read the second, so set both.@server.resource(URI, mime_type="text/html;profile=mcp-app")is the universal way to register a UI resource in the Python SDK. Returning a string makes FastMCP serve it as the resource's text content.
The TypedDict return on greet_widget matters: FastMCP introspects the type to validate structuredContent against an output schema. A plain dict return raises InvalidSignature: return type <class 'dict'> is not serializable for structured output.
Why disable DNS-rebinding protection
FastMCP's default TransportSecurityMiddleware only allows localhost in the Host header. Behind a Fly proxy, every request lands with Host: <appname>.fly.dev and the server returns 421 Misdirected Request. Setting allowed_hosts=["*"] does not help: the matcher does exact-string compare, no wildcards. Either disable the check entirely (fine for a public smoke-test server) or enumerate every host you'll be served from. For production, allow-list the canonical domain explicitly.
Run it
pip install -e .
uvicorn 'my_server:app' --host 0.0.0.0 --port 8000Hit 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/mcpgreet_widget._meta.ui.resourceUri should be ui://my-server/greet.html, and resources/list returns the view at that URI with mimeType: "text/html;profile=mcp-app".
When to reach for it
Pick this when you have a Python-only constraint and want protocol-level control with no framework opinions in the way. The trade-off is verbosity: every widget is a meta= argument plus a @server.resource(...) plus an HTML string you write by hand. For complex views you'll bring your own bundler (Vite emitting into a Python file server works fine).
If you're not strictly Python, mcp-use (TypeScript) covers the same protocol with a widget() helper that auto-registers the resource for you. The full comparison is in Deploying Seven MCP Frameworks.










