MCP ツール設計:実践的アプローチとトレードオフ
AWS は、MCP ツールの設計における「情報過多(bloat)」と「混乱(confusion)」という 2 つの主要な課題を特定し、コンテキストエンジニアリングによる実用的な解決策とトレードオフを詳述している。
キーポイント
MCP ツール設計の根本的な失敗要因
既存 API をそのまま公開するアプローチが、LLM の推論能力低下や誤ったパラメータ選択を引き起こす主な原因となる。
情報過多(Bloat)と混乱(Confusion)の悪循環
ツール定義の過剰な読み込みがコンテキストを圧迫し、推論能力を低下させる一方、曖昧な命名や類似ツールが多すぎることで LLM が誤った選択をする。
コンテキストエンジニアリングによる解決策
ツール記述の強化は混乱を減らす一方で情報過多を悪化させるため、LLM が見る情報とタイミングを最適化するバランスが不可欠である。
影響分析・編集コメントを表示
影響分析
この記事は、生成 AI エージェントの実装において頻発する「ツール呼び出しの失敗」や「コンテキスト切れ」という実務上の課題を、プロトコルの欠陥ではなく設計プロセスの問題として再定義した点で重要です。開発者に対し、単なる API の公開から、LLM の推論特性に配慮した「コンテキストエンジニアリング」への転換を促すことで、より堅牢なエージェントシステムの構築指針を提供しています。
編集コメント
MCP の仕様そのものの問題ではなく、開発者の設計思想に起因する課題を鋭く指摘しており、実務でエージェントシステムを構築しているエンジニアにとって非常に示唆に富む内容です。コンテキスト容量の制約下でいかに LLM に最適な情報だけを届けるかという視点は、今後の AI アプリ開発において必須のスキルセットと言えるでしょう。
MCP (Model Context Protocol) ツールが期待通りに機能しない場合、その原因はプロトコル自体ではなく、ツールの設計にあることがほとんどです。多くのチームは、既存の API をそのまま公開し、エージェントに残りの部分を解決させることから始めます。これは、API をエージェントシステムや生成 AI コーディングツールへ拡張する自然な方法ですが、単純なユースケースでは機能することもあります。しかし、実際にはそうならないことも多いです。
LLM (Large Language Models) やエージェントシステムの動作原理に合わせてツールを設計する必要があります。これを怠ると、ツールの呼び出し失敗、パラメータ値の誤り、コンテキストを無駄にする再試行が発生し、パフォーマンスが低下するリスクがあります。本稿では、MCP ツール設計でどこが間違っているのか、そして実践的な コンテキストエンジニアリング アプローチを用いてどのように修正すべきかを示します。
これらの失敗の背後には主に 2 つの問題があります。1 つ目は「肥大化」です。ツールの定義は、そのツールが使用されるかどうかに関わらず、呼び出しごとに LLM のコンテキストに読み込まれます。複数の MCP サーバーが接続されている場合、ユーザーがまだ質問もしていない段階で、すでに大量のコンテキストを消費してしまいます。コンテキストが埋まると、LLM の 推論能力が低下 し、セッションの生産性が低下します。
2 つ目は*混乱*です。推論能力が低下すると、LLM はより不適切な選択を行い、誤ったツールを呼び出したり、誤ったパラメータを選択したりします。その後の再試行はさらに肥大化に寄与し、問題を悪化させます。ツール間の意味的類似性、選択肢が多すぎる点、曖昧な命名 も混乱の一因となっています。一般的な解決策として、ツールの説明をより明確な定義、自然言語によるマッピング、使用例で補強することが挙げられます。これは確かに混乱の軽減に役立ちます。しかし、追加されるすべての要素が肥大化を悪化させ、解決しようとしている問題をさらに複雑にするリスクがあります。
このパターンは一般的です。エージェントが予期せぬ選択を行い、コンテキストが予想より早く埋まり、セッションがすぐに有用性を失います。肥大化と混乱への対処は、コンテキストエンジニアリングの問題です。コンテキストエンジニアリングは、LLM が何を見て、いつ見るかを形作るものであり、これによりモデルはより良い結果を生成します。このうち一方、あるいは両方を改善することは、複雑なバランスの取れた作業となります。
以下のセクションでは、これらの課題に対処するためのアプローチとトレードオフを探ります。具体例として、MCP プロトコルを使用してシミュレーションされた K-12 コンテンツ検索 API を公開するいくつかの例を作成しました。これらはローカルで実行し、Kiro CLI を使用して対話しながら、違いを自分で比較することができます。
アプローチとトレードオフ
私たちが共有するアプローチは、ツールレベルでこれらの課題に対処します。いくつかは LLM が見る内容や見るタイミングを形作り、他のものはツールの構造そのものを変更します。これらを実践する際には、それらが「肥大化」と「混乱」にどう影響するかを理解し、両者の間で適切なバランスを見つける必要があります。
説明とレスポンス
MCP ツールの動作を修正するための最初の自然なステップは、説明を改善することです。混乱を減らすための確実な方法は、値の意味や自然言語がそれらにどのようにマッピングされるか、そしてツールの目的を明確にすることです。しかし、やりすぎるとすぐに肥大化を招きます。多くの MCP サーバーがクライアントに読み込まれる場合、コンテキストとコストは急速に増大する可能性があります。
ツールが返す内容も動作を形作ります。結果ごとに 50 のフィールドを返すツールは、コンテキストをすぐに埋め尽くしてしまいます。意思決定には 5 つのフィールドで十分であれば、デフォルトではそのフィールドのみをレスポンスに含め、詳細ビューを要求するための別オプションを提供してください。詳細出力に対してオンデマンド方式へ移行することで、レスポンストークンは約 3 分の 2 削減されます Anthropic の研究によると。
適切なエラーメッセージも効率を改善する別の手段です。ツール呼び出しが失敗した場合、役立つエラーメッセージは次の試行を導くことができます。「検索にはクエリに 2 つ以上の用語が必要です」というレスポンスは、LLM に何を修正すべきかを明確に伝えます。一方、「結果なし」のみを返すレスポンスでは何も示さないため、モデルが諦めるか、推測し続けることになります。
アプローチとトレードオフ
私たちが共有するアプローチは、ツールレベルでこれらの課題に対処します。いくつかは LLM が見る内容や見るタイミングを形作り、他のものはツールの構造そのものを変更します。これらを実践する際には、それらが「肥大化」と「混乱」にどう影響するかを理解し、両者の間で適切なバランスを見つける必要があります。
説明とレスポンス
MCP ツールの動作を修正するための最初の自然なステップは、説明を改善することです。混乱を減らすための確実な方法は、値の意味や自然言語がそれらにどのようにマッピングされるか、そしてツールの目的を明確にすることです。しかし、やりすぎるとすぐに肥大化を招きます。多くの MCP サーバーがクライアントに読み込まれる場合、コンテキストとコストは急速に増大する可能性があります。
ツールが返す内容も動作を形作ります。結果ごとに 50 のフィールドを返すツールは、コンテキストをすぐに埋め尽くしてしまいます。意思決定には 5 つのフィールドで十分であれば、デフォルトではそのフィールドのみをレスポンスに含め、詳細ビューを要求するための別オプションを提供してください。詳細出力に対してオンデマンド方式へ移行することで、レスポンストークンは約 3 分の 2 削減されます Anthropic の研究によると。
適切なエラーメッセージも効率を改善する別の手段です。ツール呼び出しが失敗した場合、役立つエラーメッセージは次の試行を導くことができます。「検索にはクエリに 2 つ以上の用語が必要です」というレスポンスは、LLM に何を修正すべきかを明確に伝えます。一方、「結果なし」のみを返すレスポンスでは何も示さないため、モデルが諦めるか、推測し続けることになります。
スキーマ制約
説明と適切なエラーメッセージが LLM を正しい値へと導く一方で、列挙型やデフォルト値といったスキーマ制約は、推測を完全に排除することができます。一般的なスキーマの問題に対処するために、以下の点を考慮してください:
- パラメータ名を、データベースが列に付与している名称ではなく、LLM がドメインを理解する際の表現に合わせて変更してください。
'Student Resource' や 'Teacher Support' といった値を持つ resource_class というパラメータは、LLM にとってより明確です。一方、content_bucket カラムでは、LLM が内部の慣習を知っている必要があります。
- デフォルト値を最も一般的な値に設定し、LLM が指定するのは変化する部分だけで済むようにします。
- 有限の値を持つフィールドには列挙型(enum)を使用して、スキーマ自体が LLM に有効な値を示すようにします。
- ほとんど使用されないフィールドや、LLM が効果的に利用できないフィールドは削除してください。
MCP ストラテジーに関する AWS の推奨ガイドラインでは、ツールパラメータの数を 8 つ以下に保つことを推奨しています。
ツールの再構築とオンデマンドコンテキスト
多目的ツールを複数の特定のツールに分割することで、モデルに対する明確さが得られ、より粒度の細かい結果が得られます。遅延読み込みディスカバリーツールはその一例です。常にロードされるコンテキストから複雑なツールドキュメントを削除し、オンデマンドでの取得専用の別ツールを提供できます。LLM はタスクに必要な場合にのみこのコンテキストを取得します。これにより、コンテキストは簡潔で焦点が絞られた状態に保たれます。Anthropic の Tool Search ツールや Amazon Bedrock AgentCore Gateway では、これらの概念が大規模に適用されています。Anthropic によると、関連する場合にのみツールドキュメントを読み込むことで、最大 85% のトークン削減 が実現できると報告しています。
Skills も遅延読み込みの別の例ですが、こちらはクライアントサイドで実装されています。これらのローカルファイルにはツールの有用なコンテキストが含まれていますが、関連する場合にのみコンテキストに読み込まれます。これにより、実装と配布にかかる労力が削減されます。ただし、必要な時にスキルがロードされることや、インストール後に不変であることが保証されているわけではありません。
サーバーサイド推論
どのモデルがあなたの指示を解釈するかを制御できない場合、コンテキストエンジニアリングは困難です。また、一度デプロイされると、MCP ツールに対してどの LLM(大規模言語モデル)が呼び出されるかを制御できない可能性があります。あるモデル向けに調整された説明は、別のモデルでは混乱を招くかもしれません。クライアントが使用する可能性のあるすべての潜在的なモデルを完全にテストすることは非現実的です。
これに対処する一つの方法は、外部の LLM を直接呼び出すインスペクションツールを追加することです。クライアントは自然言語を使用してあなたのインスペクションツールに問い合わせ、他のツールの使用方法に関するターゲットを絞った指示を取得します。モデルを選択するのはあなたなので、このツールに対して適切にプロンプトエンジニアリングを行い、ゴールデンクエリでテストできます。最終的なツール呼び出しを行うのはクライアント側の LLM ですが、必要な要件の解釈と正しい値の提供はあなたの LLM が行います。ユースケースが焦点を絞られているため、より小さく高速なモデルがこのタスクを適切に処理し、スケールしてもコストを合理的に保つことができます。
エージェント型ツール
精度と完全な制御が必要な場合、次の論理的な進展は、MCP サーバー全体を独自のエージェントでバックアップすることです。インスペクションの例は単一の解釈ステップを処理しますが、エージェント型ツールは相互作用全体を処理します。ツールはクライアントに対する自然言語エンドポイントとなります。必要なものを宣言し、残りの部分はあなたのエージェントが担当します。ここで取り上げた他の技術も依然として適用されます。トレードオフは完全にあなたが責任を持つことになりますが、インスペクションと同様に、選択したモデルを用いて動作を完全にエンジニアリングすることができます。
次のセクションでは、これらのアプローチを同じバックエンドとテストクエリに適用する動作コードを紹介し、その挙動を直接比較できるようにします。
解説
6 つのバージョンそれぞれが、異なる MCP ツール設計を用いて、同じシミュレーションされた K-12 コンテンツ検索バックエンドをラップしています。このバックエンドには、制御された語彙を持つ 14 のフィルタリング可能なフィールド(科目、学年、フォーマット、基準への適合性、言語、リソースクラスなど)があります。LLM が直面する課題は、教師がリクエストを表現する方法と、各フィールドが受け付ける正確な値との間のギャップを埋めることです。V1 はこのギャップを開いたままにする生きたパススルーから始まります。その後の各バージョンは、それぞれ異なる方法でこれを解消します。
前提条件
サンプルコードを実行するには以下のものが必要です:
- Python 3.10 以上。
- SQLite(Python に同梱されています)。
- Kiro(無料枠あり)またはその他の MCP クライアント。
- Amazon Bedrock モデルアクセス権限を持つ AWS アカウント(V5 および V6 のみ):Amazon Nova 2 Lite と Anthropic の Claude Sonnet 4.6。
サンプルコードリポジトリ をクローンし、詳細な前提条件、インストール手順、サーバー起動方法についてはリポジトリの README に従ってください。続く解説では、すべての 6 つのバージョンが実行されていることを前提としています。
AWS へのインフラストラクチャのデプロイは行われません。コードはすべてローカルで実行します。Amazon Bedrock への推論呼び出し以外に発生するコストは、コーディングクライアントの利用料のみです。現在の料金は Amazon Bedrock の価格設定 をご確認ください。
このウォークスルーでは、Kiro を使用しており、リポジトリから python scripts/setup_agents.py スクリプトを実行済みであることを前提としています。
テストクエリ
これらのクエリは、サンプルデータベースからの実際の結果を返します。各バージョンで試してみてください:
- "7 年生の生徒向けの分数に関する小テストを見つけてください。"
- "中学校でのスペイン語教授用のレッスン資料はありますか?"
- "中学校で除算に取り組む児童向けに TEKS に準拠したコンテンツが必要です。"
- "検索できるコンテンツの種類は何ですか?"
- "n-sc-1096 に関する詳細を取得できますか?"
各バージョンを実行する際に、以下の点を確認してください:
- LLM(大規模言語モデル)が最初の呼び出しで適切なフィルタ値を選択したかどうか観察します。
- 必要なツール呼び出しの回数を記録します。
- 応答に LLM が不要な情報が含まれていないか確認します。
- コンテキストウィンドウがどの程度速く埋まるかを観察します。Kiro に表示されるパーセンテージを確認するか、/context show コマンドを使用して詳細な内訳をチェックしてください。

