cloudflare-mcp-server

Deploy remote MCP servers on Cloudflare Workers with TypeScript, OAuth, and Durable Objects. Supports SSE and HTTP Streamable transports with automatic WebSocket hibernation for stateful sessions; choose from five auth templates (no-auth, bearer, GitHub/Google OAuth, enterprise SSO) Base path configuration is criticalβ€”server and client URLs must match exactly (e.g., serveSSE("/sse") requires client URL https://worker.dev/sse , not https://worker.dev ) Includes 24 documented error solutions covering path mismatches, transport confusion, OAuth redirect URIs, CORS preflight, environment validation, and Durable Objects migrations Conditional tool registration enables role-based access control and feature flags; stateful tools persist across hibernation using this.state.storage instead of instance properties

INSTALLATION
npx skills add https://github.com/jezweb/claude-skills --skill cloudflare-mcp-server
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

$2a

This skill teaches you to build remote MCP servers on Cloudflare - the ONLY platform with official remote MCP support.

Use when: Avoiding 24+ common MCP + Cloudflare errors (especially URL path mismatches - the #1 failure cause)

πŸš€ Quick Start (5 Minutes)

Start with Cloudflare's 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

Choose template based on auth needs:

  • remote-mcp-authless - No auth (recommended for most)
  • remote-mcp-github-oauth - GitHub OAuth
  • remote-mcp-google-oauth - Google OAuth
  • remote-mcp-auth0 / remote-mcp-authkit - Enterprise SSO
  • mcp-server-bearer-auth - Custom auth

All templates: https://github.com/cloudflare/ai/tree/main/demos

Production examples: https://github.com/cloudflare/mcp-server-cloudflare (15 servers with real integrations)

Deployment Workflow

# 1. Create from template

npm create cloudflare@latest -- my-mcp --template=cloudflare/ai/demos/remote-mcp-authless

cd my-mcp && npm install && npm run dev

# 2. Deploy

npx wrangler deploy

# Note the output URL: https://my-mcp.YOUR_ACCOUNT.workers.dev

# 3. Test (PREVENTS 80% OF ERRORS!)

curl https://my-mcp.YOUR_ACCOUNT.workers.dev/sse

# Expected: {"name":"My MCP Server","version":"1.0.0","transports":["/sse","/mcp"]}

# Got 404? See "HTTP Transport Fundamentals" below

# 4. Configure client (~/.config/claude/claude_desktop_config.json)

{

  "mcpServers": {

    "my-mcp": {

      "url": "https://my-mcp.YOUR_ACCOUNT.workers.dev/sse"  // Must match curl URL!

    }

  }

}

# 5. Restart Claude Desktop (config only loads at startup)

Post-Deployment Checklist:

  • curl returns server info (not 404)
  • Client URL matches curl URL exactly
  • Claude Desktop restarted
  • Tools visible in Claude Desktop
  • Test tool call succeeds

⚠️ CRITICAL: HTTP Transport Fundamentals

The #1 reason MCP servers fail to connect is URL path configuration mistakes.

URL Path Configuration Deep-Dive

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)

    }

  }

}

How Base Path Affects Tool URLs

**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.

Request/Response Lifecycle

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

Testing Your URL Configuration

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

Post-Deployment Checklist

After deploying, verify:

  • curl https://worker.dev/sse returns MCP server info (not 404)
  • Client config URL matches deployed URL exactly
  • No typos in URL (common: workes.dev instead of workers.dev)
  • Using https:// (not http://) for deployed Workers
  • If using OAuth, redirect URI also updated

Transport Selection

Two transports available:

-

SSE (Server-Sent Events) - Legacy, wide compatibility

MyMCP.serveSSE("/sse").fetch(request, env, ctx)

-

Streamable HTTP - 2025 standard (recommended), single endpoint

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 });

  }

};

CRITICAL: Use pathname.startsWith() to match paths correctly!

