MCPのresultTypeフィールド — complete/input_requiredの見分け方
MCP 2026-07-28版は、すべての成功応答にresultTypeフィールドを必須化しました。complete/input_requiredの2値と、後方互換の欠落時扱いを実装例とあわせて解説します。
MCPの成功応答は、2026-07-28版からresultオブジェクトにresultTypeフィールドを含むことが必須になりました。resultTypeは結果の種類を示す文字列で、クライアントはこの値を見てからresultの残りをどう解釈するか決めます。コア仕様が定義する値は"complete"と"input_required"の2つだけです。この2値の使い分けと、値が欠けている応答の扱い、受け取ったクライアントが取るべき手順を、実装例とあわせて説明します。
resultTypeは何のために必須化されたか
resultTypeが導入される前、MCPの結果応答は基本的に単一の形しか想定していませんでした。リクエストが成功すれば、resultにその内容がそのまま入る構造です。
事情が変わったのは、サーバーが処理を終える前にクライアント側の追加入力(elicitation・sampling・roots)を必要とするケースを、MCPがマルチラウンドトリップリクエスト(MRTR)としてプロトコルに組み込んだためです。1つのリクエストに「完了した結果」と「まだ追加入力が要る」という構造の違う2種類の応答があり得るので、resultTypeがその見分け役を担います。仕様の表現では、resultは任意のJSONオブジェクト構造を取れる一方で、resultTypeだけは必ず含まれ、クライアントはこれを見てパース方法を切り替えます。
MRTR自体も2026-07-28版の新規導入で、MRTRのページ冒頭に破壊的変更と明記されています。サーバーからクライアントへroots/list・sampling/createMessage・elicitation/createを送る従来のやり方は置き換えられ、仕様は「旧来のサーバー起点リクエストのパターンはもうサポートしない」と述べています。
MRTRの狙いは、複数のサーバーインスタンスをまたいで状態を共有するストレージ層も、特定インスタンスへ固定するステートフルなロードバランシングも要らない形で、追加入力を求められるようにすることです。サーバーは必要な文脈をrequestStateという1つの不透明な文字列に入れて返し、次に届くリトライがそれを運んできます。この考え方は、同じ版でinitializeのハンドシェイクが廃止され、各リクエストが_metaにプロトコルバージョンとクライアントのcapabilitiesを載せるようになった変更(ステートレス化)と同じ流れにあります。
complete/input_requiredの2つの値
resultTypeの2つの値
complete
リクエストが成功し、resultに最終的な内容が入っています。tools/callなら、ツールの実行結果がそのまま返ります。クライアントはこの応答で処理を終えます。
input_required
リクエストは未完了で、続けるには情報が要ります。resultはInputRequiredResultという専用の構造を持ち、inputRequestsとrequestStateの少なくとも一方を含みます。
次の例は、サーバーがツール呼び出しの途中で、GitHubのユーザー名(elicitation)と、LLMへの問い合わせ結果(sampling)の2つを同時に求めているケースです。inputRequestsのキー(github_loginとcapital_of_france)はサーバーが自由に付ける識別子で、リクエスト内で重複してはいけません。
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"resultType": "input_required",
"inputRequests": {
"github_login": {
"method": "elicitation/create",
"params": {
"mode": "form",
"message": "GitHubのユーザー名を入力してください",
"requestedSchema": {
"type": "object",
"properties": { "name": { "type": "string" } },
"required": ["name"]
}
}
},
"capital_of_france": {
"method": "sampling/createMessage",
"params": {
"messages": [
{
"role": "user",
"content": { "type": "text", "text": "What is the capital of France?" }
}
],
"systemPrompt": "You are a helpful assistant.",
"maxTokens": 100
}
}
},
"requestState": "AEAD-protected blob"
}
}仕様は拡張機能が独自のresultType値を追加することも認めています。ただし使える値の集合は、コアプロトコルの値に、クライアントがcapabilitiesで広告している拡張の値を足したものです。この集合にない値は未認識として扱われ、無効と見なされます。
混同しやすい点として、resultTypeが必須になるのは成功応答(resultフィールドを持つ応答)だけです。失敗した場合は別構造のerrorフィールドを使い、resultTypeは登場しません。実装では「応答にresultとerrorのどちらが入っているか」を先に見て、resultのときだけresultTypeを参照する順序になります。
resultTypeが欠けている応答をどう扱うか
ここが実装で最も間違えやすい点です。resultTypeは2026-07-28版で必須になったフィールドですが、それより前の版のサーバーはこのフィールドを送ってきません。
仕様はこの後方互換のために、はっきりした規則を置いています。クライアントは、resultTypeが欠落した結果を"complete"として扱わなければなりません。旧サーバーからの応答は「フィールドが無いから不正」ではなく、暗黙に完了扱いというのが正しい実装です。有無で判定を分岐させる実装は、旧サーバーとの通信で、完了した処理を未完了と誤判定します。
欠落は"complete"、未認識の値は無効という2つの規則は、別々の分岐として書く必要があります。次の関数は、この2点とresult/errorの順序を押さえた分類の一例です(説明用に書いたもので、特定のSDKのコードではありません)。
type Outcome =
| { kind: "complete"; result: Record<string, unknown> }
| { kind: "input_required"; result: Record<string, unknown> }
| { kind: "error"; error: unknown }
| { kind: "invalid"; reason: string };
function classify(res: { result?: any; error?: unknown }): Outcome {
if (res.error !== undefined) return { kind: "error", error: res.error };
const r = res.result ?? {};
const type = r.resultType ?? "complete"; // 欠落は complete 扱い
if (type === "complete") return { kind: "complete", result: r };
if (type === "input_required") return { kind: "input_required", result: r };
return { kind: "invalid", reason: `unrecognized resultType: ${type}` };
}拡張のresultTypeを広告しているクライアントなら、最後のinvalidの手前に、その拡張の値を受け入れる分岐を足すことになります。
InputRequiredResultが返ってくる3つのリクエスト
InputRequiredResultを返してよいのは、仕様が明示したprompts/get・resources/read・tools/callの3種類のクライアントリクエストだけです。仕様は「これら以外のリクエストには送ってはならない」と書いています。
initializeとpingは、この版ではもう存在しません。変更履歴によると、ハンドシェイクは廃止され、代わりにサーバーがserver/discoverを実装して対応バージョンやcapabilitiesを広告します。pingもlogging/setLevelと並んで削除され、ログレベルはリクエストごとに_metaのio.modelcontextprotocol/logLevelで指定します。
InputRequiredResultはinputRequestsとrequestStateの2つの任意フィールドを持ちますが、どちらか一方は必ず含まれていなければなりません。inputRequestsはサーバーが割り当てたキーごとに、elicitation/create・sampling/createMessage・roots/listのいずれかのリクエストを並べたマップです。requestStateはサーバーだけが意味を知る不透明な文字列です。
サーバーには、クライアントがcapabilitiesで対応を宣言していない種類をinputRequestsに入れてはならない、という制約もあります。たとえばクライアントがelicitationに対応していなければ、elicitation/createは含められません。
クライアントがInputRequiredResultを受け取ったときの手順
tools/callでの往復
- 1
最初のリクエストを送る
クライアントが
tools/callをID 1で送ります。 - 2
サーバーが追加入力を求める
サーバーはelicitationが必要と判断し、ID 1への応答として
InputRequiredResult(inputRequestsとrequestState付き)を返します。最初のリクエストはここで終了します。 - 3
クライアントが入力を集める
クライアントはユーザーに入力を促し、回答を
InputResponsesにまとめます。 - 4
新しいIDでリトライする
同じ
tools/callを、新しいID(たとえば2)で再送します。paramsにinputResponsesと、受け取ったrequestStateを添えます。 - 5
サーバーが完了する
サーバーは
requestStateから文脈を復元し、今度はresultType: "complete"を返します。
リトライのparams.inputResponsesは、inputRequestsのキーに対応するマップです。値はキーごとの応答結果で、elicitationならElicitResult、samplingならCreateMessageResult、rootsならListRootsResultが入ります。スキーマ上、inputResponsesとrequestStateはどちらも省略可能なパラメータとして定義されています。
{
"github_login": {
"action": "accept",
"content": { "name": "octocat" }
},
"capital_of_france": {
"role": "assistant",
"content": { "type": "text", "text": "The capital of France is Paris." },
"model": "claude-3-sonnet-20240307",
"stopReason": "endTurn"
}
}クライアント側の義務は次の4点です。
inputRequestsがあれば、その入力をそろえてからリトライする。inputRequestsが無くrequestStateだけなら、すぐにリトライしてよいrequestStateがあれば、中身を調べたり加工したりせず、受け取った値をそのまま送り返す。requestStateが無かったなら、リトライにも付けない- JSON-RPCのIDは初回とリトライで必ず変える。両者は独立した別のリクエストとして扱われる
inputRequestsとrequestStateは元のリクエストのリトライにだけ作用し、並行して送っている他のリクエストには使わない
サーバー側のエラー処理にも規則があります。InputResponsesの形式が不正なら、通常のJSON-RPCエラーを返すべきとされています。一方、要求した情報の一部しか届かなかったときは、エラーにせず新しいInputRequiredResultで不足分を求め直します。必要としない余分な情報が含まれていた場合は無視してかまいません。
requestStateはクライアントを信用しない前提で設計する
requestStateは仕組み上、クライアントを経由して往復します。仕様はこれを攻撃者が制御し得る入力として扱うことをサーバーに義務付けています。認可判断やリソースアクセス、業務ロジックに影響するなら、HMACやAEADのような改ざん検知を付け、検証に失敗した状態は拒否しなければなりません。改ざんされても失敗以上の被害が出ない場合に限り、この保護を省略できます。
再送攻撃(リプレイ)への対策として、仕様は次の3つを改ざん検知で守られたrequestStateの中に入れ、受信のたびに検証することを推奨しています。
- 認証済みのプリンシパル(別のプリンシパルが提示した状態は拒否する)
- 短い有効期限(TTL)
- 元リクエストの識別子(メソッド名と主要パラメータのダイジェストなど。一致しないリクエストでの提示は拒否する)
ただし、これらはリプレイの窓を狭め、他ユーザー・他リクエストでの使い回しを防ぐだけで、一度きりの利用までは保証しません。ワンタイムの権限付与のように使い切りが必要な場合は、サーバー側で別途その不変条件を強制する必要があります。
resultTypeが変えたのは「新しい機能」ではなく「型付けの有無」
elicitationもsamplingも、サーバーがクライアントに情報を求める仕組みとしては以前からありました。resultTypeとInputRequiredResultが変えたのは、その問い合わせを独立したサーバー起点のリクエストではなく、元のリクエストへの応答として返す形にした点です。
旧方式では、サーバー起点の問い合わせが元のリクエストとは別のメッセージとして飛んでいました。新方式では、問い合わせも再開に必要な状態も1つの応答に収まるので、クライアントは応答のresultTypeを読むだけで、次にやることを判断できます。
クライアント・サーバーの実装をゼロから作る場合の設計はMCPサーバー自作ガイド、Agent SDKからMCPサーバーへ接続する構成はAgent SDK MCP接続ガイドにまとめています。
まとめ
実装の分岐は、errorかresultか、resultTypeの欠落("complete"扱い)、未認識値(無効)の3段で決まります。サーバーを書く場合は、input_requiredを返せるリクエストが3種類に限られることと、requestStateが攻撃者の制御し得る入力である点が設計の中心です。認証まわりの実装はリモートMCPのOAuth認証、Claude Code側からの接続設定はClaude Code MCP設定ガイドで扱っています。