バージョン間を切り替えるには、/agent swap を使用してください(例:/agent swap v1-passthrough)。各バージョン間で /clear コマンドを実行してコンテキストをクリアし、以前の結果が次の実行に影響を与えないようにしてください。
V1: 生パストスルー
v1_passthrough.py
最初のバージョンは、ベースラインのアンチパターンです。これはバックエンド API を直接公開するものです。ツール定義には 14 のパラメータが含まれており、内部名として discipline(分野)、media_type(メディアタイプ)、content_bucket(コンテンツバケット)などが使われています。また、「教育リソースに対するグローバル検索を実行します」という 1 行のドキュメントストリングがありますが、有効値のリストも、自然言語によるマッピングも、ガイダンスもありません。
ツール定義自体は小さいものの、LLM はどの値が有効かについてのガイダンスを持っていません。最初のクエリを試してみましょう。media_type に「Assessment」が有効な値であるにもかかわらず、「quiz」と入力したり、Math が必要なフィールドに「math」と入力したりする可能性があります。それぞれの誤った選択は再試行を引き起こし、より多くのコンテキストを消費します。何かが間違っていることを示す唯一の手がかりは空の結果です。LLM は次に何を修正すべきかを推測するしかありません。混乱によって実際のコストが転換(churn)を通じて上昇している場合、低いベースラインコストは誤解を招きます。

