Skip to main content
Glama

mailwarden

npm license Node Website Smithery

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

Основные возможности

  • Snooze — единственный на стороне почтового ящика snooze в Gmail MCP-сервере. Архивируйте ветку сейчас, и она снова появится во входящих в нужную дату. Основано на датированных метках + sweep, поэтому работает из любого клиента, видно в самом Gmail и переживает перезапуски. (Где другой сервер предлагает «snooze», это локальный список напоминаний — письмо никогда не покидает входящие и не возвращается в них.)

  • Поиск, которому можно доверять. Вызов Gmail threads.list — через который проходит любой поиск по веткам — может отвечать на is:unread на основе устаревшего состояния прочитанности на уровне ветки: по замерам в одном реальном почтовом ящике, 86 % возвращённых веток вообще не содержали непрочитанных сообщений; во втором ящике расхождений не было вовсе. Не заглянув внутрь, вы не можете сказать, в каком ящике находитесь, поэтому search перепроверяет каждый результат по его актуальным меткам. Пагинация через pageToken/nextPageToken.

  • Массовые операции, которые масштабируются. bulk_modify архивирует/применяет метки ко всему, что соответствует запросу, по 1000 сообщений за API-запрос — с отчётом о частичном успехе по каждому фрагменту, а не по принципу «всё или ничего». Проход snooze использует тот же пакетный механизм.

  • Структурированные выходные данные. Каждый инструмент объявляет outputSchema и возвращает проверенный structuredContent вместе с JSON-текстом в fenced-блоках — клиентам не нужно угадывать, как разбирать. Сбои тоже структурированы: поле code и флаг retryable, так что клиент может отличить «повторите позже» от «пройдите авторизацию заново», не читая текстовых пояснений.

  • Небольшая поверхность атаки. Нет инструментов отправки (нет пути для эксфильтрации писем при промпт-инъекции), опциональный режим «только чтение», никакой телеметрии, нет открытых портов по умолчанию, безопасная для симлинков изоляция загрузок, защищённые от инъекций выходные данные. И нет ни одного пути в коде, который мог бы отправить письмо: каждый запрос к Gmail проходит через контрольную точку исходящего трафика, которая отклоняет messages.send, все конечные точки черновиков, необратимое удаление и настройки пересылки — что бы ни запросил скомпрометированный или неосторожный вызывающий код. Одно намеренное исключение: unsubscribe / bulk_unsubscribe (уровень manage) связываются с endpoint отказа от рассылки, указанным в заголовке самого письма, — единственный не-Google хост, к которому mailwarden вообще обращается; развёртывание уровня read не выполняет вообще ни одного исходящего запроса. Подробности в разделах Безопасность и конфиденциальность и Отписка.

  • Корректность на реальной почте. Заголовки RFC 2047 декодируются (=?UTF-8?B?…?= → читаемый текст), тела писем декодируются в объявленной кодировке (кракозябр нет для писем в ISO-8859-1/Shift_JIS), ошибки 429/5xx повторяются с экспоненциальной задержкой.

Related MCP server: Gmail MCP

Зачем

Коннекторы, которые синхронизируют или кэшируют ваш почтовый ящик, могут не успевать за ним — а даже собственный поисковый индекс Gmail иногда неточен (см. ниже). mailwarden общается напрямую с живым Gmail API (без кэшированного снимка) и перепроверяет то, что возвращает индекс, поэтому вы видите именно то, что там на самом деле. Это универсальный слой возможностей Gmail — храните собственные правила и логику в своём ИИ-клиенте, а не в сервере.

search идёт на шаг дальше, чем сам API: индекс Gmail threads.list может отвечать на операторы состояния прочитанности на основе устаревшей копии этого состояния, поэтому is:unread возвращает ветки, которые вы дочитали недели назад, — в одном измеренном ящике это подавляющее большинство выдачи. Поскольку каждый результат в любом случае запрашивается вживую, search перепроверяет однозначные предикаты (is:unread/is:read/is:starred/in:inbox/category:…, с отрицанием) по реальным меткам каждой ветки и отбрасывает ложные срабатывания индекса.

Сравнение с другими Gmail MCP-серверами

Большинство Gmail MCP-серверов покрывают одну и ту же поверхность чтения/меток/отправки. Две возможности до сих пор уникальны для mailwarden (snooze на стороне почтового ящика, перепроверка результатов поиска), а одно намеренное отсутствие — это функция безопасности, а не недостаток. Собственный сервер Google тоже уже, чем кажется: только черновики, и нет корзины, фильтров или отписки.

Возможность

mailwarden

Официальный Google

taylorwilsdon

a-bonus

klodr

Snooze на стороне почтового ящика — архивируйте сейчас, верните во входящие в дату/время или по пресету

✅

—

—

—

—

Перепроверка результатов поиска — отбрасывает ложные срабатывания индекса веток, сверяясь с актуальными метками

✅

—

—

—

—

Sweep / массовая операция по запросу — одно действие над каждой веткой, которую вернул поиск

✅ 1000/запрос, частичный успех

—

⚠️ пакетно по явным id

—

⚠️ пакетно по явным id

Отписка — сводка по отправителям + отказ в один клик по RFC 8058, без области отправки

✅

—

⚠️ заголовок показан, действие не выполняется

—

—

Обзор разбора входящих — один вызов, который раскладывает ожидающее по категориям

✅ отправитель/метка/возраст + сигналы из заголовков

—

—

✅ эвристические флаги + статистика

—

Серверные фильтры — правила, которые продолжают разбор без участия ассистента

✅ никогда не пересылает

—

✅

—

✅

Нет инструментов отправки — намеренно — у письма, внедрённого через промпт-инъекцию, нет пути для эксфильтрации

✅ вообще нет создания писем

⚠️ только черновики

❌ отправляет

❌ отправляет

❌ отправляет

Уровни инструментов с минимальными привилегиями — области OAuth выводятся из включённых вами инструментов

✅

⚠️ разделение областей

⚠️ --read-only сужает области; уровни сужают только инструменты

—

⚠️ наоборот: инструменты ограничиваются выданными областями

Шифрование токенов в состоянии покоя (опционально)

✅ AES-256-GCM

н/п (хостинг)

✅

—

—

Нет облака вендора — вы управляете сервером

✅

❌ размещён у Google

✅

✅

✅

Структурированные выходные данные — каждый инструмент объявляет outputSchema

✅

—

—

—

⚠️ один инструмент (download_email), планируются ещё

Срез по состоянию на 26 августа 2026 года, из публичной документации и исходного кода каждого проекта; — = не предлагается / не документировано. Колонки — это серверы, к которым читатель, вероятнее всего, обратится: фирменный сервер Google, два крупнейших community-сервера — и klodr, который ближе всех к собственной схеме минимальных привилегий mailwarden. Возможность отправки указана как свойство безопасности: её отсутствие у mailwarden намеренно (см. Безопасность и конфиденциальность). Последняя строка спрашивает, кто управляет сервером, а не где он запущен: самостоятельный хостинг здесь — общая практика, и каждый community-сервер в этой таблице предлагает то или иное удалённое развёртывание, кроме klodr (только stdio) — mailwarden через --http, taylorwilsdon через streamable HTTP с OAuth 2.1, a-bonus на Cloud Run. Запуск одного из них на собственном хосте — это не облачная копия; запуск на хосте вендора — это она.

Конкурентный ров — это не какая-то отдельная строка, а snooze + живая перепроверка вместе: настоящий слой работы со входящими, который действует на основе текущего состояния почтового ящика, а не кэшированного снимка. Там, где другие уже догнали, честно отмечено выше: шифрование в состоянии покоя (taylorwilsdon), ограничение инструментов по областям доступа (klodr — в обратную сторону; taylorwilsdon — в нашу, но не так далеко: его --read-only действительно переключает OAuth-поток на карту областей только для чтения, но запрашиваемый набор строится в расчёте на сервис, а не на инструмент, поэтому уровень сужает, какие инструменты регистрируются, не сужая, что может делать токен: --tool-tier core --tools gmail по-прежнему запрашивает полные области Gmail — проверено в его auth/scopes.py и main.py 26 августа 2026 года и исправлено там же в тот же день csitte.at, который сверил это со своим собственным клоном, а не поверил нам на слово), более богатая эвристика разбора отдельных сообщений (a-bonus) и массовая сортировка почтового ящика (хостинговый mcpemails.com, у которого тоже нет snooze). Но ни один из них не действует на основе запроса и не проверяет ответ почтового ящика, прежде чем действовать.

Запуск рядом с сервером Workspace

mailwarden — это Gmail-сервер, а не Workspace-набор: если вам нужны Calendar, Drive, Docs и Sheets в одном месте, универсальный сервер вроде taylorwilsdon/google_workspace_mcp покрывает то, что этот не покроет никогда, и эти два варианта не исключают друг друга. Подключить оба — разумная конфигурация, и причина в токене, а не в количестве инструментов: сервер-набор, который умеет отправлять почту, хранит учётные данные, способные отправлять почту для каждого почтового ящика, на который он нацелен. Если вместо этого отдать Gmail серверу mailwarden, почтовая половина вашей конфигурации не будет иметь вообще никакого инструмента compose, reply, forward или send. На чём держится это обещание, зависит от уровня доступа, и это различие важно: на уровне read Google обеспечивает его на уровне токена (gmail.readonly, который отклоняют эндпоинты отправки), а на уровне manage оно опирается на сам набор инструментов — Gmail принимает gmail.modify для отправки, так что один лишь scope ничего не гарантирует. В обоих случаях защита исходящего трафика отклоняет messages.send и все эндпоинты черновиков в самом сервере, поэтому у внедрённого в ваш ящик сообщения нет ни инструмента, за который можно ухватиться, ни эндпоинта, которого можно достичь.

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

Почему повторная проверка важна — конкретный случай

Попросите ассистента «заархивировать непрочитанную рекламную почту, которая уже прошла мимо входящих», и он потянется к очевидному запросу: category:updates is:unread -in:inbox. Сервер, доверяющий индексу Gmail, теперь заархивирует темы, которые вы уже прочитали, — письма, которых вы и не думали касаться, исчезнут массовым действием, которое нелегко отменить.

Измерено, а не заявлено. Один реальный почтовый ящик (~70 000 писем), 15.08.2026, только чтение:

Запрос (threads.list)

Возвращено тем

С непрочитанным письмом

Устаревшие

category:updates is:unread

131

17

87%

category:updates is:unread -in:inbox

128

14

89%

is:unread -in:inbox

235

99

58%

Индекс не игнорирует предикат — тот же запрос без is:unread возвращает 800+ тем, значит, он применяется. Он применяется к состоянию прочитанности на уровне темы, которое не успело обновиться: темы, в которых каждое письмо прочитано, всё ещё числятся там непрочитанными. Одна из возвращённых тем несла единственную метку SENT. И это не причуда экзотических комбинаций операторов: самый простой из трёх запросов тоже показывает это — с наименьшей долей (58%), но с наибольшим числом ошибочных тем в абсолютном выражении (136).

Дело именно в индексе тем. Тот же запрос, тот же ящик, та же минута, заданный через messages.list вместо этого: 19 писем, ни одного устаревшего. Так что это не «поиск Gmail ненадёжен» — просто представление прочитанности на уровне тем отстаёт, а на уровне отдельных писем — нет. search идёт через threads.list, и именно поэтому он перепроверяет.

Второй ящик, измеренный тем же способом в тот же день, не дрейфовал вовсе — ноль сырых попаданий в индексе по is:unread, хотя через API его много раз в день помечают прочитанным. Так что это свойство конкретного ящика, а не Gmail повсеместно. Что их различает — открытый вопрос: они отличаются объёмом (примерно на три порядка) и возрастом, а во втором не хватает чего-то более базового — ни одна тема в нём никогда не архивировалась, оставаясь непрочитанной, а это единственная форма, в которой может проявиться устаревшее состояние прочитанности. Так что это не контрпример к какой-то конкретной причине; это ящик, в котором самого кандидата нет.

В этом и весь смысл: сервер не может знать, в каком именно ящике он работает. Повторная проверка ничего не стоит там, где ничто не дрейфует, и спасает там, где дрейфует, — в измерении выше каждая тема, отброшенная search, была действительно прочитана, и он не отбросил ни одного по-настоящему непрочитанного письма.

Где это не бесплатно: массовые инструменты. search перепроверяет, потому что он в любом случае загружает каждое попадание; bulk_modify (и проход applyToExisting из create_filter) рассчитывается на тысячи писем, где загрузка по одному запросу на каждое попадание — совсем другой порядок стоимости. Они действуют на то, что возвращает индекс, — поэтому теперь они сообщают unverifiedPredicates: условия из вашего запроса, принятые на слово индекса (+UNREAD, -INBOX, …). Пустое значение означает, что недоверять было нечему. Непустое, а результат должен быть точным по состоянию прочитанности? Сначала разрешите набор через search и действуйте по этим thread id. dryRun не закрывает этот разрыв: он перечитывает тот же индекс, так что подтверждает лишь размер набора, но не его правильность.

mailwarden и так загружает каждое попадание вживую, поэтому search перепроверяет однозначные предикаты (is:unread, is:read, in:inbox, category:…, с отрицанием) по настоящим меткам каждой темы и отбрасывает ложные срабатывания индекса до того, как их увидит какой-либо инструмент. Массовое действие затем выполняется ровно по тому набору, который вы запросили. Это разница между действием на основе того, что Gmail проиндексировал, и действием на основе того, что реально сейчас находится в ящике, — и именно поэтому snooze/sweep безопасно отдавать ассистенту: sweep возвращает на поверхность только те темы, чей snooze действительно наступил, с проверкой по живым меткам во время выполнения.

Проверьте сами — учётная запись Gmail не нужна. Из клона репозитория (демо — это скрипт проверки, живущий только в репозитории, а не часть npm-пакета):

git clone https://github.com/csitte/mailwarden && cd mailwarden
npm install && npm run build
node scripts/demo-reverify.mjs

Рядом с ним лежит второй скрипт, node scripts/probe-reverify.mjs, который измеряет то же самое в вашем ящике вместо вымышленного — только чтение, только метаданные (не загружаются ни тема, ни отправитель, ни тело), печатает количества и имена меток. Именно так получены цифры выше, и так вы можете проверить, дрейфует ли ваш ящик вообще.

Демо гоняет настоящий search() против фейкового Gmail API, чей индекс намеренно устарел (возвращает прочитанную тему на запрос is:unread, точно как Gmail), и показывает, как mailwarden отбрасывает ложное срабатывание. Оно проверяет результат, поэтому завершается с ненулевым кодом, если поведение когда-либо регрессирует. Тот же случай зафиксирован юнит-тестами в test/gmail.test.ts ("drops index false positives via live-label re-verify").

Инструменты

Tool

What it does

search

Синтаксис запросов Gmail → сводки по веткам (отправитель/тема/дата/метки/сниппет); предикаты состояния прочитанности/категорий повторно проверяются по актуальным меткам каждого совпадения; постраничная навигация через pageToken/nextPageToken. Каждое совпадение несёт signals — newsletter (List-Id / List-Unsubscribe / Precedence bulk или list), automated (Auto-Submitted, заголовки автоответа/подавления, отправители в стиле no-reply), calendar (часть text/calendar или .ics), replyToMismatch (Reply-To на другом домене, нежели From; поддомен того же домена считается тем же) — считываются из заголовков/MIME первого сообщения, без дополнительного вызова. Спам и корзина исключаются, если только в запросе не указано in:spam / in:trash — см. Поиск в спаме

get_thread

Полная ветка: заголовки, текстовые (plaintext) и HTML-тела, метаданные вложений. При full: false извлекаются только заголовки и метки — тогда опускаются plaintextBody/htmlBody/attachments и устанавливается metadataOnly: true, а не сообщается о них как о пустых для запроса, который их не запрашивал

list_labels

Все метки (системные и пользовательские)

get_profile

Адрес подключённого аккаунта + общее количество сообщений/веток — подтвердите, какой именно почтовый ящик подключён, прежде чем действовать

triage_digest

Структурированный обзор среза почтового ящика для принятия решений: топ отправителей (у каждого — сигналы его веток), разбивки по меткам и возрасту, количество непрочитанных и вложений, а также сколько веток являются рассылками / автоматическими / приглашениями в календарь / несовпадениями reply-to — вместо сырого списка веток

list_unsubscribe