2025 Knowledge Gaps

MCP Elicitation (August 2025)

MCP servers can now request user input during tool execution:

// Request user input during tool execution

const result = await this.elicit({

  prompt: "Enter your API key:",

  type: "password"

});

// Interactive workflows with Durable Objects state

await this.state.storage.put("api_key", result);

Use cases: Confirmations, forms, multi-step workflows

State: Preserved during agent hibernation

Code Mode (September 2025)

Agents SDK converts MCP schema β†’ TypeScript API:

// Old: Direct tool calls

await server.callTool("get_user", { id: "123" });

// New: Type-safe generated API

const user = await api.getUser("123");

Benefits: Auto-generated doc comments, type safety, code completion

MCPClientManager (July 2025)

New class for MCP client capabilities:

import { MCPClientManager } from "agents/mcp";

const manager = new MCPClientManager(env);

await manager.connect("https://external-mcp.com/sse");

// Auto-discovers tools, resources, prompts

// Handles reconnection, OAuth flow, hibernation

Task Queues & Email (August 2025)

// Task queues for background jobs

await this.queue.send({ task: "process_data", data });

// Email integration

async onEmail(message: Email) {

  // Process incoming email

  const response = await this.generateReply(message);

  await this.sendEmail(response);

}

HTTP Streamable Transport Details (April 2025)

Single endpoint replaces separate connection/messaging endpoints:

// Old: Separate endpoints

/connect  // Initialize connection

/message  // Send/receive messages

// New: Single streamable endpoint

/mcp      // All communication via HTTP streaming

Benefit: Simplified architecture, better performance

Security Considerations

PKCE Bypass Vulnerability (CRITICAL)

CVE: GHSA-qgp8-v765-qxx9

Severity: Critical

Fixed in: @cloudflare/workers-oauth-provider@0.0.5

Problem: Earlier versions of the OAuth provider library had a critical vulnerability that completely bypassed PKCE protection, potentially allowing attackers to intercept authorization codes.

Action Required:

# Check current version

npm list @cloudflare/workers-oauth-provider

# Update if < 0.0.5

npm install @cloudflare/workers-oauth-provider@latest

Minimum Safe Version: @cloudflare/workers-oauth-provider@0.0.5 or later

Token Storage Best Practices

Always use encrypted storage for OAuth tokens:

// βœ… GOOD: workers-oauth-provider handles encryption automatically

export default new OAuthProvider({

  kv: (env) => env.OAUTH_KV,  // Tokens stored encrypted

  // ...

});

// ❌ BAD: Storing tokens in plain text

await env.KV.put("access_token", token);  // Security risk!

User-scoped KV keys prevent data leakage between users:

// βœ… GOOD: Namespace by user ID

await env.KV.put(`user:${userId}:todos`, data);

// ❌ BAD: Global namespace

await env.KV.put(`todos`, data);  // Data visible to all users!

Authentication Patterns

Choose auth based on use case:

-

No Auth - Internal tools, dev (Template: remote-mcp-authless)

-

Bearer Token - Custom auth (Template: mcp-server-bearer-auth)

// Validate Authorization: Bearer <token>

const token = request.headers.get("Authorization")?.replace("Bearer ", "");

if (!await validateToken(token, env)) {

  return new Response("Unauthorized", { status: 401 });

}

-

OAuth Proxy - GitHub/Google (Template: remote-mcp-github-oauth)

import { OAuthProvider, GitHubHandler } from "@cloudflare/workers-oauth-provider";

export default new OAuthProvider({

  authorizeEndpoint: "/authorize",

  tokenEndpoint: "/token",

  defaultHandler: new GitHubHandler({

    clientId: (env) => env.GITHUB_CLIENT_ID,

    clientSecret: (env) => env.GITHUB_CLIENT_SECRET,

    scopes: ["repo", "user:email"]

  }),

  kv: (env) => env.OAUTH_KV,

  apiHandlers: { "/sse": MyMCP.serveSSE("/sse") }

});