V2: 詳細な説明
v2_better_descriptions.py
V1 と同じ構造で、バックエンドのリファクタリングは不要です。ドキュメントストリングには、各フィールドの有効値とシノニム(類義語)マッピングが記載されています。例えば、discipline では「有効値:Math, Science, Literacy/ELA…」と表示され、media_type は「'quiz'/'test' → Assessment」、「'worksheet' → Activity」とマッピングされます。また、曖昧な検索(キーワードベース)と厳格なフィルタ(それ以外すべて)を区別します。3 つのあまり使われないパラメータは削除され、エラーメッセージでは空の結果ではなく、追加すべきフィルタに関するガイダンスが返されるようになりました。
同じクエリを試してください。LLM が有効な値と類義語マッピングを確認できるため、精度は即座に向上します。ツール定義は明らかに大きくなります。これが肥大化のトレードオフです。ツールが使用されるかどうかに関わらず、呼び出しごとにこのコストが発生します。5 回のクエリ後に、V1 と比較して消費されたコンテキストの総量を評価してください。呼び出しごとのオーバーヘッドは高くなりますが、リトライ回数が減ることで全体として低くなるケースも多々あります。

V3: スキーマとデフォルト値
v3_rethought_schema.py
このバージョンでは、LLM の思考パターンに合わせてパラメータ名を変更し、スキーマ自体を通じて値を制約します。パラメータ名の変更点は以下の通りです:discipline は subject に、content_bucket は resource_class に変更されます。各有限値フィールドには、有効な選択肢を直接列挙した Literal 型が使用されます。一般的なケースに対応するため、合理的なデフォルト値が設定されています:structure='Asset'、resource_class='Student Resource'、language='en'。詳細情報を取得するための別ツールとして get_resource_detail が用意され、ドリルダウン処理を担当します。これは再構築アプローチを適用したものであり、各ツールに明確な役割を与えつつ、検索レスポンスを簡潔に保つものです。
列挙型はプロトコルレベルでの誤った値を防止するのに役立ちます。デフォルト値を設定することで、LLM は変化する部分のみを指定すればよくなります。レスポンスには defaults_applied フィールドが含まれており、LLM が何が無効にフィルタリングされたかを把握できます。定義は V2 よりも小さくなります。これは、名前と列挙型が以前冗長な説明が行っていた役割を担うためです。クエリを実行して精度を V2 と比較してください。文脈量が減少する一方で、精度は向上します。
V4: レーザーローディング(再構築)
v4_lazy_loading.py
列挙型や詳細な説明を検索ツールに埋め込むのではなく、このアプローチではそれらを別のツールの背後に移動させます。検索ツールには「科目領域、例:'Math'、'Science'、'Literacy'」のような短いヒント説明のみを残します。get_taxonomy ツールはフィールド名のリストを受け取り、現在のクエリに関連するフィールドについてのみ、有効な値と自然言語によるマッピングを返します。
検索ツールの定義はこれまでで最も軽量です。ツール呼び出しに注目してください。曖昧なクエリの場合、LLM は検索前に get_taxonomy を呼び出して有効な値を確認します。ヒントが十分で明確なクエリのケースでは、taxonomy 呼び出しを完全にスキップして直接検索することもあります。ヒントに含まれる一般的な値は、ラウンドトリップ(往復通信)なしで頻繁なクエリに対応し、一方、完全な taxonomy はエッジケースのために用意されています。taxonomy は必要な時だけ読み込まれるため、セッション内のそれ以前のすべての対話は、その文脈コストなしで実行されます。この例での節約は控えめですが、多数の接続ツールと複雑なツールスキーマを持つ環境では、この効果は急速に蓄積します。