Какие варианты отказа от подписки предлагает ветка (List-Unsubscribe) — ни с кем не связывается

list_subscriptions

Срез почтового ящика, сгруппированный по отправителю: количество веток/непрочитанных, диапазон дат, за который был замечен каждый, и варианты отказа для каждого — один запрос заголовков на отправителя, ни с кем не связывается. sendersFound сообщает, сколько отправителей было до того, как topN усек список

create_label

Создать пользовательскую метку (идемпотентно; вложенная через Parent/Child) и вернуть её id

modify_labels

Добавить/удалить метки по имени или id — неизвестное имя в add автоматически создаётся (архив = удалить INBOX, прочитано = удалить UNREAD)

bulk_modify

Пакетное изменение меток для каждого сообщения, соответствующего запросу — 1000 сообщений на один запрос API, частичный успех сообщается по чанкам (список id веток ограничен 500, в submittedThreadCount — итог). Счётчики показывают submitted, потому что messages.batchModify отвечает 204 без тела и молча игнорирует неизвестные id; verify: true считывает метки обратно и возвращает verified {applied, notApplied, unverifiable} — единственный доступный наблюдаемый результат. Работает с сырым индексом, поэтому unverifiedPredicates называет условия, за которые он не может поручиться (см. ниже). dryRun: true выполняет запрос и сообщает найденные ветки и метки, которые были бы созданы, ничего не затрагивая

archive / mark_read / mark_unread

Удобные обёртки

trash / untrash

Переместить в корзину / восстановить из корзины

download_attachment

Сохранить вложение в локальный путь (никогда не перезаписывает — при конфликтах добавляется числовой суффикс)

unsubscribe

Отписка в один клик (RFC 8058) с использованием эндпоинта из собственного заголовка сообщения — единственный инструмент, который связывается с не-Google хостом (подробнее)

bulk_unsubscribe

То же самое для нескольких веток, последовательно и не более одного запроса на отправителя (запоминается между вызовами, пока работает сервер, поэтому повторная попытка ни с кем не связывается дважды); частичный успех сообщается по веткам. dryRun: true выполняет те же чтения заголовков и дедупликацию и сообщает эндпоинт, который каждая ветка wouldCall, — ни с кем не связываясь

snooze

Архивировать сейчас, вернуть на поверхность в указанную дату или после (YYYY-MM-DD), в дата+время (2026-06-20 9am) или по пресету (tomorrow, tomorrow 9am, weekend, next week, название дня недели, in N days, in N hours)

unsnooze

Отменить отложенный показ и немедленно вернуть во входящие

list_snoozed

Все отложенные ветки и сроки возврата

sweep_snoozed

Вернуть на поверхность ветки, у которых наступил срок отложенного показа (запускается по требованию, через cron или демон); пакетно, с отчётом о частичных сбоях. dryRun: true отвечает на вопрос «что должно появиться прямо сейчас?» (dueLabels/dueThreads), ничего не активируя

list_filters

Все фильтры Gmail (критерии + действия с метками); показывает любые forward-адреса в существующих фильтрах для аудита

create_filter

Создать серверное правило автосортировки (критерии → только действия с метками; без пересылки — см. ниже). Опционально applyToExisting, чтобы также обработать подходящую почту, уже находящуюся в ящике

delete_filter

Удалить фильтр по id

Все инструменты объявляют outputSchema и возвращают структурированное содержимое (проверенное, машиночитаемое) вместе с тем же JSON в виде текста в код-блоке — клиентам никогда не приходится разбирать прозу.

Сбой тоже структурирован: isError плюс тело JSON в код-блоке с code (not_authorized, needs_reauth, insufficient_scope, forbidden_operation, not_found, rate_limited, upstream_unavailable, network_error, invalid_input, internal_error) и флагом retryable, рядом с предложением, которое читает человек. Так что «подождите и попробуйте снова» против «повторно выполните mailwarden --auth» — это то, что клиент может решить сам, а не то, что ему приходится выводить из формулировок, которые в следующем релизе могут быть переписаны. (На ошибках нет structuredContent: он проверяется по outputSchema инструмента, которая описывает успешный ответ.)

Как работает snooze (в Gmail API нет snooze — мы реализуем его сами)

snooze убирает INBOX и применяет датированную метку MCP/Snoozed/<key>, где key — это либо YYYY-MM-DD (срок — весь день), либо YYYY-MM-DDTHHMM (срок — в эту локальную минуту). Аргумент until принимает явную дату, дату+время (2026-06-20 9am, …T17:00) или пресет, вычисляемый на сервере — today, tomorrow, weekend (следующая суббота), next week (следующий понедельник), название дня недели (monday–sunday, следующее вхождение), in N days или in N hours — и пресет даты может нести завершающее время (tomorrow 9am, monday 8:30), так что вызывающей стороне никогда не приходится вычислять момент самой. sweep_snoozed находит наступившие метки и возвращает эти нити во входящие (помечая непрочитанными); отложенный по времени snooze срабатывает при первом запуске очистки в указанную минуту или после неё, так что задержка пробуждения равна вашему интервалу очистки. Запустите очистку:

  • по требованию (инструмент sweep_snoozed),

  • через cron: mailwarden --sweep,

  • или автоматически: установите MAILWARDEN_AUTO_SWEEP=1 (ежечасная очистка, пока сервер работает).

Фильтры (постоянные правила автоматической сортировки)

create_filter настраивает серверное правило Gmail: письма, соответствующие критериям, автоматически получают заданные действия с метками — почтовый ящик продолжает сортировать сам себя без участия ассистента.

  • Критерии: from, to, subject, query (полный синтаксис поиска Gmail), negatedQuery, hasAttachment, excludeChats и size + sizeComparison (smaller/larger, указываются вместе). Требуется хотя бы один.

  • Действия (только метки): addLabels / removeLabels, по имени или id (неизвестное имя в addLabels создаётся автоматически, с вложенностью через /). Типовые рецепты: пропустить входящие → removeLabels: ["INBOX"]; автоматически отметить прочитанным → removeLabels: ["UNREAD"]; автоматически в корзину → addLabels: ["TRASH"]; пометить звездой → addLabels: ["STARRED"]; никогда не в спам → removeLabels: ["SPAM"]; разложить по метке → addLabels: ["Receipts"].

  • Существующая почта: фильтр действует только на сообщения, поступающие после его создания. Передайте applyToExisting: true, чтобы также один раз применить те же действия к почте, уже находящейся в ящике — mailwarden строит поисковый запрос Gmail из критериев и выполняет массовое изменение (до maxMessages, по умолчанию 1000; та же оговорка о непроверенном индексе, что и у bulk_modify, и разовый проход исключает Spam/Trash). Для этого требуется хотя бы один положительный критерий (from/to/subject/query/hasAttachment:true/size): правило только с исключениями (negatedQuery или hasAttachment:false) отклоняется для applyToExisting, потому что оно соответствовало бы почти всему ящику — создавайте такой фильтр без этого флага. Исход возвращается в applied (использованный query, счётчики matchedMessages/submittedMessages/submittedThreadCount, capped, когда набор совпадений достиг maxMessages, failed по чанкам и строка error, если весь проход завершился сбоем); он равен null, когда applyToExisting не задан. Фоновый проход не проверяет, что в итоге попало в ящик — это делает verify из bulk_modify; запустите его повторно с тем же запросом, когда в результате очистки нужно быть уверенным. Фильтр создаётся первым, поэтому частичный или неудачный фоновый проход сообщается в applied, а не вызывает исключение — правило остаётся в силе.

  • Никакого пересылания — см. Безопасность и конфиденциальность.

  • Требуется scope gmail.settings.basic; повторно выполните --auth один раз, если вы авторизовали более старую версию. Недоступно в режиме только для чтения.

Отписка — единственный исходящий запрос

list_unsubscribe (уровень чтения) сообщает, что предлагает отправитель, ни с кем не связываясь. Он читает самое новое сообщение, в котором действительно есть заголовок List-Unsubscribe — ответ, встроенный в нить рассылки, находится в конце и ничего не предлагает, иначе это читалось бы как «у этого списка нет отписки». list_subscriptions (уровень чтения) делает то же самое по целому срезу, группируя по отправителю, так что вы видите, кто продолжает писать и кого из них действительно можно покинуть — по одному запросу заголовка на отправителя, а не на нить. unsubscribe и bulk_unsubscribe (уровень управления) действуют на основе этого — и это единственное место, где mailwarden вообще говорит с хостом, не являющимся Google, поэтому правила жёсткие:

  • Параметра URL нет. Конечная точка берётся из собственного заголовка сообщения и ниоткуда больше. URL-аргумент позволил бы письму с промпт-инъекцией превратить инструмент в канал эксфильтрации (содержимое почтового ящика в строке запроса); заголовок не может нести данные, выбранные моделью.

  • Выполняется только one-click по RFC 8058 — отправитель должен дать согласие через List-Unsubscribe-Post. Обычная ссылка https: предназначена для человека в браузере и возвращается, а не загружается.

  • Отписки через mailto: никогда не выполняются. Для них потребовалось бы отправить письмо, чего mailwarden не умеет. Адрес сообщается, чтобы вы могли действовать по нему самостоятельно.

  • Фиксированный запрос, отбрасываемый ответ. Тело POST всегда равно List-Unsubscribe=One-Click и никогда ни из чего не выводится; тело ответа отбрасывается непрочитанным. Модели возвращаются код состояния и фактически вызванный URL — никакого содержимого от конечной точки, так что она не может ответить инструкциями. (Редирект 301/302/303 выполняется как GET, то есть вообще без тела.)

  • Один запрос на отправителя, последовательно, в рамках одного бюджета. bulk_unsubscribe принимает id нитей (никогда не запрос — массовая операция на основе запроса запускала бы по запросу на каждого подходящего отправителя, прежде чем кто-либо успел посмотреть). Нити от отправителя, чей запрос уже ушёл, возвращаются с duplicateOf и не требуют второго запроса: две нити из одного списка используют одну отписку, а повторный вызов лишь подтверждает ваш адрес дважды. Отправитель запоминается только после того, как запрос действительно достиг конечной точки, так что отказ или оборванное соединение всё равно оставляют следующей нити её собственную попытку — и если пропущенная нить рекламирует другую конечную точку, в причине так и сказано, поскольку один отправитель может вести несколько рассылок. Эта память сохраняется между вызовами, пока работает сервер, и unsubscribe делится ею: вызов, истёкший по времени, можно безопасно повторить, а повторный запрос к одной и той же рассылке свяжется с отправителем один раз. Передайте force: true в unsubscribe для намеренной второй попытки — например, после того как конечная точка ответила 500. Она хранится только в памяти: её сохранение означало бы второй вид локального состояния рядом с токеном, который этот сервер намеренно не хранит, так что после перезапуска она забывается. Ограничение: 25 нитей и 60 секунд на вызов; то, что бюджет не покрыл, возвращается как skippedOutOfTime, а не молча пропускается. Ничто из этого нельзя отменить, поэтому все три ограничения и существуют.

  • Защита от SSRF. Только https, только порт по умолчанию, никаких учётных данных в URL, и каждый переход — включая редиректы, максимум 3 — должен разрешаться исключительно в глобально доступные адреса. Проверка разбирает каждый адрес до байтов и сверяет его с реестром специальных назначений IANA, так что любое написание одного и того же адреса получает одинаковый вердикт (как ::1, так и 0:0:0:0:0:0:0:1); адрес, который не удаётся разобрать, отклоняется. DNS-резолвинг и все переходы делят один 10-секундный бюджет. Не защищает от rebinding (fetch резолвит заново при подключении) — см. SECURITY.md; то, что переживает этот зазор, — слепой POST, чей ответ никогда не читается.

Проверьте это на своей почте, прежде чем доверять. Из клона репозитория (только репозиторий, не в npm-пакете), после npm run build и mailwarden --auth:

node scripts/probe-unsubscribe.mjs --vet          # category:promotions, 25 threads
node scripts/probe-unsubscribe.mjs "from:substack.com" --max 50 --vet

Он печатает каждый реальный заголовок List-Unsubscribe рядом с тем, что из него извлёк парсер, а --vet также прогоняет конечную точку через проверку URL и защиту адресов — так вы видите и то, понял ли парсер заголовок, и то, пропустили бы защитные механизмы эту отписку. Строго только чтение: запрос к отправителю никогда не выполняется, и в почтовом ящике ничего не меняется.

Что это не может отменить: запрос сообщает отправителю, что ваш адрес активен. Отправитель, который игнорирует собственную отписку, недосягаем для любого клиента — для таких случаев сочетайте unsubscribe с create_filter или trash. Отсутствие автоматизируемого варианта сообщается как unsubscribed:false с альтернативами, а не как ошибка. Развёртывание в режиме read-only получает list_unsubscribe и list_subscriptions и никогда не выполняет запрос вообще.

Поиск в спаме

Запрос, в котором не указано место, никогда не видит спам или корзину. Gmail исключает и то, и другое из любого поиска, в котором нет in:spam / in:trash, поэтому from:someone не возвращает ничего для письма, находящегося в папке спама — и в ответе об этом ничего не сказано. Проверено на живом почтовом ящике: тот же запрос from: вернул 0 совпадений по умолчанию и 1 со включённым спамом.

Это важно из-за того, почему письма попадают не туда. Спам-фильтр судит о сообщении сам по себе; он не может знать, что вы минуту назад подписались на что-то, запросили сброс пароля или разместили заказ — поэтому подтверждение, которого вы ждёте, — это как раз та почта, которая туда попадает. Вы знаете, что вы только что сделали. Фильтр не знает.

Поэтому когда ожидаемое письмо не находится, спросите снова, назвав место:

search("in:spam newer_than:2d")          # what got filed as spam recently
search("in:spam from:example.com")       # the confirmation that never arrived

Нить возвращается во входящие с помощью modify_labels (удалить SPAM, добавить INBOX), а отправителя, которого продолжают ошибочно классифицировать, лучше всего исправить насовсем правилом «никогда не в спам» — create_filter с removeLabels: ["SPAM"] (см. Фильтры).

Две вещи этот сервер намеренно не делает. Он не сканирует папку спама и не судит, что туда относится: по измерениям на одной реальной папке спама 89% писем не несут вообще никаких механизмов рассылок, так что «выглядит не как массовая рассылка» помечает почти всю папку и ничего не фильтрует. И он не действует на основе этого суждения сам по себе — освобождение письма из спама — это решение, а контекст, который делает его очевидным («я только что там зарегистрировался»), живёт в разговоре, а не в почтовом ящике.

Безопасность и конфиденциальность

