Skip to main content
Glama
conarti

yandex-messenger-mcp

by conarti

yandex-messenger-mcp

MCP-сервер для Яндекс Мессенджера. Даёт агенту читать переписку, искать по ней, скачивать вложения и отправлять текст от вашего имени.

Работает на той же сессии, что и веб-клиент: Playwright один раз логинится в Яндекс и держит persist-профиль, дальше протокол (WebSocket + HTTP) гоняется в Node на извлечённых cookie. Публичного API у Мессенджера нет, поэтому протокол снят реверсом веб-клиента chats-web - отсюда честный раздел Ограничения внизу, его стоит прочитать до того, как полагаться на инструмент.

Инструменты

Инструмент

Что делает

list_chats

Список чатов: последнее сообщение, счётчик непрочитанных, свежие первыми

get_history

Страница сообщений чата с пагинацией по курсору

get_message

Одно сообщение по chat+message_id либо по join-ссылке, без загрузки истории

get_message_context

Окно сообщений вокруг метки: N сообщений до и N после

search

Поиск по сообщениям, людям и чатам

send_message

Отправка текста, с упоминаниями/ответом/пересылкой. Двухшаговая: draft, затем confirm

send_file

Отправка картинки или файла. Двухшаговая: draft (байты не льются), затем confirm (необратимо)

set_reaction

Поставить/снять реакцию. Одним вызовом, без confirm (реверсибельно)

list_reactions

Полный список поставивших реакцию и прочитавших (кто и когда), без обрезки

mark_read

Отметить чат прочитанным. Одним вызовом, без confirm

pin_message

Закрепить/открепить сообщение. Одним вызовом, без confirm

delete_message

Удаление своего сообщения. Двухшаговая: draft, затем confirm (необратимо)

edit_message

Правка своего сообщения. Двухшаговая: draft, затем confirm (необратимо)

get_poll

Чтение опроса: варианты, мой выбор, результаты. Одним вызовом, без confirm

vote_in_poll

Голос в опросе. Двухшаговая: draft, затем confirm. Форма подтверждена живьём

get_thread

Сообщения треда как микро-чата (по thread_id либо родительскому сообщению)

join_to_thread / leave_thread

Подписка на тред и выход. Одним вызовом, без confirm

download_attachment

Скачивает вложение по рефу в папку загрузок

list_chats

Параметр

Тип

По умолчанию

limit

1-500

50

unread_only

bool

false

include_last_message_text

bool

false

Отдаёт {chats, total_chats, unread_chats}. У чата: chat_id, name, kind (private/group), last_activity, unread_count, unread, muted, last_message.

include_last_message_text:true возвращает полный текст последнего сообщения каждого чата. По умолчанию отдаются только метаданные последнего сообщения (без текста, цитат и имён файлов) - чтобы содержимое чужих переписок не попадало в контекст модели без явного запроса.

get_history

Параметр

Тип

По умолчанию

chat

ChatId либо поисковый запрос

обязателен

limit

1-200

40

before

курсор: мкс строкой

нет

from_date

ISO-дата/время, нижняя граница включающая

нет

to_date

ISO-дата/время, верхняя граница исключающая

нет

after

ISO-дата/время, строго после метки (альтернатива from_date)

нет

Если chat задан запросом и совпадений несколько, инструмент возвращает status:"ambiguous_chat" со списком кандидатов и не гадает. Пагинация: взять next_before из выдачи и передать его в before следующего вызова.

Фильтр по времени - альтернатива курсору. from_date включает сообщения от этой даты и позже, to_date исключает сообщение ровно на границе (верхняя граница исключающая), поэтому «сообщения за сегодня» берутся одним вызовом: from_date = начало дня, to_date = начало следующего. after - то же, что from_date, но строго после метки. При совпадении обеих форм одной границы курсор before приоритетнее to_date, а after приоритетнее from_date.

Self-чат «Избранное» резолвится по имени, а не только литеральным ChatId: резолвер узнаёт его по гейту <myGuid>_<myGuid> (пара одинаковых guid, PrivateChatInfo + PartnerInfo.Guid == myGuid) и больше не выбрасывает вас самих как собеседника. Резолв не-self чатов при этом не меняется.

Вложения приходят только рефами (file_id, name, size, kind). Ничего не качается - это отдельное явное действие через download_attachment.

Каждое сообщение помимо v1-полей несёт обогащение (аддитивно, старые ключи не меняются):

Ключ

Что несёт

from_me

автор == я; null, если From срезан фильтром (автор неопределим, это не «не моё»)

reads

прочтения: {tracked, count?, recent?, seen_by_partner_mcs?}. Отсутствие read-state = {tracked:false}, не ноль

mentions

упоминания в имена; нет имени для guid - явный {guid, unresolved:true}, а не guid вместо имени

reactions

