// |
| name | cloudflare-mcp-server |
| description | Build Model Context Protocol (MCP) servers on Cloudflare Workers - the only platform with official remote MCP support. TypeScript-based with OAuth, Durable Objects, and WebSocket hibernation. Use when: deploying remote MCP servers, implementing OAuth (GitHub/Google), using dual transports (SSE/HTTP), or troubleshooting URL path mismatches, McpAgent exports, OAuth redirects, CORS issues. |
| license | MIT |
| allowed-tools | ["Read","Write","Edit","Bash","Glob","Grep"] |
| metadata | {"version":"2.1.1","last_verified":"2025-11-08","sdk_versions":{"mcp_sdk":"1.21.0","oauth_provider":"0.0.13","agents_sdk":"0.2.20"},"official_examples":"https://github.com/cloudflare/ai/tree/main/demos","cloudflare_docs":"https://developers.cloudflare.com/agents/model-context-protocol/"} |
Build and deploy Model Context Protocol (MCP) servers on Cloudflare Workers with TypeScript.
This skill teaches you to build remote MCP servers on Cloudflare - the ONLY platform with official remote MCP support as of 2025.
Use this skill when:
You'll learn:
Before using this skill's templates, know that Cloudflare provides official starter templates via npm create.
For most projects, start with Cloudflare's official authless template:
npm create cloudflare@latest -- my-mcp-server \
--template=cloudflare/ai/demos/remote-mcp-authless
cd my-mcp-server
npm install
npm run dev
What you get:
Then customize with patterns from this skill to avoid the 22+ common errors!
Cloudflare maintains 14+ official MCP templates. Use these as starting points:
| Template Command | Purpose | When to Use |
|---|---|---|
--template=cloudflare/ai/demos/remote-mcp-authless | Gold standard starter - No auth, simple tools | New projects, learning, public APIs |
--template=cloudflare/ai/demos/remote-mcp-github-oauth | GitHub OAuth + Workers AI | Developer tools, GitHub integrations |
--template=cloudflare/ai/demos/remote-mcp-google-oauth | Google OAuth | Google Workspace integration |
| Template Command | Auth Method | Use Case |
|---|---|---|
--template=cloudflare/ai/demos/remote-mcp-auth0 | Auth0 | Enterprise SSO |
--template=cloudflare/ai/demos/remote-mcp-authkit | WorkOS AuthKit | B2B SaaS applications |
--template=cloudflare/ai/demos/remote-mcp-logto | Logto | Open-source auth |
--template=cloudflare/ai/demos/remote-mcp-cf-access | Cloudflare Access | Internal company tools |
--template=cloudflare/ai/demos/mcp-server-bearer-auth | Bearer tokens | Custom auth systems |
| Template Command | Demonstrates | Cloudflare Services |
|---|---|---|
--template=cloudflare/ai/demos/remote-mcp-server-autorag | RAG (Retrieval-Augmented Generation) | Workers AI + Vectorize |
--template=cloudflare/ai/demos/python-workers-mcp | Python MCP servers | Python Workers |
Use this skill's templates when:
mcp-http-fundamentals.ts)This skill's templates are MORE educational than Cloudflare's (more comments, defensive patterns, error handling).
Cloudflare's templates are FASTER to start with (minimal, production-ready).
Best approach: Start with Cloudflare's template, then reference this skill to avoid errors!
Cloudflare maintains 15 production MCP servers showing real-world integration patterns:
Key servers to study:
workers-bindings - D1, KV, R2, AI, Durable Objects usagebrowser-rendering - Web scraping + screenshot toolsautorag - Vectorize RAG patternai-gateway - Workers AI Gateway analyticsdocs - Cloudflare documentation searchRepository: https://github.com/cloudflare/mcp-server-cloudflare
Why study these? They show production-grade patterns for:
Follow this workflow for your next MCP server to avoid errors and ship fast.
Decision tree:
What are you building?
├─ 🆓 Public/dev server (no auth needed)
│ └─> Use: remote-mcp-authless ⭐ RECOMMENDED FOR MOST PROJECTS
│
├─ 🔐 GitHub integration
│ └─> Use: remote-mcp-github-oauth (includes Workers AI example)
│
├─ 🔐 Google Workspace integration
│ └─> Use: remote-mcp-google-oauth
│
├─ 🏢 Enterprise SSO (Auth0, Okta, etc.)
│ └─> Use: remote-mcp-auth0 or remote-mcp-authkit
│
├─ 🔑 Custom auth system / API keys
│ └─> Start with authless, then add bearer auth (see Step 3)
│
└─ 🏠 Internal company tool
└─> Use: remote-mcp-cf-access (Cloudflare Zero Trust)
Not sure? Start with remote-mcp-authless - you can add auth later!
# Replace [TEMPLATE] with your choice from Step 1
npm create cloudflare@latest -- my-mcp-server \
--template=cloudflare/ai/demos/[TEMPLATE]
# Example: authless template (most common)
npm create cloudflare@latest -- my-mcp-server \
--template=cloudflare/ai/demos/remote-mcp-authless
# Navigate and install
cd my-mcp-server
npm install
# Start dev server
npm run dev
Your MCP server is now running at: http://localhost:8788/sse
Now add features by copying patterns from this skill:
# Copy our Workers AI template
cp ~/.claude/skills/cloudflare-mcp-server/templates/mcp-with-workers-ai.ts src/my-ai-tools.ts
# Add AI binding to wrangler.jsonc:
# { "ai": { "binding": "AI" } }
Tools you get: generate_image, generate_text, list_ai_models
# Copy our D1 template
cp ~/.claude/skills/cloudflare-mcp-server/templates/mcp-with-d1.ts src/my-db-tools.ts
# Create D1 database:
npx wrangler d1 create my-database
# Add binding to wrangler.jsonc
Tools you get: create_user, get_user, list_users, update_user, delete_user, search_users
# Copy our bearer auth template
cp ~/.claude/skills/cloudflare-mcp-server/templates/mcp-bearer-auth.ts src/index.ts
# Add token validation (KV, external API, or static)
What you get: Authorization header middleware, token validation, authenticated tools
# Login (first time only)
npx wrangler login
# Deploy to production
npx wrangler deploy
Output shows your deployed URL:
✨ Deployment complete!
https://my-mcp-server.YOUR_ACCOUNT.workers.dev
⚠️ CRITICAL: Note this URL - you'll need it in Step 5!
# Test the exact URL you'll use in client config
curl https://my-mcp-server.YOUR_ACCOUNT.workers.dev/sse
Expected response:
{
"name": "My MCP Server",
"version": "1.0.0",
"transports": ["/sse", "/mcp"]
}
Got 404? → Your client URL will be wrong! See "HTTP Transport Fundamentals" below.
Linux/Mac: ~/.config/claude/claude_desktop_config.json
Windows: %APPDATA%/Claude/claude_desktop_config.json
For authless servers:
{
"mcpServers": {
"my-mcp": {
"url": "https://my-mcp-server.YOUR_ACCOUNT.workers.dev/sse"
}
}
}
⚠️ CRITICAL: URL must match the curl command that worked in Step 5A!
With OAuth:
{
"mcpServers": {
"my-mcp": {
"url": "https://my-mcp-server.YOUR_ACCOUNT.workers.dev/sse",
"auth": {
"type": "oauth",
"authorizationUrl": "https://my-mcp-server.YOUR_ACCOUNT.workers.dev/authorize",
"tokenUrl": "https://my-mcp-server.YOUR_ACCOUNT.workers.dev/token"
}
}
}
}
All three URLs must use the same domain!
Config only loads at startup:
Test a tool call:
If tools don't appear → See "Debugging Guide" in references/
Before declaring success, verify:
curl https://worker.dev/sse returns server info (not 404)npx wrangler tail logsAll checked? 🎉 Your MCP server is live!
Want to add more features?
init() method in your McpAgent classmcp-with-workers-ai.tsmcp-with-d1.tsmcp-bearer-auth.ts or mcp-oauth-proxy.tsmcp-stateful-do.tsWant to avoid errors?
references/debugging-guide.md when stuck# 1. Create from template (30 seconds)
npm create cloudflare@latest -- my-mcp \
--template=cloudflare/ai/demos/remote-mcp-authless
cd my-mcp && npm install
# 2. Customize (optional, 2 minutes)
# Copy patterns from this skill if needed
# 3. Deploy (30 seconds)
npx wrangler deploy
# 4. Test (30 seconds)
curl https://YOUR-WORKER.workers.dev/sse
# 5. Configure client (1 minute)
# Update claude_desktop_config.json with URL from step 4
# Restart Claude Desktop
# 6. Verify (30 seconds)
# Test a tool call in Claude Desktop
Total time: ~5 minutes from zero to working MCP server! 🚀
The #1 reason MCP servers fail to connect is URL path configuration mistakes.
When you serve an MCP server at a specific path, the client URL must match exactly.
Example 1: Serving at /sse
// src/index.ts
export default {
fetch(request: Request, env: Env, ctx: ExecutionContext) {
const { pathname } = new URL(request.url);
if (pathname.startsWith("/sse")) {
return MyMCP.serveSSE("/sse").fetch(request, env, ctx); // ← Base path is "/sse"
}
return new Response("Not Found", { status: 404 });
}
};
Client configuration MUST include /sse:
{
"mcpServers": {
"my-mcp": {
"url": "https://my-mcp.workers.dev/sse" // ✅ Correct
}
}
}
❌ WRONG client configurations:
"url": "https://my-mcp.workers.dev" // Missing /sse → 404
"url": "https://my-mcp.workers.dev/" // Missing /sse → 404
"url": "http://localhost:8788" // Wrong after deploy
Example 2: Serving at / (root)
export default {
fetch(request: Request, env: Env, ctx: ExecutionContext) {
return MyMCP.serveSSE("/").fetch(request, env, ctx); // ← Base path is "/"
}
};
Client configuration:
{
"mcpServers": {
"my-mcp": {
"url": "https://my-mcp.workers.dev" // ✅ Correct (no /sse)
}
}
}
When you call serveSSE("/sse"), MCP tools are served at:
https://my-mcp.workers.dev/sse/tools/list
https://my-mcp.workers.dev/sse/tools/call
https://my-mcp.workers.dev/sse/resources/list
When you call serveSSE("/"), MCP tools are served at:
https://my-mcp.workers.dev/tools/list
https://my-mcp.workers.dev/tools/call
https://my-mcp.workers.dev/resources/list
The base path is prepended to all MCP endpoints automatically.
1. Client connects to: https://my-mcp.workers.dev/sse
↓
2. Worker receives request: { url: "https://my-mcp.workers.dev/sse", ... }
↓
3. Your fetch handler: const { pathname } = new URL(request.url)
↓
4. pathname === "/sse" → Check passes
↓
5. MyMCP.serveSSE("/sse").fetch() → MCP server handles request
↓
6. Tool calls routed to: /sse/tools/call
If client connects to https://my-mcp.workers.dev (missing /sse):
pathname === "/" → Check fails → 404 Not Found
Step 1: Deploy your MCP server
npx wrangler deploy
# Output: Deployed to https://my-mcp.YOUR_ACCOUNT.workers.dev
Step 2: Test the base path with curl
# If serving at /sse, test this URL:
curl https://my-mcp.YOUR_ACCOUNT.workers.dev/sse
# Should return MCP server info (not 404)
Step 3: Update client config with the EXACT URL you tested
{
"mcpServers": {
"my-mcp": {
"url": "https://my-mcp.YOUR_ACCOUNT.workers.dev/sse" // Match curl URL
}
}
}
Step 4: Restart Claude Desktop
After deploying, verify:
curl https://worker.dev/sse returns MCP server info (not 404)workes.dev instead of workers.dev)https:// (not http://) for deployed WorkersMCP supports two transport methods: SSE (legacy) and Streamable HTTP (2025 standard).
Best for: Wide client compatibility (2024 clients), legacy support
Serving:
MyMCP.serveSSE("/sse").fetch(request, env, ctx)
Client config:
{
"url": "https://my-mcp.workers.dev/sse"
}
Pros:
Cons:
Best for: Modern clients (2025+), better performance
Serving:
MyMCP.serve("/mcp").fetch(request, env, ctx)
Client config:
{
"url": "https://my-mcp.workers.dev/mcp"
}
Pros:
Cons:
Serve both transports for maximum compatibility:
export default {
fetch(request: Request, env: Env, ctx: ExecutionContext) {
const { pathname } = new URL(request.url);
// SSE transport (legacy)
if (pathname.startsWith("/sse")) {
return MyMCP.serveSSE("/sse").fetch(request, env, ctx);
}
// HTTP transport (2025 standard)
if (pathname.startsWith("/mcp")) {
return MyMCP.serve("/mcp").fetch(request, env, ctx);
}
// Health check endpoint (optional but recommended)
if (pathname === "/" || pathname === "/health") {
return new Response(
JSON.stringify({
name: "My MCP Server",
version: "1.0.0",
transports: {
sse: "/sse",
http: "/mcp"
},
status: "ok",
timestamp: new Date().toISOString()
}),
{
headers: { "Content-Type": "application/json" },
status: 200
}
);
}
return new Response("Not Found", { status: 404 });
}
};
Why this works:
/sse/mcp/ or /healthCRITICAL: Use pathname.startsWith() to match paths correctly!
Now that you understand URL configuration, let's build your first MCP server.
Use the mcp-http-fundamentals.ts template - the simplest working example.
# Copy minimal template
cp ~/.claude/skills/cloudflare-mcp-server/templates/mcp-http-fundamentals.ts src/index.ts
# Install dependencies
npm install
# Start dev server
npm run dev
# Test connection
curl http://localhost:8788/sse
# Should return: {"name":"My MCP Server","version":"1.0.0",...}
# Create new MCP server from official template
npm create cloudflare@latest -- my-mcp-server \
--template=cloudflare/ai/demos/remote-mcp-authless
cd my-mcp-server
npm install
npm run dev
Your MCP server is now running at http://localhost:8788/sse
# In a new terminal
npx @modelcontextprotocol/inspector@latest
# Open http://localhost:5173
# Enter: http://localhost:8788/sse
# Click "Connect" and test tools
# Deploy
npx wrangler deploy
# Output shows your URL:
# https://my-mcp-server.YOUR_ACCOUNT.workers.dev
# ⚠️ REMEMBER: Update client config with this URL + /sse!
The McpAgent base class from Cloudflare's Agents SDK provides:
Basic pattern:
import { McpAgent } from "agents/mcp";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
export class MyMCP extends McpAgent<Env> {
server = new McpServer({
name: "My MCP Server",
version: "1.0.0"
});
async init() {
// Register tools here
this.server.tool(
"tool_name",
"Tool description",
{ param: z.string() },
async ({ param }) => ({
content: [{ type: "text", text: "Result" }]
})
);
}
}
Tools are functions that MCP clients can invoke. Use Zod for parameter validation.
Pattern:
this.server.tool(
"tool_name", // Tool identifier
"Tool description", // What it does (for LLM)
{ // Parameters (Zod schema)
param1: z.string().describe("Parameter description"),
param2: z.number().optional()
},
async ({ param1, param2 }) => { // Handler
// Your logic here
return {
content: [{ type: "text", text: "Result" }]
};
}
);
Best practices:
{ isError: true } for failuresCloudflare MCP servers support 4 authentication patterns:
Use case: Internal tools, development, public APIs
Template: templates/mcp-http-fundamentals.ts
Setup: None required
Security: ⚠️ Anyone can access your MCP server
Use case: Pre-authenticated clients, custom auth systems
How it works: Client sends Bearer token, server validates
Template: Create custom JWTVerifier middleware
Setup:
import { JWTVerifier } from "agents/mcp";
const verifier = new JWTVerifier({
secret: env.JWT_SECRET,
issuer: "your-auth-server"
});
// Validate token before serving MCP requests
Security: ✅ Secure if tokens are properly managed
Use case: GitHub, Google, Azure OAuth integration
How it works: Cloudflare Worker proxies OAuth to third-party provider
Template: templates/mcp-oauth-proxy.ts
Setup:
import { OAuthProvider, GitHubHandler } from "@cloudflare/workers-oauth-provider";
export default new OAuthProvider({
authorizeEndpoint: "/authorize",
tokenEndpoint: "/token",
clientRegistrationEndpoint: "/register",
defaultHandler: new GitHubHandler({
clientId: (env) => env.GITHUB_CLIENT_ID,
clientSecret: (env) => env.GITHUB_CLIENT_SECRET,
scopes: ["repo", "user:email"],
context: async (accessToken) => {
// Fetch user info from GitHub
const octokit = new Octokit({ auth: accessToken });
const { data: user } = await octokit.rest.users.getAuthenticated();
return {
login: user.login,
email: user.email,
accessToken
};
}
}),
kv: (env) => env.OAUTH_KV,
apiHandlers: {
"/sse": MyMCP.serveSSE("/sse"),
"/mcp": MyMCP.serve("/mcp")
},
allowConsentScreen: true,
allowDynamicClientRegistration: true
});
Required bindings:
{
"kv_namespaces": [
{ "binding": "OAUTH_KV", "id": "YOUR_KV_ID" }
]
}
⚠️ CRITICAL OAuth URL Configuration: When using OAuth, your redirect URIs MUST match:
{
"mcpServers": {
"my-mcp": {
"url": "https://my-mcp.YOUR_ACCOUNT.workers.dev/sse",
"auth": {
"type": "oauth",
"authorizationUrl": "https://my-mcp.YOUR_ACCOUNT.workers.dev/authorize",
"tokenUrl": "https://my-mcp.YOUR_ACCOUNT.workers.dev/token"
}
}
}
}
All URLs must use the same domain and protocol (https://).
Security: ✅✅ Secure, production-ready
Use case: Full OAuth provider, custom consent screens
How it works: Your Worker is the OAuth provider
Template: See Cloudflare's remote-mcp-authkit demo
Setup: Complex, requires full OAuth 2.1 implementation
Security: ✅✅✅ Most secure, full control
Use Durable Objects when your MCP server needs:
Template: templates/mcp-stateful-do.ts
Store values:
await this.state.storage.put("key", "value");
await this.state.storage.put("user_prefs", { theme: "dark" });
Retrieve values:
const value = await this.state.storage.get<string>("key");
const prefs = await this.state.storage.get<object>("user_prefs");
List keys:
const allKeys = await this.state.storage.list();
Delete keys:
await this.state.storage.delete("key");
wrangler.jsonc:
{
"durable_objects": {
"bindings": [
{
"name": "MY_MCP",
"class_name": "MyMCP",
"script_name": "my-mcp-server"
}
]
},
"migrations": [
{ "tag": "v1", "new_classes": ["MyMCP"] }
]
}
IMPORTANT: Migrations are required on first deployment!
Problem: Long-lived WebSocket connections cost CPU time
Solution: WebSocket Hibernation API suspends connections when idle
Serialize metadata (preserves data during hibernation):
webSocket.serializeAttachment({
userId: "123",
sessionId: "abc",
connectedAt: Date.now()
});
Retrieve on wake:
const metadata = webSocket.deserializeAttachment();
console.log(metadata.userId); // "123"
Storage for persistent state:
// ❌ DON'T: In-memory state lost on hibernation
this.userId = "123";
// ✅ DO: Use storage API
await this.state.storage.put("userId", "123");
Without hibernation:
With hibernation:
Use case: Wrap external API with MCP tools
Pattern:
this.server.tool(
"search_wikipedia",
"Search Wikipedia for a topic",
{ query: z.string() },
async ({ query }) => {
const response = await fetch(
`https://en.wikipedia.org/api/rest_v1/page/summary/${encodeURIComponent(query)}`
);
const data = await response.json();
return {
content: [{
type: "text",
text: data.extract
}]
};
}
);
Use case: Query D1, KV, or external databases
Pattern:
this.server.tool(
"get_user",
"Get user details from database",
{ userId: z.string() },
async ({ userId }) => {
// Query Durable Objects storage
const user = await this.state.storage.get<User>(`user:${userId}`);
// Or query D1 database
const result = await env.DB.prepare(
"SELECT * FROM users WHERE id = ?"
).bind(userId).first();
return {
content: [{
type: "text",
text: JSON.stringify(user || result, null, 2)
}]
};
}
);
Use case: Tools that call other tools
Pattern:
// Store result from first tool
await this.state.storage.put("last_search", result);
// Second tool reads it
const lastSearch = await this.state.storage.get("last_search");
Use case: Cache expensive API calls
Pattern:
this.server.tool(
"get_weather",
"Get weather (cached 5 minutes)",
{ city: z.string() },
async ({ city }) => {
const cacheKey = `weather:${city}`;
const cached = await this.state.storage.get<CachedWeather>(cacheKey);
// Check cache freshness
if (cached && Date.now() - cached.timestamp < 5 * 60 * 1000) {
return {
content: [{ type: "text", text: cached.data }]
};
}
// Fetch fresh data
const weather = await fetchWeatherAPI(city);
// Cache it
await this.state.storage.put(cacheKey, {
data: weather,
timestamp: Date.now()
});
return {
content: [{ type: "text", text: weather }]
};
}
);
Use case: Prevent abuse, respect upstream rate limits
Pattern:
async rateLimit(key: string, maxRequests: number, windowMs: number): Promise<boolean> {
const now = Date.now();
const requests = await this.state.storage.get<number[]>(`ratelimit:${key}`) || [];
// Remove old requests outside window
const recentRequests = requests.filter(ts => now - ts < windowMs);
if (recentRequests.length >= maxRequests) {
return false; // Rate limited
}
// Add this request
recentRequests.push(now);
await this.state.storage.put(`ratelimit:${key}`, recentRequests);
return true; // Allowed
}
// Use in tool
if (!await this.rateLimit(userId, 10, 60 * 1000)) {
return {
content: [{ type: "text", text: "Rate limit exceeded (10 requests/minute)" }],
isError: true
};
}
Error: TypeError: Cannot read properties of undefined (reading 'serve')
Cause: Forgot to export McpAgent class
Solution:
export class MyMCP extends McpAgent { ... } // ✅ Must export
export default { fetch() { ... } }
Error: 404 Not Found or Connection failed
Cause: serveSSE("/sse") but client configured with https://worker.dev (missing /sse)
Solution: Match base paths exactly
// Server serves at /sse
MyMCP.serveSSE("/sse").fetch(...)
// Client MUST include /sse
{ "url": "https://worker.dev/sse" } // ✅ Correct
{ "url": "https://worker.dev" } // ❌ Wrong - 404
Debug steps:
serveSSE("/sse") vs serveSSE("/")curl https://worker.dev/sseError: Connection failed: Unexpected response format
Cause: Client expects SSE but connects to HTTP endpoint (or vice versa)
Solution: Match transport types
// SSE transport
MyMCP.serveSSE("/sse") // Client URL: https://worker.dev/sse
// HTTP transport
MyMCP.serve("/mcp") // Client URL: https://worker.dev/mcp
Best practice: Support both transports (see Transport Selection Guide)
Error: Both /sse and /mcp routes fail or conflict
Cause: Incorrect path matching logic
Solution: Use startsWith() correctly
// ✅ CORRECT
if (pathname.startsWith("/sse")) {
return MyMCP.serveSSE("/sse").fetch(...);
}
if (pathname.startsWith("/mcp")) {
return MyMCP.serve("/mcp").fetch(...);
}
// ❌ WRONG: Exact match breaks sub-paths
if (pathname === "/sse") { // Breaks /sse/tools/list
return MyMCP.serveSSE("/sse").fetch(...);
}
Error: Works in dev, fails after deployment
Cause: Client still configured with localhost URL
Solution: Update client config after deployment
// Development
{ "url": "http://localhost:8788/sse" }
// ⚠️ MUST UPDATE after npx wrangler deploy
{ "url": "https://my-mcp.YOUR_ACCOUNT.workers.dev/sse" }
Post-deployment checklist:
npx wrangler deploy and note output URLError: OAuth error: redirect_uri does not match
Cause: OAuth redirect URI doesn't match deployed URL
Solution: Update ALL OAuth URLs after deployment
{
"url": "https://my-mcp.YOUR_ACCOUNT.workers.dev/sse",
"auth": {
"type": "oauth",
"authorizationUrl": "https://my-mcp.YOUR_ACCOUNT.workers.dev/authorize", // Must match deployed domain
"tokenUrl": "https://my-mcp.YOUR_ACCOUNT.workers.dev/token"
}
}
CRITICAL: All URLs must use the same protocol and domain!
Error: Access to fetch at '...' blocked by CORS policy or Method Not Allowed
Cause: Browser clients send OPTIONS requests for CORS preflight, but server doesn't handle them
Solution: Add OPTIONS handler
export default {
fetch(request: Request, env: Env, ctx: ExecutionContext) {
// Handle CORS preflight
if (request.method === "OPTIONS") {
return new Response(null, {
status: 204,
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Max-Age": "86400"
}
});
}
// ... rest of your fetch handler
}
};
When needed: Browser-based MCP clients (like MCP Inspector in browser)
Error: TypeError: Cannot read properties of undefined or Unexpected token in JSON parsing
Cause: Client sends malformed JSON, server doesn't validate before parsing
Solution: Wrap request handling in try/catch
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
try {
// Your MCP server logic
return await MyMCP.serveSSE("/sse").fetch(request, env, ctx);
} catch (error) {
console.error("Request handling error:", error);
return new Response(
JSON.stringify({
error: "Invalid request",
details: error.message
}),
{
status: 400,
headers: { "Content-Type": "application/json" }
}
);
}
}
};
Error: TypeError: env.API_KEY is undefined or silent failures (tools return empty data)
Cause: Required environment variables not configured or missing at runtime
Solution: Add startup validation
export class MyMCP extends McpAgent<Env> {
async init() {
// Validate required environment variables
if (!this.env.API_KEY) {
throw new Error("API_KEY environment variable not configured");
}
if (!this.env.DATABASE_URL) {
throw new Error("DATABASE_URL environment variable not configured");
}
// Continue with tool registration
this.server.tool(...);
}
}
Configuration checklist:
.dev.vars (local only, gitignored)wrangler.jsonc vars (public) or use wrangler secret (sensitive)Best practices:
# .dev.vars (local development, gitignored)
API_KEY=dev-key-123
DATABASE_URL=http://localhost:3000
# wrangler.jsonc (public config)
{
"vars": {
"ENVIRONMENT": "production",
"LOG_LEVEL": "info"
}
}
# wrangler secret (production secrets)
npx wrangler secret put API_KEY
npx wrangler secret put DATABASE_URL
Error: TypeError: server.registerTool is not a function or this.server is undefined
Cause: Trying to use standalone SDK patterns with McpAgent class
Solution: Use McpAgent's this.server.tool() pattern
// ❌ WRONG: Mixing standalone SDK with McpAgent
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
const server = new McpServer({ name: "My Server" });
server.registerTool(...); // Not compatible with McpAgent!
export class MyMCP extends McpAgent { /* no server property */ }
// ✅ CORRECT: McpAgent pattern
export class MyMCP extends McpAgent<Env> {
server = new McpServer({
name: "My MCP Server",
version: "1.0.0"
});
async init() {
this.server.tool("tool_name", ...); // Use this.server
}
}
Key difference: McpAgent provides this.server property, standalone SDK doesn't.
Error: Tool calls fail after reconnect with "state not found"
Cause: In-memory state cleared on hibernation
Solution: Use this.state.storage instead of instance properties
// ❌ DON'T: Lost on hibernation
this.userId = "123";
// ✅ DO: Persists through hibernation
await this.state.storage.put("userId", "123");
Error: TypeError: Cannot read properties of undefined (reading 'idFromName')
Cause: Forgot DO binding in wrangler.jsonc
Solution: Add binding (see Stateful MCP Servers section)
{
"durable_objects": {
"bindings": [
{
"name": "MY_MCP",
"class_name": "MyMCP",
"script_name": "my-mcp-server"
}
]
}
}
Error: Error: Durable Object class MyMCP has no migration defined
Cause: First DO deployment requires migration
Solution:
{
"migrations": [
{ "tag": "v1", "new_classes": ["MyMCP"] }
]
}
Error: WebSocket metadata lost on hibernation wake
Cause: Not using serializeAttachment() to preserve connection metadata
Solution: See WebSocket Hibernation section
Security risk: Users don't see what permissions they're granting
Cause: allowConsentScreen: false in production
Solution: Always enable in production
export default new OAuthProvider({
allowConsentScreen: true, // ✅ Always true in production
// ...
});
Error: Error: JWT_SIGNING_KEY environment variable not set
Cause: OAuth Provider requires signing key for tokens
Solution:
# Generate secure key
openssl rand -base64 32
# Add to wrangler secret
npx wrangler secret put JWT_SIGNING_KEY
Error: ZodError: Invalid input type
Cause: Client sends string, schema expects number (or vice versa)
Solution: Use Zod transforms
// Accept string, convert to number
param: z.string().transform(val => parseInt(val, 10))
// Or: Accept both types
param: z.union([z.string(), z.number()]).transform(val =>
typeof val === "string" ? parseInt(val, 10) : val
)
Error: /sse returns 404 after adding /mcp
Cause: Incorrect path matching (missing startsWith())
Solution: Use startsWith() or exact matches correctly (see Error #4)
Error: OAuth flow fails in local dev, or Durable Objects behave differently
Cause: Miniflare doesn't support all DO features
Solution: Use npx wrangler dev --remote for full DO support
# Local simulation (faster but limited)
npm run dev
# Remote DOs (slower but accurate)
npx wrangler dev --remote
Error: Claude Desktop doesn't recognize server
Cause: Wrong JSON format in claude_desktop_config.json
Solution: See "Connect Claude Desktop" section for correct format
Common mistakes:
// ❌ WRONG: Missing "mcpServers" wrapper
{
"my-mcp": {
"url": "https://worker.dev/sse"
}
}
// ❌ WRONG: Trailing comma
{
"mcpServers": {
"my-mcp": {
"url": "https://worker.dev/sse", // ← Remove comma
}
}
}
// ✅ CORRECT
{
"mcpServers": {
"my-mcp": {
"url": "https://worker.dev/sse"
}
}
}
Issue: Can't tell if Worker is running or if URL is correct
Impact: Debugging connection issues takes longer
Solution: Add health check endpoint (see Transport Selection Guide)
Test:
curl https://my-mcp.workers.dev/health
# Should return: {"status":"ok","transports":{...}}
Error: Access to fetch at '...' blocked by CORS policy
Cause: MCP server doesn't return CORS headers for cross-origin requests
Solution: Add CORS headers to all responses
// Manual CORS (if not using OAuthProvider)
const corsHeaders = {
"Access-Control-Allow-Origin": "*", // Or specific origin
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization"
};
// Add to responses
return new Response(body, {
headers: {
...corsHeaders,
"Content-Type": "application/json"
}
});
Note: OAuthProvider handles CORS automatically!
# Start dev server (uses Miniflare for local DOs)
npm run dev
# Start dev server with remote Durable Objects (more accurate)
npx wrangler dev --remote
Access at: http://localhost:8788/sse
npx @modelcontextprotocol/inspector@latest
http://localhost:5173# First time: Login
npx wrangler login
# Deploy
npx wrangler deploy
# Output shows your deployed URL:
# https://my-mcp-server.YOUR_ACCOUNT.workers.dev
# ⚠️ CRITICAL: Update client config with this URL!
# Check deployment logs
npx wrangler tail
~/.config/claude/claude_desktop_config.json (Linux/Mac):
{
"mcpServers": {
"my-mcp": {
"url": "https://my-mcp-server.YOUR_ACCOUNT.workers.dev/sse"
}
}
}
%APPDATA%/Claude/claude_desktop_config.json (Windows)
With OAuth:
{
"mcpServers": {
"my-mcp": {
"url": "https://my-mcp-oauth.YOUR_ACCOUNT.workers.dev/sse",
"auth": {
"type": "oauth",
"authorizationUrl": "https://my-mcp-oauth.YOUR_ACCOUNT.workers.dev/authorize",
"tokenUrl": "https://my-mcp-oauth.YOUR_ACCOUNT.workers.dev/token"
}
}
}
}
⚠️ REMEMBER: Restart Claude Desktop after config changes!
{
"name": "my-mcp-server",
"main": "src/index.ts",
"compatibility_date": "2025-01-01",
"compatibility_flags": ["nodejs_compat"],
"account_id": "YOUR_ACCOUNT_ID",
"vars": {
"ENVIRONMENT": "production",
"GITHUB_CLIENT_ID": "optional-pre-configured-id"
},
"kv_namespaces": [
{
"binding": "OAUTH_KV",
"id": "YOUR_KV_ID",
"preview_id": "YOUR_PREVIEW_KV_ID"
}
],
"durable_objects": {
"bindings": [
{
"name": "MY_MCP",
"class_name": "MyMCP",
"script_name": "my-mcp-server"
}
]
},
"migrations": [
{ "tag": "v1", "new_classes": ["MyMCP"] }
],
"node_compat": true
}
See templates/package.json
See templates/claude_desktop_config.json
Self-contained section for standalone use
Workers must export a fetch handler:
export default {
fetch(request: Request, env: Env, ctx: ExecutionContext): Response | Promise<Response> {
// Handle request
return new Response("Hello");
}
};
DOs extend McpAgent (for MCP servers):
export class MyMCP extends McpAgent<Env> {
constructor(state: DurableObjectState, env: Env) {
super(state, env);
}
// Your methods here
}
Environment bindings give Workers access to resources:
{
"kv_namespaces": [{ "binding": "MY_KV", "id": "..." }],
"durable_objects": {
"bindings": [{ "name": "MY_DO", "class_name": "MyDO" }]
},
"r2_buckets": [{ "binding": "MY_BUCKET", "bucket_name": "..." }]
}
Access in code:
env.MY_KV.get("key");
env.MY_DO.idFromName("session-123").getStub(env);
env.MY_BUCKET.get("file.txt");
references/http-transport-fundamentals.md - Deep dive on URL paths and routingreferences/transport-comparison.md - SSE vs HTTP technical detailsreferences/debugging-guide.md - Common connection issues + fixesreferences/authentication.md - Auth patterns comparisonreferences/oauth-providers.md - GitHub, Google, Azure setupreferences/common-issues.md - Error troubleshooting deep-divesreferences/official-examples.md - Curated links to Cloudflare examplesDon't use this skill when:
fastmcp skill instead)typescript-mcp skill)Use this skill specifically for: TypeScript + Cloudflare Workers + Remote MCP
Production tested: Based on Cloudflare's official MCP servers (mcp-server-cloudflare, workers-mcp)
Without this skill:
With this skill:
Savings: ~88% (55k → 6k tokens)
Errors prevented: 22 (100% prevention rate)