fastmcp4-cf

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.

Category
Visit Server

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:

  • whoami reports which container answered, and who the server thinks you are. Call it repeatedly — the instance changes mid-conversation, nothing pinned.
  • delete_notes is a guard tool: the multi-round-trip replacement for ctx.elicit(), which now raises on modern connections. It also demonstrates per-caller authorization.
  • probe.sh speaks 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.0b1 is the newest FastMCP 4 (no RC yet; stable is still 3.4.x) and pulls pydantic 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) and wrangler.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

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.

Official
Featured
TypeScript
Magic Component Platform (MCP)

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.

Official
Featured
Local
TypeScript
Audiense Insights MCP Server

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.

Official
Featured
Local
TypeScript
VeyraX MCP

VeyraX MCP

Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.

Official
Featured
Local
graphlit-mcp-server

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.

Official
Featured
TypeScript
Kagi MCP Server

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.

Official
Featured
Python
E2B

E2B

Using MCP to run code via e2b.

Official
Featured
Neon Database

Neon Database

MCP server for interacting with Neon Management API and databases

Official
Featured
Exa Search

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.

Official
Featured
Qdrant Server

Qdrant Server

This repository is an example of how to create a MCP server for Qdrant, a vector search engine.

Official
Featured