feat(mcp): внятная диагностика ошибок тулов — серверное сообщение вместо «Request failed with status code N» + fail-fast валидация id (вкл. #436) #437
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Объединяет #436 (fail-fast по обрезанному UUID) и общий механизм диагностики. Это полная спека решения.
1. Проблема
Модель получает от упавших тулов бесполезные тексты и не может самокорректироваться → слепые ретраи → петли (инцидент 2026-07-10: 24/38 упавших
createCommentза один ран, флуд дублей комментариев).Реальные кейсы:
resolve_commentс обрезанным UUID019f499a-9f8c→ сервер режет на@IsUUID()→ модель видит «Request failed with status code 400»;getPageнесуществующей страницы) → «Request failed with status code 404» вместо «Page not found»;2. Как ошибка доходит до модели (исследовано, оба транспорта)
Текст ошибки, который видит модель, — это всегда
error.messageброшенного исключения:packages/mcp/src/index.ts): throw из execute → MCP SDK превращает вisError-ответ сerror.message;tool-errorpart →normalizeToolError()(apps/server/src/core/ai-chat/ai-chat.service.ts~1760) берёт только.message; после #407 этот же текст персистится и реплеится.⇒ Единственная авторитетная точка, где рождается текст, —
DocmostClient(packages/mcp/src/client.ts) иlib/collaboration.ts. Чиним там — чинится везде. Слои index.ts / ai-chat-tools.service / normalizeToolError НЕ трогаем.3. Таксономия ошибок клиента (инвентаризация)
this.client.post/get/…mutatePageContent/CollabSession: connect/persist timeout, connection closed, auth failedcreate_comment,edit_page_textfailed[], Yjs-encode (unstorableYjsErrorуже называет offending attribute)downloadImage/upload (отдельный bare-axios инстанс, тело скрыто сознательно)4. Проектное решение
Фаза A — центральный axios-интерцептор диагностики
Один response-интерцептор на
this.clientв конструктореDocmostClient, зарегистрированный после re-login-интерцептора.Ключевое решение: НЕ подменять объект ошибки, а мутировать
error.message(+ флагerror._docmostFormattedот повторной обработки). Причина: по клиенту есть живые проверкиaxios.isAxiosError(e)/error.response?.status(~строки 362, 389, 1245, 3042–3054 — ensureAuthenticated, ретраи, upload re-login) иconfig._retry; подмена на кастомный класс их сломает, мутация — нет.Формат сообщения:
пример:
POST /comments/resolve failed (400 Bad Request): commentId must be a UUIDПравила сборки
serverMessage:data.message: массив (class-validator) → join через'; '; строка → как есть;data.error, затемstatusText;data— Buffer/ArrayBuffer (нашfetchInternalFileходит сresponseType:"arraybuffer") → guardedJSON.parse(buf.toString())c кэпом размера, при неудаче — statusText;data— строка/HTML (502 от прокси) → в сообщение НЕ включать, только statusText;path— без хоста и query;error.response(ECONNREFUSED/ETIMEDOUT/ECONNRESET) →<METHOD> <path> failed: <error.code ?? error.message> (no response from server).Безопасность: сообщение собирается только из
message/error/statusText— сырое тело, заголовки (Authorization!) и config в текст не попадают никогда; полное тело — только в stderr подDEBUG(паритет с image-путями).Взаимодействие с re-login: интерцепторы axios выполняются в порядке регистрации; ретрай re-login создаёт новый запрос → новый AxiosError → формируется заново, двойной обработки нет (флаг — страховка).
Фаза B — fail-fast валидация id (поглощает #436)
Хелпер в client.ts:
Текст:
resolve_comment: '<param>' must be the FULL comment UUID (36 chars, e.g. 019f499a-9f8c-7d68-b7be-ce100d7c6c56), got '<value>'. Copy the id verbatim from list_comments / create_comment output.Точки применения (все
commentId-параметры):resolveComment,updateComment,deleteComment,getComment,createComment.parentCommentId.pageIdНЕ валидируем — он легитимно принимает slugId (resolvePageId).Фаза C — обогащение collab-ошибок
В
lib/collaboration.ts(+ CollabSession после #431) добавить в тексты connect-timeout / persist-timeout / connection-closed:pageIdи короткую подсказку (transient — retry once; persistent failures mean the collab server is unreachable/overloaded). Yjs-encode не трогаем — уже хорош.5. Порядок работ
6. Тесты
Раннер
node --test(packages/mcp/test/unit/*.test.mjs, образец —auth-cookie.test.mjs), новыйerror-diagnostics.test.mjs:error; пустое тело → statusText;code+ путь + «no response»;_retry-логика не сломана;assertFullUuid: валидный UUID проходит; префикс/мусор/пустая строка — падают с ожидаемым текстом; все 5 точек применения покрыты (без сетевого вызова — мок не должен быть вызван).7. Критерии приёмки
commentIdв любом comment-туле → ошибка с текстом «FULL comment UUID» без сетевого запроса.message(проверка руками:resolve_commentс мусорным id после обхода фазы B;get_commentс несуществующим UUID → «Comment not found»).downloadImage/upload — поведение не изменилось (bare axios, вне интерцептора).8. Проверка эффекта после деплоя
SQL из
docs/reading-ai-logs.md(hard-error query поelem ? 'error'): полеerrorдолжно содержать реальные тексты формата<METHOD> <path> failed (…): …; доля заглушкиTool call did not complete.должна упасть до чистых абортов. Плюс глазами: топ error-текстов по тулам за первые дни.Контекст
Найдено при разборе инцидента «агенты в петле» 2026-07-10 (флуд дублей комментариев). Связка: #407 (персист tool-error — канал доставки текста, который это ишью наполняет смыслом), #427 (подсказки якоря), #431 (CollabSession). Поглощает #436.
feat(mcp): внятная диагностика ошибок REST для всех тулов — извлекать серверное сообщение вместо «Request failed with status code N»to feat(mcp): внятная диагностика ошибок тулов — серверное сообщение вместо «Request failed with status code N» + fail-fast валидация id (вкл. #436)