SSD Nodes Learn Hosting plans →
Посібники Matt ConnorВід Matt Connor

Як запустити MCP server на VPS для AI-агентів

Налаштуйте MCP на VPS: stdio та remote HTTP, systemd, nginx із TLS і auth. Розберіть чистий JSON-RPC та помилку відкритого endpoint без автентифікації.

Що ви налаштуєте

Дві робочі конфігурації MCP на одному VPS. Спочатку stdio-сервер — інструмент для роботи з файловою системою або базою даних, який Claude Code запускає як дочірній процес і взаємодіє з ним через pipe. Потім remote HTTP-сервер — довготривалий мережевий сервіс за systemd і reverse proxy nginx із TLS. До нього можна підключити будь-який MCP-клієнт. Встановлення кожної конфігурації займає небагато часу. Основна частина цього посібника присвячена двом проблемам, які найчастіше спричиняють збої: потік JSON-RPC має залишатися чистим, а endpoint інструмента в жодному разі не можна публікувати в інтернеті без автентифікації.

Що насправді таке MCP

Model Context Protocol — це стандартний спосіб, за допомогою якого AI-клієнт, Claude Code, Claude Desktop, Gemini CLI на VPS або ваш власний скрипт викликає зовнішні інструменти й читає зовнішні ресурси. Сама модель нічого не запускає. Вона надсилає запит клієнту, клієнт обмінюється даними з MCP server через JSON-RPC 2.0, а server запускає інструмент і повертає результат. Саме цей клієнт мають на увазі, коли говорять про agent harness: це цикл навколо моделі, який керує списком інструментів, перевірками дозволів і станом сесії. MCP — лише спосіб розширити його частину, що відповідає за інструменти. Протокол один, тому server, який ви написали один раз, працює з кожним клієнтом, що підтримує MCP. Якщо цей поділ для вас новий, особливо якщо ви не знаєте, як модель узагалі вирішує скористатися інструментом, перед передаванням такому server реальних облікових даних варто витратити годину на послідовний шлях вивчення основ agent.

Є два транспорти, і весь подальший посібник поділено відповідно до них:

  • stdio. Клієнт запускає server як дочірній процес і обмінюється повідомленнями JSON-RPC, розділеними символом нового рядка, через стандартні потоки введення та виведення. Мережа, порт і автентифікація не потрібні; межею довіри є сам процес. Майже кожен локальний інструмент постачається в такому режимі.
  • Streamable HTTP (і старіший варіант HTTP+SSE). Server працює як довготривалий web-сервіс. Клієнт підключається через HTTP, а server може передавати відповіді як Server-Sent Events. Так можна використовувати один server спільно з багатьма клієнтами або запускати інструмент, який має постійно працювати на сервері.

Обирайте stdio, якщо інструмент призначений для одного комп’ютера й одного користувача. Обирайте HTTP, якщо це спільний сервіс.

Передумови та важливі застереження

Припускаємо, що ви використовуєте новий KVM VPS з Ubuntu 24.04 і маєте доступ через 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 позначено як застарілий. SSE досі працює, і багато серверів усе ще його підтримують. Тому будь-яку фіксацію транспорту перевіряйте за release notes сервера, а не сприймайте її як остаточне правило.

Крок 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 handshake через stdin/stdout цього процесу. Перевірте результат:

claude mcp list

Працездатний сервер виводить свою команду та зелену позначку, filesystem: npx -y @modelcontextprotocol/server-filesystem ... - ✔ Connected. У межах сесії slash-команда /mcp показує інструменти, які надає сервер (read_text_file, write_file, list_directory), і агент може викликати їх для дозволених шляхів. Інструмент для бази даних має таку саму структуру: замініть пакет і передайте рядок підключення останнім аргументом. Водночас перевірте репозиторій самого сервера, щоб дізнатися актуальну назву пакета, оскільки reference Postgres server неодноразово переходив до інших супроводжувачів.

У цьому і полягає головна перевага запуску агента безпосередньо на сервері: сесія Claude Code працює на VPS усередині tmux, а його stdio-сервери працюють поруч із ним і мають прямий доступ до файлів проєкту та локальних сервісів без мережевого обміну. Коли агент отримує write_file, а також read_text_file, варто поєднати цей доступ із skill, який спрямовує його до найменшої працездатної зміни, оскільки filesystem tool робить масштабне переписування таким самим простим, як виправлення у два рядки. Таке саме підключення працює не лише з локальними файлами: якщо на VPS уже запущено пошукову систему, можна надати агенту власний екземпляр SearXNG як пошуковий інструмент. Це залишає запити на вашому сервері, але завантажує неперевірений текст вебсторінок безпосередньо в контекст, на основі якого агент потім виконує дії.