⚠️ CRITICAL: All OAuth URLs (url, authorizationUrl, tokenUrl) must use same domain

-

Remote OAuth with DCR - Full OAuth provider (Template: remote-mcp-authkit)

Security levels: No Auth (⚠️) < Bearer (βœ…) < OAuth Proxy (βœ…βœ…) < Remote OAuth (βœ…βœ…βœ…)

Stateful MCP Servers (Durable Objects)

McpAgent extends Durable Objects for per-session state:

// Storage API

await this.state.storage.put("key", "value");

const value = await this.state.storage.get<string>("key");

// Required wrangler.jsonc

{

  "durable_objects": {

    "bindings": [{ "name": "MY_MCP", "class_name": "MyMCP" }]

  },

  "migrations": [{ "tag": "v1", "new_classes": ["MyMCP"] }]  // Required on first deploy!

}

Critical: Migrations required on first deployment

Cost: Durable Objects now included in free tier (2025)

Architecture: Internal vs External Transports

Important: McpAgent uses different transports for client-facing vs internal communication.

Source: GitHub Issue #172

Transport Architecture

Client --- (SSE or HTTP) --> Worker --- (WebSocket) --> Durable Object

Client β†’ Worker (External):

  • SSE transport: /sse endpoint
  • HTTP Streamable: /mcp endpoint
  • Client chooses transport

Worker β†’ Durable Object (Internal):

  • Always WebSocket
  • Required by PartyServer (McpAgent's internal dependency)
  • Automatic upgrade, invisible to client

What This Means

  • SSE clients are fully supported - External interface can be SSE
  • WebSocket is mandatory for DO - Internal Worker-DO communication always uses WebSocket
  • This is not a limitation - It's an implementation detail of McpAgent's architecture

Example

export default {

  fetch(request: Request, env: Env, ctx: ExecutionContext) {

    const { pathname } = new URL(request.url);

    // Client uses SSE

    if (pathname.startsWith("/sse")) {

      // βœ… Client β†’ Worker: SSE

      // βœ… Worker β†’ DO: WebSocket (automatic)

      return MyMCP.serveSSE("/sse").fetch(request, env, ctx);

    }

    return new Response("Not Found", { status: 404 });

  }

};

Key Takeaway: You can serve SSE to clients without worrying about the internal WebSocket requirement.

Common Patterns

Tool Return Format (CRITICAL)

Source: Stytch Blog - Building MCP Server with OAuth

All MCP tools must return this exact format:

this.server.tool(

  "my_tool",

  { /* schema */ },

  async (params) => {

    // βœ… CORRECT: Return object with content array

    return {

      content: [

        { type: "text", text: "Your result here" }

      ]

    };

    // ❌ WRONG: Raw string

    return "Your result here";

    // ❌ WRONG: Plain object

    return { result: "Your result here" };

  }

);

Common mistake: Returning raw strings or plain objects instead of proper MCP content format. This causes client parsing errors.

Conditional Tool Registration

Source: Cloudflare Blog - Building AI Agents

Dynamically add tools based on authenticated user:

export class MyMCP extends McpAgent<Env> {

  async init() {

    this.server = new McpServer({ name: "My MCP" });

    // Base tools for all users

    this.server.tool("public_tool", { /* schema */ }, async (params) => {

      // Available to everyone

    });

    // Conditional tools based on user

    const userId = this.props?.userId;

    if (await this.isAdmin(userId)) {

      this.server.tool("admin_tool", { /* schema */ }, async (params) => {

        // Only available to admins

      });

    }

    // Premium features

    if (await this.isPremiumUser(userId)) {

      this.server.tool("premium_feature", { /* schema */ }, async (params) => {

        // Only for premium users

      });

    }

  }

  private async isAdmin(userId?: string): Promise<boolean> {

    if (!userId) return false;

    const userRole = await this.state.storage.get<string>(`user:${userId}:role`);

    return userRole === "admin";

  }

}

Use cases:

  • Feature flags per user
  • Premium vs free tier tools
  • Role-based access control (RBAC)
  • A/B testing new tools

Caching with DO Storage

async getCached<T>(key: string, ttlMs: number, fetchFn: () => Promise<T>): Promise<T> {

  const cached = await this.state.storage.get<{ data: T, timestamp: number }>(key);

  if (cached &#x26;&#x26; Date.now() - cached.timestamp < ttlMs) {

    return cached.data;

  }

  const data = await fetchFn();

  await this.state.storage.put(key, { data, timestamp: Date.now() });

  return data;

}

Rate Limiting

async rateLimit(key: string, maxRequests: number, windowMs: number): Promise<boolean> {

  const requests = await this.state.storage.get<number[]>(`ratelimit:${key}`) || [];

  const recentRequests = requests.filter(ts => Date.now() - ts < windowMs);

  if (recentRequests.length >= maxRequests) return false;

  recentRequests.push(Date.now());

  await this.state.storage.put(`ratelimit:${key}`, recentRequests);

  return true;

}

24 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. Base Path Configuration Mismatch (Most Common!)

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:

  • Check what path your server uses: serveSSE("/sse") vs serveSSE("/")
  • Test with curl: curl https://worker.dev/sse
  • Update client config to match curl URL

3. Transport Type Confusion

Error: 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)

