
MCP のツール出力をモデルに渡す前に正規化する — PostToolUse フックパターン
はじめに
複数の MCP サーバーを接続した AI エージェントを運用していると、こんな状況に出くわしたことはないでしょうか?
- CRM の MCP サーバーは Unix エポック(
1718409600)でタイムスタンプを返す - チケットシステムは ISO 8601(
2026-06-15T09:00:00Z) - レガシーな社内 API は
MM/DD/YYYY形式
3つのツールから返ってくる日付の形式がバラバラで、エージェントの日付処理が一貫しない。「システムプロンプトに正規化ルールを書けばいいのでは?」と考えて指示を追加しても、数%のケースでまだ失敗する——という問題です。
この記事では、この問題に対する確実な解決策として PostToolUse フック による正規化パターンを紹介します。
なぜシステムプロンプトでの正規化は不十分なのか
システムプロンプトに「すべての日付を ISO 8601 に変換してから処理してください」と指示を追加するアプローチには、根本的な限界があります。
| アプローチ | 成功率 | 問題点 |
|---|---|---|
| 指示なし | ~80% | モデルが形式を推測して処理 |
| プロンプトに正規化指示を追加 | ~92% | LLM の処理は確率的。複雑な入力で失敗しうる |
| few-shot 例を追加 | ~95% | 改善するが 100% にはならない。トークン消費も増加 |
| PostToolUse フックで正規化 | 100% | コードによる決定論的変換。失敗しない |
LLM は本質的に確率的な処理を行います。「Unix エポックを ISO 8601 に変換する」のようなルールベースの変換は、モデルが得意な仕事ではありません。コードで確実に解ける問題を、わざわざ確率的な推論に任せる理由はないのです。
PostToolUse フックとは
PostToolUse フックは、MCP ツールの実行結果がモデルに渡される前に介入できる仕組みです。処理の流れは以下の通りです:

モデルには常に正規化済みのデータだけが渡るため、プロンプトに変換ルールを書く必要がなくなります。
実装例: タイムスタンプの正規化
以下は、3種類の日付形式を ISO 8601 に統一する PostToolUse フックの実装例です。
from datetime import datetime, timezone
import re
def post_tool_use_hook(tool_name: str, tool_output: dict) -> dict:
"""MCP ツール出力のタイムスタンプを ISO 8601 に正規化する"""
return normalize_timestamps(tool_output)
def normalize_timestamp(value: str) -> str:
"""個別の値を ISO 8601 に変換する"""
# Unix epoch (例: "1718409600", "1718409600.123")
if re.fullmatch(r"\d{10}(\.\d+)?", value):
dt = datetime.fromtimestamp(float(value), tz=timezone.utc)
return dt.isoformat()
# MM/DD/YYYY (例: "06/15/2026")
try:
dt = datetime.strptime(value, "%m/%d/%Y").replace(tzinfo=timezone.utc)
return dt.isoformat()
except ValueError:
pass
# ISO 8601 はそのまま通す (例: "2026-06-15T09:00:00Z")
try:
datetime.fromisoformat(value.replace("Z", "+00:00"))
return value
except ValueError:
pass
# 未知の形式はそのまま返す
return value
def normalize_timestamps(obj):
"""再帰的にツール出力内の全フィールドを走査して正規化する"""
if isinstance(obj, str):
return normalize_timestamp(obj)
if isinstance(obj, dict):
return {k: normalize_timestamps(v) for k, v in obj.items()}
if isinstance(obj, list):
return [normalize_timestamps(item) for item in obj]
return obj
動作例
# 正規化前: 各 MCP サーバーからの生の出力
raw_outputs = {
"crm": {"customer": "田中太郎", "last_contact": "1718409600"},
"tickets": {"id": "TK-1234", "created": "2026-06-15T09:00:00Z"},
"legacy": {"order_date": "06/15/2026", "status": "shipped"},
}
# 正規化後: モデルに渡るデータ
for source, output in raw_outputs.items():
normalized = post_tool_use_hook(source, output)
print(f"{source}: {normalized}")
# crm: {"customer": "田中太郎", "last_contact": "2024-06-15T00:00:00+00:00"}
# tickets: {"id": "TK-1234", "created": "2026-06-15T09:00:00Z"}
# legacy: {"order_date": "2026-06-15T00:00:00+00:00", "status": "shipped"}
モデルが受け取るデータは常に統一された形式になります。
このパターンが有効な他のケース
PostToolUse フックによる正規化は、日付以外にも応用できます。
| ケース | 正規化内容 |
|---|---|
| 通貨の形式統一 | "$1,234.56" / "1234.56 USD" / "¥1234" → 統一フォーマット |
| 電話番号の正規化 | "090-1234-5678" / "+819012345678" → E.164 形式 |
| ステータス値のマッピング | "active" / "ACTIVE" / "1" / true → 統一された列挙値 |
| PII のマスキング | メールアドレスや電話番号をマスク → モデルに個人情報を渡さない |
| レスポンスサイズの制限 | 巨大な配列を切り詰め → トークン消費を削減 |
原則: 決定論的に解ける問題を確率的推論に任せない
この記事の核心は、タイムスタンプの正規化そのものではなく、どの処理をモデルに任せ、どの処理をコードで解くべきかという判断基準にあります。
コードで解くべき処理:
- フォーマット変換(日付、通貨、電話番号)
- データのバリデーション
- 型変換(文字列 → 数値)
- 決まったルールに基づくマッピング
モデルに任せるべき処理:
- 自然言語の理解と生成
- 文脈に応じた判断
- 曖昧な要件の解釈
- 複数のツールを組み合わせた推論
この線引きを意識するだけで、エージェントの信頼性は大きく向上します。
まとめ
- 複数の MCP サーバーからのデータ形式の不一致は、プロンプトではなくコードで解決する
- PostToolUse フックを使えば、モデルに渡る前にツール出力を確実に正規化できる
- 「決定論的に解ける問題を確率的推論に任せない」という原則は、MCP エージェント設計全般に適用できる




