メインコンテンツまでスキップ

コンテキスト肥大問題と対策

MCP サーバーを増やすほど、ツール定義と結果が LLM のコンテキストを消費します。この問題を「MCP is dead. Long live the CLI」と関連記事の議論を参照しながら整理し、サーバー側・クライアント側の対策を解説します。

1. このドキュメントで扱うこと​

  • 何がコンテキストを消費するか
  • 問題の大きさを測る
  • MCP is dead の主張と、その後の更新
  • サーバー側の対策
  • クライアント側の対策 (段階的探索、サーバーの動的接続、プログラム的ツール呼び出し)
  • キャッシュとの兼ね合い、閾値の決め方、測定の習慣

何がコンテキストを消費するか​

MCP がコンテキストを圧迫する経路は 2 つあります。

  1. ツール定義の常時ロード
    • 多くのクライアントは、接続したすべてのサーバーのツール定義 (name / description / inputSchema) を、会話の開始時にモデルへ渡します。1 回も使わないツールの定義も毎回含まれます
  2. ツール結果の中間データ
    • モデルがツールを直接呼ぶと、結果は必ずモデルのコンテキストを通ります。「Google Drive の議事録を Salesforce に添付する」場合、議事録の全文が取得時と書き込み時の 2 回、モデルを通過します。大きなデータではコンテキストの上限を超え、コピーの誤りも増えます

どちらも、MCP の仕様ではなく「全部を先に読み込み、中間結果もすべてモデルに通す」という素朴なクライアント実装に起因する問題です。

問題の大きさを測る​

Quandri のエンジニアリングブログでは、実際の環境に接続した MCP サーバーのツール定義 (名前、説明、パラメーターの JSON スキーマ) を抽出して計測しています。

MCP サーバーツール数推定トークン数
Linear42約 12,807
Notion14約 4,039
Slack12約 3,792
Postgres9約 438
合計77約 21,077

4 サーバーの定義だけで、200K コンテキストの約 10.5%、128K コンテキストの約 16.5% を占めています。Linear 単体で約 12,800 トークンあり、実際には get_issue と save_issue しか使わなくても 42 個の定義が常駐します。

なお、この値は 1 トークン約 4 文字という概算で、サーバー全体の値はサンプルの平均からの推定です。自分の環境では、実際に接続して測り直します。

MCP is dead の主張と、その後の更新​

Eric Holmes の「MCP is dead. Long live the CLI」(2026-02-28) は、コンテキストの観点では次のように主張しています。

  • LLM は man ページや GitHub のシェルスクリプトで CLI を学習済みで、特別なプロトコルや定義の常駐が不要
  • CLI ならパイプ、jq、grep で結果を絞り込める。MCP では巨大な結果をそのままコンテキストに入れるか、サーバー側に絞り込みを実装するしかない

たとえば Terraform のプランの変更件数は、次のように中間データをコンテキストに入れずに求められます。

terraform show -json plan.out | jq '[.resource_changes[] | select(.change.actions[0] == "no-op" | not)] | length'

一方で Quandri のブログは、計測後に Claude Code が Tool Search (遅延ロード) を導入し、MCP のツール定義をオンデマンドで読み込むことでコンテキスト使用量を 85% 以上削減すると追記しています。現在の Claude Code では、セッション開始時にツール名とサーバーの instructions だけを読み込み、定義は必要になってから取得します。つまり、定義の肥大は「全件を先読みする素朴な実装」の問題であり、クライアントの工夫で大幅に軽減できます。

残る論点は 2 つです。ツール結果の中間データは、サーバーとクライアントの設計で対処しないと解消されません。また、CLI と Skills の方が軽量に済む場面があること自体は変わりません (詳しくは MCPとCLI・Skillsの比較と使い分け を参照)。

サーバー側の対策​

