Как поднять свой MCP сервер на Cloudflare Workers

Вчера я решил, что самому писать себе список задач на день — это слишком в 2026 году. Поэтому я поднял MCP сервер, чтобы любая нейросетка могла подключиться к моему таск-трекеру, получить активные проекты и составить to-do на день в нужном мне формате.

MCP (Model Context Protocol) — протокол от Anthropic, который даёт AI-ассистентам доступ к внешним данным и инструментам. Я подключаюсь через Cursor, можно через Claude Desktop или даже через веб-версию ChatGPT, но пока только в режиме разработчика.

Архитектура

В моём таск-трекере я сделал простой API, который возвращает активные проекты, задачи и заметки в JSON. MCP сервер на Cloudflare Workers обращается к этому API и предоставляет данные нейросетке.

Почему Cloudflare

Cloudflare использую как хостинг доменов. Воркеры на бесплатном тарифе позволяют делать 100 000 запросов в день, 10 мс CPU на запрос. Плюс — edge-сеть. Worker крутится на ближайшем к тебе сервере, поэтому отклик минимальный.

Все делал через CLI. Установил wrangler, залогинился одной командой, секреты добавил через wrangler secret put — и деплой.

Как поднять свой MCP сервер на Cloudflare Workers

Сам MCP сервер

Весь Worker — около 150 строк TypeScript. Если коротко — нужно сделать три вещи:

  1. Описать tools — функции, которые AI сможет вызывать. У меня это get_projects (список проектов), get_today_summary (всё для планирования дня) и make_daily_plan (готовый план). Каждый tool — это название, описание и схема входных параметров.
  2. Написать логику выполнения — что делать когда AI вызывает tool. В моём случае — сходить в API таск-трекера, получить JSON и вернуть его.
  3. Реализовать MCP протокол — обработать JSON-RPC запросы initialize, tools/list и tools/call. Звучит сложно, но по факту это один switch-case на 30 строк.

Структура проекта

mcp-worker/
├── package.json       — зависимости и скрипты
├── wrangler.toml      — конфиг Cloudflare: имя воркера, переменные окружения
└── src/
    └── index.ts       — весь код MCP сервера

package.json

Минимальный набор: TypeScript, типы для Cloudflare и wrangler — CLI для деплоя.

{
  "name": "my-mcp-server",
  "type": "module",
  "scripts": {
    "dev": "wrangler dev",
    "deploy": "wrangler deploy"
  },
  "devDependencies": {
    "@cloudflare/workers-types": "^4.20241205.0",
    "typescript": "^5.7.2",
    "wrangler": "^3.99.0"
  }
}

wrangler.toml

Конфиг Cloudflare. name — имя воркера (будет в URL), vars — переменные окружения. API ключ сюда не пишем — добавим как секрет через CLI.

name = "my-mcp-server"
main = "src/index.ts"
compatibility_date = "2024-12-01"
compatibility_flags = ["nodejs_compat"]
[vars]
API_URL = "https://your-app.com/api"

src/index.ts

Основной файл. Тут описание tools, логика их выполнения и обработка MCP-запросов. Выглядит много, но половина — это просто JSON-схемы для tools.

