MCPのttlMsとcacheScopeでキャッシュ制御を仕様から理解する
MCP 2026-07-28リビジョンで必須化されたttlMsとcacheScopeの仕様を解説します。鮮度計算の式とpublic/privateの使い分け、通知との関係まで示します。
MCPの2026-07-28リビジョンで、ttlMsとcacheScopeという2つのフィールドが必須(MUST)になりました。対象はtools/list・prompts/list・resources/list・resources/read・resources/templates/listの5メソッドが返す結果です。SEP-2549として提案されたこの変更は、クライアントが不要な再取得を減らせるようにするための鮮度ヒントです。MCPサーバーを自作するなら、この2フィールドを正しく埋めることが2026-07-28リビジョン対応の必須項目になります。
なぜMCPにキャッシュの仕様が必要になったのか
MCPはもともとlistChanged通知(一覧が変わったことをサーバーからクライアントへ知らせる仕組み)を持っていました。ですが通知だけでは「変わっていないことをどう確信するか」に答えられません。通知が来ない限り再取得しない実装は、通知の実装漏れや配送失敗があった場合に古いデータを持ち続けるリスクを抱えます。逆に念のため毎回取り直すと、ネットワーク往復と処理のコストが積み上がります。
ttlMsとcacheScopeはこの両極端の中間を埋める仕組みです。サーバーが「この結果はこれくらいの時間なら新鮮とみなしてよい」という鮮度の期限を明示し、クライアントはその期限内は再取得を省略できます。仕組みとしてはHTTPのCache-Control: max-ageとほぼ同じ発想で、Web開発者には馴染みやすい設計です。
対象となるのは、resultType: "complete"(通常の完了結果)を持つ結果だけです。対象外の例が、Multi Round-Trip Requests(追加入力を求めるやり取りの標準パターン)の中間応答(resultType: "input_required")です。キャッシュヒントも付与されません。また、追加入力の応答として送られたリトライ(inputResponsesやrequestStateを含むリクエストの結果)もキャッシュしてはならない(MUST NOT)と定められています。入力に応じて結果が変わる以上、キャッシュキーに入力の中身まで含めない限り安全にキャッシュできないからです。
ttlMsとcacheScopeの仕様
ttlMsはミリ秒単位の整数で、「受信からこの時間だけ結果を新鮮とみなしてよい」という鮮度期限です。仕様が定める挙動は次のとおりです。
| 値 | クライアントの扱い |
|---|---|
0 | クライアントの扱い即座に古いとみなす。必要になるたびに再取得してよい |
| 正の整数 | クライアントの扱い受信からその時間(ミリ秒)は新鮮とみなす |
| 未指定 | クライアントの扱い0とみなす。古いサーバー実装でのみ起こりうる |
| 負の値 | クライアントの扱い無視して0として扱う |
鮮度の判定式は仕様に明記されています。
now < t_received + ttlMst_receivedは応答を受け取った時刻です。この式が真である間は、結果を新鮮として使い回します。
ttlMsが300000(5分)のtools/list結果は、次のように使われます。仕様のシーケンス図と同じ流れです。
ttlMs: 300000 の tools/list 結果の一生
- 受信時キャッシュに保存
tools/listの応答にttlMs: 300000が付いて届きます。クライアントはt_receivedを記録し、5分間は新鮮とみなします。 - 2分後キャッシュを使う
ツール一覧が必要になっても、
now < t_received + ttlMsが真なので再取得しません。 - さらに3分後(受信から5分)再取得する
次にツール一覧が必要になった時点で、期限切れのため
tools/listを取り直します。再取得が走るのは必要になった瞬間で、期限の到来そのものではありません。 - 期限前の変更通知で即無効化
TTLが残っていても
notifications/tools/list_changedを受けたら、その場で古い扱いになり再取得します。
ここで注意が必要なのは、ttlMsはポーリング間隔ではないという点です。仕様は「TTLを自動的なバックグラウンド再取得のトリガーとして扱うべきではない(SHOULD NOT)」と明記しています。あくまで「必要になったときに新鮮かどうかを確認するための鮮度ヒント」という位置づけです。どうしてもポーリングする実装にするなら、ジッター(ランダムなばらつき)とバックオフを必ず入れることが求められます。
cacheScopeは"public"か"private"のどちらかを取り、「誰がこのキャッシュを再利用してよいか」を制御します。
public と private の分かれ目
public
ユーザー固有のデータを含まない結果です。クライアント・共有ゲートウェイ・キャッシュプロキシのどれもが、任意のユーザーに再利用できます。全ユーザーに同一の内容が返るtools/list・prompts/list・リソーステンプレート一覧が対象です。
private
ユーザー固有のデータを含み、同じ認可コンテキストの中でだけ再利用できます。異なるアクセストークンの間でキャッシュを共有してはならない(MUST NOT)ので、トークンが違えば別のキャッシュを持ちます。認証ユーザーごとに内容が変わるresources/readや、ユーザーで絞り込まれた一覧が対象です。
仕様のレスポンス例では、tools/listとresources/listがttlMs: 300000とcacheScope: "public"です。ファイルを返すresources/readはttlMs: 60000でprivateです。変わりにくい一覧は5分、読み取り内容は1分という刻みで、サーバーごとの目安を決める出発点になります。
ページネーションと組み合わせる場合、各ページは独立してキャッシュされる結果として扱います。ページごとに異なるttlMsを設定してよい一方、同一の一覧リクエストに対する全ページのcacheScopeは揃える(MUST)必要があります。最初のページがprivateなら、2ページ目以降もすべてprivateでなければなりません。ページ全体の整合性は保証されません。ページを取る間にデータが変われば重複や欠落が見えることがあり、一貫したスナップショットが要るクライアントは、カーソルなしの先頭から取り直します(SHOULD)。以前有効だったカーソルがエラーになったときも、キャッシュ済みのページをすべて捨てて先頭から取り直します。カーソルによる分割取得の仕組み自体はMCPのページネーション仕様で扱っているので、あわせて参照してください。
通知・ページネーションとの組み合わせで見落としやすい点
ttlMsとlistChanged通知は補完関係にあり、両方を実装してもよい設計です。長時間かかるツール呼び出しの進捗を伝えるprogress通知とは、目的が異なります。混同しないでください。進捗通知はMCPの進捗通知を実装するパターンにあります。仕様が想定する組み合わせは2パターンあります。
listChanged: trueを広告せずttlMsだけを返す。クライアントはTTLベースの鮮度判定だけに頼るlistChanged: trueを広告しつつttlMsも返す。TTL期限内の不要な再取得を避けつつ、通知を無効化(invalidate)の合図に使う
後者の場合、通知を受けた時点で、TTLの残り時間に関係なく古い扱いになります。
キャッシュキーの決め方にも注意が必要です。キャッシュされた応答は「メソッド名 + 結果に影響するパラメータ」で識別されます。resources/readならuri、ページネーションされた一覧リクエストならcursorが該当します。メソッドやパラメータが異なるリクエストに対して、以前のキャッシュ結果を流用してはなりません(MUST NOT)。
セキュリティ上の注意点もあります。cacheScope: "public"は「認証済みエンドポイントから返ってきた結果であっても、異なるアクセストークンを持つ呼び出し元の間で共有されうる」ことを意味します。サーバー実装者は、cacheScopeの値がそのプリミティブの公開範囲を正しく反映しているか確認する必要があります。そのうえで、アクセス制御をcacheScopeだけに頼ってはならないことも明記しています。cacheScopeはあくまでキャッシュの共有可否を示すヒントであり、認可の仕組みそのものではありません。
tools/listの順序もキャッシュヒット率に影響する
ttlMsとcacheScopeと同じ2026-07-28リビジョンで、もう1つキャッシュに関わる推奨事項が追加されました。「サーバーはtools/listからツールを決定的な順序(deterministic order)で返すべき(SHOULD)」というものです。仕様が挙げる目的は2つで、クライアント側のキャッシュを機能させることと、LLMのプロンプトキャッシュのヒット率を改善することです。
tools/listの決定的な順序とは、ツール集合が変わっていないなら毎回同じ並びで返すことです。ツール一覧がモデルのコンテキストに含まれる構成では、サーバー側の並びの安定がプロンプトキャッシュのヒット率に効くと仕様は述べています。Claude Codeのようにサーバーのツールをモデルへ渡すクライアントも、この構成にあたりえます。ttlMs・cacheScopeがMCP層のキャッシュを扱うのに対し、順序はその外側のキャッシュにまで届く推奨事項です。
公式ページ間で食い違う点と、通知の側の変更
対象メソッドの数え方は、公式ページで揃っていません。変更履歴(changelog)の項目5は、必須化の対象をtools/listなど5つのメソッドとしています。server/discoverは入っていません。キャッシュの仕様ページ(Cacheable Results)は、これにserver/discoverを加えた6つを挙げます。server/discoverの結果にも付ける前提で実装するなら、仕様ページ側の6つに合わせる形になります。
もう1つ、同じリビジョンで通知の受け取り方が変わっています。変更履歴の項目4では、HTTPのGETエンドポイントとresources/subscribe・resources/unsubscribeがsubscriptions/listenに置き換わりました。クライアントは必要な型を選んで購読します。型はtoolsListChanged・promptsListChanged・resourcesListChanged・resourceSubscriptionsです(SEP-2575)。通知による無効化は、この購読で該当の型を選んだクライアントにだけ届きます。
cacheScopeの判断を誤るとどうなるか
起こりうる誤りは、「動作確認のときにたまたま自分のトークンでしか叩いていないから」という理由で、本来privateが適切な結果をpublicにしてしまうことです。個人アカウント向けにフィルタされたresources/listや、テナントごとに内容が異なるtools/listは典型的なprivate対象です。ここをpublicと誤指定すると、共有キャッシュを経由して別の認可コンテキストの利用者に他人のデータが渡る事故につながります。MCPセキュリティガイドで扱っている「サーバーへの信頼確認」とは別の層で発生する問題であり、サーバー実装者自身が防ぐしかない部分です。
もう1つの落とし穴は、ttlMsを「サーバー側のデータが変わらない保証期間」と誤解することです。仕様は明確に「TTLは鮮度のヒントであって保証ではない(not a guarantee)」と述べています。サーバーはTTLが切れる前にデータを変更してよく、TTLはあくまで「クライアントがどれくらいの間、再取得を合理的に省略できるか」を示すだけです。長すぎるTTLを設定すると、変更の反映が遅れて見える形で表面化します。逆に短すぎるTTLは、キャッシュを実質的に無効化してしまい、仕様を実装した意味が薄れます。データの変わりやすさに応じてTTLを個別に調整するのが実務上の落としどころです。
まとめ
迷ったときはprivateで出し、全ユーザーで同一と分かった一覧だけpublicに切り替える進め方があります。server/discoverも仕様ページに合わせて付けておけば、changelogの数え方に関係なく対応漏れになりません。
Claude CodeからMCPに接続する構成では、この2フィールドを含むtools/list応答をClaude Code側が検証エラーで丸ごと拒否する不具合も報告されています。サーバーのツールが0件になる症状です。回避策はtools/list応答がttlMs/cacheScopeで拒否される問題にあります。