サーバー作成者にできる対策です。

  • ツールの数を絞る
    • API の 1 エンドポイントごとにツールを作らず、ユーザーの目的単位にまとめる
  • description を短く的確にする
    • 「何をするか」と「いつ使うか」を簡潔に書く。Claude Code は、ツールの description とサーバーの instructions を既定で 2,048 文字で切り詰める。重要な情報は先頭に置く
  • 絞り込み・ページネーションの引数を用意する
    • 検索条件、フィールドの指定、limit と cursor を持たせ、結果が必要以上に大きくならないようにする
  • outputSchema を定義する
    • 構造化された結果を宣言すると、クライアントが Code Mode で型付きの API を生成でき、モデルは必要なフィールドだけを扱える
  • ツールを決定的な順序で返す
    • 2026-07-28 版では、リスト結果が決定的な順序を持つことで、再接続しても上流のプロンプトキャッシュが安定する

設計の詳細は ツール設計パターンとアンチパターン で扱います。

クライアント側の対策 1: 段階的探索​

段階的探索 (progressive discovery) は、ツール定義がコンテキストに入るタイミングを制御します。

  • ホストは通常どおり tools/list で定義を取得するが、モデルのコンテキストには入れない
  • 代わりに search_tools のような軽量なメタツールをモデルに渡す
  • 必要になった定義だけをコンテキストに読み込む

MCP 公式ドキュメントの図では、全件を先読みする場合の約 150,000 トークンが、必要なものだけ読み込むと約 2,000 トークンになる例が示されています。

実装の例として、3 層構成があります。

層役割
カタログsearch_tools が自然言語のクエリを受け取り、ツール名と 1 行の説明だけを返す
詳細確認候補のツールについて、入力スキーマなどの完全な定義を取得する
実行定義を把握したうえでツールを呼ぶ

検索戦略は次から選びます。

戦略特徴
KeywordBM25 や正規表現。単純で、ツール名と説明が明確なら有効
Embedding説明文のベクトル類似度。同義語や意味の近さに強い
サブエージェント小さく速いモデルがツールを選ぶ。精度は高いが、コストが相対的に高い
ハイブリッド複数の順位を組み合わせる、またはクエリで戦略を切り替える

OpenAI や Anthropic は、プラットフォーム側にツール検索の機能を用意しています。利用できるなら、独自実装よりもそちらを優先できます。独自に作るのは、プロバイダーが対応していない場合や、ドメイン固有のランキングやアクセス制御によるフィルタリングが必要な場合です。

実装では、次の点に注意します。

  • 詳細度を選べるようにする (名前のみ、名前と説明、完全なスキーマ)
  • 取得した定義はホスト側でメモ化し、再投入のたびに tools/list を呼ばない
  • notifications/tools/list_changed を受けたら検索カタログを再構築する
  • サーバーごとにツールをまとめて提示する

クライアント側の対策 2: サーバーの動的接続​

段階的探索は、ツール単位からサーバー単位にも広げられます。起動時にすべてのサーバーに接続せず、次のようにします。

  1. 利用可能なサーバーとその概要のレジストリを持つ
  2. モデルが必要だと判断したときだけ、そのサーバーに接続する
  3. 不要になったサーバーは切断し、コンテキストを解放する

利用者の意図が事前にわからない汎用的なエージェントに向いています。常時接続するサーバーは最小限にし、残りは必要なときに接続します。Skills と組み合わせれば、Skill が必要な MCP サーバーを宣言し、その Skill が呼ばれたときにだけホストが接続する構成にできます。

クライアント側の対策 3: プログラム的ツール呼び出し (Code Mode)​

プログラム的ツール呼び出しは、ツールの呼び出し方を変えます。モデルがツールを直接呼ぶ代わりに、ツールを呼ぶコードを書き、サンドボックス内で実行します。複数のツールの連鎖や、結果の絞り込み・重複排除はサンドボックス内で行われ、モデルには最終結果だけが返ります。