Полная модель угроз — граница доверия, меры по каждой угрозе, явные не-цели и способы сообщить об уязвимости — см. SECURITY.md. Основное:

  • Никакой телеметрии. Ничего не отправляется наружу — ни аналитики, ни отчётов о сбоях, ни отслеживания.

  • Никаких открытых портов по умолчанию. Только stdio. Опциональный слушатель --http привязывается к 127.0.0.1 (не к LAN) и отказывается запускаться без bearer-токена MAILWARDEN_TOKEN — установите MAILWARDEN_ALLOW_NO_TOKEN=1, чтобы отключить это на доверенной изолированной сети. При привязке к loopback также проверяется заголовок Host (защита от DNS-rebinding). Для удалённого размещения установите MAILWARDEN_HOST и поместите перед ним TLS.

  • Никаких инструментов отправки — намеренно. mailwarden не может составлять, отвечать или пересылать письма. Инструкция, внедрённая в письмо через промпт, не имеет пути утечки через этот сервер. create_filter следует тому же правилу: он может помечать, архивировать, перемещать в корзину, ставить звезду или отмечать почту, но никогда не создаёт пересылающий фильтр (что было бы путём утечки). list_filters по-прежнему показывает любые уже существующие на аккаунте пересылающие фильтры, так что вы можете их заметить. Это работает, потому что такого инструмента не существует и ни один не может быть зарегистрирован во время выполнения; для более строгого варианта, когда Google отказывается отправлять, а не mailwarden, см. Режим только для чтения ниже.

  • Один исходящий хост, никаких URL, выбранных моделью. Инструмент unsubscribe — единственный путь кода, который связывается с не-Google хостом. Его конечная точка берётся из заголовка List-Unsubscribe письма — никогда из аргумента инструмента; тело запроса фиксировано, а тело ответа отбрасывается, так что он не может стать каналом передачи данных. Только https/стандартный порт, редиректы повторно проверяются, и любой переход, разрешающийся в частный, loopback, link-local или metadata-адрес, отклоняется. См. Отписка.

  • Уровни инструментов (прогрессивное раскрытие + минимальные права). MAILWARDEN_TOOLS анонсирует только те уровни, которые вы называете: read (инструменты чтения), manage (изменения почтового ящика, отложенные письма, загрузки), filters (CRUD серверных фильтров — единственный уровень, чьи инструменты требуют gmail.settings.basic). По умолчанию — все три; например, read,manage даёт полную поверхность для сортировки без управления фильтрами. Запрашиваемые при --auth OAuth-области определяются включёнными уровнями — развёртывание с read запрашивает только gmail.readonly, а gmail.settings.basic запрашивается только при включённом уровне filters. И инструменты фильтров автоматически скрываются, когда сохранённый токен не несёт gmail.settings.basic (например, токен, авторизованный до включения уровня), — повторно запустите --auth, чтобы выдать его. Более старые токены без записанной области анонсируются как прежде, а сообщение о недостаточности области во время выполнения служит запасным вариантом.

  • Режим только для чтения. Установите MAILWARDEN_READONLY=1 (сокращение для MAILWARDEN_TOOLS=read) — и будут зарегистрированы только инструменты чтения (search, get_thread, list_labels, list_snoozed, get_profile, triage_digest, list_unsubscribe, list_subscriptions) — клиентам не анонсируется даже ничего, что могло бы изменить почтовый ящик или записать файлы (инструменты фильтров, которым нужна более широкая область gmail.settings.basic, тоже исключены). Рекомендуется для общих/HTTP-развёртываний, которые только сортируют. Это также единственный уровень, чьё свойство «не отправляет» обеспечивает Google: он держит токен gmail.readonly, который отправляющие конечные точки Gmail отклоняют сразу. manage требует gmail.modify, а Gmail принимает эту область для отправки — mailwarden просто не предоставляет ни одного инструмента, который бы это делал. Поэтому развёртывание с read не смогло бы отправить, даже если бы этот бинарный файл был заменён; развёртывание с manage не может отправить, потому что нечего вызывать. (Области записи без права отправки, на которую можно было бы переключиться, не существует — см. SECURITY.md, угроза 1.)

  • Защита исходящего трафика. «Нечего вызывать» — больше не просто утверждение о списке инструментов. Каждый аутентифицированный запрос к Gmail проходит через одну контрольную точку, которая разрешает ровно те конечные точки, которые использует mailwarden, и отказывает остальным — причём messages.send, drafts.*, messages.import/insert, необратимое удаление и каждая конечная точка settings, не относящаяся к фильтрам, перечисленная в списке запрещённых, проверяются первыми, чтобы последующее редактирование списка разрешённых не могло случайно открыть их снова — в том числе через маршрут /upload/gmail/v1/..., по которому идёт googleapis, когда методу передаётся media. Запрос, чей хост был переписан (GOOGLE_CLOUD_UNIVERSE_DOMAIN, опция rootUrl), отклоняется до того, как токен покинет процесс. Каждый метод из discovery-документа Gmail проверяется на соответствие защите. Она защищает этот сервер, а не токен: украденный токен gmail.modify всё ещё может отправлять откуда-то ещё.

  • Ограниченные загрузки. Если задан MAILWARDEN_DOWNLOAD_DIR, запись вложений ограничена этим каталогом (канонизация через realpath, учёт симлинков) и никогда не перезаписывает существующий файл.

  • Изоляция недоверенного содержимого. Каждый результат инструмента оборачивается в маркеры <untrusted-tool-output> и очищается от невидимых символов и символов переопределения BiDi, чтобы клиенты могли отличить цитируемое содержимое письма от инструкций. Очистка также охватывает символы Unicode-тегов и дополнение вариативных селекторов (невидимую контрабанду ASCII) и применяется как к structuredContent, так и к текстовой копии — клиент, читающий машиночитаемую часть, получает то же очищенное содержимое.

  • Живой API, без копий. Нигде не хранится ни зеркало почтового ящика, ни поисковый индекс. Единственное локальное состояние — ваш OAuth-токен в ~/.mailwarden/.

  • Необязательное шифрование токена в состоянии покоя. В token.json хранится refresh-токен; на диске он защищён только mode 0o600 (на Windows не действует). Установите MAILWARDEN_TOKEN_PASSPHRASE в парольную фразу — и токен будет храниться в зашифрованном виде AES-256-GCM (ключ, полученный через scrypt), так что копия файла — резервная копия, синхронизируемая папка, другая машина — бесполезна без парольной фразы. После установки один раз повторно запустите mailwarden --auth, чтобы зашифровать существующий токен. Обратите внимание на границу: это защищает от кражи файла, не от вредоносного ПО, работающего от вашего пользователя (которое может прочитать парольную фразу и из окружения).

Быстрый старт

claude mcp add mailwarden -- npx -y mailwarden

Это вся установка — npx загружает и запускает опубликованный пакет, без клонирования и сборки. Google OAuth-учётные данные нужны только один раз (см. ниже).

Настройка

Впервые настраиваете Google OAuth-приложение? Следуйте пошаговому руководству по настройке — оно проводит по Google Cloud Console с точными путями кликов, объясняет экран «непроверенного приложения» и описывает ловушку, из-за которой токены умирают через 7 дней. Краткая версия:

  1. Google Cloud: создайте проект → включите Gmail API → настройте экран согласия OAuth и опубликуйте его в Production (в статусе Testing Google истекает refresh-токены через 7 дней) → создайте OAuth client ID типа Desktop app → скачайте его как credentials.json.

  2. Положите credentials.json в ~/.mailwarden/ (или задайте MAILWARDEN_CREDENTIALS=/path/to/credentials.json).

  3. Авторизуйтесь один раз — откроется браузер, refresh-токен будет сохранён в ~/.mailwarden/token.json:

    npx -y mailwarden --auth

    Запрашиваемые области: gmail.modify (чтение + метка/архив/корзина) и gmail.settings.basic (только управление фильтрами). Если вы авторизовали версию до появления фильтров, один раз повторно запустите --auth, чтобы выдать добавленную область. Чтобы получить токен, с которым сам Gmail откажется отправлять, авторизуйтесь с MAILWARDEN_TOOLS=read — см. Режим только для чтения выше.

  4. Проверяйте настройку в любое время с помощью встроенного доктора:

    npx -y mailwarden --check

    Он проверяет credentials.json, существует ли токен (и зашифрован ли он), покрывают ли выданные области ваши включённые уровни, и совершает один реальный вызов Gmail, чтобы доказать, что токен всё ещё работает, — печатает конкретное исправление для любой проблемы и завершается с ненулевым кодом, если таковая есть (удобно в CI/health-проверках). Диагностирует типичные ловушки: отсутствующий/wrong файл учётных данных, отсутствие авторизации, зашифрованный токен без MAILWARDEN_TOKEN_PASSPHRASE, отсутствующую область или истечение токена из-за 7-дневного согласия «Testing».

Подключение

Claude Code (локальный stdio):

claude mcp add mailwarden -- npx -y mailwarden

Плагин Claude Code — тот же сервер плюс навык /mailwarden:setup, который проводит вас через настройку OAuth и диагностирует неисправную настройку. Корень репозитория и есть плагин (.claude-plugin/plugin.json), поэтому из клона:

claude --plugin-dir /path/to/mailwarden

Он подан в community-маркетплейс Anthropic; как только появится в списке, /plugin marketplace add anthropics/claude-plugins-community, затем /plugin install mailwarden@claude-community сделают то же самое без клонирования. Плагин запускает полную поверхность инструментов — для более узкого уровня (MAILWARDEN_TOOLS=read) или второго аккаунта используйте вместо этого claude mcp add с нужным окружением (см. Конфигурация и Несколько аккаунтов).

Claude Desktop — добавьте в claude_desktop_config.json:

{
  "mcpServers": {
    "mailwarden": { "command": "npx", "args": ["-y", "mailwarden"] }
  }
}

Или установите пакет MCPB (mailwarden-<version>.mcpb, прикреплённый к релизам GitHub, начиная с 0.10.0) как расширение Desktop — Настройки → Расширения → Установить расширение… — тот же сервер, самодостаточный во время выполнения (без npx; Node-рантайм предоставляет Claude Desktop), с уровнями инструментов в качестве настройки. Пакет собран из упакованного npm-пакета (тот же набор файлов, что и опубликованный; npm run mcpb, проверено в CI: провалидирован, распакован и запущен) и представляет собой тот же набор файлов, который распространяет Smithery. Одноразовая npx -y mailwarden --auth по-прежнему применима (Node нужен один раз для этого) — пакет читает тот же токен ~/.mailwarden/.

Smithery — указан как csitte/mailwarden, который обслуживает этот пакет:

npx -y @smithery/cli install csitte/mailwarden --client claude   # local stdio entry in the client's config

Обратите внимание, какой из двух путей Smithery вы выбираете. Установка выше создаёт обычную локальную запись сервера: процесс, ваш токен и ваша почта остаются на вашей машине, точно так же, как с npx. Если вместо этого добавить его в инструментарий Smithery (smithery mcp add), пакет также запускается локально, но трафик инструментов передаётся через шлюз Smithery, чтобы до него мог добраться удалённый клиент, — содержимое почтового ящика в этих ответах затем проходит через третью сторону. Это свойство шлюза, а не mailwarden; если вам нужна гарантия отсутствия третьей стороны, используйте локальную установку, npm-пакет или .mcpb со страницы релизов.

Удалённый (Streaming HTTP) — для VPS / пользовательского коннектора claude.ai:

# Loopback + token required by default. For real hosting, bind outward and keep the token:
MAILWARDEN_TOKEN=<secret> MAILWARDEN_HOST=0.0.0.0 npx -y mailwarden --http   # :8787/mcp

Затем в claude.ai: Настройки → Коннекторы → Добавить пользовательский коннектор → ваш URL https://your-host/mcp. В Claude Code: claude mcp add --transport http mailwarden https://your-host/mcp.

Несколько аккаунтов

Одно OAuth-приложение (один credentials.json) может авторизовать несколько аккаунтов Gmail. Каждый аккаунт хранит собственный refresh-токен в отдельном файле, выбираемом через MAILWARDEN_ACCOUNT:

mailwarden --auth --account work        # stores token.work.json
mailwarden --auth --account personal    # stores token.personal.json

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

{
  "mcpServers": {
    "gmail-work":     { "command": "npx", "args": ["-y", "mailwarden"], "env": { "MAILWARDEN_ACCOUNT": "work" } },
    "gmail-personal": { "command": "npx", "args": ["-y", "mailwarden"], "env": { "MAILWARDEN_ACCOUNT": "personal" } }
  }
}

Имена аккаунтов не чувствительны к регистру — они становятся именами файлов, поэтому Work и work были бы одним и тем же файлом на Windows/macOS. mailwarden приводит их к нижнему регистру (--account Work → token.work.json), так что имя всегда сопоставляется ровно с одним почтовым ящиком.

Какой файл пишет --auth, зависит только от --account / MAILWARDEN_ACCOUNT — и никогда от аккаунта, который вы выбираете в браузере. Поэтому авторизация второго почтового ящика без --account была бы нацелена прямо на файл токена первого, так что --auth сначала проверяет и отказывается, а не заменяет токен другого аккаунта; --force отменяет это осознанно. Эти два рычага не взаимозаменяемы: MAILWARDEN_ACCOUNT предназначен для нескольких почтовых ящиков из одного каталога конфигурации (он выбирает token.<name>.json), тогда как MAILWARDEN_DIR перемещает весь каталог — удобно, чтобы полностью разнести настройки, но второй аккаунт внутри одного он не даёт. npm run auth из клона репозитория не передаёт ни того, ни другого, то есть всегда обслуживает аккаунт по умолчанию.

mailwarden --check показывает активный аккаунт и перечисляет остальные, которые он находит. Если MAILWARDEN_ACCOUNT не задан, всё использует стандартный token.json ровно как раньше — это полностью обратно совместимо.

Более двух аккаунтов

Две записи — простейший случай. Дальше начинают иметь значение два свойства этой архитектуры.

Каждый экземпляр приносит собственные инструменты. Разбиение на уровни: 8 read + 14 manage + 3 filters, поэтому полный экземпляр предлагает 25 инструментов, а четыре таких — 100. Клиенты, которые ищут в своём наборе инструментов по требованию, это выдерживают; клиенты, которые держат все определения в контексте, — нет.

Несколько аккаунтов в одном клиенте используют общий контекст модели. Граница аккаунта привязывает вызов к одному почтовому ящику — она не мешает тексту, прочитанному из одного ящика, побудить к вызову к другому, потому что все эти наборы инструментов находятся перед одной и той же моделью. Это ограничение границы, а не её дефект; см. угрозу 8 в SECURITY.md.

Одно решение отвечает на оба пункта: дайте инструменты записи ровно одному почтовому ящику, а остальные оставьте на read.

{
  "mcpServers": {
    "gmail-main":   { "command": "npx", "args": ["-y", "mailwarden"], "env": { "MAILWARDEN_ACCOUNT": "main" } },
    "gmail-work":   { "command": "npx", "args": ["-y", "mailwarden"], "env": { "MAILWARDEN_ACCOUNT": "work",   "MAILWARDEN_TOOLS": "read" } },
    "gmail-club":   { "command": "npx", "args": ["-y", "mailwarden"], "env": { "MAILWARDEN_ACCOUNT": "club",   "MAILWARDEN_TOOLS": "read" } },
    "gmail-archive":{ "command": "npx", "args": ["-y", "mailwarden"], "env": { "MAILWARDEN_ACCOUNT": "archive","MAILWARDEN_TOOLS": "read" } }
  }
}

Отсюда сразу вытекают три вещи: записи read запрашивают только gmail.readonly — единственную область, в которой запрет отправки обеспечивается Google, а не набором инструментов mailwarden; четыре экземпляра в сумме дают 49 инструментов, а не 100; и инструкция, внедрённая в любой из них, не находит инструмента записи, за который мог бы ухватиться другой ящик. Когда одному из ящиков только для чтения всё же нужна уборка, выдайте этой записи manage на время, необходимое для работы, а не навсегда.

Отдельные клиенты — или отдельные сессии — полностью убирают общий контекст, но ценой того, что два ящика никогда не видны одновременно. Это оправдано, когда нескольким ящикам действительно нужны инструменты записи; в противном случае разбиение на уровни — более дешёвая граница.

Ни один инструмент не читает через ящики. search, triage_digest и list_subscriptions — каждый обслуживает тот единственный аккаунт, с которым настроен его экземпляр, поэтому вопрос вроде «какая рассылка пишет во все четыре» — это четыре вызова, ответы на которые объединяет вызывающий. В такой конфигурации стоит вызвать get_profile перед первым действием, которое что-либо меняет, — он называет ящик, который на самом деле находится на другом конце.

Из исходного кода

git clone https://github.com/csitte/mailwarden && cd mailwarden
npm install && npm run build
node dist/index.js --auth

Конфигурация (env)

Var

Meaning

MAILWARDEN_DIR

директория конфигурации (по умолчанию ~/.mailwarden)

MAILWARDEN_CREDENTIALS

путь к credentials.json

MAILWARDEN_ACCOUNT

выбирает именованный аккаунт (его токен — token.<name>.json; имена приводятся к нижнему регистру); не задано = стандартный token.json. См. Несколько аккаунтов

MAILWARDEN_TOKEN_PASSPHRASE

парольная фраза → шифрует token.json в состоянии покоя (AES-256-GCM); после установки повторно запустите --auth

MAILWARDEN_AUTO_SWEEP

1 → подчистка отложенных при запуске и ежечасно во время работы (пишет метки — требуется область manage/gmail.modify; грант только на чтение не может подчищать)

MAILWARDEN_DOWNLOAD_DIR

ограничивает download_attachment этим каталогом (настоятельно рекомендуется для HTTP-хостинга)

MAILWARDEN_READONLY

1 → регистрировать только инструменты чтения (search/get_thread/list_labels/list_snoozed/get_profile/triage_digest/list_unsubscribe/list_subscriptions). Сокращение для MAILWARDEN_TOOLS=read

MAILWARDEN_TOOLS

разделённые запятыми уровни инструментов для объявления: read, manage, filters (по умолчанию: все). Также определяет области OAuth, запрашиваемые в --auth. Например, read,manage убирает инструменты фильтров и их область gmail.settings.basic

