Secure MCP: Deploying Protected Remote MCP Servers on Cloudflare Workers with Auth0

A hands-on tutorial for securing and deploying remote MCP (Model Context Protocol) servers using Cloudflare Workers and Auth0. It walks through two connected pieces — a JWT-protected REST API and an OAuth-protected remote MCP server — from local development to production deployment, and documents every real error you're likely to hit along the way.

Follow on XWhatsApp ChannelRedditLicense

PythonTypeScriptCloudflare WorkersAuth0Node.jsMCP

What this is

The Model Context Protocol (MCP) lets AI clients (Claude, IDEs, custom agents) call tools exposed by a server. When that server is remote — reachable over HTTP instead of running as a local subprocess — it needs the same access controls as any other API: you don't want an anonymous caller invoking tools that touch real user data.

This repo demonstrates the standard pattern for that: a resource server (a REST API) that validates JWTs on every request, and an MCP server that runs a full OAuth 2.1 authorization flow — login, consent, token exchange — before it lets an MCP client call its tools. Both are deployed as Cloudflare Workers; both are secured by Auth0.

Repository layout

secure-mcp/
├── main.py                              # placeholder Python entry point (this repo's own package)
├── pyproject.toml                       # Python project metadata (FastMCP dependency)
├── cloudflare-todoapi-github/
│   └── ai/
│       └── demos/
│           └── remote-mcp-auth0/
│               ├── todos-api/           # Part 1 — Auth0-protected REST API (Hono + Cloudflare Workers)
│               └── mcp-auth0-oidc/      # Part 2 — Remote MCP server with Auth0 OAuth login
└── README.md

cloudflare-todoapi-github/ai is a vendored copy of cloudflare/ai, which ships a large collection of Workers AI / MCP demos. This tutorial focuses on the two folders under demos/remote-mcp-auth0/.

Prerequisites

  • Node.js 18+ and npm
  • Python 3.12+ (only needed if you extend the secure-mcp Python package itself; the two demo projects are pure TypeScript)
  • A free Cloudflare account — Workers' free tier (100k requests/day) is enough for this whole tutorial
  • A free Auth0 account
  • curl (ships with Windows 10+ as curl.exe) or PowerShell's Invoke-RestMethod
  • The MCP Inspector — no install needed, run via npx

Windows / PowerShell users: copy-pasted curl commands from documentation use bash syntax (\ line continuation, single quotes) that PowerShell doesn't understand. See the PowerShell cheat sheet below before you start pasting commands.

Part 1 — Secure REST API (todos-api)

This is a small Hono app that exposes a few endpoints and validates every request's Authorization: Bearer <token> header against Auth0's public keys (JWKS) before responding.

Route Auth required Scope required
GET /api/health No
GET /api/me Yes (valid JWT)
GET /api/todos Yes read:todos
GET /api/billing Yes read:billing

1. Create the Auth0 API

In the Auth0 Dashboard, go to Applications → APIs → Create API:

  • Name: todos-api (or anything)
  • Identifier: urn:todos-api — this becomes the audience value everywhere below

Under the API's Permissions tab, add read:todos and read:billing.

2. Configure local secrets

cd cloudflare-todoapi-github/ai/demos/remote-mcp-auth0/todos-api
npm install

