Запуск MCP-серверов на VPS для AI-агентов
Настройте Model Context Protocol на своем VPS для работы с stdio и HTTP. В статье разобраны конфигурации 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_text_file, write_file, list_directory), и теперь агент может вызывать их по разрешенным вами путям. Инструмент для работы с базой данных настраивается аналогично: замените пакет и передайте строку подключения в качестве последнего аргумента. Однако уточните актуальное имя пакета в репозитории самого сервера, так как эталонный сервер Postgres сменил владельцев уже несколько раз.
В этом и заключается смысл запуска агента на сервере: сессия Claude Code работает на VPS внутри tmux, а его stdio-серверы запускаются непосредственно рядом с ним, получая прямой доступ к файлам проекта и локальным сервисам без сетевых задержек. Как только агент получает доступ к write_file, а также к read_text_file, стоит дополнить эти возможности навыком, который направляет его к внесению минимально необходимых изменений, поскольку инструмент для работы с файловой системой делает масштабную переработку кода такой же простой, как и исправление двух строк. Такое подключение выходит за рамки локальных файлов: если на VPS уже запущен поисковый движок, вы можете предоставить агенту ваш собственный экземпляр SearXNG в качестве инструмента поиска. Это позволит выполнять запросы на вашем сервере, передавая текст с недоверенных страниц непосредственно в контекст, с которым затем работает агент.
Шаг 2: создание удаленного HTTP-сервера
Сервер, работающий через stdio, завершается вместе с родительским процессом и запускается для каждого клиента отдельно. Поэтому, если вы запускаете два сеанса Claude Code на одной машине, которые передают задачи друг другу, каждый из них получает собственную копию инструмента. Если вам нужен инструмент, который работает постоянно для всех клиентов — например, общий инструмент для операций, шлюз базы данных или сервис, к которому обращаются и ваш ноутбук, и система CI, — вам потребуется HTTP-транспорт и полноценный сервис. Ниже приведен минимальный пример сервера на Python с использованием официального SDK, который предоставляет один инструмент:
# /opt/mcp-ops/server.py
from mcp.server.mcpserver import MCPServer
import subprocess
mcp = MCPServer("ops-tools")
@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", port=8000)Обратите внимание на host="127.0.0.1". Сервер привязывается только к localhost, поэтому никто извне не сможет обратиться к нему напрямую. Это именно то, что нужно, пока не настроена аутентификация. Это SDK версии 2.x, которую pip install "mcp[cli]" загружает сегодня (2.3.0 на 2026-10-20). В ней FastMCP был переименован в MCPServer, а host и port были перенесены из конструктора в run(). Старый импорт mcp.server.fastmcp теперь вызывает ошибку ModuleNotFoundError. Установите SDK в отдельный 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 endpoint. 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. Для публичного интернета порт будет закрыт.
- Если доступ должен быть публичным, требуйте токен. Правильное решение — это OAuth-поток MCP, который нативно поддерживается 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} из переменных окружения во время чтения. Если переменная не задана, Claude Code не подставляет пустую строку: он сообщает об отсутствии переменной в claude mcp list и использует литеральный текст ${MCP_TOKEN}. Выберите собственное имя переменной, так как на удаленном сервере Claude Code намеренно считывает свои собственные переменные и переменные учетных данных вашего облачного провайдера, такие как ANTHROPIC_API_KEY, как пустые, наряду с другими учетными данными, присутствующими в вашем окружении, например NPM_TOKEN.
Каждая из перечисленных выше мер защиты охраняет endpoint, а не агента, который уже владеет токеном. Это вторая часть проблемы: если ваш клиент — это DeepSeek Harness, плагины, которые ограничивают список инструментов, доступных агенту, и сканируют вывод инструментов на наличие внедренных инструкций, закрывают эту сторону вопроса.
Шаг 6: отладка с помощью MCP Inspector
Если сервер работает некорректно, не пытайтесь угадать причину изнутри агента, управляйте им напрямую через Inspector — официальный веб-клиент для тестирования. Для stdio-сервера передайте ему ту же команду, которую запускает агент:
npx @modelcontextprotocol/inspector \
npx -y @modelcontextprotocol/server-filesystem /tmpОн запускает интерфейс на веб-сервере по адресу http://localhost:6274 и выводит URL с параметром запроса MCP_INSPECTOR_API_TOKEN; откройте эту ссылку, вместо того чтобы вводить адрес по памяти. Начиная с версии v2 (актуальная версия 2.11.0 на 2026-10-20), бэкенд также внедряет токен в страницу при каждой загрузке, поэтому перезагрузка страницы продолжает работать, а токен защищает маршруты /api/*, которые могут порождать процессы. Отдельный прокси v1 на порту 6277 больше не используется, но веб-сервер — не единственный слушающий процесс: v2 также запускает «песочницу» для MCP Apps на MCP_SANDBOX_PORT (по умолчанию 6275) и выделенный сервер app-origin на MCP_APP_ORIGIN_PORT (по умолчанию 6278), который обслуживает любое MCP App, если в его ресурсе UI указано _meta.ui.domain. Если вы подключаетесь к Inspector через SSH-туннель или запускаете его в контейнере и вам нужна вкладка Apps, пробросьте порты 6275 и 6278 вместе с 6274. Подключитесь, откройте вкладку Tools, выберите инструмент, заполните реальные аргументы и вызовите его. Если в Inspector всё работает, а в агенте нет, значит, ошибка в конфигурации вашего клиента, а не сервера. Для удаленного HTTP-сервера запустите Inspector без команды, добавьте сервер с транспортом Streamable HTTP и https://mcp.example.com/mcp, укажите заголовок Authorization в его настройках и подключитесь. Это самый быстрый способ проверить корректность аутентификации и прокси до того, как в процесс будет вовлечен агент. Inspector сохраняет заголовки в своем каталоге серверов в исходном виде, поэтому этот токен теперь также находится в ~/.mcp-inspector/mcp.json.
Поддержание серверов в актуальном состоянии
MCP развивается быстро, поэтому обновляйте компоненты по расписанию. Серверы Node, запущенные с незакрепленной версией npx -y, используют ту версию, которую разрешает npx: установленную в локальном проекте, если она есть, или копию, которую npx загружает в кэш npm. Флаг -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. Перед мажорным обновлением читайте примечания к релизу: переход с версии 1.x на 2.x в Python SDK нарушает работу кода 1.x при импорте. Руководство по миграции самого SDK рекомендует устанавливать верхнюю границу версии <2, например "mcp[cli]<2", до завершения миграции. При обновлении следите за версией спецификации, на которую ориентируется ваш SDK: переход через границу SSE-to-Streamable-HTTP может изменить протокол передачи, который должны запрашивать ваши клиенты.
Режимы сбоев и соответствующие сообщения
Агент сообщает о сбое сервера. claude mcp list выводит ✘ Failed to connect, а в текущих версиях к этой строке добавляются подробности ошибки. Выполните 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 не задана в момент чтения конфигурации клиентом, Claude Code предупреждает об отсутствующей переменной в claude mcp list и отправляет литеральный текст, поэтому nginx видит Bearer ${MCP_TOKEN}. Переменная, названная так же, как одна из учетных данных Claude Code, вашего облачного провайдера или среды, считывается как пустая, и 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: он изолированно управляет сервером, что позволяет отличить ошибку в самом сервере от ошибки в конфигурации клиента.