Build MCP servers on Cloudflare Workers - the only platform with official remote MCP support. TypeScript-based with OAuth, Durable Objects, and WebSocket hibernation. Prevents 24 documented errors. Use when: deploying remote MCP servers, implementing OAuth, or troubleshooting URL path mismatches, McpAgent exports, CORS issues, IoContext timeouts.
Build and deploy Model Context Protocol (MCP) servers on Cloudflare Workers with TypeScript.
Status: Production Ready ✅ Last Updated: 2026-01-21 Latest Versions: @modelcontextprotocol/sdk@1.25.3, @cloudflare/workers-oauth-provider@0.2.2, agents@0.3.6
Recent Updates (2025):
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)
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 devChoose template based on auth needs:
remote-mcp-authless - No auth (recommended for most)remote-mcp-github-oauth - GitHub OAuthremote-mcp-google-oauth - Google OAuthremote-mcp-auth0 / remote-mcp-authkit - Enterprise SSOmcp-server-bearer-auth - Custom authAll templates: https://github.com/cloudflare/ai/tree/main/demos
Production examples: https://github.com/cloudflare/mcp-server-cloudflare (15 servers with real integrations)
# 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:
The #1 reason MCP servers fail to connect is URL path configuration mistakes.
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 deployExample 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)
}
}
}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/listWhen 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/listThe base path is prepended to all MCP endpoints automatically.
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/callIf client connects to https://my-mcp.workers.dev (missing /sse):
pathname === "/" → Check fails → 404 Not FoundStep 1: Deploy your MCP server
npx wrangler deploy
# Output: Deployed to https://my-mcp.YOUR_ACCOUNT.workers.devStep 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
After deploying, verify:
curl https://worker.dev/sse returns MCP server info (not 404)workes.dev instead of workers.dev)https:// (not http://) for deployed WorkersTwo 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!
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
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
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 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);
}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 streamingBenefit: Simplified architecture, better performance
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@latestMinimum Safe Version: @cloudflare/workers-oauth-provider@0.0.5 or later
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!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 (✅✅✅)
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)
Important: McpAgent uses different transports for client-facing vs internal communication.
Source: GitHub Issue #172
Client --- (SSE or HTTP) --> Worker --- (WebSocket) --> Durable ObjectClient → Worker (External):
/sse endpoint/mcp endpointWorker → Durable Object (Internal):
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.
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.
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:
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;
}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;
}Error: TypeError: Cannot read properties of undefined (reading 'serve')
Cause: Forgot to export McpAgent class
Solution:
export class MyMCP extends McpAgent { ... } // ✅ Must export
export default { fetch() { ... } }Error: 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 - 404Debug steps:
serveSSE("/sse") vs serveSSE("/")curl https://worker.dev/sseError: 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/mcpBest practice: Support both transports (see Transport Selection Guide)
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(...);
}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:
npx wrangler deploy and note output URLError: 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!
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)
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" }
}
);
}
}
};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:
.dev.vars (local only, gitignored)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_URLError: 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.
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");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"
}
]
}
}Error: Error: Durable Object class MyMCP has no migration defined
Cause: First DO deployment requires migration
Solution:
{
"migrations": [
{ "tag": "v1", "new_classes": ["MyMCP"] }
]
}Error: WebSocket metadata lost on hibernation wake
Cause: Not using serializeAttachment() to preserve connection metadata
Solution: See WebSocket Hibernation section
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
// ...
});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_KEYError: 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
)Error: /sse returns 404 after adding /mcp
Cause: Incorrect path matching (missing startsWith())
Solution: Use startsWith() or exact matches correctly (see Error #4)
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 --remoteError: 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"
}
}
}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":{...}}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!
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:
/mcp are canceledAffected 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:
OAuthProvider wrapper even for custom Bearer authCloudflareMCPServer instead of standard McpServerWorkaround: 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)
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:
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:
wrangler secret)Related: Issue #640 (both involve OAuth/auth in remote deployments)
# 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 tailPackage 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
fa91c34
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.