MCPの進捗通知(progress notifications)を実装するパターン
MCPの長時間処理向け進捗通知の仕様と、Claude Codeのタイムアウトとの関係を解説します。progressTokenのルール・progress値の単調増加・アイドル判定との関係を示します。
MCPには、時間のかかる処理の進み具合をクライアントへ伝える進捗通知(progress notifications)という任意機能があります。大きなファイルのインデックス作成や、外部APIへの多段リクエストのように、応答まで数秒から数十秒かかるツールで使います。MCPサーバーを自作するときに、長いツールがあれば実装を検討する対象です。
仕様側のルールは短く、実装の迷いどころはむしろ受け手のClaude Codeにあります。通知を送るとクライアントのアイドル判定が延び、送らないと数分で呼び出しが打ち切られる場合があるためです。
progressTokenと通知メッセージの仕様
進捗通知を受け取りたいクライアントは、リクエストの_metaにprogressTokenを入れます。Managed Agentsのアウトカム評価はこの汎用の通知とは別で、start/ongoing/endの専用イベントとしてストリームに流れます。
{
"jsonrpc": "2.0",
"id": 1,
"method": "some_method",
"params": {
"_meta": {
"progressToken": "abc123"
}
}
}トークンのルールは2つです。文字列か整数でなければならず(MUST)、アクティブな全リクエストの中で一意でなければなりません(MUST)。選び方そのものはクライアントの自由です。
トークンを受け取ったサーバーは、処理の途中で次の通知を任意のタイミングで送れます。
{
"jsonrpc": "2.0",
"method": "notifications/progress",
"params": {
"progressToken": "abc123",
"progress": 50,
"total": 100,
"message": "Reticulating splines..."
}
}| フィールド | 必須 | 内容 |
|---|---|---|
progressToken | 必須必須 | 内容どのリクエストに対する通知かを示す元のトークン |
progress | 必須必須 | 内容ここまでの進捗値 |
total | 必須任意 | 内容全体量。不明なら省略してよい |
message | 必須任意 | 内容人間が読める進捗メッセージ(SHOULD) |
見落としやすいのはprogressの扱いです。totalが不明でも、通知のたびに値を増やさなければなりません(MUST)。クロールのように残り件数が読めない処理でも、処理済み件数や経過した秒数のような後戻りしない値を入れれば条件を満たせます。progressとtotalは浮動小数点でもよく、0.2や0.6のような割合の表現も仕様の図にあります。
総量が分からない処理の通知列は、たとえば次のようになります。totalを省き、progressを処理済み件数にした例です。
{"method":"notifications/progress","params":{"progressToken":"abc123","progress":120,"message":"120件取得"}}
{"method":"notifications/progress","params":{"progressToken":"abc123","progress":340,"message":"340件取得"}}
{"method":"notifications/progress","params":{"progressToken":"abc123","progress":340,"message":"待機中"}}3通目は直前と同じ340なので、progressが増えるというMUSTに反します。待機中に生存を伝えたい場合も、値を増やすか、通知を送らないかのどちらかです。
仕様の流れ図では、通知列の最後がレスポンスです。進捗値が0.2、0.6、1.0と進み、そのあとにメソッドのレスポンスが返ります。通知は応答に先立つ途中経過で、応答のあとに続くものではありません。
進捗を受け取るかどうかを決めるのはクライアントです。リクエストにprogressTokenを入れていなければ、サーバーは通知を送る前提を持てません。トークンを付けるのは、画面やログに進捗を出す用意があるリクエストだけで足ります。
通知を送らなくても仕様違反にならない理由
サーバーに課されているのは「送ると決めたときの振る舞い」だけです。受け取ったトークンについて、サーバーは次の3つを選べます(いずれもMAY)。
- 一切通知を送らない
- 適切と考える任意の頻度で送る
- 全体量が不明なら
totalを省く
進捗通知が処理の正しさに影響しない付加情報だからです。レスポンスさえ正しく返れば契約は満たされます。クライアントも、通知が0件のまま処理が完了する場面を正常系として扱う必要があります。
送ると決めた場合は、参照してよいトークンが「アクティブなリクエストで渡されたもの」かつ「進行中の操作に紐づくもの」に限られます(MUST)。レスポンスを返したあとに遅れて届く通知は、完了後に止まらなければならない(MUST)という規定に反します。クライアントとサーバーの双方がアクティブなトークンを追跡し(SHOULD)、フラッディングを防ぐレート制限を実装する(SHOULD)ことも、実装上の注意として定められています。
長いツールがClaude Codeで打ち切られるとき進捗通知は効くのか
仕様は、進捗通知を受け取ったらタイムアウトの時計をリセットしてよい(MAY)としています。処理が実際に進んでいる印になるからです。一方で、進捗通知の有無にかかわらず上限のタイムアウトは設けるべき(SHOULD)とも定めています。誤動作するクライアントやサーバーの影響を抑えるためです。SDKやミドルウェアには、このタイムアウトをリクエスト単位で設定できるようにすること(SHOULD)も求めています。
通知が届き続ける間はタイムアウトしないという保証はなく、具体的な扱いは各クライアントの実装判断です。Claude Codeの場合は次のように分かれています。
Claude Codeが長いMCPツール呼び出しに使う時間の区切り
自動でバックグラウンド化
2分
メイン会話の呼び出しが2分を超えたとき(v2.1.212以降)
アイドル判定(HTTP等)
5分
HTTP・SSE・WebSocket・claude.aiコネクタ。応答も進捗通知もない時間が続いた場合
アイドル判定(stdio)
30分
v2.1.203より前のstdioは対象外
リクエスト単位の上限
60秒
HTTP・SSE・claude.aiコネクタの最初の応答バイトまで
効き方は、時間の区切りの種類で変わります。
進捗通知が効く区切りと効かない区切り
効く:アイドル判定
応答も進捗通知もない状態が続いたときの打ち切りです。進捗通知が届けば、その分だけ待ちが延びます。窓の長さはCLAUDE_CODE_MCP_TOOL_IDLE_TIMEOUT(ミリ秒)で変えられ、0で無効になります。
効かない:壁時計の上限
.mcp.jsonのサーバーごとのtimeoutとMCP_TOOL_TIMEOUTは、1回の呼び出しに許す総時間の上限です。進捗通知が届いても延びません。1000未満のtimeoutは無視され、MCP_TOOL_TIMEOUT(未設定なら約28時間)に落ちます。
アイドル窓の設定には、押さえておきたい細かい決まりが3つあります。
CLAUDE_CODE_MCP_TOOL_IDLE_TIMEOUTは1000未満だと1秒に引き上げられ、実効のMCP_TOOL_TIMEOUTが上限になります.mcp.jsonのtimeoutが1000以上なら、そのサーバーのアイドル窓は少なくともtimeoutの値まで延びます(v2.1.203以降)- IDEサーバーとSDKのインプロセスサーバーは、アイドル判定の対象外です
2分を超えたMCP呼び出しはバックグラウンドタスクに移り、/tasksの一覧にサーバーが報告した最新の進捗が表示されます。進捗通知がユーザーの目に触れる場所はここです。
バックグラウンド化はサブエージェントからの呼び出しでは起きません。非対話モードでも、CLAUDE_AUTO_BACKGROUND_TASKSが1でなければ起きません。しきい値はCLAUDE_CODE_MCP_AUTO_BACKGROUND_MSで変えられ、0で無効になります。移ったあとも壁時計の上限とアイドル判定は効き続けます。
HTTP・SSE・claude.aiコネクタのサーバーには、これとは別にリクエスト単位のタイマーがあります。60秒、そのサーバーに適用されるツールのタイムアウト、MCP_TIMEOUTのうち最大の値が使われ、最初の応答バイトが届くまでを数えます。stdioとWebSocketにはこのタイマーがありません。
自分のサーバーで呼び出しが途中で打ち切られたときは、次の順で原因を切り分けられます。
打ち切りの原因を切り分ける順序
- 1
エラーが出るまでの時間を測る
2分前後でバックグラウンドへ移るのは正常な動作で、打ち切りではありません。5分(HTTP系)か30分(stdio)で止まるなら、アイドル判定が候補です。
- 2
通知が途切れていないか確認する
MCP Inspectorで
notifications/progressが処理中ずっと届いているか、progressが増え続けているかを見ます。通知の間隔がアイドル窓より長いなら、送る間隔を詰めるか窓を延ばします。 - 3
総時間の上限を確認する
通知が届いているのに止まるなら、サーバーごとの
timeoutかMCP_TOOL_TIMEOUTに達しています。値を上げるか、処理を分割します。
2026-07-28リビジョンへの対応は、Claude Code側の接続方式にも関わります。v2.1.232以降の既定の接続方式(v2ランタイム)は、HTTPサーバーに新しいリビジョンへの対応を問い合わせます。フィーチャーフラグを取得するセッションでは、claude.aiコネクタのサーバーも同じです。Bedrockなどフラグを取得しないセッションでは、v2が既定になるのはv2.1.274以降です。対応していないサーバーは従来のプロトコルで接続され、SSEとWebSocketは常に従来の接続です。stdioも既定では従来の接続で、MCP_PROTOCOL_NEGOTIATIONをautoにすると問い合わせの対象になります。上の時間の区切りは、どちらの接続方式の説明にも当てはまると公式ページに書かれています。
通知を送る頻度とレート制限
仕様はレート制限の具体的な数値を定めていません。間引き方は実装者が決めます。
- 時間ベース: 前回の通知から一定時間(たとえば200ミリ秒)が経つまで次を送らない
- 変化量ベース: 進捗が一定の割合(たとえば5%)以上動くまで送らない
どちらを選ぶにしても、上限だけでなく下限も意識する必要があります。Claude Codeのアイドル判定は進捗通知を生存の合図として扱うので、通知を間引きすぎて窓より長い無通知区間を作ると、処理が進んでいても打ち切られます。「1%ごとに毎回」は多すぎ、「5分に1回」は少なすぎる、という両側の制約です。
MRTR・キャンセル・購読通知との切り分け
進捗通知と混同しやすい仕組みが、2026-07-28リビジョンにはいくつかあります。
2026-07-28リビジョンでは、サーバーからの変更通知(toolsListChanged等)をsubscriptions/listenという専用の長時間ストリームに一本化する変更が入りました。ただしnotifications/progressとnotifications/messageのようにリクエストに紐づく通知は対象外で、元のリクエストの応答ストリーム上に流れ続けます。どのリクエストの進捗かが経路から自明になる設計です。
同じリビジョンで導入されたMulti Round-Trip Requests(MRTR)は、サーバーが追加情報を要求し、クライアントが情報を添えて同じリクエストをやり直すパターンです。リクエストの往復そのものが複数回に分かれます。進捗通知は、1往復のまま処理が続いている間の経過を伝えるだけです。「追加の入力が要るか」がMRTR、「今どこまで進んだか」が進捗通知です。処理を途中で打ち切るのはどちらでもなく、キャンセルの役目です。2026-07-28リビジョンでは、stdioならnotifications/cancelledを送り、Streamable HTTPなら応答ストリームを閉じることが取り消しの合図になります。詳しくはMCPのCancellationとはにあります。
進捗通知を実装する価値があるツールの選び方
進捗通知はキャッシュ仕様やページネーション仕様と性格が違います。この2つは応答の中身の組み立て方を決める仕様で、進捗通知は応答の本体とは別のメッセージで流れる処理中の実況です。対応しなくても壊れないので、優先順位は仕様適合の領域より低くなります。
実装の価値が出るのは、体感時間が長く、途中経過を見せる意味があるツールです。大量データの一括インポートや、外部APIへの連続呼び出しがその例です。逆に数百ミリ秒で終わるツールに通知を付けても、間引きの実装コストに見合いません。
もう1つ、Claude Codeを使う場合は別の判断軸があります。呼び出しが数分に及ぶツールは、通知を送らないと5分(stdioなら30分)のアイドル判定に当たる可能性があります。通知の実装が重いときは、.mcp.jsonのtimeoutを1000以上の長めの値にして、アイドル窓の下限を引き上げる手もあります。
まとめ
数分かかるツールは、通知の間隔をアイドル窓より短く保ち、総時間はtimeout側で決めるのが現実的な組み合わせです。送るならprogressを必ず増やし、完了後は止めます。タイムアウト設定の置き場所は、Claude CodeでMCPサーバーに接続する方法とあわせて確認できます。