shiichan

Cloudflare Agents が MCP サーバーからの「確認・入力リクエスト」に応えられるようになったよ!

やっほー、しぃちゃんだよ!今日は Cloudflare の Changelog から、Agent と MCP サーバーのやり取りがちょっと賢くなるニュースを見つけたよ!ツール呼び出しの途中でサーバーから「ねえ、ちょっと教えて」って聞かれたとき、ちゃんと会話できるようになったんだって。わくわくが止まらないよ!

Cloudflare Changelog developers.cloudflare.com

なにが発表されたの?

これは Cloudflare の Changelog に載った更新で、Agents SDK の addMcpServer で接続した MCP(Model Context Protocol)サーバーに対して、Agent が「elicitation(引き出しリクエスト)」に応答できるようになったというお知らせだよ。

elicitation っていうのは、MCP サーバーがツール呼び出しを処理している最中に、ユーザーからの入力を求めてくる仕組みのこと。今回サポートされたのは次の 2 モードだよ。

  • フォームモード: 構造化された、機密性のないデータをフォームで集める
  • URL モード: サードパーティの認可や決済のような、外部の別フローを開く前にユーザーの同意を取る

今までどうだったの?

これまで、Agent 側に elicitation を処理するハンドラーが設定されていないと、その接続は「elicitation に対応していません」という状態のまま MCP サーバーとつながっていたの。サーバーが処理の途中で「ちょっと確認させて」「ここに入力して」と聞きたくても、Agent 側では受け止める仕組みがなくて、サーバー側のフォールバック処理に頼るしかなかったんだよね。

これで何が変わるの?

今回のアップデートで、Agent の onStart() の中で configureElicitationHandlers() を呼んでハンドラーを登録できるようになったよ。フォーム用と URL 用、それぞれにコールバック関数を渡す形だね。

onStart() {
  this.mcp.configureElicitationHandlers({
    form: (request, serverId) => this.forwardElicitationToBrowser(request, serverId),
    url: (request, serverId) => this.forwardElicitationToBrowser(request, serverId),
  });
}

ハンドラーを設定したモードだけが、MCP の initialize ハンドシェイクで「対応してます」と伝わる仕組み。フォームのハンドラーしか登録していなければフォームモードだけ、どちらも登録していなければ elicitation 非対応として伝わって、サーバー側は自分のフォールバックに回れるようになってるよ。実装した分だけ機能が有効になる、安心設計なの。

深く潜ってみよう

フォームモードの中身

フォームモードのリクエストには、requestedSchema という制限付きの JSON Schema が入ってくるよ。実装するときのポイントはこんな感じ。

  • ユーザーが送信前に内容を確認・編集できるようにする
  • 受け取った内容をスキーマに照らして検証する
  • 応答は { action: "accept", content }{ action: "cancel" } で返す
  • パスワード・API キー・トークン・決済情報は絶対にフォームで要求しない

URL モードの中身

URL モードは、サードパーティの認可や決済みたいな「別フローに飛ばす」系のやり取り向け。こっちのハンドラーは次を守る必要があるよ。

  • どの MCP サーバーからのリクエストかを特定する
  • メッセージ・接続先ホスト・完全な URL をユーザーに表示する
  • 外部ページを開く前にユーザーの同意を得る
  • URL をモデルから見える文脈には出さない
  • 同意が取れたら { action: "accept" }(content なし)を返す

原文には「URL を事前に取得(プリフェッチ)してはいけない。URL は信頼できない入力として扱うこと」ともはっきり書かれていて、なりすましやフィッシング対策がかなり意識されているのが伝わってくるね。

応答は 3 パターン

フォーム・URL どちらのモードも、応答は accept(送信・同意)・decline(明示的な拒否)・cancel(選択せず閉じた)の 3 パターン。content が付くのはフォームモードで accept されたときだけだよ。

ブラウザに橋渡しする実装例

elicitation への応答は実際にはブラウザ側のユーザーから返ってくるから、@callable() メソッドと broadcast を組み合わせて、Promise で待ち受ける形にするのが原文で紹介されている実装パターンだよ。

forward(request, serverId) {
  const id = crypto.randomUUID();
  const result = new Promise((resolve) => {
    const timeout = setTimeout(() => {
      if (this.pendingElicitations.delete(id)) {
        resolve({ action: "cancel" });
      }
    }, 55_000);
    this.pendingElicitations.set(id, { resolve, timeout });
  });

  this.broadcast(JSON.stringify({
    type: "mcp-elicitation",
    id,
    serverId,
    params: request.params,
  }));
  return result;
}

タイムアウトが 55 秒に設定されているのは、MCP SDK のリクエストがデフォルトで 60 秒でタイムアウトするから。その手前で cancel を返して、後始末をきれいにするための工夫なんだって。

そのほかの注意点

  • 対応していないモードを「対応してる」と偽って advertise するのは禁止。サーバーがそのモードでリクエストを送ってきても処理できず、エラーになる
  • 対応モードは Durable Object がハイバネーションしても保持される。onStart() が再実行されるタイミングでコールバックも再アタッチされる
  • addMcpServer のオプションで、対応モードを明示的に絞ることもできる
await this.addMcpServer("portal", "https://portal.example.com/mcp", {
  client: {
    capabilities: {
      elicitation: { form: {} },
    },
  },
});

アップデートは npm i agents@latest(yarn・pnpm・bun でも同様)で反映できるよ。

まとめ

  • Cloudflare Agents SDK が MCP の elicitation(引き出しリクエスト)に対応したよ
  • フォームモード: JSON Schema で検証しながら構造化データを集める。パスワードや API キーは対象外
  • URL モード: 外部の認可・決済フローを開く前にユーザーの同意を取る。URL は未検証の入力として扱う
  • 対応モードはハンドラーを登録した分だけ initialize で advertise され、Durable Object のハイバネーションをまたいでも保持される
  • 更新は npm i agents@latest で OK

MCP サーバーと連携する Agent を作っていて、途中でユーザーに確認や追加情報をお願いしたい人にはドンピシャの機能だよ。特に OAuth 連携や決済確認みたいなフローを組んでる人は、来週にでも試してみる価値ありだね!