AIエージェント & MCP

AIエージェントのメール:人手なしで登録確認を通す

ブラウジングもフォーム入力も支払いもできるエージェントが、「コードについてはメールを確認してください」の一文で完全に立ち往生してしまいます。受信箱を持っていないからです。ここでは、アカウントもキーも不要な受信箱をエージェントに与える方法を紹介します — ブロッキング待機を使ったMCP経由、あるいはLangChain、OpenAI Agents SDK、Vercel AI SDK向けの素のツール関数として。さらに、サインアップを最後までやり遂げる4ステップのループと、無人稼働のエージェントに必要なガードレールも扱います。

  • 中級
  • 読了21分
灰色の机に向かう小さな灰色のロボットが青い封筒を掲げ、片手はキーボードの上、頭上には青いチェックマークが浮かんでいる

すべてのエージェントがぶつかる壁

トライアルにサインアップする、ワークスペースを作成する、APIキーを受け取る、ベータ版に参加する — そのすべてがフォームで終わり、すべてのフォームがメールボックスで終わります。人間ならスマートフォンをちらりと見るだけです。エージェントには、見るべきものが何もありません — 自分で読めるアドレスを持っていないからです。分別のあるエージェントは、そこで止まってあなたにコードを尋ねますが、それではエージェントを送り出した意味がなくなってしまいます。

解決策は、より賢いプロンプトではありません。プログラムから読み取れる受信箱であり、しかも先に作成すべきアカウントも(メールアカウントを作るエージェントは、1段階下で同じ壁にぶつかります)、管理すべきキーもないことです。使い捨てメールボックスは、まさにそれです。メールが届いた瞬間にメールボックスが存在するようになり、それを読むのはHTTPリクエスト1回だけです。

受信箱を与える2つの方法

同じメールボックスに、2つの方法でアクセスできます。どちらを選ぶかは、メールボックス側ではなく、エージェントがどう作られているかによります。

項目MCPサーバーRESTツール関数
向いている場面エージェントがMCPクライアント — Claude Code、Cursor、Claude Desktop、あるいはMCPアダプタを持つフレームワーク — の中で動いている場合。エージェントをコードで書く場合。LangChain、OpenAI Agents SDK、Vercel AI SDK、あるいは自作のループなど。
待機wait_for_messageは、サーバー側で最大25秒間ブロックし、メッセージ全体を返します。待機中に消費されるトークンはゼロです。ツール関数は、期限まで1秒に1回のループを回します。こちらもトークンはゼロです — ループはモデルの中ではなく、あなたのコードの中にあります。
セットアップクライアントの設定に、URLを1つ。コードは不要です。関数2つ、40行、HTTPライブラリ1つ。
モデルから見えるもの説明付きのツール6個と、接続時にサーバーが送る指示の段落。あなたが書いたツールの説明の内容そのものです。下のdocstringは、まさにそのために書かれています。

MCPの経路はたった1行で、クライアントごとのセットアップには、7つのクライアントそれぞれの正確な1行が載っています。AIエージェントが読める受信箱では、各ツールを詳しく説明しています。このガイドの残りの部分は、自分で組み立てるエージェント向けのREST経路です。

MCP経路、全体
{"mcpServers":{"grabmail":{"url":"https://grabmail.io/mcp"}}}

ツール関数 — LangChainとOpenAI Agents SDK向けのPython版

docstringを丁寧に書いた、素の関数が2つです。docstringはコードそのものより重要です。どちらのフレームワークでも、docstringはモデルが読む説明になり、いつツールを呼ぶか、答えをどう扱うかを決める材料になります。そのため、エージェントが間違えがちな2つのこと — エイリアスとアドレスの違い、そして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."}

それをラップするのは、フレームワークごとに1回の呼び出しだけです。LangChainのtoolはdocstringと型ヒントを読み取り、Agents SDKのfunction_toolも同様に読み取って、指示の中でループを繰り返すエージェントにそのツールを追加します。

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向けに

AI SDKのtool()は、説明、スキーマ、executeを受け取ります。説明の部分には、同じ2つの文を入れます。両方のツールを、4より大きいmaxStepsとともにgenerateTextstreamTextに渡してください。このループは4回のツール呼び出しでできているからです。

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.' };
  },
});

「ツール」という概念を持つどのフレームワークでも、形は同じです。モデルが読む説明、引数のスキーマ、そして自分の側で実行される関数です。引き継ぐべき2つの要素は、エイリアスについての文とtimed_outについての文です。それ以外はすべて、NodeガイドPythonガイドにあるクライアントそのものです。