реакции сгруппированы по типу: Reaction {type, name, emoji, count?, actors[], actors_complete}; неизвестный тип - {type, name:null, emoji:null, unknown:true}. actors[] - кто поставил ({guid?, name?, timestamp?, timestamp_mcs?}), здесь усечённый сиблинг history; actors_complete - сравнение count с длиной actors[], false значит обрезку. Полный список без обрезки - инструмент list_reactions

thread

признак has_thread + корень треда root

forwarded

оригинал пересылки: {source_author, source_chat, source_date, source_text, attachments} - отдельным ключом, не внутри context

Реакции: type - id артворка, emoji - аппроксимация. Яндекс рендерит реакции PNG-артворком по id (/reactions/{type}/{size}), а не Unicode-эмодзи. Карта type -> {name, emoji} лежит данными в src/config/reaction-map.json (52 записи, перегенерируется свипом по /reactions/{type}/small). Авторитетны type (пришёл с провода) и name (имя ассета с сервера); колонка emoji - наша аппроксимация артворка, её нельзя выдавать за «эмодзи от Яндекса». Пространство типов открыто (сервер принимает любой int), поэтому карта - lookup-с-фолбэком, а не полный справочник: неизвестный тип отдаётся как unknown, chr(type) в emoji не подставляется. Акторы и actors_complete в этом обогащении - усечённый сиблинг history; полный список поставивших/прочитавших (кто и когда), без обрезки, - инструмент list_reactions (двумя WS-вызовами: Mode - дискриминатор, дефолт -> UserReactions, Mode:1 -> UserReads+ReadsCount).

Форматирование - сырая строка. Протокол не даёт структурированных entities (ranges/spans): Text несёт ровно MessageText, markdown-символы едут как есть, разметку рисует клиент (проверено живьём, §17.14). text отдаётся без разбора; структуру форматирования тут не выдумываем.

get_message

Параметр

Тип

По умолчанию

chat

ChatId либо поисковый запрос; нужен вместе с message_id

-

message_id

timestamp сообщения в микросекундах (строка); нужен вместе с chat

-

url

join-ссылка Мессенджера (альтернатива паре chat+message_id)

-

with_reactions

bool: тянуть детальные реакции/прочтения (2 доп. WS-вызова)

true

Одно сообщение без загрузки истории (message_info, один WS-вызов). Адресация - либо парой chat+message_id (то же резолвение чата, что у get_history), либо готовой join-ссылкой (url): из неё резолвится и чат, и метка сообщения. Трёхсегментная ссылка (сообщение внутри треда) адресуется той же деривацией thread_id, что и get_thread; для бизнес-чата (2/...), где деривация недоступна, возвращается status:"thread_unsupported" с причиной, а не тихий отказ.

with_reactions по умолчанию true: для ОДНОГО сообщения два дополнительных вызова list_reactions дёшевы, поэтому reactions_detail (полные реакции и прочтения, без обрезки) приезжает вместе с сообщением. with_reactions:false возвращает только само сообщение с обычным обогащением (усечённые сиблинги reactions/reads, как в get_history).

Ответ также несёт my_reactions (мои реакции на сообщение; ключ пропадает, если своих реакций нет) и сырой chat_info (метаданные чата с провода, отдаются как есть).

get_message_context

Параметр

Тип

По умолчанию

chat

ChatId либо поисковый запрос

обязателен

message_id

timestamp целевого сообщения в микросекундах (строка)

обязателен

before

сколько сообщений ДО метки, 0-200

10

after

сколько сообщений ПОСЛЕ метки, 0-200

10

Окно вокруг конкретного сообщения - когда важен не курсор, а то, что было сказано непосредственно до и после метки. Отдельного «context»-метода на проводе нет: окно строится теми же границами history, что и get_history. Выдача несёт три ключа: before (от старых к новым), message (само сообщение по метке, если оно ещё живо - не удалено и не отфильтровано), after (от старых к новым).

Параметр

Тип

По умолчанию

query

строка

обязателен

entities

messages, users, chats

все три

limit

стартовый limit

50

Серверной пагинации у поиска нет (см. Ограничения), поэтому полнота достигается эскалацией limit: пока выдача насыщена, limit поднимается и запрос повторяется. Как это отработало, видно в ответе: escalation: {start_limit, final_limit, requests}. Если упёрлись в клиентский потолок, придёт truncated:true с причиной, а не молча обрезанный список.

Entity contacts невалиден и отвергается на входе.

send_message

Параметр

Тип

chat

ChatId либо поисковый запрос

text

текст сообщения

mentions

упоминания участников (опционально)

reply_to_message_id

timestamp сообщения-цели ответа, мкс строкой (опционально)

forward_from

timestamp пересылаемого сообщения, мкс строкой (опционально)

confirm

true - отправить; иначе draft

confirm_token

токен из draft, обязателен при confirm:true

Подробности двухшаговой отправки ниже: Двухшаговая отправка.

