Deployment11 min read

Deploying MCP Servers to Cloudflare Workers: A Complete Guide

Learn how to deploy MCP servers to Cloudflare Workers for globally distributed, serverless AI tool access. Covers setup, auth, CORS, and production best practices.

By MyMCPTools Team·

Running an MCP server on your local machine works fine for personal use — but what if you want your AI tools to be available anywhere, shared across a team, or deployed as part of a product? Cloudflare Workers solves exactly this problem.

Cloudflare Workers runs JavaScript/TypeScript at the edge, in 300+ locations worldwide, with no server management, sub-millisecond cold starts, and a generous free tier. It's one of the best platforms for deploying MCP servers that need to be always-on and globally accessible.

Why Cloudflare Workers for MCP Servers

Most MCP servers run locally — they start when your AI client starts, and they die when you close your laptop. That's fine for individual tools, but breaks down when you need:

  • Team access — Multiple developers sharing the same MCP tool configuration
  • Production agents — Autonomous AI workflows that run on schedules or respond to webhooks
  • Multi-device access — Same MCP tools available on desktop, laptop, and remote machines
  • External integrations — Third-party services calling your MCP tools via HTTP

Cloudflare Workers gives you all of this without managing infrastructure. You write the code, deploy it, and Cloudflare handles the rest.

Understanding MCP Transport on Cloudflare Workers

The MCP specification supports multiple transport layers. On Cloudflare Workers, you'll use HTTP+SSE (Server-Sent Events) transport instead of stdio (which only works locally).

The architecture looks like this:

  • Client (Claude Desktop, Cursor, your agent) — sends MCP requests over HTTP
  • Cloudflare Worker — receives requests, processes them, streams responses via SSE
  • External APIs/services — your Worker calls these to fulfill tool requests

Cloudflare's MCP toolkit (@cloudflare/mcp-server-cloudflare) handles the protocol details, letting you focus on writing tool implementations.

Step 1: Project Setup

Start with a fresh Workers project using the MCP template:

npm create cloudflare@latest my-mcp-server -- --template cloudflare/workers-mcp
cd my-mcp-server
npm install

This scaffolds a TypeScript Worker project with the MCP SDK already configured. The main file will look something like this:

import { McpServer } from "@cloudflare/mcp-server-cloudflare";

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const server = new McpServer({
      name: "My MCP Server",
      version: "1.0.0",
    });

    // Register your tools here
    server.tool("hello", "Say hello to someone", {
      name: { type: "string", description: "The name to greet" },
    }, async ({ name }) => {
      return { content: [{ type: "text", text: `Hello, ${name}!` }] };
    });

    return server.handleRequest(request);
  },
};

Step 2: Writing Your MCP Tools

Each tool in your MCP server has three components:

  1. Name — how the AI identifies and calls the tool
  2. Description — what the tool does (the AI reads this to decide when to use it)
  3. Input schema — JSON Schema defining what parameters the tool accepts

Here's a practical example — a tool that fetches data from an external API:

server.tool(
  "fetch_weather",
  "Get current weather for a location",
  {
    city: { type: "string", description: "City name" },
    country_code: { type: "string", description: "ISO 2-letter country code", optional: true },
  },
  async ({ city, country_code }, { env }) => {
    const query = country_code ? `${city},${country_code}` : city;
    const response = await fetch(
      `https://api.openweathermap.org/data/2.5/weather?q=${query}&appid=${env.WEATHER_API_KEY}`
    );
    const data = await response.json();
    return {
      content: [{
        type: "text",
        text: `${data.name}: ${data.weather[0].description}, ${Math.round(data.main.temp - 273.15)}°C`
      }]
    };
  }
);

Step 3: Managing Secrets with Cloudflare

Your MCP tools will almost certainly need API keys. Never hardcode these — use Cloudflare's secret management:

# Add a secret to your Worker
wrangler secret put WEATHER_API_KEY
# Enter the value when prompted

# For local development, use .dev.vars
echo "WEATHER_API_KEY=your_key_here" >> .dev.vars

Reference secrets in your Worker via env.SECRET_NAME — Cloudflare injects them at runtime without exposing them in your codebase.

Your wrangler.toml should declare the bindings:

[vars]
MCP_SERVER_NAME = "My Production MCP Server"

# Secrets are referenced but values aren't stored here
# Set them with: wrangler secret put SECRET_NAME

Step 4: Authentication

For a production MCP server, you'll want to restrict who can call your tools. The simplest approach is a shared API key via header authentication:

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    // Authenticate before processing MCP requests
    const authHeader = request.headers.get("Authorization");
    const expectedToken = `Bearer ${env.MCP_API_KEY}`;

    if (authHeader !== expectedToken) {
      return new Response("Unauthorized", { status: 401 });
    }

    const server = new McpServer({ name: "My MCP Server", version: "1.0.0" });
    // ... register tools ...
    return server.handleRequest(request);
  },
};

