Перевод MCP-сервера из локального прототипа в продакшен требует смены транспорта с STDIO на Streamable HTTP, контейнеризации в Docker (multi-stage build, 15–40 MB), внедрения Bearer-аутентификации, rate limiter на 100 запросов/мин на ключ и мониторинга через Prometheus + OpenTelemetry. Спецификация MCP определяет три транспорта, но для продакшена подходит только Streamable HTTP. Docker обеспечивает изоляцию, SystemPrompt описывает полный production-стек: nginx, health checks, Prometheus-метрики и HPA в Kubernetes.

3транспорта MCP (только 1 для продакшена)
100+MCP-серверов в Docker MCP Catalog
60%команд переходят на HTTP за 2025–2026
15–40MB размер production-образа

Проблема: почему STDIO не работает в продакшене

Собрать MCP-сервер на выходные — приятный воскресный проект. Вы ставите Python/NP-зависимости, пишете пару тулов для базы данных, подключаете к Claude Desktop через конфиг — и AI-агент уже умеет отвечать на вопросы по вашей БД. Работает.

В понедельник приходит коллега и спрашивает: «Дай доступ». И всё ломается.

Проблема в том, что STDIO-транспорт — это процесс, который Claude Code порождает как дочерний. Он читает из stdin и пишет в stdout. STDIO привязан к одной машине, одному терминалу, одной сессии. Нет URL, который можно передать коллеге. Нет эндпоинта, на который можно указать из другого клиента. Нет способа подключить второго пользователя.

Спецификация MCP определяет STDIO как транспорт по умолчанию — и это удобно для разработки. Но продакшену нужны другие свойства:

Несколько разработчиков, подключающихся к одному серверу. Централизованные логи и мониторинг. Аутентификация, чтобы только авторизованные пользователи вызывали тулы. Rate limiting, чтобы развязавшаяся сессия AI не положила ваш бэкенд. Health checks для оркестратора. Горизонтальное масштабирование.

С STDIO ни одно из этих требований не выполняется полностью. Решение — Streamable HTTP: транспорт, спроектированный для продакшена.

Архитектура production MCP-сервера: от клиентов через nginx, аутентификацию, rate limiter и сессии к HTTP-обработчику с мониторингом и бэкендами

Архитектура production MCP-сервера: клиенты → nginx → аутентификация → rate limiter → сессии → HTTP handler → бэкенды. SystemPrompt

Транспорты MCP: STDIO vs Streamable HTTP

Спецификация MCP определяет три механизма транспорта. Выбор между ними — первое и самое важное архитектурное решение.

ТранспортНаправлениеКонкурентностьСостояние сессииКогда использовать
STDIOДвунаправленный через stdin/stdout дочернего процессаОдин клиент на процессНеявное (время жизни процесса)Локальная разработка, одно-пользовательские тулы, Claude Code по умолчанию
Streamable HTTPPOST для запросов; опциональный SSE-апгрейд для стримингаМножество клиентов и сессийЧерез заголовок Mcp-Session-IdПродакшен, удалённый доступ, multi-user
HTTP+SSE (legacy)Отдельные эндпоинты для клиент-сервер и сервер-клиентМножество клиентовСервернаяУстарел; заменён на Streamable HTTP

Источник: MCP Transports specification 2025-11-25

STDIO — самый простой способ. Клиент порождает сервер как дочерний процесс, сообщения идут через stdin/stdout. Не требуется сетевой конфигурации, работает везде. Но он принципиально локальный: один клиент, один сервер, одна машина.

Streamable HTTP — это production-транспорт. Сервер выставляет HTTP-эндпоинт (обычно /mcp), клиент отправляет JSON-RPC запросы в теле POST. Сервер может ответить простым JSON или апгрейднуть соединение до SSE для стриминга. Управление сессиями — через заголовок Mcp-Session-Id. С июня 2025 спецификация требует также заголовок MCP-Protocol-Version для согласования версий протокола.

SSE (legacy) — отдельный SSE-эндпоинт для сообщений от сервера к клиенту и отдельный POST для клиент-сервер. Всё ещё работает, но спецификация рекомендует Streamable HTTP для всех новых реализаций.

Докеризация MCP-сервера

Docker решает три ключевые проблемы MCP-серверов, которые описал Docker в своём гайде:

Runtime. MCP-серверы зависят от конкретной версии Python или Node.js. Комбинирование тулов из разных экосистем — головная боль. Docker стабилизирует среду выполнения: любой, у кого есть Docker Engine, запускает ваш сервер без ручного управления рантаймами.

Безопасность. Давать LLM прямой доступ к хост-системе неприемлемо за пределами pet-проектов. Docker обеспечивает sandbox-изоляцию: у LLM нет доступа к хост-файловой системе, если только вы явно не примонтируете том. Конфиги с API-ключами — это ещё одна головная боль: MCP-конфиг в текстовом JSON хранит все данные, нужные агенту для действия, там же лежит всё, что нужно злоумышленнику для эксплойта. Docker Secrets решают эту проблему: секреты видны только процессу внутри контейнера и не появляются даже при docker inspect.

Деплой. Один Docker-образ можно развернуть где угодно — на сервере, в Kubernetes, в CI/CD пайплайне. Multi-stage build даёт образы 15–40 MB вместо гигабайтов с dev-зависимостями.

Пример production-ready Dockerfile для MCP-сервера на TypeScript:

FROM node:22-alpine AS builder
WORKDIR /app
COPY package.json tsconfig.json ./
RUN npm ci
COPY src/ ./src/
RUN npm run build

FROM node:22-alpine AS runtime
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3001
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:3001/health || exit 1
USER node
CMD ["node", "dist/index.js"]

Сборка и запуск:

docker build -t mcp-production-server .
docker run -d -p 3001:3001 \
  -e MCP_API_KEYS="sk-..." \
  --name mcp-server \
  mcp-production-server

Docker Compose для продакшена

Когда к MCP-серверу подключаются база данных, Redis и nginx:

version: "3.8"
services:
  mcp-server:
    build: .
    ports:
      - "3001:3001"
    environment:
      - MCP_API_KEYS=${MCP_API_KEYS}
      - DATABASE_URL=${DATABASE_URL}
    secrets:
      - db_password
    healthcheck:
      test: ["CMD", "wget", "--spider", "http://localhost:3001/health"]
      interval: 30s
      timeout: 5s
      retries: 3

  nginx:
    image: nginx:alpine
    ports:
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      mcp-server:
        condition: service_healthy

secrets:
  db_password:
    file: ./secrets/db_password.txt

Такой Compose-файл — основа для production-деплоя. nginx терминирует TLS, балансирует запросы (если несколько инстансов), а Docker Secrets защищают учётные данные.

Аутентификация и Rate Limiting

Production MCP-сервер без аутентификации — это открытая дверь к вашим внутренним системам. Каждый тул, который вы выставили, может вызвать любой, кто знает эндпоинт. Если ваш MCP-сервер умеет делать запросы к базе данных — неаутентифицированный сервер позволяет любому делать эти запросы.

Самый простой подход — Bearer token. Middleware проверяет каждый запрос до того, как он попадёт в MCP-обработчик. Реализация на Express/TypeScript:

const API_KEYS=*** Set(
  (process.env.MCP_API_KEYS || "").split(",").filter(Boolean)
);

function authenticate(req, res, next) {
  const auth = req.headers.authorization;
  if (!auth || !auth.startsWith("Bearer ")) {
    return res.status(401).json({
      jsonrpc: "2.0",
      error: { code: -32001, message: "Authentication required" },
      id: null,
    });
  }
  const token = auth.slice(7);
  if (!API_KEYS.has(token)) {
    return res.status(403).json({
      jsonrpc: "2.0",
      error: { code: -32002, message: "Invalid credentials" },
      id: null,
    });
  }
  next();
}

app.post("/mcp", authenticate, async (req, res) => {
  // MCP handling
});

Bearer-токены работают для service-to-service коммуникации. Для пользовательских сценариев, где каждый разработчик аутентифицируется своими credentials, OAuth 2.1 — рекомендованный механизм по спецификации MCP.

Rate Limiting

AI-клиенты ведут себя иначе, чем люди. Одна сессия Claude Code может генерировать десятки вызовов тулов в секунду — особенно во время agentic-воркфлоу, когда агент итеративно решает задачу. Без rate limiter один разработчик с агрессивной сессией может положить ваш бэкенд.

Скользящее окно на ключ API или ID сессии — рабочее решение:

const RATE_LIMIT = 100;   // запросов на окно
const WINDOW_MS = 60000;  // 1 минута

