ИИ-агенты и MCP

MCP-почта для ИИ-агента: подтверждение регистрации

Автономный агент доходит до фразы «проверьте почту — там код» и останавливается. Вот и всё решение: шесть MCP-инструментов, без аккаунта и без ключа, а один из них не отвечает, пока письмо не придёт на самом деле.

  • Средний
  • 13 мин на чтение
Небольшой серый робот держит синий конверт, протягивая его к открытому почтовому ящику; между ними ждёт секундомер

Что нужно агенту и чего не даёт REST API

На этом сайте есть REST API, и программист, который его интегрирует, справится без проблем. С агентом иначе: он не может открыть справочник, выбрать нужный из трёх эндпоинтов и вручную собрать запрос с правильной строкой параметров. Вместо этого он спрашивает сервер, что тот умеет, получает машиночитаемые схемы и вызывает одну из них.

Поэтому всё, что умеет сервис, доступно и во втором виде — как инструменты. Их шесть, и между вызовами не нужно хранить никакого состояния:

ИнструментДля чего нужен
create_inboxПридумывает новый адрес, который агент может сразу отдать. На сервере при этом ничего не резервируется, поэтому вызов не может провалиться. Принимает необязательный читаемый prefix; уникальность обеспечивает случайный суффикс.
list_domainsПубличные домены, которыми может пользоваться кто угодно, — пригодится, когда форма только что отклонила один из них.
list_messagesВсё, что скопилось по адресу, от нового к старому. Отвечает сразу же, даже если там пусто.
read_messageОдно письмо целиком: отправитель, тема, текст, HTML, вложения. Именно здесь находится код или ссылка для входа.
wait_for_messageНе отвечает, пока что-нибудь не придёт, а затем возвращает письмо целиком. Инструмент, который нужно вызывать сразу после отправки формы.
delete_messageУдаляет письмо сразу, а не ждёт 5 дней до истечения срока. Идемпотентен, поэтому повторный вызов агенту ничего не стоит.

На initialize сервер также отвечает коротким абзацем инструкций, который большинство клиентов сразу передают модели. Поэтому агент с самого начала знает, для чего этот сервис нужен и в чём его единственная реальная оговорка, — и никому не приходится прописывать это в промпте.

Подключить клиент одной строкой

Эндпоинт — это один URL, и на публичных доменах регистрироваться незачем. Любой MCP-клиент принимает конфигурацию одной и той же формы — Claude Desktop, Claude Code, Cursor, Continue, OpenAI Agents SDK и всё остальное, что говорит на этом протоколе:

mcp.json
{"mcpServers":{"grabmail":{"url":"https://grabmail.io/mcp"}}}

Транспорт — Streamable HTTP: один POST с JSON-RPC 2.0, один JSON-ответ, никакого удерживаемого потока. Поэтому всё можно проверить из терминала ещё до того, как в дело вступит агент:

Список инструментов из терминала
curl -sX POST https://grabmail.io/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

Клиенты, которые ищут сервер ещё до того, как спросить человека, найдут на домене /.well-known/mcp.json — там указаны тот же эндпоинт и его транспорт.

Весь цикл регистрации — в четырёх вызовах инструментов

Вот последовательность, которая нужна почти любому агенту, и больше в ней ничего нет:

  1. Вызовите create_inbox. В ответ придут адрес, алиас, домен и пометка, какой из двух отдавать. Ничего при этом не создаётся — ящик начинает существовать, когда в него попадает первое письмо.
  2. Впишите алиас в форму. Сервис, в котором идёт регистрация, получает рабочий адрес, который доставляет письма в ящик, но не даёт их прочитать.
  3. Вызовите wait_for_message с этим адресом. Сразу после отправки формы, а не по таймеру. Вызов не отвечает, пока не придёт письмо, — агенту не нужно писать цикл опроса самому.
  4. Возьмите код из письма. Вместе с ответом на ожидание приходит и всё тело письма, так что второй вызов обычно вообще не нужен — read_message нужен только для того, что пришло раньше.
Ваш агентполучает адрес и алиасСайтполучает алиас, отправляет кодGrabMailхранит всё, что приходитwait_for_message, до 25 свозвращает письмо целиком либо timed_out
Четыре вызова, и ждёт из них только один. Алиас уходит на сайт; адрес остаётся у агента — именно на нём он и слушает.

Почему wait_for_message отвечает раньше, чем приходит письмо