For team deployments, consider per-user API keys stored in a KV namespace, or integrate with Cloudflare Access for enterprise SSO authentication.

Step 5: Handling CORS

If your MCP server will be called from browser-based AI clients or web apps, you need CORS headers:

function addCorsHeaders(response: Response): Response {
  const headers = new Headers(response.headers);
  headers.set("Access-Control-Allow-Origin", "*");
  headers.set("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
  headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization");
  return new Response(response.body, { ...response, headers });
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    if (request.method === "OPTIONS") {
      return new Response(null, {
        headers: {
          "Access-Control-Allow-Origin": "*",
          "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
          "Access-Control-Allow-Headers": "Content-Type, Authorization",
        },
      });
    }

    // ... rest of your handler ...
    const response = await server.handleRequest(request);
    return addCorsHeaders(response);
  },
};

Step 6: Deploy to Production

# Deploy to Cloudflare's global network
wrangler deploy

# Your MCP server is now live at:
# https://my-mcp-server.YOUR_SUBDOMAIN.workers.dev

The deployment takes under 30 seconds and your server is immediately available in all 300+ Cloudflare edge locations.

Step 7: Connecting Clients to Your Deployed Server

Update your Claude Desktop config to point to the remote server instead of a local process:

{
  "mcpServers": {
    "my-remote-mcp": {
      "url": "https://my-mcp-server.YOUR_SUBDOMAIN.workers.dev/mcp",
      "headers": {
        "Authorization": "Bearer YOUR_MCP_API_KEY"
      }
    }
  }
}

For Cursor, add it under the MCP servers section in your workspace settings with the same URL and auth header.

Production Best Practices

Error Handling

Always return structured errors from your tools rather than throwing exceptions:

server.tool("risky_operation", "...", schema, async (params) => {
  try {
    const result = await doRiskyThing(params);
    return { content: [{ type: "text", text: result }] };
  } catch (error) {
    return {
      content: [{ type: "text", text: `Error: ${error.message}` }],
      isError: true,
    };
  }
});

Rate Limiting

Use Cloudflare's Rate Limiting rules (available in the dashboard) or implement token-bucket rate limiting with a KV namespace to prevent abuse of expensive tool calls.

Observability

Log tool invocations using Cloudflare Workers Logs or pipe to a logging service. This is essential for debugging when an agent runs autonomously and a tool call fails silently:

server.tool("my_tool", "...", schema, async (params, { env }) => {
  console.log(JSON.stringify({ tool: "my_tool", params, timestamp: Date.now() }));
  // ... tool logic ...
});

Caching with KV

For tools that fetch the same external data repeatedly (like stock prices or weather), use Cloudflare KV for caching:

const cacheKey = `weather:${city}`;
const cached = await env.CACHE_KV.get(cacheKey);
if (cached) return { content: [{ type: "text", text: cached }] };

const result = await fetchWeather(city, env.WEATHER_API_KEY);
await env.CACHE_KV.put(cacheKey, result, { expirationTtl: 300 }); // 5 min cache
return { content: [{ type: "text", text: result }] };

Using Durable Objects for Stateful MCP Servers

Standard Workers are stateless — each request starts fresh. For MCP servers that need to maintain session state across calls, use Cloudflare Durable Objects:

  • Track conversation context between tool calls
  • Maintain WebSocket connections for real-time streaming
  • Coordinate multi-step tool workflows with persistent state

The Cloudflare MCP SDK has built-in Durable Object support — check the McpDurableObject class in @cloudflare/mcp-server-cloudflare for the pattern.

Cost Considerations

Cloudflare Workers pricing is extremely favorable for MCP deployments:

  • Free tier: 100,000 requests/day — sufficient for personal and small team use
  • Paid tier ($5/mo): 10 million requests/month — covers most production deployments
  • CPU time: 10ms per request on free tier, 30ms on paid — enough for API-proxying tools

For CPU-intensive tools (heavy data processing, image manipulation), consider AWS Lambda which offers longer execution windows, though with higher latency and more complex deployment.

When Not to Use Cloudflare Workers

Cloudflare Workers is excellent but not universal. Consider alternatives when you need:

  • Long-running operations (>30 seconds) — Use AWS Lambda with extended timeout or a dedicated server
  • Local filesystem access — Workers run in a sandboxed environment with no disk access
  • Python tools — Workers runs JavaScript/TypeScript/Wasm only; Python MCP servers need AWS Lambda or Railway
  • Heavy compute (ML inference) — Workers has CPU time limits; use Cloudflare AI or a dedicated GPU instance

