AI agent & MCP

Email cho tác tử AI: xác minh đăng ký không cần con người

Một agent có thể duyệt web, điền biểu mẫu và thanh toán vẫn khựng lại hoàn toàn ở đúng câu “hãy kiểm tra email để lấy mã”, vì nó không có hộp thư nào cả. Đây là cách cấp cho nó một hộp thư không cần tài khoản và không cần key — qua MCP với một lượt chờ bị block, hoặc dưới dạng một hàm công cụ thuần túy cho LangChain, OpenAI Agents SDK và Vercel AI SDK — cùng vòng lặp bốn bước đưa trọn một lượt đăng ký đi hết, và những rào chắn mà một agent không người giám sát cần có.

  • Trung cấp
  • 21 phút đọc
Một robot xám nhỏ ngồi ở bàn xám giơ cao phong bì xanh, một tay đặt trên bàn phím, phía trên lơ lửng một dấu tích xanh

Bức tường mà agent nào cũng đâm phải

Đăng ký một bản dùng thử, tạo một workspace, xin một API key, tham gia một bản beta: mọi việc trong số này đều kết thúc ở một biểu mẫu, và mọi biểu mẫu đều kết thúc ở một hộp thư. Một con người thì liếc qua điện thoại. Một agent không có gì để liếc qua cả — nó không có địa chỉ nào để đọc, và những agent biết điều sẽ dừng lại và hỏi bạn xin mã, điều này lại phá hỏng chính mục đích của việc cử một agent đi làm việc đó.

Cách khắc phục không phải là một prompt khôn khéo hơn. Đó là một hộp thư mà agent có thể đọc được bằng chương trình, không cần tạo tài khoản trước (một agent tạo tài khoản email sẽ đâm phải chính bức tường đó, chỉ lùi lại một bậc) và không cần quản lý key nào cả. Một hộp thư dùng một lần chính xác là như vậy: hộp thư tồn tại ngay khoảnh khắc thư đến, và đọc nó chỉ là một yêu cầu HTTP.

Hai cách để cấp cho nó một hộp thư

Cùng một hộp thư có thể tiếp cận theo hai cách, và lựa chọn phụ thuộc vào cách agent được xây dựng chứ không phải vào bản thân hộp thư:

Tiêu chíMáy chủ MCPHàm công cụ REST
Phù hợp khiAgent chạy trong một MCP client — Claude Code, Cursor, Claude Desktop, hoặc một framework có adapter MCP.Bạn tự viết agent bằng code: LangChain, OpenAI Agents SDK, Vercel AI SDK, hoặc vòng lặp tự viết của riêng bạn.
Chờ đợiwait_for_message block ở phía máy chủ tối đa 25 giây rồi trả về nguyên vẹn cả thư. Không tốn token nào trong lúc chờ.Hàm công cụ lặp lại mỗi giây một lần cho đến hạn chót của nó. Cũng không tốn token nào — vòng lặp nằm trong code của bạn, chứ không nằm trong mô hình.
Thiết lậpMột URL trong cấu hình của client. Không cần code.Hai hàm, bốn mươi dòng, một thư viện HTTP.
Những gì mô hình nhìn thấySáu công cụ kèm mô tả, cộng thêm một đoạn instructions mà máy chủ gửi đi ngay khi kết nối.Bất cứ điều gì mô tả công cụ của bạn nói. Các docstring bên dưới được viết ra chính là để đóng vai trò đó.

Đường MCP chỉ tốn một dòng, và bài cách cấu hình cho từng client có sẵn dòng chính xác cho bảy client; bài một hộp thư mà AI agent có thể đọc được giải thích các công cụ này sâu hơn. Phần còn lại của hướng dẫn này là đường REST, dành cho những agent bạn tự xây dựng.

đường MCP, đầy đủ
{"mcpServers":{"grabmail":{"url":"https://grabmail.io/mcp"}}}

Một hàm công cụ: Python, cho LangChain và OpenAI Agents SDK