Это тот самый инструмент, который делает агента работоспособным, и его поведение чаще всего удивляет — стоит потратить минуту. Вызов выглядит так:

Ожидание письма со словом «code» в теме
curl -sX POST https://grabmail.io/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":
        {"name":"wait_for_message",
         "arguments":{"address":"demo.5kuqarzuch@grabmail.io",
                       "subject_contains":"code"}}}'

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

Ответ после тихого ожидания
{
  "timed_out": true,
  "waited_seconds": 25,
  "message": null,
  "note": "Nothing arrived yet. Call wait_for_message again ..."
}
Зачем вообще нужен предел
Каждая секунда ожидания — это серверный обработчик, который всё это время бездействует, а число обработчиков ограничено. Ожидание длиной в пять минут означало бы, что один агент занимает слот, который нужен ещё сотне других. 25 секунд укладываются и в тайм-аут по умолчанию у любого клиента, поэтому вызов успевает вернуть ответ раньше, чем клиент сам от него откажется.
Одновременно — не больше 8 ожиданий
Сверх этого числа инструмент отвечает мгновенно — timed_out и пометка о том, что произошло. Услышать «попробуй ещё раз» лучше, чем молча встать в очередь за семью другими агентами, не зная об этом.
Фильтры, чтобы не сработать на чужое письмо
from_contains и subject_contains заставляют ожидание игнорировать всё остальное, что придёт за это время. since_id нужен, когда в ящике уже что-то было: передайте в нём id последнего уже увиденного письма, и вызов завершится только на действительно новом.

Раздавайте алиас, опрашивайте адрес

У каждого ящика здесь есть второй адрес длиной в двенадцать символов: он доставляет письма в тот же ящик, но не позволяет их прочитать. Для агента это различие важнее, чем для человека, — агент без колебаний вставит то, что ему дали, в любое подвернувшееся поле.

Поэтому create_inbox не просто возвращает адрес наудачу. Он возвращает оба значения и next_step с пояснением, где что, — агент читает собственный результат вызова, и инструкция оказывается именно там, где нужна, а не на странице документации, которую в этой цепочке никто не читает.

Что написать в инструкциях самого агента

Инструменты достаточно хорошо описывают себя сами, и способная модель справляется с этим и без подсказок. Но пять строк превращают вероятность в надёжность:

  • Один адрес на одну регистрацию. Не один и тот же адрес везде: ящик, где лежит почта от шести сервисов, — это шесть подтверждений, которые агенту нужно различать, и одна утечка, которая раскрывает их все разом.
  • Раздавайте алиас и никогда — адрес. Стоит сказать это прямо, даже если то же самое написано и в результате вызова.
  • Вызывайте wait_for_message сразу после отправки формы и вызывайте его снова при timed_out, не считая это сбоем. Два-три раза — это нормально.
  • Передавайте since_id, если ящик не новый, — иначе ожидание завершится на старом письме, и агент прочитает код, который истёк час назад.
  • Удаляйте письмо, как только код использован. Это не обязательно — всё и так исчезнет через 5 дней, — но так окно закрывается раньше, а стоит это один идемпотентный вызов.

В виде блока инструкций это выглядит примерно так:

Системный промпт или строка в AGENTS.md
Когда нужен email-адрес, вызывайте create_inbox и раздавайте АЛИАС —
то, что он возвращает, а не адрес. Сразу после отправки формы вызывайте
wait_for_message с этим адресом. Если ответ — timed_out, вызывайте снова:
это нормально, ничего не потеряно. Если в ящике уже что-то было, передайте
since_id. Удаляйте письмо, как только код использован.

Ограничения, которые стоит знать, прежде чем строить на этом

Все они опубликованы, а не найдены методом проб, и ни для одного нет тарифа, который бы его снимал:

ОграничениеЗначениеЧто это значит для агента
Одно ожидание25 секундЗатем timed_out. Вызовите ещё раз — это не ошибка.
Ожиданий одновременно8Сверх этого инструмент отвечает сразу и сообщает об этом. В таком случае используйте list_messages.
ЧтенийОдно в секунду на адресЭто намного больше, чем нужно циклу вызовов. Блокирующее ожидание — один запрос, а не шестьдесят.
Размер письма5 MBОтклоняется прямо во время SMTP-сессии, так что об этом узнаёт отправитель, а не агент, ожидающий того, что никогда не придёт.
Срок хранения5 днейЖёсткий предел, который обеспечивает фоновая задача. Всё, что агенту нужно сохранить, ему придётся записать самому.

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