V5: LLM 自己分析(LLM introspection)
v5_llm_introspect.py
このバージョンでは、Amazon Bedrock 上の Amazon Nova 2 Lite をバックエンドとする自己分析ツールを追加しました。introspect_query ツールは、教師の自然言語による質問を受け取り、推奨されるフィルタ値と各選択の根拠を説明した結果を返します。「中学で除算に取り組む子供向けの TEKS に準拠したコンテンツ」というクエリを解釈し、推奨フィルタとして「Math(数学)」、学年 6-8 歳、「TX-TEKS」(州標準)、トピック「dividing, division(除算)」を返します。
Kiro は最終的な検索の判断を下します。Introspect は選択したモデル上で動作するため、プロンプトエンジニアリング とテストが確実に行えます。解釈はサーバー側で行われるため、コンテキストは軽量に保たれます。その代償はコストです。サーバー呼び出しには費用がかかりますが、Kiro がどのモデルを使用するかに関わらず結果は一貫して安定します。/model でモデルを切り替えて V4 と V5 を比較できます。Introspection なしでは、 weaker モデル(より能力の低いモデル)は不安定な結果を生みますが、Introspection ありであれば結果は安定したままです。
V6: エージェントをツールとして
v6/app/v6/main.py
最終バージョンは、独自のシステムプロンプトと内部ツールを持つ Strands Agents エージェントに裏打ちされた単一の MCP ツールを公開します。外部インターフェースは 1 つのツールで、パラメータも 1 つのみです:agentic_search_content(question: str)。このエージェントは、分類体系の検索、検索、詳細取得、レスポンスフォーマットを、クライアント LLM が見ることのできない独自のツールを使用して内部で行います。
5 つすべてのクエリを試して、他のバージョンとのクライアントコンテキストの使用量を比較してください。クライアント LLM は最小限の作業しか行いません。どのクライアントが接続しても動作は一定です。なぜなら推論を担っているのはエージェント自身だからです。会話履歴は呼び出し間でも保持されるため、フォローアップ質問も自然に行えます。その代償はコストとレイテンシですが、代わりに挙動と一貫性に対する直接的な制御が可能になります。Kiro で /model を使用してモデルを切り替えてみてください。どのモデルを使用しても結果が安定していることに気づくでしょう。
Tradeoffs at a glance
各バージョンは、あるコストを別のコストと引き換えにしています。この表ではそれらを並べて比較します。
<table>
<tr>
<th><strong>Version</strong></th>
<th><strong>Approach</strong></th>
<th><strong>Tradeoff</strong></th>
</tr>
<tr>
<td>V2</td>
<td>Rich descriptions</td>
<td><td
原文を表示
When Model Context Protocol (MCP) tools underperform, the cause is rarely the protocol itself but the tool design. Many teams start by exposing an existing API as-is and trusting the agent to figure out the rest. It is a natural way to extend APIs to agentic systems and generative AI coding tools. For straightforward use cases, it can work. But often it does not.
You must design your tools for how large language models (LLMs) and agentic systems work. Without this, you risk failed tool calls, wrong parameter values, and retries that waste context and degrade performance. In this post, we show where MCP tool design goes wrong and how to fix it with practical context engineering approaches.
Two problems are behind most of these failures. The first is *bloat*. Tool definitions load into the LLM’s context on every call, whether the tool is used or not. Multiple connected MCP servers can consume significant context before the user has asked a single question. As context fills, an LLM’s ability to reason can degrade and cause the session to become less productive.
The second is *confusion*. As reasoning degrades, the LLM makes poorer choices, calls the wrong tool and chooses incorrect parameters. Subsequent retries compound the issue by further contributing to bloat. Semantic similarity between tools, too many options, and ambiguous naming also contribute to confusion. A common solution is to enrich the tool descriptions with clearer definitions, natural language mappings, and usage examples. This does help with confusion. But everything added risks worsening bloat and compounding the very issue you are working to solve.
This pattern is common. Your agent makes unexpected choices, the context fills sooner than expected, and your session quickly becomes less useful. Addressing bloat and confusion is a context engineering problem. Context engineering shapes what the LLM sees and when it sees it, so the model produces better results. Improving one, or both, of these is a complex balancing act.
The following sections explore approaches and tradeoffs to address these issues. To make them concrete, we built several examples that expose a simulated K-12 content search API using the MCP protocol. You will run them locally and interact with them using Kiro CLI to compare the differences yourself.
Approaches and tradeoffs
The approaches we share address these issues at the tool level. Some shape what the LLM sees or when it sees it. Others change how tools are structured. As you apply them, you need to understand how they impact bloat and confusion to find the right balance between the two.
Descriptions and responses
The natural first step to fixing MCP tool behavior is to improve the descriptions. A reliable way to reduce confusion is to clarify what values mean, how natural language maps to them, and what the tool is for. But take it too far, and you quickly add to bloat. Context and cost can grow quickly when many MCP servers are loaded into a client.
What the tool returns shapes behavior too. A tool that returns 50 fields per result fills context quickly. If 5 are sufficient for a decision, default the response to those fields and provide a separate option to request a detailed view. Shifting to an on-demand approach for detailed output cuts response tokens by roughly two-thirds, according to Anthropic’s research.
Proper error messages are another way to improve efficiency. When a tool call fails, helpful errors can steer the next attempt. A response that says “search requires 2 or more terms in query” tells the LLM exactly what to change. A response that returns only “no results” gives it nothing, causing the model to either give up or keep guessing.
Schema constraints
Where descriptions and proper errors guide the LLM toward correct values, schema constraints like enums and default values can remove the guesswork entirely. Consider the following to address common schema issues:
- Rename parameters to match how the LLM might understand the domain, not how your database labels its columns.
A parameter called resource_class with values like ‘Student Resource’ or ‘Teacher Support’ is clearer for the LLM. A content_bucket column requires the LLM to know an internal convention.
- Set defaults to the most common values so the LLM only needs to specify what varies.
- Constrain finite-value fields with enums so the schema itself tells the LLM what is valid.
- Drop fields that are rarely used or that the LLM cannot use well.
AWS Prescriptive Guidance for MCP Strategies recommends keeping tool parameter counts around eight or fewer.
Restructuring tools and on-demand context
Splitting a multi-purpose tool into several specific tools provides clarity to the model and gives more granular results. A lazy loading discovery tool is one example. You can remove complex tool descriptions from the always-loaded context and provide a separate tool for on-demand retrieval. The LLM only retrieves this context when the task requires it. This keeps context lean and focused. Anthropic’s Tool Search Tool and Amazon Bedrock AgentCore Gateway show these concepts applied at scale. Anthropic reports up to 85% token reduction by loading tool definitions only when relevant.
Skills are another example of lazy loading, but implemented client side. These local files contain useful context for the tool but are only read into context when relevant. This reduces the effort to implement and distribute them. But there is no guarantee that the skill loads when needed or remains unchanged once installed.
Server-side inference
Context engineering is challenging when you do not control which model will interpret your instructions. And once deployed, you may not control which LLM calls your MCP tools. A description tuned against one model might confuse another. Fully testing all the potential models a client might use is unrealistic.
One way to address this is to add an introspection tool that directly calls an external LLM. The client uses natural language to query your introspection tool and gets targeted instructions for using the other tools. Because you choose the model, you can properly prompt engineer this tool and test with golden queries. The client LLM still makes the final tool call, but your LLM interprets the need and provides correct values. Because the use case is focused, a smaller, faster model handles this task well and keeps costs reasonable at scale.
Agentic tools
When you need accuracy and full control, the next logical progression is to back your entire MCP server with an agent of your own. The introspection example handles a single interpretation step, but an agentic tool handles the entire interaction. The tools become natural language endpoints to the client. It states what it needs, and your agent does the rest. The other techniques covered still apply. The tradeoffs are yours to own completely, but as with introspection, you can fully engineer the behavior with the model of your choice.
The next section walks through working code that applies these approaches against the same backend and test queries, so you can compare their behavior directly.
Walkthrough
Each of the 6 versions wraps the same simulated K-12 content search backend with a different MCP tool design. The backend has 14 filterable fields (subject, grade, format, standards alignment, language, resource class, and others) with controlled vocabularies. The challenge for the LLM is bridging the gap between how a teacher phrases a request and the exact values those fields accept. V1 starts with the raw passthrough that leaves this gap open. Each later version closes it differently.
Prerequisites
You need the following to run the sample code:
- Python 3.10+.
- SQLite (included with Python).
- Kiro (free tier available) or another MCP client.
- AWS account with Amazon Bedrock model access (V5 and V6 only): Amazon Nova 2 Lite and Anthropic’s Claude Sonnet 4.6.
Clone the sample code repository and follow the repository README for detailed prerequisites, installation, and server startup instructions. The walkthrough that follows assumes all six versions are running.
No infrastructure is deployed to AWS. You run all code locally. Inference calls to Amazon Bedrock are the only cost aside from your coding client. See Amazon Bedrock pricing for current rates.
This walkthrough assumes you are using Kiro and have run python scripts/setup_agents.py from the repository.
Test queries
These queries return real results from the sample database. Try them against each version:
- “Find me a quiz on fractions for my 7th graders.”
- “Do you have any lessons for teaching Spanish in middle school?”
- “I need TEKS-aligned content for kids working on dividing in middle school.”
- “What types of content can I search for?”
- “Can I get details on n-sc-1096?”
As you run through each version:
- Observe whether the LLM picked the right filter values on the first call.
- Note how many tool calls it took.
- Check whether the response included information the LLM didn’t need.
- Observe how quickly the context window fills. Check the percentage displayed in Kiro, or use /context show for a detailed breakdown.