Упоминания (mentions). Двухшаговый контракт: на draft элементы mentions - это запросы (@Имя, @<guid> либо голый guid), каждый резолвится в guid участника; неоднозначность (несколько кандидатов) или отсутствие результата отклоняет весь вызов целиком, не только спорное упоминание. Ответ несёт mentions: [{guid, name?}] - уже резолвнутые. На confirm нужно предъявить РОВНО те же guid, в том же порядке, что вернул draft: изменение состава или порядка отклоняет отправку (порядок значим - он часть отпечатка нагрузки). Резолв идёт по каталогу организации (глобальный поиск людей), а НЕ по участникам конкретного чата: @Имя может найти не того тёзку. Для приватного чата есть бесплатная проверка принадлежности - оба guid собеседников лежат в самом chat_id (<guidA>_<guidB>), резолвнутый guid вне этой пары отклоняется (mention_not_in_chat). Для группового чата такой проверки нет (эндпоинта участников группы в протоколе не нашлось) - точный адрес гарантирует только явный @<guid>, а не поиск по имени.

Ответ (reply_to_message_id). Timestamp цели входит в отпечаток нагрузки - его изменение между draft и confirm отклонит отправку. Цитата цели (reply_quote в draft) перечитывается с сервера отдельным запросом и в отпечаток НЕ входит: правка текста цели между draft и confirm отправку не отклоняет, потому что цитата на confirm берётся заново с сервера, а не из draft. Цитата обрезается до 200 символов (quote_truncated:true, если обрезана) - число выведено из реверса, живого случая цитаты длиннее 200 символов не было.

Пересылка (forward_from). Timestamp пересылаемого сообщения, тоже часть отпечатка. В отличие от ответа, пересылка идёт без цитаты.

send_file

Параметр

Тип

chat

ChatId либо поисковый запрос

path

абсолютный путь к файлу или картинке на диске

confirm

true - залить и отправить; иначе draft (байты не льются)

confirm_token

токен из draft, обязателен при confirm:true

Отправка картинки (image) или произвольного файла (file); тип определяется по расширению. Voice и gallery не отправляются - для них закрыт только долг чтения (см. [Non-Goals]). Двухшаговая: draft показывает имя/размер/тип/чат и НЕ льёт байты - заливка и все 3 шага (§12.1: upload_to_disk → PUT сырых байт → add_files) идут только на confirm. После confirm уходит обычное сообщение с file_info.id, и его можно прочитать обратно по file_id и скачать через download_attachment. Ошибки загрузки разделены: 507/403 → квота, 413 → размер, не «upload failed». Ответ без числового Status = отказ, как и у send_message.

Перезаливка при повторном confirm после рестарта. Локальная память токена живёт в процессе. Если процесс рестартовал между draft и confirm, повторный confirm пройдёт 3 шага загрузки заново - байты уйдут повторно. Дубля сообщения при этом нет: PayloadId зафиксирован в токене на draft и переживает рестарт, поэтому повтор придёт серверу тем же id и вернётся DUPLICATE (нового сообщения не создаст). Потери - только повторно израсходованные байты, и они ограничены квота-ошибкой. Не блокер, но знать полезно.

set_reaction

Параметр

Тип

chat

ChatId либо поисковый запрос

message_id

timestamp сообщения в микросекундах (строка)

type

целочисленный id реакции (артворк, не emoji) из поля reactions сообщения

remove

true - снять реакцию (Action:REMOVE); иначе поставить

Реверсибельно, поэтому без confirm: снятие откатывает постановку тем же вызовом. Подтверждено живьём (US-009, self-чат): постановка (Reaction без Action) -> реакция видна через list_reactions; снятие (Action:REMOVE) -> реакция исчезает; Status:1 в обоих случаях. type валидируется по карте reaction-map.json ДО отправки - неизвестный тип отвергается на входе со status: invalid_type и на провод не уходит (сервер Type не валидирует и принял бы любой int, включая мусор). Реакция уходит полным конвертом ClientMessage (плоский push({Reaction}) дал бы ложный NO_SUCH_CHAT).

list_reactions

Параметр

Тип

По умолчанию

chat

ChatId либо поисковый запрос для резолва чата

обязателен

message_id

timestamp целевого сообщения в микросекундах (строка)

обязателен

limit

лимит на провод в обоих вызовах

50

invite_hash

для чтения по join-ссылке

нет

Без confirm (чтение). Полный список «кто и когда» по сообщению - реакции (сгруппированы по типу, actors_complete:true ВСЕГДА, в отличие от get_history/get_message/get_message_context/get_thread, где акторы реакций - усечённый сиблинг агрегата) и прочтения. Стоит ДВА WS-вызова (UserReactions + UserReads/Mode:1) - дороже обогащения на сиблингах, зато без обрезки: сиблинги history обрезаны и источником истины не служат (живьём видено ReadsCount:10 при RecentUserReads длиной 3). limit обязателен на проводе в обоих вызовах - без него сервер отвечает BACKEND_CALL_ERROR(2).

