Когда MCP-серверов стало шесть, а коллег — десять, фраза «просто запусти npx локально» перестала работать. Не у всех есть желания ставить Node.js, у менеджеров нет Docker, а локальный claude_desktop_config.json начинает напоминать хранилище секретов от всех систем.

Я прошёл путь от remote MCP → локальный запуск → Docker → Kubernetes с единым Helm-чартом и JWT-аутентификацией через Envoy. Расскажу, на что напоролся, что получилось, и что еще предстоит решить.

Уровень 1: Remote MCP — когда вендор постарался за тебя

Первое знакомство с MCP было максимально простым. Я добавил atlassian-mcp-server в Claude как remote mcp, прошел аутентификацию и мог им пользоваться. Все выглядело достаточно просто и доступно, Atlassian сам предоставлял возможность подключения и аутентификации.

{
  "mcpServers": {
    "atlassian": {
      "type": "http",
      "url": "https://mcp.atlassian.com/v1/sse"
    }
  }
}

Уровень 2: Локальный запуск — первые компромиссы

Дальше я захотел связать свою IDE с Kubernetes. Здесь уже дело оказалось сложнее, потому, что Kubernetes не предоставляет доступ через MCP из коробки. Тут уже пришлось ставить зависимости:

{
  "mcpServers": {
    "kubernetes": {
      "command": "npx",
      "args": ["-y", "kubernetes-mcp-server@latest"]
    }
  }
}

Заработало, но для одного сервера нужен Node.js, для другого — Python и uvx, для третьего — Go-бинарник. Зоопарк рантаймов на рабочей машине множится с каждым новым MCP. Совсем не хочется забивать этим все рабочий компьютер, тем более, что я даже не разработчик)

Уровень 3: Docker — изоляция без боли

Логичный следующий шаг — контейнеры. Каждый MCP-сервер со своим рантаймом, без мусора на хосте:

{
  "mcpServers": {
    "grafana": {
      "command": "docker",
      "args": [
        "run", "--rm", "-i",
        "-e", "GRAFANA_URL",
        "-e", "GRAFANA_SERVICE_ACCOUNT_TOKEN",
        "grafana/mcp-grafana",
        "-t", "stdio"
      ],
      "env": {
        "GRAFANA_URL": "https://grafana.example.com",
        "GRAFANA_SERVICE_ACCOUNT_TOKEN": "<token>"
      }
    }
  }
}

На этом можно было бы и остановится. Для SaaS решений использовать их remote-mcp. Для self-hosted решений, запускаемые локально в контейнере. Для одного инженера на одной машине — достаточно. Но когда это нужно десяти людям, возникают вопросы:

  • Токены от продакшн-систем разбросаны по ноутбукам.

  • Автоматизированным workflow (n8n, CI/CD) тоже нужен доступ к MCP — а они работают удалённо.

  • Менеджеры и аналитики хотят использовать AI-инструменты, но не готовы разбираться с docker run.

Всё это привело к одному выводу: MCP-серверы надо выносить в общую инфраструктуру.

Уровень 4: Kubernetes — централизованный деплой

Изначально идея была простой: развернуть удаленные MCP сервера во внутреннем контуре вашей инфраструктуры. Здесь мы как минимум можем ограничить доступ через корпоративный VPN. 

Все кто занимался подобной задачей сталкивались с тем, как организовать доступ к серверу принимающему данные через stdin удаленно. Например mcp-digitalocean

Здесь на помощь приходят решения вроде MCP Proxy, которые могут наладить коммуникацию между HTTP и stdin. Так как MCP сервера в основном однотипные stateless (если отключить нотификации об изменении инструментов и промптов) сервисы их удобно запускать в Kubernetes.

Схема выглядит так: клиент (Claude Desktop, IDE, n8n) обращается по HTTPS к API Gateway. Gateway проксирует запрос в Kubernetes Service. В поде рядом с MCP-сервером крутится MCP Gateway — принимает HTTP и передаёт в stdin процесса.

Универсальный Helm-чарт

Чтобы не писать манифесты под каждый MCP-сервер, я сделал универсальный Helm-чарт: mcp-helm-chart на ArtifactHub.

Что он умеет:

  • mode: proxy — запускает MCP Gateway sidecar-контейнером рядом с MCP-сервером, транслирует HTTP ↔ stdio.

  • mode: native — для серверов, которые уже поддерживают HTTP (без sidecar).

  • Интеграция с Hashicorp Vault и ExternalSecrets для секретов.

  • Поддержка Gateway API и классического Ingress.

  • HPA для горизонтального масштабирования.

Установка mcp-digitalocean c Ingress-nginx без аутентификации

helm repo add mcp https://javdet.github.io/mcp-helm-chart
helm install my-mcp mcp/mcp -f values.yaml

В values.yaml из важного указать

---
mode: proxy

proxy:
 image:
   repository: node
   tag: "20-bookworm"
   pullPolicy: IfNotPresent
 gateway:
   package: "@michlyn/mcpgateway"
   stdioCommand: "npx -y @digitalocean/mcp --services apps,droplets,doks,networking"
   outputTransport: streamable-http
   port: 8080
   httpPath: /mcp


# Токен можно хранить в Hashicorp Vault и получать через Vault Webhook
vault:
 enabled: true
 role: "mcp"
 path: "kubernetes_dev-fra1-01"

env:
 - name: DIGITALOCEAN_API_TOKEN
   value: vault:devops/data/ai/mcp/digitalocean#token