To switch between versions, use /agent swap (for example, /agent swap v1-passthrough). Clear context with /clear between versions so prior results don’t influence the next one.
V1: Raw passthrough
v1_passthrough.py
The first version is the baseline anti-pattern. It exposes the backend API directly. The tool definition has 14 parameters with internal names like discipline, media_type, content_bucket, and a one-line docstring that says “Performs a global search for educational resources.” No valid values listed, no natural language mappings, no guidance.
The tool definition is small, but the LLM has no guidance on what values are valid. Try the first query. It might pass “quiz” for media_type when the valid value is “Assessment”, or “math” when the field expects “Math”. Each wrong choice triggers a retry that consumes more context. The only clue that something went wrong is an empty result. The LLM is left guessing what to change on the next attempt. Low baseline cost is misleading when confusion drives up the actual cost through churn.

V2: Rich descriptions
v2_better_descriptions.py
Same structure as V1, zero backend refactoring. The docstring now lists valid values and synonym mappings for each field. For example, discipline shows “Valid: Math, Science, Literacy/ELA…” and media_type maps “‘quiz’/‘test’ → Assessment, ‘worksheet’ → Activity.” It also distinguishes fuzzy search (keyword) from strict filters (everything else). Three rarely-used parameters are dropped and error messages now return guidance on which filters to add, rather than an empty result.
Try the same queries. Accuracy improves immediately because the LLM sees valid values and synonym mappings. The tool definition is noticeably larger. That’s the bloat tradeoff. Every call pays it whether the tool is used or not. After five queries, compare total context consumed to V1. The per-call overhead is higher, but fewer retries often make the total lower.

