Запуск MCP-серверов на VPS для AI-агентов
Настройте работу MCP-серверов через stdio и HTTP на собственном VPS. В руководстве описана установка systemd, настройка TLS, аутентификация и решение проблем с JSON-RPC потоком.
Что вы создаете
Две рабочие конфигурации MCP на одном VPS. Первая — это stdio-сервер, инструмент для работы с файловой системой или базой данных, который Claude Code запускает как дочерний процесс и с которым взаимодействует через канал (pipe). Вторая — это remote HTTP-сервер, работающий как долгоживущий сетевой сервис под управлением systemd и обратного прокси-сервера nginx с TLS, доступный для любого MCP-клиента, который вы к нему направите. Установка любого из них занимает немного времени. Большая часть этого руководства посвящена двум аспектам, которые чаще всего вызывают сложности: поддержанию чистоты потока JSON-RPC и недопущению размещения неаутентифицированных конечных точек инструментов в общедоступном интернете.
Что такое MCP на самом деле
Model Context Protocol — это стандартный способ для AI-клиента, будь то Claude Code, Claude Desktop, Gemini CLI на VPS или ваш собственный скрипт, вызывать внешние инструменты и считывать внешние ресурсы. Сама модель ничего не исполняет. Она отправляет запрос клиенту, клиент общается с сервером MCP по протоколу JSON-RPC 2.0, а сервер запускает инструмент и возвращает результат. Этот клиент — именно то, что имеют в виду, когда говорят agent harness: цикл вокруг модели, который управляет списком инструментов, проверками прав доступа и состоянием сессии, а MCP — это просто способ расширить инструментальную часть. Протокол один, поэтому написанный вами сервер будет работать с любым клиентом, поддерживающим MCP. Если такое разделение для вас в новинку, и особенно если вас интересует, как именно модель решает обратиться к инструменту, поэтапное руководство по основам работы агентов стоит изучить в течение часа, прежде чем предоставлять таким серверам реальные учетные данные.
Существует два типа транспорта, и всё остальное руководство разделено в соответствии с ними:
- stdio. Клиент запускает сервер как дочерний процесс и обменивается JSON-RPC сообщениями, разделенными символом новой строки, через стандартный ввод и вывод. Никакой сети, портов или аутентификации; граница доверия проходит по самому процессу. Почти все локальные инструменты работают именно так.
- Streamable HTTP (и его предшественник HTTP+SSE). Сервер представляет собой постоянно работающий веб-сервис. Клиент подключается по HTTP, а сервер может передавать ответы обратно в виде Server-Sent Events. Это способ использовать один сервер для множества клиентов или запустить инструмент, который должен постоянно находиться на машине.
Выбирайте stdio, если инструмент принадлежит одной машине и одному пользователю. Выбирайте HTTP, если это общий сервис.
Предварительные требования и важные нюансы
Предполагается использование свежей VPS на базе Ubuntu 24.04 KVM с доступом root или sudo. Кроме того:
- Среда выполнения, на которой написан сервер. Большинство эталонных серверов реализованы на Node или Python. В Ubuntu 24.04 поставляется Node 18, однако многие современные пакеты MCP требуют Node 20 или новее. Поэтому установите актуальную LTS-версию из NodeSource или через nvm, не полагаясь на
apt. Python 3.12 уже предустановлен. - Домен и DNS A-запись. Это необходимо только для удаленного HTTP-сервера, так как для TLS требуется имя, разрешающееся в IP-адрес данной VPS. Для примера со stdio DNS не требуется вовсе.
- 512 MB RAM вполне достаточно. MCP-серверы — это легковесные процессы JSON-RPC; потребление памяти зависит от того, с чем работает ваш инструмент (драйвер базы данных, файловый кэш), а не от самого протокола.
- Спецификация молода и постоянно меняется. В редакции от 2025-03-26 HTTP+SSE был заменен на Streamable HTTP, а SSE объявлен устаревшим. SSE по-прежнему работает, и многие серверы его поддерживают, поэтому рассматривайте любые привязки к транспорту как параметры, которые стоит перепроверять по примечаниям к релизу сервера, а не как догму.
Шаг 1: подключение stdio-сервера к Claude Code
Начните с сервера файловой системы. Он является официальным, активно поддерживается и требует только наличия Node. Приведенная ниже команда регистрирует его в Claude Code и ограничивает область действия текущим проектом, чтобы конфигурация сохранилась в файле, который можно добавить в репозиторий:
cd /home/matt/projects/api
claude mcp add --scope project --transport stdio filesystem \
-- npx -y @modelcontextprotocol/server-filesystem /home/matt/projects/apiРазделитель -- имеет значение: всё, что идет после него, является командой, которую выполнит Claude Code, а не флагом для самого Claude Code. Это создает файл .mcp.json в корне проекта:
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"/home/matt/projects/api"
]
}
}
}Пока ничего не запущено. При следующем запуске Claude Code в этой директории агент прочитает .mcp.json, запустит npx -y @modelcontextprotocol/server-filesystem ... как дочерний процесс и выполнит рукопожатие MCP через stdin/stdout этого процесса. Проверьте успешность подключения:
claude mcp listИсправно работающий сервер выводит свою команду и зеленую галочку, filesystem: npx -y @modelcontextprotocol/server-filesystem ... - ✓ Connected. Внутри сессии слэш-команда /mcp выводит список инструментов, предоставляемых сервером (read_file, write_file, list_directory), и теперь агент может вызывать их по путям, которые вы разрешили. Инструмент для работы с базой данных настраивается аналогично: замените пакет и передайте строку подключения в качестве последнего аргумента. Однако уточните актуальное имя пакета в репозитории самого сервера, так как эталонный сервер Postgres сменил владельца уже не один раз.
В этом и заключается смысл запуска агента на сервере: сессия Claude Code работает на VPS внутри tmux, а его stdio-серверы функционируют рядом, имея прямой доступ к файлам проекта и локальным сервисам без сетевых задержек. Как только агент получает write_file и read_file, стоит дополнить эти возможности навыком поиска минимально необходимых изменений, поскольку инструмент для работы с файловой системой делает масштабную переработку кода такой же простой, как и исправление двух строк. Такое подключение выходит за рамки локальных файлов: если на VPS уже работает поисковая система, вы можете предоставить агенту ваш собственный экземпляр SearXNG в качестве инструмента поиска. Это позволит выполнять запросы на вашем сервере, передавая текст с непроверенных страниц напрямую в контекст, с которым затем работает агент.
Шаг 2: создание удаленного HTTP-сервера
Сервер, работающий через stdio, завершается вместе с родительским процессом и запускается для каждого клиента отдельно. Поэтому, если вы запускаете два сеанса Claude Code на одной машине, которые передают задачи друг другу, каждый из них получает собственную изолированную копию инструмента. Если вам нужен инструмент, который работает постоянно для всех клиентов — например, общая утилита для операций, шлюз к базе данных или сервис, к которому обращаются и ваш ноутбук, и система CI, — вам потребуется HTTP-транспорт и полноценный сервис. Ниже приведен пример минималистичного Python-сервера с использованием официального SDK, который предоставляет один инструмент:
# /opt/mcp-ops/server.py
from mcp.server.fastmcp import FastMCP
import subprocess
mcp = FastMCP("ops-tools", host="127.0.0.1", port=8000)
@mcp.tool()
def disk_free() -> str:
"""Return `df -h` for the server."""
out = subprocess.run(["df", "-h"], capture_output=True, text=True)
return out.stdout
if __name__ == "__main__":
# Serves Streamable HTTP at /mcp on 127.0.0.1:8000
mcp.run(transport="streamable-http")Обратите внимание на host="127.0.0.1". Сервер привязывается только к localhost, поэтому никто извне не сможет получить к нему прямой доступ. Это именно то, что нужно, пока не настроена аутентификация. Установите его в отдельный virtualenv, чтобы у systemd был стабильный путь к интерпретатору:
sudo useradd --system --home /opt/mcp-ops --shell /usr/sbin/nologin mcp
sudo install -d -o mcp -g mcp /opt/mcp-ops
sudo -H -u mcp python3 -m venv /opt/mcp-ops/.venv
sudo -H -u mcp /opt/mcp-ops/.venv/bin/pip install "mcp[cli]"Шаг 3: обеспечение постоянной работы через systemd
Инструмент, который недоступен в момент обращения агента, хуже, чем полное отсутствие инструмента. Это критически важно, когда сам клиент является долгоживущим процессом: постоянно работающий агент, сохраняющий состояние памяти и расписание между перезагрузками, будет вызывать эти инструменты по расписанию без участия оператора, поэтому сервер должен восстанавливаться самостоятельно. Создайте файл /etc/systemd/system/mcp-ops.service:
[Unit]
Description=MCP ops-tools server
After=network.target
[Service]
Type=simple
User=mcp
WorkingDirectory=/opt/mcp-ops
ExecStart=/opt/mcp-ops/.venv/bin/python /opt/mcp-ops/server.py
Restart=on-failure
RestartSec=2
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
[Install]
WantedBy=multi-user.targetИспользование абсолютного пути к Python внутри venv в ExecStart обязательно. Укажите /usr/bin/python3, чтобы процесс запускался с ModuleNotFoundError: No module named 'mcp', так как системный интерпретатор не видит ваш pip install. Включите и проверьте службу:
sudo systemctl daemon-reload
sudo systemctl enable --now mcp-ops
sudo systemctl status mcp-ops
curl -si -H 'Accept: application/json, text/event-stream' \
-H 'Content-Type: application/json' \
-X POST http://127.0.0.1:8000/mcpstatus должен вернуть active (running). curl вернет HTTP/1.1 400 Bad Request с ошибкой JSON-RPC в теле ответа; запрос не содержал сессии и корректной JSON-полезной нагрузки, и это именно то, что требуется: это доказывает, что порт открыт и протокол работает. Connection refused или пустой ответ означают, что процесс не привязан к ожидаемому адресу; изучите journalctl -u mcp-ops -n 50.
Шаг 4: настройка TLS и обратного прокси
Сервер ожидает соединений на localhost. Чтобы получить доступ к нему извне, настройте TLS-терминацию на nginx и проксирование запросов внутрь. Установите nginx, получите сертификат согласно руководству Certbot и Let's Encrypt в nginx, а затем создайте блок location. Важно отключить буферизацию, так как стандартное поведение nginx предполагает ожидание полного ответа, что блокирует поток SSE:
server {
listen 443 ssl;
server_name mcp.example.com;
# ssl_certificate lines managed by Certbot
location /mcp {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
# The four lines that make SSE work through nginx:
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
chunked_transfer_encoding off;
}
}Примените конфигурацию командой sudo nginx -t && sudo systemctl reload nginx. Если вы уже используете набор контейнеров, эту задачу проще решить с помощью обратного прокси Traefik с автоматическим TLS. Он самостоятельно выпускает сертификаты и выполняет маршрутизацию по именам хостов; вам достаточно добавить метки (labels) к контейнеру MCP. В любом случае, теперь обратный прокси — единственный компонент, доступный через публичный порт, и он указывает на сервис, который вы ещё не защитили. Выполните настройку безопасности перед тем, как публиковать URL.
Шаг 5: правило безопасности, определяющее эту тему
Никогда не открывайте неаутентифицированную конечную точку MCP. Сервер MCP — это не API только для чтения. Он предоставляет доступ к инструментам, вашим файлам и базе данных, а иногда и к shell. Открытый /mcp в общедоступном Интернете — это посторонний пользователь с теми же возможностями, что и ваш AI-агент: он перечисляет доступные инструменты, а затем вызывает их. Относитесь к нему так же, как к неаутентифицированному административному сокету, потому что это и есть такой сокет. Последствия кражи токена также зависят от сервера, который находится за ним: сервер MCP только для чтения, поставляемый с трекером тренировок openGym, может возвращать только данные о тренировках, тогда как инструмент для работы с файловой системой или shell предоставляет доступ ко всему серверу.
Три способа защиты в порядке предпочтения:
- Не публикуйте его. Оставьте сервер на
127.0.0.1и подключайтесь к нему со своего ноутбука через SSH-туннель:ssh -L 8000:127.0.0.1:8000 matt@vps, затем укажите клиенту адресhttp://127.0.0.1:8000/mcp. Ничего не будет выставлено наружу. - Разместите его в частной сети. Привяжите туннельный адрес к самостоятельно развернутому WireGuard VPN и разрешите доступ только участникам VPN. Для общедоступного интернета порт будет закрыт.
- Если он должен быть публичным, требуйте токен. Правильное решение — это процесс MCP OAuth, который нативно поддерживается HTTP-транспортом. Прагматичный минимум — это общий bearer-токен, проверяемый на прокси; это дешево и полностью предотвращает случайные атаки:
location /mcp {
if ($http_authorization != "Bearer REPLACE_WITH_LONG_RANDOM") {
return 401;
}
proxy_pass http://127.0.0.1:8000;
# ...buffering-off block from above...
}Сгенерируйте токен с помощью openssl rand -hex 32 и никогда не привязывайте сам сервер к 0.0.0.0 без использования одного из этих методов защиты. Клиент затем отправляет токен в заголовке. В Claude Code:
claude mcp add --scope project --transport http ops-tools https://mcp.example.com/mcp \
--header 'Authorization: Bearer ${MCP_TOKEN}'Установите MCP_TOKEN в своей оболочке, чтобы секрет никогда не попадал в .mcp.json в открытом виде; Claude Code подставляет ${MCP_TOKEN} из переменных окружения во время чтения.
Каждый из перечисленных выше методов защиты охраняет endpoint, а не агента, который уже владеет токеном, что является второй частью проблемы: если ваш клиент — это DeepSeek Harness, плагины, которые ограничивают список инструментов, доступных агенту, и сканируют вывод инструментов на наличие внедренных инструкций, закрывают эту сторону вопроса.
Шаг 6: отладка с помощью MCP Inspector
Если сервер работает некорректно, не пытайтесь угадать причину внутри агента. Используйте Inspector — официальный веб-клиент для тестирования. Для сервера, работающего через stdio, передайте ему ту же команду, которую запускает агент:
npx @modelcontextprotocol/inspector \
npx -y @modelcontextprotocol/server-filesystem /tmpОн запускает интерфейс на http://localhost:6274 (в последних версиях выводится URL с параметром запроса MCP_PROXY_AUTH_TOKEN, используйте именно эту ссылку, иначе интерфейс отклонит подключение) и прокси на порту 6277. Нажмите Connect, затем List Tools и выполните Call Tool с реальными аргументами. Если в Inspector всё работает, а в агенте — нет, значит, ошибка в конфигурации клиента, а не сервера. Для удалённого HTTP-сервера выберите транспорт Streamable HTTP, введите https://mcp.example.com/mcp, добавьте заголовок Authorization и подключитесь. Это самый быстрый способ проверить корректность аутентификации и прокси до того, как будет задействован агент.
Поддержание серверов в актуальном состоянии
MCP развивается быстро, поэтому выполняйте обновление по расписанию. Серверы Node, запущенные с помощью npx -y, при каждом запуске получают последнюю версию, что удобно, но не обеспечивает воспроизводимость. Зафиксируйте точную версию, которую вы протестировали: считайте её из npm view @modelcontextprotocol/server-filesystem version и добавьте к имени пакета в .mcp.json (@modelcontextprotocol/server-filesystem@<version>), как только сервер станет критически важным, и обновляйте её осознанно. Серверы на Python под управлением systemd обновляются с помощью sudo -H -u mcp /opt/mcp-ops/.venv/bin/pip install -U "mcp[cli]", за которой следует sudo systemctl restart mcp-ops. При обновлении следите за версией спецификации, на которую ориентируется ваш SDK: переход через границу SSE-to-Streamable-HTTP может изменить транспорт, который должны запрашивать ваши клиенты.
Режимы сбоев и соответствующие сообщения
Агент сообщает о сбое сервера. claude mcp list выводит ✗ Failed to connect, а TUI сообщает MCP server 'filesystem' failed to start. Запустите claude --debug, и вы, как правило, увидите Error: spawn npx ENOENT: команда отсутствует в переменной PATH агента. Среда выполнения отсутствует или находится не там, где её ищет агент: Node не установлен, npx отсутствует или Python из virtualenv вызывается по короткому имени. Укажите абсолютный путь к команде или установите среду выполнения, затем переподключитесь.
Сервер stdio подключается и сразу разрывает соединение. Клиент записывает в лог ошибку парсинга JSON, например Unexpected token 'S', "Server sta"... is not valid JSON или Failed to parse message. Причина всегда одна: сервер записал строку лога в stdout. При использовании stdio stdout является каналом JSON-RPC, поэтому любой посторонний текст повреждает поток, и рукопожатие прерывается. В Node console.log выводит данные в stdout, используйте console.error. В Python обычный print() выводит данные в stdout; записывайте логи с помощью logging, настроенного на sys.stderr, или передавайте file=sys.stderr. Правило неизменно: при работе через stdio в stdout должен быть только JSON-RPC, всё остальное — в stderr.
Удалённый сервер превышает время ожидания или закрывает соединение во время рукопожатия. Клиент выдаёт ошибку MCP error -32000: Connection closed, или Inspector зависает на этапе Connect и не отображает инструменты. При работе через nginx проблема заключается в буферизации: прокси удерживает поток SSE вместо того, чтобы сбрасывать его, поэтому клиент ждёт ответа, который не приходит. Добавьте proxy_buffering off; (и остальную часть блока из шага 4) в location. Проверьте результат с помощью curl -N для публичного URL: данные событий должны поступать постепенно, а не все сразу в конце.
Ошибка авторизации. Клиент сообщает Error POSTing to endpoint (HTTP 401) или просто 401 Unauthorized. Либо отсутствует заголовок, либо неверный токен, либо переменная окружения была пуста в момент чтения конфигурации клиентом. Это распространённая ловушка: ${MCP_TOKEN} раскрывается в пустую строку, если переменная не задана, и nginx видит Bearer без значения. Выведите значение переменной через echo, добавьте заголовок заново и убедитесь, что байты в точности совпадают с токеном в if для nginx.
Сервис не запускается под управлением systemd. journalctl -u mcp-ops показывает ModuleNotFoundError: No module named 'mcp', ExecStart указывает на системный Python вместо интерпретатора из venv. Либо Address already in use — порт 8000 занят другим процессом; найдите его с помощью sudo ss -ltnp | grep 8000.
FAQ
Что именно представляет собой MCP server?
Это программа, которая предоставляет инструменты и ресурсы AI-клиенту через протокол Model Context Protocol с использованием JSON-RPC 2.0. Модель AI не запускает инструмент самостоятельно: она отправляет запрос клиенту, клиент обращается к MCP server, а сервер выполняет задачу и возвращает результат. Поскольку протокол стандартизирован, один сервер работает с любым совместимым клиентом, будь то Claude Code, Claude Desktop или Gemini CLI.
В чем разница между транспортом stdio и HTTP?
Сервер stdio запускается клиентом как дочерний процесс и взаимодействует через stdin/stdout. Он существует и завершается вместе с одним клиентом на одной машине и не требует сети или аутентификации. HTTP-сервер — это постоянно работающая сетевая служба, к которой могут обращаться сразу несколько клиентов, поэтому для него обязательны TLS и аутентификация. Используйте stdio для локальных инструментов с одним пользователем; используйте HTTP (в текущих версиях — Streamable HTTP) для любых общих или постоянно доступных ресурсов.
Как обеспечить безопасность удаленного MCP server?
Исходите из того, что сервер предоставляет доступ к вашим файлам, базе данных или оболочке, поэтому никогда не открывайте его без аутентификации. Лучший вариант — привязать его к localhost и подключаться через SSH-туннель или частную VPN. Если сервер должен быть публичным, разместите его за reverse proxy, который требует bearer token или использует поток MCP OAuth. Генерируйте токен с помощью openssl rand -hex 32 и никогда не привязывайте сервер к 0.0.0.0 без использования этих средств защиты.
Как отладить сервер, который не запускается?
Сначала проверьте claude mcp list, ✗ Failed to connect. Если команда возвращает spawn ... ENOENT, значит, отсутствует сама команда или среда выполнения — исправьте путь или установите необходимые компоненты. Если соединение устанавливается, но затем разрывается с ошибкой JSON parse error, значит, сервер выводит логи в stdout, что нарушает поток JSON-RPC; перенаправьте весь вывод логов в stderr. В остальных случаях запустите команду напрямую через MCP Inspector: он изолированно управляет сервером, что позволяет отличить ошибку в коде сервера от ошибки в конфигурации клиента.