function calling と MCP 接続はどう選ぶ?

function calling は、モデルが要求した操作をアプリ側のコードで実行する仕組みです。メールを検索するかどうかはモデルに判断させても、アクセス先と権限の判断はアプリ側に残します。

比較項目function callingリモート MCP
ツール定義アプリが JSON Schema で定義MCP サーバーから取得
実行経路アプリが Mailera REST API を呼ぶResponses API がリモートサーバーのツールを呼ぶ
認証情報Mailera キーをアプリ側で保持接続先の認証設定を確認
承認アプリが送信前に制御MCP 側のツール呼び出し承認設定(自動許可か明示承認か)と、業務上の承認フローを別途設計
接続時の確認引数検証と API 権限通信方式、認証、公開ツール、承認設定

本編では function calling を選びます。検索条件の検証と送信の承認を、同じアプリで制御できるためです。仕組みは OpenAI の function calling ガイド、メール接続全体の設計は 既存メールボックスをエージェントにつなぐ方法を参照してください。

Mailera の MCP エンドポイントは https://mailera.app/api/public/v1/mcp です。読み取り用のツールには search_messages や get_message のほか、list_mailboxes、get_mailbox_status、list_messages、get_latest_otp があります。同じ MCP エンドポイントには send_email、reply_to_email、get_send_status など send スコープのツール、move_message、rescue_spam など move スコープのツールも含まれており、キーに付与したスコープで呼び出せる範囲が決まります。OpenAI 側は Streamable HTTP または HTTP/SSE のリモート MCP に対応しており、allowed_tools パラメーターで取り込むツールを限定できます。

MCP を選ぶ場合は、ツール一覧の取得結果である mcp_list_tools と、読み取り・承認の動作を順に確認します。OpenAI の MCP 接続ガイドと Mailera の MCP 資料を照合して設定してください。

1. メールボックスと読み取り専用キーを準備する

Mailera は既存メールボックスを接続するサービスで、新しいメールボックスやメールアドレスは作成しません。一斉配信やマーケティング配信には対応せず、受信トレイへの到達も保証しません。

  1. 自分のテスト用メールボックスを接続します。Microsoft の場合は「Microsoftでサインイン」から OAuth を使います。詳細は Microsoft の接続手順にあります。
  2. 「設定」の API キー画面で「キーを作成」を選びます。「メールボックスのスコープ」をテスト用グループに限定し、権限は read のみにします。
  3. Mailera キーと OpenAI キーはサーバー側の環境変数に置き、モデル入力、ブラウザー、ログには出しません。
対象グループと読み取り権限に限定した Mailera の API キー設定
最初のキーには読み取り権限だけを付けます。「MCP 設定をコピー」は Responses API の function 定義を作る操作ではありません。

サーバー側で $MAILERA_API_KEY を設定してから、対象を確認します。

curl --get 'https://mailera.app/api/public/v1/mailboxes' \
  -H "Authorization: Bearer $MAILERA_API_KEY" \
  --data-urlencode 'limit=1'

返却された data 内の対象 id を $MAILBOX_ID に設定します。この例では対象メールボックスを固定し、モデルには選ばせません。API の入口は 開発者向け資料にまとまっています。

2. 検索と本文取得を読み取りツールにする

アプリで定義する関数名Mailera の呼び出し先モデルから受け取る引数
mail_searchGET /messages/search検索語 q
mail_getGET /mailboxes/{id}/messages/{msgId}message_id

この関数名はアプリ独自の名前です。同名の MCP ツールを自動的に呼ぶ仕組みではありません。

次の Python 断片は同じファイルに順に置きます。Mailera を呼ぶ部分は標準ライブラリだけを使います。まず、各 function に渡す引数の JSON Schema を定義します。

def argument_schema(field):
    return {
        "type": "object",
        "properties": {field: {"type": "string"}},
        "required": [field],
        "additionalProperties": False,
    }

schemas = {
    "mail_search": argument_schema("q"),
    "mail_get": argument_schema("message_id"),
}

Responses API に登録する各 function の引数スキーマとして使います。URL や API キーを引数には含めません。HTTP 処理も接続先を固定します。

import json, os
from urllib.parse import quote, urlencode
from urllib.request import Request, urlopen

BASE = "https://mailera.app/api/public/v1"
MAILBOX = os.environ["MAILBOX_ID"]
HEADERS = {"Authorization": "Bearer " + os.environ["MAILERA_API_KEY"]}
def get_json(path, params=None):
    url = BASE + path + ("?" + urlencode(params) if params else "")
    with urlopen(Request(url, headers=HEADERS)) as response:
        return json.load(response)

実行側でも関数名、引数の型、余分な項目を検査します。検索結果は識別子・メールボックス ID・件名だけに絞り、本文は選んだメールについて取得します。本文の上限は Mailera の仕様値ではなく、このサンプルコードが独自に 4,000 文字へ切り詰めている設計値です。