mark_read

Параметр

Тип

chat

ChatId либо поисковый запрос

message_id

опционально: timestamp (мкс), до которого отметить прочитанным; без него - до самого свежего сообщения

seqno

опционально: SeqNo той же границы

Без confirm (безобидно). Без message_id тянет последнюю страницу истории и отмечает прочитанным до самого свежего сообщения. Форма и эффект подтверждены живьём (US-009): маркер SeenMarker бэкенд принимает, и именно он пишет seen-позицию, обнуляя непрочитанное (перебором: в уже прочитанном self-чате SeenMarker отвечает DUPLICATE, а ReadMarker/UnseenMarker коммитят заново; на чате с реальным непрочитанным от другого аккаунта unread_count ушёл с 2 до 0). В выдаче form_status: verified (см. Ограничения).

pin_message

Параметр

Тип

chat

ChatId либо поисковый запрос

message_id

опционально: timestamp (мкс) закрепляемого сообщения; без него - открепить

Без confirm (легко откатить). Семантика подтверждена живьём (US-009): закреп с меткой добавляет PinnedMessageInfo на это сообщение, а Pin без метки его убирает (открепление) - проверено на проводе через ChatData. В выдаче form_status: verified.

delete_message

Параметр

Тип

chat

ChatId либо поисковый запрос

message_id

timestamp (мкс) удаляемого сообщения

confirm

true - удалить; иначе draft

confirm_token

токен из draft, обязателен при confirm:true

Двухшаговая (удаление необратимо). Шаг 1 (без confirm): резолвит чат, перечитывает удаляемое (автор/время/текст) и возвращает превью с confirm_token. В сокет не уходит ничего - превью строится чтением. Шаг 2 (confirm:true + токен): удаляет пустым Plain{ChatId, Timestamp} (§9.3). На confirm chat и message_id сверяются с подтверждёнными; расхождение - отказ. Форма подтверждена живьём (US-009, self-чат): Status:1, повторное чтение даёт deleted:true и пустой текст. Серверного дедупа на повторе нет: сырой replay того же удаления дважды на уже удалённом вернул снова FULLY_COMMITTED (переприменяет), от повтора защищает локальная память confirm-слоя. Удаление чужого сообщения отклоняет сервер (внятный commit-статус вроде NO_PERMISSION) - своего ограничения тут нет.

edit_message

Параметр

Тип

chat

ChatId либо поисковый запрос

message_id

timestamp (мкс) правимого сообщения

new_text

новый текст

confirm

true - применить правку; иначе draft

confirm_token

токен из draft, обязателен при confirm:true

Двухшаговая (правка необратима). Шаг 1: резолвит чат, перечитывает сообщение и возвращает превью was_text -> will_text с confirm_token, ничего не меняя. Шаг 2: правит через convertMessageToPlain + Timestamp (§9.3). На confirm сверяются chat, message_id и new_text (смена текста инвалидирует токен). Форма подтверждена живьём (US-009, self-чат): Status:1, после правки сообщение читается с новым текстом, edited:true и непустым edited_at (LastEditTimestamp). Повторный confirm тем же токеном отдаёт запомненный результат, второго push не шлёт. Правку чужого отклоняет сервер.

get_poll

Параметр

Тип

chat

ChatId либо поисковый запрос

message_id

timestamp (мкс) сообщения-опроса

Без confirm (чтение). message_info даёт вопрос/варианты/лимит выбора, poll_info (§14.3) - агрегат: {is_poll, answers, my_choices, is_anonymous, voted_count, recent_voters, results} плюс сырой ответ. answers[i] несёт votes и, для не-анонимного опроса с голосами, voters (имя+время из AnswerVotes); у анонимного опроса сервер скрывает список голосующих даже по явному запросу - это отражено флагом voters_hidden:true (виден только агрегат и свой выбор). Признак «это опрос» виден и здесь (is_poll), и в обычной выдаче сообщения (kind:'poll'). Не опрос - статус not_a_poll. Чтение разрешено против любого реального опроса в любом чате.

vote_in_poll

Параметр

Тип

chat

ChatId либо поисковый запрос

message_id

timestamp (мкс) сообщения-опроса

choices

массив выбранных вариантов (индексы/id)

confirm

true - проголосовать; иначе draft

confirm_token

токен из draft, обязателен при confirm:true

