Claude Media
MCPホストをスケールさせる設計パターン — 数千のツールをどう捌くか

MCPホストをスケールさせる設計パターン — 数千のツールをどう捌くか

MCPホストが抱えるサーバー・ツールが増えたときのスケール手法。段階的なツール開示とコード実行による呼び出しの2パターンを解説します。

ツール定義と呼び出し結果、どちらがコンテキストを圧迫しているか

MCPホスト(Host・Client・Serverという役割分担はMCPアーキテクチャの解説を参照)が接続するサーバーとツールが増えると、素朴な実装は破綻します。すべてのツール定義を会話の最初にまとめてモデルへ渡す方式では、サーバーが数十、ツールが数百に及んだ時点で、定義だけでコンテキストウィンドウの大部分を使い切りかねません。MCPのクライアント向けベストプラクティスは、この状態を「モデルがユーザーのメッセージを読む前に」起きる消費として説明しています。

膨らむ場所は2つあります。会話の冒頭に積まれるツール定義と、ツールを連鎖させたときに途中の結果がモデルを通過する分です。前者に効くのがProgressive Discovery(段階的なツール開示)、後者に効くのがProgrammatic Tool Calling(コード実行によるツール呼び出し)です。前者は「いつツール定義をコンテキストに入れるか」を、後者は「ツールをどう呼び出すか」を制御します。

どちらから手を付けるかは、症状で切り分けられます。

症状膨らんでいる場所最初に効く手
1ターン目からトークンが大きい。ツールの選び間違いが増える(目安は30〜50個超)膨らんでいる場所ツール定義最初に効く手段階的なツール開示
読む→変換する→書く、のような連鎖で、途中結果が毎回モデルを通る膨らんでいる場所ツール結果最初に効く手コード実行による呼び出し
両方に当てはまる膨らんでいる場所定義と結果最初に効く手2つの組み合わせ
ツールが10個前後で、定義がコンテキストの数%に収まる膨らんでいる場所問題なし最初に効く手全件読み込みのまま

最後の行の目安は、Agent SDKのTool Searchの説明にあります。ツールが約10個未満で定義がコンテキストに余裕で収まるなら、最初に全部読み込むほうが速いという記述です。同じ説明に、50個のツールで10〜20Kトークンを消費しうること、同時に読み込むツールが30〜50個を超えると選択精度が落ちることも書かれています。定義の量だけでなく、選び間違いの増加もスケールの症状です。

数字

MCP公式の図に出てくる桁の違い

  • 全ツール定義を先に読み込む

    約150,000トークン

    定義だけでこの量

  • 段階的に開示する

    約2,000トークン

    タスクに要る分だけ

  • 途中結果をすべてモデルに通す

    100K+トークン

    直接呼び出しの連鎖

  • コードで合成する

    約200+約15トークン

    スクリプトと要約出力

図中の例示値です。実測値ではなく、方式による桁の差を示すための数字です

段階的なツール開示(Progressive Discovery)とは何か

Progressive Discoveryは、接続済みサーバーの全ツール定義を最初から渡さず、モデルが必要とした時点でだけ読み込む方式です。ホストはtools/listでこれまでどおり定義を取得しますが、モデルのコンテキストへの注入は遅らせます。代わりに、軽量なsearch_toolsメタツールをモデルに渡します。

カタログ・検査・実行の3層

実装の一例が、検索ベースの3層構造です。

手順

3層でツールを絞り込む

  1. 1

    カタログ層

    search_toolsのような検索メタツールを1つだけ渡します。自然文のクエリを受け取り、候補ツールの名前と一行説明だけを返します。

  2. 2

    検査層

    モデルが候補を1つに絞ったら、そのツールだけの完全な定義(入力スキーマ・出力スキーマ・説明)をget_tool_detailsのような形で取得します。

  3. 3

    実行層

    完全な定義を得たツールだけを呼び出します。

カタログ層の呼び出しと戻り値は、たとえば次の形です。

// モデルは軽量な検索ツールを呼ぶ
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" }
  ]

候補が出たあとにget_tool_details({ name: "salesforce_updateRecord" })で1件ぶんのスキーマだけを取り、実行層に進みます。モデルが数個の関連ツールだけを見ればよくなるため、トークン削減に加えて選択精度の改善も見込めます。

検索層の選択肢

