重い処理を、MCPはどう見せて、どう止めるのか
連載「ReactでMCPクライアントを作る」第5回 — 進捗通知・購読・Tasks拡張と、協調的なキャンセル
第1回では、MCPがホスト・クライアント・サーバーという三者をJSON-RPC 2.0でつなぐ仕組みであることを確認しました。第2回では、そのやり取りがStreamable HTTPという1本のPOSTの上でどう組み立てられているかを、実際にリクエストを送って確かめました。第4回では、ブラウザから実際にツールを呼び出したときの挙動——進捗通知がどう届くか、接続を切ったときに何が起きるか——を扱いました。
この連載で作っているのは、自然言語の問い合わせをSQLに変換し、データベースへ問い合わせて表を返すアプリでした。今回はその中でも、すぐには終わらない問い合わせを主役にします。たとえば「過去1年分の注文データを、商品ごとに集計して教えて」のような、実行に数秒から数十秒かかる重いクエリです。ユーザーは結果を待つあいだ何を見せられ、途中でやめたくなったときに何ができるのでしょうか。
MCPの仕様には、この「待つ」という問題への答えが3つの層に分かれて用意されています。進捗通知、変更通知の購読、そしてTasks拡張です。今回はこの3つを順に見ていき、とくに3つ目のTasks拡張を中心に掘り下げます。
目次
待てない処理の3つの層
まず、3つの層がそれぞれ何を担当するのか、地図を描きます。
| 層 | 仕組み | 向いている場面 | この記事での扱い |
|---|---|---|---|
| 層1: 進捗通知 | notifications/progress。実行中のリクエストの応答ストリームに相乗りする | 「今どのくらい進んだか」を一言で伝えたいとき | 第4回で実測済み。ここでは短く引く |
| 層2: 変更通知の購読 | subscriptions/listen。長命のストリームを別途開く | ツールやリソースの一覧が変わった、といった非同期の変化を継続的に受け取りたいとき | 未実測。仕様文の引用のみ |
| 層3: Tasks拡張 | 処理をハンドル(taskId)として受け取り、tasks/getでポーリングする | 接続を保持し続けられない、あるいは保持したくないほど長い処理を扱いたいとき | この記事の中心。実測あり |
3つとも「オプトイン」という共通点があります。進捗通知はクライアントがprogressTokenを添えたときだけ、購読はsubscriptions/listenを呼んだときだけ、Tasksはクライアントとサーバーの両方が対応を宣言したときだけ動きます。どれも黙っていても動く仕組みではありません。
層1: 進捗通知 — 同じ会話に相乗りする実況
進捗通知の仕組み自体は第4回で実測済みなので、ここでは要点だけ引きます。クライアントがリクエストの_metaにprogressTokenを添えると、サーバーはその値・現在の進捗・任意の合計値・任意のメッセージを持つnotifications/progressを、同じリクエストの応答ストリームの上に、最終結果より前に送ってよいことになっています。
The progress value MUST increase with each notification, even if the total is unknown.
「progressの値は、合計が不明であっても、通知のたびに増加しなければならない」という規定です。第4回の実測では、5件の進捗通知が約0.7秒間隔で先に届き、最終結果は最後に届くことを確認しています。進捗通知は「今の会話の実況」であり、リクエストが終わればそれも終わります。
Progress notifications MUST stop after completion.
サーバーが応答を返した後は、進捗通知はもう来ません。この層でできることは「今動いているリクエストの実況」までで、リクエストをまたいだ状態の保持や、接続を切った後の再開はできません。それを担うのが次の層です。
層2: 変更通知の購読 — 長命ストリームを開く
進捗通知が「1本のリクエストに相乗りする」仕組みなら、リクエストと無関係にサーバー側の変化を受け取りたいときはどうするのでしょうか。それがsubscriptions/listenです。1本のリクエストに相乗りするのではなく、それ自体が長命のストリームを開くためのリクエストになります。
subscriptions/listenopens a long-lived notification stream from the server to the client. Unlike one-off requests, the stream stays open and delivers notifications until the client cancels it. It replaces the formerresources/subscribeRPC and the HTTP GET endpoint.
「subscriptions/listenは、サーバーからクライアントへの長命の通知ストリームを開く。単発のリクエストと違い、このストリームはクライアントが打ち切るまで開いたままで通知を届け続ける。かつてのresources/subscribeというRPCと、HTTPのGETエンドポイントを置き換えるものである」という説明です。
第1回で触れたとおり、2026-07-28のリビジョンはGETストリームを仕様から取り除きました。その代わりを務めるのが、このsubscriptions/listenです。
クライアントはリクエストのボディでtoolsListChangedやresourceSubscriptionsといったフィルタを指定し、欲しい通知の種類だけを購読します。サーバーはnotifications/subscriptions/acknowledgedという最初のメッセージで、実際にどの種類を配送するかを返してから、ストリームを開いたままにします。そしてこのストリームは、進捗通知とは明確に切り分けられています。
Request-scoped notifications like notifications/progress and notifications/message are not delivered on the listen stream — they flow only on the response stream of the request they relate to.
「notifications/progressやnotifications/messageのようなリクエスト単位の通知は、購読のストリームには流れない。それらは、関係するリクエスト自身の応答ストリーム上でのみ流れる」という規定です。つまり、層1と層2は同じ「通知」という言葉を使いますが、乗っているストリームが別です。
このSDK(@modelcontextprotocol/sdk 1.30.0)は、第1回・第2回で確認したとおり2025-11-25までの仕様を実装しており、subscriptions/listenは実装されていません。この節の内容はすべて仕様文からの引用であり、実装で動作を確かめたものではありません。
層3: Tasks拡張 — 処理をハンドルにして持ち帰る
ここからが今回の中心です。そもそもTasks拡張は、何を解決しようとしているのでしょうか。公式ドキュメントはこう説明しています。
Not every tool call returns instantly. Some operations — CI pipelines, batch processing, human approvals — take seconds, minutes, or longer. MCP Tasks let servers return a durable handle instead of blocking, so clients can poll for progress, provide input when needed, and retrieve the final result after reconnecting.
「すべてのツール呼び出しがすぐに結果を返すわけではない。CIパイプライン、バッチ処理、人手による承認のような処理は、数秒、数分、あるいはそれ以上かかる。MCP Tasksは、サーバーがブロックする代わりに永続的なハンドルを返せるようにし、クライアントが進捗をポーリングしたり、必要なときに入力を渡したり、再接続後に最終結果を取得したりできるようにする」というのが、Tasksの狙いです。接続を張ったまま待つ、という層1・層2のやり方とは違い、Tasksは「一度その場を離れて、あとで見に来る」ための仕組みです。
Tasksはオプトインの拡張機能であり、コア仕様ではありません。
MCP Tasks is an extension to the core MCP specification. Host support varies by client.
「MCP Tasksはコアとなる MCP 仕様に対する拡張である。ホストの対応状況はクライアントによって異なる」と明記されています。さらに、実測に使った公式TypeScript SDK(1.30.0)でも、Tasksはexperimental/tasksというモジュール名で同梱されており、モジュール自身が冒頭のコメントで「実験的なAPIであり、予告なく変わりうる」と書いています。仕様上は拡張、実装上は実験的という、二重に「まだ確定していない」領域を扱っている点は、この先を読み進めるうえで踏まえておいてください。
以降の実測は、Node.js v22.12.0、@modelcontextprotocol/sdk 1.30.0、実行日2026-08-06の環境によるものです。quickという即座に返るツールと、long_jobというexecution: { taskSupport: 'required' }を持つツールを1本ずつ持つサーバーを、Streamable HTTP(ステートレス)で立てて検証しました。特定の製品やサービスには依存していません。
タスク化の合図は、二つの形がある
まずtools/listを見ると、タスク対応かどうかがツールの定義に現れています。
"name":"quick" "taskSupport":"forbidden"
"name":"long_job" "taskSupport":"required"
taskSupport: 'required'のツールは、タスクとして呼ばないと実行できません。実際に、タスク化の合図を付けずにlong_jobを呼ぶと、ツールの失敗として返ってきます。
event: message
data: {"result":{"content":[{"type":"text","text":"MCP error -32601: Tool long_job requires task augmentation (taskSupport: 'required')"}],"isError":true},"jsonrpc":"2.0","id":2}
ここで問題になるのが、「タスクとして呼ぶ」という意思をどう伝えるかです。そしてここで、仕様の形と実装の形が食い違います。 仕様(2026-07-28、Tasks拡張の実装ガイド)は、リクエストごとに_metaの中でネゴシエーションすることを定めています。
{
"jsonrpc": "2.0",
"id": 1,
"method": "...",
"params": {
"_meta": {
"io.modelcontextprotocol/clientCapabilities": {
"extensions": {
"io.modelcontextprotocol/tasks": {}
}
}
}
}
}
一方、実測した公式TypeScript SDK(1.30.0)が受け付けるのは、これとは別の形でした。paramsにtaskという専用フィールドを直接添えると、タスクとして実行されます。
{
jsonrpc: '2.0', id: 3, method: 'tools/call',
params: { name: 'long_job', arguments: { steps: 5 }, task: { ttl: 300000 } }
}
_metaの下で拡張として名乗る仕様上の形と、params.taskという専用フィールドを使うこのSDKの形は、別物です。 第1回で確認したとおり、このSDKは2026-07-28で定義された_metaベースの拡張ネゴシエーションの仕組みをまだ実装していません。実際に効いたのはparams.taskという、いずれ変わりうる実装依存の形でした。今日このSDKでTasksを呼ぶコードを書くなら、そこは覚えておきたいところです。
サーバー側にも宣言が要る
クライアント側の合図をそろえても、サーバー側の準備が欠けていると、今度は本物のJSON-RPCエラーになります。
event: message
data: {"jsonrpc":"2.0","id":3,"error":{"code":-32603,"message":"Server does not support task creation (required for tools/call)"}}
これを解消するには、サーバーの構築時に次を渡す必要がありました。
new McpServer(
{ name: 'sandbox-server', version: '0.0.1' },
{
capabilities: { tasks: { requests: { tools: { call: true } }, list: true, cancel: true } },
taskStore,
taskMessageQueue: taskQueue,
},
);
capabilities.tasksでどの操作をタスク化してよいかを宣言し、taskStoreでタスクの状態をどこに保持するかを渡します。このどちらが欠けても動きません。 仕様は、サーバー側の対応表明をserver/discoverという別メソッドの応答で行うと定めていますが、第1回で確認したとおりserver/discover自体、このSDKにはまだありません。実測したサーバー側の宣言も、クライアント側のparams.taskと同じく、SDK独自の形です。
タスクの状態遷移
タスクが取りうる状態は、仕様が5つに定めています。
| 状態 | 意味 |
|---|---|
working | 処理が進行中 |
input_required | サーバーが続行前にクライアントからの入力を必要としている |
completed | 処理が完了し、resultに最終出力が入っている |
failed | 実行中にJSON-RPCエラーが発生し、errorに詳細が入っている |
cancelled | 処理がキャンセルされた(必ず尊重されるとは限らない) |
completed, failed, and cancelled are terminal — once reached, the task’s state does not change.
「completed・failed・cancelledは終端状態であり、一度到達すると、そのタスクの状態はもう変わらない」という規定です。今回の実測はこのうちworkingからcompletedへの遷移と、workingからcancelledへの遷移を確かめました。input_required(サーバーからの追加入力要求)は、この検証環境では作っていません。
CreateTaskResultとポーリング間隔
タスクとして呼び出すと、結果の代わりにハンドルが返ってきます。
event: message
data: {"result":{"task":{"taskId":"eec0548145956f108cd21273beabae13","status":"working","ttl":300000,"createdAt":"2026-08-06T08:41:26.796Z","lastUpdatedAt":"2026-08-06T08:41:26.796Z","pollInterval":500}},"jsonrpc":"2.0","id":3}
taskId・status・ttl(有効期限)・createdAt・lastUpdatedAt・pollInterval(サーバーが提示する推奨ポーリング間隔)が入っています。Tasks拡張の実装ガイドはこれをttlMs・pollIntervalMsという名前で説明していますが、実測したSDKのレスポンスはttl・pollIntervalという、末尾にMsの付かない名前でした。ここでも仕様の語彙と実装の語彙にズレがあります。フィールド名を仕様の記述だけで決め打ちせず、実際に返ってきたJSONを見て確かめましょう。
pollInterval: 500は、「500ミリ秒おきにtasks/getで見に来てください」というサーバーからの提案です。強制ではありません。 クライアントがこれより短い間隔で叩いても、長い間隔で叩いても、プロトコル上はエラーになりません。ただし、間隔の選び方は次の節で見るとおり、見える情報の粒度に直結します。
tasks/get でポーリングする — statusMessageは現在値であって履歴ではない
tasks/getを、サーバーの更新間隔(0.6秒)より少し長い0.8秒おきに3回投げてみました。
data: {"result":{"taskId":"eec…13","status":"working",…,"statusMessage":"step 1/5"},"jsonrpc":"2.0","id":4}
data: {"result":{"taskId":"eec…13","status":"working",…,"statusMessage":"step 2/5"},"jsonrpc":"2.0","id":5}
data: {"result":{"taskId":"eec…13","status":"working",…,"statusMessage":"step 4/5"},"jsonrpc":"2.0","id":6}
3回目でstep 3/5が飛び、step 4/5になっています。 サーバーは5段階すべてでstatusMessageを更新していますが、ポーリングの間隔(0.8秒)が更新の間隔(0.6秒)より長いため、途中の状態がまるごと見えなくなりました。statusMessageは、tasks/getを呼んだその瞬間にサーバーが持っている値を返しているだけで、そこまでの進捗の履歴を蓄積して返しているわけではありません。
UIに逐次表示したい場合、ポーリング間隔をサーバーの更新頻度より十分短くしない限り、途中の段階は取りこぼれます。層1の進捗通知(同じストリームに乗り続ける実況)と、層3のポーリング(点で観測する)の違いが、ここに表れています。
完了後もstatusMessageは残る
タスクが完了した後、同じtasks/getをもう一度呼んでみました。
data: {"result":{"taskId":"eec…13","status":"completed",…,"statusMessage":"step 4/5"},"jsonrpc":"2.0","id":7}
statusはcompletedに変わっているのに、statusMessageは最後に書き込まれた"step 4/5"のまま残っています。 サーバーは完了時にstatusMessageを空にしたり、別の文言に書き換えたりはしません。これをそのままUIに出すと、「4/5まで進んで完了した」と誤解される表示になりかねません。statusMessageは進捗の説明であって、完了そのものの説明ではないことを踏まえ、statusが終端に達したらstatusMessageの表示を切り替える、といった扱いは、書き手が自分で用意することになります。
結果はtasks/resultで受け取る
完了したstatusを見ても、そこには結果本体は入っていません。結果は別のメソッド、tasks/resultで取ります。
data: {"result":{"content":[{"type":"text","text":"finished after 5 steps"}],"_meta":{"io.modelcontextprotocol/related-task":{"taskId":"eec0548145956f108cd21273beabae13"}}},"jsonrpc":"2.0","id":8}
中身は、そのツールを同期的に呼んでいたら返ってきていたはずのCallToolResultそのもので、_metaに元のタスクへの参照が付いています。状態を知るメソッド(tasks/get)と、結果を受け取るメソッド(tasks/result)が分かれているという点は、素朴に「ポーリングして結果を待つ」実装を書くときに見落としやすいところです。なおtasks/listを呼ぶと、これまで作ったタスクの一覧が返ってきます。
data: {"result":{"tasks":[{"taskId":"eec…13","status":"completed",…}],"_meta":{}},"jsonrpc":"2.0","id":9}
存在しないtaskIdをtasks/getに渡すと、ツールの失敗としてではなく、本物のJSON-RPCエラーで返ります。
data: {"jsonrpc":"2.0","id":5,"error":{"code":-32602,"message":"MCP error -32602: Failed to retrieve task: Task not found"}}
tasks/cancel は、状態を変えるだけ
クライアントは、いつでもtasks/cancelを送れます。仕様は、これが何を保証し、何を保証しないかをはっきり書いています。
Cancellation is cooperative — the server acknowledges the intent but is not obligated to stop the work.
「キャンセルは協調的なものである。サーバーは意図を受け取るが、作業を止める義務は負わない」という規定です。実際に、20ステップのタスクを作り、1.2秒後にtasks/cancelを送ってみました。
data: {"result":{"_meta":{},"taskId":"76f5…f0","status":"cancelled",…,"statusMessage":"Client cancelled task execution."},"jsonrpc":"2.0","id":11}
statusはきちんとcancelledに変わり、以降のtasks/getもcancelledを返します。ここまでは仕様どおりで、期待とも一致します。
問題は、その裏で実際に処理を行っているコードです。今回の検証サーバーは、tasks/cancelを受けても処理のループを止めていませんでした。ループは走り続け、次に状態を書き込もうとしたところで例外になりました。
[server] task 76f50aa2b84a9305a41c7beb08d0a3f0 working 1/20
[unhandledRejection] Error: Cannot update task 76f50aa2b84a9305a41c7beb08d0a3f0 from terminal status 'cancelled' to 'working'. Terminal states (completed, failed, cancelled) cannot transition to other states.
at InMemoryTaskStore.updateTaskStatus (…/experimental/tasks/stores/in-memory.js:112:19)
タスクストアは、終端状態から別の状態への遷移を拒みます(先に見た終端状態の規定どおりです)。しかし拒むだけで、処理そのものを止めてはくれません。 書き手がキャンセルの意図を自分のコードで観測して、そこで処理を打ち切らない限り、裏の処理は走り続け、状態を更新しようとするたびに例外が飛び続けます。今回のように素朴に書くと、これはunhandled rejectionとしてプロセスに届きます。
tasks/cancelは状態を変えるが、裏の処理は自分では止まらない旧連載が予定していた「自作ポーリング」への答え
この連載の前身では、この回に「自分でポーリングの仕組みを書く」という内容を予定していました。タスクIDを自前で発行し、一定間隔で状態を見に行き、終わったら止める——という実装です。Tasks拡張は、この素朴な実装が担っていた部分のかなりを、仕様側の答えとして持っています。タスクの識別子(taskId)、推奨ポーリング間隔(pollInterval)、終端状態の定義、結果の取得方法は、もう自分で設計しなくても仕様とSDKが用意しています。
一方で、今回の実測でわかったとおり、まだ自分で書く必要がある部分も残っています。
statusMessageの安全な表示: ポーリング間隔次第で途中の状態は取りこぼれ、完了後も古い文言が残る。UIに出す前に、書き手が状態を見て取捨選択するしかない- キャンセルの実効化:
tasks/cancelはサーバーに意図を伝えるだけで、実際に処理を止めるコードは書き手が用意する必要がある。放置すると、今回のように例外が飛び続ける - タスクIDの永続化: 「再接続後にポーリングを再開できる」というTasksの利点は、クライアント側が
taskIdをどこかに保存しておいて初めて生きる。仕様はハンドルの形までは決めるが、保存先までは決めていない
自作のポーリングをすべて書き直す必要はなくなりましたが、「ポーリングして待つ」という設計判断自体は、依然として書き手の仕事として残っています。
通底するテーマ — キャンセルは協調的
第4回では、SSEの応答ストリームを閉じても、キャンセル信号(extra.signal)を見ていないツールのハンドラは最後まで走り切ることを確認しました。今回、Tasksのtasks/cancelでも同じ性質が現れました。状態はcancelledに変わるのに、裏の処理は自分では止まらない。
この2つは、見た目の違うAPI(一方はストリームの切断、もう一方はtasks/cancelという明示的な呼び出し)ですが、根っこは同じです。仕様が定めているのは「キャンセルの意図を伝える手段」であって、「処理を止める手段」ではありません。
仕様が繰り返す”cancellation is cooperative”(キャンセルは協調的である)という一言に尽きます。プロトコルはキャンセルの意図を運びますが、それを受けて実際に処理を止めるかどうかは、ハンドラを書いたコード次第です。ストリームの切断であればextra.signalを、Tasksであればタスクの状態を、処理の途中で確認して自分から抜ける——その責任は常に書き手の側にあります。
もう一度、アプリの全体像へ
冒頭の「過去1年分の注文データを、商品ごとに集計して教えて」という重いクエリに、ここまでを当てはめてみましょう。ホストはまず、クエリを実行するToolを、進捗通知を受け取れる形で呼びます。サーバーが数秒で終わると判断すれば、層1の進捗通知だけで実況しながら結果を返してくれるでしょう。
サーバーがそのツールをタスク対応(taskSupport: 'required')にしていれば、ホストはparams.taskを添えて呼び出し、返ってきたtaskIdを使ってtasks/getをポーリングします。ポーリング間隔はpollIntervalを目安にしつつ、statusMessageをそのまま画面に出す前にstatusが終端かどうかを必ず確認します。
ユーザーが待ちきれずにキャンセルボタンを押したら、ホストはtasks/cancelを送ります。ただしそれは「サーバーに止めてほしいとお願いする」だけで、実際に止まる保証にはなりません。 ボタンを押したあともサーバー側の処理は続いているかもしれない——そう思ってホスト側のUIを設計しておきましょう。
この先扱うこと
今回で拾いきれなかった論点は、次のように送ります。
| 積み残した論点 | 送り先 |
|---|---|
subscriptions/listenの実測、notifications/tasksのpush配送、2026-07-28形式の拡張ネゴシエーションの実測 | いずれも未実測。この連載では扱わず、必要になった時点で個別に確認する論点 |
MRTR(InputRequiredResultとinputRequests / requestStateによるサーバーからの追加入力要求)の実測往復 | この連載の範囲外。 第6回は認可の設計・legacyとmodernの互換・生成SQLの是非がテーマで、MRTRは扱いません。仕様はTasks拡張のinput_requiredとMulti Round-Trip Requests(2026-07-28)を参照してください |
| 認可の設計、legacyとmodernのデュアルエラ互換、生成SQLをそのまま実行してよいか | 第6回 |
まとめ
MCPが「待てない処理」に用意している答えは、進捗通知・変更通知の購読・Tasks拡張という3つの層に分かれています。層1は今動いているリクエストの実況、層2は長命ストリームでの継続的な変化の配送、そして層3のTasksは、接続を保持できないほど長い処理を、ハンドルとポーリングで扱うための仕組みです。
Tasksは仕様上はコアではなく拡張であり、今回実測に使ったSDKでも「実験的で予告なく変わりうる」と明記された機能でした。 タスク化の合図一つとっても、仕様が定める_metaのネゴシエーションと、実測したSDKが受け付けるparams.taskは別の形で、フィールド名(ttlとttlMsなど)にも食い違いがありました。実装を動かして確かめると、statusMessageはポーリング間隔次第で取りこぼれ、完了後も古い文言が残ります。そしてtasks/cancelは状態を変えるだけで、裏の処理を止める保証にはなりません。
これは第4回で見た「ストリームを閉じてもハンドラは止まらない」と同じ、キャンセルは協調的という一貫した性質です。プロトコルが運ぶのは意図であり、実際に止める責任は常に書き手の側にあります。次回は連載の最終回として、認可の設計と、legacyとmodernの互換、そして生成されたSQLをそのまま実行してよいのかという問いを扱います。
この連載の記事
第1回から順に読むと、仕様の地図を描くところから、ブラウザで動くコードまでが一本の線でつながります。どの回からでも単体で読めるようには書いていますが、前後を行き来したくなったらここから飛んでください。
- 第1回: MCPクライアントは、結局どこにいるのか — ホスト・クライアント・サーバーの三者と、legacy/modernの断層
- 第2回: MCPサーバーに、どう話しかければ聞いてもらえるのか — Streamable HTTPの必須ヘッダー、SSE、Origin検証
- 第3回: MCPサーバーの窓口は、何を差し出し、何を隠すべきか — スキーマ、annotations、失敗の通り道
- 第4回: Reactは、MCPサーバーを直接叩けるのか — CORS、BFF、SSEの逐次読み、協調的キャンセル
- 第5回: 重い処理を、MCPはどう見せて、どう止めるのか(この記事) — 進捗通知、購読、Tasks拡張
- 第6回: その扉を開けるのは、誰の仕事か — 認可の設計、legacy/modernの互換判定、生成SQLの危うさ
参考にした一次情報
- Tasks Extension Overview(Tasksの狙い、タスクのライフサイクル、
CreateTaskResult、tasks/get/tasks/update/tasks/cancel、キャンセルが協調的であること、拡張ネゴシエーションの形、コア仕様への拡張である旨) - Progress(2026-07-28)(
progressToken、progressが増加しなければならない規定、完了後は停止する規定) - Subscriptions(2026-07-28)(
subscriptions/listenの長命ストリーム、通知フィルタ、resources/subscribeとGETエンドポイントの置き換え) - Cancellation(2026-07-28)(Streamable HTTPでのキャンセル、ストリーム切断が意図になる規定)
- Streamable HTTP(2026-07-28)(リクエスト単位の通知は購読ストリームには流れないという規定、GETストリームとプロトコルレベルセッションの廃止)
- Multi Round-Trip Requests(2026-07-28)(「この先扱うこと」で送ったMRTRの参照先。
InputRequiredResultとinputRequests/requestStateによるサーバーからの追加入力要求の仕組み。この記事では未実測)