V3: Schema and defaults
v3_rethought_schema.py
This version renames parameters to match how the LLM thinks and constrains values through the schema itself. Parameters are renamed: discipline becomes subject, content_bucket becomes resource_class. Each finite-value field uses a Literal type that lists valid options directly in the schema. Sensible defaults handle the common case: structure='Asset', resource_class='Student Resource', language='en'. A separate get_resource_detail tool handles drill-down. This applies the restructuring approach, giving each tool a clear job and keeping search responses concise.
Enums help prevent wrong values at the protocol level. Defaults mean the LLM only specifies what varies. The response includes a defaults_applied field so the LLM knows what was filtered implicitly. The definition is smaller than V2 because names and enums do the work that verbose descriptions did before. Run the queries and compare accuracy to V2. Accuracy improves while context drops.
V4: Lazy loading (restructuring)
v4_lazy_loading.py
Instead of embedding enums and detailed descriptions in the search tool, this approach moves them behind a separate tool. The search tool keeps only short hint descriptions like “Subject area, e.g. ‘Math’, ‘Science’, ‘Literacy’”. A get_taxonomy tool takes a list of field names and returns valid values and natural language mappings only for the fields relevant to the current query.
The search tool definition is the leanest so far. Notice the tool calls. For ambiguous queries, the LLM calls get_taxonomy before searching to confirm valid values. For straightforward queries where the hints are sufficient, it might skip the taxonomy call entirely and search directly. Common values in the hints handle frequent queries without a round-trip, whereas the full taxonomy is available for edge cases. Because the taxonomy loads only when needed, every prior interaction in the session runs without that context cost. The savings in this example is modest, but in environments with many connected tools and complex tool schemas it compounds quickly.