カタログ層の検索は、4つの戦略から選べます。

  • キーワード型: BM25や正規表現で照合します。実装が単純で、ツール名や説明文が具体的なら十分な精度が出ます。
  • 埋め込み型: ツール説明のベクトル類似度で探します。同義語や意味的な近さを拾えます。
  • サブエージェント型: Claude HaikuやGemini Flashのような小型で高速なモデルにツール選定を任せます。うまく機能しやすい反面、他の2つよりコストがかさみます。
  • ハイブリッド型: キーワードと埋め込みの順位を合算したり、用途やクエリで戦略を切り替えたりします。

3層の原則は、どの戦略を選んでも変わりません。AnthropicやOpenAIのようにプラットフォーム側がツール検索を提供している場合は、まずそちらを検討するのが近道です。自前実装が要るのは、プラットフォームが対応していないときと、ドメイン固有のランキングやアクセス制御が必要なときです。

切り替えの基準は2つの文書で違う

全件読み込みで足りるうちは、段階開示を入れる必要はありません。MCPのベストプラクティスは、ツール定義がコンテキストウィンドウの一定割合を超えた時点で切り替える設計を勧めています。割合の例は1〜5%です。

一方、Agent SDKのTool Searchは別の既定を持っています。両者を並べると、基準の置き方が違うことが分かります。

くらべる

切り替えしきい値の置き方

ホストを自作する側

MCPのクライアント向け指針

しきい値を自分で決め、定義の総量を測って動的に切り替える設計にします。例として挙がっているのは、コンテキストウィンドウの1〜5%です。

SDKを使う側

Agent SDKのauto

ENABLE_TOOL_SEARCH=autoは、遅延できる定義がモデルのコンテキストウィンドウの10%に達すると検索を有効にします。auto:5のように割合を変えられます。

自作ホストでは、数値そのものより「測って切り替える」仕組みを先に入れておくと、モデルやコンテキスト長が変わっても閾値だけ調整すれば済みます。

サーバー単位でも同じ原則が働く

段階開示は個々のツールだけでなく、サーバー全体にも適用できます。起動時に全サーバーへ接続せず、ホストがサーバーの一覧と概要をレジストリとして持ちます。モデルが必要と判断したサーバーにだけ接続し、不要になったら切断します。

MCP側の例では、モデルがsearch_available_serversで候補を探し、enable_serverで接続を頼みます。ホストはserver/discoverで対応バージョンと機能を取得し、続けてtools/listで定義を受け取ります。終わったらdisable_serverで切断し、コンテキストを解放します。

ユーザーの意図が事前に分からない汎用エージェントで特に効果があります。常時接続するサーバーを最小限にし、残りは必要になってから繋ぎます。Agent Skillsと組み合わせる設計もあります。スキルファイルに必要なMCPサーバーを宣言させておき、そのスキルが呼ばれたときだけ接続する連携です。

実装時に押さえる4点

ガイドライン理由
詳細度を複数用意する理由名前のみ・名前と説明・完全なスキーマから、モデルが必要な粒度を選べるようにする
取得済み定義をキャッシュする理由ホスト側でメモ化し、再注入のたびにtools/listを呼び直さない。モデルのコンテキストにある内容とは別の話
list_changedで再インデックス理由サーバーがnotifications/tools/list_changedを送ってきたら、検索カタログを作り直す
ツールをサーバー単位でグループ化する理由モデルが関連するツール群として推論できるよう、提供元サーバーごとに整理して示す

tools/listなどの一覧結果と、server/discover・resources/readの結果には、ttlMsとcacheScopeのヒントが付きます。これに従ってキャッシュしますが、list_changedが届いたら、TTLが残っていてもキャッシュ済みの一覧は古いものとして扱います。

プロンプトキャッシュとの相性

多くのプロバイダーは、tools配列を含むプロンプトの先頭部分をキャッシュします。会話の途中でツール定義を足したり外したりすると、このキャッシュが無効になります。外した定義より多くのトークンを、キャッシュミスで失うこともあります。

避ける方法は2つあります。新しく見つけた定義を、tools配列を並べ替えずにキャッシュの区切りより後ろへ追記します。または、すべての呼び出しをcall_tool({name, args})のような安定したメタツール1本に通し、配列自体を変えません。サーバーの切断も、毎ターンではなく会話の区切りで行う操作として扱います。

コードでツールを合成する(Programmatic Tool Calling)とは何か

本記事は、MCPのクライアント向けベストプラクティスに沿ったホスト実装設計を扱います。同じProgrammatic Tool Callingを「数百ツールを抱えたMCPホスト」の現実解として、Anthropicエンジニアリングブログの削減事例から読み解いた記事にMCPでコード実行があります。実装の詳細を先に知りたい場合はそちらも参照してください。

