はじめに
本記事は「Context Engineering 完全入門」シリーズの第 3 回です。今回は 結果ハンドリング を取り上げます。
第 2 回でツール設計のベストプラクティスをご紹介しましたが、外部の MCP サーバーが返す巨大な出力はツール側での制御だけでは限界があります。本記事では、クライアント側でのトリミング・通知・復元の仕組みと、「Reversible Compaction(可逆圧縮)」を優先する設計思想について解説します。
前回:第 2 回 ツール設計
次回:第 4 回 履歴圧縮🗺️ 単独でも読めます:MCP クライアント側のトリミング実装は本記事だけで理解・適用できます。
📝 本記事は、生成 AI で作成した草案をベースに、筆者が加筆・修正し、技術的な正確性を確認した上で公開しています。
📝 本記事の要点
- 問題: MCP プロトコルには context size hint が無いため、外部 MCP サーバから巨大な出力が無防備に流れ込む
- 解決: クライアント側で 境界トリミング + 構造化通知 + 復元手段の提示、かつ Reversible Compaction を優先する
- 実装の要点:
ResourceStoreで full content を退避し、LLM にはisTruncated=trueとlookup_full_content(...)を返す急ぐ方は §1「6 つのベストプラクティス」 と §6 アンチパターン だけでも価値があります。
🎯 本記事の対象者
- MCP 経由の外部ツールを使っている
- ツール側のハードリミットだけでは制御しきれない出力に困っている
-
ContextProviderを使って独自のハンドリングロジックを書きたい - Reversible Compaction と Irreversible Summarization の使い分けを学びたい
- ルート全体でのトークン予算を明示的に管理したい
1. ⭐ 原則
MCP プロトコルの根本的な限界
VS Code Copilot Chat エンジニアの connor4312 氏は Issue #9718 で以下の認識を示しています:
"MCP does not allow passing context window hints in tool calls, and we could also never depend on an MCP server respecting that."
要点:
- MCP プロトコルにはコンテキストヒントを渡す仕組みがない
- 仮にあったとしても、サードパーティ MCP サーバが従う保証はない
- だからクライアント側でやるしかない
つまり第 2 回のツール設計を完璧にしても、外部 MCP サーバが返す巨大な出力には独自のハンドリング層が必要 です。
6 つのベストプラクティス
| # | プラクティス | 概要 | 参照項目 |
|---|---|---|---|
| 1 | 境界で切る | バイト/文字単位ではなく意味のある単位で切る | §2「要素 1」 |
| 2 | 構造化された通知 |
isTruncated フラグ + メタデータで機械可読に伝える |
§2「要素 3」 |
| 3 | 復元手段の提示 | ページネーション or 専用検索ツールを提供 | §2「要素 4」 |
| 4 | アンチパターンの回避 | 動かない復元手段を案内しない | §6 |
| 5 | Reversible Compaction を優先 | Irreversible Summarization は最後の手段 | §2 |
| 6 | Token Budgeting:ルート別予算配分 | 用途別に予算を割り当て、超過時は明示的にエラー | §2 |
2. 📐 パターン
VS Code Copilot Chat の現状実装と既知バグ
GitHub MCP の get_file_contents で 50KB–999KB のファイルを取得した場合:
設計の良いところ
- ✅ 一定値(50KB 超)を超過したら自動トリミング
- ✅
isTruncated=trueフラグでメタデータとして明示 - ✅ 「フル取得の方法」までエージェントに伝えている
⚠️ 現状発生しているバグ
Issue #311068(2026年4月、Openのまま):ヒントに書かれた回復手段が実際には動かない。設計思想は正しいが実装が追いついていない典型例であり、独自実装する際に重点的に検証が必要な項目です。
デファクトパターンの 5 要素
外部ツールの大きな出力をクライアント側でハンドリングする際、業界で共通して見られる実装パターンは 5 つの要素 で構成されます。要素 1〜4 は「順に適用する処理ステップ」、要素 5 は「要素 1〜4 の連携で生じる不整合を防ぐ横断的な観点」として位置付けられます。
| 要素 | 名称 | 役割 |
|---|---|---|
| 要素 1 | 境界の選び方 | どの単位で切るか(行・段落・レコード等) |
| 要素 2 | 閾値の決め方 | いつ切るか(文字数・トークン・残予算) |
| 要素 3 | 通知メッセージの書き方 | 切ったことをどう LLM に伝えるか |
| 要素 4 | 復元手段の準備 | 切った情報にどうアクセスさせるか |
| 要素 5 | 要素間の整合性リスク | 要素 1〜4 の連携で生じる不整合を防ぐ |
要素 1:境界の選び方
大きな出力をどこで区切るかを決めます。バイト数や文字数で機械的に切ると、コードや JSON の途中で分断され、LLM が構造を読み取れなくなります。そのため、出力タイプごとに「意味のある単位(行・段落・レコードなど)」を境界として選ぶことが重要です。
| 出力タイプ | 推奨される境界 |
|---|---|
| ソースコード | 行、関数、ブロック |
| ログ | 行、エントリ(タイムスタンプ単位) |
| JSON 配列 | 配列要素 |
| Markdown | 段落、見出しセクション |
| 検索結果 | レコード/マッチ |
要素 2:閾値の決め方
「どのサイズを超えたらトリミングするか」の基準を決めます。実装の手軽さと精度はトレードオフの関係にあり、文字数ベースは簡単だが不正確、トークンベースは正確だが tokenizer が必要です。ワークロードに応じて、以下の 3 方式から選択します。
| 方式 | 例 | 適用場面 |
|---|---|---|
| 固定(文字数/バイト) | 40KB | 簡単。MCP サーバ実装で多い |
| 固定(トークン数) | 4000 トークン | より正確だが tokenizer が必要 |
| 動的(残予算ベース) | 残予算の 30% | Copilot Chat の prompt-tsx 方式 |
要素 3:通知メッセージの書き方
トリミングが発生した際、「切られたこと」と「元データへのアクセス方法」を LLM に伝える通知を作成します。LLM が読むのは結果文字列(content)だけなので、この通知を content 内に構造化データとして埋め込むことで、LLM が自律的に「復元が必要」と判断できるようになります。
isTruncated: true
originalSize: 245678 chars
returnedSize: 40000 chars
returnedFraction: "16.3%"
truncationReason: "exceeded max char limit (40000)"
recoveryHint: |
Full content is available via the resource:
Use `lookup_full_content(resource_id="abc123")` to retrieve.
resourceId: "abc123"
通知メッセージ設計の原則:
- ✅ 構造化データ(JSON/YAML)で機械可読に
- ✅ 「トリミングされた」を文字列で明示
- ✅ 総量と部分量の両方 を示す
- ✅ 復元の具体的な手段 を書く(ツール名と引数の例まで)
要素 4:復元手段の選択肢
トリミングで切り離した情報に、LLM が後からアクセスするための手段を用意します。通知(要素 3)だけでは LLM は「切られた」と分かっても取り戻せないため、通知と復元手段はペアで設計します。用途に応じて、ページネーション・URI 経由・専用検索ツール・JSON 退避から選択します。
推奨:「ページネーション」を基本に、必要なら「専用検索ツール」を追加。
要素 5:要素間の整合性リスク
要素 1〜4 を個別に正しく実装しても、要素間の連携で整合性が崩れると結果ハンドリング全体が破綻します。以下は、単一要素のレビューでは検出しにくい要素間の整合性リスクです。設計レビュー・コードレビュー時のチェック観点としてご活用ください。
| 整合性リスク | 関係する要素 | 対策 |
|---|---|---|
| トリミング(要素 1)を実施したが退避(要素 4)を欠き、復元不能になる | 要素 1 × 要素 4 | 「トリミング」と「退避」を必ずセットで実装する |
| 通知(要素 3)で「復元可能」と提示したが、復元手段(要素 4)が未実装 | 要素 3 × 要素 4 | 通知に記載した手段は必ず実装し、結合テストで動作確認する |
| ツール・セッションごとに通知フォーマットが不統一になる | 要素 3(複数箇所) | 通知生成を共通処理に集約し、フォーマットを標準化する |
📌 要素 1〜4 の各ステップ単体の注意点は、それぞれの要素の説明に記載しています。本節(要素 5)は、それらを組み合わせたときに初めて顕在化する整合性リスクに絞っています。なお、Summarization の乱用や Token Budgeting の欠如など、記事全体を通じた設計判断レベルのアンチパターンは §6 で扱います。
Reversible Compaction vs Irreversible Summarization
業界研究(Manus Part 2、2025/12)で明確化された重要な区別です。
| 観点 | Reversible Compaction | Irreversible Summarization |
|---|---|---|
| 元データの保持 | 可(外部退避) | 不可 |
| Context 上の表現 | <Output saved to resource_id=abc> |
「ユーザーは X を求めている」等の要約 |
| 復元 | LLM が必要時に lookup ツール呼び出し | 不可能 |
| 情報損失 | ゼロ | あり |
| 推奨度 | 第一選択 | 最後の手段 |
重要な設計原則:まず Reversible で逃せないか考える。Irreversible は本当に元データが不要な場合のみ 使用します。
業務系での具体例
// ❌ Irreversible: 顧客データを要約してしまうと監査時に困る
return "顧客 A の購入履歴を要約: 主に書籍を購入";
// ✅ Reversible: 顧客データは退避、Context には参照のみ
var resourceId = _store.Save(customerData, ...);
return $"Customer A's purchase history: 1,247 records " +
$"summary={topCategories}. " +
$"Full data: lookup_customer_data(resource_id=\"{resourceId}\")";
業務系では監査・コンプライアンスの観点で Reversible が圧倒的に有利です。
Token Budgeting:用途(ルート)別予算配分
第 2 回では、長時間タスクにおいてツール呼び出し応答がトークン消費の75% を占めるという Manus 社の実運用観測データをご紹介しました。
本節では、これを踏まえて 運用前に「どの用途に何トークン割り当てるか」を予算として明示的に設計するアプローチをご紹介します。
つまり第 2 回が「実運用でどう消費されているか(観測)」を示していたのに対し、本節は「あらかじめどう割り当てるべきか(設計)」という 補完関係 にあります。
コンテキストウィンドウは容量に限りがある 有限資源 であり、「なんとなく詰め込む」のではなく 予算項目として計画的に配分する ことが重要です。
推奨配分(典型例:128K window)
Manus社が提示した実装の原則
Token Budgeting を実装する際は、以下の 3 つの原則を守ることが重要です。
これらは Manus 社の Part 2 論文および業界研究で提唱されている、本番運用でのコスト暴走を防ぐための実装ガイドラインです。
-
各用途(ルート)に上限を設定
「System Prompt には 4,000 トークンまで」「会話履歴は 60,000 トークンまで」など、用途ごとに上限を明示的に設定します。上限がないと、どこか一つの用途が肥大化した際に他の用途を圧迫し、コンテキスト全体が破綻します。
-
ハードリミット到達時は loudly fail(明示的にエラーを出す)
予算超過時に「静かに切り捨てる」(超過分を無言でトリミング)ではなく、例外を投げて明示的に失敗させることが重要です。静かに切り捨てると、LLM は「情報が足りない」ことに気づかず誤った推論を続けます。エラーを出すことで、開発者は予算設計の見直しを迫られます。
-
推論実行前に予算チェックを行う
LLM を呼び出す前に、各用途の現在の消費量を集計して予算内に収まっているか確認します。超過が予測される場合は、履歴の Compaction や Retrieved Context の絞り込みで動的に再配分します。呼び出し後にエラーになるより、事前に対処する方がコストもレイテンシも節約できます。
3. 💡 実装の方針と設計判断
サンプルコードの実装方針は以下の通りです。
設計判断 1:なぜ ResourceStore を「インターフェース経由」で実装するか
サンプルではインメモリ実装で十分ですが、本番では Redis / Blob Storage への置換を想定 して IResourceStorage インターフェースを介した設計にしています。「Reversible Compaction」の本質は「LLM に見せる文字列」と「実データ」を分離することです。インターフェースを明確化しておくことで、本番移行時のコストを下げられます。
設計判断 2:復元ツールを「LLM 向けの一級ツール」として公開する
LookupFullContent を [Description] 付きで公開することで、LLM が 自律的に「復元が必要」と判断して呼び出せる ようになります。通知(isTruncated: true)だけでは LLM は手段を持ちません。「通知 + 復元ツール」のペアで「LLM の自律的回復」が可能になります。
設計判断 3:境界(boundary)でトリミングする
文字数だけで Substring(0, 4000) すると、コードや JSON の途中で切れます。改行・段落・JSON 要素単位で切ることで 「読み取り可能な単位」を保持 します。
設計判断 4:通知メッセージを構造化 JSON にする
LLM は構造化 JSON を機械的に処理しやすいため、「次は lookup_full_content(resource_id="abc123") を呼べ」と一発で判断できます。監視用のログ収集・パース時にも便利です。
4. 💻 実装:C# / Microsoft Agent Framework (MAF)
4-1. ResourceStore — トリミング前の全文を一時保存
以下の実装は、サンプル用の PoC 実装です。本番運用では、IResourceStorage を実装した永続化バックエンド(Redis、Blob Storage 等)への置き換えが推奨されます(補章 C 参照)。
永続化の抽象化(インターフェース定義):
💻 IResourceStorage — 永続化バックエンドの抽象化(C#・約10行、クリックで展開)
// 🔑 サンプルではインメモリ実装、本番では Redis / Blob Storage 等に置換
public interface IResourceStorage
{
Task SaveAsync(string resourceId, string content,
IReadOnlyDictionary<string, object> metadata, CancellationToken ct = default);
Task<(string Content, IReadOnlyDictionary<string, object> Metadata)?> GetAsync(
string resourceId, CancellationToken ct = default);
}
本体実装:
💻 ResourceStore — 全文退避とページネーション取得のサンプル実装(C#・約55行、クリックで展開)
using System.ComponentModel;
using System.Text.Json;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
namespace ContextEngineering.ResultHandling;
/// <summary>
/// LLM に返す chunk と、その元になった full content の対応関係。
/// </summary>
public record ResourceContent(
string ResourceId, int Offset, int ReturnedChars,
int TotalChars, bool HasMore, string Content);
/// <summary>
/// Reversible Compaction の中核:トリミング前の full content を保存。
/// </summary>
public class ResourceStore
{
private readonly IResourceStorage _storage;
public ResourceStore(IResourceStorage storage) => _storage = storage;
public async Task<string> SaveAsync(
string content, Dictionary<string, object> metadata, CancellationToken cancellationToken = default)
{
// 🔑 8 文字の短い ID を生成(LLM が扱いやすい長さ)
// → 32 文字の full UUID だと description に書きにくい
var resourceId = Guid.NewGuid().ToString("N")[..8];
await _storage.SaveAsync(resourceId, content, metadata, cancellationToken);
return resourceId;
}
public async Task<ResourceContent?> GetAsync(
string resourceId, int offset = 0, int limit = 4000, CancellationToken cancellationToken = default)
{
var entry = await _storage.GetAsync(resourceId, cancellationToken);
if (entry is null) return null;
var content = entry.Value.Content;
// 🔑 安全な範囲計算(境界を超えないように)
var safeOffset = Math.Min(offset, content.Length);
var safeLimit = Math.Min(limit, content.Length - safeOffset);
var chunk = content.Substring(safeOffset, safeLimit);
// 🔑 HasMore で「まだ続きがある」ことを LLM に伝える
// → LLM は次に offset=Offset+ReturnedChars で呼ぶべきと判断できる
return new ResourceContent(
ResourceId: resourceId,
Offset: safeOffset,
ReturnedChars: chunk.Length,
TotalChars: content.Length,
HasMore: safeOffset + safeLimit < content.Length,
Content: chunk
);
}
}
4-2. 復元用ツール(LLM が呼ぶ)
💻 ResourceLookupTool — トリミング済み全文を復元するツール(C#・約30行、クリックで展開)
public class ResourceLookupTool
{
private readonly ResourceStore _store;
public ResourceLookupTool(ResourceStore store) => _store = store;
[Description(
// 🔑 description の核心:「いつ使うか」を明示
// → LLM が isTruncated=true を見たら反射的にこれを呼べるように
"Retrieve full content of a previously truncated tool result. " +
"Use this when a previous tool result was marked with isTruncated=true. " +
"Pass the resource_id from the truncation notice."
)]
public async Task<string> LookupFullContentAsync(
[Description("Resource ID from truncation notice")] string resourceId,
[Description("Starting offset")] int offset = 0,
[Description("Max chars to return")] int limit = 4000)
{
var result = await _store.GetAsync(resourceId, offset, limit);
// 🔑 失敗時もエラーメッセージで返す(例外を投げない)
// → LLM が「resource_id が無効」と認識して別の手を打てる
return result == null
? JsonSerializer.Serialize(new { error = $"resource_id '{resourceId}' not found" })
: JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true });
}
}
4-3. 境界トリミングと通知
💻 BoundaryTruncation — 境界トリミングと構造化通知のサンプル実装(C#・約55行、クリックで展開)
public static class BoundaryTruncation
{
/// <summary>
/// 指定した境界(改行など)で切り取る。
/// </summary>
public static (string Truncated, int OriginalSize) TruncateWithBoundary(
string content, int maxChars, char boundary = '\n')
{
// 🔑 そもそも上限以下ならそのまま返す
if (content.Length <= maxChars) return (content, content.Length);
// 🔑 maxChars 以内の最後の境界(改行)を探す
// → 例: 4000 文字以内の最後の \n
var cutoff = content.LastIndexOf(boundary, Math.Min(maxChars, content.Length - 1));
// 🔑 境界が見つからなければ maxChars でハード切断
if (cutoff == -1) cutoff = maxChars;
return (content[..cutoff], content.Length);
}
/// <summary>
/// LLM 向けの構造化通知を生成。
/// </summary>
public static string BuildTruncationNotice(
int originalSize, int returnedSize, string resourceId, string toolName)
{
var fraction = originalSize > 0 ? (returnedSize / (double)originalSize) * 100 : 0;
// 🔑 匿名型で構造化通知を作る
// → 後で MAF が JSON シリアライズしてくれる
var notice = new
{
isTruncated = true, // LLM が条件分岐できるフラグ
tool_name = toolName, // どのツールが生成したか
original_chars = originalSize,
returned_chars = returnedSize,
returned_fraction_percent = Math.Round(fraction, 1), // 何 % が返されたか
truncation_reason = "exceeded max char limit",
// 🔑 復元手段を「実際に呼べる関数呼び出し形式」で明示
recovery_hint =
$"Full content is available. Call " +
$"`LookupFullContent(resourceId=\"{resourceId}\", offset=<int>, limit=<int>)`.",
resource_id = resourceId,
};
var json = JsonSerializer.Serialize(notice, new JsonSerializerOptions { WriteIndented = true });
// 🔑 通知を区切り線で囲む
// → LLM が「ここから先がメタデータ」と認識しやすい
return $"\n\n--- TRUNCATION NOTICE ---\n{json}\n";
}
}
4-4. ツールラッパパターン
💻 TruncatingToolWrapper — 全文退避・境界トリミング・通知を統合するラッパー(C#・約40行、クリックで展開)
public class TruncatingToolWrapper
{
private readonly ResourceStore _store;
private readonly int _maxChars;
private readonly char _boundary;
public TruncatingToolWrapper(ResourceStore store, int maxChars = 4000, char boundary = '\n')
{
_store = store;
_maxChars = maxChars;
_boundary = boundary;
}
public async Task<string> WrapAsync(string toolName, string content, CancellationToken cancellationToken = default)
{
// 🔑 まず境界で切る
var (truncated, originalSize) = BoundaryTruncation.TruncateWithBoundary(
content, _maxChars, _boundary);
// 🔑 そもそも切られなかったらそのまま返す(通知不要)
if (truncated.Length == originalSize) return content;
// 🔑 full content を ResourceStore に保存(Reversible にするため)
var resourceId = await _store.SaveAsync(content, new Dictionary<string, object>
{
["tool"] = toolName,
["original_size"] = originalSize,
}, cancellationToken);
// 🔑 通知メッセージを生成
var notice = BoundaryTruncation.BuildTruncationNotice(
originalSize, truncated.Length, resourceId, toolName);
// 🔑 truncated 本文 + 通知 を結合して返す
// → LLM は本文を読んでから通知でメタ情報を取得
return truncated + notice;
}
}
4-5. Token Budget Guard
💻 TokenBudget — ルート別トークン予算と超過検知のサンプル実装(C#・約65行、クリックで展開)
/// <summary>
/// ルート別にトークン予算を管理。
/// 超過時に「loudly fail」させて、本番でのコスト暴走を防ぐ。
/// </summary>
public class TokenBudget
{
// 🔑 ルート別に予算と消費量を別管理
// → "system_prompt"、"tool_definitions" などの単位
private readonly Dictionary<string, int> _budgets = new();
private readonly Dictionary<string, int> _used = new();
// 🔑 並列ツール呼び出し(第 5 回)での同時消費による予算超過見逃しを防ぐため lock で排他制御
private readonly object _lock = new();
public void SetBudget(string route, int maxTokens)
{
lock (_lock)
{
_budgets[route] = maxTokens;
_used[route] = 0;
}
}
public void Consume(string route, int tokens)
{
lock (_lock)
{
if (!_budgets.TryGetValue(route, out var budget))
throw new InvalidOperationException($"Budget for '{route}' not set");
_used[route] = _used.GetValueOrDefault(route) + tokens;
// 🔑 「fail loudly」原則
// → 静かに切り捨てるより例外で知らせる方が運用しやすい
if (_used[route] > budget)
throw new InvalidOperationException(
$"Token budget exceeded for '{route}': {_used[route]} > {budget}");
}
}
public int Remaining(string route)
{
lock (_lock)
return _budgets.GetValueOrDefault(route) - _used.GetValueOrDefault(route);
}
}
public static class BudgetExample
{
public static TokenBudget BuildDefault()
{
var budget = new TokenBudget();
budget.SetBudget("system_prompt", 4000);
budget.SetBudget("tool_definitions", 4000);
budget.SetBudget("retrieved_context", 16000);
budget.SetBudget("history", 60000);
budget.SetBudget("user_input", 4000);
budget.SetBudget("output_reserved", 40000);
return budget;
}
}
5. 📊 ケーススタディ
主要 3 ツールの実装比較
主要 3 ツールの結果ハンドリング戦略を比較すると、Reversibility に対するアプローチが最も差が出るポイントです。
| ツール | トリミング戦略 | 復元手段 | Reversibility |
|---|---|---|---|
| VS Code Copilot Chat |
isTruncated フラグ + 復元 URI |
read_file 経由(バグあり) |
Reversible 設計 |
| Codex CLI | サイズ閾値で truncate | offset/limit ページネーション | 安定、シンプル |
| Claude Code | サイズ閾値で truncate + サマリ | 専用検索ツール経由 | 一部 Irreversible |
教訓:
- VS Code Copilot Chat は Reversible を志向するも、復元 URI が実装レベルで機能しないバグ(Issue #311068)を抱えています。設計思想と実装の乖離が読者への警鐘となります(§2「動かない復元手段」参照)
- Codex CLI は offset/limit ページネーションというシンプルな Reversible 手段に絞り、実装の安定性を優先しています。業務系での参考実装として最も真似しやすい設計です
- Claude Code は一部で Summarization(Irreversible)を採用しており、監査要件が厳しい業務系ではそのまま採用できない部分があります。本記事の §1「Reversible 優先」原則と一致しない箇所です
つまり、「Reversible 設計を目指すことは業界標準になりつつあるが、実装は難しい」 というのが 2026 年 6 月時点の実態です。業務系開発者は Codex CLI 流のシンプルな Reversible 実装から始めることをお勧めします。
MCP コミュニティのデファクトパターン
MCP エコシステムの中でも、doobidoo/mcp-memory-service の Response Size Limiter は コミュニティで参考にされる代表的な実装 です。設計判断のポイントは以下の通りです。
| 要素 | 実装 | 設計判断のポイント |
|---|---|---|
| 閾値設定 | 環境変数 MCP_MAX_RESPONSE_CHARS(デフォルト 40KB) |
環境変数で外部化することで、ワークロードごとに調整可能 |
| 切る単位 | 意味のある境界で切る(このサービスでは 1 メモリエントリ単位) | 本記事のベストプラクティス「意味のある境界で切る」に一致 |
| 通知 | format 時に warning header を追加 | LLM に「トリミングされた」旨を明示、§2 のパターンと一致 |
| 余裕 |
MEMORY_OVERHEAD_CHARS = 200 をフォーマット用に確保 |
通知メッセージ用のバッファを予め確保、実装上の工夫 |
教訓: 業界の第一線の MCP サーバー実装でも、本記事で解説した 「境界で切る」「トリミングを LLM に明示する」「バッファを確保する」 という基本パターンが実装されています。逆に言えば、これらのパターンを守らない MCP サーバーは、コミュニティで参考にされにくい状態です。
6. ⚠️ アンチパターン
本記事で扱った結果ハンドリングの要点を、実装時のセルフレビュー用チェックリスト(アンチパターン)としてまとめました。
実装時は本表を「やってはいけないことリスト」としてご参照ください。特に #5・#6・#7 は業務系の監査要件やコスト暴走、実装事故に直結する重要項目です。
| # | アンチパターン | 根拠 |
|---|---|---|
| 1 | 通知をサイドチャネル(メタデータ)で渡す | LLM のプロンプトに含まれず無効化される |
| 2 | 復元手段を提示しない | LLM が重要情報を諦めて誤った推論を続ける |
| 3 | 途中の文字でトリミングする | 意味のない位置で切れ、LLM を錯乱させる |
| 4 | 通知フォーマットを毎回変える | LLM の学習済みパターンが機能しなくなる |
| 5 | すぐに Summarization で済ます | 元データに戻れず、監査要件に対応できない |
| 6 | 動かない復元手段を案内する | Issue #311068 の教訓、無限ループを誘発 |
| 7 | Token Budgeting なしで運用する | 予算超過が可視化されず、コスト暴走 |
おわりに
本記事では、ツール出力の制御において欠かせない「Reversible Compaction」の考え方と、クライアント側でのトリミング・通知・復元の実装パターンをご紹介しました。
特に「復元可能な形でデータを退避してから参照のみをコンテキストに残す」という発想は、業務系での監査要件とも相性が良く、実践的な価値があります。次回は「履歴圧縮」として、セッションが長くなったときの会話履歴の扱いについて解説します。
参考文献
VS Code Issue / 仕様
- VS Code Issue #311068「MCP tool results for large files are silently truncated with a vscode-chat-response-resource:// hint that does not work」
- microsoft/vscode-copilot-release Issue #9718「Large tool results lead to looping」(リポジトリは 2026/3/23 にアーカイブ済み)
- GitHub Community Discussion #169224「Handling large text output from MCP server」
MCP コミュニティ実装
- doobidoo/mcp-memory-service — Response Size Limiter (v9.0.0 PR #344)
- doobidoo/mcp-memory-service「Response Size Limiter」DeepWiki
- Model Context Protocol (MCP) 公式サイト
Microsoft / Microsoft Agent Framework 公式
-
Microsoft Learn「agent_framework.ContextProvider class」(Python API リファレンス)
- 補足: 概念解説は Microsoft Learn「Context Providers」、Python サンプルは
02-agents/context_providers/を参照。
- 補足: 概念解説は Microsoft Learn「Context Providers」、Python サンプルは
- microsoft/agent-framework — GitHub repository
業界研究
- Philipp Schmid (2025/12/4). "Context Engineering for AI Agents: Part 2" — Reversible Compaction vs Irreversible Summarization
- SurePrompts (2026/4/19, updated 2026/4/22).「Context Engineering Best Practices (2026): A 12-Point Checklist」— Token Budgeting の出典