ИИ-агенты и MCP

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

Агент, который умеет просматривать страницы, заполнять формы и платить, всё равно намертво стопорится на «проверьте почту и введите код», потому что у него нет ящика. Вот как дать ему такой, для которого не нужны ни аккаунт, ни ключ, — через MCP с блокирующим ожиданием либо в виде обычной функции-инструмента для LangChain, OpenAI Agents SDK и Vercel AI SDK, — вместе с циклом из четырёх шагов, который проводит регистрацию до конца, и ограничителями, которые нужны агенту, работающему без присмотра.

  • Средний
  • 21 мин на чтение
Маленький серый робот за серым столом поднимает синий конверт, одна рука на клавиатуре, над ним парит синяя галочка

Стена, в которую упирается любой агент

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

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

Два способа дать ему ящик

До одного и того же ящика можно достучаться двумя способами, и выбор зависит от того, как построен агент, а не от самого ящика:

MCP-серверФункция-инструмент REST
Подходит, когдаАгент работает в MCP-клиенте — Claude Code, Cursor, Claude Desktop либо во фреймворке с адаптером MCP.Вы пишете агента в коде: LangChain, OpenAI Agents SDK, Vercel AI SDK либо собственный цикл.
Ожиданиеwait_for_message блокирует вызов на стороне сервера до 25 секунд и возвращает письмо целиком. Ноль токенов, потраченных на ожидание.Функция-инструмент крутит цикл раз в секунду до своего дедлайна. Тоже ноль токенов — цикл в вашем коде, а не в модели.
НастройкаОдин URL в настройке клиента. Никакого кода.Две функции, сорок строк, одна HTTP-библиотека.
Что видит модельШесть инструментов с описаниями плюс абзац инструкций, который сервер отправляет в момент подключения.Ровно то, что написано в описаниях ваших инструментов. Docstring'и ниже написаны именно для этой роли.

Путь через MCP — это одна строка, и в настройке для конкретных клиентов есть точная строка для семи клиентов; в «Ящик, который может прочитать AI-агент» инструменты разобраны подробно. Остальная часть этого руководства — путь через REST, для агентов, которых вы строите сами.

путь через MCP целиком
{"mcpServers":{"grabmail":{"url":"https://grabmail.io/mcp"}}}

Функция-инструмент: Python, для LangChain и OpenAI Agents SDK

Две обычные функции с тщательно продуманными docstring'ами. Docstring важнее самого кода: в обоих фреймворках именно он становится описанием, которое читает модель, чтобы решить, когда вызывать инструмент и что делать с ответом, — поэтому в нём указаны те самые две вещи, которые агент путает: алиас против адреса и что значит timed_out.

inbox_tools.py
"""inbox_tools.py — two plain functions any agent framework can wrap. No key, no account."""
import secrets
import time

import requests

API = "https://grabmail.io/api/v1"


def create_inbox() -> dict:
    """Create a fresh disposable email inbox for this task.

    Returns the ADDRESS to poll and the ALIAS to give to websites. Put the alias
    into forms; never hand out the address. Nothing is created server-side.
    """
    address = f"agent-{secrets.token_hex(4)}@grabmail.io"
    r = requests.get(f"{API}/mailbox", params={"address": address}, timeout=15)
    r.raise_for_status()
    return {
        "address": address,
        "alias": r.json().get("alias"),
        "next_step": "Put the alias into the form. Then call wait_for_message with the address.",
    }


def wait_for_message(address: str, subject_contains: str = "", timeout_seconds: int = 60) -> dict:
    """Wait for an email to arrive at the address, up to timeout_seconds.

    Returns the message (from, subject, text, html) or {"status": "timed_out"}.
    On timed_out, call again — up to three times — before concluding no mail was sent.
    """
    deadline = time.monotonic() + timeout_seconds
    while time.monotonic() < deadline:
        r = requests.get(f"{API}/mailbox", params={"address": address}, timeout=15)
        if r.status_code == 429:                       # slow down, do not fail
            time.sleep(float(r.headers.get("Retry-After", 1)))
            continue
        r.raise_for_status()
        for m in r.json()["messages"]:
            if subject_contains.lower() in m["subject"].lower():
                full = requests.get(f"{API}/message/{m['id']}", params={"mailbox": address}, timeout=15)
                full.raise_for_status()
                return full.json()
        time.sleep(1)                                   # one read a second, never throttled
    return {"status": "timed_out", "hint": "Call again, up to three times, before giving up."}

Обернуть их — один вызов на фреймворк. tool из LangChain читает docstring и подсказки типов; function_tool из Agents SDK делает то же самое и добавляет инструменты агенту, в чьих инструкциях повторяется тот же цикл:

LangChain
# LangChain: the docstring becomes the tool description the model reads.
from langchain_core.tools import tool

create_inbox_tool = tool(create_inbox)
wait_for_message_tool = tool(wait_for_message)
# agent = create_react_agent(model, tools=[create_inbox_tool, wait_for_message_tool, ...])
OpenAI Agents SDK
# OpenAI Agents SDK: same two functions, same docstrings.
from agents import Agent, Runner, function_tool