Hai hàm thuần túy với docstring được viết cẩn thận. Docstring quan trọng hơn cả bản thân code: trong cả hai framework, chúng trở thành phần mô tả mà mô hình đọc để quyết định khi nào gọi công cụ và làm gì với câu trả lời, nên chúng nói rõ hai điều mà agent hay làm sai — alias so với address, và timed_out nghĩa là gì.

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."}

Bọc chúng lại chỉ tốn một lệnh gọi cho mỗi framework. tool của LangChain đọc docstring và type hint; function_tool của Agents SDK làm điều tương tự và thêm các công cụ đó vào một agent có instructions lặp lại đúng vòng lặp này:

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)

Cùng công cụ đó bằng TypeScript, cho Vercel AI SDK

tool() của AI SDK nhận vào một description, một schema và một execute; description mang theo đúng hai câu đó. Truyền cả hai công cụ vào generateText hoặc streamText với maxSteps lớn hơn bốn, vì vòng lặp này dài đúng bốn lượt gọi công cụ:

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

Hình dạng này giống hệt nhau ở bất kỳ framework nào có khái niệm về công cụ: một description để mô hình đọc, một schema cho các tham số, và một hàm chạy ở phía bạn. Hai điều cần mang theo là câu về alias và câu về timed_out; mọi thứ còn lại chính là client lấy từ hướng dẫn Node.js hoặc hướng dẫn Python.

Vòng lặp, trong bốn bước

Dù là framework nào, một lượt đăng ký cũng chỉ là bốn lệnh gọi công cụ giống nhau, và agent nên được nói rõ điều này ngay trong instructions của nó, thay vì để nó tự khám phá ra:

  1. create_inbox, một lần cho mỗi tác vụ. Trả về một address, một alias, và một câu cho biết cái nào là cái nào.
  2. Alias được điền vào biểu mẫu. Gửi đi.
  3. wait_for_message trên address, ngay lập tức, với subject_contains đặt thành một từ mà thư xác nhận chắc chắn sẽ mang theo — “code”, “verify”, “confirm”. Không phải theo một bộ đếm giờ, không phải sau khi làm việc khác: thư đã đang trên đường đến rồi.
  4. Mã được lấy ra từ chính thư mà lượt chờ đã trả về; agent gõ mã đó vào hoặc đi theo liên kết. Sáu chữ số đứng sau những từ mà template sử dụng — bài mã OTP trong các bài kiểm thử tự động có các quy tắc trích xuất, áp dụng cho một agent hệt như cho một bài kiểm thử.

Một lượt chờ sáu mươi giây trả về timed_out không phải là một thất bại; đó là “chưa xong đâu”. Instructions nên nói rõ: gọi lại, tối đa ba lần. Ba lượt gọi là hơn ba phút, đủ bao trùm mọi thư giao dịch thực sự đã được gửi đi — và cho agent ba cơ hội để nhận ra rằng biểu mẫu đã báo lỗi, hoặc nó đã gõ sai alias.

Quy tắc về alias

Mọi hộp thư đều có hai địa chỉ. Address là địa chỉ mà agent dùng để đọc; bất kỳ ai có nó đều có thể mở hộp thư, vì không có tài khoản nào cả và address chính là chìa khóa duy nhất. Alias là một địa chỉ thứ hai trên một tên miền riêng biệt, đưa thư vào cùng hộp thư đó nhưng không thể dùng để đọc nó.

Vậy nên trang web nhận được alias, còn agent giữ lại address. Một agent dán address vào một biểu mẫu đã trao cho trang web đó — và cho bất kỳ ai mà trang web làm rò rỉ nó tới — khả năng đọc mọi thư mà agent sẽ nhận được ở đó về sau. Các hàm công cụ ở trên trả về cả hai kèm một next_step cho biết cái nào dùng ở đâu, và instructions lặp lại điều đó một lần nữa, vì một quy tắc được nói hai lần là một quy tắc được tuân theo.

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.
Alias là thứ agent đưa ra và khi đọc lại chỉ thấy một hộp thư trống; address là thứ nó giữ lại và khi đọc lại sẽ thấy mọi thư.

Rào chắn cho một agent không có người giám sát