ingress:
 enabled: true
 className: "internal"
 annotations:
   nginx.ingress.kubernetes.io/proxy-buffering: "off"
   nginx.ingress.kubernetes.io/proxy-http-version: "1.1"
   nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
   nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
   nginx.ingress.kubernetes.io/use-regex: "true"
   nginx.ingress.kubernetes.io/rewrite-target: /$2
 hosts:
   - host: aitool.example.com
     paths:
       - path: /digitalocean(/|$)(.*)
         pathType: ImplementationSpecific
 tls:
   - secretName: ssl-certificate
     hosts:
       - aitool.example.com

Инсталляция будет выглядеть следующим образом

MCP-серверы в режиме Streamable HTTP — stateless. Горизонтально масштабируются штатным HPA без каких-либо проблем. Самым насущным вопросом тут остается аутентификация, а еще лучше авторизация. Сами MCP сервера не всегда поддерживают входящую аутентификацию, значит нужно решать это самостоятельно.

Аутентификация: JWT через Envoy

Basic-аутентификация немногим лучше, чем ничего, поэтому сразу JWT. Я использовал Envoy API Gateway — он нативно поддерживает JWT-валидацию и уже был в нашем стеке.

Генерация ключей и токена

# 1. Генерируем RSA-ключи
openssl genrsa -out mcp-jwt-private.pem 4096
openssl rsa -in mcp-jwt-private.pem -pubout -out mcp-jwt-public.pem

# 2. Генерируем Key ID
KID=$(openssl rand -hex 16)

# 3. Формируем JWT header (base64url)
HEADER=$(echo -n "{\"alg\":\"RS256\",\"typ\":\"JWT\",\"kid\":\"${KID}\"}" \
  | base64 -w0 | tr '+/' '-_' | tr -d '=')

# 4. Формируем JWT payload (срок — 1 год)
PAYLOAD=$(echo -n "{\"sub\":\"claude-desktop\",\"aud\":\"mcp-servers\",\"iss\":\"https://your-domain.com\",\"iat\":$(date +%s),\"exp\":$(( $(date +%s) + 31536000 ))}" \
  | base64 -w0 | tr '+/' '-_' | tr -d '=')

# 5. Подписываем
SIGNATURE=$(echo -n "${HEADER}.${PAYLOAD}" \
  | openssl dgst -sha256 -sign mcp-jwt-private.pem \
  | base64 -w0 | tr '+/' '-_' | tr -d '=')

# 6. Финальный токен
echo "${HEADER}.${PAYLOAD}.${SIGNATURE}"

Публичный ключ упаковывается в JWKS и кладётся в ConfigMap. Envoy валидирует каждый входящий запрос, проверяя issuer, audience и подпись.

Конфигурация аутентификации в values чарта (вариант с Gateway API):

gatewayApi:
  enabled: true
  parentRefs:
    - name: internal     # Тут уже должен быть создан Gateway с именем internal
      namespace: ai-infra
      sectionName: https
  hostnames:
    - mcptools.example.com
  timeouts:
    request: "3600s"
    backendRequest: "3600s"
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /digitalocean
      filters:
        - type: URLRewrite
          urlRewrite:
            path:
              type: ReplacePrefixMatch
              replacePrefixMatch: /
  auth:
    type: jwt
    jwt:
      providers:
        - name: mcp-jwt-auth
          issuer: mcp-issuser
          audiences:
            - mcptools.example.com
          localJWKS:
            type: ValueRef
            valueRef:
              group: ""
              kind: ConfigMap
              name: jwks-config

# Если пользуетесь External Secret Operator секреты можно получать через него
externalSecrets:
  enabled: true
  refreshInterval: 1h
  secretStoreRef:
    name: aws
    kind: ClusterSecretStore
  target:
    creationPolicy: Owner
  dataFrom:
    - extract:
        key: infra/mcp/digitalocean

Сейчас доступ к целевым системам (DigitalOcean, Grafana, Kubernetes) осуществляется через единый сервисный аккаунт. Для read-only задач этого хватает: мониторинг, диагностика, получение информации. Для write-операций — вопрос открытый.

Автоматизированный доступ

Периодические задачи (n8n-воркфлоу, CI/CD-пайплайны) подключаются к тем же MCP-серверам по Streamable HTTP с отдельными сервисными JWT-токенами. Схема идентична — отличается только subject в payload токена и, при необходимости, scope доступа на уровне Gateway.

Итого

MCP инструментам и инфраструктуре еще предстоит сделать несколько шагов на встречу друг к другу, чтобы использование стало по настоящему простым, надежным и безопасным.

Текущая схема работает: шесть MCP-серверов в Kubernetes, единый Helm-чарт, JWT-аутентификация через Envoy, секреты в Vault. Коллеги подключаются к remote MCP-серверам без локальных зависимостей, автоматизация использует те же эндпоинты.

Чего пока не хватает:

  • Per-user авторизация. Протокол MCP не предусматривает передачу контекста пользователя. Пока живём с сервисными аккаунтами.

  • Audit log. Кто какой инструмент вызвал и с какими параметрами — пока не логируется на уровне MCP. Можно собирать на уровне Envoy, но без контекста вызова.

  • Стандарт аутентификации. Каждый вендор делает по-своему. OAuth, API Key, Bearer — единого подхода нет. Но в некоторых серверах уже появляется сквозная аутентификация, это радует.

Как вы решаете per-user авторизацию для MCP? Мы пока живём с единым сервисным аккаунтом — буду рад услышать, кто продвинулся дальше.


Ранее писал о разных интересных использования таких инструментов

Про Алерт ассистента

Про CI/СD ассистента