signup_agent = Agent(
    name="Signup agent",
    instructions=(
        "When a site needs an email address, call create_inbox once. Put the ALIAS in the form. "
        "Right after submitting, call wait_for_message with the ADDRESS and a word from the expected "
        "subject. If it returns timed_out, call it again, up to three times."
    ),
    tools=[function_tool(create_inbox), function_tool(wait_for_message)],
)

result = Runner.run_sync(signup_agent, "Sign up for a trial at https://app.example.com/signup and report the login.")
print(result.final_output)

Тот же инструмент на TypeScript, для Vercel AI SDK

tool() из AI SDK принимает описание, схему и execute; в описании — те же два предложения. Передайте оба инструмента в generateText или streamText с maxSteps больше четырёх, потому что цикл состоит из четырёх вызовов инструментов:

inbox-tools.ts
// inbox-tools.ts — the same two tools for the Vercel AI SDK (v5 shape: inputSchema + execute).
import { tool } from 'ai';
import { z } from 'zod';

const API = 'https://grabmail.io/api/v1';
const sleep = (ms: number) => new Promise(r => setTimeout(r, ms));

export const createInbox = tool({
  description: 'Create a fresh disposable email inbox for this task. Returns the ADDRESS to poll and the ALIAS to give to websites. Put the alias into forms; never hand out the address.',
  inputSchema: z.object({}),
  execute: async () => {
    const address = `agent-${crypto.randomUUID().slice(0, 8)}@grabmail.io`;
    const res = await fetch(`${API}/mailbox?address=${encodeURIComponent(address)}`);
    const { alias } = (await res.json()) as { alias: string | null };
    return { address, alias, next_step: 'Put the alias into the form. Then call waitForMessage with the address.' };
  },
});

export const waitForMessage = tool({
  description: 'Wait for an email to arrive at the address, up to timeoutSeconds. Returns the message (from, subject, text, html) or { status: "timed_out" }. On timed_out, call again — up to three times — before concluding no mail was sent.',
  inputSchema: z.object({
    address: z.string(),
    subjectContains: z.string().optional(),
    timeoutSeconds: z.number().int().min(5).max(120).default(60),
  }),
  execute: async ({ address, subjectContains = '', timeoutSeconds }) => {
    const deadline = Date.now() + timeoutSeconds * 1000;
    while (Date.now() < deadline) {
      const res = await fetch(`${API}/mailbox?address=${encodeURIComponent(address)}`);
      if (res.status === 429) { await sleep(Number(res.headers.get('retry-after') ?? 1) * 1000); continue; }
      if (!res.ok) throw new Error(`GET /mailbox answered ${res.status}`);
      const { messages } = (await res.json()) as { messages: { id: string; subject: string }[] };
      const hit = messages.find(m => m.subject.toLowerCase().includes(subjectContains.toLowerCase()));
      if (hit) return (await fetch(`${API}/message/${hit.id}?mailbox=${encodeURIComponent(address)}`)).json();
      await sleep(1000);
    }
    return { status: 'timed_out', hint: 'Call again, up to three times, before giving up.' };
  },
});

Форма одинакова в любом фреймворке, где есть понятие инструмента: описание, которое читает модель, схема аргументов и функция, которая выполняется на вашей стороне. Две вещи, которые нужно перенести без изменений, — предложение про алиас и предложение про timed_out; всё остальное — это клиент из руководства по Node или руководства по Python.

Цикл из четырёх шагов

Независимо от фреймворка, регистрация — это одни и те же четыре вызова инструментов, и агенту стоит сказать об этом прямо в инструкциях, а не оставлять его самому это выяснять:

  1. create_inbox, один раз на задачу. В ответ приходят адрес, алиас и предложение, объясняющее, что есть что.
  2. Алиас идёт в форму. Отправить.
  3. wait_for_message по адресу, сразу же, с subject_contains, выставленным на слово, которое будет в письме с подтверждением, — «code», «verify», «confirm». Не по таймеру и не после другой работы: почта уже в пути.
  4. Код извлекается из письма, которое вернуло ожидание; агент вводит его или переходит по ссылке. Шесть цифр после слов, которые использует шаблон, — правила извлечения, применимые к агенту точно так же, как и к тесту, есть в «OTP-коды в автотестах».

Шестидесятисекундное ожидание, вернувшее timed_out, — это не провал, это «пока нет». Инструкции должны говорить: вызови снова, до трёх раз. Три вызова — это больше трёх минут, чего хватает для любого транзакционного письма, которое действительно было отправлено, — и это даёт агенту три шанса заметить, что форма показала ошибку или что он ввёл алиас неверно.

Правило алиаса

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

Поэтому сайт получает алиас, а агент оставляет адрес себе. Агент, вставивший адрес в форму, отдал сайту — и всем, кому сайт этот адрес сольёт, — возможность читать любое письмо, которое агент когда-либо получит в этом ящике. Функции-инструменты выше возвращают оба значения вместе с next_step, объясняющим, что куда идёт, а инструкции это повторяют, — потому что правило, сказанное дважды, соблюдается.