Một bộ kiểm thử thất bại thì dừng lại. Một agent đọc sai tình huống thì vẫn tiếp tục, và tiếp tục tốn kém. Năm giới hạn sau giữ cho bước email không trở thành phần đắt đỏ nhất của một tác vụ:

Một hộp thư cho mỗi tác vụ
Đừng bao giờ dùng lại một địa chỉ giữa các tác vụ hay các lượt chạy khác nhau. Một thư cũ chứa một mã trông có vẻ hợp lý là cách nhanh nhất khiến agent tự tin làm sai một cách chắc nịch. create_inbox không tốn kém gì cả; hãy gọi nó mỗi lần.
Ngân sách lượt chờ
Ba lượt gọi wait_for_message, sau đó dừng lại và báo cáo. Một agent cứ chờ vô thời hạn cho một thư chưa từng được gửi sẽ đốt mất một worker slot và cả tiền bạc.
Một hạn chót cho toàn bộ bước này
Năm phút tính từ lúc gửi biểu mẫu đến lúc có mã, trọn vẹn từ đầu đến cuối. Quá thời hạn đó, hành động đúng đắn là báo cho một con người biết chuyện gì đã xảy ra, chứ không phải thử lại biểu mẫu lần nữa.
Lọc theo subject
Luôn truyền subject_contains. Nếu không, một thư chào mừng đến trước thư chứa mã sẽ trở thành “bức thư đó”, và agent sẽ phân tích ra sáu chữ số từ một dòng chân trang quảng cáo.
Ghi log địa chỉ
Ghi address vào log của tác vụ. Thư được giữ lại 5 ngày, nên một con người có thể mở hộp thư sau đó và xem chính xác những gì agent đã thấy — điều hữu ích nhất khi một lượt chạy gặp trục trặc.

Agent điều khiển trình duyệt

Một agent điều khiển một trình duyệt thật — Browser Use, một máy chủ MCP cho Playwright, một mô hình computer-use — chính là trường hợp mà bước email gây khó chịu nhất, vì nó sẽ gặp biểu mẫu trước khi có ai kịp lên kế hoạch cho việc đó. Ba điều sau giúp nó hoạt động trơn tru:

  • Cấp cho nó cả hai máy chủ. Các công cụ trình duyệt và các công cụ hộp thư trong cùng một phiên, để “kiểm tra email của bạn” trở thành một lệnh gọi công cụ chứ không phải một ngõ cụt.
  • Đặt vòng lặp này vào system prompt. Bốn dòng: tạo một hộp thư ngay tại ô nhập email đầu tiên; điền alias vào biểu mẫu; chờ trên address ngay sau khi gửi; thử lại ba lần khi gặp timed_out.
  • Hãy lường trước việc bị từ chối. Một biểu mẫu từ chối tên miền của alias sẽ báo điều đó ngay trên trang; agent nên đọc lỗi đó và dừng lại, chứ không nên thử các tên khác nhau. Những cách xử lý đường hoàng để vượt qua một sự từ chối — một tên miền riêng của bạn, hoặc một tên miền từ nhóm được giữ ngoài các danh sách chặn — là quyết định về cấu hình dành cho bạn, không phải cho agent.

Để chính agent đọc được, trang web này công bố llms.txt, một bản đồ dạng văn bản thuần túy nói lên đúng những điều trang này nói, dưới dạng mà một mô hình ưa thích hơn, cùng một tài liệu OpenAPI mà một agent viết code có thể dựa vào để xây dựng client.

Trước khi bạn để nó chạy không người giám sát

  • Các mô tả công cụ nói rõ quy tắc về alias và timed_out nghĩa là gì.
  • Instructions có vòng lặp bốn bước và ngân sách ba lượt chờ.
  • create_inbox được gọi một lần cho mỗi tác vụ, không bao giờ dùng lại.
  • subject_contains trên mọi lượt chờ.
  • Address được ghi vào log của tác vụ.
  • Một dòng cấm gửi bất cứ điều gì mang tính bí mật đến hộp thư.