4. pathname.startsWith() Logic Error

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(...);

}

5. Local vs Deployed URL Mismatch

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:

  • Run npx wrangler deploy and note output URL
  • Update client config with deployed URL
  • Test with curl
  • Restart Claude Desktop

6. OAuth Redirect URI Mismatch

Error: 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!

7. Missing OPTIONS Handler (CORS Preflight)

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)

8. Request Body Validation Missing

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" }

        }

      );

    }

  }

};

9. Environment Variable Validation Missing

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:

  • Development: Add to .dev.vars (local only, gitignored)
  • Production: Add to 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

10. McpAgent vs McpServer Confusion

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.

11. 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

// ❌ DON'T: Lost on hibernation

this.userId = "123";

// βœ… DO: Persists through hibernation

await this.state.storage.put("userId", "123");

12. Durable Objects Binding Missing

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"

      }

    ]

  }

}

13. 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"] }

  ]

}

14. serializeAttachment() Not Used

Error: WebSocket metadata lost on hibernation wake

Cause: Not using serializeAttachment() to preserve connection metadata

Solution: See WebSocket Hibernation section

15. OAuth Consent Screen Disabled

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

  // ...

});

16. JWT Signing Key Missing

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

17. Tool Schema Validation Error

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

)

18. Multiple Transport Endpoints Conflicting

Error: /sse returns 404 after adding /mcp

Cause: Incorrect path matching (missing startsWith())

Solution: Use startsWith() or exact matches correctly (see Error #4)

19. Local Testing with Miniflare Limitations

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

20. 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

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"

    }

  }

}

21. Health Check Endpoint Missing

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":{...}}

22. CORS Headers Missing

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!

23. IoContext Timeout During MCP Initialization

Error: IoContext timed out due to inactivity, waitUntil tasks were cancelled

Source: GitHub Issue #640

Cause: When implementing MCP servers using McpAgent with custom Bearer authentication, the IoContext times out during the MCP protocol initialization handshake (before any tools are called).

Symptoms:

  • Timeout occurs before any tools are called
  • ~2 minute gap between initial request and agent initialization
  • Internal methods work (setInitializeRequest, getInitializeRequest, updateProps)
  • Both GET and POST to /mcp are canceled
  • Error: "IoContext timed out due to inactivity, waitUntil tasks were cancelled"

Affected Code Pattern:

// Custom Bearer auth without OAuthProvider wrapper

