SSD Nodes Learn
Руководства Matt ConnorАвтор: Matt Connor · Обновлено 2026-07-24

Запуск 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, потому что это именно он.

Три метода защиты в порядке приоритетности:

  1. Не публикуйте его. Держите сервер на 127.0.0.1 и подключайтесь к нему со своего ноутбука через SSH-туннель: ssh -L 8000:127.0.0.1:8000 matt@vps, затем укажите клиенту адрес http://127.0.0.1:8000/mcp. Ничего не будет доступно извне.
  2. Разместите его в частной сети. Используйте адрес туннеля self-hosted WireGuard VPN и разрешите доступ только участникам VPN. Для публичного интернета порт будет закрыт.
  3. Если сервер должен быть публичным, требуйте токен. Правильный вариант — это процесс 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. Это позволит запустить сервер изолированно и отличить ошибку в коде сервера от ошибки в конфигурации клиента.