MAILWARDEN_DEBUG

1 → выводить полные ошибки с трассировками стека вместо однострочного сообщения (для отчётов об ошибках)

PORT

HTTP-порт (по умолчанию 8787)

MAILWARDEN_HOST

адрес привязки HTTP (по умолчанию 127.0.0.1; для удалённого хостинга укажите, например, 0.0.0.0)

MAILWARDEN_TOKEN

bearer-токен для HTTP-эндпоинта — обязателен для --http, если не переопределён

MAILWARDEN_ALLOW_NO_TOKEN

1 → разрешить --http без токена (только доверенные/изолированные сети)

MAILWARDEN_ALLOWED_HOSTS

дополнительные значения host:port через запятую, принимаемые белым списком Host для loopback

Статус

Работает и используется в повседневной автоматизации почты. Основные инструменты Gmail + snooze реализованы на googleapis, покрыты набором тестов vitest (1001 тест — npm run coverage). Текущая версия: см. npm-бейдж выше, журнал изменений или релизы. PR приветствуются — CONTRIBUTING.md описывает цикл сборки/тестирования и правила дизайна, которые не подлежат пересмотру.

Лицензия

MIT © C.Sitte Softwaretechnik

Available Tools

26 tools
archiveA
Idempotent

Archive a thread (remove it from the inbox). USE WHEN: inbox triage — the thread is handled and should leave the inbox. DO NOT USE: to delete (use trash) or to defer to a date (use snooze). SIDE EFFECTS: thread leaves the inbox; reversible via modify_labels add INBOX.

ParametersJSON Schema
NameRequiredDescriptionDefault
threadIdYes

Output Schema

ParametersJSON Schema
NameRequiredDescription
okYes

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Discloses side effect 'thread leaves the inbox' and reversibility via 'modify_labels add INBOX', adding value beyond annotations (idempotentHint=true, destructiveHint=false). No contradiction.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Concise, front-loaded with action, then usage, then side effects. Every sentence contributes meaning with no waste.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Covers purpose, usage guidelines, side effects, and reversibility; output schema exists so return values need not be explained. Complete for a simple tool.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Only one parameter (threadId) with 0% schema coverage, but the description does not elaborate on it beyond the schema; the parameter is self-explanatory, so minimal added value.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Clearly states 'Archive a thread (remove it from the inbox)' with specific verb and resource, and distinguishes from siblings 'trash' and 'snooze' by naming them.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly provides when-to-use ('inbox triage') and when-not-to-use ('to delete use trash, to defer use snooze'), with alternatives named.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

bulk_modifyA
DestructiveIdempotent

Bulk-apply label changes to every message matching a Gmail query, batched at 1000 messages per API request. Labels may be given by name or by id: an unknown name in add is created automatically (use '/' for nested labels), an unknown name in remove is ignored. Returns matched/submitted counts, matched and submitted thread IDs (both lists capped at 500 — matchedThreadCount/submittedThreadCount hold the true totals), and per-chunk failures (partial success is reported, not hidden). IMPORTANT: submittedMessages is how many ids were handed to the API, NOT how many messages changed — messages.batchModify answers 204 with no body and ignores ids it does not recognise without a word, so an accepted request is not a performed one. Set verify:true to read the labels back afterwards and get verified {applied, notApplied[], unverifiable[]} — the only field here that reports an observed outcome. It costs one extra read per affected thread, so it is off by default; use it when a wrong 'done' would be acted on (trashing, or anything the user is told completed). If more messages match than maxMessages, only the first maxMessages are processed and 'capped' is true — raise maxMessages or re-run to finish the rest. NOTE: the query hits Gmail's search index as-is, WITHOUT the live re-verification search performs. The staleness that makes search re-verify was measured on threads.list (132 threads returned, 114 carrying no unread message at all); the same query through the message index this tool uses returned 19 hits, none stale — same mailbox, same minute. So the known drift does not reach this path, but that is one measurement, not a guarantee: unverifiedPredicates in the result names the conditions taken on the index's word, and when the outcome must be read-state-precise, resolve the set with search (which verifies against live labels) and act on those thread ids instead. Set crossCheck:true to ask Gmail the same question a second way before writing: each derived predicate is re-run as a label filter (labelIds) instead of a query operator, and any message the two routes disagree about is left untouched and listed in crossChecked.dropped. It costs one extra list per predicate — flat, not per message — so unlike verify it stays cheap on a large sweep. Read it as a contradiction detector: a disagreement is real, agreement proves nothing, because both routes read the same index. unverifiedPredicates therefore stays as it is even when this runs. A capped match set is not cross-checked at all (crossChecked.capped), since a message missing from a page is not a message missing the label. Set dryRun:true to rehearse: the same query resolution, matched counts/threads and the labels that would be created — and no message or label is touched. A dry run reads the SAME unverified index, so it confirms the size of the set, never its correctness. USE WHEN: mass operations — 'archive all newsletters older than 30 days' (query + remove INBOX), bulk labeling, bulk mark-read; dryRun first when the query is broad or the user should see the set before it changes. DO NOT USE: for a single thread (use modify_labels or the dedicated tools), or with neither add nor remove. SIDE EFFECTS: modifies up to maxMessages messages in one call (none with dryRun); label changes are reversible by the inverse call.

ParametersJSON Schema
NameRequiredDescriptionDefault
addNo
queryYes
dryRunNo
removeNo
verifyNo
crossCheckNo
maxMessagesNo

Output Schema

ParametersJSON Schema
NameRequiredDescription
cappedYes
dryRunYes
failedYes
verifiedNo
crossCheckedNo
labelsToCreateNo
matchedThreadsYes
matchedMessagesYes
submittedThreadsYes
submittedMessagesYes
matchedThreadCountYes
submittedThreadCountYes
unverifiedPredicatesYes

TDQS

A5/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The description goes far beyond the annotations by disclosing partial success reporting, the 204-with-no-body caveat of messages.batchModify, the difference between submitted and applied changes, verification costs, index staleness measurements, and the exact meaning of verified and crossChecked results. The destructiveHint and idempotentHint annotations are consistent with the described reversible but mutating behavior.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Though long, the description is information-dense with no filler; every sentence addresses a real operational concern for a high-risk bulk mutation tool. It is structured with clear sections (IMPORTANT, USE WHEN, DO NOT USE, SIDE EFFECTS) that make the content scannable, and the core purpose is front-loaded.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given 7 parameters, bulk effects, subtle Gmail API behavior, and a rich output schema, the description is exceptionally complete: it covers result fields, capping behavior, failure reporting, verification options, costs, index staleness, alternatives, and side effects. An agent has enough context to decide whether to call it, set flags appropriately, and interpret results correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description fully compensates by explaining every parameter: query, add/remove label creation and unknown-name handling, maxMessages capping, dryRun rehearsal behavior, verify's read-back semantics, and crossCheck's second query route. It adds meaning far beyond the bare schema types and defaults.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific verb and resource: 'Bulk-apply label changes to every message matching a Gmail query, batched at 1000 messages per API request.' It clearly differentiates from sibling tools in DO NOT USE, naming modify_labels and dedicated single-thread tools, so an agent can select it correctly.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The USE WHEN section explicitly lists the intended scenarios ('mass operations — archive all newsletters older than 30 days, bulk labeling, bulk mark-read') and advises dryRun first for broad queries. The DO NOT USE section gives concrete exclusions and alternatives, including not using it for a single thread and requiring at least one of add or remove.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

bulk_unsubscribeA
Idempotent

Unsubscribe from several mailing lists in one call, one thread id per list. Threads are processed SEQUENTIALLY, and at most ONE request is made per sender — a second thread from a sender whose request already went out is reported with duplicateOf and no request (and says so when it advertises a DIFFERENT endpoint, i.e. is probably a separate list from the same sender). The whole call shares a 60-second budget; threads left over when it runs out come back with skippedOutOfTime and a reason, so re-running with the remaining ids finishes the job. Like unsubscribe, there is no URL parameter: every endpoint comes from that thread's own List-Unsubscribe header. Only RFC 8058 one-click senders are contacted; the rest come back with their alternatives in options. Partial success is reported, never hidden: a thread that cannot be read or whose endpoint fails becomes an entry with a reason, and the remaining threads still run. Set dryRun:true to rehearse: same header reads, same per-sender dedupe (as a real run with every request succeeding), and each entry a real run would contact reports the endpoint it wouldCall; refusals and duplicates carry none — and nobody is contacted. USE WHEN: clearing out several newsletters at once — pair with list_subscriptions, which gives you the sender rows and their newestThreadId; dryRun first to show the user which senders would be contacted. DO NOT USE: for one thread (use unsubscribe), or to find candidates (use list_subscriptions — it contacts nobody). SIDE EFFECTS: up to one outbound HTTPS request per DISTINCT sender (plus up to 3 redirects each) — the only non-Google hosts mailwarden ever contacts (none with dryRun). Each confirms to that sender that the address is live, and none of it can be undone. The mailbox itself is not changed.

ParametersJSON Schema
NameRequiredDescriptionDefault
dryRunNo
threadIdsYes

Output Schema

ParametersJSON Schema
NameRequiredDescription
dryRunYes
resultsYes
requestsYes
attemptedYes
requestedYes
unsubscribedYes
skippedOutOfTimeYes
skippedDuplicatesYes

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Discloses sequential processing, per-sender deduplication, a 60-second time budget, dryRun behavior, external HTTP requests (with redirects), confirmation of address liveness, and irreversibility. These details go well beyond the annotations (readOnlyHint:false, openWorldHint:true, idempotentHint:true) and provide necessary caveats about side effects. No contradiction with annotations is apparent.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is lengthy but well-structured with bolded section headers (USE WHEN, DO NOT USE, SIDE EFFECTS). It front-loads the core purpose and packs each sentence with meaningful details. Slightly verbose, but each part contributes to understanding a complex tool.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Covers all major edge cases: duplicateOf, skippedOutOfTime, options for non-RFC 8058, partial failure handling, dryRun output, and side effects. Given the complexity and the existence of an output schema, the description provides comprehensive operational knowledge for correct usage.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Although schema coverage is 0%, the description explains the meaning of threadIds ('one thread id per list') and dryRun ('rehearse' with no contact). It also clarifies that no URL parameter is used and that endpoint comes from List-Unsubscribe headers. This adds useful context beyond the raw schema, though it could be more explicit about max 25 items and preconditions.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the action (unsubscribe) and the scope (several mailing lists in one call, one thread id per list). It distinguishes itself from the sibling tool 'unsubscribe' by explicitly noting it is for multiple threads, and the phrase 'one thread id per list' adds specificity.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Provides explicit 'USE WHEN' and 'DO NOT USE' sections, naming alternative tools (list_subscriptions for candidate discovery, unsubscribe for single thread) and pairing recommendations with list_subscriptions. This gives agents clear decision criteria.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

create_filterA
Destructive

Create a Gmail filter: matching incoming mail automatically gets the given label actions. Give at least one criterion and at least one action. Actions are label add/remove only (labels by name or id; an unknown name in addLabels is auto-created). Common recipes: skip the inbox → removeLabels ['INBOX']; auto-mark-read → removeLabels ['UNREAD']; auto-trash → addLabels ['TRASH']; star → addLabels ['STARRED']; file under a label → addLabels ['Receipts']. A filter only affects mail arriving AFTER it's created; set applyToExisting:true to ALSO apply the same actions once to mail already in the mailbox (builds a Gmail search from the criteria and runs a bulk modify — same unverified-index caveat as bulk_modify — the sweep acts on what the index returns, which can be badly stale on read state; up to maxMessages, default 1000). The sweep's applied.submittedMessages is how many ids were handed to the API, NOT how many messages changed; set verify:true alongside applyToExisting to read the labels back afterwards and get applied.verified {applied, notApplied[], unverifiable[]}. It costs one extra read per affected thread, so it is off by default; a read-back that fails lands in unverifiable and never fails the call — the filter stands either way. USE WHEN: setting up a persistent auto-triage rule (e.g. 'always archive + label newsletters from x'), optionally cleaning up the existing backlog too. NOTE: forwarding filters are intentionally not supported — mailwarden creates no send/exfiltration path. SIDE EFFECTS: adds a server-side rule affecting future mail (reversible via delete_filter); with applyToExisting also modifies existing messages. Requires gmail.settings.basic.

ParametersJSON Schema
NameRequiredDescriptionDefault
toNo
fromNo
sizeNo
queryNo
verifyNo
subjectNo
addLabelsNo
maxMessagesNo
excludeChatsNo
negatedQueryNo
removeLabelsNo
hasAttachmentNo
sizeComparisonNo
applyToExistingNo

Output Schema

ParametersJSON Schema
NameRequiredDescription
idYes
appliedYes
forwardNo
criteriaYes
addLabelIdsYes
removeLabelIdsYes

TDQS

A4.6/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The description goes far beyond the annotations. It explains that a filter affects only mail arriving after creation, that applyToExisting runs a bulk sweep with a stale-index caveat, that submittedMessages is not the number of changed messages, that verify costs an extra read and failures are non-fatal, that labels can auto-create unknown names, and that the operation requires gmail.settings.basic. This is rich, honest behavioral disclosure consistent with destructiveHint=true.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is long, but nearly every sentence adds a distinct operational detail: core semantics, common recipes, applyToExisting caveats, verification behavior, use case, and side effects. It is front-loaded with the primary purpose and structured with labeled sections such as 'USE WHEN' and 'SIDE EFFECTS'. It could be tighter, but the density is justified for a tool with 14 parameters and subtle edge cases.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a complex tool with 14 parameters, no required fields, and an output schema, this description is exceptionally complete. It covers what happens on creation, how existing mail is handled, the semantics of verification, failure behavior, permission requirements, reversibility, and unsupported forwarding. An agent can safely construct a valid call and predict the consequences; the output schema can fill in remaining return-value details.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

With 0% schema description coverage, the description carries the semantic burden and does so well for the action parameters: it defines addLabels/removeLabels semantics, gives concrete recipes like removeLabels ['INBOX'] and addLabels ['TRASH'], and explains applyToExisting, maxMessages, and verify. The criteria parameters (from, to, subject, query, etc.) are not individually detailed, but their Gmail meanings are conventional and the overall parameter guidance is well above the minimum viable level.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific verb-resource pair — 'Create a Gmail filter' — and immediately states what the filter does: matching incoming mail automatically gets label actions. It also distinguishes the tool from siblings by emphasizing persistent auto-triage rules and explicitly noting that forwarding filters are not supported, so there is no confusion with bulk_modify, modify_labels, or other Gmail mutators.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

There is a clear 'USE WHEN' statement defining the intended scenario: setting up a persistent auto-triage rule, with the optional applyToExisting use case explained. It also gives an explicit 'not supported' exclusion for forwarding. It stops short of naming a sibling alternative such as bulk_modify for one-time bulk label changes, but the context is strong enough for an agent to choose this tool for filter creation.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

create_labelA
Idempotent

Create a user label and return its id. Idempotent: if the name already exists (case-insensitive), its existing id is returned and nothing is created. Nested labels: separate levels with '/' (e.g. 'Clients/Acme') — each missing parent level is created too. Colour: pass backgroundColor AND textColor together as '#rrggbb' (Gmail rejects one without the other, and accepts only colours from its own palette). Giving a colour for a label that already exists RECOLOURS it — that is how an existing label, such as the snooze label, gets a colour. Gmail colours only labels you created yourself; system labels like INBOX or CATEGORY_UPDATES refuse one. USE WHEN: you want a label's id up front, to pre-create a label without applying it to anything, or to make an existing label visible at a glance in Gmail. DO NOT USE: just to file mail under a new label — modify_labels/bulk_modify already auto-create an unknown name passed in add. SIDE EFFECTS: creates the label if missing, and sets its colour when one is given; no mail is changed.

ParametersJSON Schema
NameRequiredDescriptionDefault
nameYes
textColorNo
backgroundColorNo

Output Schema

ParametersJSON Schema
NameRequiredDescription
idYes
nameYes
colorNo

TDQS