function rateLimit(req, res, next) {
  const token = req.headers.authorization?.slice(7) || "anonymous";
  const now = Date.now();
  let bucket = rateLimits.get(token);
  if (!bucket || now > bucket.resetAt) {
    bucket = { count: 0, resetAt: now + WINDOW_MS };
    rateLimits.set(token, bucket);
  }
  bucket.count++;
  res.setHeader("X-RateLimit-Remaining",
    Math.max(0, RATE_LIMIT - bucket.count));
  if (bucket.count > RATE_LIMIT) {
    return res.status(429).json({
      jsonrpc: "2.0",
      error: { code: -32003, message: "Rate limit exceeded" },
      id: null,
    });
  }
  next();
}

100 запросов в минуту — хорошая стартовая точка для внутренних инструментов. Для дорогих тулов (миграция БД, запуск длинных пайплайнов) стоит добавить per-tool лимиты в дополнение к глобальному.

По материалам гайда SystemPrompt.

Мониторинг и Observability

Production MCP-сервер без мониторинга — сервер, который вы будете отлаживать вслепую в 2 часа ночи. Представьте: тул, который делает запрос к внешнему API, начал периодически таймаутить. Без метрик вы узнаете об этом только когда пользователи скажут, что «Claude тормозит».

Минимальный стек observability состоит из трёх уровней:

СигналMCP-специфичные поляOpenTelemetry конвенция
ЛогиsessionId, имя тула, JSON-RPC метод, статус, длительность, код ошибкиOTel log attributes
Метрикиmcp_tool_calls_total (counter), mcp_tool_call_duration_seconds (histogram), mcp_active_sessions (gauge)OTel RPC metrics + HTTP metrics
ТрейсыОдин span на вызов тула; атрибуты rpc.system=jsonrpc, rpc.method, mcp.session.idOTel RPC spans

Источник: OpenTelemetry Semantic Conventions

Health checks

Два эндпоинта — liveness и readiness — это стандарт для Kubernetes и Docker Compose:

app.get("/health", (req, res) => {
  res.json({
    status: "ok",
    uptime: process.uptime(),
    activeSessions: sessions.size,
  });
});

app.get("/ready", async (req, res) => {
  try {
    await db.query("SELECT 1");
    res.json({ status: "ready" });
  } catch {
    res.status(503)
      .json({ status: "not ready", reason: "db unavailable" });
  }
});

/health говорит оркестратору «перезапусти контейнер», /ready — «не роути трафик на этот инстанс». Сервер может быть жив (liveness OK), но не готов (readiness FAIL), если упала база данных.

Prometheus-метрики

Счётчики вызовов тулов с тегами tool_name и status — базовая метрика «работает ли сервер?». Гистограмма длительности — «не начал ли тул тормозить?». Гейдж активных сессий — «не течёт ли память?»:

const toolCallCounter = new Counter({
  name: "mcp_tool_calls_total",
  help: "Total number of MCP tool calls",
  labelNames: ["tool_name", "status"],
});

const toolCallDuration = new Histogram({
  name: "mcp_tool_call_duration_seconds",
  help: "Duration of MCP tool calls",
  labelNames: ["tool_name"],
});

const activeSessions = new Gauge({
  name: "mcp_active_sessions",
  help: "Currently active MCP sessions",
});

Эти метрики уже не раз ловили реальные проблемы: внезапный рост mcp_tool_call_duration_seconds означал, что внешнее API, к которому ходит тул, начало тормозить; рост mcp_tool_calls_total на одном ключе за минуту с последующими 429 — что rate limiting сработал корректно.

Docker: 5 Best Practices for MCP Servers: управляйте tool budget-ом, пишите документацию для агентов и людей, тестируйте через MCP Inspector, проектируйте тулы для LLM-потребителя, а не для человека.

Масштабирование и продвинутые паттерны

Когда один инстанс MCP-сервера перестаёт справляться с нагрузкой, в дело вступают паттерны масштабирования.

Горизонтальное масштабирование. Streamable HTTP — stateless транспорт. Сессии хранятся в памяти каждого инстанса, но клиент привязан к сессии через Mcp-Session-Id. Если сессия ушла на инстанс A, все последующие запросы этой сессии должны идти на инстанс A. Решение — nginx с ip_hash или sticky sessions:

upstream mcp_backend {
  hash $http_mcp_session_id consistent;
  server 127.0.0.1:3001;
  server 127.0.0.1:3002;
  server 127.0.0.1:3003;
}

