fastmcp4-cf
A Python MCP server on Cloudflare implementing the stateless MCP 2026-07-28 protocol, enabling load-balanced and scalable tool execution without session affinity.
README
fastmcp4-cf
A Python MCP server on Cloudflare — FastMCP 4 speaking the MCP 2026-07-28 stateless protocol, on Cloudflare Containers behind a Worker.
Clone it, add tools, deploy.
Why this exists
MCP 2026-07-28 is the largest revision since the protocol launched. It removes
protocol-level sessions: no initialize handshake, no Mcp-Session-Id. Every
request carries its own protocol version and client identity in a _meta
envelope, so any instance can answer any request — which is what makes
round-robin load balancing and scale-to-zero possible at all.
This is a working place to watch that happen:
whoamireports which container answered, and who the server thinks you are. Call it repeatedly — the instance changes mid-conversation, nothing pinned.delete_notesis a guard tool: the multi-round-trip replacement forctx.elicit(), which now raises on modern connections. It also demonstrates per-caller authorization.probe.shspeaks the raw protocol over curl, so you can watch a tool call succeed with no handshake — impossible under 2025-11-25.- CI boots the real container behind the real Worker and asserts all of it.
Tracks a beta.
fastmcp==4.0.0b1is the newest FastMCP 4 (no RC yet; stable is still 3.4.x) and pullspydantic 2.14.0a1. Pin your versions.
Why Containers, not Workers
Workers run JS/TS on V8 isolates; Python there goes through Pyodide, which takes
only pure-Python and PyEmscripten wheels — and FastMCP's tree includes
pydantic-core, which is Rust. So Python MCP on Cloudflare means Containers,
which run an ordinary linux/amd64 image with FastMCP unmodified.
The tradeoff: containers are regional, started on demand in the nearest region — not per-PoP like Workers. Closer to Cloud Run than to edge. If you want true edge, you want TypeScript and Cloudflare's Agents SDK, not this.
Shape
request → src/index.ts (Worker) → Dockerfile → server.py (FastMCP 4)
routes + passes secrets in
Containers are not standalone: a Worker receives every request and picks which instance handles it.
| file | role |
|---|---|
server.py |
the MCP server — put your tools here |
Dockerfile |
linux/amd64 image, required by Containers |
src/index.ts |
the Worker: routing, secrets, protocol-era handling |
wrangler.jsonc |
container, Durable Object binding, migration |
probe.sh |
smoke tool — raw 2026-07-28 over curl |
AUTH.md |
read before exposing this — authn/authz |
NOTES.md |
gotchas that cost real time to find |
Prerequisites
- Docker running — wrangler builds the image locally
- Workers Paid plan — Containers are not on the free tier
- Node 20+
Run locally
No Cloudflare account needed — the container runs in local Docker.
npm install
cp .dev.vars.example .dev.vars
python -c "import secrets; print(secrets.token_hex(32))" # paste into .dev.vars
npm run dev # ready on :8787
Then, in another shell:
./probe.sh http://localhost:8787 server/discover
./probe.sh http://localhost:8787 tools/call whoami
.dev.vars is local-dev config and deliberately holds no auth settings — you
cannot mint a Cloudflare Access token on localhost, so adding them makes every
local request 401. Deploy-time values live in .deploy.vars instead.
server/discover replaced the initialize handshake — it returns
supportedVersions, resultType, instructions, and cache hints. Call
whoami a few times and watch instance change: several containers answering
one client, no session between them.
Faster loop: no Docker, no Worker
wrangler dev rebuilds the image on every start, which is slow when you are
just iterating on tool code. To run the MCP server on its own — no container,
no Worker, no load balancing, but the same protocol:
python -m venv .venv && . .venv/bin/activate
pip install --pre -r requirements.txt
REQUEST_STATE_KEY=$(python -c "import secrets; print(secrets.token_hex(32))") \
PORT=9000 python server.py
./probe.sh http://localhost:9000 tools/call whoami
Use wrangler dev when you care about the Worker, the routing, or anything
Cloudflare-shaped; use this when you are writing tools.
The server asking a question back
delete_notes returns a question instead of blocking on one:
# Round 1 — the server asks. Note resultType: "input_required".
./probe.sh http://localhost:8787 tools/call delete_notes '{"folder":"notes"}'
# Round 2 — retry the SAME call with the answer and the state.
STATE=$(./probe.sh http://localhost:8787 tools/call delete_notes '{"folder":"notes"}' \
| python3 -c "import json,sys; print(json.load(sys.stdin)['result']['requestState'])")
./probe.sh http://localhost:8787 tools/call delete_notes '{"folder":"notes"}' \
'{"confirm":{"action":"accept","content":{"value":true}}}' "$STATE"
The tool body runs from the top on both rounds — locals don't survive.
ctx.input_responses is None the first time, populated the second; anything
else that must carry over goes in request_state.
Deploy
npx wrangler login
npx wrangler secret put REQUEST_STATE_KEY # 32+ bytes of randomness
npm run deploy
First provisioning takes a few minutes. Then:
./probe.sh https://fastmcp4-cf.<your-subdomain>.workers.dev tools/call whoami
REQUEST_STATE_KEY seals the multi-round-trip request_state. Every instance
needs the same value — see NOTES.md for why the default breaks
under a load balancer. The server refuses to boot without it.
Adding your own tools
@mcp.tool
def my_tool(arg: str) -> str:
"""Docstring becomes the tool description the model reads."""
return f"got {arg}"
Two constants are duplicated by necessity — keep them in sync:
- port
8080:Dockerfile(EXPOSE),src/index.ts(defaultPort),server.py - instance count:
src/index.ts(INSTANCES) andwrangler.jsonc(max_instances)
server.py runs with stateless_http=True, which is what lets both
protocol eras be load-balanced with no affinity. Turn it off and clients on the
old protocol break intermittently — see NOTES.md.
Authentication
Off by default — as cloned, anyone with the URL can call every tool, and the server says so on boot.
Cloudflare Access is wired up and needs no code: Access runs the OAuth flow
at the edge and forwards a signed JWT, which server.py verifies.
npx wrangler secret put ACCESS_TEAM_DOMAIN # yourteam.cloudflareaccess.com
npx wrangler secret put ACCESS_AUD # the application's AUD tag
npx wrangler secret put ADMIN_EMAILS # who may call gated tools
npm run deploy
Verified end to end on a plain workers.dev hostname — no custom domain needed.
Two dashboard settings will block you if missed; both are in
AUTH.md, along with per-caller tool visibility, service tokens
for CI, and the rules worth treating as non-negotiable.
Any other provider — Google, Auth0, WorkOS, Keycloak — swaps into build_auth().
Teardown
wrangler delete removes the Worker but leaves the container application and
the pushed image, which keeps costing storage:
npx wrangler delete
npx wrangler containers list && npx wrangler containers delete <ID>
npx wrangler containers images list && npx wrangler containers images delete <image>
Delete the Access application separately in the Zero Trust dashboard.
probe.sh
A smoke tool, not a client library. It sends the required headers and _meta
envelope and supports multi-round-trip retries. Its requestState escaping
handles quotes only. For real client work use the
MCP Inspector.
License
MIT — see LICENSE.
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.