alias@examplewhat you hand overGrabMailroutes it to the mailboxyou@examplewhat you keepRead it back and you getan empty mailbox. Always.Read it back and you getevery message.The service you signed up to only ever holds the left-hand one.
Алиас — это то, что агент отдаёт и в ответ читает пустой ящик; адрес — это то, что он оставляет себе и в ответ читает каждое письмо.

Ограничители для агента без присмотра

Набор тестов падает и останавливается. Агент, неверно понявший ситуацию, продолжает работать — и продолжает тратить. Пять ограничений не дают почтовому шагу стать самой дорогой частью задачи:

Один ящик на задачу
Никогда не переиспользуйте адрес между задачами или прогонами. Старое письмо с правдоподобно выглядящим кодом — самый быстрый способ заставить агента уверенно сделать неправильную вещь. create_inbox ничего не стоит — вызывайте его каждый раз.
Бюджет ожиданий
Три вызова wait_for_message, а затем — остановиться и сообщить об этом. Агент, бесконечно ждущий письма, которое так и не отправили, сжигает слот воркера и деньги.
Дедлайн на весь шаг целиком
Пять минут от отправки формы до кода, от начала до конца. По истечении этого времени правильное действие — сообщить человеку, что произошло, а не пробовать форму заново.
Фильтрация по теме
Всегда передавайте subject_contains. Иначе приветственное письмо, пришедшее раньше письма с кодом, окажется тем самым «письмом», и агент извлечёт шесть цифр из маркетингового подвала.
Логирование адреса
Записывайте адрес в лог задачи. Письма хранятся 5 дней, так что человек потом может открыть ящик и увидеть ровно то же, что видел агент, — самая полезная вещь, когда прогон пошёл не так.

Браузерные агенты

Агент, управляющий настоящим браузером, — Browser Use, MCP-сервер Playwright, модель с computer use, — это как раз тот случай, где почтовый шаг кусается сильнее всего, потому что он встретит форму раньше, чем кто-либо это запланировал. Три вещи заставляют это работать:

  • Дайте ему оба сервера. Инструменты браузера и инструменты ящика в одной сессии, чтобы «проверьте почту» было вызовом инструмента, а не тупиком.
  • Пропишите цикл в системном промпте. Четыре строки: создать ящик при первом же поле для email; алиас — в форму; ожидание по адресу сразу после отправки; три повтора при timed_out.
  • Ожидайте отказов. Форма, отклоняющая домен алиаса, сообщит об этом прямо на странице; агент должен прочитать ошибку и остановиться, а не подбирать имена. Честные способы обойти отказ — собственный домен или домен из пула, который держат подальше от чёрных списков, — это решения по настройке, которые принимаете вы, а не агент.

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

Прежде чем запускать без присмотра

  • Описания инструментов формулируют правило алиаса и то, что значит timed_out.
  • Инструкции содержат цикл из четырёх шагов и бюджет из трёх ожиданий.
  • create_inbox вызывается один раз на задачу, никогда не переиспользуется.
  • subject_contains на каждом ожидании.
  • Адрес записывается в лог задачи.
  • Строка, запрещающая отправлять на ящик что-либо конфиденциальное.

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

Вопросы

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

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

MCP или REST-инструмент — что выбрать?

Если агент уже живёт в MCP-клиенте — MCP: это одна строка, а ожидание выполняется на сервере. Если вы пишете агента во фреймворке — функция-инструмент: это сорок строк, она работает с любой моделью, и вы контролируете описание, которое читает модель. Оба пути ведут к одному и тому же ящику.

Сколько токенов стоит ожидание?

Нисколько — в обоих случаях. Ожидание MCP блокируется на сервере; REST-инструмент крутит цикл в вашем коде. Модель тратит токены на сам вызов инструмента и на чтение результата, а не на шестьдесят секунд между ними, — и именно поэтому не стоит давать модели опрашивать ящик самостоятельно.

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

Да. У каждой задачи свой ящик, и состояния сессии не существует. Ограничения — одно чтение в секунду на адрес и 1200 запросов в минуту на клиента через REST, а также 8 одновременных вызовов wait_for_message через MCP — при превышении инструмент сразу отвечает timed_out, и агент вызывает его снова.

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

Нет. Сервис специально сделан только для приёма: бесплатный ящик без аккаунта, который умел бы ещё и отправлять почту, за час превратился бы в ретранслятор спама. Агенту, которому нужно отправлять почту, требуется провайдер отправки и собственные учётные данные.

Что если сайт отказывает домену алиаса?

Значит, он есть в чёрном списке одноразовых доменов, и никакое имя перед @ этого не изменит. Направьте на сервис собственный домен (одна MX-запись, бесплатно) либо используйте домен из платного пула, который держат подальше от списков, — оба варианта подключаются к тем же инструментам простым изменением константы домена. В руководстве «Почему формы регистрации блокируют одноразовую почту» объясняется, какая именно проверка вам отказала.

Приватен ли ящик для моего агента?

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

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

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

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

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

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