A5/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The annotations only declare idempotentHint=true, readOnlyHint=false, and destructiveHint=false. The description goes far beyond this by explaining case-insensitive name matching, nested label creation, recoloring existing labels, system label color restrictions, and the fact that no mail is changed. This is rich behavioral context with no contradiction.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is long but every section earns its place: core action, idempotency, nesting, color behavior, usage guidance, and side effects. It is front-loaded with the most important information and uses clear section markers to keep the density navigable.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the complexity of label creation in Gmail, the description covers all relevant caveats: idempotency, color pairing, palette restrictions, recoloring, system label refusals, and side effects. Since an output schema exists, the return value need not be further explained. Nothing essential is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description carries the full burden of explaining parameters. It explains name with nesting syntax, and describes that textColor and backgroundColor must be passed together as '#rrggbb' and must come from Gmail's palette. This is far more than the schema provides.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific verb and resource: 'Create a user label and return its id.' It also distinguishes itself from modify_labels by explicitly stating that filing mail under a new label should use modify_labels/bulk_modify, which auto-create unknown names.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description includes explicit USE WHEN and DO NOT USE sections, giving clear conditions for tool selection. It names the exact alternative tools (modify_labels/bulk_modify) and explains when they should be used instead, leaving no ambiguity.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

delete_filterA

Delete a Gmail filter by id (get ids from list_filters). USE WHEN: removing an auto-triage rule. SIDE EFFECTS: removes the server-side rule; future mail is no longer auto-processed by it. Requires gmail.settings.basic.

ParametersJSON Schema
NameRequiredDescriptionDefault
idYes

Output Schema

ParametersJSON Schema
NameRequiredDescription
okYes

TDQS

A3.8/5.0
Behavior1/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotation Contradiction: The description says 'removes the server-side rule' and the tool is named delete_filter, indicating destructive behavior, yet annotations declare destructiveHint=false. This is a direct contradiction and severely undermines trust in safety-critical metadata.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is compact and well-structured, with labeled sections (USE WHEN, SIDE EFFECTS) that immediately convey the core action, when to use it, and consequences. Every sentence contributes valuable information without redundancy.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a simple single-parameter delete operation, the description covers purpose, usage context, side effects, and required permissions. An output schema exists so return values are not required. It lacks explicit error-handling/idempotency details, but annotations and simplicity compensate.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

With zero schema description coverage, the description adds essential meaning to the 'id' parameter by explaining it is a Gmail filter id obtainable from list_filters. This gives the agent a clear source and purpose, going beyond just the schema's string type.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description starts with 'Delete a Gmail filter by id', a specific verb and resource, and directly distinguishes itself from sibling tools like create_filter and list_filters by noting how to obtain ids. It clearly states the tool's scope.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The 'USE WHEN: removing an auto-triage rule' provides a clear context. It also instructs the agent to get ids from list_filters, implicitly guiding when to use this tool vs its sibling. However, it does not explicitly state when not to use it or mention alternatives beyond listing filters.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

download_attachmentA

