Запуск MCP серверов на VPS для AI агентов
Настройте stdio и remote HTTP транспорт для MCP на VPS. Инструкция по использованию systemd, nginx и TLS для безопасного подключения AI агентов к инструментам.
Что вы создаете
Две рабочие конфигурации 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 или вашему собственному скрипту) вызывать внешние инструменты и считывать внешние ресурсы. Сама модель ничего не запускает. Она отправляет запрос клиенту, клиент передает сообщение по протоколу JSON-RPC 2.0 на сервер MCP, а сервер выполняет инструмент и возвращает результат. Использование одного протокола позволяет использовать один и тот же сервер со всеми клиентами, поддерживающими MCP.
Существует два типа транспорта, и дальнейшее руководство разделено по этому признаку:
- stdio. Клиент запускает сервер как дочерний процесс и обменивается сообщениями JSON-RPC, разделенными символом новой строки, через стандартный ввод (stdin) и стандартный вывод (stdout). Сеть, порты и аутентификация не требуются — границей доверия является сам процесс. Почти все локальные инструменты используют этот способ.
- Streamable HTTP (и его предшественник HTTP+SSE). Сервер представляет собой постоянно работающий веб-сервис. Клиент подключается по HTTP, а сервер может передавать ответы потоком через Server-Sent Events. Этот способ подходит для предоставления одного сервера множеству клиентов или для запуска инструментов, которые должны постоянно работать на хосте.
Выбирайте stdio, если инструмент предназначен для одной машины и одного пользователя. Выбирайте HTTP, если это общий сервис.
Предварительные требования и известные нюансы
Предположим, у вас есть чистая Ubuntu 24.04 KVM VPS с правами root или sudo. Помимо этого:
- Среда выполнения, на которой написан сервер. Большинство эталонных серверов используют Node или Python. В Ubuntu 24.04 предустановлен Node 18, однако многим современным MCP-пакетам требуется Node 20 или выше. Рекомендуется установить актуальную версию LTS через NodeSource или nvm, а не полагаться на
apt. Python 3.12 уже установлен. - Домен и DNS A-запись, но только для удаленного HTTP-сервера — для TLS требуется имя, которое разрешается в этот VPS. При использовании stdio DNS не требуется.
- 512 MB RAM достаточно. MCP-серверы — это легковесные процессы JSON-RPC; потребление памяти зависит от используемых инструментов (драйвер базы данных, кэш файлов), а не от протокола.
- Спецификация еще развивается. Редакция от 2025-03-26 заменила HTTP+SSE на Streamable HTTP и пометила SSE как устаревший (deprecated). 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-серверы работают непосредственно рядом с ней. Это обеспечивает прямой доступ к файлам проекта и локальным сервисам без сетевых задержек.
Шаг 2: создание удаленного HTTP-сервера
stdio-сервер завершает работу вместе с родительским процессом. Если вам нужен инструмент, который остается доступным для всех клиентов — например, общий инструмент администрирования, шлюз базы данных или сервис, к которому обращаются и ваш ноутбук, и 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/mcpВывод status должен содержать 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 — он сам выпускает сертификат и выполняет маршрутизацию по hostname, вам нужно только добавить labels в контейнер MCP. В любом случае, теперь на публичном порту работает только обратный прокси, который направляет трафик на еще не защищенный сервис. Исправьте это перед регистрацией URL в каких-либо сервисах.
Шаг 5: основное правило безопасности для этой темы
Никогда не открывайте неавторизованный MCP endpoint. MCP-сервер — это не API только для чтения. Он предоставляет доступ к инструментам: вашим файлам, базе данных, а иногда и к shell. Открытый /mcp в публичном интернете дает постороннему лицу те же возможности, что и вашему AI-агенту: он может перечислить ваши инструменты и вызвать их. Относитесь к этому так же, как к неавторизованному admin socket, потому что это именно он.
Три метода защиты в порядке приоритетности:
- Не публикуйте его. Держите сервер на
127.0.0.1и подключайтесь к нему со своего ноутбука через SSH-туннель:ssh -L 8000:127.0.0.1:8000 matt@vps, затем укажите клиенту адресhttp://127.0.0.1:8000/mcp. Ничего не будет доступно извне. - Разместите его в частной сети. Используйте адрес туннеля self-hosted WireGuard VPN и разрешите доступ только участникам VPN. Для публичного интернета порт будет закрыт.
- Если сервер должен быть публичным, требуйте токен. Правильный вариант — это процесс MCP OAuth, который нативно поддерживается в HTTP transport. Прагматичный минимум — это общий bearer token, проверяемый на прокси: это дешево и полностью защищает от случайных атак:
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 в вашей shell, чтобы секрет не сохранялся в .mcp.json в открытом виде — Claude Code подставляет ${MCP_TOKEN} из переменных окружения во время чтения.
Шаг 6: отладка с помощью MCP Inspector
Если сервер работает некорректно, не пытайтесь угадать причину через агент. Используйте Inspector — официальный веб-клиент для тестирования. Для stdio-сервера используйте ту же команду, которую запускает агент:
npx @modelcontextprotocol/inspector \
npx -y @modelcontextprotocol/server-filesystem /tmpИнтерфейс запустится на http://localhost:6274 (в последних версиях выводится URL с query-строкой MCP_PROXY_AUTH_TOKEN — используйте именно эту ссылку, иначе интерфейс отклонит соединение). Прокси-сервер будет запущен на порту 6277. Нажмите Connect, затем List Tools, затем Call Tool с реальными аргументами. Если в Inspector всё работает, а в агенте — нет, ошибка находится в конфигурации клиента, а не в сервере. Для удаленного HTTP-сервера выберите транспорт Streamable HTTP, введите https://mcp.example.com/mcp, добавьте заголовок Authorization и нажмите connect. Это самый быстрый способ проверить корректность авторизации и прокси до подключения агента.
Обновление серверов
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, заново добавьте заголовок и убедитесь, что байты точно соответствуют токену в nginx if.
Сервис не запускается через 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 transport?
Stdio server запускается клиентом как дочерний процесс и взаимодействует через stdin/stdout. Такой сервер работает только в рамках одного сеанса одного клиента на одной машине и не требует сетевого соединения или аутентификации. HTTP server — это постоянно работающий сетевой сервис, к которому могут одновременно подключаться множество клиентов, поэтому он требует TLS и аутентификации. Используйте stdio для локальных инструментов для одного пользователя; используйте HTTP (Streamable HTTP на текущих серверах) для общих или постоянных сервисов.
Как обезопасить удаленный MCP server?
Предположите, что сервер предоставляет доступ к вашим файлам, базам данных или shell; никогда не оставляйте его без аутентификации. Рекомендуется привязать сервер к localhost и подключаться через SSH-туннель или частный VPN. Если сервер должен быть публичным, разместите его за reverse proxy, который требует bearer token или использует MCP OAuth flow. Сгенерируйте токен с помощью openssl rand -hex 32 и никогда не привязывайте сервер к 0.0.0.0 без использования одного из этих методов защиты.
Как отладить сервер, который не запускается?
Сначала проверьте claude mcp list. Ошибка ✗ Failed to connect с spawn ... ENOENT означает, что команда или среда выполнения (runtime) отсутствуют; исправьте путь или установите их. Если соединение устанавливается, но затем обрывается с ошибкой JSON parse error, значит, сервер записывает логи в stdout, повреждая поток JSON-RPC; перенаправьте все логи в stderr. В остальных случаях запустите ту же команду через MCP Inspector. Это позволит запустить сервер изолированно и отличить ошибку в коде сервера от ошибки в конфигурации клиента.