MCP-сервер: частые ошибки и решения
MCP-сервер устроен обманчиво просто: процесс, который отвечает по JSON-RPC. Простота кончается, когда клиент отваливается с Connection closed через секунду после старта, каждый запрос возвращает 406 Not Acceptable, а стрим обрывается ровно на шестидесятой секунде. Разберём частые ошибки MCP-сервера по слоям — от лишней строки в stdout до перехода на stateless-спеку 2026-07-28 — с текстами ошибок и рабочими конфигами.
Содержание
- Сначала определите слой, потом чините
- stdio: Connection closed и мусор в stdout
- Streamable HTTP: 406, 404 и забытые заголовки
- Спека 2026-07-28: что ломается при переходе на stateless
- Соединение рвётся: Nginx, буферизация и таймауты
- 401, 403 и MCP, открытый всему интернету
- Какой сервер под MCP брать в MAATRIX
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество 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. Лечение: весь лог в stderr — logging.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 closed — PATH: клиенты с 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 — десятки моделей в одном окне. Оплата картой РФ и по СБП.