Đó là tất cả những gì một agent cần. Cùng một hộp thư phục vụ một bộ kiểm thử vào ban ngày và một agent vào ban đêm, vì bên dưới vẫn chỉ là ba lệnh gọi HTTP giống nhau — và nếu trang web mà agent đăng ký từ chối các tên miền công khai, một tên miền riêng của bạn hoặc nhóm được giữ ngoài các danh sách chặn sẽ lắp vào vừa khít mà không cần thay đổi gì ở các công cụ.

Câu hỏi

Tôi có cần API key cho agent không?

Không cần. Các tên miền công khai không đòi hỏi key, tài khoản hay header nào cả, cả qua REST lẫn qua MCP. Chỉ có nhóm tên miền trả phí được giữ ngoài các danh sách chặn email dùng một lần mới dùng đến bearer token, còn ngoài điều đó ra thì các hàm công cụ vẫn giống hệt.

MCP hay một công cụ REST — tôi nên chọn cái nào?

Nếu agent đã sống sẵn trong một MCP client, hãy dùng MCP: chỉ tốn một dòng và lượt chờ nằm ở phía máy chủ. Nếu bạn đang viết agent bằng một framework, hãy dùng một hàm công cụ: chỉ tốn bốn mươi dòng, hoạt động với bất kỳ mô hình nào, và bạn kiểm soát được phần mô tả mà mô hình đọc. Cả hai đều dẫn đến cùng một hộp thư.

Việc chờ đợi tốn bao nhiêu token?

Không tốn gì cả, theo cả hai cách. Lượt chờ MCP block ở phía máy chủ; công cụ REST lặp trong code của bạn. Mô hình chỉ tốn token cho lệnh gọi công cụ và cho việc đọc kết quả, chứ không tốn cho sáu mươi giây ở khoảng giữa — và đó chính xác là lý do vì sao không nên để một mô hình tự mình thăm dò một hộp thư.

Nhiều agent có thể chạy cùng lúc không?

Có. Mỗi tác vụ có hộp thư riêng của nó và không có trạng thái phiên nào cả. Các giới hạn là một lần đọc mỗi giây cho mỗi địa chỉ và 1200 yêu cầu mỗi phút cho mỗi client qua REST, cùng 8 lượt gọi wait_for_message chạy đồng thời qua MCP — vượt quá mức đó, công cụ sẽ trả lời timed_out ngay lập tức và agent gọi lại.

Agent có thể gửi email từ hộp thư này không?

Không thể. Dịch vụ này được thiết kế để chỉ nhận thư — một hộp thư miễn phí, không cần tài khoản mà lại có thể gửi thư thì sẽ trở thành một trạm chuyển tiếp spam chỉ trong vòng một giờ. Một agent cần gửi thư phải có một nhà cung cấp dịch vụ gửi thư và thông tin xác thực riêng của nó.

Điều gì xảy ra nếu trang web từ chối tên miền của alias?

Khi đó tên miền này đang nằm trong một danh sách chặn tên miền dùng một lần, và không cái tên nào đứng trước dấu @ có thể thay đổi được điều đó. Hãy trỏ một tên miền bạn sở hữu vào dịch vụ này (một bản ghi MX, miễn phí), hoặc dùng một tên miền từ nhóm trả phí được giữ ngoài các danh sách chặn — cả hai đều lắp vào cùng các công cụ đó chỉ bằng cách đổi hằng số tên miền. Bài vì sao biểu mẫu đăng ký chặn email dùng một lần giải thích chính xác cách kiểm tra nào đã từ chối bạn.

Hộp thư này có riêng tư với agent của tôi không?

Không riêng tư. Bất kỳ ai biết địa chỉ đều có thể đọc được nó, đó chính là lý do alias tồn tại và cũng là lý do vì sao không bao giờ nên gửi bất cứ điều gì mang tính bí mật đến đó. Với một mã chỉ tồn tại mười phút thì điều đó không sao cả; đây là quy tắc duy nhất mà instructions của agent phải nói rõ ràng.

Hãy thử ngay khi nó còn mới

Một địa chỉ chỉ mất một cú nhấp, không tài khoản và không thẻ. Mọi thứ trong hướng dẫn này đều hoạt động ngay trên đó.

Chào mừng trở lại

Hộp thư và tên miền của bạn, ở cùng một nơi.