MCP 公式ドキュメントの図では、中間結果をモデルに通す約 100,000 トークン以上が、約 200 トークンのスクリプトと約 15 トークンの要約で済む例が示されています。Anthropic のエンジニアリングブログでも、ツールをコードの API として提示することで、トークン使用量が 150,000 から 2,000 になった (98.7% の削減) と報告されています。Cloudflare は同様の手法を「Code Mode」と呼んでいます。

仕組みは次の 3 ステップです。

  1. ホストが MCP のツールスキーマから、サンドボックス内で使える型付きの API を生成する (outputSchema があれば戻り値の型も正確になる)
  2. モデルがその API を呼ぶスクリプトを書く
  3. サンドボックスがスクリプトを実行し、関数呼び出しをホストが仲介して MCP サーバーへ中継する。モデルには console.log の出力や最終的な戻り値だけが返る

Code Mode はコード実行の面を持ち込むため、安全性への配慮が必須です。

  • サンドボックスにネットワークアクセスを与えず、外部との通信はホストの仲介だけにする
  • 認可トークンや認証情報はホストが保持し、生成されたコードには渡さない
  • スクリプトの承認は、実行時の個々のツール呼び出しの承認を意味しない。仲介役は呼び出しごとに許可の範囲内かを評価する
  • あるサーバーのツール結果は、別のサーバーにとって信頼できない入力として扱う
  • タイムアウトとメモリの上限を設け、サンドボックスの出力は検証・切り詰めてからモデルに返す

ツールのエラーは isError: true の成功レスポンスとして届くため、生成するラッパーで例外に変換し、モデルのコードが try / catch を使えるようにします。

キャッシュとの兼ね合い​

MCP 側のキャッシュとプロンプトキャッシュは別物で、両方を考慮します。

  • MCP のキャッシュ
    • tools/list や server/discover、resources/read などの結果には、ttlMs (新鮮と見なせるミリ秒数) と cacheScope (public か private) が付く。クライアントはこれに従い、list_changed 通知を受けたら TTL の期限前でも古いものとして扱う。TTL をポーリング間隔として使わず、必要になったときに新鮮さを確認して再取得する
  • プロンプトキャッシュ
    • 多くのプロバイダーは、tools 配列を含むプロンプトの先頭部分をキャッシュする。会話の途中でツール定義を追加・削除すると、このキャッシュが無効になり、削ったトークンより高くつくことがある

プロンプトキャッシュを保つには、次のようにします。

  • 新しく見つけた定義は、tools 配列を並べ替えずにキャッシュの切れ目の後ろへ追加する
  • すべての呼び出しを、変わらない 1 つの call_tool({name, args}) メタツール経由にして、配列自体を変えない
  • サーバーの切断は、ターンごとではなく会話の区切りの操作として扱う

閾値の決め方​

ツール定義が少ないうちは、全件をロードして構いません。目安として、定義がコンテキストの 1〜5% を超えたら段階的探索へ切り替えます。MCP のクライアントベストプラクティスも、コンテキスト比のしきい値を持つことを推奨しています。Claude Code では環境変数 ENABLE_TOOL_SEARCH=auto:5 で 5% を閾値にでき、未指定なら MCP ツールはすべて遅延ロードされます。

測定の習慣​

「重そうだから対策する」ではなく、測って判断します。

  • 接続する前に、tools/list の結果のトークン数を数える
  • 同じプロンプトで、段階的探索の有無や検索戦略を変え、実際に正しいツールが選ばれるかを確認する
  • 結果のサイズを確認する。Claude Code は MCP ツールの出力が 10,000 トークンを超えると警告を出し、既定では 25,000 トークンを上限にする (超えた結果はファイルに保存され、会話にはそのパスが入る)

2. 対策の比較​

