MCPのMulti-Round-Trip Tools入門|実行前にユーザー確認を挟む

保護カバー付きの確認ボタンとノートPCを置いたMCP実行確認の検証机 MCP
カテゴリー
MCP
公開日
2026.09.06

はじめに

ファイル削除、デプロイ、メール送信のような操作を、AIの判断だけで即実行する設計は危険です。MCP 2026-07-28のMulti-Round-Trip Tools(MRTR)は、ツールが途中で入力を要求し、クライアントが回答を付けて同じ呼び出しを再試行できる仕組みです。

ここでは本番デプロイを模したツールに確認を挟みます。実際のデプロイは行わず、プロトコル上の流れだけを実装します。

この記事で分かること

通常のツールは「呼び出す→結果を返す」の1往復で終わります。MRTRでは途中で会話をいったん返し、ユーザーの回答を受け取ってから再開します。この記事の完成イメージは、productionへデプロイしますか?という確認が表示され、許可した場合だけ模擬結果を返すツールです。

  • Round Trip:要求を送り、応答を受け取る1往復
  • inputRequired:追加の入力が必要だとサーバーが知らせる応答
  • inputResponses:クライアントが集めた回答
  • 冪等性:同じ要求が再送されても、処理が意図せず二重実行されない性質

SDKの基本的なサーバー作成とstdio接続は、前の記事「MCP TypeScript SDK v2入門」を先に試しておくと理解しやすくなります。

この例は@modelcontextprotocol/client@modelcontextprotocol/serverの2.0.0を対象にします。2026-07-28の機能を使うには、クライアントでプロトコルバージョンを自動判定するか、次のように固定します。

const client = new Client(
  { name: "safe-deploy-client", version: "1.0.0" },
  {
    capabilities: { elicitation: { form: {} } },
    versionNegotiation: { mode: { pin: "2026-07-28" } },
    inputRequired: { autoFulfill: true, maxRounds: 3 },
  },
);

elicitation/createのhandlerをクライアントへ登録しておくと、callTool()は入力要求をhandlerへ渡し、回答を付けて元のツールを自動的に再呼び出しします。maxRoundsは、サーバーが入力要求を繰り返した場合の無限ループを防ぐ上限です。

仕組み

  1. クライアントがdeployを呼ぶ
  2. サーバーがinputRequired(...)を返す
  3. クライアントが確認フォームをユーザーへ表示する
  4. 回答をinputResponsesへ入れて同じツールを再試行する
  5. サーバーが回答を検査して処理する

これはサーバー側で処理を待ち続ける方式ではありません。再試行に必要な状態は、入力やrequestStateから復元できるようにします。

実装

import {
  acceptedContent,
  inputResponse,
  inputRequired,
  McpServer,
} from "@modelcontextprotocol/server";
import { z } from "zod";

const confirmSchema = z.object({ confirm: z.boolean() });
const server = new McpServer({ name: "safe-deploy", version: "1.0.0" });

server.registerTool(
  "deploy",
  {
    description: "指定環境へデプロイします",
    inputSchema: z.object({ environment: z.enum(["staging", "production"]) }),
  },
  async ({ environment }, ctx) => {
    const response = inputResponse(ctx.mcpReq.inputResponses, "confirmation");
    if (response.kind === "elicit" && response.action !== "accept") {
      return { content: [{ type: "text", text: "デプロイを中止しました" }] };
    }

    const answer = acceptedContent<{ confirm: boolean }>(
      ctx.mcpReq.inputResponses,
      "confirmation",
    );

    if (!answer) {
      return inputRequired({
        inputRequests: {
          confirmation: inputRequired.elicit({
            message: `${environment}へデプロイしますか?`,
            requestedSchema: confirmSchema,
          }),
        },
      });
    }

    const checked = confirmSchema.safeParse(answer);
    if (!checked.success || !checked.data.confirm) {
      return { content: [{ type: "text", text: "デプロイを中止しました" }] };
    }

    return { content: [{ type: "text", text: `${environment}へデプロイしました` }] };
  },
);

inputResponse()で利用者のdeclineまたはcancelを先に検出し、その場合は再質問せず中止します。acceptedContent()は、回答がacceptedの場合に内容を取り出しますが、業務用の入力検証までは代行しません。同じZodスキーマで再検証しています。

requestStateを使う場合

requestStateはサーバーが発行し、クライアントから再送される不透明な文字列です。クライアントを経由した時点で信頼できない入力として扱います。注文IDや権限を入れるなら、HMAC署名または暗号化を行い、改ざんを検出してください。

確認後にも次を再検査します。

  • 現在のユーザーに実行権限があるか
  • 対象環境が許可リストにあるか
  • 確認時から重要な対象情報が変わっていないか
  • 同じ操作が二重実行されていないか

動作確認

正常系は「最初の呼び出しで入力要求」「trueを回答した再試行で完了」の2段階です。異常系としてfalse、decline、cancel、回答欠落、confirm: "yes"、改ざんしたrequestStateを試します。

クライアントがMRTRをサポートしない場合は完走できません。従来のプロトコルバージョンで接続した場合もinput_requiredの語彙を利用できません。導入前にクライアント能力とネゴシエーション結果を確認し、非対応時の説明や代替フローを用意します。

確認画面では、対象を省略せず「production環境へデプロイ」のように表示します。「実行しますか?」だけでは、利用者が何を許可するのか判断できません。falseを選んだ場合に「中止しました」と表示され、実処理へ進まないことも確認してください。

よくあるトラブル

  • 最初の呼び出しで処理が完了する:確認回答がない場合のinputRequired()分岐を確認する
  • 確認画面が表示されない:利用中のクライアントがMRTRへ対応しているか確認する
  • 再試行しても再び確認される:回答IDのconfirmationが要求側と一致しているか確認する
  • "yes"を送ると失敗する:この例のconfirmは文字列ではなく真偽値である

確認を挟むだけでは、権限チェックや二重実行防止の代わりにはなりません。学習用コードをそのまま本番デプロイへ接続しないでください。

関連記事

まとめ

MRTRは、ツールがユーザー入力を必要とすることをプロトコル上で表現できます。ただし、確認画面を出すだけで安全になるわけではありません。回答の再検証、再認可、改ざん対策、冪等性を組み合わせてください。

参考リンク

コメント

タイトルとURLをコピーしました