MCP-сервер на VPS: подключаем LLM к своим сервисам
MCP — стандарт, через который нейросеть получает доступ к вашим файлам, базам и API. Разбираемся, как он устроен, и поднимаем свой MCP-сервер на Ubuntu: Docker, домен с HTTPS, вход по токену и подключение из Claude Code и Open WebUI.
Что такое MCP и какую проблему он решает
Языковая модель сама по себе умеет только читать текст и писать текст. Она не видит ваши файлы, не знает, что лежит в базе, и не может создать задачу в трекере. Чтобы это изменить, нужен посредник — код, который объясняет модели «вот доступные действия» и выполняет их, когда модель попросит.
До 2025 года такого посредника каждый писал сам. Если у вас 5 ИИ-клиентов и 10 сервисов, которые нужно к ним подключить, получается 50 отдельных интеграций — и все ломаются независимо друг от друга. Классическая задача «N×M».
MCP (Model Context Protocol) превращает 50 интеграций в 15. Сервис один раз поднимает у себя MCP-сервер и объявляет список инструментов в стандартном формате. Клиент один раз учится читать этот формат. Дальше любой клиент работает с любым сервером — так же, как любая USB-C зарядка подходит к любому телефону с этим разъёмом.
Три вещи, которые сервер может отдать клиенту
- Инструменты (tools) — действия, которые модель может выполнить: прочитать файл, сделать запрос в базу, отправить сообщение. Самая используемая часть протокола.
- Ресурсы (resources) — данные для чтения: содержимое файла, запись из базы, страница документации.
- Промпты (prompts) — заготовки запросов, которые пользователь может выбрать в интерфейсе клиента.
Чем это отличается от плагинов и function calling. Function calling — умение модели сказать «вызови функцию X». Оно появилось раньше MCP, но описание функций и код их выполнения вы держите внутри своего приложения. Плагины — то же самое в формате конкретной платформы, и работают только на ней. MCP стоит уровнем выше: это протокол поверх function calling, который описывает, как инструменты объявляются и вызываются по сети. Один написанный MCP-сервер работает со всеми клиентами и не привязан ни к модели, ни к вендору.
Зачем для этого отдельный сервер
Большинство MCP-серверов из примеров в документации запускаются прямо на компьютере пользователя: клиент стартует программу и общается с ней через стандартный ввод-вывод. Никакой сети, никаких портов. Для одного человека за одним ноутбуком этого достаточно.
Отдельный сервер нужен, когда:
- инструменты нужны с разных устройств — с рабочего ноутбука, домашнего компьютера и телефона, без установки чего-либо на каждом;
- работа должна идти круглосуточно: сервер помнит контекст и остаётся доступен, когда ноутбук закрыт;
- инструментами пользуется команда, и нужна одна точка входа с общими правами и логами;
- сервис, к которому подключается модель, и так живёт на VPS — база, репозиторий, мониторинг. Держать MCP-сервер рядом с данными быстрее и безопаснее.
Два транспорта: stdio и Streamable HTTP
Транспорт — это способ доставки сообщений. Сам протокол от него не зависит: команды одинаковые, меняется только «труба». В актуальной спецификации 2026-07-28 их ровно два.
| Транспорт | Как работает | Когда выбирать |
|---|---|---|
| stdio | Клиент сам запускает программу-сервер и пишет ей в стандартный ввод | Всё на одной машине. Портов не открывает, посторонним недоступен |
| Streamable HTTP | Сервер слушает один адрес вида /mcp, клиент шлёт туда обычные POST-запросы | Наш случай. Сервер на VPS, клиенты подключаются по сети |
| HTTP+SSE | Два адреса: отдельно для отправки, отдельно для получения ответов | Устарел с марта 2025 года. Для нового не использовать |
Старый транспорт HTTP+SSE держал постоянно открытое соединение, которое разрывалось при любом сбое сети и плохо переживало обычные обратные прокси. Streamable HTTP работает как привычный веб-сервис: один адрес, обычные запросы, ответ приходит либо целиком, либо потоком, если задача долгая. Именно поэтому его легко спрятать за Nginx.
Спецификация меняется, и это нормально
Меньше чем за два года MCP пережил пять редакций. Последняя, 2026-07-28, заметно упростила Streamable HTTP: убрала сессии и отдельный поток для уведомлений. Клиенты умеют определять версию собеседника и откатываться на старое поведение, так что смешанные пары «новый клиент — старый сервер» работают. Практический вывод для вас: фиксируйте версии образов и пакетов, а не ставьте всё по тегу latest. Ниже версии зафиксированы везде.
Что мы построим
Официальные MCP-серверы — filesystem, memory, git и остальные из репозитория проекта — умеют только stdio: они рассчитаны на запуск рядом с клиентом. Чтобы вынести такой сервер на VPS, между ним и сетью ставят мост: он принимает HTTP-запросы и переводит их в стандартный ввод-вывод. Мы используем Supergateway — маленький мост в одном контейнере, которому не нужно ничего, кроме Docker. Сразу оговорка: это тонкая прослойка без собственной логики, и обновляется она редко — версия 3.4.3 вышла в октябре 2025. Для перевода stdio в HTTP этого достаточно, а проверку доступа мы намеренно возлагаем не на неё, а на Nginx.
Claude Code / Open WebUI / другой клиент
│ HTTPS + токен
▼
Nginx (порт 443, сертификат Let's Encrypt)
│ проверяет токен, проксирует на localhost
▼
Supergateway в Docker (127.0.0.1:8001)
│ переводит HTTP в stdio
▼
MCP-сервер (видит только папку /opt/mcp/data)В качестве первого MCP-сервера возьмём официальный filesystem — он даёт модели чтение и запись файлов в одной разрешённой папке. Это самый наглядный пример: сразу видно, что модель получила новые возможности, и сразу понятно, где проходит граница прав.
Почему не Docker MCP Gateway
У Docker есть свой официальный шлюз с каталогом готовых серверов. Он хорош, но рассчитан прежде всего на Docker Desktop, а на голом Docker Engine ставится отдельным бинарником-плагином. И у него была показательная уязвимость: в режиме HTTP до версии 0.28.0 шлюз не проверял заголовок Origin, из-за чего обычная веб-страница в браузере жертвы могла достучаться до всех подключённых MCP-серверов (CVE-2025-64443). Схема из этой статьи проще, состоит из стандартных кирпичей и с самого начала закрыта токеном.
Шаг 1. Подготовка сервера
Подойдёт любой VPS с Ubuntu 22.04 или новее и 1 ГБ оперативной памяти. Ставим Docker с официального скрипта — версия из репозитория Ubuntu старая и не содержит нужного плагина Compose.
sudo apt update && sudo apt upgrade -y
curl -fsSL https://get.docker.com | sudo sh
sudo docker compose versionПоследняя команда должна напечатать версию Compose — значит, всё встало. Теперь создаём рабочие папки:
sudo mkdir -p /opt/mcp/data
cd /opt/mcp/opt/mcp/data — единственная папка, которую увидит MCP-сервер. Всё остальное на сервере для него не существует.
Шаг 2. Запускаем MCP-сервер
Создайте файл /opt/mcp/docker-compose.yml — папка принадлежит root, поэтому редактор нужно открыть через sudo: sudo nano /opt/mcp/docker-compose.yml.
services:
mcp-filesystem:
image: supercorp/supergateway:3.4.3
container_name: mcp-filesystem
restart: unless-stopped
command: >
--stdio "npx -y @modelcontextprotocol/server-filesystem@2026.7.10 /data"
--outputTransport streamableHttp
--port 8000
--healthEndpoint /healthz
volumes:
- ./data:/data
ports:
- "127.0.0.1:8001:8000"Что здесь важно:
--stdio "..."— команда запуска самого MCP-сервера. Supergateway поднимет её и будет переводить HTTP в стандартный ввод-вывод. Версия пакета зафиксирована через@2026.7.10./dataв конце команды — единственная папка, к которой разрешён доступ. Она же подключена как том из./data.--outputTransport streamableHttp— тот самый актуальный транспорт. Адрес внутри контейнера получится/mcp.127.0.0.1:8001:8000— порт слушает только сам сервер. Без этой привязки Docker открыл бы порт всему интернету в обход ufw. Наружу сервис отдаст только Nginx.
cd /opt/mcp
sudo docker compose up -dПри первом запуске контейнер скачивает пакет MCP-сервера — это занимает 10–30 секунд. Проверяем, что мост жив:
curl http://127.0.0.1:8001/healthzДолжно ответить ok. Если ответа нет — смотрите sudo docker compose logs mcp-filesystem.
Шаг 3. Проверяем, что сервер отдаёт инструменты
Проверить MCP-сервер можно обычным curl — под капотом это JSON-RPC поверх HTTP. Спросим список инструментов:
curl -s -X POST http://127.0.0.1:8001/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'В ответ придёт длинный JSON со списком: read_text_file, write_file, list_directory, search_files и другие. Заголовок Accept обязателен — клиент по спецификации должен объявлять, что понимает оба формата ответа.
Теперь по-настоящему вызовем инструмент. Сначала положим в папку файл:
echo "Проверка MCP" | sudo tee /opt/mcp/data/note.txt
curl -s -X POST http://127.0.0.1:8001/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call",
"params":{"name":"read_text_file","arguments":{"path":"/data/note.txt"}}}'Ответ содержит "text":"Проверка MCP\n". Сервер работает. Обратите внимание на путь: внутри контейнера папка называется /data, а не /opt/mcp/data — модель тоже будет видеть её именно так.
Шаг 4. Домен, HTTPS и вход по токену
Сейчас сервер доступен только с самого VPS. Чтобы подключаться снаружи, нужны три вещи: домен, сертификат и проверка на входе. Без последней любой, кто узнает адрес, получит полный доступ к вашим файлам — MCP-сервер сам по себе никого не спрашивает.
Направьте A-запись поддомена (например mcp.example.com) на IP сервера — без этого сертификат не выпустится. Ставим Nginx:
sudo apt install -y nginx certbot python3-certbot-nginx
sudo ufw allow 'Nginx Full'Придумайте токен — это пароль для доступа к серверу. Сохраните вывод, он понадобится в клиенте:
openssl rand -hex 32Создайте /etc/nginx/sites-available/mcp, подставив свой домен и токен:
map $http_authorization $mcp_ok {
default "0";
"Bearer ВАШ_ТОКЕН_ИЗ_OPENSSL" "1";
}
server {
listen 80;
server_name mcp.example.com;
location = /fs {
if ($mcp_ok = "0") { return 401; }
proxy_pass http://127.0.0.1:8001/mcp;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Origin "";
# Потоковые ответы: не копить в буфере и не рвать по таймауту
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
}
}Разберём непривычные строки:
mapсравнивает присланный заголовокAuthorizationс вашим токеном. Не совпало — переменная равна нулю, и запрос получает 401 ещё до контейнера. Блокmapстоит внеserver— это обязательно, иначе Nginx не запустится.location = /fs— знак равенства означает точное совпадение адреса. Снаружи открыт ровно один путь, а не всё дерево. Внутренний/mcpвproxy_passподставляется автоматически, в клиенте его писать не нужно.proxy_set_header Origin ""— убираем заголовок, который шлёт браузер. MCP-клиенты его не отправляют, так что на работу это не влияет, а вот запросу со случайной веб-страницы лишний повод выглядеть доверенным мы не даём. Настоящая защита здесь — токен.proxy_buffering offи большойproxy_read_timeoutнужны для потоковых ответов: по умолчанию Nginx копит ответ в буфере и обрывает соединение через 60 секунд.
Включаем сайт и выпускаем сертификат — certbot сам допишет секцию с HTTPS и редирект с 80-го порта:
sudo ln -s /etc/nginx/sites-available/mcp /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d mcp.example.comПроверяем защиту снаружи:
# ожидаем 401
curl -s -o /dev/null -w "%{http_code}\n" -X POST https://mcp.example.com/fs
# ожидаем список инструментов
curl -s -X POST https://mcp.example.com/fs \
-H "Authorization: Bearer ВАШ_ТОКЕН_ИЗ_OPENSSL" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'Если Nginx и certbot настраивать не хочется
MCP-сервер на Streamable HTTP — это обычное веб-приложение, поэтому его можно выложить на PaaS-платформу: вы отдаёте код, платформа собирает контейнер и выдаёт домен с готовым сертификатом. Весь этот шаг с Nginx и certbot тогда отпадает, а токен задаётся переменной окружения. Из русскоязычных платформ так работает Hostim. Ограничение важное: файловая система контейнера пересоздаётся при каждом деплое, поэтому наш пример с доступом к файлам сервера так не поднять — для него нужен VPS. Подходит для серверов, которые ходят в базу или во внешний API.
Шаг 5. Подключаем клиента
Claude Code
Одна команда на вашем рабочем компьютере:
claude mcp add --transport http my-files https://mcp.example.com/fs \
--header "Authorization: Bearer ВАШ_ТОКЕН_ИЗ_OPENSSL"
claude mcp listclaude mcp list покажет статус: ✔ Connected — всё в порядке. Теперь можно просить «положи черновик статьи в файл draft.md» — и файл появится в /opt/mcp/data на сервере.
Open WebUI — полностью локальный вариант с Ollama
Если не хотите отправлять данные во внешние API, поставьте рядом Ollama с Open WebUI. Начиная с версии 0.6.31 Open WebUI понимает MCP нативно — и ровно по Streamable HTTP, который мы настроили.
- Откройте Admin Settings → Integrations и нажмите +.
- Тип — MCP (Streamable HTTP).
- URL —
https://mcp.example.com/fs. - Auth — Bearer, в поле ключа вставьте токен (только сам токен, без слова
Bearer). - Сохраните.
Два условия, без которых инструменты подключатся, но работать не будут. Первое: модель в Ollama должна уметь вызывать инструменты (tool calling) — в библиотеке Ollama такие модели собраны под фильтром Tools, на август 2026 это, например, qwen3.5, gemma4 и granite4.1. Второе: у Open WebUI должна быть задана переменная WEBUI_SECRET_KEY, иначе после перезапуска контейнера сохранённые ключи доступа перестанут расшифровываться. Добавлять MCP-серверы может только администратор.
Шаг 6. Добавляем второй сервер
Схема масштабируется копированием. Добавим memory — долговременную память: модель складывает туда факты и достаёт их в следующих разговорах. Дописываем в docker-compose.yml:
mcp-memory:
image: supercorp/supergateway:3.4.3
container_name: mcp-memory
restart: unless-stopped
command: >
--stdio "npx -y @modelcontextprotocol/server-memory@2026.7.4"
--outputTransport streamableHttp
--port 8000
--healthEndpoint /healthz
environment:
MEMORY_FILE_PATH: /data/memory.json
volumes:
- ./memory:/data
ports:
- "127.0.0.1:8002:8000"Отдельная папка ./memory — чтобы у каждого сервера были свои данные и он не видел чужие:
sudo mkdir -p /opt/mcp/memory
sudo docker compose up -dИ новый location в конфиге Nginx. После выпуска сертификата в файле стало два блока server: короткий с редиректом на HTTPS и основной с listen 443 ssl. Добавлять нужно в основной, рядом с уже существующим location = /fs:
location = /memory {
if ($mcp_ok = "0") { return 401; }
proxy_pass http://127.0.0.1:8002/mcp;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Origin "";
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
}sudo nginx -t && sudo systemctl reload nginxПравило одно: новый сервис в Compose со своим портом на 127.0.0.1 плюс новый location в Nginx. Остальные официальные серверы подключаются так же — важно только не перепутать язык, на котором сервер написан:
| Сервер | Что даёт | Команда для --stdio |
|---|---|---|
| sequential-thinking | Пошаговые рассуждения | npx -y @modelcontextprotocol/server-sequential-thinking@2026.7.4 |
| fetch | Загрузка веб-страниц | uvx --with mcp==1.29.0 mcp-server-fetch@2026.7.10 |
| time | Текущее время и часовые пояса | uvx --with mcp==1.29.0 mcp-server-time@2026.7.10 |
Серверы на Node запускаются через npx и работают на образе supercorp/supergateway:3.4.3. Серверы на Python запускаются через uvx, и им нужен другой образ — supercorp/supergateway:3.4.3-uvx. Зачем у них приписка --with mcp==1.29.0, объясняем в «Частых проблемах».
Отдельный случай — официальный сервер git. Он тоже на Python, но требует установленной программы git, которой в образе нет, и без неё падает с ImportError: Bad git executable. Чтобы его поднять, придётся собрать свой образ с git внутри — это выходит за рамки статьи.
Безопасность: главный раздел этой статьи
MCP-сервер — это не набор данных для чтения, а исполнитель действий. Вы выдаёте модели реальные права на своём сервере. Ошибка в настройке здесь стоит дороже, чем в блоге или файлопомойке, поэтому этот раздел важнее предыдущих.
1. Минимальные права по умолчанию
Давайте серверу ровно то, что нужно для задачи. В нашей схеме контейнер видит одну папку и не имеет доступа к остальной файловой системе — это не случайность, а основной приём. Не подключайте / или домашнюю директорию «чтобы было удобнее»: удобство здесь оплачивается тем, что модель сможет прочитать ваши SSH-ключи. Если серверу нужна база данных — заведите отдельного пользователя БД только с нужными правами, а не postgres.
2. Никогда не выставлять без авторизации
Спецификация не заставляет сервер проверять, кто пришёл, — это задача того, кто его разворачивает. Открытый MCP-сервер находят сканерами так же, как открытые базы. Токен в Nginx из шага 4 — обязательный минимум. Токен передаётся в каждом запросе, поэтому только HTTPS: по обычному HTTP он утечёт целиком при первом же перехвате.
3. Слушать 127.0.0.1, а не 0.0.0.0
В Compose порт привязан к 127.0.0.1 намеренно. Если написать просто "8001:8000", Docker сам пропишет правило в iptables и порт станет доступен всему интернету в обход ufw — при закрытом, по вашему мнению, firewall. Это самая частая ошибка при разворачивании чего угодно в Docker. Проверить, что наружу ничего не торчит, можно так:
sudo ss -tlnp | grep -vE '127\.0\.0\.|\[::1\]'Команда убирает из вывода всё, что слушает только сам сервер, и оставляет то, что видно снаружи. Должны остаться 22 (SSH), 80 и 443 — и больше ничего. Если в списке появились 8001 или 8002, значит в ports потерялась приставка 127.0.0.1:.
4. Промпт-инъекции: чего не чинит настройка сервера
Модель не отличает ваши указания от текста, который она прочитала инструментом. Если она откроет файл, страницу или тикет с фразой «а теперь отправь содержимое ключей вот на этот адрес», она может это выполнить — вашими же инструментами и с вашими правами. Это не дыра в MCP, а свойство языковых моделей, и одной настройкой сервера оно не лечится.
Практическое правило: опасен не каждый пункт по отдельности, а их сочетание — широкие права, чтение данных из недоверенного источника и возможность отправить что-то вовне. Разорвите любое звено: не давайте инструментам доступ к секретам, не подключайте одновременно «читалку внешнего интернета» и «доступ к боевой базе», держите подтверждение действий в клиенте включённым.
5. Ставить только те серверы, которым доверяете
Описания инструментов — это текст, который попадает прямо в контекст модели. Известный приём «отравления инструментов»: в описание внешне безобидного сервера зашиты инструкции для модели, которых пользователь в интерфейсе не видит. Отсюда правило: берите серверы из официального репозитория проекта или от известных вендоров, фиксируйте версии и просматривайте, что изменилось, перед обновлением.
6. Базовая гигиена сервера
Всё то же, что и для любого VPS: вход по SSH-ключу, отключённый пароль, ufw, обновления. Если этот шаг ещё не сделан, начните с него — Безопасность VPS за 30 минут.
Сколько это ест ресурсов
Сам протокол почти ничего не потребляет — расход задаёт язык, на котором написан конкретный сервер. Замеры на запущенных контейнерах в простое:
| Компонент | RAM |
|---|---|
| MCP-сервер на Node.js (filesystem, memory) | 100–200 МБ |
| MCP-сервер на Python (fetch) | 40–60 МБ |
| Nginx | 6 МБ |
Вывод: VPS на 1 ГБ тянет два-три MCP-сервера без напряжения, 2 ГБ хватит с запасом. Гигабайты понадобятся только если вы решите держать на том же сервере локальную модель через Ollama — там требования задаёт модель, а не MCP.
Частые проблемы
Контейнер запущен, но инструментов нет и ответы пустые
Мост работает, а MCP-сервер внутри упал — снаружи это выглядит как молчание. Смотрите логи: sudo docker compose logs mcp-filesystem. Ищите строки Child stderr и Child exited — там будет настоящая причина. Самый частый случай — несовместимые версии. Живой пример на август 2026: библиотека mcp для Python вышла в версии 2.0 и переименовала внутри себя McpError в MCPError, из-за чего все официальные Python-серверы (fetch, time, git) падают с ImportError: cannot import name 'McpError'. Лечится добавлением --with mcp==1.29.0 — это последняя версия ветки 1.x. Именно поэтому в статье все версии прибиты гвоздями: latest ломается молча.
Клиент показывает 401, хотя токен вроде правильный
Проверьте три вещи. Первое: в Nginx значение в map должно включать слово Bearer и один пробел — "Bearer abc123", а не просто "abc123". Второе: в интерфейсах вроде Open WebUI в поле ключа вставляют только сам токен, слово Bearer клиент добавит сам. Третье: лишний пробел или перенос строки при копировании — самая частая причина; сгенерируйте токен заново и вставьте аккуратно.
Соединение рвётся на долгих операциях
Признак того, что в location потерялись proxy_buffering off или proxy_read_timeout. По умолчанию Nginx копит ответ в буфере и обрывает соединение через 60 секунд, а потоковые ответы MCP живут дольше. Важный нюанс: если вы пишете эти строки внутри location, как в статье, их нужно повторить в каждом location. Чтобы не дублировать, вынесите обе строки на уровень server — оттуда они действуют на все вложенные блоки.
Модель не видит файл, который вы положили в папку
Почти всегда дело в пути. Внутри контейнера папка называется /data, поэтому файл /opt/mcp/data/note.txt для модели — это /data/note.txt. Проверить границы прав можно инструментом list_allowed_directories. Вторая причина — права на файл: контейнер работает от root и создаёт файлы от root, так что созданное моделью может не открываться вашим обычным пользователем. Лечится sudo chown -R $USER:$USER /opt/mcp/data.
Первый запрос после перезапуска долго висит
Это нормально. После docker compose down и повторного подъёма контейнер заново скачивает пакет MCP-сервера — уходит 10–30 секунд. Обычный docker compose restart кэш не теряет и поднимается за пару секунд. Если ждать не хочется, соберите свой образ с уже установленным пакетом.
Коротко: план с нуля
- Ubuntu 22.04 или новее, 1 ГБ RAM, Docker с get.docker.com
/opt/mcp/docker-compose.ymlс Supergateway и портом на127.0.0.1- Проверить локально через
curl:/healthzиtools/list - A-запись поддомена, Nginx, сертификат через certbot
- Токен через
openssl rand -hex 32и проверка вmapна входе - Подключить клиента:
claude mcp add --transport httpили Open WebUI → Integrations - Проверить
ss -tlnp, что наружу торчат только 22, 80 и 443
Под MCP-серверы хватает VPS от 1 ГБ RAM — выбрать можно из проверенных провайдеров
Топ VPS-провайдеров →