Как поднять MCP-сервер на VPS
На ноутбуке MCP-сервер запускается одной строкой в конфиге клиента и работает. Стоит подключить второго человека, веб-клиент или CI — и «поднять на VPS» оказывается другой задачей: другой транспорт, прокси, умеющий держать поток, и открытый в интернет эндпоинт с доступом к вашим данным. Разберём установку MCP-сервера на VPS целиком — от транспорта до заголовков, на которых спотыкается половина клиентов.
Содержание
- Что вы на самом деле поднимаете: stdio против сетевого транспорта
- Версия протокола: спецификация 2026-07-28 и почему деплой стал проще
- Установка MCP-сервера на VPS: подготовка и первый запуск
- Как поднять stdio-сервер, который писали не вы
- Реверс-прокси: три кода ответа, на которых ловятся все
- Кто имеет право дёргать ваши инструменты
- Какой сервер взять под MCP в MAATRIX
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество 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 сверяет Host (и Origin, если есть) со списком разрешённых; по умолчанию список пуст — проходят только 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 — десятки моделей в одном окне. Оплата картой РФ и по СБП.