server {
  listen 443 ssl;
  location /mcp {
    proxy_pass http://mcp_backend;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
  }
}

Если сессии не критичны (сервер возвращает только stateless данные), можно обойтись round-robin без привязки. Но для тулов с состоянием (например, пошаговые агентские взаимодействия) session affinity необходим.

Kubernetes HPA. Автоматическое масштабирование по метрике mcp_active_sessions или CPU-usage:

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: mcp-server-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: mcp-server
  minReplicas: 2
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70

Docker MCP Gateway. Docker предложил альтернативный подход: один MCP-сервер — Docker — работает как gateway к динамическому набору контейнеризованных тулов. Вместо того чтобы запускать 5 разных MCP-серверов под 5 разных Node/Python версий, вы запускаете один gateway, который через Docker API управляет контейнерами с тулами. Пользователи добавляют/удаляют тулы через UI Docker Desktop, не трогая конфиги. Docker MCP Catalog выступает централизованным хабом для обнаружения и публикации тулов — по аналогии с Docker Hub, но для MCP.

Graceful Shutdown. При выключении MCP-сервер должен завершить активные сессии, сохранить состояние и только потом остановиться. Перехват SIGTERM с таймаутом 30 секунд — стандартная практика:

process.on("SIGTERM", async () => {
  console.error("Shutting down...");
  const shutdownPromises = [];
  for (const [id, transport] of sessions) {
    shutdownPromises.push(transport.close());
  }
  await Promise.all(shutdownPromises);
  server.close(() => process.exit(0));
  setTimeout(() => process.exit(1), 30000); // force exit
});

Что выбрать под свой стек

Выбор архитектуры деплоя MCP-сервера зависит от вашей команды и нагрузки:

СценарийАрхитектураИнструменты
1–2 разработчика, внутренние тулыОдин Docker-контейнер + Bearer tokenDocker Compose, nginx или Caddy
Команда 5–15 человек, несколько туловDocker Compose + nginx + health checksDocker Secrets, Prometheus + Grafana
Продакшен, много клиентов, SLAKubernetes + HPA + Streamable HTTPK8s, cert-manager, Prometheus Operator, OAuth 2.1
Разрозненные тулы из разных экосистемDocker MCP GatewayDocker Desktop, Docker MCP Catalog

Чего не стоит делать

Не пытайтесь передавать STDIO через SSH-туннели. Это технически возможно, но не поддерживает несколько клиентов, не имеет мониторинга и ломается при обрыве соединения.

Не пишите в stdout ничего, кроме протокола. MCP использует stdout как канал протокола в STDIO-режиме. Даже с HTTP-транспортом эта дисциплина предотвращает баги, если придётся поддерживать оба транспорта. Используйте console.error для логов.

Не используйте HTTP+SSE (legacy) для новых проектов. Streamable HTTP — это то же самое, но чище: один эндпоинт вместо двух, встроенная поддержка сессий через заголовок, лучшее согласование версий.

Что ещё важно знать

Можно ли использовать STDIO в production для одного пользователя?

Технически — да, если сервер и клиент на одной машине. Но без мониторинга и health checks вы не узнаете, что сервер упал, пока не перезапустите сессию вручную. Для ответственных задач даже с одним пользователем Streamable HTTP лучше.

Какой язык лучше для MCP-сервера — Python или TypeScript?

TypeScript (через @modelcontextprotocol/sdk) имеет наиболее зрелую поддержку Streamable HTTP транспорта. Python SDK тоже поддерживает его, но с июня 2025 TypeScript-реализация стабильнее для production-сценариев.

Обязательно ли ставить rate limiter для внутреннего сервера?

Да. Даже внутренний сервер может быть положен agentic-воркфлоу, который в цикле дёргает один и тот же медленный тул. Rate limiter на 100–200 запросов/мин — минимальная защита от случайного DoS со стороны своего же агента.

Что делать, если MCP-сервер должен ходить в несколько внешних API?

Самый чистый подход — один MCP-сервер-wrapper, который внутри оркестрирует вызовы к внешним API и возвращает единый интерфейс тулов. Альтернатива — Docker MCP Gateway, который запускает каждый тул в отдельном контейнере.

Как дебажить MCP-сервер в продакшене?

MCP Inspector для локального тестирования, console.error со структурированными JSON-логами в production, и Prometheus-метрики для долгосрочного мониторинга. Все логи должны включать sessionId и requestId для трассировки конкретного вызова.