mudko
← back to docs

Cloudflare Worker MCP

Free path to L5

The kit's mcp/server-card.json declares/api/mcp as a callable JSON-RPC endpoint. This is a copy-paste Cloudflare Worker that implements it for you — five-minute deploy, no servers to run.

Using the generated Cloudflare delivery? That bundle's worker.js already implements /api/mcp with the full default tool suite live — you don't need this page. Use this hand-written Worker only to add /api/mcp onto a files-only deploy. To add custom tools either way, see Craft your own MCP tool.

Prerequisites
  • Your domain is on Cloudflare (DNS proxied through them).
  • Wrangler installed: pnpm dlx wrangler@latest --version
  • You've already deployed the kit's .well-known/ files.
1. Project setup
mkdir yoursite-mcp && cd yoursite-mcp
pnpm dlx wrangler@latest init -y
# replace src/index.js and wrangler.toml with the templates below
2. src/index.js
// Cloudflare Worker — implements MCP 2025-06-18 at /api/mcp on your domain.
// Bind a route in wrangler.toml: routes = [{ pattern = "yoursite.com/api/mcp/*", zone_name = "yoursite.com" }]
//
// Edit TOOLS below to match what you advertise in /.well-known/mcp/server-card.json.

const PROTOCOL_VERSION = "2025-06-18";
const SERVER_INFO = {
  name: "yoursite-mcp",      // <-- match mcp/server-card.json serverInfo.name
  version: "1.0.0",
  title: "Yoursite MCP Server",
};

const TOOLS = [
  {
    name: "get_business_info",
    title: "Get Business Info",
    description: "Return name, hours, contact, and a short summary of the business.",
    inputSchema: { type: "object", properties: {} },
    handler: async () => ({
      name: "Yoursite",
      hours: "Mon-Fri 9-5 PT",
      phone: "+1 555 555 0100",
      website: "https://yoursite.com",
      summary: "What you do, in one sentence.",
    }),
  },
  // Add more tools — book_appointment, get_quote, etc.
];

export default {
  async fetch(request) {
    if (request.method === "OPTIONS") {
      return new Response(null, {
        status: 204,
        headers: corsHeaders(),
      });
    }
    if (request.method !== "POST") {
      return json({
        name: SERVER_INFO.name,
        note: "POST JSON-RPC 2.0 here. Methods: initialize, tools/list, tools/call.",
      });
    }
    let body;
    try {
      body = await request.json();
    } catch {
      return json(rpcError(null, -32700, "Parse error"), 400);
    }
    const result = await handle(body);
    return json(result);
  },
};

async function handle(req) {
  if (!req || req.jsonrpc !== "2.0" || typeof req.method !== "string") {
    return rpcError(req?.id ?? null, -32600, "Not a valid JSON-RPC 2.0 request");
  }
  try {
    switch (req.method) {
      case "initialize":
        return rpcOk(req.id, {
          protocolVersion: PROTOCOL_VERSION,
          serverInfo: SERVER_INFO,
          capabilities: { tools: { listChanged: false } },
        });
      case "tools/list":
        return rpcOk(req.id, {
          tools: TOOLS.map(({ handler, ...t }) => t),
        });
      case "tools/call": {
        const { name, arguments: args } = req.params || {};
        const tool = TOOLS.find(t => t.name === name);
        if (!tool) return rpcError(req.id, -32601, "Unknown tool: " + name);
        const result = await tool.handler(args || {});
        return rpcOk(req.id, {
          content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
          structuredContent: result,
          isError: false,
        });
      }
      case "ping":
        return rpcOk(req.id, {});
      default:
        return rpcError(req.id, -32601, "Method not found: " + req.method);
    }
  } catch (err) {
    return rpcError(req.id, -32603, err.message || String(err));
  }
}

function rpcOk(id, result) { return { jsonrpc: "2.0", id: id ?? null, result }; }
function rpcError(id, code, message) { return { jsonrpc: "2.0", id: id ?? null, error: { code, message } }; }
function corsHeaders() {
  return {
    "access-control-allow-origin": "*",
    "access-control-allow-methods": "GET, POST, OPTIONS",
    "access-control-allow-headers": "content-type, accept",
  };
}
function json(obj, status = 200) {
  return new Response(JSON.stringify(obj), {
    status,
    headers: { ...corsHeaders(), "content-type": "application/json" },
  });
}
3. wrangler.toml
name = "yoursite-mcp"
main = "src/index.js"
compatibility_date = "2025-01-01"

routes = [
  { pattern = "yoursite.com/api/mcp", zone_name = "yoursite.com" },
  { pattern = "yoursite.com/api/mcp/*", zone_name = "yoursite.com" },
]

Replace yoursite.com with your domain in both fields.

4. Deploy
pnpm dlx wrangler deploy

Wrangler will prompt for Cloudflare auth on first run. After deploy, verify:

curl -X POST https://yoursite.com/api/mcp \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","method":"tools/list","id":1}'

Should return your tool list.

5. Re-scan

Run the scanner against your domain. Themcp-liveness check should now pass.

Don't want to maintain this?

The flat $450 package ships a live MCP for you (WordPress-plugin / Cloudflare-Worker delivery), and we build & wire custom tools to your data for $150 each. It closes in chat and delivers automatically the moment payment clears. See pricing or buy it in chat.