MCP Server с нуля: полное руководство для разработчиков

Около 22 мин чтения · MACCOME

Аудитория: backend- и AI-разработчики с Python или TypeScript, которым нужно подключить Claude, Cursor или VS Code к внутренним API, БД и файлам без отдельного адаптера на каждую модель. Результат: рабочий MCP Server — от STDIO-прототипа до HTTP+SSE в продакшене, плюс практика с базой знаний. Структура: механика протокола, шесть типичных ошибок, таблицы транспорта и capabilities, код Hello World / Tools / Resources / Prompts, отладка JSON-RPC, восемь шагов rollout, экосистема. Теория протокола: deep dive MCP.

Шесть ошибок при первом MCP Server

  1. MCP как обёртка REST: REST отвечает «можно ли вызвать»; MCP стандартизирует runtime-discovery через tools/list, JSON Schema и stateful-сессии JSON-RPC 2.0.
  2. Только Tools: без Resources (read-only контекст) и Prompts (шаблоны) модель вызывает tools с неполным контекстом — растёт доля неверных аргументов.
  3. STDIO в продакшене: дочерний процесс падает при sleep хоста или расхождении PATH между shell и launchd/systemd.
  4. Auth в каждом клиенте: разрозненные токены в Cursor/Claude Desktop усложняют аудит и увеличивают blast radius.
  5. Community-серверы без pin: ~1 000 публичных серверов без auth (2026) — прямой риск утечки внутренних API.
  6. Отладка только в UI Host: без логов JSON-RPC нельзя отделить timeout transport от schema error (-32602).

Это практический слой под статьёй о протоколе — здесь собираем сервер, там разбираем N×M и экосистему.

Механика протокола: Host → Client → Server

Host (Cursor, Claude Desktop) запускает MCP Client. Client держит 1:1 сессию с MCP Server по JSON-RPC 2.0. Сервер публикует три типа capabilities и проксирует вызовы во внешние системы.

Жизненный цикл запроса: initialize → capability negotiation → tools/listtools/call с валидацией аргументов по JSON Schema на стороне сервера до выполнения side effect.

CapabilityRPCСемантикаГраница производительности
Toolstools/list, tools/callМутации и запросы с side effectТаймаут tool-call должен учитывать cold start + IO; типично 10–30 s steady
Resourcesresources/list, resources/readRead-only по URIРазмер payload влияет на token budget Host; стриминг пока ограничен spec
Promptsprompts/list, prompts/getПараметризованные шаблоныСнижает дублирование system prompt; фиксируйте schema параметров
TransportМеханизмLatency (локально)Ограничение масштаба
STDIOstdin/stdout, subprocess< 5 ms cold (pinned bundle)1 процесс Host ↔ 1 инстанс Server
HTTP + SSEServer-Sent Events + POST /message+ RTT 20–80 ms (EU)Session affinity на LB; не stateless

Шаг 1: подготовка окружения

Требования: Node.js 20 LTS или Python 3.11+, Git, MCP-совместимый Host. Зафиксируйте minor-версии runtime — drift окружения даёт ~40 % «tool not registered» в field-отчётах команд.

bash
mkdir mcp-hello && cd mcp-hello
npm init -y
npm install @modelcontextprotocol/sdk zod
# Python: python3 -m venv .venv && pip install mcp
SDKПакетСтартПрофиль
TypeScript@modelcontextprotocol/sdknode dist/index.jsSTDIO, быстрая интеграция Cursor
Pythonmcppython server.pyData API, ML-пайплайны

Шаг 2: Hello World — минимальный STDIO-сервер

Минимум: ответ на initialize, регистрация tool echo. Ожидание: Host показывает ровно один tool в tools/list.

typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const server = new McpServer({ name: "hello-mcp", version: "1.0.0" });

server.tool(
  "echo",
  { message: z.string().describe("Текст для вывода") },
  async ({ message }) => ({ content: [{ type: "text", text: message }] })
);

await server.connect(new StdioServerTransport());
json
// ~/.cursor/mcp.json
{
  "mcpServers": {
    "hello-mcp": {
      "command": "node",
      "args": ["/abs/path/mcp-hello/dist/index.js"]
    }
  }
}
info

Проверка: абсолютные пути в command/args; тот же OS-user, что и Host. После смены config — перезапуск Host и повтор tools/list.

Шаг 3: Tools — side effects и JSON Schema

Tool выполняет мутацию или запрос с побочным эффектом. Обязательны: уникальное имя, Zod-schema, документированные side effects, явный timeout handler.

typescript
server.tool(
  "get_customer_status",
  {
    customerId: z.string().uuid(),
    includeOrders: z.boolean().default(false),
  },
  async ({ customerId, includeOrders }) => {
    const res = await fetch(`${process.env.CRM_BASE}/customers/${customerId}`);
    if (!res.ok) throw new Error(`CRM HTTP ${res.status}`);
    return { content: [{ type: "text", text: JSON.stringify(await res.json()) }] };
  }
);

Правила проектирования Tools

  • Read/Write split: отдельные серверы или namespace — least privilege и проще аудит.
  • Идемпотентность: для write-tools — ключ идемпотентности в schema, если retry неизбежен.
  • Ошибки JSON-RPC: код + message — модель различает retry и escalate.

Шаг 4: Resources — контекст по URI

Resources — read-only. Стабильные URI: kb://doc/{id}, file:///policy/handbook.md. Host читает через resources/read до tool-call, если контекст критичен.

