Back to Blog
DevelopmentMar 8, 202616 min read

Build and Deploy an MCP Server on Cloudflare Workers — Complete Guide (2026)

NT

Nikhil Tiwari

MCP Playground

📖 TL;DR — Key Takeaways

  • Cloudflare Workers is the fastest path to a globally deployed, remotely accessible MCP server — no containers, no cold starts, 300+ edge locations
  • Two official approaches: McpAgent (Agents SDK — recommended for stateful servers) and workers-mcp (CLI approach — simpler for beginners)
  • Free tier: 100,000 requests/day — enough for personal projects and early production. Paid plan from $5/month for 10M requests
  • Deploy with wrangler deploy — your MCP server is live at your-worker.your-account.workers.dev/mcp in under a minute
  • Cloudflare KV gives your MCP tools persistent storage; Durable Objects enable per-session state with zero infrastructure

Most MCP server tutorials end at "it works on localhost." You get a stdio-based server running in Claude Desktop, and everything is great — until you want to share it with your team, expose it to a remote MCP client, or run it in production without keeping your laptop open.

Cloudflare Workers solves this. It gives you a serverless runtime that is genuinely global — your MCP server runs within milliseconds of any user, anywhere in the world, on Cloudflare's edge network. No VPS to maintain, no Docker containers to orchestrate, no cold start delays. Deploy a TypeScript file and you have a remote MCP server with Streamable HTTP transport that any MCP client can connect to.

Cloudflare has made MCP a first-class citizen in its developer platform. The official Agents SDK ships with an McpAgent class that handles transport, session state, and tool registration out of the box. The older workers-mcp package offers a simpler CLI-based approach for teams who want to translate existing TypeScript Workers into MCP tools without rewriting anything.

This guide covers both approaches with real code, explains when to use each, adds persistent KV storage, and walks through deploying and connecting to Claude Desktop.


Why Cloudflare Workers for MCP?

Running an MCP server on Cloudflare Workers gives you advantages that are genuinely hard to replicate with other deployment options:

⚡ Zero Cold Starts

Workers use V8 isolates, not containers. Your MCP server responds in milliseconds — no spinning up waiting for a Lambda to wake

🌍 300+ Edge Locations

Your server runs in the nearest Cloudflare datacenter to each user — sub-50ms latency globally, not just in us-east-1

🆓 Generous Free Tier

100,000 requests/day free forever. Enough for personal projects, team tools, and early-stage production without paying anything

🔌 Streamable HTTP Native

The Agents SDK handles the Streamable HTTP transport (the current MCP spec standard) automatically — no manual transport code

🗃️ KV + Durable Objects

Persistent key-value storage (KV) and per-session stateful objects (Durable Objects) built in — no external database needed for most use cases

🚀 One Command Deploy

wrangler deploy ships your server to the global edge. No Docker, no CI/CD setup, no infrastructure configuration

Compared to other deployment options: a VPS (Hetzner, DigitalOcean) requires server management and is a single location. AWS Lambda has cold starts and per-region configuration. A local Python server means keeping your machine running 24/7. Cloudflare Workers removes all of these tradeoffs for most MCP use cases.


Two Approaches: McpAgent vs workers-mcp

Cloudflare has two official paths to building MCP servers on Workers. Understanding the difference saves significant confusion:

McpAgent (Agents SDK) workers-mcp
Packagecloudflare/agentsworkers-mcp
ApproachExtend the McpAgent class, define tools in init()CLI translates TypeScript class methods into MCP tools via a local proxy
TransportStreamable HTTP (built-in, no config)Stdio proxy → HTTP (local Node.js proxy required)
StateDurable Object per session — built-in statefulStateless (Workers stateless model)
Remote clientsYes — any MCP client connects via URLLocal only via stdio proxy
Best forProduction, remote MCP, stateful tools, team sharingQuick prototypes, existing Workers you want to expose as MCP tools
Recommended✅ Yes — current recommended approachSimpler for getting started locally

This guide covers McpAgent first (the recommended production path), then workers-mcp for simpler use cases.


Prerequisites

  • Node.js 18+ installed
  • A Cloudflare account — free to create
  • Wrangler CLI: npm install -g wrangler
  • Claude Desktop installed (for testing)

Authenticate Wrangler with your Cloudflare account before starting:

wrangler login

Approach 1: McpAgent (Recommended)

The Agents SDK's McpAgent class is Cloudflare's first-class approach to MCP servers. It handles Streamable HTTP transport automatically, creates a Durable Object per session for state management, and deploys as a standard Cloudflare Worker.

Step 1 — Scaffold the Project