interface Env {
  API_URL: string;
  API_KEY: string;
}
// Запрос к вашему API
async function apiRequest(env: Env, endpoint: string, params: Record<string, string> = {}) {
  const url = new URL(`${env.API_URL}/${endpoint}`);
  Object.entries(params).forEach(([k, v]) => v && url.searchParams.set(k, v));
  const res = await fetch(url.toString(), {
    headers: { "X-API-Key": env.API_KEY }
  });
  
  if (!res.ok) throw new Error(`API error: ${res.status}`);
  return res.json();
}
// Инструменты MCP
const TOOLS = [
  {
    name: "get_projects",
    description: "Get list of projects with tasks",
    inputSchema: {
      type: "object",
      properties: {
        status: { type: "string", enum: ["active", "done", "archive"] },
        limit: { type: "number" }
      }
    }
  },
  {
    name: "get_today_summary",
    description: "Get all data for daily planning: projects, tasks, notes",
    inputSchema: {
      type: "object",
      properties: {
        date: { type: "string", description: "YYYY-MM-DD" }
      }
    }
  },
  {
    name: "make_daily_plan",
    description: "Analyze projects and create work plan for the day",
    inputSchema: {
      type: "object",
      properties: {
        date: { type: "string" },
        work_hours: { type: "number", description: "Available hours (default 8)" }
      }
    }
  }
];
// Выполнение tool'а — AI вызывает по имени, мы делаем запрос к API
async function executeTool(env: Env, name: string, args: any): Promise<string> {
  switch (name) {
    
    // Простой случай: получаем JSON из API и возвращаем как есть
    case "get_projects": {
      const data = await apiRequest(env, "projects", {
        status: args?.status || "active",
        limit: args?.limit?.toString() || "50"
      });
      return JSON.stringify(data, null, 2);
    }
    
    // То же самое — просто прокидываем данные
    case "get_today_summary": {
      const data = await apiRequest(env, "today", { date: args?.date || "" });
      return JSON.stringify(data, null, 2);
    }
    
    // А тут интереснее — получаем данные и сами формируем план
    // Можно было бы отдать сырой JSON и пусть AI сам форматирует,
    // но так результат стабильнее и в нужном мне формате
    case "make_daily_plan": {
      const data = await apiRequest(env, "today", { date: args?.date || "" });
      
      let plan = `# План на ${data.date}\n\n`;
      
      // Сначала фокус — главные задачи на день
      plan += `## Фокус\n`;
      data.focus_items?.forEach((item: any) => {
        plan += `- **${item.project}**: ${item.task}\n`;
      });
      
      // Потом активные проекты с задачами
      plan += `\n## Проекты\n`;
      data.projects?.filter((p: any) => p.active).forEach((p: any) => {
        plan += `\n### ${p.name}\n`;
        p.tasks?.slice(0, 3).forEach((t: any) => {
          plan += `- ${t.name}\n`;
        });
      });
      
      return plan;
    }
    
    default:
      throw new Error(`Unknown tool: ${name}`);
  }
}
// Обработка MCP запросов — это JSON-RPC 2.0
// AI-клиент присылает method, мы отвечаем result
async function handleMCP(env: Env, request: any): Promise<any> {
  const { method, params, id } = request;
  switch (method) {
    // Первый запрос при подключении — отдаём инфу о сервере
    case "initialize":
      return {
        jsonrpc: "2.0", id,
        result: {
          protocolVersion: "2024-11-05",
          capabilities: { tools: {} },
          serverInfo: { name: "my-mcp", version: "1.0.0" }
        }
      };
    // AI спрашивает какие tools доступны
    case "tools/list":
      return { jsonrpc: "2.0", id, result: { tools: TOOLS } };
    // AI вызывает конкретный tool с аргументами
    case "tools/call":
      const result = await executeTool(env, params.name, params.arguments);
      return {
        jsonrpc: "2.0", id,
        result: { content: [{ type: "text", text: result }] }
      };
    default:
      return { jsonrpc: "2.0", id, error: { code: -32601, message: "Method not found" } };
  }
}
// Cursor ожидает ответ в формате Server-Sent Events
// Просто оборачиваем JSON в event: message
function formatSSE(data: any): string {
  return `event: message\ndata: ${JSON.stringify(data)}\n\n`;
}
// Точка входа — обычный HTTP handler для Cloudflare Workers
export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);
    
    // CORS нужен, чтобы браузерные клиенты могли подключаться
    const cors = {
      "Access-Control-Allow-Origin": "*",
      "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
      "Access-Control-Allow-Headers": "Content-Type, Accept",
    };
    // Preflight запрос для CORS
    if (request.method === "OPTIONS") {
      return new Response(null, { headers: cors });
    }
    // Health check — удобно для проверки что сервер живой
    if (url.pathname === "/") {
      return new Response(JSON.stringify({ status: "ok" }), {
        headers: { "Content-Type": "application/json", ...cors }
      });
    }
    // Основной endpoint — сюда приходят MCP запросы
    if (url.pathname === "/mcp" && request.method === "POST") {
      const body = await request.json();
      const response = await handleMCP(env, body);
      return new Response(formatSSE(response), {
        headers: { "Content-Type": "text/event-stream", ...cors }
      });
    }
    return new Response("Not Found", { status: 404 });
  }
};

Деплой

# Установка
npm install
# Логин в Cloudflare
npx wrangler login
# Секретный ключ для API
npx wrangler secret put API_KEY
# Деплой
npm run deploy

Получаете URL: https://my-mcp-server.username.workers.dev Подключаетесь к Cursor и готово.

keyboard_arrow_up