Крок 2: створіть віддалений HTTP-сервер

Сервер stdio завершує роботу разом із батьківським процесом і запускається окремо для кожного клієнта. Тому якщо ви запустите два сеанси Claude Code на сервері, які передають роботу один одному, кожен сеанс отримає власну ізольовану копію інструмента. Якщо потрібен інструмент, який працює для всіх клієнтів, спільний ops-інструмент, шлюз до бази даних або сервіс, до якого одночасно звертаються ваш ноутбук і 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

Абсолютний шлях до venv Python у 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 і reverse proxy перед сервісом

Сервер слухає 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. Якщо ви вже використовуєте багато контейнерів, те саме завдання виконає reverse proxy Traefik з автоматичним TLS: він отримає сертифікат і маршрутизує запити за іменем хоста, а вам потрібно лише додати labels до контейнера MCP. У будь-якому разі reverse proxy тепер буде єдиним компонентом на публічному порту, і він вказуватиме на сервіс, який ще не захищено. Виправте це, перш ніж реєструвати URL будь-де.

Крок 5: правило безпеки, яке має пріоритет у цій темі

Ніколи не відкривайте неавтентифікований MCP endpoint. MCP server — це не API лише для читання. Він надає доступ до інструментів, ваших файлів, бази даних, а іноді й shell. Відкритий /mcp у публічному інтернеті — це сторонній користувач із такими самими можливостями, як у вашого AI agent: він переглядає доступні інструменти, а потім викликає їх. Ставтеся до нього так само, як до неавтентифікованого admin socket, адже фактично це він і є. Наслідки викрадення token також залежать від server, що працює за ним: MCP server лише для читання, який постачається з трекером тренувань openGym, може повертати тільки дані про тренування, тоді як filesystem або shell tool надає доступ до всього сервера.

Три способи захисту в порядку пріоритетності:

  1. Не публікуйте його. Залиште server на 127.0.0.1 і підключайтеся до нього з laptop через SSH tunnel: ssh -L 8000:127.0.0.1:8000 matt@vps, а потім налаштуйте client на http://127.0.0.1:8000/mcp. Нічого не буде відкрито назовні.
  2. Розмістіть його в приватній мережі. Прив’яжіть tunnel address до self-hosted WireGuard VPN і дозвольте доступ лише VPN peers. Публічний інтернет бачитиме закритий порт.
  3. Якщо endpoint обов’язково має бути публічним, вимагайте token. Правильний варіант — MCP OAuth flow, який HTTP transport підтримує нативно. Практичний мінімум — спільний bearer token, перевірений на proxy. Це дешево й повністю блокує випадкові спроби доступу:
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...
}

Згенеруйте token за допомогою openssl rand -hex 32 і ніколи не прив’язуйте сам server до 0.0.0.0 без одного із цих механізмів перед ним. Після цього client надсилає token у header. У Claude Code:

claude mcp add --scope project --transport http ops-tools https://mcp.example.com/mcp \
  --header 'Authorization: Bearer ${MCP_TOKEN}'

Встановіть MCP_TOKEN у shell, щоб secret ніколи не потрапляв до .mcp.json у відкритому вигляді. Claude Code розгортає ${MCP_TOKEN} зі змінної середовища під час читання. Якщо змінну не встановлено, Claude Code не підставляє порожній рядок: він позначає відсутню змінну в claude mcp list і використовує буквальний текст ${MCP_TOKEN}. Також виберіть власне ім’я, оскільки в headers remote server Claude Code навмисно читає власні credential variables і credential variables вашого cloud provider, наприклад ANTHROPIC_API_KEY, як порожні. Те саме стосується інших credentials, які передає ваше середовище, наприклад NPM_TOKEN.

Усі наведені вище способи захищають endpoint, а не agent, який уже має token. Це інша частина проблеми: якщо вашим client є DeepSeek Harness, plugins, які обмежують інструменти, доступні agent, і перевіряють результат роботи інструментів на ін’єктовані інструкції, захищають саме цей бік.

Крок 6: налагодження за допомогою MCP Inspector

Якщо сервер працює неправильно, не намагайтеся вгадати причину з agent. Керуйте ним безпосередньо через Inspector — офіційний вебклієнт для тестування. Для сервера stdio передайте йому ту саму команду, яку запускає agent:

npx @modelcontextprotocol/inspector \
  npx -y @modelcontextprotocol/server-filesystem /tmp

