1002 lines
25 KiB
Markdown
1002 lines
25 KiB
Markdown
---
|
|
name: cloudflare-mcp-server
|
|
description: |
|
|
Build Model Context Protocol (MCP) servers on Cloudflare Workers - the only platform with official remote MCP support. TypeScript-based with OAuth, Durable Objects, and WebSocket hibernation.
|
|
|
|
Use when: deploying remote MCP servers, implementing OAuth (GitHub/Google), using dual transports (SSE/HTTP), or troubleshooting URL path mismatches, McpAgent exports, OAuth redirects, CORS issues.
|
|
license: MIT
|
|
allowed-tools: ["Read", "Write", "Edit", "Bash", "Glob", "Grep"]
|
|
metadata:
|
|
version: "2.2.0"
|
|
last_verified: "2025-11-24"
|
|
sdk_versions:
|
|
mcp_sdk: "1.22.0"
|
|
oauth_provider: "0.1.0"
|
|
agents_sdk: "0.2.23"
|
|
official_examples: "https://github.com/cloudflare/ai/tree/main/demos"
|
|
cloudflare_docs: "https://developers.cloudflare.com/agents/model-context-protocol/"
|
|
---
|
|
|
|
# Cloudflare MCP Server Skill
|
|
|
|
Build and deploy **Model Context Protocol (MCP) servers** on Cloudflare Workers with TypeScript.
|
|
|
|
**Status**: Production Ready ✅
|
|
**Last Updated**: 2025-11-24
|
|
**Latest Versions**: @modelcontextprotocol/sdk@1.22.0, @cloudflare/workers-oauth-provider@0.1.0, agents@0.2.23
|
|
|
|
**Recent Updates (2025)**:
|
|
- **September 2025**: Code Mode (agents write code vs calling tools, auto-generated TypeScript API from schema)
|
|
- **August 2025**: MCP Elicitation (interactive workflows, user input during execution), Task Queues, Email Integration
|
|
- **July 2025**: MCPClientManager (connection management, OAuth flow, hibernation)
|
|
- **April 2025**: HTTP Streamable Transport (single endpoint, recommended over SSE), Python MCP support
|
|
- **May 2025**: Claude.ai remote MCP support, use-mcp React library, major partnerships
|
|
|
|
---
|
|
|
|
## What is This Skill?
|
|
|
|
This skill teaches you to build **remote MCP servers** on Cloudflare - the ONLY platform with official remote MCP support.
|
|
|
|
**Use when**: Avoiding 22+ common MCP + Cloudflare errors (especially URL path mismatches - the #1 failure cause)
|
|
|
|
---
|
|
|
|
## 🚀 Quick Start (5 Minutes)
|
|
|
|
**Start with Cloudflare's official 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
|
|
```
|
|
|
|
**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
|
|
|
|
```bash
|
|
# 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`**
|
|
```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
|
|
|
|
**Two transports available:**
|
|
|
|
1. **SSE (Server-Sent Events)** - Legacy, wide compatibility
|
|
```typescript
|
|
MyMCP.serveSSE("/sse").fetch(request, env, ctx)
|
|
```
|
|
|
|
2. **Streamable HTTP** - 2025 standard (recommended), single endpoint
|
|
```typescript
|
|
MyMCP.serve("/mcp").fetch(request, env, ctx)
|
|
```
|
|
|
|
**Support both for maximum compatibility:**
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```typescript
|
|
// 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:
|
|
|
|
```typescript
|
|
// 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:
|
|
|
|
```typescript
|
|
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)
|
|
|
|
```typescript
|
|
// 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:
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
---
|
|
|
|
## Authentication Patterns
|
|
|
|
**Choose auth based on use case:**
|
|
|
|
1. **No Auth** - Internal tools, dev (Template: `remote-mcp-authless`)
|
|
|
|
2. **Bearer Token** - Custom auth (Template: `mcp-server-bearer-auth`)
|
|
```typescript
|
|
// Validate Authorization: Bearer <token>
|
|
const token = request.headers.get("Authorization")?.replace("Bearer ", "");
|
|
if (!await validateToken(token, env)) {
|
|
return new Response("Unauthorized", { status: 401 });
|
|
}
|
|
```
|
|
|
|
3. **OAuth Proxy** - GitHub/Google (Template: `remote-mcp-github-oauth`)
|
|
```typescript
|
|
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**
|
|
|
|
4. **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:
|
|
|
|
```typescript
|
|
// 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)
|
|
|
|
---
|
|
|
|
## Common Patterns
|
|
|
|
### Caching with DO Storage
|
|
|
|
```typescript
|
|
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 && 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
|
|
|
|
```typescript
|
|
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;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 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!
|
|
|
|
---
|
|
|
|
## Testing & Deployment
|
|
|
|
```bash
|
|
# 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
|
|
|
|
- **Cloudflare Agents**: https://developers.cloudflare.com/agents/
|
|
- **MCP Specification**: https://modelcontextprotocol.io/
|
|
- **Official Templates**: https://github.com/cloudflare/ai/tree/main/demos
|
|
- **Production Servers**: https://github.com/cloudflare/mcp-server-cloudflare
|
|
- **workers-oauth-provider**: https://github.com/cloudflare/workers-oauth-provider
|
|
|
|
---
|
|
|
|
**Package Versions**: @modelcontextprotocol/sdk@1.22.0, @cloudflare/workers-oauth-provider@0.1.0, agents@0.2.23
|
|
**Last Verified**: 2025-11-24
|
|
**Errors Prevented**: 22 documented issues (100% prevention rate)
|