Двухшаговая. Форма Vote{ChatId, Timestamp, Action:0, Choices} (§9.3/§11.4) подтверждена живьём (2026-07-17, commit_status:1 FULLY_COMMITTED): Action:0 обязателен (без него - BACKEND_CALL_ERROR(2)), Results не шлётся, Choices - 0-based индексы в Poll.Answers[] и полный набор выбора. Голос публичен и меняемый: повторная отправка заменяет прежний выбор целиком (несколько вариантов - все индексы в одном Choices) - это подтверждено повторным чтением, где my_choices сменился с [0] на [1] после повторной отправки. form_status в выдаче - verified (AC-29 закрыт). Почему confirm сохранён, хотя голос меняемый: сам факт голоса необратим - voted_count растёт, а в не-анонимном опросе голосующий попадает в список голосовавших; отменить голос до нуля протоколом не подтверждено (единственный оставшийся мелкий вопрос).

download_attachment

Параметр

Тип

file_id

из file_info рефа сообщения

chat_id

опционально, только контекст вызывающего: в запрос не идёт

size

SMALL, SMALL48, MIDDLE2048, ORIGINAL - превью для картинок; без него скачивается оригинал

Возвращает {path, bytes, content_type}. Повторный вызов не идемпотентен: имена вложений не уникальны (photo.jpg у всех), затирать чужой файл нельзя, поэтому повтор кладёт рядом копию с суффиксом.

Related MCP server: time-messenger-mcp-server

Треды

Тред - это чат: у него собственный ChatId, и всё, что умеет обычный чат (чтение, отправка, реакции, прочтения), работает в треде тем же способом - паритет с обычным чатом, а не отдельная механика.

  • Чтение: get_thread открывает тред по готовому thread_id либо по паре chat + message_id родительского сообщения. Пагинация та же, что у get_history: limit (1-200, по умолчанию 40) и курсор before (мкс строкой - вернуть сообщения строго старше него; значение для следующей страницы - next_before из выдачи). Пустой (ещё не материализованный) тред приходит с empty:true, а не ошибкой доступа.

  • Создание = деривация, без сети. Отдельного серверного «создать тред» нет. thread_id выводится строкой из родительского сообщения (§17.10, radix 10), и тред материализуется первым отправленным в него сообщением. «Обсудить» из веб-клиента - ровно эта деривация, реверса не требует. Бизнес-чаты (2/...) недоступны для деривации - это зафиксировано, а не забыто.

  • Отправка в тред идёт обычным send_message, где в поле chat передан thread_id (тред = валидный ChatId) - с той же двухшаговой отправкой draft->confirm.

  • Подписка: join_to_thread / leave_thread по thread_id - вступление и выход, обратимы, поэтому без confirm.

Требования

  • Node.js >= 22

  • Chromium для Playwright

Установка

Через npx (рекомендуется)

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

npx playwright install chromium

Дальше сервер запускает MCP-клиент командой npx yandex-messenger-mcp - см. Подключение к MCP-клиенту. Руками её запускать не требуется.

Из исходников (для разработки)

git clone https://github.com/conarti/yandex-messenger-mcp.git
cd yandex-messenger-mcp
npm install
npx playwright install chromium
npm run build

Скрипты сборки, тестов и typecheck - в разделе Разработка.

Первый запуск и авторизация

Отдельного шага логина нет: авторизация ленивая и случается на первом вызове инструмента, который реально идёт к серверу. tools/list отвечает и без сессии.

Как это выглядит:

  1. Первый вызов. Поднимается headed-браузер Playwright на странице Мессенджера. Войдите обычным способом: QR-код, пароль, что настроено у вас. Сервер ждёт появления сессии до 5 минут.

  2. Сессия сохраняется в persist-профиле (~/.config/yandex-messenger-mcp/profile/). Дальше браузер не нужен: cookie извлекается, протокол гоняется в Node.

  3. Последующие запуски работают на сохранённом профиле, ручной вход не требуется.

  4. Протухание. Когда Яндекс отвергает cookie, профиль поднимается headless и даёт Паспорту рефрешнуть сессию (обычно пара секунд, незаметно). Если рефреш не помог, происходит эскалация в headed-логин, и вас снова попросят войти руками.

Единственный канал хрупкости здесь - сама cookie-сессия: долгий простой может потребовать ре-логина. Ничего другого не истекает.

Профиль и загрузки - это ваши личные данные. Каталог ~/.config/yandex-messenger-mcp/ содержит живую сессию Яндекса: он не должен попадать в git, синхронизацию или бэкапы, из которых его кто-то достанет.

Конфигурация

Файл: ~/.config/yandex-messenger-mcp/config.json. Целиком опционален - без него всё работает на дефолтах. Переопределять можно точечно, любую секцию и любое поле.

Ключ

Дефолт

Смысл

paths.profileDir

profile

Persist-профиль Playwright. Относительный путь резолвится от каталога конфига

paths.downloadsDir

downloads

Папка загрузок, туда же

downloads.ttlDays

7

TTL автоочистки загрузок. 0 или меньше - выключить подметание

limits.listChatsDefaultLimit

50

Дефолтный limit для list_chats

limits.searchDefaultLimit

50

Стартовый limit эскалации в search

