Tags: AI, Engineering, Cloudflare, MCP
Host an MCP Server for Free on Cloudflare Workers
You can host a production-shaped remote MCP server for exactly $0. Public HTTPS URL, TLS cert, persistent state, 100,000 requests a day - all on the Cloudflare Workers free plan, no credit card required.
I know because I’ve been building Pravi MCP, a set of deployable templates for exactly this. This post covers what the free tier actually gives you, the limits that matter for MCP specifically and the gotchas that only show up after you deploy.
Why Host a Remote MCP Server at All
Many MCP implementations today run over local stdio: the client spawns a process on your machine and talks to it directly. That works great for personal tools and single-device workflows, but the moment you need to share a server across a team, use it from multiple devices or phones, or add OAuth, you hit a wall. Remote HTTP MCP servers solve that - they’re equally valid in the spec, just less common in early implementations.
A remote MCP server is just an HTTPS endpoint. Any client that speaks the streamable HTTP transport can connect to it from anywhere. The catch is that now you’re operating a service: hosting, TLS, state, deploys. That’s the part Cloudflare makes free.
If you’re new to MCP itself, my Claude developer productivity guide covers where it fits in the broader tooling stack.
What the Cloudflare Free Plan Gives an MCP Server
Verified against Cloudflare’s docs this week, here’s what an MCP server needs and what the free plan provides:
- A public HTTPS URL - every Worker gets
https://<name>.<your-subdomain>.workers.devwith a TLS cert included. You do not need a custom domain to ship a real MCP endpoint. - 100,000 requests per day - each tool call is at least one request. For a personal or small-team server this is a lot of headroom.
- Durable Objects with SQLite storage - this is how the Agents SDK’s
McpAgentclass models MCP sessions, and it’s what gives your tools persistent state. SQLite-backed Durable Objects are on the free plan; 5 GB of storage, 5 million row reads a day. - Local dev with zero signup -
wrangler devruns the whole stack on your machine, Durable Objects included. You don’t need a Cloudflare account until the moment you deploy. - Logs - observability is free to enable, sampled.
The architecture is one Worker fronting a Durable Object per session. The Worker handles the HTTP transport at /mcp, the Durable Object holds session state and your tools are methods that read and write it. The MCP SDK plus Cloudflare’s Agents SDK fit comfortably inside the 3 MB Worker size limit.
The 10 ms CPU Limit: The Free Tier Gotcha
The limit that surprises everyone: free-plan Workers get 10 ms of CPU time per request. Sounds disqualifying. It mostly isn’t, because it’s CPU time, not wall-clock time.
Time spent awaiting a fetch() doesn’t count. A tool that calls three upstream APIs and waits two seconds for responses is fine - it might use a fraction of a millisecond of actual CPU. A tool that parses a 5 MB JSON blob or runs crypto in a loop is not fine.
The practical rule: API-proxy tools work great on free. Compute-heavy tools don’t. Since most MCP tools are thin wrappers around existing APIs - fetch, reshape, return - the free tier covers the majority of real servers. If you genuinely need compute, Workers Paid at $5/month raises the limit to 5 minutes.
The Overage Behavior Is the Real Fine Print
Here’s the detail that should shape your architecture: on the free plan, exceeding any daily limit doesn’t throttle you or bill you. It fails with an error until the reset at 00:00 UTC.
For a personal server, fine - you’ll notice, you’ll wait. But it makes one pattern a trap: hosting multiple users or tenants on a single free account. One heavy user burning the daily quota takes down every other user on that account for the rest of the UTC day, and you cannot buy your way out mid-incident. Upgrading to paid flips the behavior from “fail” to “bill”, which is the single biggest operational difference between the tiers.
Related cap worth knowing: 100 Workers per account, which rules out one-Worker-per-customer setups on free.
Building and Deploying an MCP Server
The structure is straightforward: one Cloudflare Worker that handles the HTTP transport at /mcp, backed by a Durable Object for session state. Your tools are methods on that Durable Object that read and write to its SQLite database.
When Claude connects, here’s what actually happens:
Step 1: Claude initializes and discovers tools
Claude sends:
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": { "name": "Claude", "version": "1.0" }
}
}
Your server responds with its tools:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"serverInfo": { "name": "dice-server", "version": "1.0" },
"tools": [
{
"name": "roll_dice",
"description": "Roll some dice",
"inputSchema": {
"type": "object",
"properties": {
"sides": { "type": "number", "description": "Number of sides" },
"count": { "type": "number", "description": "How many dice" }
},
"required": ["sides", "count"]
}
}
]
}
}
Step 2: Claude calls a tool
When the user asks “roll 3d20”, Claude sends:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "roll_dice",
"arguments": { "sides": 20, "count": 3 }
}
}
Your server executes and responds:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"content": [
{
"type": "text",
"text": "Rolled 3d20: 14, 19, 7 (total: 40)"
}
]
}
}
Claude gets the result and shows it to the user.
Here’s a server implementation that handles this:
import { Hono } from 'hono'
const app = new Hono()
const tools = {
roll_dice: (sides: number, count: number) => {
const rolls = Array.from({ length: count }, () =>
Math.floor(Math.random() * sides) + 1
)
const total = rolls.reduce((a, b) => a + b)
return `Rolled ${count}d${sides}: ${rolls.join(', ')} (total: ${total})`
}
}
app.post('/mcp', async (c) => {
const { method, params, id } = await c.req.json()
if (method === 'initialize') {
return c.json({
jsonrpc: '2.0',
id,
result: {
protocolVersion: '2024-11-05',
capabilities: {},
serverInfo: { name: 'dice-server', version: '1.0' },
tools: [
{
name: 'roll_dice',
description: 'Roll dice',
inputSchema: {
type: 'object',
properties: {
sides: { type: 'number' },
count: { type: 'number' }
},
required: ['sides', 'count']
}
}
]
}
})
}
if (method === 'tools/call') {
const { name, arguments: args } = params
const result = tools[name as keyof typeof tools](args.sides, args.count)
return c.json({
jsonrpc: '2.0',
id,
result: {
content: [{ type: 'text', text: result }]
}
})
}
return c.json({ jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found' } })
})
export default app
Deploy with wrangler:
wrangler deploy
Then connect to Claude Code:
claude mcp add --transport http my-server https://my-server.<your-subdomain>.workers.dev/mcp
Once connected, Claude can call your tools. Here’s what a conversation looks like:
You: Roll 3d20 for me.
(meaning: roll a 20-sided die three times)
Claude: I'll roll 3d20 for you using the dice server.
[Claude calls roll_dice with sides=20, count=3]
Result: Rolled 3d20: 14, 19, 7 (total: 40)
Great! You rolled a total of 40 across three rolls of a 20-sided die.
The individual rolls were 14, 19, and 7.
Every time you ask Claude to roll, it calls your live HTTP endpoint on Cloudflare. Latency is typically under 200ms. If you add more tools - weather lookups, database queries, API calls - Claude discovers and uses them automatically.
The Pravi MCP repo at github.com/cavanpage/pravi-mcp has more complete examples with persistent state and external API calls if you want a reference for adding Durable Objects or handling OAuth.
Frequently Asked Questions
Is the free tier enough for production?
For a personal server or a small team, yes - 100k requests a day is real capacity, and the workers.dev URL with TLS is a legitimate endpoint. For anything multi-tenant or revenue-bearing, the fail-on-overage behavior is the dealbreaker: pay the $5/month so a traffic spike degrades to a bill instead of an outage.
Do I need a custom domain?
No. The *.workers.dev subdomain and its certificate are free and work with every MCP client. A custom domain is cosmetic until you have users who care.
How does authentication work?
The MCP spec uses OAuth 2.1 for remote servers. Cloudflare’s OAuth provider library runs on the same Worker, and the oauth-mcp template wires it to GitHub sign-in end to end. For a server only you use, you can skip auth entirely and treat the URL as the secret - just understand that anyone with the URL can call your tools.
What about state between calls?
Durable Objects give each MCP session its own SQLite database. Tools can write in one call and read in a later one, and the state survives between sessions. This is the piece most “host it on a VPS” tutorials make you build yourself with Redis or Postgres - on Workers it’s built into the platform, free tier included.
Why not just run the MCP server locally?
If only you use it from one machine, local stdio is simpler - keep doing that. Remote earns its complexity when you want the same server from multiple devices, shared tools across a team or OAuth-gated access for other people.
The Takeaway
The gap between “I wrote an MCP server” and “my MCP server is running at a URL” used to be a day of undifferentiated work: hosting, TLS, OAuth, state, deploys. Cloudflare’s free tier collapses most of that, and the limits - 10 ms CPU, fail-on-overage, 100 Workers - are all workable once you know them going in.
Start with npx create-pravi-mcp, get a URL in five minutes and upgrade to the $5 plan only when a limit actually bites. The templates are open source at github.com/cavanpage/pravi-mcp, alongside our other open source projects. And if you’re orchestrating agents that consume these servers, the Temporal orchestration post covers the other half of the stack.