MAATRIX / Блог / MCP-сервер: частые ошибки и решения

MCP-сервер: частые ошибки и решения

MCP-сервер: частые ошибки и решения

MAATRIX

MCP-сервер устроен обманчиво просто: процесс, который отвечает по JSON-RPC. Простота кончается, когда клиент отваливается с Connection closed через секунду после старта, каждый запрос возвращает 406 Not Acceptable, а стрим обрывается ровно на шестидесятой секунде. Разберём частые ошибки MCP-сервера по слоям — от лишней строки в stdout до перехода на stateless-спеку 2026-07-28 — с текстами ошибок и рабочими конфигами.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →

Сначала определите слой, потом чините

Больше всего времени на отладке MCP уходит не на тот слой: слоёв четыре — процесс, транспорт, протокол, авторизация, и текст ошибки сразу называет нужный.

Транспорта два. stdio — клиент сам порождает процесс и говорит через stdin/stdout, без сокета и порта. Streamable HTTP — один эндпоинт, обычно /mcp, для удалённых серверов; предшественник HTTP+SSE (ревизия 2024-11-05, адреса /sse и /messages) устарел в 2025-03-26, но живых серверов хватает — половина «непонятных 404» отсюда.

Что видитеСлойКуда смотреть
MCP error -32000: Connection closedпроцесскоманда запуска, PATH, права
Unexpected token 'I', "INFO Sta"... is not valid JSONпроцесспосторонний вывод в stdout
406 Not Acceptableтранспортзаголовок Accept у клиента
404 Not Found / 405 Method Not Allowedтранспортпуть эндпоинта и метод
400 Bad Request: Missing session IDпротоколнесовпадение ревизий спеки
403 Forbiddenтранспортзащита от DNS rebinding, Host
401 Unauthorizedавторизациятокен, аудитория, метаданные

Проверка — один curl для актуальной ревизии 2026-07-28:

curl -i -X POST https://mcp.example.com/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H 'MCP-Protocol-Version: 2026-07-28' \
  -H 'Mcp-Method: tools/list' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'

Пришло result.tools — сервер жив, версия совпала, дальше виноват клиент или прокси; другой код называет слой по таблице. На ревизиях 2025-06-18 и 2025-11-25 порядок другой: сначала initialize, из ответа берёте Mcp-Session-Id.

stdio: Connection closed и мусор в stdout

У stdio одно жёсткое правило, и нарушают его чаще всего: stdout принадлежит исключительно JSON-RPC, по одному сообщению в строке. Любой посторонний байт — баннер, версия, цветной лог — ломает разбор:

SyntaxError: Unexpected token 'E', "Error: typ"... is not valid JSON

Источник почти всегда один: забытый print() в Python, console.log() в Node, логгер с выводом в stdout по умолчанию, вывод сборщика при mvn spring-boot:run или npm run dev. Лечение: весь лог в stderrlogging.basicConfig(stream=sys.stderr) в Python, console.error() в Node. Проверяется без клиента:

echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' \
  | /opt/mcp/venv/bin/python -m my_server 2>/dev/null

На выходе — ровно одна строка валидного JSON; баннер или пустота — вот и ошибка.

Вторая причина Connection closedPATH: клиенты с GUI не наследуют окружение шелла, в конфиге написан npx, а процесс стартует с урезанным PATH без /opt/homebrew/bin и каталогов nvm. В журнале — spawn npx ENOENT. Пишите абсолютные пути (command -v node) либо задавайте PATH блоком env.

Где смотреть: на macOS конфиг — ~/Library/Application Support/Claude/claude_desktop_config.json, stderr сервера — ~/Library/Logs/Claude/mcp-server-<имя>.log; статус в консоли — claude mcp list и /mcp, подробности — claude --mcp-debug.