protocol.*

см. ниже

Протокольные константы: на случай, если Яндекс их поменяет

Секция protocol существует ради устойчивости к ротации: если новая версия chats-web уедет на другие хосты, их можно поправить в конфиге, не трогая код. Значения по умолчанию и полный пример - в config.example.json.

Пример минимального конфига:

{
  "downloads": { "ttlDays": 3 },
  "limits": { "listChatsDefaultLimit": 100 }
}

Уровень логов задаётся переменной YANDEX_MESSENGER_MCP_LOG_LEVEL (debug/info/warn/error, по умолчанию info). Лог идёт в stderr: stdout принадлежит MCP stdio-транспорту. Секреты сессии и содержимое переписки в логах редактируются.

Подключение к MCP-клиенту

Сервер говорит по stdio. Путь до dist/index.js - абсолютный.

Таймаут вызова инструмента у MCP-клиента - минимум 5 минут. Первый вызов, идущий к серверу, поднимает браузерный вход и ждёт логина до 5 минут (см. Первый запуск). Дефолтные таймауты многих клиентов (30-60 секунд) короче этого ожидания и оборвут первый вызов молчаливым таймаутом ещё до того, как вы успеете войти - авторизация при этом выглядит «сломанной», хотя дело в таймауте. Поднимите таймаут вызова инструмента до >= 5 минут. Запас нужен и после первого входа: худший легальный read доходит до ~90 секунд, что тоже длиннее дефолтов.

Claude Code

Через npx (основной способ, после установки пакета из npm - см. Установка):

claude mcp add yandex-messenger -- npx -y yandex-messenger-mcp

Из собранных исходников (разработка):

claude mcp add yandex-messenger -- node /абсолютный/путь/yandex-messenger-mcp/dist/index.js

Claude Desktop

~/Library/Application Support/Claude/claude_desktop_config.json (macOS), через npx:

{
  "mcpServers": {
    "yandex-messenger": {
      "command": "npx",
      "args": ["-y", "yandex-messenger-mcp"]
    }
  }
}

Из собранных исходников (разработка) - тот же конфиг с command: "node" и путём до dist/index.js:

{
  "mcpServers": {
    "yandex-messenger": {
      "command": "node",
      "args": ["/абсолютный/путь/yandex-messenger-mcp/dist/index.js"]
    }
  }
}

Первый вызов любого инструмента откроет окно браузера для входа - это ожидаемо, см. Первый запуск.

Автоочистка загрузок (TTL)

Скачанное вложение - это копия чужой переписки на диске, и она не должна жить вечно только потому, что агент один раз её открыл.

  • Что удаляется: файлы в downloads/, у которых mtime старше TTL (дефолт 7 дней). Строго старше: файл ровно на границе остаётся.

  • Когда: на старте сервера и перед каждым скачиванием. Второе важнее первого - сервер может месяцами не рестартовать, и старт как единственная точка очистки не сработал бы.

  • Границы: только обычные файлы непосредственно в downloads/. Без рекурсии, без следования за симлинками, за пределы папки очистка не выходит.

  • Выключить: downloads.ttlDays: 0. Это именно «не подметать», а не «удалить всё разом».

Confirm-политика

Подтверждение (draft->confirm) стоит только у необратимых мутаций. Критерий один: откатывается ли последствие тем же инструментом за один вызов. Раньше необратимой была только отправка текста; с добавлением файлов, правки, удаления и голоса необратимых операций стало пять, и политика обобщена под общий критерий.

  • С confirm (двухшаговые, необратимые): send_message, send_file, delete_message, edit_message, vote_in_poll. Сообщение или файл уходят живому собеседнику, правка перезаписывает текст, удаление стирает, голос не снимается - отменить одним вызовом нельзя.

  • Без confirm (одним вызовом, обратимые или безобидные): set_reaction (Action:REMOVE откатывает тем же вызовом), pin_message (легко открепить), mark_read (безобидна). Read-пути (get_*, search, list_chats) и подписка на тред (join_to_thread/leave_thread) confirm тоже не требуют.

Почему confirm не раздан всем мутациям: дешёвый confirm обесценивает дорогой. Если подтверждать приходится и обратимую реакцию, его начинают жать не глядя - и тогда прожмут на delete_message. Подтверждение бережётся для того, что действительно необратимо.

Токен confirm несёт дискриминатор операции (op): токен, выданный одной операции, при предъявлении другой отвергается как op_mismatch - перепутать draft удаления с draft правки нельзя. На confirm заново резолвится чат и заново считается отпечаток нагрузки; любое расхождение - отказ, а не отправка «наиболее вероятного».

Двухшаговая отправка (send_message как образец)

Шаг 1 - draft. Обычный вызов резолвит чат и возвращает превью:

{
  "status": "draft",
  "chat_id": "...",
  "chat_name": "Имя чата",
  "text": "текст",
  "confirm_token": "...",
  "next_step": "Ничего не отправлено. ..."
}

В сокет не уходит ничего.

Шаг 2 - confirm. Повторный вызов с confirm:true, тем же confirm_token и неизменёнными chat и text.

Почему на confirm идёт повторная сверка, а не «отправить то, что в токене»: между draft и confirm может смениться всё. Тот же запрос chat завтра резолвится в другой чат - человек переименовался, появился однофамилец. Или к старому токену подставили другой текст. Поэтому токен несёт резолвнутый ChatId и хэш текста, на confirm чат резолвится и текст хэшируется заново, и результаты сверяются. Расхождение - отказ, а не отправка «наиболее вероятного».

Токен намеренно не подписан: подделывать его бессмысленно. Он не полномочие, а память о драфте - отправка всё равно идёт в заново проверенный чат.

Идемпотентность держится двумя слоями, потому что ретрая у отправки нет:

  1. Локально: израсходованный токен возвращает запомненный результат, второй push не уходит.

  2. На сервере: PayloadId фиксируется в токене на шаге 1, поэтому даже если локальная память потерялась (рестарт), повтор придёт как DUPLICATE - тоже успех, но нового сообщения не создаст.

Неоднозначный чат отправку блокирует: наружу уходят кандидаты, push не отправляется.

Ограничения и пробелы в доказательствах

Раздел честный. Часть протокола подтверждена живыми прогонами, часть - только фикстурами по реверсу веб-клиента, и это разные уровни уверенности.

Чего нет по решению (отложено)

  • Real-time. Никаких подписок на LIVE-события: новые сообщения, typing, seen, presence. Инструмент отвечает на запрос, а не слушает поток. Бота и автоответы на нём не построить.

  • Отправка вложений - только image и file. Картинки и произвольные файлы отправляются (send_file); voice и gallery отправлять нельзя (только читать). Голосовые и галереи читаются, но не создаются.

  • Мутации - частично. Реализованы реверсибельные (реакции set_reaction, отметка прочтения mark_read, закреп pin_message - одним вызовом, без confirm) и необратимые (отправка файла send_file, правка edit_message, удаление delete_message, голос vote_in_poll - двухшаговые draft->confirm). Чтение опроса - get_poll. Пока НЕ реализованы: отправка voice/gallery, звонки, управление чатами, мульти-аккаунт.

  • Только cookie-авторизация. OAuth не поддержан: в бандле это другой транспорт с другим форматом кадров, а не «второй режим», и он потребует отдельного WS-клиента.

Что не подтверждено живьём

  • Reply на исходящем подтверждён живьём, forward и refs - нет (US-010, 2026-07-21). Отправка ответа (reply_to_message_id) даёт Status:1, и перечитывание возвращает context.is_reply:true с непустым quotes - цитата и признак ответа работают. Forward (forward_from) уходит с Status:1, но перечитывание НЕ показывает признака пересылки (context отсутствует, forwarded пуст) - на self-чате форма не рендерится. context.refs пуст и у reply: ссылка на оригинал (ForwardedMessageRefs) на чтении не восстанавливается, reply держится на Quote. Оба пробела и гипотезы (cross-chat forward, дискриминатор формы) - в issue #13. Входящий reply/forward в профиле по-прежнему не наблюдался (0 на 372), поэтому форма чтения refs/forwarded остаётся доко-выведенной.

  • Чтение вложений: image, file и gallery проверены живьём; voice - долг. Скачивание типо-агностично (voice/gallery_image - это те же file_id, тянутся тем же generic-путём). Живьём (US-009) скачана настоящая галерейная картинка с аккаунта (gallery_image, непустые байты + content_type) - долг по галерее закрыт. Реального голосового на профиле нет, поэтому voice живьём по-прежнему не проверено - долг остаётся дословно, фикстура доказательством не объявляется.

  • Отправка вложений: путь не подтверждён до конца, но 403→квота поймана живьём (US-010, 2026-07-21). 3 шага загрузки (upload_to_disk → PUT → add_files) и обычное сообщение с Plain.Image/Plain.MiscFile + FileInfo.Id2 взяты из реверса веб-клиента: входящие вложения живьём наблюдались, а исходящий путь - нет. Width/Height картинки намеренно не проставляются (для доставки достаточно file_info). Живой прогон в self-чате упёрся в upload_to_disk HTTP 403, и код классифицировал его как upload quota (не generic «upload failed») - ветка 403→квота подтверждена живьём. Полный трёхшаговый путь и ветка 413→размер остаются доко-выведенными (заливка отклонена сервером до конца), см. issue #5. Билдеры в src/protocol/push.ts, загрузка в src/attachments/uploader.ts.

  • Форма ответа на отправку подтверждена живьём (US-009). Успешный push текста вернул { Status:1, MessageInfo:{TimestampMcs, PrevTimestampMcs, SeqNo, Version}, DebugInfo } - имена контейнера MessageInfo/PrevTimestampMcs/TimestampMcs/SeqNo/Version теперь наблюдены, а не реконструированы. DebugInfo (адреса/тайминги/попытки) сервер тоже отдаёт - не парсится, безвреден. RateLimit на успешной отправке не приходит. Парсер по-прежнему принимает оба написания; ответ без числового Status трактуется как отказ. Инвариант распространён и на путь вложений.

  • Постановка/снятие реакции подтверждены живьём (US-009, долг закрыт). Постановка (Reaction без поля Action, серверный дефолт ADD) -> реакция появляется в list_reactions; снятие (Action:REMOVE=1) -> реакция исчезает; Status:1 в обоих случаях. REPLACE(2) живьём не гонялся.

  • Голос vote_in_poll: отмена до нуля не подтверждена. Форма Vote{ChatId, Timestamp, Action:0, Choices} (§9.3/§11.4) закрыта живьём (2026-07-17, commit_status:1 FULLY_COMMITTED): Action:0 обязателен, Results не шлётся, Choices - 0-based индексы в Poll.Answers[], полный набор выбора. Голос публичен и меняемый - повторная отправка заменяет выбор целиком (подтверждено повторным чтением: my_choices сменился с [0] на [1]). form_status в выдаче - verified, AC-29 закрыт. Confirm сохранён, потому что сам факт голоса необратим: voted_count растёт, а в не-анонимном опросе голосующий попадает в список голосовавших. Единственный оставшийся мелкий вопрос - отмена голоса до нуля (пустой Choices либо иной Action) протоколом не подтверждена (кнопки в веб-UI нет).