Inspector запускає свій інтерфейс на одному вебсервері на http://localhost:6274 і виводить URL із рядком запиту MCP_INSPECTOR_API_TOKEN. Відкривайте це посилання, а не вводьте адресу вручну. Починаючи з v2 (станом на 2026-10-20 актуальною є версія 2.11.0), бекенд також додає токен до сторінки під час кожного завантаження. Тому після перезавантаження сторінка продовжує працювати. Токен захищає маршрути /api/*, які можуть запускати процеси. Окремого проксі v1 на 6277 більше немає. Водночас вебсервер не є єдиним listener: v2 також запускає sandbox для MCP Apps на MCP_SANDBOX_PORT (за замовчуванням 6275) і окремий сервер app-origin на MCP_APP_ORIGIN_PORT (за замовчуванням 6278). Він обслуговує будь-який MCP App, UI-ресурс якого оголошує _meta.ui.domain. Якщо ви підключаєтеся до Inspector через SSH tunnel або запускаєте його в контейнері й використовуєте вкладку Apps, пробросьте 6275 і 6278 разом із 6274.

Підключіться, відкрийте вкладку Tools, виберіть інструмент, укажіть реальні аргументи та викличте його. Якщо інструмент працює в Inspector, але не працює в agent, проблема у конфігурації клієнта, а не сервера. Для віддаленого HTTP-сервера запустіть Inspector без команди, додайте сервер із транспортом Streamable HTTP і https://mcp.example.com/mcp, укажіть заголовок Authorization у його налаштуваннях і підключіться. Це найшвидший спосіб перевірити автентифікацію та проксі до підключення будь-якого agent. Inspector зберігає заголовки у своєму каталозі серверів у незмінному вигляді, тому цей токен тепер також міститься в ~/.mcp-inspector/mcp.json.

Оновлення серверів

MCP швидко розвивається, тому встановлюйте виправлення за розкладом. Node-сервери, запущені за допомогою неприбитого до версії npx -y, використовують версію, яку визначає npx: версію, встановлену в локальному проєкті, якщо вона є, або копію, яку npx встановлює в npm cache. -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. Перед переходом на major-версію прочитайте нотатки до релізу: перехід із Python SDK 1.x на 2.x спричиняє помилки під час імпорту коду для 1.x, а власний посібник SDK з міграції радить тимчасово встановити верхню межу <2, наприклад "mcp[cli]<2", доки міграцію не буде завершено. Під час оновлення перевіряйте ревізію специфікації, на яку орієнтується ваш SDK. Перехід через межу між SSE і 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; (і решту блоку з Step 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, вашого cloud provider або середовища, вона натомість читається як порожня, і nginx отримує Bearer без значення. Виведіть значення змінної, додайте заголовок повторно та перевірте, що точна послідовність байтів збігається з токеном у 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?

Це програма, яка через Model Context Protocol надає AI-клієнту доступ до tools і resources, використовуючи JSON-RPC 2.0. AI-модель не запускає tool самостійно. Вона надсилає запит своєму клієнту, клієнт викликає MCP server, а server виконує операцію та повертає результат. Оскільки протокол стандартизований, один server працює з будь-яким сумісним клієнтом, зокрема Claude Code, Claude Desktop або Gemini CLI.

У чому різниця між transport через stdio та HTTP?

Stdio server запускається клієнтом як дочірній процес і обмінюється даними через stdin/stdout. Тому він працює лише з одним клієнтом на одній машині та не потребує мережі або authentication. HTTP server — це довготривалий мережевий сервіс, до якого одночасно можуть підключатися багато клієнтів. Саме тому для нього потрібні TLS та authentication. Використовуйте stdio для локальних tools одного користувача, а HTTP (Streamable HTTP на поточних servers) — для спільних або постійно доступних tools.

Як захистити віддалений MCP server?

Вважайте, що він надає доступ до ваших файлів, database або shell, і ніколи не відкривайте його без authentication. Найкраще прив’язати його до localhost і підключатися через SSH tunnel або приватну VPN. Якщо server має бути доступним з Internet, розмістіть його за reverse proxy, який перевіряє bearer token або використовує MCP OAuth flow. Створіть token за допомогою openssl rand -hex 32 і ніколи не прив’язуйте server до 0.0.0.0 без одного з цих механізмів перед ним.

Як налагодити server, який не запускається?

Спочатку перевірте claude mcp list, ✘ Failed to connect за допомогою spawn ... ENOENT. Це означає, що command або runtime відсутній, тому виправте path або встановіть потрібний компонент. Якщо server підключається, а потім завершує роботу з помилкою розбору JSON, він записує журнали у stdout і пошкоджує потік JSON-RPC. Перенаправте все журналювання у stderr. В інших випадках виконайте точну command за допомогою MCP Inspector. Він запускає server ізольовано, щоб можна було відрізнити помилку server від помилки конфігурації клієнта.