Two things happened this week that turn “can I run an MCP server for free” from a
compromise into a plain yes, with one measurable condition. Today the Model Context
Protocol shipped its 2026-07-28 revision, and the headline change is that the protocol
core went stateless: sessions, the Mcp-Session-Id header, and the initialize
handshake are gone. The release post states the consequence outright:
Any request can now land on any server instance behind a plain round-robin load balancer without needing shared storage.
And yesterday, one day ahead of the release, Cloudflare’s own documentation flipped. The
McpAgent class — the official path that backed every MCP server with a Durable Object
because the transport needed somewhere to keep its session — is now marked deprecated and
feature frozen, with a stateless request handler as the recommended replacement for new
servers.
Those two moves close the same gap from both ends. A Worker on the free plan was always a natural place for a small MCP server except for one thing: the transport demanded state, and state meant a Durable Object and a sticky instance. Now the protocol itself promises that every request is self contained. What remains is the free plan’s one hard ceiling — 10 ms of CPU per request — and whether a real server fits under it is not a question you argue about. It is a question you measure. So we built one, on this site, and measured it.
What the revision actually removed
The old lifecycle opened every connection with an initialize round trip, minted a
session id, and required the client to carry it on every call. All of that is deleted.
Version, client identity, and capabilities now travel inside each request’s _meta
fields, and the one thing a server must implement is server/discover, which reports its
supported versions and capabilities to anyone who asks.
The transport got stricter in exchange. Every POST must carry an MCP-Protocol-Version
header that matches the version inside the body, plus an Mcp-Method header mirroring
the JSON-RPC method — and Mcp-Name on tool calls — so load balancers and gateways can
route without parsing bodies. A mismatch is a hard 400. Batching stays gone, ping is
removed entirely, an unknown method is now an HTTP 404, and the 2024 era HTTP+SSE
transport is formally classified as deprecated. A server that never pushes messages may
answer every request with plain JSON and refuse the streaming path with a 405.
For a server that only reads, the protocol surface left over is small: server/discover,
tools/list, tools/call, and the header discipline. That is the whole list.
The server this site now runs
This blog already publishes an agent surface — Markdown mirrors of every article and a
posts.json feed. The MCP server is those same files behind two tools: list_articles
returns the feed, get_article returns an article’s full Markdown. It lives at
https://301.sh/mcp on the same Worker that does the site’s
content negotiation, with zero dependencies — after the
revision, the remaining protocol surface is short enough to write out by hand. Cloudflare
ships an SDK route for the same job (createMcpHandler plus the protocol SDK), which is
the right answer the moment you need resources, prompts, or elicitation; a two tool read
only server does not.
The dispatch, condensed to its shape:
switch (message.method) {
case 'server/discover':
return reply(id, { resultType: 'complete', supportedVersions: ['2026-07-28'],
capabilities: { tools: {} }, ttlMs: 3_600_000, cacheScope: 'public' });
case 'tools/list':
return reply(id, { resultType: 'complete', tools: TOOLS, ttlMs: 3_600_000,
cacheScope: 'public' });
case 'tools/call': {
// Mcp-Name header must equal params.name, or 400 + HeaderMismatch.
const mirror = await env.ASSETS.fetch(new URL(`/${slug}.md`, origin));
return reply(id, { resultType: 'complete',
content: [{ type: 'text', text: await mirror.text() }], isError: false });
}
default:
return fail(id, 404, -32601, `Method not found: ${message.method}`);
}
Nothing in it computes. Every answer is an already deployed static asset fetched through the assets binding, and that choice is exactly what the measurement rewards.
The measurement
Cloudflare’s definition of the limit is the part most people skip past:
CPU time measures how long the CPU spends executing your Worker code. Waiting on network requests (such as
fetch()calls, KV reads, or database queries) does not count toward CPU time.
So the 10 ms budget is spent on parsing, validating, and assembling JSON — not on
fetching the article body. We sent 40 requests at the production endpoint and read the
per invocation cpuTime that Workers Logs record:
| Method | CPU, ms (min / median / max) | Wall, ms (min / median / max) |
|---|---|---|
| server/discover | 0 / 0 / 1 | 0 / 1 / 2 |
| tools/list | 0 / 0 / 0 | 0 / 1 / 2 |
| list_articles | 0 / 1 / 1 | 10 / 13 / 23 |
| get_article, largest article | 0 / 1 / 2 | 6 / 10 / 88 |
The worst case spends 2 ms of the 10 ms budget, and the slowest response — 88 ms of wall time serving the longest article on the site — is almost entirely waiting on the asset fetch, which the meter ignores. A serving tool runs at a fifth of the free ceiling with room to spare, and the free plan’s other number, 100,000 requests a day, is a different class of problem entirely for an endpoint agents call a few times per conversation.
Where 10 ms actually bites
The ceiling is real; it just lives somewhere else than the transport. When we audited this site’s agent surface, we measured a template engine we considered exposing as a service, and it blew through the same 10 ms by a factor of eight. That is the honest division: a tool that serves bytes costs one or two milliseconds; a tool that computes — parses documents, renders templates, hashes at scale — eats the budget almost immediately. Cloudflare does allow occasional overruns (“Each isolate has some built-in flexibility”), but a Worker that exceeds the limit consistently is terminated with error 1102, and burst tolerance is not a plan.
The lift has a price tag, and per this site’s house rule it belongs next to the limit: the Workers Paid plan at $5 a month raises the per request cap to a default of 30 seconds (configurable up to 5 minutes) and includes 30 million CPU milliseconds and 10 million requests a month. If your tools compute, that is the number to compare against, not the free tier’s 10 ms.
Try it, and what this does not need
The endpoint is live, and one request shows the whole new shape of the protocol:
curl https://301.sh/mcp -X POST \
-H 'Content-Type: application/json' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'Mcp-Method: server/discover' \
-d '{"jsonrpc":"2.0","id":1,"method":"server/discover",
"params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28"}}}'
No session to open, no state to hold, no Durable Object on the bill. If your site already publishes Markdown mirrors or a feed — and after the agent readiness audit ours did — the distance from “static files” to “MCP server” is one route on a Worker you may already be running. The spec finally matches what a small read only server always wanted to be: a plain HTTP endpoint that answers questions about content you have already built. The free plan carries that easily. It is the computing tools that were never going to be free, and now you know the number that decides.