MCPサーバーを何十本も「使って」きた経験を、「作る側」の知識に反転させる週。後半は構造化データ抽出(D4)——スキーマ設計・few-shot・検証リトライ・バッチの4点セット。合わせて配点38%をカバーする。
MCPサーバーの正体は「ツールのカタログと実装をまとめて、標準の通信規約(Model Context Protocol)で公開する小さなプログラム」。Python なら公式SDKで20行で書ける。
# 最小のMCPサーバー(天気ツール1個)
# pip install "mcp[cli]"
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("weather") # サーバー名
@mcp.tool()
def get_weather(city: str) -> str:
"""指定した都市の現在の天気を返す。
都市名は日本語・英語どちらでも可(例: 東京, Osaka)。
現在の天気・気温の質問に使う。過去の天気には get_weather_history を使う。
"""
# ↑ この docstring がそのまま「ツールの説明文」としてAIに渡る(D2.1の主戦場)
return f"{city}: 晴れ、28度"
if __name__ == "__main__":
mcp.run() # 標準入出力で通信(Claude Code から接続できる)
上のdocstringに詰めた4要素:何をするか/入力の形式と例/エッジケース/境界(いつ使い・いつ使わないか)。「過去の天気には get_weather_history を」の1行が誤ルーティングを防ぐ肝。
@mcp.tool()
def get_weather(city: str) -> dict:
"""(説明文は同じ)"""
if not weather_api.is_alive():
return { # 一時的な障害 → リトライする価値あり
"error": True,
"errorCategory": "transient",
"isRetryable": True,
"message": "天気サービスが応答しません。少し待って再試行してください。",
}
result = weather_api.lookup(city)
if result is None:
return { # 入力の問題 → リトライしても無駄
"error": True,
"errorCategory": "validation",
"isRetryable": False,
"message": f"都市 '{city}' が見つかりません。表記を確認してください。",
}
return {"city": city, "weather": result} # 成功
# 重要な区別: 「検索は成功したが該当0件」はエラーではなく正常な結果として返す
1体のエージェントに渡すツールは役割に必要な4〜5個まで。18個渡すと選択の信頼性が落ちる。高頻度の単純ニーズには機能を絞った専用ツール(例:統合係に verify_fact だけ渡す)、複雑ケースはコーディネーター経由——「85/15の分離」が模範解答の型。
// .mcp.json(プロジェクト直下・チーム共有・git管理)
{
"mcpServers": {
"weather": {
"command": "python",
"args": ["servers/weather.py"],
"env": { "WEATHER_API_KEY": "${WEATHER_API_KEY}" } // ← 直書き禁止。環境変数展開
}
}
}
// 個人の実験用サーバーは ~/.claude.json 側に書く(チームに配らない)
W1のSTEP 7(tool_choice強制でJSONを取る)が土台。その上のスキーマ設計の試験ポイントは3つ:
"due_date": {"type": ["string", "null"]}。requiredにすると、モデルが「埋めなきゃ」と値を捏造する。「無いものは無い」と言える設計が捏造防止"category": {"enum": ["請求", "返品", "other"]} + "category_detail": {"type": "string"}。想定外を無理やり既存カテゴリに押し込ませない。曖昧ケース用に "unclear" を足すのも同じ発想# retry-with-error-feedback の骨格
def extract_with_retry(document, max_retries=2):
result = extract(document) # tool_choice強制で抽出(W1 STEP7)
for _ in range(max_retries):
errors = validate(result) # Pydantic等で意味の検証
if not errors:
return result
# ★ 具体的なエラー内容を添えて再依頼するのが肝
result = extract(
document,
feedback=f"前回の抽出はこの検証に失敗した: {errors}。"
f"前回の出力: {result}。エラーを修正して再抽出せよ。"
)
return flag_for_human_review(result) # 直らなければ人間レビュー行き
conflict_detected: true を立てる——data-accuracy の合計突合と同じ技detected_pattern(何を根拠に判定したか)を出力に含めると、後で「どのパターンが誤検知源か」を集計できる# Message Batches API の骨格
batch = client.messages.batches.create(
requests=[
{
"custom_id": f"doc-{i}", # ★ 結果との対応付けに必須
"params": { "model": "claude-haiku-4-5", "max_tokens": 1024,
"messages": [...], "tools": [...], "tool_choice": {...} },
}
for i, doc in enumerate(documents) # 100件まとめて投入
]
)
# → 後でポーリングして結果回収。失敗した custom_id だけ修正して再投入
暗記3点セット+判断1つ:
私(Claude)に「W3演習やる」と言えば伴走する。
get_weather_history を足し、境界を書き分けた説明文で「昨日の東京の天気」「今の東京の天気」が正しく振り分けられるかを Claude Code に接続してテストする