cloudflare-mcp-server — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited cloudflare-mcp-server (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
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:
# 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 devYour MCP server is now running at http://localhost:8788/sse
# 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# 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 production
npx wrangler deploy
# Your MCP server is now live at:
# https://my-mcp-server.your-account.workers.dev/sseThe 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 failuresMCP 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 });
}
};Cloudflare MCP servers support 4 authentication patterns:
Use case: Internal tools, development, public APIs
Template: templates/basic-mcp-server.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 requestsSecurity: ✅ 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" }
]
}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:
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");# Start dev server (uses Miniflare for local DOs)
npm run dev
# Start dev server with remote Durable Objects (more accurate)
npx wrangler dev --remoteAccess at: http://localhost:8788/sse
npx @modelcontextprotocol/inspector@latesthttp://localhost:5173# First time: Login
npx wrangler login
# Deploy
npx wrangler deploy
# Check deployment
npx wrangler tailYour server is live at:
https://my-mcp-server.YOUR_ACCOUNT.workers.dev/sse~/.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.
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: Connection failed: Unexpected response format
Cause: Client expects /sse but server only serves /mcp
Solution: Serve both transports (see Transport Methods section)
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
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
Error: Error: Cannot read properties of undefined (reading 'idFromName')
Cause: Forgot DO binding in wrangler.jsonc
Solution: Add binding (see Stateful MCP Servers section)
Error: Error: Durable Object class MyMCP has no migration defined
Cause: First DO deployment requires migration
Solution:
{
"migrations": [
{ "tag": "v1", "new_classes": ["MyMCP"] }
]
}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
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
Error: WebSocket metadata lost on hibernation wake
Cause: Not using serializeAttachment()
Solution: See WebSocket Hibernation section
Security risk: Users don't see permissions
Cause: allowConsentScreen: false in production
Solution: Always set allowConsentScreen: true in production
Error: Error: JWT_SIGNING_KEY environment variable not set
Cause: OAuth Provider requires signing key
Solution:
openssl rand -base64 32
# Add to wrangler.jsonc varsError: env.MY_VAR is undefined
Cause: Variables in .dev.vars but not in wrangler.jsonc
Solution: Add to "vars" section in wrangler.jsonc
Error: ZodError: Invalid input type
Cause: Client sends string, schema expects number
Solution: Use Zod transforms:
z.string().transform(val => parseInt(val, 10))Error: /sse returns 404 after adding /mcp
Cause: Incorrect path matching
Solution: Use startsWith() or exact matches
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
{
"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
Don'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: ~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~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.