Когда форма отклоняет публичные домены

Многие сервисы ведут списки доменов одноразовой почты, и три публичных домена отсюда в них тоже есть. Для агента это выглядит как форма, которая отклоняет только что полученный адрес, или, что хуже, принимает его и ничего не отправляет.

Надёжный ответ — домен, которым владеете вы сами. Одна MX-запись превращает на нём любой адрес в ящик здесь же, его нет ни в одном списке, потому что на этом сайте он нигде не упомянут, и те же шесть инструментов работают на нём без изменений — кроме create_inbox, потому что он придумывает адреса только на публичных доменах. Агент просто использует you-pick-it@your-domain и вызывает на этом адресе wait_for_message.

MX-запись для вашего домена10 smtp.grabmail.io

Полная инструкция здесь — сама запись, что доказывает её публикация, и ограничения ящика без пароля.

Чего не стоит поручать агенту с этим сервисом

Честная часть — и та, что экономит полдня:

  • Ничего, что потом понадобится восстановить. Ничего, что связано с деньгами, личностью или работой. Через 5 дней ящик снова пуст, а прочитать его может любой, кто знает адрес, так что сброс пароля, отправленный на него через год, не дойдёт ни до кого — или дойдёт не до вас.
  • Не как второй фактор. Ящик без пароля не может быть фактором.
  • Не для чего-либо личного. Не потому, что мы это читаем, а потому, что адрес — единственный секрет в этой схеме, а агент вполне мог записать его в лог, транскрипт или коммит.
  • Не для массовости. Агент, открывающий аккаунты сотнями, — это ровно то поведение, из-за которого существуют чёрные списки, и самый быстрый способ добиться, чтобы публичные домены отказали всем остальным.

Если использовать его по назначению — как шаг подтверждения, что стоит между агентом и тем, о чём его на самом деле попросили, — он снимает единственное препятствие, которое надёжно его останавливает.

Вопросы

Нужен ли API-ключ или аккаунт?

Нет. Публичные домены, инструменты и собственный домен — всё это бесплатно и без авторизации. Ключ нужен только в одном случае: если домен закрыли по запросу, — тогда потребуется заголовок Authorization.

С какими клиентами это работает?

С любым клиентом, который говорит на Model Context Protocol, — Claude Desktop, Claude Code, Cursor, Continue, OpenAI Agents SDK и остальные. Транспорт — Streamable HTTP, он же используется по умолчанию в текущих клиентах, а поддержка трёх версий протокола позволяет подключиться и более старому клиенту.

Почему wait_for_message возвращает timed_out?

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

Может ли агент использовать вместо этого мой собственный домен?

Да, и меняется только адрес. Направьте MX-запись на smtp.grabmail.io — и любой адрес на этом домене станет доступен через те же инструменты. Только create_inbox работает исключительно с публичными доменами, потому что именно он придумывает имя за вас.

Может ли агент отправлять почту через этот сервис?

Нет. Инструмента и эндпоинта для отправки нет — так и задумано: неавторизованный сервис, который умел бы отправлять почту, превратился бы в спам-релей за день. Наш SPF — v=spf1 -all, а DMARC — p=reject, так что всё, что якобы приходит с адреса отсюда, — подделка.

Ящик приватный?

Нет, и это единственная оговорка, которую агенту стоит сообщить явно. На публичном домене прочитать ящик может любой, кто знает или угадал адрес. Используйте адрес, который невозможно угадать, раздавайте алиас, а не адрес, и никогда не подпускайте к этому ящику ничего личного.

Могут ли два агента одновременно ждать на одном адресе?

Могут, и письмо при получении достанется обоим. Ограничено число ожиданий одновременно по всему сервису — 8; сверх этого инструмент сразу отвечает и сообщает об этом, а list_messages при этом продолжает работать.

Сколько хранятся письма?

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

Чем это отличается от вызова REST API из скрипта?

Для скрипта — ничем, и REST API там подходит лучше: руководство о сквозном тестировании проверки email разбирает именно эту форму, вместе со сроками и вспомогательными функциями. MCP нужен там, где цикл никто не писал: модель сама решает открыть ящик, и ей нужно, чтобы инструменты были обнаруживаемыми, а не задокументированными.

Читать дальше

Попробуйте, пока свежо

Адрес — это один клик, без аккаунта и карты. Всё из этого руководства сразу заработает на нём.

С возвращением

Ваши ящики и ваши домены в одном месте.