Долгий старт — отдельная ловушка: MCP_TIMEOUT управляет ожиданием рукопожатия, но не запуском процесса, и значения больше 60 секунд в части сборок игнорируются (issue #16837 у claude-code). Честное ограничение: stdio не масштабируется — один клиент, один процесс, никакой сети.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Streamable HTTP: 406, 404 и забытые заголовки

406 Not Acceptable на ровном месте — самая обидная ошибка: виноват обычно не сервер. Спека требует, чтобы POST нёс Accept: application/json, text/event-stream — оба типа сразу; сервер, получив только application/json, отвечает 406. Клиенты присылают неполный заголовок регулярно — это ловили в Claude Code (issue #45368), в claude-agent-sdk-typescript (issue #202) и в opencode. Смотрите, что реально приходит:

curl -s -o /dev/null -w '%{http_code}\n' -X POST http://127.0.0.1:8000/mcp \
  -H 'Content-Type: application/json' -H 'Accept: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'

Прагматичное решение — сделать сервер терпимым к одиночному application/json. Костыль на прокси — дописать заголовок за клиента:

location /mcp {
    proxy_set_header Accept "application/json, text/event-stream";
    proxy_pass http://127.0.0.1:8000;
}

404 и 405 — почти всегда путь: клиент настроен на /sse старой схемы, сервер отдаёт по /mcp; либо приложение примонтировано под префиксом, а в конфиге остался /mcp (в Python SDK путь задаёт streamable_http_path, по умолчанию /mcp). Неочевидный вариант — слэш на конце: /mcp при смонтированном /mcp/ даёт от Starlette редирект 307, часть клиентов теряет тело POST, и вы видите пустой ответ. Проверяйте перебором:

for p in /mcp /mcp/ /sse /api/mcp; do
  printf '%s -> ' "$p"
  curl -s -o /dev/null -w '%{http_code}\n' -X POST "http://127.0.0.1:8000$p" \
    -H 'Content-Type: application/json' \
    -H 'Accept: application/json, text/event-stream' -d '{}'
done

Ещё две мелочи: 415 — забытый Content-Type: application/json, а MCP-Protocol-Version обязателен на всех запросах с ревизии 2025-06-18 — без него сервер считает клиента старым, отсюда «то работает, то нет» после обновления.

Спека 2026-07-28: что ломается при переходе на stateless

Ревизия 2026-07-28 — крупнейшее изменение транспорта за историю протокола: MCP стал stateless на уровне протокола. Заголовок Mcp-Session-Id не устарел, а удалён — вместе с рукопожатием initialize. Каждый запрос самодостаточен: версия протокола и возможности клиента едут в _meta тела вызова.

Появились обязательные заголовки (SEP-2243): Mcp-Method на каждом запросе и Mcp-Name — на тех, что называют цель (tools/call, resources/read, prompts/get), чтобы балансировщик маршрутизировал, не разбирая тело. Сервер обязан отвергать запрос, если заголовок расходится с теломcurl с Mcp-Method: tools/list и телом tools/call вернёт 400, и это не баг.

Рассинхрон версий вживую:

  • Новый клиент — старый сервер. Клиент не шлёт initialize: 400 Bad Request: Missing session ID либо -32600 Invalid Request.
  • Старый клиент — новый сервер. Клиент ищет Mcp-Session-Id в ответе, не находит и падает на следующем запросе — в логах сервера код 200.
  • Прокси посередине. Шлюз срезает нестандартный Mcp-Method, новый сервер отвечает 400 на всё при исправных концах.

Три следствия для кода: состояние переезжает в аргументы — курсор выборки, контекст авторизации становятся дескриптором, который передаёт клиент; списки не зависят от соединенияtools/list, resources/list, prompts/list одинаковы для всех; липкие сессии не нужныip_hash или Redis под сессии можно убирать.

Честный минус: stateless перекладывает состояние на вас. Если инструмент держит SSH-сессию или транзакцию, таблицу дескрипторов и TTL теперь ведёте сами.

Соединение рвётся: Nginx, буферизация и таймауты

Классическая жалоба: «на 127.0.0.1 летает, через домен рвётся» — или ответ приходит целиком в конце, а не течёт по мере генерации. Причина — настройки Nginx по умолчанию: буферизация ответа и proxy_read_timeout в 60 секунд. Отсюда обрывы с точностью до секунды и 504 Gateway Time-out на длинных вызовах.

location /mcp {
    proxy_pass http://127.0.0.1:8000;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;

    proxy_buffering off;
    proxy_request_buffering off;
    proxy_cache off;
    chunked_transfer_encoding on;

    proxy_read_timeout 600s;
    proxy_send_timeout 600s;
}

proxy_http_version 1.1 с пустым Connection обязателен: без них Nginx уходит на бэкенд по HTTP/1.0, и стрим невозможен. Если конфиг менять нельзя, proxy_buffering off даёт заголовок X-Accel-Buffering: no из приложения. Отдельная засада — Nginx выбрасывает заголовки с подчёркиванием: для x_api_key нужен underscores_in_headers on;. Через Cloudflare — потолок 100 секунд на бесплатных тарифах, длинный вызов вернёт error 524.

Третий источник обрывов — сам сервис: запуск «в screen» не переживает перезагрузку.

[Unit]
Description=MCP server
After=network-online.target

[Service]
User=mcp
WorkingDirectory=/opt/mcp
EnvironmentFile=/etc/mcp/mcp.env
ExecStart=/opt/mcp/venv/bin/python -m my_server --host 127.0.0.1 --port 8000
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target

journalctl -u mcp -f покажет Main process exited, code=killed, status=9/KILL — вас убил OOM-killer, подтверждает dmesg -T | grep -i oom. Не пропускайте WorkingDirectory: без него systemd стартует из /, где нет .env. Отсюда вечное «руками работает, сервисом нет».

401, 403 и MCP, открытый всему интернету

С ревизии 2025-06-18 MCP-сервер формально считается OAuth 2.0 Resource Server, и большинство ошибок авторизации — сломанное обнаружение метаданных, а не «неверный пароль». Голый 401 без WWW-Authenticate — тупик: неоткуда узнать, куда идти за токеном:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token",
  error_description="Authentication required",
  resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource"

Проверка: curl -i https://mcp.example.com/mcp должен показать заголовок, а curl -s https://mcp.example.com/.well-known/oauth-protected-resource | jq — вернуть документ RFC 9728 с полями resource и authorization_servers.

«Авторизация прошла, а сервер всё равно 401» — почти всегда аудитория токена: по RFC 8707 клиент передаёт resource, сервер авторизации кладёт его в токен, а ваш сервер сверяет с собственным адресом. Расходится что угодно: https://mcp.example.com против .../mcp, слэш в issuer, а чаще схема — за прокси видно http, а клиент получил токен на https. Лечится X-Forwarded-Proto и явным публичным URL вместо автоопределения.

403 — обычно защита от DNS rebinding: SDK по умолчанию пускают запрос, только если Host равен 127.0.0.1:<порт>, localhost:<порт> или [::1]:<порт>. Поставили сервер за Nginx с доменом — 403 на всё, пока не добавите хост в разрешённые.

Честная часть про безопасность:

  • CVE-2025-66416. Python SDK mcp до 1.23.0 не включал защиту от DNS rebinding по умолчанию — страница в браузере жертвы могла вызвать инструменты локального сервера. Парный CVE-2025-66414 — у соседнего SDK. Обновление — pip install -U "mcp>=1.23.0".
  • CVE-2025-49596. MCP Inspector до 0.14.1 держал прокси без аутентификации — RCE с оценкой 9.4 по CVSS. В 0.14.1 добавили session-токен и проверку Origin; запускайте как npx @modelcontextprotocol/inspector@latest, не привязывайте порт 6277 к 0.0.0.0.
  • Фаервол. Наружу только TLS: ufw allow 22/tcp, ufw allow 443/tcp, ufw deny 8000/tcp.

Открытый эндпоинт без авторизации хуже открытого API: это API, спроектированный для управления моделью.

Какой сервер под MCP брать в MAATRIX

MCP-сервер — тонкая прослойка: он почти не считает, он ждёт ввода-вывода. Нагрузку создают инструменты за ним, поэтому конфигурацию выбирают по самому тяжёлому из них, а не по числу подключений.

Честно про установку: готовой сборки MCP-сервера в каталоге apps.maatrix.io нет — слишком разный класс инструментов для одной кнопки. Сервер приходит чистым (Ubuntu 24.04 или Debian 12), и вы ставите своё сами — пошагово разобрано в статье «Как поднять MCP-сервер на VPS». В каталоге есть готовые сборки родственных сервисов, которые часто стоят рядом с MCP — n8n, Dify, LiteLLM, AnythingLLM, Qdrant, Portainer: разворачиваются автоматически при заказе, и MCP-сервер вы добавляете к уже работающему стеку.

Минимум: 1 vCPU, 2 ГБ RAM, 20 ГБ NVMe. Хватает на один-два лёгких сервера, Nginx и certbot. Гигабайт брать не советую: npm ci для TypeScript-сервера съедает сотни мегабайт разом, и установка зависимостей закончится тем самым status=9/KILL. Сколько ест ваш процесс — systemd-cgtop и ps -o rss=,comm= -C node.

Комфорт: 2 vCPU, 4 ГБ RAM, 40–60 ГБ NVMe. Под несколько серверов за одним Nginx, Docker, Postgres под дескрипторы после перехода на stateless. Если есть headless-браузер — планка выше: Chromium под нагрузкой легко уходит за гигабайт на вкладку, это уже 8 ГБ.

Локация — Великобритания, Лондон. Площадку выбирают по тому, куда ходят инструменты, а не по пингу протокола. С британского адреса зарубежные API отвечают штатно, без региональных отказов, которые ловит российский IP; RTT из Москвы и Европы — несколько десятков миллисекунд, короче, чем до Северной Америки; и это юрисдикция рядом с GDPR — подробнее в статье про аренду VPS для доступа к нейросетям в Великобритании. Российская площадка — когда данные подпадают под 152-ФЗ; американская — когда инструменты завязаны на SaaS из США.

Оплата — картой российского банка, через СБП, криптовалютой или токеном MAAT: зарубежная карта не нужна. Порядок после выдачи доступа: закрыть лишнее в ufw, поднять сервер на 127.0.0.1, выпустить сертификат — и публиковать эндпоинт наружу только после того, как заработает авторизация.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →

Частые вопросы

Сервер отвечает на curl с localhost, но клиент пишет «Failed to connect». С чего начать?

Разделите слой. Для stdio запустите команду из конфига руками в шелле: обычно spawn ... ENOENT из-за PATH или посторонний вывод в stdout. Для HTTP сравните коды: 404/405 — неверный путь, 406 — неполный Accept, 401/403 — авторизация и Origin.

Нужно ли срочно переходить на спеку 2026-07-28?

Не срочно, если сервер обслуживает известный набор клиентов и вы контролируете обе стороны. Но откладывать надолго не стоит: новые клиенты не шлют initialize и не понимают Mcp-Session-Id. Практичный путь — обновить SDK и поднять новый эндпоинт рядом со старым.

Можно ли держать MCP-сервер на том же VPS, что и сайт?

Технически да — процесс на локальном порту за тем же Nginx. Разумно при двух условиях: отдельный системный пользователь с отдельным location и авторизацией — инструменты выполняют действия, а не отдают страницы, — и запас памяти, иначе тяжёлый вызов положит по OOM веб-сервер целиком.

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.