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
Open 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
The 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/mcp-detect-mcp-python
Live /mcp: bold-forge-w0fwk.run.mcp-use.com/mcp — serverInfo.name is mcp-detect-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
pyproject.toml:
Project layout:
The server
src/my_server/__init__.py:
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
Hit it:
greet_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.