Download an attachment to a local file path. If MAILWARDEN_DOWNLOAD_DIR is set, destPath is resolved inside (and restricted to) that directory. USE WHEN: the user wants an attachment saved to disk (IDs come from get_thread's attachment metadata). SIDE EFFECTS: writes a local file; never overwrites — an existing file gets a numeric suffix (file-1.pdf). The response's 'saved' field is the path actually used. Mailbox unchanged.

ParametersJSON Schema
NameRequiredDescriptionDefault
destPathYes
messageIdYes
attachmentIdYes

Output Schema

ParametersJSON Schema
NameRequiredDescription
savedYes

TDQS

A4.4/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Discloses critical behaviors beyond annotations: writes a local file, never overwrites (numeric suffix file-1.pdf), explains the MAILWARDEN_DOWNLOAD_DIR path restriction, confirms mailbox unchanged. The 'saved' field semantics are explained. Slight gap: doesn't mention permission/error behavior for the download operation, but coverage is strong given annotations already signal non-readOnly, non-destructive, non-idempotent.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Four tight sentences, each earning its place: definition, directory restriction, usage trigger, side effects with concrete example, and response field meaning. Zero waste, well front-loaded with the primary action.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a non-readOnly, non-idempotent write tool with zero param documentation, the description provides strong behavioral disclosure: overwrite policy, path resolution, mailbox unaffected. An output schema exists, so return details needn't be enumerated. Minor gap: no mention of failure modes (unresolvable path, missing attachment) but solid overall.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 0% and there are 3 required parameters with no descriptions. The description does explain destPath semantics well (directory resolution, suffix behavior) and touches on IDs coming from get_thread. However, messageId and attachmentId meanings are only implied via the get_thread reference, not explicitly defined. Description partially compensates but doesn't fully cover all three params.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Clear verb+object: 'Download an attachment to a local file path.' It distinguishes from siblings by noting IDs come from get_thread's attachment metadata, explicitly differentiating from search/read-only tools. Purpose is immediately unambiguous.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicit 'USE WHEN' statement defines the trigger condition (user wants attachment saved to disk) and cites the exact source of input IDs (get_thread's attachment metadata). This gives strong selection guidance among the sibling tools.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_profileA
Read-only

Return the authorized account: email address plus total message/thread counts. USE WHEN: confirming WHICH mailbox is connected before a bulk or filter action, or as a cheap liveness check. DO NOT USE: to enumerate messages — this returns only counts, not a listing (use search). SIDE EFFECTS: none.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

Output Schema

ParametersJSON Schema
NameRequiredDescription
historyIdYes
emailAddressYes
threadsTotalYes
messagesTotalYes

TDQS

A4.7/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint=true, and the description reinforces this with 'SIDE EFFECTS: none.' It adds useful behavioral context beyond the annotation by stating the tool returns only counts, not a message listing, and positions it as a liveness check.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is compact and well-structured: the core function is first, followed by USE WHEN, DO NOT USE, and SIDE EFFECTS. Every sentence carries actionable information with no redundancy.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a simple no-parameter, read-only tool with an output schema present, the description provides all necessary context: what it returns, when to use it, when not to use it, and that there are no side effects. Nothing important is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool has zero parameters and full schema description coverage, so there are no parameter semantics to document. The description usefully clarifies what the response contains, satisfying the baseline for no-parameter tools.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific verb and resource ('Return the authorized account') and specifies exactly what is returned: email address plus total message/thread counts. It also explicitly distinguishes itself from message enumeration by pointing to 'search' as the tool for listings.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides explicit USE WHEN guidance (confirming which mailbox is connected, cheap liveness check) and explicit DO NOT USE guidance (enumerating messages), with an alternative named. This gives clear decision criteria for an agent.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_threadA
Read-only

Fetch a thread by ID: headers, plaintext + HTML bodies, and attachment metadata. full defaults to true and should stay true whenever content matters. full: false is a headers-and-labels fetch for when a thread is too large to read or only its metadata is of interest — it does NOT fetch bodies or attachment metadata, and the result then omits plaintextBody, htmlBody and attachments entirely and sets metadataOnly: true. The fields are omitted rather than empty on purpose: an empty attachment list from a request that never looked is indistinguishable from a message that truly has none, and treating it as 'no attachment' has already nearly caused an invoice to be archived as attachment-less. If search reported hasAttachments: true, or the sender matters, use full: true. EVERY MESSAGE CARRIES authentication — SPF/DKIM/DMARC as the RECEIVING server reported them, for answering 'is this mail really from who it says?'. Read dmarc first: it is the only one of the three that ties a passing check to the visible From domain, so spf: pass alone proves nothing about the sender the user sees (a lookalike domain gets that trivially). signedBy/mailedBy/headerFrom name the domains each check actually validated. They do NOT have to match each other or the From address, and a mismatch is not by itself suspicious: a forwarded message legitimately shows the forwarder in mailedBy (the envelope sender is rewritten in transit) and in signedBy (the forwarder signs with its own key), while only headerFrom still names the original sender — which is exactly why dmarc is the result that carries meaning. Read those three as the explanation of a result, not as a test of their own. authservId says WHO asserts all this: a message can carry forged Authentication-Results headers of its own, and only the receiving server's report (for Gmail, mx.google.com) counts; otherReports counts further reports that were NOT read, and alsoReported lists results that disagree with the ones above. unchecked: true means the message carried no report at all — that is 'nobody looked', NOT 'nothing wrong'; unauthenticated does not imply forged, and authenticated does not imply honest (a phisher can hold a passing DMARC on his own lookalike domain). USE WHEN: reading a thread's content after finding it via search. DO NOT USE: with a message ID — this takes thread IDs. SIDE EFFECTS: none (does not mark as read).

ParametersJSON Schema
NameRequiredDescriptionDefault
fullNo
threadIdYes

Output Schema

ParametersJSON Schema
NameRequiredDescription
messagesYes
threadIdYes
metadataOnlyNo

TDQS

A4.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare `readOnlyHint: true`, and the description reinforces this with 'SIDE EFFECTS: none (does not mark as read).' It goes well beyond annotations by disclosing that `full: false` omits fields entirely and sets `metadataOnly: true`, explaining the rationale for omission rather than empty lists. It also warns about authentication-header forgery and how to interpret `authservId`, which is valuable behavioral context for downstream decisions.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is front-loaded and well structured, with the core purpose in the first sentence and clear USE WHEN / DO NOT USE / SIDE EFFECTS sections. It is verbose, especially the detailed authentication-fields discussion, which goes beyond what is strictly needed for tool selection and invocation. Still, the extra detail is purposeful and organized rather than redundant.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the output schema exists and the two-parameter input schema is minimal, the description is thorough: it covers both parameters, side effects, use cases, exclusions, and even edge-case semantics like omitted fields and authentication-result interpretation. An agent has everything needed to call this tool correctly and interpret the result safely.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description carries full parameter-semantics responsibility. It thoroughly explains `full` semantics, including defaults, behavior differences, omitted fields, and when to set it. For `threadId`, it clarifies that this tool expects thread IDs and not message IDs. Both parameters are meaningfully documented beyond the bare schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The first sentence states a specific verb and resource: 'Fetch a thread by ID: headers, plaintext + HTML bodies, and attachment metadata.' It also distinguishes this from message-level or search tools with 'DO NOT USE: with a message ID — this takes thread IDs.' The purpose is unmistakable and well differentiated from siblings.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description gives explicit usage guidance: 'USE WHEN: reading a thread's content after finding it via search' and 'DO NOT USE: with a message ID.' It also provides clear conditions for when to use `full: true` vs `full: false`, including the case where search reported `hasAttachments: true`. This is exactly the kind of actionable selection guidance an agent needs.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

list_filtersA
Read-only

List all Gmail filters — the server-side rules that auto-apply label actions to incoming mail. Shows each filter's criteria and label actions, and (for auditing) any forward address an existing filter carries. USE WHEN: reviewing existing automation, or to get a filter's id before delete_filter. SIDE EFFECTS: none. Requires the gmail.settings.basic scope — re-run mailwarden --auth if you authorized an earlier version.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

Output Schema

ParametersJSON Schema
NameRequiredDescription
filtersYes

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The description explicitly declares 'SIDE EFFECTS: none' and adds the required scope ('gmail.settings.basic') with a re-authentication note ('re-run mailwarden --auth'). This goes beyond the readOnlyHint annotation by providing operational auth context and confirming no side effects, which is valuable for an agent deciding to invoke the tool.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is concise, structured with clear segments (purpose, output details, usage, side effects/scope), and front-loaded with the core action. Every sentence earns its place without repetition, and the formatting improves readability.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given zero parameters and an existing output schema, the description provides ample context: it explains what filters are, what information is returned, when to use the tool, that there are no side effects, and what scope is required. This is complete for a simple list tool with no input complexity.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

There are zero parameters, so the baseline is 4 per the rubric. The description adds no parameter-specific details but instead clarifies what the tool returns (criteria, label actions, forward addresses), which compensates for the lack of parameters and enriches the agent's understanding of the output.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the action ('List all Gmail filters') and the resource (filters), with a definition of what filters are and what information is shown (criteria, label actions, forward addresses). It distinguishes the tool from siblings like create_filter and delete_filter by focusing on listing and auditing existing filters.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides explicit 'USE WHEN' guidance: reviewing existing automation or getting a filter's id before delete_filter. It gives clear context but does not explicitly name alternative tools for exclusion, so it falls short of the highest calibration example. The intent is clear enough for an agent to select this tool appropriately.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

list_labelsA
Read-only

List all Gmail labels (system + user). USE WHEN: inspecting the mailbox structure, or to get exact label names/ids — though modify_labels/bulk_modify/create_label all accept a plain label name directly, so a lookup is rarely required. SIDE EFFECTS: none.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

Output Schema

ParametersJSON Schema
NameRequiredDescription
labelsYes

TDQS

A4.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations include readOnlyHint=true, and the description adds the explicit 'SIDE EFFECTS: none' statement, reinforcing the safe read-only nature. It also provides behavioral context that the listing includes both system and user labels, which is not in the structured annotations. This adds value beyond the annotations without contradicting them.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is extremely concise: two short segments ('List all Gmail labels (system + user)' and 'SIDE EFFECTS: none') plus a focused usage clause. It is front-loaded with the core action, with no wasted words, making it easy to parse quickly.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's simplicity (no parameters, read-only, output schema exists), the description covers all necessary context: purpose, usage, side effects, and scope. It explicitly mentions the labels' scope (system + user) and when to use it, making it fully complete for an agent to select and invoke correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool has zero parameters, and the schema is empty, so schema description coverage is 100%. With 0 params, the baseline is 4. The description doesn't discuss parameters because none exist, which is appropriate. No additional parameter semantics are needed.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's function: 'List all Gmail labels (system + user).' It uses a specific verb ('List') and resource ('Gmail labels'), and the scope is explicit. This distinguishes it from sibling tools like modify_labels, bulk_modify, and create_label, which perform mutations.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description includes an explicit 'USE WHEN' clause, specifying appropriate contexts (inspecting mailbox structure, getting exact label names/ids). It also names alternative tools (modify_labels/bulk_modify/create_label) and notes that a lookup is rarely required because they accept plain label names. This is clear guidance on when to use the tool vs. alternatives.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

list_snoozedA
Read-only

List all snoozed threads with their due dates. SIDE EFFECTS: none.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

Output Schema

ParametersJSON Schema
NameRequiredDescription
snoozedYes

TDQS

A4/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Explicitly states 'SIDE EFFECTS: none', which complements the readOnlyHint=true annotation and adds clarity beyond the annotation alone.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two sentences with no fluff. Front-loads the main function and includes side-effect note concisely.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given no parameters and an existing output schema, the description adequately covers the tool's behavior. Mentions 'due dates' hinting at output content.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

No parameters exist, so baseline is 4. The description adds no param info, but none is needed.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool lists snoozed threads with due dates, distinguishing it from sibling tools like snooze or trash which perform actions.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

No guidance on when to use this tool versus alternatives such as search or sweep_snoozed. The description is purely descriptive without usage context.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

list_subscriptionsA
Read-only

Who keeps writing, how often, and whether you can get off the list — a mailbox slice grouped by SENDER, each row carrying its opt-out options. Contacts nobody: opt-out options come from the List-Unsubscribe header of each sender's newest thread (one metadata fetch per sender, not per thread). optOut is 'one-click' (the unsubscribe tool can perform it), 'link' (a human opens it in a browser), 'mailto' (would need sending, which mailwarden never does), 'none', or 'unknown' when that sender's header fetch failed or was never made because Gmail's quota ran out mid-listing. oldestDate/newestDate bound what the SAMPLE saw of that sender, not the sender's whole history — a query capped at max reaches back only as far as those threads go, which on a busy mailbox is days. There is deliberately NO precomputed frequency: judge it from threads across that span, with the sampling caveat in view. newestThreadId is what to hand to unsubscribe or bulk_unsubscribe. sendersFound is how many DISTINCT senders the sample held — when it exceeds topN, the list is truncated and raising topN shows more. USE WHEN: 'what am I subscribed to', 'which newsletters flood me', or picking targets before a bulk unsubscribe. DO NOT USE: for a general inbox overview (use triage_digest — it buckets by label and age too), or for one known thread (use list_unsubscribe). SIDE EFFECTS: none.

ParametersJSON Schema
NameRequiredDescriptionDefault
maxNo
topNNo
queryNocategory:promotions

Output Schema

ParametersJSON Schema
NameRequiredDescription
queryYes
hasMoreYes
sampledYes
sendersFoundYes
subscriptionsYes

TDQS

A4.3/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations provide readOnlyHint=true, but the description goes well beyond that, disclosing that opt-out options come from metadata fetches, that Gmail quota exhaustion can produce 'unknown', that dates bound the sample not the sender's history, and that there is deliberately no precomputed frequency. These are real behavioral traits an agent needs to interpret results correctly.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness3/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is dense and front-loads the purpose, but it runs long with several sentences that could be tightened, and the flow moves between purpose, mechanics, field semantics, and usage guidance without clean visual separation. Every sentence earns its place informationally, but the size is above ideal conciseness.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

An output schema exists, so return values needn't be fully explained, and the description covers most of what's missing: field semantics for optOut, oldestDate/newestDate, newestThreadId, sendersFound, and sampling caveats. It's comprehensive for a 3-param list tool, though the query parameter's behavior could be explained slightly more.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description must compensate, and it does substantially: it explains that max bounds the sample ('a query capped at max reaches back only as far as those threads go'), that topN truncation happens when sendersFound exceeds topN, and implies the query parameter's role through the 'mailbox slice' framing. The query parameter itself ('category:promotions' default) isn't explicitly discussed, leaving a minor gap.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly establishes the resource (sender-grouped mailbox slice with opt-out options) and distinguishes itself from siblings by naming triage_digest and list_unsubscribe as alternatives. The opening is slightly metaphorical ('Who keeps writing, how often, whether you can get off the list'), but it resolves into a precise function. It's clear what the tool does, though the verb is less direct than an ideal 5.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicit 'USE WHEN' and 'DO NOT USE' sections name the exact scenarios and the sibling tools to prefer in each case: triage_digest for general inbox overview, list_unsubscribe for one known thread. This is textbook when/when-not guidance with alternatives.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

list_unsubscribeA
Read-only

Report the opt-out options a thread advertises (List-Unsubscribe / RFC 8058), without contacting anyone. Reads the newest message that carries the header, so a reply threaded onto a newsletter does not hide it. oneClick means the sender supports the automatable one-click opt-out — the unsubscribe tool can perform it. httpsUrls without oneClick are links for a human to open in a browser; mailtos would require sending mail, which mailwarden never does. bodyCandidates appears only when the headers advertise nothing: unsubscribe links found in the message TEXT, which is written by the sender and therefore untrusted. Show them to the user to open; never fetch one, and do not treat them as an opt-out that happened. hasUnsubscribe stays false for them — it describes the headers. USE WHEN: checking whether a newsletter can be unsubscribed from, or showing the user the link to click. SIDE EFFECTS: none — no request is made to the sender.

ParametersJSON Schema
NameRequiredDescriptionDefault
threadIdYes

Output Schema

ParametersJSON Schema
NameRequiredDescription
fromYes
mailtosYes
subjectYes
oneClickYes
threadIdYes
httpsUrlsYes
messageIdYes
bodyCandidatesYes
hasUnsubscribeYes

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint=true, and the description strongly reinforces this with 'without contacting anyone' and 'SIDE EFFECTS: none — no request is made to the sender.' It also discloses important nuances: it reads the newest message carrying the header, bodyCandidates come from untrusted message text, and they must not be treated as an actual opt-out. This goes well beyond the annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is long but every major section earns its place: field meanings, safety warnings, use cases, and side effects. It is front-loaded with the core purpose before diving into details. There is minor redundancy between 'without contacting anyone' and 'SIDE EFFECTS: none,' but overall the length is justified by the nuanced output behavior.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The description covers the tool's purpose, when to use it, how it behaves regarding headers and body text, safety constraints, side effects, and relationships to sibling tools. It also explains the special cases of `hasUnsubscribe`, `bodyCandidates`, and untrusted content. Given the output schema exists, no important gap remains for an agent to invoke this tool correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The schema only defines `threadId` as a required string with no description, so the description carries the burden. It adds meaning by explaining that the tool operates on a thread and reads the newest message in that thread, even noting how a reply threaded onto a newsletter is handled. This gives enough context for an agent to understand what `threadId` represents, though no explicit format guidance is provided.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific verb and resource: 'Report the opt-out options a thread advertises (List-Unsubscribe / RFC 8058), without contacting anyone.' It clarifies this is a read-only inspection tool and distinguishes it from the sibling `unsubscribe` tool by explicitly noting that `unsubscribe` can perform the one-click opt-out.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides an explicit 'USE WHEN' section: 'checking whether a newsletter can be unsubscribed from, or showing the user the link to click.' It also gives alternative routing by pointing to the `unsubscribe` tool for oneClick cases and instructs the agent to never fetch bodyCandidates, establishing clear boundaries for safe use.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

mark_readA
Idempotent

Mark a thread as read. SIDE EFFECTS: removes UNREAD; reversible via mark_unread.

ParametersJSON Schema
NameRequiredDescriptionDefault
threadIdYes

Output Schema

ParametersJSON Schema
NameRequiredDescription
okYes

TDQS

A4.5/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The description goes beyond annotations by detailing the specific side effect and reversibility, fully disclosing the tool's behavior.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is concise with two sentences, front-loading the purpose and listing side effects efficiently.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a simple tool with one parameter, clear annotations, and an output schema, the description covers all necessary context including side effects and reversibility.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The single parameter 'threadId' is self-explanatory given the tool name, and with 0% schema coverage the description adds no extra meaning, which is adequate for this simple case.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the action 'Mark a thread as read', which directly matches the tool name and distinguishes it from siblings like 'mark_unread'.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It explains the side effect (removes UNREAD) and reversibility via mark_unread, providing clear context for usage, though it does not explicitly state when not to use it.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

mark_unreadA
Idempotent

Mark a thread as unread. SIDE EFFECTS: adds UNREAD; reversible via mark_read.

ParametersJSON Schema
NameRequiredDescriptionDefault
threadIdYes

Output Schema

ParametersJSON Schema
NameRequiredDescription
okYes

TDQS

A3.9/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already indicate idempotent and non-destructive behavior. The description adds value by explicitly stating the side effect 'adds UNREAD' and noting reversibility, which aligns with and supplements the annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is extremely concise: one sentence plus a bullet for side effects. Every word is necessary, and the structure immediately conveys the core action and notable consequences.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the simplicity of the tool (1 parameter, no enums, output schema present), the description covers the essential behavior. It could benefit from explaining what 'threadId' represents, but the context from sibling tools and the name likely suffices for an AI agent.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters2/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema has one parameter 'threadId' with no description, and schema description coverage is 0%. The description does not explain the parameter meaning, format, or constraints. While the name is self-explanatory, the description should add more context to compensate for the schema gap.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the action: 'Mark a thread as unread.' The verb 'Mark' and resource 'thread' are specific. It distinguishes from sibling 'mark_read' implicitly through the opposite action, and the name itself clarifies the purpose.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description mentions reversibility via 'mark_read,' which hints at when not to use it (if you want to undo). However, it does not provide explicit context on when to use this tool over alternatives like 'archive' or 'trash,' leaving some ambiguity.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

modify_labelsA
Idempotent

Add/remove labels on a thread. Archive = remove 'INBOX'; mark read = remove 'UNREAD'. Labels may be given by name or by id: an unknown name in add is created automatically (use '/' for nested labels), an unknown name in remove is ignored. USE WHEN: applying custom labels or label combinations in one call. DO NOT USE: for plain archive/read/unread — the dedicated tools are clearer. SIDE EFFECTS: changes the thread's labels (and may create a label named in add); reversible by the inverse call.

ParametersJSON Schema
NameRequiredDescriptionDefault
addNo
removeNo
threadIdYes

Output Schema

ParametersJSON Schema
NameRequiredDescription
okYes

TDQS

A5/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Beyond annotations, the description discloses side effects ('changes the thread's labels'), the auto-creation of labels in 'add', the ignoring of unknown names in 'remove', and reversibility via inverse call. This adds meaningful behavioral context not present in the idempotentHint or readOnlyHint annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is tightly written with front-loaded purpose and structured segments (USE WHEN, DO NOT USE, SIDE EFFECTS). Every sentence conveys needed information without fluff, making it easy for an agent to parse quickly.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's simple scope (single thread labels) and the presence of an output schema, the description covers all essential usage aspects: operation semantics, edge cases (unknown labels), side effects, and reversibility. It is complete enough to guide correct invocation without requiring external knowledge.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description must bear the full weight of parameter explanation. It does so by explaining that labels can be given by name or id, describing the special INBOX/UNREAD mapping, and clarifying the behavior of unknown names in each array. This goes well beyond the bare schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with 'Add/remove labels on a thread,' a specific verb and resource. It distinguishes itself from siblings by explicitly mapping archive to removing 'INBOX' and mark read to removing 'UNREAD,' and by pointing out that dedicated tools exist for simpler operations.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides explicit 'USE WHEN' and 'DO NOT USE' guidance, naming alternatives ('dedicated tools') for plain archive/read/unread. This clearly tells an agent when to select this tool over similar ones like mark_read or archive.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

snoozeA
Idempotent

Snooze a thread until a date or time: archives it now, resurfaces when it comes due and sweep_snoozed runs. until accepts an explicit date (YYYY-MM-DD), a date+time (YYYY-MM-DD HH:MM or e.g. '2026-06-20 9am'), OR a preset resolved server-side: today, tomorrow, weekend (next Saturday), next week (next Monday), a weekday name (monday–sunday, next occurrence), 'in N days', or 'in N hours'. A preset may carry a trailing time ('tomorrow 9am', 'monday 8:30'). A timed snooze wakes at the next sweep on/after that minute. USE WHEN: deferring a thread to a later date/time instead of leaving it in the inbox. DO NOT USE: for permanent removal (use archive or trash). SIDE EFFECTS: removes INBOX, adds a dated MCP/Snoozed label; reversible via unsnooze.

ParametersJSON Schema
NameRequiredDescriptionDefault
untilYes
threadIdYes

Output Schema

ParametersJSON Schema
NameRequiredDescription
threadIdYes
snoozedUntilYes

TDQS

A5/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Explains side effects in detail ('removes INBOX, adds a dated MCP/Snoozed label'), notes reversibility ('reversible via unsnooze'), and describes the resurfacing trigger ('sweep_snoozed runs'). This goes well beyond the annotations (readOnly=false, destructive=false, idempotent=true) and is consistent with them.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Structured and front-loaded: one-sentence summary, then detailed `until` format, then usage guidance, then side effects. Every sentence adds value; the length is justified by the complexity of the `until` parameter.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Covers the operation, timing semantics, side effects, reversibility, and usage boundaries. With an output schema present, no return-value explanation is needed, and the description is sufficient for an agent to select and invoke the tool correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema has no descriptions for either parameter (0% coverage), but the description provides exhaustive detail for `until`, including explicit date/time formats, presets, and trailing times. `threadId` is self-explanatory from its name, so the description compensates fully.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Clearly identifies the operation ('Snooze a thread') and its resource ('thread'), and explains the mechanism (archives now, resurfaces later). It also distinguishes from siblings by stating 'DO NOT USE for permanent removal (use archive or trash).'

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly states when to use ('USE WHEN: deferring a thread to a later date/time instead of leaving it in the inbox') and when not to ('DO NOT USE: for permanent removal (use archive or trash)'), naming alternatives.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

sweep_snoozedA
Idempotent

Resurface all snoozed threads whose date is due (<= today), batched at 1000 messages per API request. Set dryRun:true to rehearse: reports the due labels and threads (dueLabels/dueThreads) as the sweep would find them (from the live label listing; a single snooze label with more than 5000 messages is under-counted in the rehearsal), and wakes nothing. USE WHEN: the user asks to process due snoozes, or as a scheduled maintenance call; dryRun to answer 'what is due right now?' without acting. SIDE EFFECTS: due threads return to the inbox marked unread (none with dryRun); safe to run repeatedly. failedCount/errors report messages a batch could not wake (their label is kept for the next sweep).

ParametersJSON Schema
NameRequiredDescriptionDefault
dryRunNo

Output Schema

ParametersJSON Schema
NameRequiredDescription
dateYes
wokenYes
dryRunYes
errorsYes
dueLabelsYes
dueThreadsYes
wokenCountYes
failedCountYes
dueThreadCountYes

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The description goes well beyond the annotations by disclosing concrete side effects: due threads return to the inbox marked unread, dryRun wakes none, and failures keep the label for the next sweep. It also discloses a real limitation (under-counting in rehearsal for labels with >5000 messages), the batch size, and idempotence, giving the agent an accurate behavioral model.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is well-structured with labeled sections (USE WHEN, SIDE EFFECTS) and front-loaded with the core function. Every sentence adds value—batch size, dryRun behavior, side effects, and error handling—without fluff. It is longer than ideal but earns its length through density of useful information.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's complexity and the presence of an output schema, the description covers all critical aspects: behavior, batch limits, dryRun rehearsal details, side effects, idempotence, and failure handling ('failedCount/errors report messages a batch could not wake'). No significant gaps remain for the agent to make an informed invocation decision.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Even though the schema only defines a single boolean dryRun, the description thoroughly explains its purpose and outcome: setting dryRun:true produces a rehearsal that reports dueLabels/dueThreads and wakes nothing. This fully compensates for the schema's minimal parameter info, adding essential semantics for correct invocation.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific verb ('Resurface') and a precise target ('all snoozed threads whose date is due (<= today)'), making it unmistakable what the tool does. It clearly differentiates from sibling tools such as list_snoozed, snooze, and unsnooze by focusing on acting on due threads rather than listing or individual operations.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description includes an explicit 'USE WHEN' section stating clear triggers: 'the user asks to process due snoozes, or as a scheduled maintenance call,' and also covers the dryRun rehearsal use case for answering 'what is due right now?' without acting. It lacks an explicit 'when not to use' or direct alternative names, but the context is sufficiently clear.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

trashA
DestructiveIdempotent

Move a thread to Trash. USE WHEN: the thread should be discarded. DO NOT USE: for inbox cleanup of mail worth keeping (use archive). SIDE EFFECTS: thread moves to Trash; recoverable via untrash for ~30 days, then Gmail deletes it permanently.

ParametersJSON Schema
NameRequiredDescriptionDefault
threadIdYes

Output Schema

ParametersJSON Schema
NameRequiredDescription
okYes

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already indicate destructiveHint=true, but the description adds critical context: thread is recoverable via untrash for ~30 days, then permanently deleted. This goes beyond annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is three sentences, front-loaded with the action, and every sentence adds value with no redundancy.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a simple tool with one parameter and an output schema present, the description covers purpose, usage guidelines, and side effects completely.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The schema has 0% description coverage for the single required string parameter 'threadId'. While the description doesn't detail its format, the tool's context makes it clear. A format hint (e.g., 'Gmail thread ID') would improve clarity.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description explicitly states 'Move a thread to Trash' with a specific verb and resource, and distinguishes from the sibling 'archive' tool by noting when not to use it.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides clear USE WHEN ('the thread should be discarded') and DO NOT USE conditions ('for inbox cleanup of mail worth keeping') with explicit alternative (archive). This is exemplary guidance.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

triage_digestA
Read-only

Structured overview of a mailbox slice for triage DECISIONS — sender / label / age buckets, unread and attachment counts, and header-derived signals (newsletter / automated / calendar / replyToMismatch — thread counts overall, and per sender the set of signals its threads carry), instead of a raw thread list. USE WHEN: deciding what to bulk-archive/snooze/label, or summarizing inbox state ('what's in my inbox?'). DO NOT USE: to read a specific thread (use search/get_thread). Samples up to max most-recent matches; hasMore flags that more matched than were sampled. byAge buckets by each thread's FIRST message date (thread age, not last activity). SIDE EFFECTS: none.

ParametersJSON Schema
NameRequiredDescriptionDefault
maxNo
topNNo
queryNoin:inbox

Output Schema

ParametersJSON Schema
NameRequiredDescription
byAgeYes
queryYes
unreadYes
hasMoreYes
sampledYes
signalsYes
topLabelsYes
topSendersYes
withAttachmentsYes

TDQS

A4.4/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint=true, but the description adds valuable behavioral details: sampling up to 'max' with hasMore, age bucketing by first message date, and explicit 'SIDE EFFECTS: none'. This goes beyond annotations and answers likely agent questions.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is dense yet well-organized into clear sections: overview, use cases, behavior, and side effects. Every sentence contributes new information, with no redundancy or filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The description covers the tool's purpose, usage, sampling, age bucketing, and side effects, while an output schema likely handles return-value documentation. However, it leaves parameter semantics incomplete (especially topN/query), a notable gap given the low schema coverage.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters2/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description must explain parameters. It only mentions 'max' (sampling limit) but leaves 'topN' and 'query' undefined. This forces the agent to infer their meaning, which is insufficient for correct invocation.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states it provides a structured overview of a mailbox slice for triage decisions, listing specific aggregated fields (sender/label/age buckets, counts, signals) and explicitly contrasts with a raw thread list. It also distinguishes from reading a specific thread by referencing search/get_thread.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Includes explicit 'USE WHEN' and 'DO NOT USE' sections, naming alternatives (search/get_thread) and giving concrete examples like bulk-archiving/snoozing/labeling. This provides clear guidance on when to choose this tool over siblings.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

unsnoozeA
Idempotent

Cancel a snooze: return the thread to the inbox now. SIDE EFFECTS: removes the snooze label, restores INBOX.

ParametersJSON Schema
NameRequiredDescriptionDefault
threadIdYes

Output Schema

ParametersJSON Schema
NameRequiredDescription
threadIdYes
unsnoozedYes

TDQS

A4.1/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The description discloses side effects beyond annotations: 'removes the snooze label, restores INBOX'. Annotations already indicate idempotentHint=true and destructiveHint=false, and the description aligns with these, adding valuable behavioral context without contradiction.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is extremely concise with two sentences. The first sentence front-loads the primary action, and the second lists side effects. No wasted words.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a simple one-parameter tool with an output schema (though not shown), the description covers the essential effect. However, it omits any mention of error conditions or prerequisites (e.g., thread must be snoozed). Still, given low complexity and presence of output schema, it is largely complete.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters1/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The single parameter threadId has no description in the schema (0% coverage) and the tool description does not add any meaning beyond the name. It fails to explain what threadId represents or its format, which is necessary for correct usage.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the action: 'cancel a snooze' and 'return the thread to the inbox', with a specific verb and resource. It distinguishes from sibling tools like snooze (the reverse operation).

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description explicitly states when to use the tool (to cancel a snooze) and mentions side effects, but does not explicitly state when not to use it or list alternatives like mark_read or archive. However, the sibling list includes snooze as the inverse, making usage clear.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

unsubscribeA
Idempotent

Unsubscribe from the mailing list a thread came from, via the sender's RFC 8058 one-click endpoint. There is deliberately no URL parameter: the endpoint is taken from the message's own List-Unsubscribe header and nowhere else. Only https one-click endpoints are called (fixed request body, response body discarded); a plain link is reported for the user to open, and a mailto: opt-out is never performed because mailwarden cannot send mail. If the sender offers nothing automatable this returns unsubscribed:false with the alternatives in options — it is not an error. USE WHEN: the user wants off a newsletter. Pair with archive/trash or create_filter to deal with mail already in the mailbox. DO NOT USE: to check whether unsubscribing is possible (use list_unsubscribe — it contacts nobody). A sender already contacted in this session is reported with duplicateOf and NOT contacted again — safe to retry after a timeout. Pass force:true for a deliberate second attempt (e.g. the endpoint answered 500). SIDE EFFECTS: makes an outbound HTTPS request to the sender's unsubscribe endpoint (plus up to 3 redirects) — the only non-Google host mailwarden ever contacts. This confirms to the sender that the address is live, and it cannot be undone. The mailbox itself is not changed.

ParametersJSON Schema
NameRequiredDescriptionDefault
forceNo
threadIdYes

Output Schema

ParametersJSON Schema
NameRequiredDescription
urlNo
fromYes
reasonNo
statusNo
optionsYes
threadIdYes
messageIdYes
duplicateOfNo
unsubscribedYes

TDQS

A4.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Beyond the annotations (readOnlyHint=false, openWorldHint=true, idempotentHint=true, destructiveHint=false), the description discloses crucial side effects: makes an outbound HTTPS request, confirms the address is live, cannot be undone, and never modifies the mailbox. It also details the duplicateOf behavior and force:true for retries. No contradictions with annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is dense but well-organized with 'USE WHEN', 'DO NOT USE', and 'SIDE EFFECTS' sections. Every sentence adds value: security protocol, lacking URL parameter, redirect limits, non-error on failure. Despite length, it remains front-loaded and efficiently structured.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's complexity (external network call, alternative handling, duplicate prevention), the description covers all aspects: side effects, alternatives, failure modes, and output semantics (duplicateOf, options). An output schema exists, so return values are covered. No significant gaps remain.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 0%, so the description must explain parameters. It implicitly defines threadId as the thread containing the mailing list email. It explicitly explains force:true for a deliberate second attempt. While it doesn't specify threadId's exact format, it adds meaningful context beyond the bare schema. Slight room for more explicit parameter documentation.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a specific action: 'Unsubscribe from the mailing list a thread came from, via the sender's RFC 8058 one-click endpoint.' It clearly distinguishes from sibling tools (e.g., list_unsubscribe) by emphasizing it actually performs the subscription removal. The scope and method are precise.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicit guidance is given: 'USE WHEN: the user wants off a newsletter' and 'DO NOT USE: to check whether unsubscribing is possible (use list_unsubscribe — it contacts nobody).' It also suggests pairing with archive/trash or create_filter for post-processing. This fully covers when and when not to use, with alternative tools named.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

untrashA
Idempotent

Restore a thread from Trash. SIDE EFFECTS: removes the TRASH label; user labels are preserved, but INBOX is NOT re-added — use modify_labels (add INBOX) to return it to the inbox.

ParametersJSON Schema
NameRequiredDescriptionDefault
threadIdYes

Output Schema

ParametersJSON Schema
NameRequiredDescription
okYes

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations (readOnlyHint=false, destructiveHint=false) already indicate mutation and non-destructive nature. Description adds critical context: side effects on labels (TRASH removal, INBOX not re-added). No contradiction with annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two sentences: first provides core action, second details side effects and follow-up. No superfluous words. Efficient and front-loaded.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Output schema exists, so return values are documented externally. Description covers side effects, usage, and follow-up. With one simple param and clear annotations, the description is fully adequate.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Only one parameter 'threadId' with 0% schema description coverage. Description does not explicitly detail the parameter, but context from 'Restore a thread from Trash' implies its purpose. Adequate for a simple string param, but could be more explicit.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Description clearly states 'Restore a thread from Trash.' verb+resource pair. Distinguishes from sibling tools like 'trash' (opposite action) and 'archive' (different destination).

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly notes side effects: removes TRASH label, preserves user labels, but does NOT re-add INBOX. Recommends using 'modify_labels (add INBOX)' to return to inbox. Provides clear when-to-use and follow-up action.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

what_changedA
Read-only

What happened in the mailbox since a point you already hold: messages that arrived or were removed, and labels applied or taken off — from Gmail's own event log, in one call. Pass the historyId a previous what_changed or get_profile returned; the reply carries the next one to keep. mailwarden stores NOTHING between calls, so the id lives with you. Reports EVENTS, not current state: a message that was marked unread and then read appears under both, and both are true. For how the mailbox looks NOW, use search or get_thread. Gmail keeps roughly a week of history. An id older than that is an ERROR, never an empty result — it means the question can no longer be answered incrementally, so take a fresh historyId from get_profile and re-establish state with search. labelId narrows the feed to events touching one label (e.g. 'INBOX', or a snooze label). Counts are complete even when the id lists are cut at 200 (truncated). USE WHEN: a recurring check — 'what came in since I last looked', watching a label, or confirming a bulk action landed. Far cheaper than re-running a search over the whole slice. DO NOT USE: for a first look at a mailbox (there is no id yet — start with triage_digest or search), or to read content: this returns ids only, no sender, subject or body. SIDE EFFECTS: none.

ParametersJSON Schema
NameRequiredDescriptionDefault
maxNo
labelIdNo
sinceHistoryIdYes

Output Schema

ParametersJSON Schema
NameRequiredDescription
addedYes
deletedYes
recordsYes
historyIdYes
truncatedYes
labelsAddedYes
labelsRemovedYes

TDQS

A4.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Goes far beyond the annotations: explains events vs current state, history retention limits, error-on-stale-id behavior, truncation semantics, count completeness, and side-effect-free behavior. No contradiction with readOnlyHint=true.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is long but every sentence earns its place: mechanics, semantics, error handling, use cases, and exclusions are all present. Critical context is front-loaded before the extended guidance.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a delta-reporting tool with one required parameter, the description covers return shape, state management, error behavior, filter behavior, and alternatives. Even with an output schema present, nothing important is left ambiguous.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, but the description adds deep semantics for sinceHistoryId (how to obtain, carry, and refresh it) and labelId (narrowing to label events). max is not explicitly described, though its schema constraints make it largely self-explanatory.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states exactly what the tool reports — mailbox changes (arrivals, removals, label changes) from Gmail's event log since a held historyId. It explicitly contrasts with search and get_thread, so an agent can distinguish this from siblings immediately.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Provides explicit USE WHEN guidance for recurring checks and DO NOT USE guidance for first looks and content reading, naming alternatives like triage_digest, search, and get_thread. This is exemplary routing information.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 1 tool updatev0.21.0
    • Changedcreate_filter2 fields changed
      • addedInput schema / properties / verify
        Added value: +{
        +  "default": false,
        +  "type": "boolean"
        +}
      • changedOutput schema / properties / applied / anyOf
        Previous value: -[
        -  {
        -    "additionalProperties": false,
        -    "properties": {
        -      "capped": {
        -        "type": "boolean"
        -      },
        -      "error": {
        -        "type": "string"
        -      },
        -      "failed": {
        -        "items": {
        -          "additionalProperties": false,
        -          "properties": {
        -            "error": {
        -              "type": "string"
        -            },
        -            "messageIds": {
        -              "items": {
        -                "type": "string"
        -              },
        -              "type": "array"
        -            }
        -          },
        -          "required": [
        -            "messageIds",
        -            "error"
        -          ],
        -          "type": "object"
        -        },
        -        "type": "array"
        -      },
        -      "matchedMessages": {
        -        "type": "number"
        -      },
        -      "query": {
        -        "type": "string"
        -      },
        -      "submittedMessages": {
        -        "type": "number"
        -      },
        -      "submittedThreadCount": {
        -        "type": "number"
        -      }
        -    },
        -    "required": [
        -      "query",
        -      "matchedMessages",
        -      "submittedMessages",
        -      "submittedThreadCount",
        -      "capped",
        -      "failed"
        -    ],
        -    "type": "object"
        -  },
        -  {
        -    "type": "null"
        -  }
        -]New value: +[
        +  {
        +    "additionalProperties": false,
        +    "properties": {
        +      "capped": {
        +        "type": "boolean"
        +      },
        +      "error": {
        +        "type": "string"
        +      },
        +      "failed": {
        +        "items": {
        +          "additionalProperties": false,
        +          "properties": {
        +            "error": {
        +              "type": "string"
        +            },
        +            "messageIds": {
        +              "items": {
        +                "type": "string"
        +              },
        +              "type": "array"
        +            }
        +          },
        +          "required": [
        +            "messageIds",
        +            "error"
        +          ],
        +          "type": "object"
        +        },
        +        "type": "array"
        +      },
        +      "matchedMessages": {
        +        "type": "number"
        +      },
        +      "query": {
        +        "type": "string"
        +      },
        +      "submittedMessages": {
        +        "type": "number"
        +      },
        +      "submittedThreadCount": {
        +        "type": "number"
        +      },
        +      "verified": {
        +        "additionalProperties": false,
        +        "properties": {
        +          "applied": {
        +            "type": "number"
        +          },
        +          "notApplied": {
        +            "items": {
        +              "type": "string"
        +            },
        +            "type": "array"
        +          },
        +          "unverifiable": {
        +            "items": {
        +              "type": "string"
        +            },
        +            "type": "array"
        +          }
        +        },
        +        "required": [
        +          "applied",
        +          "notApplied",
        +          "unverifiable"
        +        ],
        +        "type": "object"
        +      }
        +    },
        +    "required": [
        +      "query",
        +      "matchedMessages",
        +      "submittedMessages",
        +      "submittedThreadCount",
        +      "capped",
        +      "failed"
        +    ],
        +    "type": "object"
        +  },
        +  {
        +    "type": "null"
        +  }
        +]
  2. 5 tool updatesv0.18.0
    • Changedbulk_modify2 fields changed
      • addedInput schema / properties / crossCheck
        Added value: +{
        +  "default": false,
        +  "type": "boolean"
        +}
      • addedOutput schema / properties / crossChecked
        Added value: +{
        +  "additionalProperties": false,
        +  "properties": {
        +    "capped": {
        +      "type": "boolean"
        +    },
        +    "dropped": {
        +      "items": {
        +        "additionalProperties": false,
        +        "properties": {
        +          "id": {
        +            "type": "string"
        +          },
        +          "predicate": {
        +            "type": "string"
        +          },
        +          "threadId": {
        +            "type": "string"
        +          }
        +        },
        +        "required": [
        +          "id",
        +          "threadId",
        +          "predicate"
        +        ],
        +        "type": "object"
        +      },
        +      "type": "array"
        +    },
        +    "droppedMessages": {
        +      "type": "number"
        +    },
        +    "predicates": {
        +      "items": {
        +        "type": "string"
        +      },
        +      "type": "array"
        +    },
        +    "skipped": {
        +      "items": {
        +        "type": "string"
        +      },
        +      "type": "array"
        +    }
        +  },
        +  "required": [
        +    "predicates",
        +    "droppedMessages",
        +    "dropped",
        +    "capped"
        +  ],
        +  "type": "object"
        +}
    • Changedcreate_label3 fields changed
      • addedInput schema / properties / backgroundColor
        Added value: +{
        +  "type": "string"
        +}
      • addedInput schema / properties / textColor
        Added value: +{
        +  "type": "string"
        +}
      • addedOutput schema / properties / color
        Added value: +{
        +  "additionalProperties": false,
        +  "properties": {
        +    "backgroundColor": {
        +      "type": "string"
        +    },
        +    "textColor": {
        +      "type": "string"
        +    }
        +  },
        +  "required": [
        +    "backgroundColor",
        +    "textColor"
        +  ],
        +  "type": "object"
        +}
    • Changedget_profile2 fields changed
      • addedOutput schema / properties / historyId
        Added value: +{
        +  "type": "string"
        +}
      • changedOutput schema / required
        Previous value: -[
        -  "emailAddress",
        -  "messagesTotal",
        -  "threadsTotal"
        -]New value: +[
        +  "emailAddress",
        +  "messagesTotal",
        +  "threadsTotal",
        +  "historyId"
        +]
    • Changedlist_unsubscribe2 fields changed
      • addedOutput schema / properties / bodyCandidates
        Added value: +{
        +  "items": {
        +    "additionalProperties": false,
        +    "properties": {
        +      "evidence": {
        +        "enum": [
        +          "link-text",
        +          "url"
        +        ],
        +        "type": "string"
        +      },
        +      "text": {
        +        "type": "string"
        +      },
        +      "url": {
        +        "type": "string"
        +      }
        +    },
        +    "required": [
        +      "url",
        +      "evidence",
        +      "text"
        +    ],
        +    "type": "object"
        +  },
        +  "type": "array"
        +}
      • changedOutput schema / required
        Previous value: -[
        -  "threadId",
        -  "messageId",
        -  "from",
        -  "subject",
        -  "hasUnsubscribe",
        -  "oneClick",
        -  "httpsUrls",
        -  "mailtos"
        -]New value: +[
        +  "threadId",
        +  "messageId",
        +  "from",
        +  "subject",
        +  "hasUnsubscribe",
        +  "bodyCandidates",
        +  "oneClick",
        +  "httpsUrls",
        +  "mailtos"
        +]
    • Addedwhat_changed
  3. 1 tool updatev0.17.0
    • Changedget_thread2 fields changed
      • addedOutput schema / properties / messages / items / properties / authentication
        Added value: +{
        +  "additionalProperties": false,
        +  "properties": {
        +    "alsoReported": {
        +      "items": {
        +        "type": "string"
        +      },
        +      "type": "array"
        +    },
        +    "authservId": {
        +      "type": "string"
        +    },
        +    "dkim": {
        +      "type": "string"
        +    },
        +    "dmarc": {
        +      "type": "string"
        +    },
        +    "headerFrom": {
        +      "type": "string"
        +    },
        +    "mailedBy": {
        +      "type": "string"
        +    },
        +    "otherReports": {
        +      "type": "integer"
        +    },
        +    "returnPath": {
        +      "type": "string"
        +    },
        +    "signedBy": {
        +      "type": "string"
        +    },
        +    "spf": {
        +      "type": "string"
        +    },
        +    "unchecked": {
        +      "const": true,
        +      "type": "boolean"
        +    }
        +  },
        +  "type": "object"
        +}
      • changedOutput schema / properties / messages / items / required
        Previous value: -[
        -  "id",
        -  "threadId",
        -  "labelIds",
        -  "from",
        -  "to",
        -  "subject",
        -  "date",
        -  "snippet"
        -]New value: +[
        +  "id",
        +  "threadId",
        +  "labelIds",
        +  "from",
        +  "to",
        +  "subject",
        +  "date",
        +  "snippet",
        +  "authentication"
        +]
  4. 3 tool updatesv0.15.1
    • Changedbulk_modify9 fields changed
      • addedInput schema / properties / verify
        Added value: +{
        +  "default": false,
        +  "type": "boolean"
        +}
      • removedOutput schema / properties / modifiedMessages
        Removed value: -{
        -  "type": "number"
        -}
      • removedOutput schema / properties / modifiedThreadCount
        Removed value: -{
        -  "type": "number"
        -}
      • removedOutput schema / properties / modifiedThreads
        Removed value: -{
        -  "items": {
        -    "type": "string"
        -  },
        -  "type": "array"
        -}
      • addedOutput schema / properties / submittedMessages
        Added value: +{
        +  "type": "number"
        +}
      • addedOutput schema / properties / submittedThreadCount
        Added value: +{
        +  "type": "number"
        +}
      • addedOutput schema / properties / submittedThreads
        Added value: +{
        +  "items": {
        +    "type": "string"
        +  },
        +  "type": "array"
        +}
      • addedOutput schema / properties / verified
        Added value: +{
        +  "additionalProperties": false,
        +  "properties": {
        +    "applied": {
        +      "type": "number"
        +    },
        +    "notApplied": {
        +      "items": {
        +        "type": "string"
        +      },
        +      "type": "array"
        +    },
        +    "unverifiable": {
        +      "items": {
        +        "type": "string"
        +      },
        +      "type": "array"
        +    }
        +  },
        +  "required": [
        +    "applied",
        +    "notApplied",
        +    "unverifiable"
        +  ],
        +  "type": "object"
        +}
      • changedOutput schema / required
        Previous value: -[
        -  "dryRun",
        -  "matchedMessages",
        -  "matchedThreadCount",
        -  "matchedThreads",
        -  "modifiedMessages",
        -  "modifiedThreadCount",
        -  "modifiedThreads",
        -  "capped",
        -  "unverifiedPredicates",
        -  "failed"
        -]New value: +[
        +  "dryRun",
        +  "matchedMessages",
        +  "matchedThreadCount",
        +  "matchedThreads",
        +  "submittedMessages",
        +  "submittedThreadCount",
        +  "submittedThreads",
        +  "capped",
        +  "unverifiedPredicates",
        +  "failed"
        +]
    • Changedcreate_filter1 field changed
      • changedOutput schema / properties / applied / anyOf
        Previous value: -[
        -  {
        -    "additionalProperties": false,
        -    "properties": {
        -      "capped": {
        -        "type": "boolean"
        -      },
        -      "error": {
        -        "type": "string"
        -      },
        -      "failed": {
        -        "items": {
        -          "additionalProperties": false,
        -          "properties": {
        -            "error": {
        -              "type": "string"
        -            },
        -            "messageIds": {
        -              "items": {
        -                "type": "string"
        -              },
        -              "type": "array"
        -            }
        -          },
        -          "required": [
        -            "messageIds",
        -            "error"
        -          ],
        -          "type": "object"
        -        },
        -        "type": "array"
        -      },
        -      "matchedMessages": {
        -        "type": "number"
        -      },
        -      "modifiedMessages": {
        -        "type": "number"
        -      },
        -      "modifiedThreadCount": {
        -        "type": "number"
        -      },
        -      "query": {
        -        "type": "string"
        -      }
        -    },
        -    "required": [
        -      "query",
        -      "matchedMessages",
        -      "modifiedMessages",
        -      "modifiedThreadCount",
        -      "capped",
        -      "failed"
        -    ],
        -    "type": "object"
        -  },
        -  {
        -    "type": "null"
        -  }
        -]New value: +[
        +  {
        +    "additionalProperties": false,
        +    "properties": {
        +      "capped": {
        +        "type": "boolean"
        +      },
        +      "error": {
        +        "type": "string"
        +      },
        +      "failed": {
        +        "items": {
        +          "additionalProperties": false,
        +          "properties": {
        +            "error": {
        +              "type": "string"
        +            },
        +            "messageIds": {
        +              "items": {
        +                "type": "string"
        +              },
        +              "type": "array"
        +            }
        +          },
        +          "required": [
        +            "messageIds",
        +            "error"
        +          ],
        +          "type": "object"
        +        },
        +        "type": "array"
        +      },
        +      "matchedMessages": {
        +        "type": "number"
        +      },
        +      "query": {
        +        "type": "string"
        +      },
        +      "submittedMessages": {
        +        "type": "number"
        +      },
        +      "submittedThreadCount": {
        +        "type": "number"
        +      }
        +    },
        +    "required": [
        +      "query",
        +      "matchedMessages",
        +      "submittedMessages",
        +      "submittedThreadCount",
        +      "capped",
        +      "failed"
        +    ],
        +    "type": "object"
        +  },
        +  {
        +    "type": "null"
        +  }
        +]
    • Changedget_thread2 fields changed
      • changedOutput schema / properties / messages / items / required
        Previous value: -[
        -  "id",
        -  "threadId",
        -  "labelIds",
        -  "from",
        -  "to",
        -  "subject",
        -  "date",
        -  "snippet",
        -  "plaintextBody",
        -  "htmlBody",
        -  "attachments"
        -]New value: +[
        +  "id",
        +  "threadId",
        +  "labelIds",
        +  "from",
        +  "to",
        +  "subject",
        +  "date",
        +  "snippet"
        +]
      • addedOutput schema / properties / metadataOnly
        Added value: +{
        +  "const": true,
        +  "type": "boolean"
        +}
  5. 1 tool updatev0.14.0
    • Changedunsubscribe2 fields changed
      • addedInput schema / properties / force
        Added value: +{
        +  "default": false,
        +  "type": "boolean"
        +}
      • addedOutput schema / properties / duplicateOf
        Added value: +{
        +  "type": "string"
        +}
  6. 5 tool updatesv0.11.0
    • Changedbulk_modify7 fields changed
      • addedInput schema / properties / dryRun
        Added value: +{
        +  "default": false,
        +  "type": "boolean"
        +}
      • addedOutput schema / properties / dryRun
        Added value: +{
        +  "type": "boolean"
        +}
      • addedOutput schema / properties / labelsToCreate
        Added value: +{
        +  "items": {
        +    "type": "string"
        +  },
        +  "type": "array"
        +}
      • addedOutput schema / properties / matchedThreadCount
        Added value: +{
        +  "type": "number"
        +}
      • addedOutput schema / properties / matchedThreads
        Added value: +{
        +  "items": {
        +    "type": "string"
        +  },
        +  "type": "array"
        +}
      • addedOutput schema / properties / unverifiedPredicates
        Added value: +{
        +  "items": {
        +    "type": "string"
        +  },
        +  "type": "array"
        +}
      • changedOutput schema / required
        Previous value: -[
        -  "matchedMessages",
        -  "modifiedMessages",
        -  "modifiedThreadCount",
        -  "modifiedThreads",
        -  "capped",
        -  "failed"
        -]New value: +[
        +  "dryRun",
        +  "matchedMessages",
        +  "matchedThreadCount",
        +  "matchedThreads",
        +  "modifiedMessages",
        +  "modifiedThreadCount",
        +  "modifiedThreads",
        +  "capped",
        +  "unverifiedPredicates",
        +  "failed"
        +]
    • Changedbulk_unsubscribe5 fields changed
      • addedInput schema / properties / dryRun
        Added value: +{
        +  "default": false,
        +  "type": "boolean"
        +}
      • addedOutput schema / properties / dryRun
        Added value: +{
        +  "type": "boolean"
        +}
      • addedOutput schema / properties / requests
        Added value: +{
        +  "type": "number"
        +}
      • addedOutput schema / properties / results / items / properties / wouldCall
        Added value: +{
        +  "type": "string"
        +}
      • changedOutput schema / required
        Previous value: -[
        -  "requested",
        -  "attempted",
        -  "unsubscribed",
        -  "skippedDuplicates",
        -  "skippedOutOfTime",
        -  "results"
        -]New value: +[
        +  "dryRun",
        +  "requested",
        +  "attempted",
        +  "unsubscribed",
        +  "skippedDuplicates",
        +  "skippedOutOfTime",
        +  "requests",
        +  "results"
        +]
    • Changedsearch2 fields changed
      • addedOutput schema / properties / threads / items / properties / signals
        Added value: +{
        +  "items": {
        +    "enum": [
        +      "newsletter",
        +      "automated",
        +      "calendar",
        +      "replyToMismatch"
        +    ],
        +    "type": "string"
        +  },
        +  "type": "array"
        +}
      • changedOutput schema / properties / threads / items / required
        Previous value: -[
        -  "threadId",
        -  "messageCount",
        -  "from",
        -  "subject",
        -  "date",
        -  "labelIds",
        -  "snippet",
        -  "hasAttachments"
        -]New value: +[
        +  "threadId",
        +  "messageCount",
        +  "from",
        +  "subject",
        +  "date",
        +  "labelIds",
        +  "snippet",
        +  "hasAttachments",
        +  "signals"
        +]
    • Changedsweep_snoozed8 fields changed
      • addedInput schema / $schema
        Added value: +"http://json-schema.org/draft-07/schema#"
      • addedInput schema / additionalProperties
        Added value: +false
      • addedInput schema / properties / dryRun
        Added value: +{
        +  "default": false,
        +  "type": "boolean"
        +}
      • addedOutput schema / properties / dryRun
        Added value: +{
        +  "type": "boolean"
        +}
      • addedOutput schema / properties / dueLabels
        Added value: +{
        +  "items": {
        +    "type": "string"
        +  },
        +  "type": "array"
        +}
      • addedOutput schema / properties / dueThreadCount
        Added value: +{
        +  "type": "number"
        +}
      • addedOutput schema / properties / dueThreads
        Added value: +{
        +  "items": {
        +    "type": "string"
        +  },
        +  "type": "array"
        +}
      • changedOutput schema / required
        Previous value: -[
        -  "date",
        -  "wokenCount",
        -  "woken",
        -  "failedCount",
        -  "errors"
        -]New value: +[
        +  "date",
        +  "dryRun",
        +  "dueLabels",
        +  "dueThreadCount",
        +  "dueThreads",
        +  "wokenCount",
        +  "woken",
        +  "failedCount",
        +  "errors"
        +]
    • Changedtriage_digest4 fields changed
      • addedOutput schema / properties / signals
        Added value: +{
        +  "additionalProperties": false,
        +  "properties": {
        +    "automated": {
        +      "type": "number"
        +    },
        +    "calendar": {
        +      "type": "number"
        +    },
        +    "newsletter": {
        +      "type": "number"
        +    },
        +    "replyToMismatch": {
        +      "type": "number"
        +    }
        +  },
        +  "required": [
        +    "newsletter",
        +    "automated",
        +    "calendar",
        +    "replyToMismatch"
        +  ],
        +  "type": "object"
        +}
      • addedOutput schema / properties / topSenders / items / properties / signals
        Added value: +{
        +  "items": {
        +    "enum": [
        +      "newsletter",
        +      "automated",
        +      "calendar",
        +      "replyToMismatch"
        +    ],
        +    "type": "string"
        +  },
        +  "type": "array"
        +}
      • changedOutput schema / properties / topSenders / items / required
        Previous value: -[
        -  "sender",
        -  "name",
        -  "count",
        -  "unread"
        -]New value: +[
        +  "sender",
        +  "name",
        +  "count",
        +  "unread",
        +  "signals"
        +]
      • changedOutput schema / required
        Previous value: -[
        -  "query",
        -  "sampled",
        -  "hasMore",
        -  "unread",
        -  "withAttachments",
        -  "byAge",
        -  "topSenders",
        -  "topLabels"
        -]New value: +[
        +  "query",
        +  "sampled",
        +  "hasMore",
        +  "unread",
        +  "withAttachments",
        +  "byAge",
        +  "signals",
        +  "topSenders",
        +  "topLabels"
        +]
  7. 4 tool updatesv0.9.0
    • Addedbulk_unsubscribe
    • Addedlist_subscriptions
    • Addedlist_unsubscribe
    • Addedunsubscribe
  8. 8 tool updatesv0.6.0
    • Changedbulk_modify1 field changed
      • addedInput schema / properties / query / minLength
        Added value: +1
    • Addedcreate_filter
    • Addedcreate_label
    • Addeddelete_filter
    • Addedget_profile
    • Addedlist_filters
    • Changedsnooze2 fields changed
      • addedInput schema / properties / until / minLength
        Added value: +1
      • removedInput schema / properties / until / pattern
        Removed value: -"^\\d{4}-\\d{2}-\\d{2}$"
    • Addedtriage_digest
  9. 1 tool updatev0.1.9
    • Changedbulk_modify2 fields changed
      • addedOutput schema / properties / capped
        Added value: +{
        +  "type": "boolean"
        +}
      • changedOutput schema / required
        Previous value: -[
        -  "matchedMessages",
        -  "modifiedMessages",
        -  "modifiedThreadCount",
        -  "modifiedThreads",
        -  "failed"
        -]New value: +[
        +  "matchedMessages",
        +  "modifiedMessages",
        +  "modifiedThreadCount",
        +  "modifiedThreads",
        +  "capped",
        +  "failed"
        +]
  10. 15 tool updatesv0.1.8
    • First observedarchive
    • First observedbulk_modify
    • First observeddownload_attachment
    • First observedget_thread
    • First observedlist_labels
    • First observedlist_snoozed
    • First observedmark_read
    • First observedmark_unread
    • First observedmodify_labels
    • First observedsearch
    • First observedsnooze
    • First observedsweep_snoozed
    • First observedtrash
    • First observedunsnooze
    • First observeduntrash

TDQS

A4.2/5.0

Scored across 26 tools

Disambiguation5/5

Every tool has a clear, distinct job: archive/trash/snooze are separated, single-thread vs bulk operations are explicitly contrasted, and unsubscribe-related tools are carefully split by purpose (checking, performing, bulk, sender overview). Algorithm solutions that could overlap, like `modify_labels` vs `bulk_modify`, provide strong usage guidance that prevents misselection.

Naming Consistency4/5

Tool names primarily follow a verb_noun convention (`list_labels`, `create_filter`, `get_thread`, `bulk_modify`) with clear verbs like list/get/fetch/create. Minor deviations exist (`what_changed`, `triage_digest`, `archive`/`trash`/`snooze`), but they are still readable and do not break the overall predictable style.

Tool Count3/5

At 26 tools, the set is slightly above the typical well-scoped range and crosses the 'heavy' threshold, but the domain is broad enough that most tools earn a place due to distinct single-versus-bulk variants and separate list views. Some consolidation would benefit usability, though the count is not egregiously inflated.

Completeness4/5

The mail domain is comprehensively covered: read state, archive/trash, labels, filters, snooze, search, bulk operations, subscription lists, and attachment downloads. The main gaps are marginal—direct spam-classification and permanent-delete tools are not explicitly present, while sending/forwarding is an intentionally documented exclusion rather than an accidental omission.

Maintenance

ActivityActive
ResponsivenessSlow

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    An MCP server that enables Gmail integration, allowing users to manage emails (send, receive, read, trash, mark as read) directly through MCP clients like Claude Desktop.
    1
    MIT
  • A
    license
    B
    quality
    D
    maintenance
    Manage your emails effortlessly with a standardized interface for drafting, sending, retrieving, and organizing messages. Streamline your email workflow with complete Gmail API coverage, including label and thread management.
    64
    1,075 npm
    62
    MIT
  • A
    license
    Not graded
    quality
    A
    maintenance
    Gmail MCP server — scope-gated tools (readonly / send / modify), path jails for attachments + downloads, hardened OAuth credentials, Sigstore-signed releases.
    357 npm
    13
    MIT
  • A
    license
    A
    quality
    F
    maintenance
    A Gmail MCP server with native multi-account support, enabling management of multiple Gmail accounts from a single server instance.
    7
    6
    MIT