Известные грубости

  • Единица rate_limit.wait_for неизвестна. Ни в доке, ни в живом захвате она не встретилась (на успешной отправке rate_limit не приходит вовсе). Гадать не стали: сырое значение трактуется как миллисекунды и зажимается в 1-60 секунд. Кламп ограничивает ущерб при любой из трёх гипотез: если это секунды, минимум не даст устроить ретрай-шторм; если микросекунды, максимум не даст зависнуть на часы; если миллисекунды, значение проходит как есть. Точность здесь принесена в жертву осознанно. В лог пишется сырое значение - первый живой случай позволит определить единицу.

  • У поиска нет серверной пагинации. Параметры page/offset/from/skip сервером игнорируются, поля page/pages в ответе вестигиальны (всегда 1), а total - это число возвращённых элементов, а не общее число совпадений. Полнота достигается эскалацией limit, то есть несколькими запросами вместо одного. Серверного потолка limit найти не удалось (1000 отвечает штатно), поэтому потолок эскалации клиентский: при упоре - truncated:true.

  • Автоматизация личного аккаунта. Это личный инструмент на неофициальном протоколе. Риски по ToS вы принимаете на себя. Отправка не распараллеливается, массовых рассылок здесь нет.

  • Протокол может уехать. Он снят с конкретной версии chats-web. Обновление веб-клиента может сломать инструмент; протокольные константы вынесены в protocol.* конфига, чтобы часть таких поломок чинилась без правки кода.

Границы доверия

Сводка открытых пунктов из разделов выше - для быстрой сверки, без деталей (детали в тексте по ссылке-issue или в самом разделе).

Пункт

Статус

Issue

Чтение voice не подтверждено живьём

долг

#4

Разделение ошибок загрузки 507/403 (квота) / 413 (размер) не проверено живьём

долг

#5

REPLACE(2) у реакции объявлен, но не используется и не наблюдался на проводе

долг

#6

Отмена голоса в опросе до нуля не подтверждена (кнопки в UI нет)

долг

#7

Единица rate_limit.wait_for неизвестна, сырое значение зажато в 1-60 секунд

долг

#8

Обрезка цитаты reply до 200 символов (QUOTE_MAX_LENGTH, src/protocol/push.ts)

выведено

-

actors_complete реакций в обогащении get_history/get_message/get_message_context/get_thread - сравнение count с числом акторов (src/protocol/enrichMessage.ts), случай расхождения живьём не наблюдался

выведено

-

Разработка

npm run build       # tsc
npm run typecheck   # tsc с тестами
npm test            # vitest, e2e при этом скипается
npm run test:watch

E2E

Живой smoke-тест ходит на реальный аккаунт и по умолчанию скипается. Запуск явный:

YMCP_E2E=1 npm test

Он намеренно только читает (whoami, list_chats, get_history): ничего не отправляет и ничего не качает, чтобы его можно было безопасно гонять повторно. Ассерты идут только по числам и булям - живые данные не попадают ни в вывод, ни в диагностику падений.

Install Server
F
license - not found
A
quality
B
maintenance

Maintenance

Maintainers
18hResponse time
Release cycle
1Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/conarti/yandex-messenger-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server