ループ、4つのステップで

どのフレームワークであっても、サインアップは同じ4回のツール呼び出しです。エージェントに自分で発見させるのではなく、指示の中でそう伝えておくべきです。

  1. create_inboxを、タスクごとに1回。アドレス、エイリアス、そしてどちらがどちらかを説明する一文が返ってきます。
  2. フォームに入れるのはエイリアス。送信する。
  3. そのアドレスに対して、すぐにwait_for_messageを呼ぶ。確認メールに含まれるであろう単語 — 「code」「verify」「confirm」など — をsubject_containsに設定します。タイマーを待ってからでも、他の作業をこなしてからでもありません。メールはすでに向かっているところだからです。
  4. コードは、待機が返したメッセージから取り出します。エージェントはそれを入力するか、リンクをたどります。テンプレートが使う言葉の後に続く6桁の数字です — 自動テストにおけるOTPコードに抽出ルールがあり、それはテストと同じようにエージェントにもそのまま当てはまります。

timed_outを返す60秒の待機は、失敗ではありません。「まだ届いていない」というだけです。指示には、最大3回までもう一度呼び出すよう書いておくべきです。3回の呼び出しは3分以上に相当し、実際に送信されたトランザクションメールならどれでもカバーできます — そして同時に、フォームがエラーを表示していたことや、エイリアスの入力を間違えていたことに気づく機会を、エージェントに3回与えることにもなります。

エイリアスのルール

すべてのメールボックスには、2つのアドレスがあります。アドレスは、エージェントが読み取りに使うものです。アカウントというものが存在せず、アドレスだけが鍵であるため、それを持っている人なら誰でもメールボックスを開けます。エイリアスは、別のドメイン上にある第2のアドレスで、同じメールボックスへ配信はしますが、そこから読み取ることには使えません。

そのため、サイトにはエイリアスを渡し、エージェントはアドレスを手元に残します。フォームにアドレスを貼り付けてしまったエージェントは、そのサイトに — そして、そのサイトがそれを漏らした先の誰にでも — そこでエージェントが今後受け取るすべてのメッセージを読む力を与えてしまったことになります。上のツール関数はどちらも、どちらをどこに使うかを示すnext_stepとともに両方を返し、指示の中でもそれを繰り返しています。2回言われたルールは、守られるルールだからです。

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.
エイリアスは、エージェントが差し出すものであり、それで読み返そうとしても空のメールボックスしか返ってきません。アドレスは、エージェントが手元に残すものであり、それで読み返せばすべてのメッセージが返ってきます。

無人稼働のエージェントのためのガードレール

テストスイートは、失敗すれば止まります。しかしエージェントは、状況を読み違えても動き続け、コストを消費し続けます。次の5つの制限によって、メールのステップがタスクの中で最もコストのかかる部分になることを防げます。

タスクごとに1つの受信箱
タスクや実行をまたいでアドレスを使い回さないでください。もっともらしいコードが入った古いメッセージは、エージェントが自信満々に間違ったことをしてしまう最短経路です。create_inboxにコストはかかりません。毎回呼び出してください。
待機の予算
wait_for_messageは3回まで。それを超えたら止まって報告してください。決して送信されないメールを無期限に待ち続けるエージェントは、ワーカーの枠と費用を無駄に消費します。
ステップ全体に対する期限
送信からコード取得まで、全体で5分間。それを過ぎたら、正しい行動はフォームをもう一度試すことではなく、人間に何が起きたかを伝えることです。
件名によるフィルタリング
常にsubject_containsを渡してください。そうしないと、コードメールより先に届くウェルカムメールが「そのメッセージ」になってしまい、エージェントはマーケティング用のフッターから6桁の数字を抜き出そうとしてしまいます。
アドレスをログに記録する
アドレスをタスクのログに書き込んでください。メッセージは5日間残るため、後から人間がそのメールボックスを開き、エージェントが実際に見たものを正確に確認できます — 実行がうまくいかなかったときに、これほど役立つものはありません。

ブラウザエージェント

