Деплой MCP-серверов, Docker, SSE-транспорт, масштабирование и мониторинг.
Деплой MCP-сервера в продакшен требует больше, чем python server.py. В этой теме разберём контейнеризацию, SSE-транспорт, reverse proxy, масштабирование и мониторинг.
Цель деплоя: сервер доступен, надёжен, безопасен и наблюдаем.
Контейнер — стандартный способ упаковки MCP-сервера. Все зависимости включены, сервер работает одинаково на любом хосте.
FROM python:3.12-slim
WORKDIR /app
# Установка зависимостей
COPY pyproject.toml poetry.lock ./
RUN pip install --no-cache-dir fastmcp pydantic aiohttp requests
# Копирование кода сервера
COPY server.py .
# Порт для SSE-транспорта
EXPOSE 8000
# Запуск сервера
CMD ["python", "server.py"]Сервер внутри контейнера запускается в SSE-режиме:
# server.py
from fastmcp import FastMCP
import logging
logging.basicConfig(level=logging.INFO)
mcp = FastMCP("ProductionServer")
@mcp.tool()
def get_metrics() -> str:
"""Возвращает метрики системы."""
return '{"cpu": 42.5, "memory": 67.3, "disk": 55.0}'
if __name__ == "__main__":
mcp.run(transport="sse", host="0.0.0.0", port=8000)Запуск контейнера:
docker build -t mcp-server .
docker run -d -p 8000:8000 --name mcp-instance mcp-serverФлаг -d запускает контейнер в фоне, -p 8000:8000 пробрасывает порт на хост.
Docker Compose упрощает запуск сервера с зависимостями (БД, Redis):
version: "3.8"
services:
mcp-server:
build: .
ports:
- "8000:8000"
environment:
- DATABASE_URL=postgresql://user:pass@db:5432/mcp
- REDIS_URL=redis://redis:6379/0
depends_on:
- db
- redis
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: mcp
volumes:
- pgdata:/var/lib/postgresql/data
redis:
image: redis:7-alpine
volumes:
pgdata:docker compose up -dВсе сервисы запускаются одной командой. MCP-сервер подключается к БД и Redis по внутренним именам сервисов (db, redis).
В продакшене перед MCP-сервером ставят reverse proxy (nginx, Caddy). Proxy берёт на себя HTTPS, rate limiting, логирование.
server {
listen 443 ssl;
server_name mcp.example.com;
ssl_certificate /etc/letsencrypt/live/mcp.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mcp.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
# SSE требует специальных заголовков
proxy_set_header Connection '';
proxy_buffering off;
proxy_cache off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}Ключевые настройки для SSE:
proxy_buffering off — отключает буферизацию, SSE-события доставляются немедленноproxy_cache off — отключает кэширование, каждое соединение индивидуальноproxy_set_header Connection '' — поддерживает постоянное соединениеCaddy проще — автоматически получает HTTPS-сертификаты:
mcp.example.com {
reverse_proxy 127.0.0.1:8000
}
Caddy автоматически терминализирует HTTPS и проксирует запросы к MCP-серверу.
Добавьте эндпоинт проверки здоровья для мониторинга:
from fastmcp import FastMCP
from fastmcp.server.file_route import file_route
import uvicorn
mcp = FastMCP("ProductionServer")
# MCP-инструменты
@mcp.tool()
def get_status() -> str:
"""Возвращает статус сервера."""
return '{"status": "ok"}'
# HTTP health check (отдельный от MCP)
@file_route("/health")
async def health_check():
"""HTTP эндпоинт для проверки здоровья."""
from starlette.responses import JSONResponse
return JSONResponse({"status": "ok", "version": "1.0.0"})
if __name__ == "__main__":
# Для production используем uvicorn напрямую
config = uvicorn.Config(
"server:mcp.app",
host="0.0.0.0",
port=8000,
log_level="info"
)
server = uvicorn.Server(config)
server.run()Health check позволяет orchestrator (Kubernetes, Docker) проверить, жив ли сервер:
curl https://mcp.example.com/health
# {"status": "ok", "version": "1.0.0"}Структурированное логирование для продакшена:
import logging
import json
class JSONFormatter(logging.Formatter):
"""Форматирует логи в JSON для сбора в ELK/Datadog."""
def format(self, record):
log_entry = {
"timestamp": self.formatTime(record),
"level": record.levelname,
"message": record.getMessage(),
"module": record.module,
"function": record.funcName,
"line": record.lineno,
}
if record.exc_info:
log_entry["exception"] = self.formatException(record.exc_info)
return json.dumps(log_entry, ensure_ascii=False)
handler = logging.StreamHandler()
handler.setFormatter(JSONFormatter())
logger = logging.getLogger()
logger.addHandler(handler)
logger.setLevel(logging.INFO)Каждый лог — JSON-объект, который легко парсится системами мониторинга (ELK Stack, Datadog, Grafana Loki).
Один MCP-сервер может обслуживать нескольких клиентов с изоляцией данных:
from fastmcp import FastMCP
from fastmcp.server.context import Context
mcp = FastMCP("MultiTenantServer")
@mcp.tool()
async def get_dashboard(ctx: Context) -> str:
"""Возвращает дашборд для текущего клиента."""
# В реальности: извлекаем tenant_id из контекста сессии
tenant_id = extract_tenant_from_context(ctx)
data = get_tenant_data(tenant_id)
return json.dumps(data, indent=2, ensure_ascii=False)
@mcp.resource("config://tenant")
async def get_tenant_config(ctx: Context) -> str:
"""Конфигурация текущего клиента."""
tenant_id = extract_tenant_from_context(ctx)
config = load_tenant_config(tenant_id)
return json.dumps(config, indent=2)Каждый клиент получает только свои данные. Сервер один — данные изолированы.
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.