MAATRIX / Блог / Как поднять MCP-сервер на VPS

Как поднять MCP-сервер на VPS

Как поднять MCP-сервер на VPS

MAATRIX

На ноутбуке MCP-сервер запускается одной строкой в конфиге клиента и работает. Стоит подключить второго человека, веб-клиент или CI — и «поднять на VPS» оказывается другой задачей: другой транспорт, прокси, умеющий держать поток, и открытый в интернет эндпоинт с доступом к вашим данным. Разберём установку MCP-сервера на VPS целиком — от транспорта до заголовков, на которых спотыкается половина клиентов.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →

Что вы на самом деле поднимаете: stdio против сетевого транспорта

MCP (Model Context Protocol) описывает доступ модели к инструментам, ресурсам и промптам поверх JSON-RPC; транспорт под ним и определяет, можно ли сервер «поднять» в принципе.

stdio. Клиент сам запускает процесс и говорит с ним через ввод-вывод: ни порта, ни сети, ни авторизации — снаружи процесса просто не существует. "command": "npx" или "uvx" в конфиге — это stdio, и на VPS его напрямую не выставить: выставлять нечего.

Streamable HTTP. Сервер слушает порт и обслуживает запросы одним эндпоинтом (по умолчанию /mcp), отвечая JSON либо потоком text/event-stream, если инструмент думает долго. Предшественник — HTTP+SSE из ревизии 2024-11-05, пара эндпоинтов GET /sse и POST /messages; такие клиенты ещё встречаются, новый сервер под них писать не надо.

ТранспортПроцессПортАвторизацияГодится для VPS
stdioу клиентанетне нужнатолько через обёртку
HTTP+SSE (2024-11-05)на сервереестьопциональнонаследие
Streamable HTTPна сервереестьOAuth 2.1 / bearerда

Вынос на VPS даёт один адрес вместо конфига на каждой машине и общие секреты вместо токена в четырёх ноутбуках. Плата честная: сетевой MCP-сервер — публичный API с правом дёргать ваши инструменты, а не stdio, защищённый лишь тем, что его никто не видит: HTTP-эндпоинт сканеры находят за считаные часы.

Версия протокола: спецификация 2026-07-28 и почему деплой стал проще

Ревизии MCP нумеруются датами: от версии зависят обязательные заголовки. К августу 2026 в обороте 2024-11-05, 2025-03-26, 2025-06-18, 2025-11-25 и свежая 2026-07-28.

Последняя — крупнейшая переработка с момента запуска, и новость хорошая: протокол стал stateless. Убрали рукопожатие initialize/notifications/initialized и заголовок Mcp-Session-Id — раньше сессия была привязана к конкретному экземпляру, и «два контейнера за nginx» требовали ip_hash или общего хранилища. Теперь версия протокола и возможности клиента едут в _meta каждого запроса (io.modelcontextprotocol/protocolVersion, io.modelcontextprotocol/clientCapabilities, io.modelcontextprotocol/clientInfo), и запрос приземляется на любой экземпляр обычным round-robin.

Что ещё поменялось: заголовки Mcp-Method и Mcp-Name (SEP-2243) — маршрутизация без разбора тела JSON-RPC, расхождение заголовка с телом сервер отклоняет как HeaderMismatchError; GET-поток и resources/subscribe заменили на subscriptions/listen, long-lived POST с явной подпиской на типы уведомлений; RPC server/discover отдаёт версии, возможности и идентичность до первого запроса; несовпадение версий — UnsupportedProtocolVersionError (-32022).

Python-пакет mcp вышел веткой 2.x: понимает 2026-07-28 и все прежние ревизии, pip install mcp теперь ставит именно её, а 1.x остаётся только с багфиксами. Но спека убежала вперёд клиентов: часть инструментов ещё говорит по 2025-06-18 или по старой SSE-схеме, так что совместимость со старыми ревизиями выключать рано.

Развернуть за пару минут

Готовый образ на VPS MAATRIX: NVMe, AMD EPYC, root-доступ. Локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Развернуть Portainer

Установка MCP-сервера на VPS: подготовка и первый запуск

Берём чистый VPS с Ubuntu 24.04 LTS или Debian 13, ставим Docker: curl -fsSL https://get.docker.com | sh, systemctl enable --now docker. docker --version && docker compose version в августе 2026 показывает Docker version 29.7.2 и Docker Compose version v2.x. Дальше — каталог и пользователь без оболочки, сервер не должен работать от root:

useradd -r -s /usr/sbin/nologin -d /opt/mcp mcp
mkdir -p /opt/mcp/app && chown -R mcp:mcp /opt/mcp
curl -LsSf https://astral.sh/uv/install.sh | sh

Минимальный сервер на FastMCP из официального Python SDK — /opt/mcp/app/server.py:

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("maatrix-demo", host="0.0.0.0", port=8000)

@mcp.tool()
def whoami(name: str) -> str:
    """Возвращает приветствие. Минимальный инструмент для проверки связи."""
    return f"hello, {name}"

if __name__ == "__main__":
    mcp.run(transport="streamable-http")

host="0.0.0.0" — процесс поедет в контейнер, наружу порт всё равно не отдадим. Эндпоинт по умолчанию — /mcp на 8000. Рядом — /opt/mcp/app/Dockerfile, без него compose.yaml собирать нечего:

FROM python:3.12-slim
RUN pip install --no-cache-dir "mcp[cli]>=2.0"
WORKDIR /app
COPY server.py .
CMD ["python", "server.py"]

Дальше /opt/mcp/compose.yaml:

services:
  mcp:
    build: ./app
    container_name: mcp-demo
    restart: unless-stopped
    ports:
      - "127.0.0.1:8000:8000"
    env_file: /opt/mcp/mcp.env
    read_only: true
    tmpfs:
      - /tmp
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

Ключевая строка — 127.0.0.1:8000:8000: голый 8000:8000 открыл бы порт всему интернету, а ufw этому не помешает (почему — в шестом разделе). Секреты — в /opt/mcp/mcp.env с chmod 600, не в compose.yaml, который завтра уедет в git. Поднимаем и проверяем руками, до клиентов:

docker compose -f /opt/mcp/compose.yaml up -d
curl -i -X POST http://127.0.0.1:8000/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H 'MCP-Protocol-Version: 2026-07-28' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'