V5: LLM introspection
v5_llm_introspect.py
This version adds an introspect tool backed by Amazon Nova 2 Lite on Amazon Bedrock. The introspect_query tool takes the teacher’s natural-language question and returns recommended filter values with rationale explaining each choice. It interprets “TEKS-aligned content for kids working on dividing in middle school” and returns recommended filters: subject “Math”, grades 6-8, state_standard “TX-TEKS”, topic “dividing,division”.
Kiro still makes the final search call. Because introspect runs on a model of your choice, prompt engineering and testing are reliable. The interpretation happens server-side, so your context stays lean. The tradeoff is cost. You pay for the server call but results stay consistent regardless of which model Kiro uses. Switch models with /model and compare V4 versus V5. Without introspection, weaker models produce inconsistent results. With introspection, results stay stable.
V6: Agent-as-tool
v6/app/v6/main.py
The final version exposes a single MCP tool backed by a Strands Agents agent with its own system prompt and internal tools. The external interface is one tool with one parameter: agentic_search_content(question: str). Your agent handles taxonomy lookup, search, detail retrieval, and response formatting internally using its own tools that the client LLM does not see.
Try all 5 queries and compare client context usage to the other versions. The client LLM does minimal work. Behavior is consistent regardless of which client connects because your agent owns the reasoning. Conversation history persists across calls, so follow-up questions work naturally. The tradeoff is cost and latency in exchange for direct control over behavior and consistency. Switch between models with /model in Kiro and notice results remain stable across models.
Tradeoffs at a glance
Each version trades one cost for another. This table lays them side by side.
Version
Approach
Tradeoff
V2
Rich descriptions
<td
関連記事
今日のまとめ
AI日報で今日の重要ニュースをまとめ読み