# Cloudflare MCP Server

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

- Skill: `ovachiever/cloudflare-mcp-server` (Agent Skill, multi-file: 22 files)
- Install (CLI): `npx skillmds add ovachiever/cloudflare-mcp-server`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ovachiever/cloudflare-mcp-server/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- License: MIT
- Author: ovachiever (https://skillmd.com/u/ovachiever)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/ovachiever/cloudflare-mcp-server

---


# Cloudflare MCP Server Skill

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 22+ common MCP + Cloudflare errors (especially URL path mismatches!)

**You'll learn**:
1. **HTTP transport fundamentals** (URL path configuration, routing)
2. **Transport selection** (SSE vs Streamable HTTP)
3. McpAgent class patterns and tool definitions
4. OAuth integration (all 5 auth patterns)
5. Cloudflare service integrations (Workers AI, D1, Vectorize, etc.)
6. Durable Objects for per-session state
7. WebSocket hibernation API
8. Complete deployment workflow

---

## 🚀 Official Cloudflare Templates (Start Here!)

**Before using this skill's templates**, know that Cloudflare provides **official starter templates** via `npm create`.

### Recommended Starting Point

**For most projects, start with Cloudflare's official authless template:**

```bash
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:**
- ✅ Minimal working MCP server (~50 lines)
- ✅ Dual transport support (SSE + Streamable HTTP)
- ✅ Pre-configured wrangler.jsonc
- ✅ Ready to deploy immediately

**Then customize with patterns from this skill** to avoid the 22+ common errors!

---

### All Available Cloudflare Templates

Cloudflare maintains 14+ official MCP templates. Use these as starting points:

#### Basic Templates

| 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 |

#### Advanced Authentication Templates

| 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 |

#### Integration Examples

| 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 |

---

### When to Use This Skill's Templates

**Use this skill's templates when:**
- Learning how URL paths work (use `mcp-http-fundamentals.ts`)
- Need comprehensive error prevention (all templates include warnings)
- Want detailed comments explaining every decision
- Building complex integrations (Workers AI, D1, Bearer auth)

**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!

---

### Production MCP Servers (Study These!)

Cloudflare maintains **15 production MCP servers** showing real-world integration patterns:

**Key servers to study:**
- `workers-bindings` - D1, KV, R2, AI, Durable Objects usage
- `browser-rendering` - Web scraping + screenshot tools
- `autorag` - Vectorize RAG pattern
- `ai-gateway` - Workers AI Gateway analytics
- `docs` - Cloudflare documentation search

**Repository**: https://github.com/cloudflare/mcp-server-cloudflare

**Why study these?** They show production-grade patterns for:
- Error handling
- Rate limiting
- Caching strategies
- Real API integrations
- Security best practices

---

## 🎯 Quick Start Workflow (Your Step-by-Step Guide)

**Follow this workflow for your next MCP server to avoid errors and ship fast.**

---

### Step 1: Choose Your Starting Template

**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!

---

### Step 2: Create from Template

```bash
# 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`

---

### Step 3: Customize with This Skill's Patterns

**Now add features by copying patterns from this skill:**

#### Need Workers AI (image/text generation)?
```bash
# 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`

---

#### Need a database (D1)?
```bash
# 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`

---

#### Need bearer token auth?
```bash
# 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

---

### Step 4: Deploy to Cloudflare

```bash
# 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!**

---

### Step 5: Test & Configure Client

#### A. Test with curl (PREVENTS 80% OF ERRORS!)

```bash
# Test the exact URL you'll use in client config
curl https://my-mcp-server.YOUR_ACCOUNT.workers.dev/sse
```

**Expected response**:
```json
{
  "name": "My MCP Server",
  "version": "1.0.0",
  "transports": ["/sse", "/mcp"]
}
```

**Got 404?** → Your client URL will be wrong! See "HTTP Transport Fundamentals" below.

---

#### B. Update Claude Desktop Config

**Linux/Mac**: `~/.config/claude/claude_desktop_config.json`
**Windows**: `%APPDATA%/Claude/claude_desktop_config.json`

**For authless servers**:
```json
{
  "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**:
```json
{
  "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!**

---

#### C. Restart Claude Desktop

**Config only loads at startup:**
1. Quit Claude Desktop completely
2. Reopen
3. Check for your MCP server in tools list

---

### Step 6: Verify It Works

**Test a tool call:**
1. Open Claude Desktop
2. Type: "List available MCP tools"
3. Your server's tools should appear
4. Try calling one: "Use the add tool to add 5 + 3"

**If tools don't appear** → See "Debugging Guide" in references/

---

### Post-Deployment Checklist

Before declaring success, verify:

- [ ] `curl https://worker.dev/sse` returns server info (not 404)
- [ ] Client config URL matches curl URL exactly
- [ ] Claude Desktop restarted after config update
- [ ] Tools visible in Claude Desktop
- [ ] Test tool call succeeds
- [ ] (OAuth only) All three URLs use same domain
- [ ] No errors in `npx wrangler tail` logs

**All checked?** 🎉 **Your MCP server is live!**

---

### Common Next Steps

**Want to add more features?**

1. **More tools** - Add to `init()` method in your McpAgent class
2. **Workers AI** - Copy patterns from `mcp-with-workers-ai.ts`
3. **Database** - Copy patterns from `mcp-with-d1.ts`
4. **Authentication** - Copy patterns from `mcp-bearer-auth.ts` or `mcp-oauth-proxy.ts`
5. **Durable Objects state** - Copy patterns from `mcp-stateful-do.ts`

**Want to avoid errors?**
- Read "HTTP Transport Fundamentals" section below (prevents URL path errors)
- Read "22 Known Errors" section (prevents all common mistakes)
- Check `references/debugging-guide.md` when stuck

---

### TL;DR - The 5-Minute Workflow

```bash
# 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! 🚀

---

## ⚠️ 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`**
```typescript
// 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`**:
```json
{
  "mcpServers": {
    "my-mcp": {
      "url": "https://my-mcp.workers.dev/sse"  // ✅ Correct
    }
  }
}
```

**❌ WRONG client configurations**:
```json
"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)**
```typescript
export default {
  fetch(request: Request, env: Env, ctx: ExecutionContext) {
    return MyMCP.serveSSE("/").fetch(request, env, ctx);  // ← Base path is "/"
  }
};
```

**Client configuration**:
```json
{
  "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**
```bash
npx wrangler deploy
# Output: Deployed to https://my-mcp.YOUR_ACCOUNT.workers.dev
```

**Step 2: Test the base path with curl**
```bash
# 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**
```json
{
  "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 Guide

MCP supports **two transport methods**: SSE (legacy) and Streamable HTTP (2025 standard).

### SSE (Server-Sent Events)

**Best for**: Wide client compatibility (2024 clients), legacy support

**Serving**:
```typescript
MyMCP.serveSSE("/sse").fetch(request, env, ctx)
```

**Client config**:
```json
{
  "url": "https://my-mcp.workers.dev/sse"
}
```

**Pros**:
- ✅ Supported by all MCP clients (2024+)
- ✅ Easy debugging (plain HTTP)
- ✅ Works with MCP Inspector

**Cons**:
- ❌ Less efficient than HTTP streaming
- ❌ Being deprecated in 2025

---

### Streamable HTTP

**Best for**: Modern clients (2025+), better performance

**Serving**:
```typescript
MyMCP.serve("/mcp").fetch(request, env, ctx)
```

**Client config**:
```json
{
  "url": "https://my-mcp.workers.dev/mcp"
}
```

**Pros**:
- ✅ More efficient than SSE
- ✅ 2025 standard
- ✅ Better streaming support

**Cons**:
- ❌ Newer clients only
- ❌ Less mature tooling

---

### Support Both (Recommended)

**Serve both transports** for maximum compatibility:

```typescript
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 clients connect to `/sse`
- HTTP clients connect to `/mcp`
- Health checks available at `/` or `/health`
- No transport conflicts

**CRITICAL**: Use `pathname.startsWith()` to match paths correctly!

---

## Quick Start (5 Minutes)

Now that you understand URL configuration, let's build your first MCP server.

### Option 1: Copy Minimal Template

Use the `mcp-http-fundamentals.ts` template - the simplest working example.

```bash
# 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",...}
```

### Option 2: Deploy from Cloudflare Template

```bash
# 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`

### Test with MCP Inspector

```bash
# In a new terminal
npx @modelcontextprotocol/inspector@latest

# Open http://localhost:5173
# Enter: http://localhost:8788/sse
# Click "Connect" and test tools
```

### Deploy to Cloudflare

```bash
# Deploy
npx wrangler deploy

# Output shows your URL:
# https://my-mcp-server.YOUR_ACCOUNT.workers.dev

# ⚠️ REMEMBER: Update client config with this URL + /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**:
```typescript
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**:
```typescript
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

---

## Authentication Patterns

Cloudflare MCP servers support **4 authentication patterns**:

### Pattern 1: No Authentication

**Use case**: Internal tools, development, public APIs

**Template**: `templates/mcp-http-fundamentals.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**:
```typescript
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**:
```typescript
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**:
```jsonc
{
  "kv_namespaces": [
    { "binding": "OAUTH_KV", "id": "YOUR_KV_ID" }
  ]
}
```

**⚠️ CRITICAL OAuth URL Configuration**: When using OAuth, your redirect URIs MUST match:
```json
{
  "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

---

### 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**:
```typescript
await this.state.storage.put("key", "value");
await this.state.storage.put("user_prefs", { theme: "dark" });
```

**Retrieve values**:
```typescript
const value = await this.state.storage.get<string>("key");
const prefs = await this.state.storage.get<object>("user_prefs");
```

**List keys**:
```typescript
const allKeys = await this.state.storage.list();
```

**Delete keys**:
```typescript
await this.state.storage.delete("key");
```

### Configuration

**wrangler.jsonc**:
```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):
```typescript
webSocket.serializeAttachment({
  userId: "123",
  sessionId: "abc",
  connectedAt: Date.now()
});
```

**Retrieve on wake**:
```typescript
const metadata = webSocket.deserializeAttachment();
console.log(metadata.userId); // "123"
```

**Storage for persistent state**:
```typescript
// ❌ 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!)

---

## Common Patterns

### API Proxy MCP Server

**Use case**: Wrap external API with MCP tools

**Pattern**:
```typescript
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**:
```typescript
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**:
```typescript
// 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**:
```typescript
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**:
```typescript
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
  };
}
```

---

## 22 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**:
```typescript
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
```typescript
// 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**:
1. Check what path your server uses: `serveSSE("/sse")` vs `serveSSE("/")`
2. Test with curl: `curl https://worker.dev/sse`
3. 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
```typescript
// 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
```typescript
// ✅ 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
```json
// 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
```json
{
  "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
```typescript
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
```typescript
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
```typescript
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**:
```bash
# .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
```typescript
// ❌ 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
```typescript
// ❌ 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)
```jsonc
{
  "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**:
```jsonc
{
  "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
```typescript
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**:
```bash
# 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
```typescript
// 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
```bash
# 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**:
```json
// ❌ 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**:
```bash
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
```typescript
// 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!

---

## Deployment & Testing

### Local Development

```bash
# 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

```bash
npx @modelcontextprotocol/inspector@latest
```

1. Open `http://localhost:5173`
2. Enter MCP server URL
3. Click "Connect"
4. Use "List Tools" to see available tools
5. Test tool calls with parameters

### Deploy to Cloudflare

```bash
# 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
```

### Connect Claude Desktop

**~/.config/claude/claude_desktop_config.json** (L

…(truncated)
