Вчера я решил, что самому писать себе список задач на день — это слишком в 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 сервер
Весь Worker — около 150 строк TypeScript. Если коротко — нужно сделать три вещи:
- Описать tools — функции, которые AI сможет вызывать. У меня это
get_projects(список проектов),get_today_summary(всё для планирования дня) иmake_daily_plan(готовый план). Каждый tool — это название, описание и схема входных параметров. - Написать логику выполнения — что делать когда AI вызывает tool. В моём случае — сходить в API таск-трекера, получить JSON и вернуть его.
- Реализовать 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 и готово.