typescript
server.resource(
  "handbook",
  "kb://policy/handbook",
  async (uri) => ({
    contents: [{
      uri: uri.href,
      mimeType: "text/markdown",
      text: await fs.readFile("./data/handbook.md", "utf8"),
    }],
  })
);

Шаг 5: Prompts — параметризованные workflow

Prompts снижают дублирование длинных system instructions. Параметры фиксируйте в schema — иначе Host и Server расходятся в интерпретации.

typescript
server.prompt(
  "incident_triage",
  { service: z.string(), severity: z.enum(["P1", "P2", "P3"]) },
  ({ service, severity }) => ({
    messages: [{
      role: "user",
      content: { type: "text", text: `Triage ${severity} для ${service}: логи, root cause, 3 шага.` },
    }],
  })
);

Шаг 6: HTTP+SSE для удалённого доступа

При shared-доступе команды переходите с STDIO на HTTP+SSE. SSE держит long-lived connection — load balancer обязан обеспечить session affinity, иначе POST /message попадёт не в тот transport instance.

typescript
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import express from "express";

const app = express();
let transport;
app.get("/sse", async (req, res) => {
  transport = new SSEServerTransport("/message", res);
  await server.connect(transport);
});
app.post("/message", express.json(), (req, res) => transport.handlePostMessage(req, res));
app.listen(8787, "0.0.0.0");
ПараметрDevProd
TLSlocalhostnginx/Caddy termination, HSTS
First-call timeout60 s30 s после prewarm
AuthопциональноBearer/OAuth на entrypoint сервера
LB affinityN/Asticky cookie или single pod per session

Шаг 7: отладка и тесты JSON-RPC

СимптомДанныеКореньДействие
Пустой tools/listexit code, stderrНеверный cwd/PATHАбсолютный путь; тот же user
Медленный первый callnpm/pip fetchnpx -y JITPin версий; prewarm image
-32602 Invalid paramsdiff schemaZod vs client expectationСверить output tools/list
Steady timeoutCPU, FD countBlocking IOA/B с read-only tool
bash
npx @modelcontextprotocol/inspector node dist/index.js
# Цепочка: initialize → tools/list (≥1) → tools/call echo

Шаг 8: деплой в продакшен

  • Process manager: launchd/systemd, Restart=on-failure, EnvironmentFile.
  • Secrets: только secret store — не в git-tracked mcp.json.
  • Observability: structured log на каждый tools/call с correlation_id; метрики p95 latency, error rate, active SSE sessions.
  • Release: semver сервера; Host pin minor.

Практика: сервер базы знаний

Связка Resources (Markdown-доки) + tool semantic_search (векторный или BM25 индекс). Поток: resources/list → search → цитирование URI — снижает hallucination в support- и compliance-сценариях.

typescript
server.tool(
  "semantic_search",
  { query: z.string().min(3), topK: z.number().int().max(10).default(5) },
  async ({ query, topK }) => {
    const hits = await index.search(query, topK);
    return {
      content: [{
        type: "text",
        text: hits.map(h => `${h.uri} (${h.score})\n${h.snippet}`).join("\n---\n"),
      }],
    };
  }
);

Экосистема 2026

  • 10 000+ community servers — reuse с version pin и security review.
  • AAIF / Linux Foundation — governance с Q2 2026; следите за semver spec.
  • A2A — горизонталь agent↔agent; MCP — вертикаль model↔tools.
  • Связь с гайдом Agent Skill: Skills — пакеты в Host; MCP — portable tool protocol.

Восемь шагов: от нуля до prod

  1. Выбор стека и pin runtime.
  2. Hello World STDIO — зелёный inspector.
  3. Read-tool + Resource на реальном API.
  4. Prompt для повторяемого workflow.
  5. Config Cursor/Claude с абсолютными путями.
  6. HTTP+SSE прототип при team > 1.
  7. Auth + logging до PII-данных.
  8. 7×24 host для long-lived sessions.

Три измеримых параметра

  • −38…−55 % стоимости интеграции — один Server на Claude/GPT/Gemini.
  • STDIO cold < 5 ms (pinned) vs 30–60 s первый npx -y без pin.
  • 10 000+ servers (2026) — сетевой эффект экосистемы.

Итог: локальный прототип vs стабильный runtime

MCP Server на dev-Mac — правильный старт, но три предела видны быстро: STDIO/SSE рвётся при sleep, PATH и secrets расходятся в команде, многошаговые agent workflow требуют стабильного uptime. Generic Linux VM без нативного macOS ломает toolchain для Xcode/Metal, если server трогает Apple-native API.

Для постоянного запуска MCP Server и agent-оркестрации выделенный узел MACCOME Mac mini M4 / M4 Pro 7×24 обычно дешевле по TCO, чем борьба с sleep на ноутбуке. Тарифы: цены аренды; онбординг: центр помощи.

Частые вопросы

Python или TypeScript для первого MCP Server?

TypeScript — быстрый STDIO и Cursor. Python — data-пайплайны. Оба production-ready; выбор по стеку команды.

STDIO или HTTP+SSE?

STDIO для solo-dev. HTTP+SSE при remote team — TLS, session affinity, централизованная auth.

Как связаны MCP и Agent Skills?

Skills — prompt-пакеты в Host; MCP — открытый tool protocol. Подробнее в гайде Agent Skill.

Какой хост для prod MCP?

Избегайте sleep ноутбука для long-lived SSE. MACCOME — выделенные M4/M4 Pro 7×24. Цены и центр помощи.