Command Palette

Search for a command to run...

UnylyUnyly
Весь каталог

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 affinit

GitHubEmbed

Описание

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

A Python MCP server on CloudflareFastMCP 4 speaking the MCP 2026-07-28 stateless protocol, on Cloudflare Containers behind a Worker.

Clone it, add tools, deploy.

What it exercises

feature here
No sessions, no initialize any instance answers any request
server/discover replaces the handshake, cacheable
resultType on every result complete / input_required
MRTR with sealed requestState delete_notes
ttlMs + cacheScope hints every list result
Deterministic tools/list order prompt-cache friendly
Extension negotiation capabilities.extensions
MCP Apps (io.modelcontextprotocol/ui) monitor and its ui:// resource
Declared sandbox CSP enforced by host/ on the widget's iframe
A host that speaks it host/, served at /host
Load balancing, scale-to-zero Worker getRandom, sleepAfter
Edge OAuth + per-caller authz Cloudflare Access, admin-gated tool

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.

The core also got smaller. Server-initiated requests are gone, replaced by MRTR. Roots, sampling, logging and dynamic client registration are deprecated rather than removed — still functional for a twelve-month window, but not for new code. Tasks moved out into a separately-versioned extension you negotiate, which is now the pattern for anything beyond the core.

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.
  • monitor is an MCP App: a tool bound to a ui:// resource that the host renders as an interactive widget. Its refresh button makes the host call the tool again, so each refresh may be answered by a different container — visible in the chart, since the colour is whichever one replied.
  • 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, serves /host
host/index.html a host that drives the widget over 2026-07-28
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

To run locally: Python 3.12+. FastMCP 4 has no wheel for the 3.9 that ships with macOS, where the install fails with a misleading "no matching distribution found". Node 20+ and Docker running only if you want the Worker and several containers.

To deploy: the above, plus a Workers Paid plan — Containers are not on the free tier.

Run locally

No Cloudflare account needed.

1. The tools — no Docker

git clone https://github.com/tameernoor/fastmcp4-cf && cd fastmcp4-cf
uv venv --python 3.12 .venv                    # or: python3.12 -m venv .venv
uv pip install --prerelease=allow -r requirements.txt

REQUEST_STATE_KEY=$(python3 -c "import secrets; print(secrets.token_hex(32))") \
  ALLOW_OPEN_MODE=1 PORT=9000 .venv/bin/python server.py

ALLOW_OPEN_MODE=1 is required with no Cloudflare Access configured. The server exits without it rather than come up unauthenticated by accident, and in that mode every caller is an admin.

In another shell:

./probe.sh http://localhost:9000 server/discover
./probe.sh http://localhost:9000 tools/call monitor

server/discover replaced the initialize handshake — it returns supportedVersions, resultType, instructions, and cache hints.

2. Plus the widget in a browser

uv pip install --prerelease=allow 'fastmcp[apps]'   # dev-only; NOT in the image
ALLOW_OPEN_MODE=1 .venv/bin/fastmcp dev apps server.py

A tool picker, a real host bridge, and a JSON-RPC log panel. One process, so the instance never changes — for that you want the Worker.

3. The Worker and three containers — needs Docker

npm install
cp .dev.vars.example .dev.vars                              # has ALLOW_OPEN_MODE=1
python3 -c "import secrets; print(secrets.token_hex(32))"   # paste into .dev.vars
npm run dev                                                 # :8787 by default

That serves two things on the same origin:

/mcp the MCP endpoint
/host a host page that drives the widget over 2026-07-28

Open http://localhost:8787/host. Refresh the widget repeatedly and a different container answers each time. probe.sh works against the Worker too:

./probe.sh http://localhost:8787 tools/call whoami

To drive the widget from FastMCP's preview instead, point it at the Worker rather than letting it boot its own server — fastmcp run accepts a URL:

.venv/bin/fastmcp dev apps http://localhost:8787/mcp

Note that preview connects on 2025-11-25, not 2026-07-28. See host/ below for why.

.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.

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:9000 tools/call delete_notes '{"folder":"notes"}'

# Round 2 — retry the SAME call with the answer and the state.
STATE=$(./probe.sh http://localhost:9000 tools/call delete_notes '{"folder":"notes"}' \
  | python3 -c "import json,sys; print(json.load(sys.stdin)['result']['requestState'])")

./probe.sh http://localhost:9000 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.

An interactive widget

monitor renders as HTML in a sandboxed iframe instead of printing as text — the io.modelcontextprotocol/ui extension. It is negotiated, not core, so monitor returns a plain dict and hosts without it just show the numbers.

A tool points at a resource, and the resource is the HTML:

@mcp.tool(app=AppConfig(resource_uri=MONITOR_URI, visibility=["model", "app"]))
def monitor() -> dict[str, object]:
    ...