実際のブラウザを操作するエージェント — Browser Use、PlaywrightのMCPサーバー、コンピュータ操作モデルなど — にとって、メールのステップは最も厳しく効いてきます。誰かが想定するより先に、そのフォームに出会ってしまうからです。うまく機能させるための3点を挙げます。

  • 両方のサーバーを与える。ブラウザ用のツールと受信箱用のツールを同じセッションに置き、「メールを確認してください」がツール呼び出しで済むようにし、行き止まりにしないでください。
  • ループをシステムプロンプトに入れる。4行です。最初のメールアドレス欄で受信箱を作る、フォームにはエイリアスを入れる、送信直後にアドレスを対象に待機する、timed_outの場合は3回まで再試行する。
  • 拒否されることを想定しておく。エイリアスのドメインを拒否するフォームは、ページ上にそう表示します。エージェントは、名前を変えて試すのではなく、そのエラーを読んで止まるべきです。拒否を正攻法で回避する方法 — 独自ドメイン、あるいはブロックリストに載らないプールのドメイン — は、エージェントではなくあなたが決める設定上の判断です。

エージェント自身が読むためのものとして、このサイトはllms.txtを公開しています。これは、このページと同じ内容を、モデルが好む形式で示すプレーンテキストのマップです。また、コードを書くエージェントがクライアントを構築するためのOpenAPIドキュメントもあります。

無人で実行させる前に

  • エイリアスのルールとtimed_outの意味を明記したツールの説明。
  • 4ステップのループと、3回までの待機予算を含む指示。
  • create_inboxはタスクごとに1回呼び出し、使い回さない。
  • すべての待機でsubject_containsを使う。
  • アドレスをタスクのログに書き込む。
  • 機密情報を受信箱へ送ることを禁じる一文。

エージェントに必要なのは、これがすべてです。同じ受信箱が、昼はテストスイートに、夜はエージェントに使われます。その内部は、同じ3つのHTTP呼び出しにすぎないからです — そして、エージェントがサインアップするサイトが公開ドメインを拒否する場合は、独自ドメインブロックリストに載らないプールを、ツールを変更することなく差し込めます。

質問

エージェント用にAPIキーは必要ですか?

いいえ。公開ドメインは、RESTでもMCPでも同様に、キーもアカウントもヘッダーも不要です。使い捨てメールのブロックリストに載らない有料のドメインプールだけがベアラートークンを使いますが、それ以外のツール関数はまったく同じです。

MCPとRESTツール、どちらを選ぶべきですか?

エージェントがすでにMCPクライアントの中で動いているなら、MCPです。1行で済み、待機はサーバー側で行われます。フレームワークでエージェントを書いているなら、ツール関数です。40行で済み、どのモデルでも動作し、モデルが読む説明を自分でコントロールできます。どちらも、同じメールボックスにたどり着きます。

待機には、トークンとしてどれくらいのコストがかかりますか?

どちらの場合も、コストはかかりません。MCPの待機はサーバー側でブロックされ、RESTツールはあなたのコードの中でループします。モデルがトークンを消費するのは、ツール呼び出しと結果の読み取りに対してであり、間の60秒に対してではありません — これこそが、モデル自身にメールボックスをポーリングさせるべきではない理由のすべてです。

複数のエージェントを同時に実行できますか?

できます。それぞれのタスクが専用の受信箱を持ち、セッション状態というものはありません。上限は、RESTではアドレスあたり1秒に1回の読み取りとクライアントあたり1分に1200リクエスト、MCPでは同時に8件までのwait_for_message呼び出しです — それを超えると、ツールは即座にtimed_outを返し、エージェントはもう一度呼び出します。

エージェントは、その受信箱からメールを送信できますか?

できません。設計上、このサービスは受信専用です。送信もできてしまうアカウント不要の無料受信箱は、1時間以内にスパムの中継地点になってしまうでしょう。メールを送信する必要があるエージェントには、送信用のプロバイダと、それ専用の認証情報が必要です。

サイトがエイリアスのドメインを拒否した場合はどうなりますか?

その場合、そのドメインは使い捨てドメインのブロックリストに載っており、@の前にどんな名前を置いても状況は変わりません。独自ドメインをこのサービスに向けるか(MXレコード1件で、無料です)、ブロックリストに載らない有料プールのドメインを使ってください — どちらも、ドメインの定数を変えるだけで同じツールに差し込めます。サインアップフォームが使い捨てメールをブロックする理由では、自分がどちらのチェックに拒否されたのかを解説しています。

その受信箱は、自分のエージェント専用の非公開のものですか?

いいえ。アドレスを知っている人なら誰でも読めます。だからこそエイリアスが存在し、だからこそ機密情報をそこへ送ってはいけません。10分しかもたないコードであれば問題ありませんが、これはエージェントの指示の中で、はっきりと明記しなければならない唯一のルールです。

新しいうちに試してみてください

アドレスの取得はワンクリックで、アカウントもカードも不要です。このガイドの内容はすべて、そのアドレスですぐに試せます。

おかえりなさい

受信箱とドメインを、ひとつの場所に。