Use the official Cloudflare remote MCP template — the quickest path to a working server:

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

cd my-mcp-server
npm install

This gives you a minimal Worker with McpAgent pre-configured. Alternatively, start from scratch:

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

Step 2 — Project Structure

my-mcp-server/
├── src/
│   └── index.ts        # Your MCP server
├── wrangler.jsonc       # Cloudflare Worker config
├── package.json
└── tsconfig.json

Step 3 — Write Your MCP Server

Open src/index.ts and build your MCP server by extending McpAgent. Tools are defined in the init() method — they have access to this (the agent instance), which means they can read and write state:

import { McpAgent } from "cloudflare/agents/mcp";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";

// Define your environment bindings
export interface Env {
  MY_KV: KVNamespace;       // optional — add if using KV storage
  MCP_OBJECT: DurableObjectNamespace;
}

// Extend McpAgent with your Env type
export class MyMcpServer extends McpAgent {
  server = new McpServer({
    name: "my-mcp-server",
    version: "1.0.0",
  });

  async init() {
    // Tool 1: Simple calculation tool
    this.server.tool(
      "add_numbers",
      "Add two numbers together",
      {
        a: z.number().describe("First number"),
        b: z.number().describe("Second number"),
      },
      async ({ a, b }) => ({
        content: [{ type: "text", text: `${a} + ${b} = ${a + b}` }],
      })
    );

    // Tool 2: Fetch data from an external API
    this.server.tool(
      "get_weather",
      "Get current weather for a city",
      {
        city: z.string().describe("City name"),
      },
      async ({ city }) => {
        const res = await fetch(
          `https://wttr.in/${encodeURIComponent(city)}?format=3`
        );
        const text = await res.text();
        return {
          content: [{ type: "text", text: text.trim() }],
        };
      }
    );

    // Tool 3: Read from KV storage
    this.server.tool(
      "get_note",
      "Retrieve a saved note by key",
      {
        key: z.string().describe("The note key to retrieve"),
      },
      async ({ key }) => {
        const value = await this.env.MY_KV.get(key);
        return {
          content: [
            {
              type: "text",
              text: value ?? `No note found for key: ${key}`,
            },
          ],
        };
      }
    );

    // Tool 4: Write to KV storage
    this.server.tool(
      "save_note",
      "Save a note with a key",
      {
        key: z.string().describe("The key to save under"),
        value: z.string().describe("The note content"),
      },
      async ({ key, value }) => {
        await this.env.MY_KV.put(key, value);
        return {
          content: [{ type: "text", text: `Saved note under key: ${key}` }],
        };
      }
    );
  }
}

// Export the Worker handler — McpAgent.serve() handles routing
export default McpAgent.serve("/mcp", MyMcpServer);

Note: McpAgent.serve("/mcp", MyMcpServer) creates the Worker's fetch handler and routes POST /mcp requests to your agent. The Streamable HTTP transport is handled entirely by the SDK — you don't write any transport code.

Step 4 — Configure wrangler.jsonc

{
  "name": "my-mcp-server",
  "main": "src/index.ts",
  "compatibility_date": "2025-11-01",
  "compatibility_flags": ["nodejs_compat"],

  "durable_objects": {
    "bindings": [
      {
        "name": "MCP_OBJECT",
        "class_name": "MyMcpServer"
      }
    ]
  },

  "kv_namespaces": [
    {
      "binding": "MY_KV",
      "id": "YOUR_KV_NAMESPACE_ID"
    }
  ],

  "migrations": [
    {
      "tag": "v1",
      "new_classes": ["MyMcpServer"]
    }
  ]
}

If you are not using KV storage, remove the kv_namespaces section entirely. The Durable Objects binding is required for McpAgent session state.


Approach 2: workers-mcp (Simpler, Local)

The workers-mcp package takes a different approach: you write a normal TypeScript Worker class with regular methods, and the CLI translates those methods into MCP tools. A local Node.js proxy acts as the stdio bridge between Claude Desktop and your remote Worker.

This approach is simpler to get started with, but the local proxy means it only works for stdio-based MCP clients (like Claude Desktop locally). It does not support remote MCP connections without extra configuration.

Step 1 — Create a Worker and Install workers-mcp

npm create cloudflare@latest -- my-worker
cd my-worker
npm install workers-mcp

Step 2 — Write Your Worker as a TypeScript Class

import { WorkerEntrypoint } from "cloudflare:workers";
import { ProxyToSelf } from "workers-mcp";

export default class MyWorkerMCP extends WorkerEntrypoint<Env> {
  /**
   * Returns a greeting message for the given name.
   * @param name {string} The name to greet
   * @return {string} A personalized greeting
   */
  async greet(name: string): Promise<string> {
    return `Hello, ${name}! This response came from a Cloudflare Worker.`;
  }