Next Steps

Once your MCP server is deployed to Cloudflare Workers:

  1. Share the URL and API key with your team — they get identical tool access without local setup
  2. Set up Cloudflare Workers Observability for production monitoring
  3. Add Cloudflare Access if you need enterprise SSO instead of API key auth
  4. Explore Cloudflare Queues for async tool operations that don't block the AI client

Browse the Cloudflare MCP server and all Cloud MCP servers in our directory.

Recommended Tools

Better Stack

Free Plan

Get alerted when your APIs, browser tests, payment pipelines, or MCP server dependencies go down. Used by 100K+ developers.

Start monitoring free →

1Password

14-day Free Trial

Store and inject API keys, payment credentials, tokens, and file access secrets into your MCP server configs. Trusted by 150K+ developers.

Try 1Password free →

🔧 MCP Servers Mentioned in This Article

☁️

Cloudflare MCP Server

Cloudflare ships two different things under this name. The mcp-server-cloudflare repo provides 16 remote, domain-specific MCP servers rather than one monolith — Documentation, Workers Bindings (storage/AI/compute primitives), Workers Builds, Observability (logs/analytics), Container sandboxes, Browser Rendering (fetch pages, convert to markdown, screenshots), Logpush health, AI Gateway (prompt/response search), AI Search, Audit Logs, DNS Analytics, Digital Experience Monitoring, Cloudflare One CASB, Radar, GraphQL analytics and the Agents SDK docs server, each on its own `*.mcp.cloudflare.com/mcp` hostname. Separately, the Cloudflare API MCP server at mcp.cloudflare.com/mcp (repo: cloudflare/mcp) exposes the whole 2,500+ endpoint Cloudflare API through just two tools, `search` and `execute`, using the Code Mode pattern — model-written JavaScript runs in an isolated Dynamic Worker, costing ~1,000 tokens of context against the ~1.17M an equivalent native-tool server would need. Pick a domain server when you want a readable, curated tool list for one product area; pick the API server for breadth or for endpoints nobody wrote a tool for. All endpoints are Streamable HTTP on `/mcp` and support the MCP 2026-07-28 spec; the historical `/sse` URLs remain as aliases for the same Streamable HTTP handler but no longer serve the deprecated HTTP+SSE transport, so clients pinned to SSE must switch. Auth is OAuth on connect, or a scoped Cloudflare API token as a bearer header for CI. Clients without native remote-MCP support bridge via `npx mcp-remote https://<subdomain>.mcp.cloudflare.com/mcp`.

Live📘
☁️

Cloudflare Pages

Deploy and manage Cloudflare Pages projects via MCP. Trigger deployments, manage environment variables, configure custom domains, and inspect build logs.

Local
🌐

Fetch

Web content fetching and conversion for efficient LLM usage. Extract readable content from any URL.

Local
💻

GitHub MCP Server

authenticated access to the whole GitHub platform — repositories, files, branches, issues, pull requests, Actions runs, security alerts, discussions and notifications — from Claude, Cursor, VS Code, Copilot CLI and any other MCP host. There is no npm package for this server, and that trips up most people who try to install it: `@github/mcp-server` is not published to the npm registry, so any `npx` line you find for it will fail. GitHub ships it three other ways. The easiest is the hosted remote server at https://api.githubcopilot.com/mcp/, which needs no install at all — point an HTTP-transport MCP client at that URL and log in with OAuth (VS Code 1.101+, Claude Desktop, Claude Code, Cursor and Windsurf all support this). The second is the official Docker image ghcr.io/github/github-mcp-server, which is what the copy-paste command on this page runs; on github.com it now performs a browser-based OAuth login on first use and keeps the token in memory only, which is why the published Docker configs map a fixed loopback callback port (-p 127.0.0.1:8085:8085 with GITHUB_OAUTH_CALLBACK_PORT=8085) so the container can receive the callback. Prefer a token? Set GITHUB_PERSONAL_ACCESS_TOKEN instead — it takes precedence over OAuth, and the minimum useful scopes are repo, read:org and read:packages. The third is the native Go binary from the repository's releases, which needs no fixed port for the OAuth flow. GitHub Enterprise Server has no hosted option: use the local server with --gh-host or GITHUB_HOST set to your instance (include the https:// scheme — it defaults to http://, which GHES rejects). Toolsets can be narrowed with GITHUB_TOOLSETS, and an insiders channel is available at /mcp/insiders or via the X-MCP-Insiders header.

Auth required📘

📚 More from the Blog