Create a .dev.vars file in this folder (never committed — it's in .gitignore):

AUTH0_DOMAIN=your-tenant.us.auth0.com
AUTH0_AUDIENCE=urn:todos-api

3. Run it locally

npm run dev

Wrangler starts the Worker at http://127.0.0.1:8789.

4. Get an access token

Create a Machine to Machine application in Auth0 (Applications → Create Application → Machine to Machine), authorize it for the todos-api API, and grant it the scopes you want to test.

$body = @{
    client_id     = "YOUR_M2M_CLIENT_ID"
    client_secret = "YOUR_M2M_CLIENT_SECRET"
    audience      = "urn:todos-api"
    grant_type    = "client_credentials"
} | ConvertTo-Json

$response = Invoke-RestMethod -Method Post -Uri "https://your-tenant.us.auth0.com/oauth/token" -ContentType "application/json" -Body $body
$token = $response.access_token

5. Call the protected API

Invoke-RestMethod -Method Get -Uri "http://127.0.0.1:8789/api/me" -Headers @{ authorization = "Bearer $token" }

You should see the token's claims echoed back (iss, sub, aud, scope, …). A 401 Unauthorized at this point almost always means a typo in AUTH0_DOMAIN/AUTH0_AUDIENCE, or the token's aud claim doesn't match urn:todos-api.

6. Deploy

npx wrangler deploy

Then set the same secrets on the deployed Worker. Interactive wrangler secret put on Windows is prone to mangling pasted values (stray quotes / trailing newlines end up baked into the secret) — the reliable way is a bulk file:

Set-Content -Path secrets.json -Encoding ascii -Value '{"AUTH0_DOMAIN":"your-tenant.us.auth0.com","AUTH0_AUDIENCE":"urn:todos-api"}'
npx wrangler secret bulk secrets.json
Remove-Item secrets.json

Secret names are case- and character-exact — AUTH0_DOMAIN and AUTHO_DOMAIN (letter O instead of zero) look identical at a glance in the dashboard and will silently break auth with a confusing 500 Internal Server Error instead of a clean 401:

7. Test the deployment

curl.exe --request GET --url "https://your-worker-name.workers.dev/api/me" --header "authorization: Bearer $token"

Part 2 — Secure Remote MCP Server (mcp-auth0-oidc)

This Worker exposes an MCP endpoint at /mcp and wraps it in a full OAuth 2.1 Authorization Code flow: an MCP client that hasn't authenticated gets redirected through Auth0 login and a consent screen before it can call any tool. It uses the todos-api from Part 1 as the downstream resource it acts on behalf of the user.

1. Create an Auth0 Regular Web Application

Applications → Create Application → Regular Web Application.

Under Settings → Allowed Callback URLs, add (comma-separated as you add more later):

http://localhost:8788/callback

Note the Client ID and Client Secret — you'll need both.

2. Authorize this application for the API

This step is easy to miss and produces an opaque error if skipped. Go to Applications → APIs → todos-api → Machine To Machine Applications (or the app's own APIs tab) and explicitly authorize the MCP server's application, ticking the scopes it should be able to request (read:todos, etc.). Without this, Auth0 rejects the login redirect with:

error_description: Client "..." is not authorized to access resource server "urn:todos-api".

even though the application type and callback URL are both correct.

3. Create a KV namespace

The OAuth provider uses Workers KV to store authorization state:

cd ../mcp-auth0-oidc
npx wrangler kv namespace create OAUTH_KV

Copy the returned id into wrangler.jsonc under the matching kv_namespaces binding.

4. Configure local secrets

npm install --legacy-peer-deps

The --legacy-peer-deps flag is needed here: the agents package pins @cloudflare/workers-types@^4.x while the latest wrangler wants ^5.x. It's a soft (peerOptional) conflict — safe to bypass, not a real incompatibility.

Create .dev.vars:

AUTH0_DOMAIN=your-tenant.us.auth0.com
AUTH0_CLIENT_ID=your-regular-web-app-client-id
AUTH0_CLIENT_SECRET=your-regular-web-app-client-secret
AUTH0_AUDIENCE=urn:todos-api
AUTH0_SCOPE=openid profile email offline_access read:todos
API_BASE_URL=http://127.0.0.1:8789
NODE_ENV=development

Plus a random signing key for the consent-approval cookie:

node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
COOKIE_ENCRYPTION_KEY=<paste the generated hex string>

Without COOKIE_ENCRYPTION_KEY, approving the consent screen fails server-side with Error: cookieSecret is required for signing cookies.

5. Run both Workers together

The MCP server calls the Todos API, so run both at once, in separate terminals:

# terminal 1
cd todos-api
npm run dev        # http://127.0.0.1:8789

# terminal 2
cd mcp-auth0-oidc
npm run dev         # http://127.0.0.1:8788

6. Test with the MCP Inspector

# terminal 3
npx @modelcontextprotocol/inspector

This opens a browser tab with a session-token URL. In the sidebar:

  • Transport Type: Streamable HTTP (not SSE, not the default STDIO)
  • URL: http://localhost:8788/mcp
  • Click Connect

You'll be redirected through Auth0 login, then a consent screen ("MCP Inspector is requesting access…"). Approve it, and the Inspector connects and lists the server's tools.

If Approve fails with "Missing CSRF token cookie": this is a browser issue, not a config issue. The consent cookie is set with Secure and the __Host- name prefix, which some browsers (Brave with Shields on, in particular) refuse to store on http://localhost. Use Chrome/Edge, or disable Shields for the page, and retry the full connect flow (the CSRF token is one-time-use, so a stale attempt won't work — reconnect from scratch).

7. Deploy

npx wrangler deploy

Set the same variables as secrets on the deployed Worker (via wrangler secret bulk, as in Part 1), then add the deployed callback URL to the Auth0 application's Allowed Callback URLs:

http://localhost:8788/callback, https://your-mcp-worker.workers.dev/callback

Security notes

  • JWT validation is signature-based, not a database lookup. The API fetches Auth0's public keys once (JWKS) and verifies every token's signature, issuer, and audience locally — no round-trip to Auth0 per request.
  • Scopes enforce least privilege. A valid, correctly-signed token with no scope claim can still authenticate (/api/me works) but is rejected by scope-gated routes (403 Forbidden) — authentication and authorization are checked separately.
  • .dev.vars is local-only. Cloudflare Workers have no filesystem in production; wrangler dev reads .dev.vars, but a deployed Worker only sees values pushed via wrangler secret put / wrangler secret bulk. Never commit .dev.vars — it's excluded in this repo's .gitignore.
  • CSRF + signed cookies protect the consent flow. The MCP server's OAuth implementation issues a one-time CSRF token bound to a cookie before rendering the consent form, and signs the "approved clients" cookie with COOKIE_ENCRYPTION_KEY so it can't be forged client-side.
  • Rotate anything that leaked. If a client secret or access token is ever pasted into a chat log, shared terminal, or committed by mistake, rotate it in the Auth0 dashboard (Application → Settings → Rotate Secret) — treat exposure as compromise regardless of how it happened.

PowerShell cheat sheet

Most MCP/Auth0 tutorials show bash-style curl. On Windows PowerShell, translate:

Bash PowerShell
curl ... curl.exe ... (plain curl is aliased to Invoke-WebRequest, which takes different flags)
\ at end of line ` (backtick) at end of line — nothing may follow it, not even a space
'single quotes' "double quotes" (needed for $variable expansion)
multi-line JSON body PowerShell here-string: $json = @'{ ... }'@ — closing '@ must start at column 0

For scripted calls, Invoke-RestMethod is usually more convenient than curl — it parses JSON responses into objects automatically:

$response = Invoke-RestMethod -Method Post -Uri "..." -ContentType "application/json" -Body $json
$response.access_token

Troubleshooting

Symptom Cause Fix
npm error ERESOLVE ... @cloudflare/workers-types A dependency pins workers-types v4 while wrangler wants v5 npm install --legacy-peer-deps
Missing expression after unary operator '--' in PowerShell Pasted a bash curl command with \ line continuations Replace \ with `, use curl.exe
404 Not Found on a valid-looking URL Hit a route the app doesn't define (e.g. / instead of /api/me) Check the server's route table in its source
401 with WWW-Authenticate: ... "Token verification failure" Deployed secret values don't exactly match what the token was issued for (typo, stray whitespace) Re-push secrets with wrangler secret bulk; verify names with wrangler secret list
500 Internal Server Error, no useful browser message Unhandled exception in the Worker (often a missing/misnamed secret) npx wrangler tail while reproducing — prints the real exception
Inspector: 'fastmcp' is not recognized... / spawn fastmcp ENOENT Inspector UI kept a stale STDIO transport config from a previous (unrelated) server In the sidebar, switch Transport Type to Streamable HTTP and set the URL explicitly
Missing CSRF token cookie on consent approval Browser refused a Secure/__Host- cookie on http://localhost Use Chrome/Edge, or disable Brave Shields for the page; reconnect from scratch
Error: cookieSecret is required for signing cookies COOKIE_ENCRYPTION_KEY missing from .dev.vars Generate one, add it, restart wrangler dev
Callback URL mismatch on the Auth0 login page redirect_uri the app sent isn't in Allowed Callback URLs Add the exact URL (scheme + host + port + path) in Auth0 Application settings
Client "..." is not authorized to access resource server "..." The application isn't linked to the API in Auth0 Authorize the app under the API's Machine To Machine Applications tab, with the needed scopes

Credits

License

MIT

If this helped you, a follow keeps more of this coming: X / Twitter · WhatsApp Channel · Reddit

MCP Server · Populars

MCP Server · New

    DareDev256

    FCPXML MCP

    🎬 The first AI-powered MCP server for Final Cut Pro XML. Control your edits with natural language.

    Community DareDev256
    acunningham-ship-it

    Veil

    Stealth browser for AI agents — real Chrome over raw CDP, no Playwright/Puppeteer. TypeScript + MCP-native. Passes sannysoft 57/57, bypasses Cloudflare.

    Cassette-Editor

    Oh My Cassette: Chat Your Raw Clips Into a Finished Cut

    你的随身 AI 剪辑搭档 | Pocket AI co-editor for video montage — AI video editing plugin & MCP server for Claude Code, Codex, Hermes & OpenCode

    Community Cassette-Editor
    trendsmcp-ai

    Trends MCP

    MCP server for live trend data. Query Google Search, YouTube, TikTok, Reddit, Amazon, Wikipedia, News sentiment, Web Traffic, App Downloads, Steam, npm and more. Works with Claude, Cursor, VS Code, GitHub Copilot, ChatGPT, Windsurf, Cline, Raycast and any MCP-compatible.

    Community trendsmcp-ai
    jacob-bd

    Gemini Notebook (formerly Google NotebookLM) CLI & MCP Server

    Programmatic access to Gemini Notebook - via command-line interface (CLI), Model Context Protocol (MCP) server, and AI agent skills.

    Community jacob-bd