  /**
   * Fetches the current time in a given timezone.
   * @param timezone {string} IANA timezone string e.g. "America/New_York"
   * @return {string} Current time in that timezone
   */
  async getCurrentTime(timezone: string): Promise<string> {
    return new Date().toLocaleString("en-US", { timeZone: timezone });
  }

  // Required: routes MCP requests to the correct method
  async fetch(request: Request): Promise<Response> {
    return new ProxyToSelf(this).fetch(request);
  }
}

Important: JSDoc comments on your methods are requiredworkers-mcp uses them to generate the MCP tool descriptions. The @param and @return tags become the tool's input schema and output description.

Step 3 — Run the CLI Setup

npx workers-mcp setup

This command deploys your Worker to Cloudflare and generates the local proxy configuration. It outputs a config block you add to Claude Desktop.


Adding KV Storage to Your MCP Tools

Cloudflare KV is a globally distributed key-value store — ideal for saving notes, caching API responses, storing user preferences, or maintaining tool state between sessions. Here's how to create a KV namespace and bind it to your Worker:

Create the KV Namespace

# Create the namespace and note the ID it returns
npx wrangler kv namespace create "MY_KV"

# Output:
# ✅ Successfully created namespace
# Add the following to your wrangler.jsonc:
# { "binding": "MY_KV", "id": "abc123...your-namespace-id" }

Add the Binding to wrangler.jsonc

{
  "kv_namespaces": [
    {
      "binding": "MY_KV",
      "id": "abc123...your-namespace-id"
    }
  ]
}

Use KV in Your MCP Tool

// Inside your McpAgent's init() method:
this.server.tool(
  "remember",
  "Store a piece of information for later retrieval",
  {
    label: z.string().describe("A short label for what you're storing"),
    content: z.string().describe("The information to remember"),
  },
  async ({ label, content }) => {
    await this.env.MY_KV.put(label, content, {
      expirationTtl: 86400, // expires after 24 hours (optional)
    });
    return {
      content: [{ type: "text", text: `Stored "${label}" — I'll remember that.` }],
    };
  }
);

this.server.tool(
  "recall",
  "Retrieve previously stored information by label",
  {
    label: z.string().describe("The label of the information to recall"),
  },
  async ({ label }) => {
    const value = await this.env.MY_KV.get(label);
    return {
      content: [
        {
          type: "text",
          text: value
            ? `Recalled "${label}": ${value}`
            : `Nothing stored under "${label}"`,
        },
      ],
    };
  }
);

Deploy with Wrangler

Test Locally First

npx wrangler dev

This starts a local dev server at http://localhost:8787. Your MCP endpoint is available at http://localhost:8787/mcp. You can test it with any MCP client — or paste the URL into MCP Playground to inspect tools and run live requests without any client setup.

Deploy to Production

npx wrangler deploy

That's it. Wrangler builds your TypeScript, bundles it, and deploys to Cloudflare's global edge. The output shows your live URL:

✅ Deployed my-mcp-server (1.23 sec)
   https://my-mcp-server.your-account.workers.dev

Your MCP endpoint is live at:
   https://my-mcp-server.your-account.workers.dev/mcp

This URL is your permanent remote MCP server URL. Share it with teammates, add it to Claude Desktop, or connect any MCP-compatible client.

View Logs

# Stream real-time logs from your deployed Worker
npx wrangler tail my-mcp-server

Connect to Claude Desktop

Open your Claude Desktop config file at ~/.config/claude/claude_desktop_config.json (macOS/Linux) or %APPDATA%\Claude\claude_desktop_config.json (Windows) and add your Worker URL:

For McpAgent (Remote, Recommended)

{
  "mcpServers": {
    "my-mcp-server": {
      "url": "https://my-mcp-server.your-account.workers.dev/mcp"
    }
  }
}

For workers-mcp (Local Proxy)

The npx workers-mcp setup command generates this config automatically. It looks like:

{
  "mcpServers": {
    "my-worker": {
      "command": "node",
      "args": [
        "/path/to/your/project/node_modules/workers-mcp/dist/index.js",
        "my-worker",
        "https://my-worker.your-account.workers.dev"
      ]
    }
  }
}

Save the config file and restart Claude Desktop. You should see your server listed in the MCP connections panel — and your tools available when starting a new conversation.

Testing tip: Before adding to Claude Desktop, paste your /mcp URL into MCP Playground to verify your server responds correctly and your tools are visible. It takes 10 seconds and saves debugging time.