直接的なツール呼び出しは、1回ごとにモデルとのラウンドトリップが発生し、結果全体がモデルのコンテキストに戻ります。Programmatic Tool Calling(Code Modeとも呼ばれる)は、モデルにツールを直接呼ばせません。ツールを呼ぶコードを書かせ、そのコードをサンドボックスで実行します。モデルに返るのは、コードが出力した要約だけです。

手順

コード実行の3ステップ

  1. 1

    MCPスキーマから型付きAPIを生成する

    ホストが各サーバーのツール定義を読み、引数とoutputSchemaから型付きの関数を作ります。outputSchemaがあれば、戻り値の型まで正確に生成できます。

  2. 2

    モデルがそのAPIを呼ぶスクリプトを書く

    「直近1時間のエラーログを集めて重複を除き、種類ごとにチケットを作る」なら、ログ1件ずつをモデルに読ませず、サンドボックス内でフィルタします。

  3. 3

    サンドボックスが実行する

    関数呼び出しはホスト経由でMCPサーバーへ中継されます。モデルに戻るのはconsole.logの出力のような、最小限の結果だけです。

outputSchemaが無いサーバーの扱い

サーバーがoutputSchemaを提供していないと、戻り値の型を生成できません。この場合の選択肢は2つです。1つはanyやstringのような汎用型で受け、非構造の出力を後段で扱う方法で、MCP側が簡便な道として先に挙げています。本筋の解決は、サーバー作者にoutputSchemaを付けてもらうことです。

もう1つは、ループの外で1回だけ呼ぶ場合に、小型モデルで値を期待する型へ整えさせる方法です。ホストがextract(value, ExpectedType)のような関数をスタブ経由で用意し、サンドボックス自身はネットワークに出ません。呼び出しのたびに遅延が加わります。フィールドの捏造や欠落も起こりうるので、使う前にExpectedTypeで検証します。

サンドボックスの選び方

実行したい言語と、ホスト側の言語で選びます。

実行言語代表的な実行環境特徴
JavaScript代表的な実行環境Deno / isolated-vm特徴V8ベース。権限を細かく無効化でき、完全なロックダウンも可能
Python代表的な実行環境Monty(実験的)特徴AI用途向けの最小限のPythonインタプリタ。既定でI/Oなし
TypeScript代表的な実行環境pctx(初期段階)特徴コードモードの概念をライブラリ化。Rust製の低レイヤーを持つ
任意の言語(Wasm経由)代表的な実行環境Wasmtime特徴任意の言語をWasmにコンパイルし、ケーパビリティベースで実行

表は例示で、推奨ではありません。成熟度は用途ごとに見極める前提で並べられています。どのサンドボックスでも統合パターンは同じです。ホストが関数スタブを注入し、プロセス内通信かstdio経由で呼び出しを横取りして、tools/callとしてMCPサーバーへ振り分けます。これならネットワーク権限は常に拒否したままで済みます。

3者の役割分担

役割は3つに分かれます。サンドボックスは、モデルが書いたコードを直接のネットワークアクセスなしで実行します。外に出る道は、ホストへ戻る関数スタブだけです。ホストはブローカーとして、サンドボックスからの呼び出しを正しいサーバーへ振り分けて実行し、結果を返します。認証トークンや資格情報はホストが持ち、生成コードには渡しません。モデルが見るのはconsole.logか最終的な戻り値だけで、コンテキストに入る情報をここで細かく制御できます。

セキュリティで見ておきたいこと

コード実行は、直接呼び出しに無い攻撃面を増やします。直接呼び出しと同じ確認は省略できません。

  • 呼び出しごとの承認: サンドボックス発の呼び出しにも、直接呼び出しと同じ人間による確認ポリシーを当てます。スクリプトの承認は、実行時のすべてのツール呼び出しの包括承認ではありません。たとえば「このスクリプトの実行中はticketing_createIssueを許可する」のような区分単位の承認は出せます。その場合もブローカーは、各呼び出しを承認の範囲と照らして評価します。
  • サーバーをまたぐデータの流れ: あるサーバーの結果は、別のサーバーにとって信頼できない入力です。出力を切り詰めるだけでは、情報の持ち出しを防げません。
  • 資源の上限: サンドボックスの実行にタイムアウトとメモリ上限を設け、暴走したスクリプトを止めます。
  • 出力の検証: サンドボックスのコンソール出力は、検証して切り詰めてからモデルに戻します。