export default {

  fetch: async (req, env, ctx) => {

    const authHeader = req.headers.get("Authorization");

    if (!authHeader?.startsWith("Bearer ")) {

      return new Response("Unauthorized", { status: 401 });

    }

    if (url.pathname === "/sse") {

      return MyMCP.serveSSE("/sse")(req, env, ctx);  // ← Timeout here

    }

    return new Response("Not found", { status: 404 });

  }

};

Root Cause Hypothesis:

  • May require OAuthProvider wrapper even for custom Bearer auth
  • Possible missing timeout configuration for Durable Object IoContext
  • May need CloudflareMCPServer instead of standard McpServer

Workaround: Use official templates with OAuthProvider pattern instead of custom Bearer auth:

// Use OAuthProvider wrapper (recommended)

import { OAuthProvider } from "@cloudflare/workers-oauth-provider";

export default new OAuthProvider({

  authorizeEndpoint: "/authorize",

  tokenEndpoint: "/token",

  // ... OAuth config

  apiHandlers: { "/sse": MyMCP.serveSSE("/sse") }

});

Status: Investigation ongoing (issue open as of 2026-01-21)

24. OAuth Remote Connection Failures

Error: Connection to remote MCP server fails when using OAuth (works locally but fails when deployed)

Source: GitHub Issue #444

Cause: When deploying MCP client from Cloudflare Agents repository to Workers, client fails to connect to MCP servers secured with OAuth.

Symptoms:

  • Works perfectly in local development
  • Fails after deployment to Workers
  • OAuth handshake never completes
  • Client can't establish connection

Troubleshooting Steps:

-

Verify OAuth tokens are handled correctly during remote connection attempts

// Check token is being passed to remote server

console.log("Connecting with token:", token ? "present" : "missing");

-

Check network permissions to access OAuth provider

// Ensure Worker can reach OAuth endpoints

const response = await fetch("https://oauth-provider.com/token");

-

Verify CORS configuration on OAuth provider

// OAuth provider must allow Worker origin

headers: {

  "Access-Control-Allow-Origin": "https://your-worker.workers.dev",

  "Access-Control-Allow-Methods": "POST, OPTIONS",

  "Access-Control-Allow-Headers": "Content-Type, Authorization"

}

-

Check redirect URIs match deployed URLs

{

  "url": "https://mcp.workers.dev/sse",

  "auth": {

    "authorizationUrl": "https://mcp.workers.dev/authorize",  // Must match deployed domain

    "tokenUrl": "https://mcp.workers.dev/token"

  }

}

Deployment Checklist:

  • All OAuth URLs use deployed domain (not localhost)
  • CORS headers configured on OAuth provider
  • Network requests to OAuth provider allowed in Worker
  • Redirect URIs registered with OAuth provider
  • Environment variables set in production (wrangler secret)

Related: Issue #640 (both involve OAuth/auth in remote deployments)

Testing &#x26; Deployment

# Local dev

npm run dev                    # Miniflare (fast)

npx wrangler dev --remote      # Remote DOs (accurate)

# Test with MCP Inspector

npx @modelcontextprotocol/inspector@latest

# Open http://localhost:5173, enter http://localhost:8788/sse

# Deploy

npx wrangler login  # First time only

npx wrangler deploy

# ⚠️ CRITICAL: Update client config with deployed URL!

# Monitor logs

npx wrangler tail

Official Documentation

Package Versions: @modelcontextprotocol/sdk@1.25.3, @cloudflare/workers-oauth-provider@0.2.2, agents@0.3.6

Last Verified: 2026-01-21

Errors Prevented: 24 documented issues (100% prevention rate)

Skill Version: 3.1.0 | Changes: Added IoContext timeout (#23), OAuth remote failures (#24), Security section (PKCE vulnerability), Architecture clarification (internal WebSocket), Tool return format pattern, Conditional tool registration

BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills β†’

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free Β· no credit card