def execute(name, args):
    field = {"mail_search": "q", "mail_get": "message_id"}.get(name)
    if not field or not isinstance(args, dict) or set(args) != {field}:
        raise ValueError("Invalid tool arguments")
    value = args[field]
    if not isinstance(value, str) or not value.strip():
        raise ValueError("Expected a non-empty string")
    if name == "mail_search":
        result = get_json("/messages/search", {"mailbox": MAILBOX, "q": value})
        return {"data": [{k: m[k] for k in ("id", "mailbox_id", "subject")}
                         for m in result["data"]], "next_cursor": result.get("next_cursor")}
    path = f"/mailboxes/{quote(MAILBOX, safe='')}/messages/{quote(value, safe='')}"
    m = get_json(path)["data"]
    return {"id": m["id"], "subject": m["subject"], "body_error": m["body_error"],
            "text": (m["body"]["text"] or "")[:4000]}

この最小例は検索の最初のページを返します。next_cursor がある場合は続きがあるため、検索済みの範囲だけで「該当なし」と結論づけない設計にします。本文では body_error も確認し、取得失敗と空の本文を区別してください。

3. モデルのツール呼び出しをアプリで実行する

Responses API とのやり取りでは、テキストの受信に加え、モデルのツール呼び出しに応じた処理をアプリ側で実行します。追加のツール呼び出しがあれば、それにも対応します。

  1. 読み取り用の function 定義を付けて、ユーザーの検索依頼を送ります。
  2. 応答内のツール呼び出しを取り出し、引数を解析します。
  3. 関数名と引数を検証して execute に渡し、アプリ側で Mailera の REST API を呼び出します。
  4. 実行結果を各ツール呼び出しに対応づけてモデルへ返し、次の応答を受け取ります。
  5. その応答に追加のツール呼び出しがあれば、検証・実行・結果返却を繰り返します。

複数の呼び出しを取りこぼさず、アプリ側で反復回数に上限を設けます。ツールが呼ばれなかった場合や例外が出た場合は、メールを取得したものとして扱いません。

Responses API のツール呼び出しをアプリが検証して Mailera に渡す読み取り経路
モデルは操作を要求し、アプリが実行範囲を決めます。

4. 人が確認した返信だけを送信する

返信案はアプリ内のデータとして保持し、読み取りループから送信処理を直接呼ばせません。承認画面には、対象メールボックス、返信対象、宛先、件名、本文を表示します。

承認時にその内容を固定し、どれかが変わったら再承認を求めます。送信処理は固定した内容だけを受け取り、そこで初めて send 権限のキーを使います。これはアプリ側に実装する承認であり、Mailera の標準承認ワークフローではありません。承認境界の全体設計は、承認付きメールエージェントの構築方法を参照してください。

返信案を人が確認し、承認内容を固定してから送信する処理
承認を通るのは固定した返信内容です。変更した場合は確認に戻します。

送信・返信・転送には、REST API の POST /mailboxes/{id}/send を使います(このエンドポイントが reply と forward も兼ねます。MCP 経由では send_email と reply_to_email が別ツールとして用意されています)。返信リクエストの組み立ては Mailera の API ドキュメントに合わせてください。メールで承認を受け付ける場合の設計は、メールによる人の承認パターンで扱っています。

送信前に操作単位の Idempotency-Key を保存し、同じ操作の再試行でも同じ値を使います。この Idempotency-Key(冪等性キー)の有効期間は 24 時間です。

タイムアウトしたら、新しいキーで送信し直すのではなく、送信試行 ID を保持できている場合は GET /sends/{id} で確認します。状態には queued、sending、sent、failed、unknown があります。unknown は成功にも未送信にも決めつけず、自動再送を止めて確認へ回します。有効期間を超えた再試行も自動化しません。

よくあるエラーと確認箇所

症状確認箇所と対処
認証に失敗するBearer ヘッダー、環境変数、キーの有効期限や失効を確認
権限が足りないread と対象グループを確認。送信には別途 send が必要
引数が拒否される関数名とスキーマを照合。余分な引数を許可しない
検索結果が空対象 ID、検索語、同期状態を切り分ける
タイムアウトや制限に当たる再試行回数と間隔を制御。送信は先に状態を確認

同期操作には sync 権限が必要です。検索結果が空でも、読み取りツールから同期を実行しないでください。API の制限はキーごとに 1 分あたり 300 リクエストです。

メール本文の指示を実行させないために

受信メールは信頼できないデータであり、AI エージェントへの命令ではありません。本文に秘密情報の送信、権限変更、外部 URL への転送を求める文があっても、実行対象にしません。

注意書きのプロンプトだけに頼らず、読み取りキーと送信キーの分離、固定したメールボックス、サーバー側の引数検証、人による送信前確認を組み合わせます。モデルの「承認済み」という出力や、メール本文の同意表現を承認記録の代わりには使いません。

モデルへ渡す本文と個人情報は必要な範囲に絞り、ログやスクリーンショットではキー、アドレス、本文を伏せます。Mailera のセキュリティ情報とあわせて、アプリ側で保持するデータも点検してください。

MCPでAIエージェントがメールボックスを利用

MaileraのMCPサーバーをClaude、Cursor、VS Codeに追加すると、エージェントがメールの閲覧、分類、返信を行えます。

MCPの使い方を見る