エラー処理 — isErrorを例外に変換する

MCPのツールエラーは、通信自体の失敗ではなくisError: trueを持つ正常なレスポンスとして返ってきます。生成するラッパー関数は、このisErrorを検知したら例外としてスローし直すようにします。そうすると、モデルが書いたコード側は普通のtry/catchでエラーを扱えます。

スクリプトが未捕捉のエラーで止まった場合は、その例外をスクリプトの実行結果としてモデルに返し、自己修正させます。止まる前に確定した副作用の報告は、モデルの責任です。

2つのパターンは組み合わせて使う

Progressive DiscoveryとProgrammatic Tool Callingは競合しません。モデルは検索ツールで必要なツールを見つけ、スキーマを読み込み、それらを呼ぶ1本のスクリプトを書いて一度に実行できます。定義のトークンコストと、結果のトークンコストの両方を同時に抑えられる組み合わせです。

Agent SDKでは何がすでに実装されているか

自分でホストを実装せず、Anthropic純正のAgent SDKを使う場合は事情が変わります。Progressive Discoveryに相当する機能は、Tool Searchとして組み込まれています。既定でオンで、1回の検索で関連度の高い上位5件までを読み込みます。読み込んだツールは、それを発見したメッセージをSDKが圧縮するまでコンテキストに残ります。圧縮後に必要になれば、再び検索します(詳細はAgent SDK Tool Searchの使い方)。

挙動はENABLE_TOOL_SEARCH環境変数で変えられます。query()のenvオプションに渡します。TypeScriptではenvが子プロセスの環境を丸ごと置き換えるので、...process.envを展開して既存の変数を残します。

for await (const message of query({
  prompt: "Find and run the appropriate database query",
  options: {
    mcpServers: {
      "enterprise-tools": { type: "http", url: "https://tools.example.com/mcp" }
    },
    allowedTools: ["mcp__enterprise-tools__*"],
    env: {
      ...process.env,
      ENABLE_TOOL_SEARCH: "auto:5" // 遅延できる定義が5%に達したら有効化
    }
  }
})) {
  // message を処理する
}

値は次のとおりです。

値動作
未設定動作検索が有効。定義は遅延され、必要になった時点で発見される
true動作常に有効
auto / auto:N動作遅延できる定義の合計が、コンテキストウィンドウの10%(auto:NならN%)に達したら有効
false動作無効。毎ターンすべての定義を読み込む

未設定のときは、検索が働かない環境があります。Microsoft FoundryのAzureホスト型デプロイは、サーバー側で検索を拒否するため、SDKは定義を最初に読み込みます。ANTHROPIC_BASE_URLが自社以外のホストを指すときは、プロキシがtool_referenceブロックを中継しない前提で、既定で無効になります。trueで上書きできますが、tool_referenceブロックを中継しないプロキシではリクエストが失敗します。

Google CloudのAgent Platformでは、Claude Opus 4.5・Sonnet 4.5・Haiku 4.5以降が既定でオンで、それより前のモデルは定義を最初に読み込みます。FoundryのAzureホスト型デプロイと、Agent Platformの4.5より前のモデルは、trueでも上書きできません。CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETASを設定している場合も検索はオフのままで、ENABLE_TOOL_SEARCHでは上書きできません。Claude Code v2.1.221より前のSDKは、ENABLE_TOOL_SEARCHを設定しない限り、Agent Platform上の全モデルで検索を無効にしていました。

autoの数え方にも注意点があります。Bash・Read・Editのような中核の組み込みツールは最初に読み込まれ、しきい値には数えません。数えるのは、alwaysLoadを付けていないMCPツールすべてと、オンデマンドで読み込まれる組み込みツールです。検索のカタログに載せられるツールは最大1万個です。

本記事で扱った3層の原則は、Agent SDKを使わず独自のホストアプリケーションを書く場合や、Tool Searchの挙動を理解したい場合に効いてきます。Agent SDKのTool Searchが担うのは定義の開示です。Programmatic Tool Callingのコード実行に要るサンドボックスは、ホスト側で実装します。

まとめ

膨らんでいるのが定義か結果かを先に切り分け、定義なら測って切り替えるしきい値を、結果ならサンドボックスを、設計に入れます。ツールが10個前後のうちは、全件読み込みのままで構いません。

MCPの基本的な仕組みはMCPとはにまとめています。自分でホストアプリケーションを書く手順はMCPクライアント自作ガイドで扱っています。

この記事を共有:XはてブLinkedIn
MCP をもっと見る →