Как я писал MCP-сервер. Часть 2: что сломалось и чему научило
Три отказа в том порядке, в котором я их нашёл, и проверка, оставшаяся после каждого
Написано 2 сентября 2026 года. Три отказа, в том порядке, в котором я их нашёл. Третий на момент начала этого текста всё ещё был в коде — и только поэтому он здесь: разбор, который пишешь про себя, легко закруглить, а этот не закругляется.

Часть 2 серии: что сломалось
Первая часть была про то, зачем этот сервер существует. Эта — про три способа, которыми он ошибался, и про то, что все три — один и тот же способ.
Первое: вкладка, закрывшаяся слишком рано
Первое: вкладка, закрывшаяся слишком рано
Полный разбор этого — с таблицей аудита и со скриптом — отдельная статья, так что здесь коротко и только та часть, которая относится к серверу, а не к инциденту.
Адаптер Medium ждал смены адреса в браузере на адрес редактирования нового черновика, считал дело сделанным и закрывал вкладку в блоке finally. Medium в это время досохранял тело несколькими фоновыми запросами. 25 июля 2026 года аудит всех двадцати пяти записей календаря с адресом публикации нашёл двенадцать живых постов, обрезанных до 41–72% отправленного текста, у части пропали блоки кода; ещё один адрес отдавал HTTP 410. Все они вернули валидный URL и ok.
Сюда относится то, что это сделало с устройством. До этого у сервера была одна проверка: предпубликационная, по тексту, до того как он куда-то уедет. После — три, и три их потому, что каждая отвечает на вопрос, на который две другие не отвечают.
- До публикации, по площадке и по типу записи: переживёт ли этот текст адаптер вообще. Таблицы, незакрытые фенсы, H1, дублирующий поле заголовка.
- После публикации, доступность и активность: жив ли адрес, набирает ли просмотры. Это та самая проверка, которая существовала и неделями мне врала.
- После публикации, полнота: совпадает ли то, что сейчас лежит на сервере, с тем, что я отправлял. Вот этой не было.
Три проверки — это не тщательность. Это признание, что проверка отвечает только на тот вопрос, который задаёт, а я обращался с одним вопросом так, будто он покрывает три.
Второе: детектор, который читал публикуемую статью
Второе: детектор, который читал публикуемую статью
Адаптеру нужно было ещё замечать, когда редактор Medium не сохранил, и он искал на странице фразы, которые Medium показывает при ошибке, — одна из них «something is wrong». Искал он их по тексту всей страницы, а на этой странице лежит и публикуемая статья.
В одной из статей была строчка «if something is wrong you flip back».
Безупречно отработавший прогон объявили провалившимся. Отработал повтор. Появился дубль черновика. Починка маленькая — смотреть только на элементы, которыми страница объявляет статус: [role=alert], [role=status], [aria-live], — а оставшееся после неё правило не маленькое: никогда не вычитывайте статус из прозы. Если единственный способ узнать исход — поискать слова в связном тексте, вы его угадали. Проза рано или поздно случайно будет содержать ваше ключевое слово, и выберет для этого худший момент — тот, когда проза как раз про то, что вы проверяете.
Третье: ограждение, которое говорит «ok» про площадки, о которых не слышало
Третье: ограждение, которое говорит «ok» про площадки, о которых не слышало
Это то, что мне публиковать неуютнее всего, и поэтому это стоит опубликовать.
Предпубликационная проверка ветвится по площадке. У Medium свои правила, у dev.to свои, у Telegram, Hacker News и сайтов Stack Exchange — свои. Всё остальное проваливается в ветку по умолчанию, которая записывает информационную заметку: для этой площадки проверяльщика нет. Итоговый вердикт вычисляется как «ошибок уровня error не найдено», а информационная заметка не ошибка — и запись проходит зелёной.
Перечитайте это, держа в голове весь остальной цикл. Моё ограждение против инструментов, которые отчитываются об успехе, не сделав работы, отчитывается об успехе, не сделав никакой работы, всякий раз, когда встречает незнакомую площадку.
Это не гипотеза. Кампания, которая опубликовала этот самый цикл, идёт по площадкам, у которых проверяльщика нет вообще: посты в X, пост в LinkedIn, рассылка и записи блога, которые являются чек-листами, а не публикациями. Каждая из них проходит предпубликационную проверку с чистой справкой, и чистая справка не значит ровным счётом ничего. А если я добавлю площадку — план этого цикла назвал одну, русскоязычный сайт, для которого нет ни адаптера, ни проверяльщика, и слова о котором нет нигде в кодовой базе, — проверка пропустит её, а публикация упадёт.
Починка в две строки: сделать заметку по умолчанию предупреждением, а не информацией. Предупреждение видно и оно не блокирует. Это правильная строгость для «я понятия не имею, отрисуется ли это», а информацией она была потому, что ветку по умолчанию я написал раньше, чем ей стало что ловить, и не вернулся.
Есть и вторая версия той же формы, и она ведёт себя правильно — тем и поучительна. Запись с пустым телом даёт ошибку, а не заметку, и публикатор отказывается наотрез. Все двадцать записей этой кампании попали в календарь с пустым телом — то есть каждой из них проверка откажет, пока я не напишу текст. Это проверяльщик, делающий ровно свою работу, — и одновременно точный замер того, какая доля того, что я называю «планированием», состоит из заголовка, даты и ненаписанной статьи.
Что знает проверяльщик и почему каждое его правило — шрам
Что знает проверяльщик и почему каждое его правило — шрам
Предпубликационная проверка — зеркало публикующего диспетчера: для каждой площадки она знает, что этот путь публикации способен отрисовать. Прочитать её правила — самая быстрая экскурсия по площадкам, какая у меня есть.
У Medium в редакторе нет блока таблицы, поэтому markdown-таблица рисуется голыми пайпами; проверка такую отклоняет. Отклоняет она и незакрытый фенс, и ведущий H1, повторяющий поле заголовка, — потому что отдельного поля заголовка у Medium нет вовсе: первая строка редактора и есть заголовок. Этот факт меня уже кусал и укусит вас один раз.
Telegram получает простой текст, потому что адаптер отправляет его без режима разметки: любые звёздочки жирного, обратные кавычки, ссылки, заголовки и таблицы приедут в сообщение дословно. Плюс потолок в 4096 знаков — обрезание, которое ждёт своего часа, если никто не считает.
Hacker News не отрисовывает markdown вообще, а у заголовка там потолок в 80 знаков, который площадка иначе обрежет молча. «Молча» здесь ключевое: это готовый отказ третьей ступени с валидным адресом, и не случился он у меня только потому, что проверка считает знаки.
dev.to отрисовывает GFM, так что таблицы и списки в порядке, и правил там меньше: незакрытые фенсы, H1, дублирующий заголовок, пустое тело.
Сайты Stack Exchange не хотят повтора заголовка первой строкой тела, требуют адрес родительского вопроса для ответа и настоящих тегов у вопроса вместо заглушки, которую оставляет шаблон записи, — и наотрез отказываются автоматизировать комментарий к чужому ответу: у него нет фиксированной цели, и это не то, что машина должна писать от моего имени.
Ни одно из этих правил не спроектировано. Каждое — шрам.
Перезаливка — отдельная лотерея
Перезаливка — отдельная лотерея
Двенадцать обрезанных постов надо было перезалить, и про то, как это идёт, честнее рассказать, чем закончить на починке.
Перезаливка истории заново играет в ту же гонку автосейва, которая её и обрезала, так что ремонтный прогон может оставить пост хуже, чем нашёл. Ремонтный скрипт печатает FAIL, когда его финальная проверка уходит в таймаут, — в том числе тогда, когда сохранение прошло; значит, верить надо не коду возврата, а сверке полноты. Истории с блоком рассылки скрипт исключает совсем, потому что замена тела ломает этот блок: только руками. А правка опубликованной истории у части статей уходит в живой пост сразу, у части остаётся неопубликованной ревизией, — поэтому после каждого ремонта надо снова прогонять анонимный режим и смотреть, что на самом деле получает посторонний.
Общий вывод неприятный: цена тихого отказа — не проваленный прогон. Цена в том, что путь ремонта никто не проектировал, потому что путь ремонта не проектируют для отказа, который считают невозможным.
Двадцать инструментов и честная арифметика
Двадцать инструментов и честная арифметика
Я советую пятнадцать, а держу двадцать, и после всего вышеизложенного могу точнее сказать, почему это важно здесь, а не вообще. Каждый инструмент — это описание, оплачиваемое в каждом запросе, и каждое описание — шанс, что модель выберет из списка не тот пункт. Но конкретная цена в этом сервере другая: пять инструментов из двадцати относятся к совершенно иной предметной области, заползшей сюда потому, что этот сервер уже был подключён, и у этих пяти нет ни проверяльщика, ни адаптера, ни места в публикующем процессе. Они не дорогие. Они шум в каталоге, который модель читает, прежде чем решить, что делать со статьёй.
Что я сделал бы иначе
Что я сделал бы иначе
Сначала написал бы проверку полноты. Не адаптер, не описания инструментов — ту штуку, которая вычитывает результат обратно и сверяет с отправленным. В версии, которая считает блоки кода и ищет текст последней ссылки — а моя только это и делает, — это двадцать минут работы, и она поймала бы все отказы из этой статьи, кроме третьего.
А для третьего: в любом ветвлении по имени делайте ветку по умолчанию предупреждением, а не тишиной. Весь цикл — про инструменты, отчитывающиеся об успехе, за который они не отвечают. Самое вероятное место, где это заведётся в вашем собственном коде, — ветка, написанная для случаев, о которых вы ещё не подумали.
Первая часть — зачем я его писал. Инцидент с обрезанными публикациями целиком, с таблицей аудита и скриптом проверки полноты, — здесь. Про сам протокол: откуда взялись MCP и REST, сколько MCP стоит в токенах, из чего сделан MCP-сервер.
Источники
Источники
Мой собственный MCP-сервер календаря продвижения: публикующий диспетчер, проверяльщики, README и рунбук docs/publishing.md, составленный 25 июля 2026 года. Аудит всех 25 записей календаря с адресом публикации, проведённый в тот же день. scripts/check-published-completeness.mjs, scripts/check-published-health.mjs и скрипт правки историй Medium. Спецификация MCP — modelcontextprotocol.io, редакции 2025-06-18 и 2026-07-28.