Pricing and Free Tier Limits

Feature Free Plan Paid Plan ($5/month)
Requests100,000 / day10 million / month (~333k/day avg)
CPU time10ms per request30 million CPU-ms / month
KV reads100,000 / day10 million / month
KV writes1,000 / day1 million / month
Durable ObjectsNot includedIncluded (required for McpAgent sessions)
Custom domainsworkers.dev subdomain onlyCustom domains supported

Important: McpAgent uses Durable Objects for session state. Durable Objects are not available on the free plan — you need at minimum the $5/month Workers Paid plan to use the McpAgent approach in production. The free workers-mcp approach does not require Durable Objects and works on the free tier.

For most personal and team MCP servers, the paid plan at $5/month is more than sufficient. At 10M requests/month, you would need roughly 3–4 MCP tool calls per second continuously to hit the limit.


When NOT to Use Cloudflare Workers for MCP

Cloudflare Workers is excellent for most MCP use cases, but it has real constraints:

  • CPU-intensive tools — Workers have a 30-second CPU limit (paid). Long-running ML inference, video processing, or heavy computation will hit this. Use a VPS or dedicated compute instead.
  • Large binary data — Workers have a 128MB memory limit. If your MCP tools process large files, images, or datasets, you'll need a different runtime.
  • Node.js-specific libraries — Workers run in V8 isolates, not Node.js. Most npm packages work, but packages that depend on native Node.js APIs (child_process, fs in full form) do not.
  • Persistent long-running connections — Workers terminate after the request completes. If your tools need to maintain a persistent connection to an external system, Durable Objects can help but add complexity.
  • Database-heavy MCP servers — KV is not a relational database. For complex queries, joins, or large datasets, pair your Worker with an external database (PlanetScale, Supabase, Neon) via their APIs.

For these cases, the Docker deployment guide or a traditional VPS is a better fit. Cloudflare Workers excels at lightweight, fast, API-wrapping MCP servers — which covers the majority of what developers actually build.


Complete wrangler.jsonc Reference

For reference, a fully configured wrangler.jsonc for an McpAgent server with KV storage:

{
  "$schema": "node_modules/wrangler/config-schema.json",
  "name": "my-mcp-server",
  "main": "src/index.ts",
  "compatibility_date": "2025-11-01",
  "compatibility_flags": ["nodejs_compat"],

  // Durable Objects — required for McpAgent session state
  "durable_objects": {
    "bindings": [
      {
        "name": "MCP_OBJECT",
        "class_name": "MyMcpServer"
      }
    ]
  },

  // KV namespace — optional, for persistent storage
  "kv_namespaces": [
    {
      "binding": "MY_KV",
      "id": "YOUR_KV_NAMESPACE_ID",
      "preview_id": "YOUR_PREVIEW_KV_ID"  // for local dev
    }
  ],

  // Required when using Durable Objects
  "migrations": [
    {
      "tag": "v1",
      "new_classes": ["MyMcpServer"]
    }
  ]
}

Frequently Asked Questions

Do I need a Cloudflare account with a paid plan?+
It depends on which approach you use. The workers-mcp approach (local proxy) works on the free tier. The McpAgent approach requires Durable Objects, which require the Workers Paid plan at $5/month. For most production MCP servers, the $5/month plan is the right choice.
Can I use environment variables and secrets in my MCP tools?+
Yes. Use wrangler secret put SECRET_NAME to store secrets securely. They're available in your Worker as this.env.SECRET_NAME. Never hardcode API keys in your Worker source — always use Wrangler secrets for anything sensitive.
Can multiple MCP clients connect to the same Worker simultaneously?+
Yes. Each MCP session gets its own Durable Object instance when using McpAgent — there is no shared state between sessions. Workers scale automatically with traffic, so multiple clients connecting simultaneously is fully supported.
How do I add authentication so only my team can use the MCP server?+
Cloudflare supports OAuth 2.1 for remote MCP servers via the Agents SDK — see the authorization docs. For simpler token-based auth, you can check an Authorization header in your Worker's fetch handler before routing to McpAgent. Cloudflare Access can also protect your Worker URL without any code changes.
How is this different from just building a REST API?+
A REST API requires the AI to know your endpoint structure, parameter names, and response format — and someone has to write the integration code. An MCP server exposes tools with self-describing schemas that any MCP client (Claude, Cursor, any agent) can discover and use automatically, without custom integration code. MCP is the protocol layer on top of your Worker that makes it AI-native.

Related Guides


Resources:

NT

Written by Nikhil Tiwari

15+ years in product development. AI enthusiast building developer tools that make complex technologies accessible to everyone.