MCP в продакшене: решение проблем транспорта, аутентификации и масштабирования
Engineering Team
От прототипа к продакшену: что меняется
Создать MCP-сервер, работающий на вашем ноутбуке, несложно. Запустить сервер, обрабатывающий тысячи одновременных сессий AI-агентов в распределённой инфраструктуре — это совсем другая инженерная задача. Продакшен-развёртывания MCP должны решать пять проблем, которые прототипы могут игнорировать: масштабируемость транспорта, аутентификация и авторизация, управление сессиями в масштабе, аудит-трейлы и оркестрация нескольких серверов.
Эта статья — техническое руководство для инженерных команд, переводящих MCP-серверы из разработки в продакшен. Мы предполагаем, что вы уже создали хотя бы один MCP-сервер и понимаете основы протокола. Если нет, начните с нашей сопутствующей статьи о создании первого MCP-сервера.
Масштабируемость транспорта: stdio vs SSE vs Streamable HTTP
MCP определяет три транспортных механизма. Выбор правильного для продакшена — ваше первое архитектурное решение.
Транспорт stdio
Транспорт stdio общается через стандартные потоки ввода/вывода. Хост-приложение запускает MCP-сервер как дочерний процесс и обменивается JSON-RPC сообщениями через stdin/stdout.
Преимущества:
- Нулевая сетевая конфигурация
- Изоляция на уровне процесса
- Отсутствие конфликтов портов
- Минимальная задержка (без сетевого стека)
Ограничения:
- Сервер должен работать на той же машине, что и хост
- Один серверный процесс на клиентскую сессию
- Невозможна балансировка нагрузки
- Невозможно горизонтальное масштабирование
Лучше всего для: локальных инструментов разработки, расширений IDE, однопользовательских десктопных приложений.
Транспорт SSE (Server-Sent Events)
SSE-транспорт использует HTTP для сообщений клиент-сервер и Server-Sent Events для сообщений сервер-клиент. Сервер работает как HTTP-сервис.
Преимущества:
- Сетевой доступ (удалённые серверы)
- Совместимость с существующей HTTP-инфраструктурой
- Поддержка нескольких одновременных клиентов
- Работает через файрволы и прокси
Ограничения:
- Однонаправленная потоковая передача (сервер-клиент только через SSE)
- Требуется привязка сессий (стейтфул-соединения)
- Некоторые балансировщики плохо работают с долгоживущими SSE-соединениями
- Нет встроенной семантики переподключения в протоколе
Лучше всего для: малых и средних развёртываний, внутренних инструментов, команд с существующей HTTP-инфраструктурой.
Транспорт Streamable HTTP
Streamable HTTP — новейший транспорт, разработанный специально для продакшен-развёртываний. Использует стандартный HTTP POST для всех сообщений с опциональным SSE-стримингом для долгих операций.
Преимущества:
- Полностью статлесс модель запрос/ответ
- Работает с любым HTTP-балансировщиком
- Встроенное управление сессиями через заголовок
Mcp-Session-Id - Поддержка потоковых и непотоковых ответов
- Совместимость с CDN и прокси
Ограничения:
- Требуется внешнее хранилище сессий (Redis, база данных)
- Чуть выше оверхед на сообщение, чем у stdio
- Более новый транспорт — меньше инструментов экосистемы
Лучше всего для: продакшен-развёртываний в облаке, мультитенантных платформ, корпоративных сред.
Сравнительная матрица транспортов
| Характеристика | stdio | SSE | Streamable HTTP |
|---|---|---|---|
| Сетевой доступ | Только локальный | Удалённый | Удалённый |
| Балансировка нагрузки | Невозможна | Стики-сессии | Стандартный HTTP LB |
| Горизонтальное масштабирование | Нет | Ограничено | Да |
| Через файрвол | Н/Д | Да | Да |
| Переподключение | Н/Д | Вручную | Встроено |
| Одновременные клиенты | 1 | Много | Много |
| Задержка | Минимальная | Низкая | Низкая |
| Статлесс | Стейтфул | Стейтфул | Возможен статлесс |
| Готовность к продакшену | Только разработка | Средняя | Высокая |
Аутентификация: SSO-интеграция, API-ключи и OAuth
MCP-серверы в продакшене должны аутентифицировать как AI-хост-приложение, так и конечного пользователя, от имени которого AI действует.
OAuth 2.0 для удалённых серверов
Спецификация MCP включает встроенный поток OAuth 2.0 для удалённых (HTTP-based) серверов. Процесс выглядит так:
- MCP-клиент отправляет запрос
initializeбез учётных данных. - Сервер отвечает HTTP 401 с заголовком
WWW-Authenticate, указывающим на OAuth-эндпоинт авторизации. - Хост-приложение открывает браузер для аутентификации пользователя.
- После успешной аутентификации хост получает токен доступа.
- Последующие MCP-запросы включают токен в заголовке
Authorization: Bearer.
// Серверный OAuth middleware для Axum/Express
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import express from "express";
const app = express();
app.use("/mcp", async (req, res, next) => {
const token = req.headers.authorization?.replace("Bearer ", "");
if (!token) {
res.status(401).json({
error: "unauthorized",
oauth_url: "https://auth.example.com/oauth/authorize",
});
return;
}
// Валидация токена через провайдер аутентификации
const user = await validateToken(token);
if (!user) {
res.status(403).json({ error: "invalid_token" });
return;
}
// Прикрепление контекста пользователя для авторизации инструментов
req.mcpUser = user;
next();
});
Аутентификация через API-ключи
Для межсервисного MCP-взаимодействия (без участия человека) API-ключи проще:
const API_KEYS = new Map([
["sk-prod-abc123", { name: "analytics-service", scopes: ["read"] }],
["sk-prod-def456", { name: "admin-service", scopes: ["read", "write"] }],
]);
function authenticateApiKey(key: string) {
return API_KEYS.get(key) || null;
}
Паттерн SSO-интеграции
Для корпоративных развёртываний — интеграция с существующим SSO (SAML, OIDC):
Пользователь -> AI-хост -> MCP-клиент -> MCP-сервер -> SSO-провайдер
|
v
Валидация OIDC-токена
Извлечение ролей
Применение RBAC на уровне инструментов
Каждый MCP-инструмент может проверять роли аутентифицированного пользователя перед выполнением:
server.tool(
"delete_record",
"Удалить запись из базы данных по ID",
{ table: z.string(), id: z.string() },
async ({ table, id }, { authContext }) => {
if (!authContext.roles.includes("admin")) {
return {
content: [{ type: "text", text: "Запрещено: требуется роль admin" }],
isError: true,
};
}
// Выполнение удаления
}
);
Масштабирование: стейтфул-сессии vs балансировщики нагрузки
MCP-сессии по своей природе стейтфулны: хендшейк initialize согласовывает возможности, и сервер может хранить контекст между вызовами инструментов в рамках сессии. Это создаёт противоречие с горизонтальным масштабированием.
Архитектура хранилища сессий
Вынесите состояние сессий из серверного процесса во внешнее хранилище:
interface McpSession {
id: string;
userId: string;
capabilities: ServerCapabilities;
createdAt: Date;
lastActivityAt: Date;
metadata: Record<string, unknown>;
}
class RedisSessionStore {
constructor(private redis: Redis) {}
async create(session: McpSession): Promise<void> {
await this.redis.set(
`mcp:session:${session.id}`,
JSON.stringify(session),
"EX",
3600 // TTL 1 час
);
}
async get(sessionId: string): Promise<McpSession | null> {
const data = await this.redis.get(`mcp:session:${sessionId}`);
return data ? JSON.parse(data) : null;
}
async touch(sessionId: string): Promise<void> {
await this.redis.expire(`mcp:session:${sessionId}`, 3600);
}
}
Горизонтальное масштабирование со Streamable HTTP
С вынесенными сессиями и транспортом Streamable HTTP можно запускать несколько экземпляров MCP-сервера за стандартным балансировщиком:
┌──────────────┐
│ Балансировщик│
MCP-клиент ──>│ нагрузки │
│ (nginx/ALB) │
└──────┬───────┘
│
┌────────────┼────────────┐
v v v
┌──────────┐ ┌──────────┐ ┌──────────┐
│ MCP │ │ MCP │ │ MCP │
│ Сервер 1 │ │ Сервер 2 │ │ Сервер 3 │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
v v v
┌────────────────────────────────────┐
│ Redis (сессии) │
└────────────────────────────────────┘
Стики-маршрутизация не требуется. Любой экземпляр сервера может обработать любой запрос, загрузив сессию из Redis.
Конфигурация автомасштабирования
Пример Kubernetes HPA для MCP-серверов:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: mcp-server
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: mcp-server
minReplicas: 2
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Pods
pods:
metric:
name: mcp_active_sessions
target:
type: AverageValue
averageValue: "100"
Аудит-трейлы и логирование
Каждый вызов MCP-инструмента в продакшене должен логироваться для безопасности, комплаенса и отладки.
Схема структурированного логирования
interface McpAuditLog {
timestamp: string;
sessionId: string;
userId: string;
toolName: string;
toolInput: Record<string, unknown>;
toolOutput: string;
durationMs: number;
success: boolean;
errorMessage?: string;
ipAddress: string;
userAgent: string;
}
// Middleware, оборачивающий каждый вызов инструмента
function auditMiddleware(server: McpServer, logger: Logger) {
const originalTool = server.tool.bind(server);
server.tool = (name, description, schema, handler) => {
originalTool(name, description, schema, async (args, context) => {
const start = Date.now();
try {
const result = await handler(args, context);
logger.info({
event: "mcp_tool_call",
tool: name,
input: args,
durationMs: Date.now() - start,
success: !result.isError,
sessionId: context.sessionId,
userId: context.authContext?.userId,
});
return result;
} catch (error) {
logger.error({
event: "mcp_tool_error",
tool: name,
input: args,
error: (error as Error).message,
durationMs: Date.now() - start,
sessionId: context.sessionId,
});
throw error;
}
});
};
}
Требования комплаенса
| Требование | Реализация |
|---|---|
| Доступ к данным GDPR | Логирование какие данные пользователей AI получил через MCP |
| Аудит-трейл SOC 2 | Неизменяемые логи всех вызовов инструментов с метками времени |
| Защита PII | Редактирование чувствительных полей во входных данных перед логированием |
| Хранение данных | Установка TTL логов в соответствии с требованиями комплаенса |
| Ревью доступа | Логирование событий аутентификации и проверок разрешений |
Паттерны шлюзов для многосерверных развёртываний
Корпоративные среды часто нуждаются в десятках MCP-серверов — по одному для каждой внутренней системы. MCP-шлюз централизует управление:
┌──────────────┐
│ MCP-шлюз │
MCP-клиент ──────>│ │
│ - Auth │
│ - Роутинг │
│ - Лимиты │
│ - Аудит │
└──────┬───────┘
│
┌──────────────────┼──────────────────┐
v v v
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Jira MCP │ │ Slack MCP│ │ DB MCP │
│ Сервер │ │ Сервер │ │ Сервер │
└──────────┘ └──────────┘ └──────────┘
Шлюз обеспечивает:
- Единую аутентификацию: один поток аутентификации для всех бэкенд-серверов.
- Маршрутизацию пространств имён инструментов: инструменты имеют префикс имени сервера (
jira.create_issue,slack.send_message). - Централизованное ограничение частоты: лимиты по пользователю, инструменту и серверу.
- Маршрутизацию запросов: направление вызовов инструментов к соответствующему бэкенд-серверу.
- Circuit breaking: если бэкенд-сервер недоступен, шлюз возвращает грейсфул-ошибки вместо зависания.
Реализация базового шлюза
class McpGateway {
private backends = new Map<string, McpClient>();
async registerBackend(prefix: string, url: string) {
const client = new McpClient({ name: `gateway-${prefix}` });
const transport = new StreamableHTTPClientTransport(new URL(url));
await client.connect(transport);
this.backends.set(prefix, client);
}
async routeToolCall(
toolName: string,
args: Record<string, unknown>
) {
// формат toolName: "prefix.actualTool"
const [prefix, ...rest] = toolName.split(".");
const actualTool = rest.join(".");
const backend = this.backends.get(prefix);
if (!backend) {
throw new Error(`Неизвестный бэкенд: ${prefix}`);
}
return backend.callTool({ name: actualTool, arguments: args });
}
}
FAQ
В: Какой транспорт использовать в продакшене — stdio или HTTP? О: Используйте Streamable HTTP для любого развёртывания, которое должно масштабироваться за пределы одной машины. Используйте stdio только для локальных десктопных интеграций, где AI-хост и MCP-сервер работают на одной машине.
В: Сколько одновременных сессий может обрабатывать один MCP-сервер? О: С транспортом Streamable HTTP и внешним хранилищем сессий один экземпляр Node.js-сервера обычно обрабатывает 500–1000 одновременных сессий. На серверах Rust или Go достижимо 5000–10 000. Масштабируйте горизонтально за этими пределами.
В: Как обрабатывать простой MCP-сервера?
О: Реализуйте health-проверки (эндпоинт /health), используйте liveness/readiness пробы Kubernetes и настройте шлюз с circuit breaker’ами. MCP-клиент получит ошибки соединения, которые AI-хост может показать как «инструмент временно недоступен».
В: Могут ли MCP-серверы вызывать другие MCP-серверы? О: Да. MCP-сервер также может быть MCP-клиентом, создавая цепочку. Это основа межагентной коммуникации. Используйте этот паттерн осторожно — глубокие цепочки увеличивают задержку и вероятность отказа.
В: Как версионировать API MCP-сервера?
О: Используйте поле версии сервера в ответе initialize. Для ломающих изменений (удаление инструментов, изменение схем) поднимайте мажорную версию и поддерживайте обе версии в период миграции. Версия самого протокола MCP согласовывается при инициализации.
В: Каков максимальный размер MCP-сообщения? О: Протокол не определяет максимума, но практические ограничения зависят от транспорта. Для Streamable HTTP держите ответы менее 10 МБ. Для stdio системные буферы пайпов (обычно 64 КБ) могут потребовать чанкинга для больших ответов.