@mcp.resource(MONITOR_URI, meta={"ui": app_config_to_meta_dict(
    AppConfig(csp=ResourceCSP(resource_domains=["https://unpkg.com"])))})
def monitor_ui() -> str:
    return MONITOR_HTML

ui:// infers text/html;profile=mcp-app. The host renders that HTML in a sandboxed iframe: scripts boxed off from the rest of the app, and a CSP built from the origins you declared. Declare every external one — anything missing is blocked, so a forgotten entry means a blank panel and no error.

The refresh button never talks to this server. It asks the host to call the tool, which is why the model sees that you clicked. Run it with step 2 or 3 above.

fastmcp[apps] is only needed for FastMCP's preview — AppConfig is core FastMCP, so the image installs plain requirements.txt.

The host

Nothing shipping today drives an MCP App over 2026-07-28. [email protected] peer-depends on @modelcontextprotocol/sdk ^1.29.0, and that line's LATEST_PROTOCOL_VERSION is still 2025-11-25 even at 1.30.0 — so FastMCP's preview, and every other MCP Apps host, connects on the old protocol. A v2 SDK exists (@modelcontextprotocol/[email protected]); ext-apps has no 2.x line.

So host/index.html is a minimal host that does. The Worker serves it at /host, same origin as /mcp, which is the only reason it needs no CORS. Around 200 lines, no dependencies, no build step:

  • The MCP leg is hand-written fetch, mirroring probe.sh. Using an SDK would hide the thing being demonstrated, and no JS SDK speaks this yet anyway. Four calls, none of them initialize, no session.
  • The view leg is the ui/* dialect over postMessage, which is plain JSON-RPC with no envelope. The widget is an unmodified ext-apps App and cannot tell the other side is speaking a newer protocol.
  • It enforces the resource's CSP. Building the iframe policy from _meta.ui.csp is the host's job; ignore it and the widget runs unrestricted.
  • It gates on _meta.ui.visibility. Only tools marked "app" may be invoked from the widget, so an app cannot reach your whole tool surface.
  • It answers the server's questions. delete_notes returns input_required, the page renders it, and retries the same call with inputResponses and the sealed requestState. FastMCP's preview has no handling for this at all.

The iframe runs with sandbox="allow-scripts" and no allow-same-origin: srcdoc inherits the parent origin, so keeping that flag would leave the widget same-origin with the host, able to read its DOM and call /mcp with your session.

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

The host page deploys with the Worker, at https://fastmcp4-cf.<your-subdomain>.workers.dev/host. It is a plain route, not a static asset binding, so there is nothing extra to provision — but it is also public unless you put Access in front of it, and it carries a one-click destructive control. See Authentication.

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.

Python changes need a container restart, not just a deploy — running containers keep serving the previous image for several minutes. NOTES.md covers that. The host page is different: it is bundled into the Worker, so it updates the moment wrangler deploy finishes.

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.

Before pushing:

npm run typecheck    # tsc --noEmit; `npm run check` bundles but does NOT check
npm run check        # wrangler deploy --dry-run

Authentication

The server refuses to boot without a decision about it. With ACCESS_TEAM_DOMAIN and ACCESS_AUD unset it exits unless ALLOW_OPEN_MODE=1 is set too — open is a fine thing to want locally and a bad thing to reach by accident.

Open mode means no authentication and every caller is an admin, so monitor and delete_notes are callable by anyone with the URL. That includes /host, a working console with a destructive button on it. Access covers the whole Worker, /mcp and /host alike.

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.

from github.com/tameernoor/fastmcp4-cf

Установка Fastmcp4 Cf

У этого сервера нет опубликованного пакета — он собирается из исходников. Открой репозиторий и следуй инструкции в README.

▸ github.com/tameernoor/fastmcp4-cf

FAQ

Fastmcp4 Cf MCP бесплатный?

Да, Fastmcp4 Cf MCP бесплатный — установка в пару кликов через Unyly без оплаты.

Нужен ли API-ключ для Fastmcp4 Cf?

Нет, Fastmcp4 Cf работает без API-ключей и переменных окружения.

Fastmcp4 Cf — hosted или self-hosted?

Self-hosted: сервер запускается локально на твоей машине командой из раздела установки.

Как установить Fastmcp4 Cf в Claude Desktop, Claude Code или Cursor?

Открой Fastmcp4 Cf на unyly.org, выбери вкладку своего клиента (Claude Desktop, Claude Code, Cursor) и нажми Install — конфиг сгенерируется автоматически, без правки JSON.

Похожие MCP

Compare Fastmcp4 Cf with

Не уверен что выбрать?

Найди свой стек за 60 секунд

Автор?

Embed-бейдж для README

Похожее

Все в категории development