対策制御する対象トレードオフ
ツール数・description の削減定義のサイズ設計の手間。機能を絞りすぎると使えなくなる
段階的探索 (Tool Search)定義がコンテキストに入るタイミング検索の精度に依存。プロンプトキャッシュの無効化に注意
サーバーの動的接続接続するサーバーの数接続・認可のレイテンシ
Code Mode中間結果がコンテキストを通ることサンドボックスの実装と安全性の担保が必要
絞り込み引数・ページネーションツール結果のサイズツール設計が複雑になる
Skills + CLI必要な手順だけをオンデマンドでロードCLI が存在し、エージェントにシェルがあることが前提

3. 開発者がつまずきやすいポイント​

段階的探索を入れたのにツールが選ばれない​

検索の精度が低いと、モデルが必要なツールを見つけられません。ツール名と description に、利用者がやりたいことの言葉 (動詞と対象) を含めます。Claude Code の Tool Search のように、サーバーの instructions に「どんな種類のタスクを扱うか」「いつ検索すべきか」を書くのも効果的です。毎ターン必要な少数のツールは、alwaysLoad のように遅延させない指定を使います。

ツール配列の変更でプロンプトキャッシュが切れる​

段階的探索や動的接続で、会話の途中にツール定義を足したり引いたりすると、プロンプトキャッシュが無効になり、節約したつもりのトークンより高くつくことがあります。ツール定義の増減は、会話の区切りや、キャッシュの切れ目の後ろへの追加に限ります。

古い list_changed を無視する​

ツールの一覧をキャッシュしていても、list_changed を受けたら古いものとして扱い、再取得します。通知は最善努力で、欠落もあり得るため、ttlMs によるポーリングを併用します。

絞り込み引数がないツールが巨大な結果を返す​

ページネーションも絞り込みもないツールは、1 回の呼び出しで数万トークンを返せます。クライアント側で切り詰められても、必要な情報が失われます。サーバー側で limit、フィールドの指定、検索条件を用意します。

Code Mode のサンドボックスを過信する​

生成コードの実行には隔離が不可欠です。コードに認証情報を渡さない、ネットワークを遮断する、呼び出しごとに承認ポリシーを適用する、をすべて満たさないと、プロンプトインジェクションの経路が増えます。コンテキストの節約を優先して、安全性の設計を省かないようにします。

CLI を使えば解決すると考える​

CLI は定義を常駐させず、パイプで絞り込める強みがありますが、エージェントにシェルがあり、成熟した CLI が存在し、認証済みの環境であることが前提です。この前提がない場面では、MCP を段階的探索や Code Mode と組み合わせて使います。

測らずに全サーバーを接続する​

サーバーを 1 つ足すごとに定義が増え、モデルの選択精度も下がり得ます。使わないサーバーは接続せず、必要になったときだけ有効にします。

4. コード例​

ツール定義のサイズを測る​

tools/list の結果を保存し、文字数から概算します (Quandri と同じ 1 トークン約 4 文字の目安)。実際のトークン数は、利用するモデルのトークナイザーで確認します。

# tools-list.json は tools/list のレスポンス全体を保存したもの
chars=$(jq -c '.result.tools[]' tools-list.json | wc -c)
tools=$(jq '.result.tools | length' tools-list.json)
echo "tools=${tools} chars=${chars} approx_tokens=$((chars / 4))"

# 200K コンテキストに対する割合 (%)
echo "scale=1; ${chars} / 4 * 100 / 200000" | bc

段階的探索の 3 層​

MCP 公式ドキュメントの例です。モデルは軽量な検索から始め、必要なツールの完全な定義だけを取得します。

// 層 1: カタログ。名前と 1 行の説明だけを返す
search_tools({ query: "update salesforce record" });
// → [
// { name: "salesforce_updateRecord", description: "Update fields on a Salesforce object" },
// { name: "salesforce_upsertRecord", description: "Insert or update based on external ID" }
// ]

// 層 2: 詳細確認。候補のツールだけ完全な定義を取得する
get_tool_details({ name: "salesforce_updateRecord" });

// 層 3: 実行。定義を把握したうえで呼び出す
salesforce_updateRecord({ objectType: "Lead", recordId: "00Q...", data: { Status: "Working" } });

