Cloudflare Integration Guide
What is an edge platform?
Code at the nearest PoP, behind one anycast IP.
"The edge" means your code executes in the data centre closest to each request instead of a fixed origin region. Cloudflare runs every service on every server in every PoP behind one anycast IP, so there is no region to choose — the network routes to the nearest node (hundreds of cities, 405+ Tbps, ~50 ms to 95% of users). Workers boot in ~0 ms via V8 isolates rather than containers. The trade-offs to design around: no persistent TCP (reach for HTTP stores — R2 / KV / D1), KV is eventually consistent, and when you would rather run near your database than near the user, use Smart Placement.
The four building blocks
Workers run the code; D1, KV and R2 hold the data. Tap a card to dig in.
Why R2 has zero egress fees
Egress is data leaving the provider out to the internet — every file your users download. Traditional clouds bill it per gigabyte on top of storage, so at scale the bandwidth bill dwarfs storage and quietly locks your data in (moving or serving it gets expensive — known as “data gravity”). Cloudflare charges $0 for egress because it already runs a global network with huge peering — bandwidth isn’t a metered resale cost for them, and dropping egress fees is a deliberate move to break that lock-in.
Same bytes, same users. On R2 you still pay for storage and operations — but moving the data out to the internet is $90/mo cheaper here, and the gap only widens with traffic. (S3 figure is the first-tier rate; it tiers down at scale — R2 stays $0.)
It’s exactly why this dashboard serves its tech thumbnails from R2 (thumbs.codeamanilabs.org) — shipping images to a data-conscious East-African audience costs nothing to egress.
██████╗██╗ ██████╗ ██╗ ██╗██████╗ ███████╗██╗ █████╗ ██████╗ ███████╗
██╔════╝██║ ██╔═══██╗██║ ██║██╔══██╗██╔════╝██║ ██╔══██╗██╔══██╗██╔════╝
██║ ██║ ██║ ██║██║ ██║██║ ██║█████╗ ██║ ███████║██████╔╝█████╗
██║ ██║ ██║ ██║██║ ██║██║ ██║██╔══╝ ██║ ██╔══██║██╔══██╗██╔══╝
╚██████╗███████╗╚██████╔╝╚██████╔╝██████╔╝██║ ███████╗██║ ██║██║ ██║███████╗
╚═════╝╚══════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝Cloudflare Integration Guide
Focus: Building, deploying, and managing Cloudflare Workers, D1, KV, R2, and the full Cloudflare platform from Claude Code using the official MCP server and Wrangler CLI.
Overview
Cloudflare's developer platform offers Workers (serverless), D1 (SQLite at the edge), KV (key-value), R2 (object storage), Durable Objects, Queues, Hyperdrive, Pages, and 2,500+ API endpoints. Claude Code integrates through Cloudflare's remote MCP servers (*.mcp.cloudflare.com, installed via the cloudflare/skills plugin) and the wrangler CLI. Work from the root of your Workers project — Claude Code reads wrangler.jsonc to understand your bindings automatically.
Here is the big picture — a single request hits the edge, runs your Worker, and reaches whichever bindings it needs:
Official Documentation
| Resource | URL |
|---|---|
| Cloudflare Developers | https://developers.cloudflare.com |
| Claude Code + Cloudflare | https://developers.cloudflare.com/agent-setup/claude-code/ |
| Cloudflare's own MCP servers | https://developers.cloudflare.com/agents/model-context-protocol/cloudflare/servers-for-cloudflare/ |
| Wrangler CLI | https://developers.cloudflare.com/workers/wrangler/ |
| Workers Docs | https://developers.cloudflare.com/workers/ |
| Workers Best Practices | https://developers.cloudflare.com/workers/best-practices/workers-best-practices/ |
| Workers limits | https://developers.cloudflare.com/workers/platform/limits/ |
| D1 Docs | https://developers.cloudflare.com/d1/ |
| R2 Docs | https://developers.cloudflare.com/r2/ |
MCP Server Setup
This changed in 2026. Cloudflare no longer ships a single local
npxserver. The old@cloudflare/mcp-server-cloudflarepackage is legacy; the Cloudflare API server now lives atgithub.com/cloudflare/mcp. Today Cloudflare runs a catalog of managed remote MCP servers you connect to over OAuth.
Remote MCP servers
Every server is a hosted HTTPS endpoint under *.mcp.cloudflare.com. Use the
Streamable HTTP endpoint at /mcp for new connections (the older /sse URL
stays only as an alias — the deprecated HTTP+SSE transport is gone). Authorize
with OAuth on first connect.
| Server | Streamable HTTP endpoint | What it does |
|---|---|---|
| Cloudflare API ("Code Mode") | https://mcp.cloudflare.com/mcp | search() + execute() over 2,500+ API endpoints |
| Documentation | https://docs.mcp.cloudflare.com/mcp | Search the Cloudflare docs |
| Workers Bindings | https://bindings.mcp.cloudflare.com/mcp | Create/list Workers, KV, R2, D1, Hyperdrive |
| Workers Builds | https://builds.mcp.cloudflare.com/mcp | Inspect Workers Builds CI runs + logs |
| Observability | https://observability.mcp.cloudflare.com/mcp | Query Workers logs, traces, metrics |
| Radar | https://radar.mcp.cloudflare.com/mcp | Internet traffic + URL analysis |
| AI Gateway | https://ai-gateway.mcp.cloudflare.com/mcp | AI Gateway logs + config |
| Logpush | https://logs.mcp.cloudflare.com/mcp | Logpush job health |
| GraphQL | https://graphql.mcp.cloudflare.com/mcp | Query the Cloudflare GraphQL analytics API |
(Full catalog — Container, Browser Run, AI Search/AutoRAG, Audit Logs, DNS Analytics, DEX, CASB — at the "Cloudflare's own MCP servers" doc above.)
Connect from Claude Code
The recommended path is the Cloudflare Skills plugin, which bundles the MCP servers with contextual skills and slash commands. Run inside Claude Code:
/plugin marketplace add cloudflare/skills
/plugin install cloudflare@cloudflareThen verify the servers registered:
claude mcp listTo add a single remote server directly instead (Streamable HTTP transport):
claude mcp add --transport http cloudflare-bindings https://bindings.mcp.cloudflare.com/mcpFirst use opens an OAuth browser flow. In CI (no browser), skip OAuth by
passing a scoped Cloudflare API token as a bearer token — keep it in the
environment manager, never in the repo (see ENV_MASTER.md).
Representative tools (Workers Bindings server)
The domain servers expose named tools; the Cloudflare API server exposes just
search/execute (Code Mode). Common Workers Bindings tools:
| Tool | Description |
|---|---|
workers_list | List all Workers scripts |
workers_get_worker_code | Fetch Worker source |
d1_databases_list / d1_database_query | List D1 databases / run SQL |
kv_namespaces_list | List KV namespaces |
r2_buckets_list / r2_bucket_create | List / create R2 buckets |
hyperdrive_configs_list | List Hyperdrive configs |
The Cloudflare MCP servers manage Workers/KV/R2/D1/Hyperdrive but cannot edit DNS and cannot upload R2 objects — use a scoped DNS token for DNS and
wrangler r2 object put/ the S3 API for object uploads (see the credentials table below).
CLI Integration (Wrangler)
Installation
npm install -g wrangler # Wrangler 4.x is current (v4.125+); needs Node.js 20+Authentication
# Interactive OAuth login
wrangler login
# Use API token (for CI)
export CLOUDFLARE_API_TOKEN=...Key Commands
# Create a new Worker project
npm create cloudflare@latest my-worker -- --type worker
# Local development (with hot reload)
wrangler dev
# Deploy to Cloudflare
wrangler deploy
# View production logs (live tail)
wrangler tail my-worker
# D1 database commands
wrangler d1 create my-database
wrangler d1 execute my-database --command "CREATE TABLE users (id INTEGER PRIMARY KEY)"
wrangler d1 execute my-database --file schema.sql
wrangler d1 migrations apply my-database --local
wrangler d1 migrations apply my-database --remote
# KV namespace commands (v3.60+ uses a SPACE, not a colon; kv:namespace is deprecated)
wrangler kv namespace create MY_NAMESPACE
wrangler kv key put --binding=MY_NAMESPACE "key" "value" # add --remote to write to production
wrangler kv key get --binding=MY_NAMESPACE "key"
# R2 bucket commands
wrangler r2 bucket create my-bucket
wrangler r2 object put my-bucket/path/to/file.json --file ./data.json
# Pages deployment
wrangler pages deploy dist/ --project-name my-site
# Secret management
wrangler secret put MY_SECRET
wrangler secret listWorker Example
src/index.ts:
export interface Env {
DB: D1Database;
KV: KVNamespace;
MY_SECRET: string;
}
export default {
async fetch(req: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const url = new URL(req.url);
if (url.pathname === "/users") {
const { results } = await env.DB.prepare(
"SELECT * FROM users ORDER BY created_at DESC LIMIT 10"
).all();
return Response.json(results);
}
if (url.pathname === "/kv") {
const value = await env.KV.get("my-key");
return new Response(value ?? "not found");
}
return new Response("Not Found", { status: 404 });
},
};wrangler.jsonc:
{
"name": "my-worker",
"main": "src/index.ts",
// Set this to today's date when you start a project, then bump deliberately.
"compatibility_date": "2026-08-23",
"d1_databases": [
{ "binding": "DB", "database_name": "my-database", "database_id": "..." }
],
"kv_namespaces": [
{ "binding": "KV", "id": "..." }
]
}Node.js compat is now on by default. For
compatibility_dateof2026-08-04or later,nodejs_compat(andnodejs_compat_v2) are enabled automatically —node:crypto,node:buffer,node:stream, etc. and npm packages that use them work with no flag. Only older compat dates still need"compatibility_flags": ["nodejs_compat"]. Prefer generating yourEnvtype withwrangler typesover hand-writing it, so config and types can't drift.
Bindings reference
Verified against developers.cloudflare.com/workers/wrangler/configuration. A binding is a direct, in-process handle to a Cloudflare resource on
env— no network hop, no auth token. Best practice is to use bindings over REST APIs.
| Resource | wrangler.jsonc key | Runtime type on env | Create with |
|---|---|---|---|
| KV | kv_namespaces: [{ binding, id }] | KVNamespace | wrangler kv namespace create |
| R2 | r2_buckets: [{ binding, bucket_name }] | R2Bucket | wrangler r2 bucket create |
| D1 | d1_databases: [{ binding, database_name, database_id }] | D1Database | wrangler d1 create |
| Durable Objects | durable_objects.bindings: [{ name, class_name }] + migrations | your DO class namespace | declare class + migration |
| Queues (producer) | queues.producers: [{ binding, queue }] | Queue<T> | wrangler queues create |
| Queues (consumer) | queues.consumers: [{ queue, max_batch_size, dead_letter_queue }] | queue() handler | (bound on the consumer Worker) |
| Hyperdrive | hyperdrive: [{ binding, id }] | Hyperdrive | wrangler hyperdrive create |
// Durable Objects need a migration the first time a class is added.
{
"durable_objects": { "bindings": [{ "name": "COUNTER", "class_name": "Counter" }] },
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["Counter"] }],
"queues": {
"producers": [{ "binding": "JOBS", "queue": "jobs" }],
"consumers": [{ "queue": "jobs", "max_batch_size": 10, "dead_letter_queue": "jobs-dlq" }]
},
"hyperdrive": [{ "binding": "HYPERDRIVE", "id": "<config-id>" }]
}- Durable Objects — low-latency coordination + strongly-consistent per-object
storage. New classes use the SQLite backend (
new_sqlite_classes). - Queues — decouple background work from the request path; a
dead_letter_queuecaptures messages that fail pastmax_retries. Pages Functions can produce to a queue but cannot consume — put the consumer in a separate Worker. - Hyperdrive — pools + caches connections to an existing Postgres/MySQL database
so a Worker can query it without exhausting connections. Drivers like
postgres.jsneed Node.js compat (default for compat date ≥ 2026-08-04).
Pages & Functions
Verified against Cloudflare's official docs (developers.cloudflare.com/pages/functions). Pages serves your static build; Pages Functions add server-side code on the same deploy — file-based routing out of a
functions/directory, running on Workers.
Pages is two layers in one deploy: static assets plus an optional functions/ directory that Cloudflare compiles into a single Worker. Files map to URL paths automatically:
File-based routing
A file's path under functions/ becomes its route:
| File | Route |
|---|---|
functions/index.ts | / |
functions/api/hello.ts | /api/hello |
functions/users/[user].ts | /users/:user (single segment → params.user string) |
functions/api/[[path]].ts | /api/* (catch-all → params.path array) |
More specific routes (fewer wildcards) win over catch-alls.
Pages Function example
A catch-all API handler at functions/api/[[path]].ts. Each onRequest (or method-specific onRequestGet / onRequestPost) receives an EventContext with request, env, params, waitUntil, next, and data. The PagesFunction<Env> generic types your bindings:
interface Env {
KV: KVNamespace;
DB: D1Database;
}
// Handles GET /api/anything/here
export const onRequestGet: PagesFunction<Env> = async (context) => {
const { params, env } = context;
// params.path is the segments after /api/ as a string[]
const segments = params.path as string[];
if (segments[0] === "ping") {
return Response.json({ ok: true, ts: Date.now() });
}
const cached = await env.KV.get(segments.join("/"));
return cached
? new Response(cached)
: new Response("Not Found", { status: 404 });
};
// A bare onRequest runs for any verb without a more specific onRequestVerb export.
export const onRequest: PagesFunction<Env> = async ({ next }) => {
return next(); // fall through to the static asset server
};Deploy
# Build your site, then deploy the output directory (Functions in ./functions are bundled)
wrangler pages deploy dist/ --project-name my-site
# Local dev with Functions + bindings emulated
wrangler pages dev dist/Bindings
Pages Functions read bindings off context.env, same as Workers. Configure them in wrangler.jsonc (or the Pages project's dashboard Settings → Bindings for production/preview). Keep compatibility_date current.
{
"name": "my-site",
"pages_build_output_dir": "dist",
"compatibility_date": "2026-08-23",
"kv_namespaces": [
{ "binding": "KV", "id": "..." }
],
"d1_databases": [
{ "binding": "DB", "database_name": "my-database", "database_id": "..." }
]
}_routes.json
Cloudflare auto-generates this, but you can ship your own at the build-output root to control which paths invoke Functions (vs. serving a static asset directly). exclude takes priority over include; wildcards match any number of segments:
{
"version": 1,
"include": ["/api/*"],
"exclude": ["/api/static/*"]
}Gotcha: if a path matches no
includerule (or hits anexclude), the request is served as a static asset and your Function never runs — a silent 404/wrong-content instead of an error. When an API route mysteriously bypasses your handler, check_routes.jsonfirst. Runwrangler pages deployto regenerate the auto version.
Environment Variables
# Required
CLOUDFLARE_API_TOKEN=... # From dash.cloudflare.com → Profile → API Tokens
CLOUDFLARE_ACCOUNT_ID=... # From dash.cloudflare.com (right sidebar)
# Wrangler picks these up automatically from environment
# Or use: wrangler secret put MY_SECRET for runtime secretsAutomation Workflows
Claude Code Hook: Auto-deploy on Save
.claude/settings.json:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "if echo \"$CLAUDE_FILE_PATH\" | grep -q 'src/'; then echo 'Worker source changed — run wrangler deploy to push'; fi"
}
]
}
]
}
}Slash Command: Deploy and Tail Logs
.claude/commands/cf-deploy.md:
Deploy the current Cloudflare Worker and confirm it's live.
1. Use Bash to run `wrangler deploy` and capture the deployed URL
2. Use Bash to run `wrangler tail --format pretty` for 10 seconds to check for errors
3. Report the deployed Worker URL and any runtime errors observedUsage: /project:cf-deploy
GitHub Actions: CI Deploy to Cloudflare Workers
# .github/workflows/cloudflare.yml
name: Deploy Worker
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '22' }
- run: npm ci
- name: Run D1 migrations
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
run: npx wrangler d1 migrations apply my-database --remote
- name: Deploy Worker
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }}R2: Create Buckets & API Tokens (step-by-step)
Verified against Cloudflare's official docs (developers.cloudflare.com/r2). Bucket names: lowercase letters, numbers, hyphens only.
You have three clean paths into R2 — pick the one that matches the job, and serving objects publicly is just as straightforward:
Create a bucket
Dashboard — open R2 → Overview
- Go to R2 object storage → Overview.
- Select Create bucket.
- Enter a name, pick a location + default storage class.
- Select Create bucket.
Wrangler (auth via wrangler login, no keys needed):
npx wrangler r2 bucket create my-bucket
npx wrangler r2 bucket listREST API (needs an API token with R2 edit — see below):
curl https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/r2/buckets \
-H "Authorization: Bearer $R2_ADMIN_TOKEN" -H "Content-Type: application/json" \
--data '{"name":"my-bucket"}'Get R2 API tokens (S3 Access Key ID + Secret)
Needed for S3 SDKs (boto3, AWS SDK, rclone) and for an app to read/write objects. Wrangler does not need these.
Dashboard — open R2 API tokens
- R2 → Overview → under Account details, select Manage next to API Tokens.
- Choose Create Account API token (tied to the account, survives user removal — best for automation) or Create User API token (tied to your user).
- Under Permissions pick one: Object Read & Write (typical), Object Read, Admin Read & Write, or Admin Read.
- Select Apply to specific buckets only and choose your bucket (least privilege).
- Create API Token, then copy the Access Key ID + Secret Access Key now — the secret is shown only once.
- Your S3 endpoint is
https://<ACCOUNT_ID>.r2.cloudflarestorage.com.
Deriving S3 creds from any Cloudflare API token: Access Key ID = the token's
id; Secret Access Key = the SHA‑256 hash of the token value.
Serve objects publicly (for <img> on the frontend)
- Custom domain (recommended; needs the domain's zone on Cloudflare): bucket →
Settings → Public access → Connect Domain → e.g.
thumbs.codeamanilabs.org. - r2.dev URL: bucket → Settings → Public access → enable the managed
r2.devURL. - Both make objects world-readable. Keep private buckets behind an app proxy + a scoped read token instead.
Credentials & Permissions for Claude Code automation
What Claude Code needs to automate Cloudflare, and the gotchas that block it:
| Credential | Create at | Scope / permission | Lets Claude Code automate |
|---|---|---|---|
| Account ID | R2 Overview / dash URL | identifier (not secret) | target API + S3 endpoint |
| Zone ID | domain → Overview (API section) | identifier (not secret) | DNS API calls for that zone |
| R2 S3 token (Access Key + Secret) | R2 → Manage R2 API Tokens | Object Read & Write, scoped to a bucket | upload/serve objects (boto3, AWS SDK, rclone, app proxy) |
| R2 admin token | Account API Tokens → Custom | Workers R2 Storage: Edit | create/list/delete buckets + settings via API |
| DNS token | Account API Tokens → Custom | Zone → DNS → Edit (+ Zone → Read) | add/edit DNS records (subdomains, R2 custom domains) |
| Global API Key | My Profile → API Tokens | full account (legacy) | everything via wrangler legacy auth |
OAuth (wrangler login) | local browser | your user's permissions | all local wrangler commands |
Automation gotchas (learned the hard way):
- The Cloudflare MCP (claude.ai connector) manages Workers/R2 buckets/KV/D1 — but
cannot edit DNS and cannot upload R2 objects. Use a DNS token for DNS and
wrangler
r2 object put/ the S3 API for object uploads. wrangler r2 object put <bucket>/<key> --remoteuploads objects; auth viaCLOUDFLARE_API_TOKEN(preferred) orCLOUDFLARE_API_KEY+CLOUDFLARE_EMAIL.- Global API Key is all-powerful — never put it in an app, repo, or CI. Prefer scoped tokens; keep the global key for local CLI only.
- Prefer Account API tokens for unattended automation (they don't break when a user leaves). Always scope R2 tokens to specific buckets.
- An R2 custom domain requires the domain's zone to be on Cloudflare.
Common Use Cases
| Use Case | Approach |
|---|---|
| Edge API with D1 | Worker + wrangler d1 execute for schema |
| Global KV cache | KVNamespace binding in Worker |
| Static site | wrangler pages deploy dist/ |
| File storage | R2 bucket + Worker presigned URLs |
| Background jobs / fan-out | Queues producer + separate consumer Worker |
| Query an existing Postgres/MySQL DB | Hyperdrive binding (connection pooling + cache) |
| Stateful coordination / counters | Durable Objects (SQLite storage) |
| Rate limiting | Cloudflare Rate Limiting via the API MCP execute |
| DNS management | scoped DNS token (not the MCP — it can't edit DNS) |
Troubleshooting
| Issue | Fix |
|---|---|
CLOUDFLARE_API_TOKEN missing | Create token at dash.cloudflare.com with Workers:Edit permissions |
wrangler dev port conflict | Use wrangler dev --port 8788 |
| D1 migration not applying | Run wrangler d1 migrations list my-database --remote to check state |
| Worker over size limit | Limit is 3 MB (Free) / 10 MB (Paid) after gzip — minify (on by default in v4), trim deps, or split into sub-workers |
kv:namespace command errors | The colon form is deprecated — use a space: wrangler kv namespace ..., wrangler kv key ... |
| Exceeded CPU time (Error 1102) | Free CPU is 10 ms; on Paid raise limits.cpu_ms (default 30 s, max 5 min) or offload to Queues/Durable Objects |
| KV stale reads | KV is eventually consistent; use D1 or Durable Objects for strong consistency |