Cloudflare + Claude Code 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
Egressis 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 egressbecause 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 + Claude Code 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), Pages, and 2,500+ API endpoints. Claude Code integrates natively through the official cloudflare/mcp-server-cloudflare MCP server and 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 MCP Servers | https://developers.cloudflare.com/agents/model-context-protocol/mcp-servers-for-cloudflare/ |
| Wrangler CLI | https://developers.cloudflare.com/workers/wrangler/ |
| Workers Docs | https://developers.cloudflare.com/workers/ |
| D1 Docs | https://developers.cloudflare.com/d1/ |
| R2 Docs | https://developers.cloudflare.com/r2/ |
MCP Server Setup
Official Cloudflare MCP Server
The official server is at github.com/cloudflare/mcp-server-cloudflare. It uses "Code Mode" — Claude writes JavaScript to call any of 2,500+ Cloudflare API endpoints via two unified tools: search() and execute().
# Add via Claude Code CLI (npx transport)
claude mcp add cloudflare -- npx -y @cloudflare/mcp-server-cloudflare.mcp.json Configuration
{
"mcpServers": {
"cloudflare": {
"command": "npx",
"args": ["-y", "@cloudflare/mcp-server-cloudflare"],
"env": {
"CLOUDFLARE_API_TOKEN": "${CLOUDFLARE_API_TOKEN}",
"CLOUDFLARE_ACCOUNT_ID": "${CLOUDFLARE_ACCOUNT_ID}"
}
}
}
}Available MCP Tools
| Tool | Description |
|---|---|
search | Search for any Cloudflare API endpoint by description |
execute | Execute any Cloudflare API operation by endpoint ID |
workers_list | List all Workers scripts |
workers_get_worker | Get Worker script code |
workers_get_worker_code | Fetch Worker source |
d1_database_query | Run SQL against a D1 database |
d1_databases_list | List D1 databases |
kv_namespaces_list | List KV namespaces |
kv_namespace_get | Read KV values |
r2_buckets_list | List R2 buckets |
accounts_list | List Cloudflare accounts |
CLI Integration (Wrangler)
Installation
npm install -g wranglerAuthentication
# 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
wrangler kv:namespace create MY_NAMESPACE
wrangler kv:key put --binding MY_NAMESPACE "key" "value"
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",
"compatibility_date": "2025-01-01",
"d1_databases": [
{ "binding": "DB", "database_name": "my-database", "database_id": "..." }
],
"kv_namespaces": [
{ "binding": "KV", "id": "..." }
]
}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": "2025-01-01",
"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 |
| Rate limiting | Cloudflare Rate Limiting via MCP API |
| DNS management | MCP execute DNS zone endpoints |
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 1MB | Enable wrangler deploy --minify or split into sub-workers |
| KV stale reads | KV is eventually consistent; use D1 for strong consistency |