Code Mode: 絞り込みをサンドボックスで行う​

ホストが outputSchema から生成した型付きの関数を、モデルが書いたスクリプトが呼びます。ログ数千件はサンドボックス内で重複排除され、モデルに返るのは 1 行の要約だけです。

// モデルが生成し、サンドボックスで実行されるコード
const logs = await logging_getLogs({
level: "error",
since: Date.now() - 3600000,
});

// 絞り込みと重複排除は、モデルのコンテキストではなくサンドボックスで行う
const uniqueErrors = new Map<string, LogEntry>();
for (const log of logs.entries) {
if (!uniqueErrors.has(log.message)) uniqueErrors.set(log.message, log);
}

for (const [message, log] of uniqueErrors) {
await ticketing_createIssue({
title: `Error: ${message}`,
body: `First seen: ${log.timestamp}`,
priority: "high",
});
}

// モデルに返るのはこの 1 行だけ
console.log(`Filed ${uniqueErrors.size} tickets from ${logs.entries.length} error logs`);

サーバー側: 絞り込み引数と outputSchema​

結果の大きさを呼び出し側が制御できるよう、limit / cursor / fields を持たせ、outputSchema で構造を宣言します。

{
"name": "issues_search",
"description": "Search issues by status and assignee. Returns at most `limit` items.",
"inputSchema": {
"type": "object",
"properties": {
"status": { "type": "string", "enum": ["open", "closed"] },
"assignee": { "type": "string" },
"fields": {
"type": "array",
"items": { "type": "string", "enum": ["id", "title", "status", "url"] },
"description": "Fields to include. Defaults to id and title."
},
"limit": { "type": "integer", "minimum": 1, "maximum": 50, "default": 20 },
"cursor": { "type": "string" }
}
},
"outputSchema": {
"type": "object",
"properties": {
"items": { "type": "array", "items": { "type": "object" } },
"nextCursor": { "type": "string" }
},
"required": ["items"]
}
}

プロンプトキャッシュを保つ call_tool メタツール​

ツールの追加・削除で tools 配列が変わらないよう、実行を 1 つの安定したメタツールにまとめる構成です。

{
"name": "call_tool",
"description": "Call a tool found with search_tools.",
"inputSchema": {
"type": "object",
"properties": {
"name": { "type": "string" },
"args": { "type": "object" }
},
"required": ["name"]
}
}

Claude Code での設定​

# 定義がコンテキストの 5% を超えたら遅延ロードに切り替える
ENABLE_TOOL_SEARCH=auto:5 claude

# 遅延ロードを無効にして、すべてのツールを先読みする
ENABLE_TOOL_SEARCH=false claude

# ツールの出力トークン数の上限を変更する (既定は 25,000)
MAX_MCP_OUTPUT_TOKENS=50000 claude

毎ターン使うサーバーは、.mcp.json で遅延させないようにできます。

{
"mcpServers": {
"core": {
"type": "http",
"url": "https://example.com/mcp",
"alwaysLoad": true
}
}
}

5. まとめ​

  • コンテキストを消費するのは、常駐するツール定義と、モデルを通過するツール結果の中間データの 2 つ
  • Quandri の計測では、4 サーバー・77 ツールの定義だけで約 21,000 トークン (200K の約 10.5%) だった
  • 定義の肥大は、全件を先読みする素朴な実装の問題で、Tool Search のような遅延ロードで大幅に軽減できる
  • サーバー側は、ツールを絞り、description を短くし、絞り込み引数と outputSchema を用意する
  • クライアント側は、段階的探索、サーバーの動的接続、Code Mode を、ツール定義と結果のどちらを減らしたいかで使い分ける
  • ツール配列を途中で変えるとプロンプトキャッシュが切れる。変更は会話の区切りか、キャッシュの切れ目の後ろに限る
  • 定義が 1〜5% を超えたら段階的探索へ切り替え、導入前後を測定で確かめる

関連ドキュメント​

参考資料​