В ответе — HTTP/1.1 200 OK, content-type: text/event-stream и строка data: {"jsonrpc":"2.0","id":1,"result":{"tools":[...; пришло другое — коды разобраны в пятом разделе. Живая проверка, она же годится для CI, — официальный инспектор:

npx @modelcontextprotocol/inspector --cli http://127.0.0.1:8000/mcp --method tools/list
npx @modelcontextprotocol/inspector --tui http://127.0.0.1:8000/mcp

Клиент подключается, когда перед сервером уже встал HTTPS: claude mcp add --transport http maatrix https://mcp.example.com/mcp --header "Authorization: Bearer ...".

Как поднять stdio-сервер, который писали не вы

Большинство готовых серверов из реестра — stdio: npx -y @modelcontextprotocol/server-filesystem /data, uvx mcp-server-git, десятки коннекторов к трекерам и базам. Переписывать не нужно, нужна сетевая обёртка: Supergateway поднимает stdio-сервер дочерним процессом и отдаёт наружу по SSE, WebSocket или Streamable HTTP:

docker run -d --restart unless-stopped --name mcp-fs \
  -p 127.0.0.1:8001:8000 -v /srv/data:/data:ro \
  supercorp/supergateway \
  --stdio "npx -y @modelcontextprotocol/server-filesystem /data" \
  --outputTransport streamableHttp

Каждый обёрнутый сервер — свой сервис в compose со своим портом (8001 — файлы, 8002 — git), nginx разводит их по /mcp/fs, /mcp/git. Альтернатива — mcp-proxy, тот же мост. Три грабли на вечер:

  • Сервер печатает диагностику в stdout. Для stdio это канал JSON-RPC: любой print() ломает поток, клиент падает с ошибкой парсинга раньше списка инструментов. Логи — только в stderr.
  • Переменные нужны контейнеру, а не вам. GITHUB_TOKEN из ssh-сессии до процесса в контейнере не доедет. Кладите в env_file, проверяйте: docker exec mcp-fs sh -c 'echo "[$GITHUB_TOKEN]"' — скобки заодно покажут лишние кавычки.
  • Апстрим падает молча. Шлюз жив и отвечает пустым списком инструментов вместо ошибки; healthcheck вешайте на tools/list, не на TCP-порт.

Честное ограничение: обёртка не даёт ни авторизации, ни мультиарендности — один процесс, один набор секретов на всех, кто дотянулся до порта. Нужны разные права разным людям — отдельный контейнер с отдельным токеном на каждого либо сервер, который сам умеет OAuth.

Реверс-прокси: три кода ответа, на которых ловятся все

MCP по Streamable HTTP — не обычный REST, и типовой конфиг nginx его ломает. Коды, которые вы увидите:

406 Not Acceptable. Дословный ответ спека-совместимого сервера:

HTTP/1.1 406 Not Acceptable
Not Acceptable: Client must accept both application/json and text/event-stream

Клиент прислал Accept: application/json без второго типа — баг клиента, чинить его вам обычно нечем, а костыль с дописыванием заголовка на прокси работает.

421 Misdirected Request с телом Invalid Host header. Защита от DNS rebinding в Python SDK сверяет HostOrigin, если есть) со списком разрешённых; по умолчанию список пуст — проходят только localhost и 127.0.0.1. Встаёт прокси с настоящим доменом — запрос отлетает, а подлость в том, что 421 приходит простым текстом, не JSON-RPC-ошибкой: клиент видит невнятную «транспортную ошибку», а нежеланный хост — только в логах сервера. Лечится в коде, не в прокси:

from mcp.server.fastmcp import FastMCP
from mcp.server.transport_security import TransportSecuritySettings

mcp = FastMCP(
    "maatrix-demo",
    host="0.0.0.0", port=8000,
    transport_security=TransportSecuritySettings(
        enable_dns_rebinding_protection=True,
        allowed_hosts=["mcp.example.com", "localhost:*", "127.0.0.1:*"],
        allowed_origins=["https://mcp.example.com"],
    ),
)

403 Forbidden на неверный Origin — требование спецификации: сервер обязан отвечать 403, если заголовок есть и не проходит проверку. Не отключайте её целиком «чтобы заработало»: в июне 2026 на точно такой же непровалидированной DNS-подмене поймал CVE-2026-11624 (CWE-346) сервер Google MCP Toolbox for Databases — до фикса в 0.25.0 Origin там не проверялся вовсе.

Рабочий блок для nginx:

location /mcp {
    proxy_pass http://127.0.0.1:8000;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Connection "";
    proxy_set_header Accept "application/json, text/event-stream";
    proxy_buffering off;
    proxy_cache off;
    chunked_transfer_encoding off;
    proxy_read_timeout 3600s;
    proxy_send_timeout 3600s;
}

proxy_buffering off обязателен: иначе nginx копит поток и отдаёт его одним куском в конце. Пустой Connection с proxy_http_version 1.1 не даёт закрыть keep-alive, а proxy_read_timeout по умолчанию 60 секунд — инструмент думает дольше минуты и обрывается на середине. Заголовок с подчёркиванием вроде x_api_key nginx молча выбрасывает без underscores_in_headers on;. Caddy решает то же короче, автоматическим TLS и flush_interval -1; выбор под свой стек — в статье Caddy или Nginx.

Кто имеет право дёргать ваши инструменты

Открытый MCP-эндпоинт — не «утечка когда-нибудь», а чужой доступ к тому, что умеют ваши инструменты: читать файлы, ходить в базу, писать в трекер.

Порт не публикуем. Docker пишет правила прямо в iptables (цепочки DOCKER и DOCKER-USER), они обрабатываются раньше ufw — контейнер с -p 8000:8000 доступен из интернета, даже когда ufw status показывает deny (incoming). Проверка с другой машины: nmap -Pn -p 8000 203.0.113.10; ответ 8000/tcp open значит: сервер уже нашли. Поэтому везде выше — 127.0.0.1:8000:8000, наружу смотрит только прокси на 443, подробнее — в статье, почему Portainer не создаёт порты.

Авторизация по спецификации — OAuth 2.1. Сервер без токена обязан ответить 401 с заголовком:

WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource"

По этому адресу — документ Protected Resource Metadata (RFC 9728): resource и authorization_servers, откуда клиент возьмёт токен, зарегистрировавшись динамически. Писать это руками — не на вечер, проще поставить рядом Keycloak или Authentik.

Честный минимум для внутренней команды. Трём людям из одной компании статичный bearer на прокси плюс allow-список IP закрывают вопрос на порядок лучше открытого эндпоинта. Спеке это не соответствует, внешние коннекторы так не подключить, но делается за десять минут.

Разделяйте права по контейнерам. Читающие и пишущие инструменты — разные процессы, разные тома (:ro для читающих), разные токены: модель вызывает инструменты по содержимому чужих документов, и промпт-инъекция в README доходит до реального вызова так же легко, как ваша команда. Логи при сбоях — docker compose logs -f --since 10m mcp и journalctl -u nginx -n 50; разбор поломок — в статье про частые ошибки MCP-сервера.

Какой сервер взять под MCP в MAATRIX

MCP-сервер почти не считает — он ходит в чужие API, базу или файловую систему и перекладывает ответ. Нагрузка сетевая и памятная, не процессорная.

Минимум: 1 vCPU, 2 ГБ RAM, 20 ГБ NVMe. Пустой FastMCP — порядка 100–150 МБ RSS, обёрнутый supergateway Node-сервер — примерно столько же, плюс nginx и Docker. Три-пять обёрток с прокси в 2 ГБ помещаются спокойно; на 1 ГБ тоже заводится, но первый же инструмент, читающий в память двадцатимегабайтный JSON, кончится OOM.

Комфортный вариант: 2 vCPU, 4 ГБ RAM, 40–60 ГБ NVMe. Место под Postgres, десяток контейнеров, спокойные обновления. Векторную базу на том же хосте считайте отдельно — там память зависит от размера индекса, не числа клиентов.

Локальная модель — другой класс железа. Наш замер на AMD EPYC 9554, 16 vCPU (Ollama 0.33.1, num_thread 16): qwen2.5:7b Q4_K_M — 7,4 ток/с и 5,1 ГБ весов, llama3.1:8b — 12,8 ток/с и 5,6 ГБ. Упирается в память уже на 4 потоках, а num_thread 32 на тех же 16 vCPU роняет скорость до 0,35 — под модель нужен отдельный сервер, не «побольше».

Локация — Великобритания, Лондон. Сервер весь день ходит в европейские и глобальные SaaS: RTT Москва — Лондон короче, чем до Восточного побережья США, а британский адрес не упирается в региональные ограничения, которые ловит российский. Проксируете OpenAI или Anthropic — смотрите на Нью-Йорк; данные россиян по 152-ФЗ — базу в РФ.

Про установку честно. MCP-сервер — не продукт, а класс: кнопки «поставить MCP» в каталоге нет и быть не может. Зато в apps.maatrix.io есть Portainer — ставится автоматически при заказе, доступы в кабинете; в этой панели удобно держать десяток MCP-контейнеров — compose-файл из третьего раздела стеком через веб-редактор, логи и рестарты в два клика, не по ssh. Подробности — в статье про установку Portainer на VPS. Оплата — картой российского банка, СБП, криптовалютой или токеном MAAT; иностранная не нужна, хотя сервер в Лондоне.

Развернуть за пару минут

Готовый образ на VPS MAATRIX: NVMe, AMD EPYC, root-доступ. Локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Развернуть Portainer

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →

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

Можно ли выставить наружу stdio-сервер, ничего не переписывая?

Да, через supergateway или mcp-proxy — запускают его дочерним процессом и отдают по Streamable HTTP. Но обёртка не даёт ни авторизации, ни разделения пользователей: секреты общие на всех, поэтому порт публикуйте только на 127.0.0.1, а авторизацию вешайте на прокси.

Клиент пишет 406 Not Acceptable, хотя сервер точно работает. Что не так?

Клиент шлёт Accept: application/json без text/event-stream, а спека требует оба типа; ответ так и звучит: Not Acceptable: Client must accept both application/json and text/event-stream. Пока клиент не обновили, добавьте на прокси proxy_set_header Accept "application/json, text/event-stream";.

Нужно ли срочно переезжать на спецификацию 2026-07-28?

Не срочно, но переезд упрощает жизнь: без Mcp-Session-Id балансировщику не нужна привязка клиента к экземпляру. Python SDK 2.x понимает и новую ревизию, и все прежние — обновление SDK не ломает уже подключённых клиентов, совместимость со старыми ревизиями выключать не надо.

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.