Build and deploy Model Context Protocol (MCP) servers on Cloudflare Workers with TypeScript.
What is This Skill?
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:
- Building MCP servers with TypeScript (@modelcontextprotocol/sdk)
- Deploying remote MCP servers to Cloudflare Workers
- Implementing OAuth authentication (GitHub, Google, Azure, custom)
- Creating stateful MCP servers with Durable Objects
- Optimizing costs with WebSocket hibernation
- Supporting both SSE and Streamable HTTP transports
- Avoiding 15+ common MCP + Cloudflare errors
You'll learn:
- McpAgent class patterns and tool definitions
- OAuth integration (all 4 auth patterns)
- Durable Objects for per-session state
- WebSocket hibernation API
- Dual transport configuration (SSE + HTTP)
- Complete deployment workflow
Quick Start (5 Minutes)
Option 1: Deploy from Template
# Create new MCP server from Cloudflare 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
Option 2: Copy Templates from This Skill
# Copy basic MCP server template
cp ~/.claude/skills/cloudflare-mcp-server/templates/basic-mcp-server.ts src/index.ts
cp ~/.claude/skills/cloudflare-mcp-server/templates/wrangler-basic.jsonc wrangler.jsonc
cp ~/.claude/skills/cloudflare-mcp-server/templates/package.json package.json
# Install dependencies
npm install
# Start development server
npm run dev
Test with MCP Inspector
# In a new terminal, start MCP Inspector
npx @modelcontextprotocol/inspector@latest
# Open http://localhost:5173
# Enter your MCP server URL: http://localhost:8788/sse
# Click "Connect" and test tools
Deploy to Cloudflare
# Deploy to production
npx wrangler deploy
# Your MCP server is now live at:
# https://my-mcp-server.your-account.workers.dev/sse
Core Concepts
1. McpAgent Class
The McpAgent base class from Cloudflare's Agents SDK provides:
- Automatic Durable Objects integration
- Built-in state management with SQL database
- Tool, resource, and prompt registration
- Transport handling (SSE + HTTP)
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" }]
})
);
}
}
2. Tool Definitions
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:
- Detailed descriptions: Help LLMs understand tool purpose
- Parameter descriptions: Explain expected values and constraints
- Error handling: Return
{ isError: true }for failures - Few, focused tools: Better than many granular ones
3. Transport Methods
MCP supports two transports:
SSE (Server-Sent Events) - Legacy, widely supported:
MyMCP.serveSSE("/sse").fetch(request, env, ctx)
Streamable HTTP - 2025 standard, more efficient:
MyMCP.serve("/mcp").fetch(request, env, ctx)
Support both for maximum compatibility:
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);
}
if (pathname.startsWith("/mcp")) {
return MyMCP.serve("/mcp").fetch(request, env, ctx);
}
return new Response("Not Found", { status: 404 });
}
};
Authentication Patterns
Cloudflare MCP servers support 4 authentication patterns:
Pattern 1: No Authentication
Use case: Internal tools, development, public APIs
Template: templates/basic-mcp-server.ts
Setup: None required
Security: ⚠️ Anyone can access your MCP server
Pattern 2: Token Validation (JWTVerifier)
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
Pattern 3: OAuth Proxy (workers-oauth-provider)
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" }
]
}
Security: ✅✅ Secure, production-ready
Pattern 4: Remote OAuth with DCR
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
Stateful MCP Servers with Durable Objects
Use Durable Objects when your MCP server needs:
- Per-session persistent state
- Conversation history
- Game state (chess, tic-tac-toe)
- Cached API responses
- User preferences
Storage API Pattern
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");
Configuration
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!
WebSocket Hibernation for Cost Optimization
Problem: Long-lived WebSocket connections cost CPU time
Solution: WebSocket Hibernation API suspends connections when idle
Pattern
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");
Cost Savings
Without hibernation:
- 1000 concurrent WebSockets × 10ms CPU/sec = 10 CPU-sec/sec
- Cost: ~$0.50/day
With hibernation:
- CPU only on messages (99% idle time suspended)
- Cost: ~$0.01/day (50x reduction!)
Worker & Durable Objects Basics
Self-contained section for standalone use
Worker Export Pattern
Workers must export a fetch handler:
export default {
fetch(request: Request, env: Env, ctx: ExecutionContext): Response | Promise<Response> {
// Handle request
return new Response("Hello");
}
};
Durable Objects Class Structure
DOs extend McpAgent (for MCP servers):
export class MyMCP extends McpAgent<Env> {
constructor(state: DurableObjectState, env: Env) {
super(state, env);
}
// Your methods here
}
Bindings Configuration
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");
Deployment & Testing
Local Development
# 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
Test with MCP Inspector
npx @modelcontextprotocol/inspector@latest
- Open
http://localhost:5173 - Enter MCP server URL
- Click "Connect"
- Use "List Tools" to see available tools
- Test tool calls with parameters
Deploy to Cloudflare
# First time: Login
npx wrangler login
# Deploy
npx wrangler deploy
# Check deployment
npx wrangler tail
Your server is live at:
https://my-mcp-server.YOUR_ACCOUNT.workers.dev/sse
Connect Claude Desktop
~/.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"
}
}
}
}
Restart Claude Desktop after config changes.
Common Patterns
API Proxy MCP Server
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
}]
};
}
);
Database-Backed Tools
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)
}]
};
}
);
Multi-Tool Coordination
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");
Caching Strategy
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 }]
};
}
);
Rate Limiting with Durable Objects
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
};
}
15 Known Errors (With Solutions)
1. McpAgent Class Not Exported
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() { ... } }
2. Transport Mismatch
Error: Connection failed: Unexpected response format
Cause: Client expects /sse but server only serves /mcp
Solution: Serve both transports (see Transport Methods section)
3. OAuth Redirect URI Mismatch
Error: OAuth error: redirect_uri does not match
Cause: Client configured with localhost, but deployed to workers.dev
Solution: Update claude_desktop_config.json after deployment
4. WebSocket Hibernation State Loss
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
5. Durable Objects Binding Missing
Error: Error: Cannot read properties of undefined (reading 'idFromName')
Cause: Forgot DO binding in wrangler.jsonc
Solution: Add binding (see Stateful MCP Servers section)
6. Migration Not Defined
Error: Error: Durable Object class MyMCP has no migration defined
Cause: First DO deployment requires migration
Solution:
{
"migrations": [
{ "tag": "v1", "new_classes": ["MyMCP"] }
]
}
7. CORS Errors on Remote MCP
Error: Access to fetch at '...' blocked by CORS policy
Cause: MCP server doesn't return CORS headers
Solution: Use OAuthProvider (handles CORS) or add headers manually
8. Client Configuration Format Error
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
9. serializeAttachment() Not Used
Error: WebSocket metadata lost on hibernation wake
Cause: Not using serializeAttachment()
Solution: See WebSocket Hibernation section
10. OAuth Consent Screen Disabled
Security risk: Users don't see permissions
Cause: allowConsentScreen: false in production
Solution: Always set allowConsentScreen: true in production
11. JWT Signing Key Missing
Error: Error: JWT_SIGNING_KEY environment variable not set
Cause: OAuth Provider requires signing key
Solution:
openssl rand -base64 32
# Add to wrangler.jsonc vars
12. Environment Variables Not Configured
Error: env.MY_VAR is undefined
Cause: Variables in .dev.vars but not in wrangler.jsonc
Solution: Add to "vars" section in wrangler.jsonc
13. Tool Schema Validation Error
Error: ZodError: Invalid input type
Cause: Client sends string, schema expects number
Solution: Use Zod transforms:
z.string().transform(val => parseInt(val, 10))
14. Multiple Transport Endpoints Conflicting
Error: /sse returns 404 after adding /mcp
Cause: Incorrect path matching
Solution: Use startsWith() or exact matches
15. Local Testing with Miniflare Limitations
Error: OAuth flow fails in local dev
Cause: Miniflare doesn't support all DO features
Solution: Use npx wrangler dev --remote for full DO support
Configuration Reference
Complete wrangler.jsonc (All Features)
{
"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
}
Complete package.json
See templates/package.json
Complete claude_desktop_config.json
See templates/claude_desktop_config.json
Additional Resources
Official Documentation
- Cloudflare Agents: https://developers.cloudflare.com/agents/
- MCP Specification: https://modelcontextprotocol.io/
- workers-oauth-provider: https://github.com/cloudflare/workers-oauth-provider
- Durable Objects: https://developers.cloudflare.com/durable-objects/
Official Examples
- Cloudflare AI Demos: https://github.com/cloudflare/ai/tree/main/demos
- 13 MCP Servers: https://blog.cloudflare.com/thirteen-new-mcp-servers-from-cloudflare/
Tools
- MCP Inspector: https://github.com/modelcontextprotocol/inspector
- Wrangler CLI: https://developers.cloudflare.com/workers/wrangler/
When NOT to Use This Skill
Don't use this skill when:
- Building Python MCP servers (use
fastmcpskill instead) - Building local-only MCP servers (use
typescript-mcpskill) - You need non-Cloudflare hosting (AWS Lambda, GCP, etc.)
- You're working with Claude.ai web interface skills (different from MCP)
Use this skill specifically for: TypeScript + Cloudflare Workers + Remote MCP
Version Information
- @modelcontextprotocol/sdk: 1.21.0
- @cloudflare/workers-oauth-provider: 0.0.13
- agents (Cloudflare Agents SDK): 0.2.20
- Last Verified: 2025-11-04
Production tested: Based on Cloudflare's official MCP servers (mcp-server-cloudflare, workers-mcp)
Token Efficiency
Without this skill:
- Research scattered docs: ~10k tokens
- Debug 15 errors: ~30k tokens
- Total: ~40k tokens
With this skill:
- Read skill: ~4k tokens
- Copy templates: ~1k tokens
- Total: ~5k tokens
Savings: ~87% (40k → 5k tokens)
Errors prevented: 15 (100% prevention rate)
Questions? Check:
references/authentication.md- Auth patterns comparisonreferences/transport.md- SSE vs HTTP technical detailsreferences/oauth-providers.md- GitHub, Google, Azure setupreferences/common-issues.md- Error troubleshooting deep-divesreferences/official-examples.md- Curated links to Cloudflare examples