目次
なぜスキーマ設計がMCPサーバーの品質を支配するのか
2026年に入って、MCPサーバーの実装品質は『機能の多さ』ではなく『ツールスキーマ設計の精度』で評価されるようになった。理由はシンプルで、エージェントから見ると ツール定義そのものがプロンプトの一部としてコンテキストに常駐するからだ。
Anthropicのエンジニアリングブログは2026年に 「最適化前のツール定義は134Kトークンを消費していた」と公開し、MCP Tool Search(lazy loading機構)導入後は約5Kトークン、約85%削減を達成したことを示した。MindStudio等の業界調査も「ツール定義1個あたり通常100〜500トークン、5サーバー58ツール構成で55K、Jira単体で約17K」というデータを公表している。スキーマ設計を間違えると、エージェントの会話開始前から100K以上のトークンを焼くサーバーが完成する。
(1) トークン消費の削減 — verbose descriptionは大幅に高コスト。
(2) tool selection精度の向上 — Anthropic engineering blogでは『tool selection精度43%→14%』のようにスキーマ品質低下で精度が大きく劣化する事例が報告されている。
(3) ホストアプリでの自動承認/手動承認の最適化 — annotationsを付けないとClaude Connector Directoryで30%のrejection原因になる。
原則1: 命名は動詞+目的語、サーバー内で命名規則を統一
MCP公式仕様はnameの命名規則を明示的に強制していないが、業界ベストプラクティスは『snake_case または camelCase で、動詞+目的語』に集約されている。LLMから見ると、動詞先頭の命名は『何をするツールか』の判定を高速化する。
最も重要なのは『サーバー内で命名規則を統一する』こと。同一サーバーで database_query と createInvoice が混在していると、LLMが暗黙にカテゴリ分けしてしまい、tool selection精度が落ちる。
// ✅ 良い例(snake_case で統一、動詞+目的語)
{
"name": "search_customers",
"name": "create_invoice",
"name": "send_email"
}
// ❌ 悪い例(命名規則がバラバラ、動詞抜け)
{
"name": "customers", // 動詞がない
"name": "InvoiceCreate", // PascalCase + 語順逆
"name": "send-email-v2" // kebab-case + version suffix
}
原則2: descriptionは『何を』+『いつ使うか』を200〜400文字で
descriptionは『機能の説明』だけでなく『いつそのツールを使うべきか』を必ず含める必要がある。LLMはdescriptionからtool selectionを行うため、機能だけ書かれた「APIで顧客一覧を返します」のようなdescriptionでは、似た機能の別ツールと混同される。
長さの目安は1〜3文・200〜400文字。verbose multi-paragraph description はトークン削減ボトルネックの最大要因と認識されている。凝縮する4要素:
- 動作 — 何をするか
- 入力前提 — 何が必要か(認証スコープ・前提条件)
- 出力期待 — 何が返るか(型・件数上限など)
- 使い分け判断 — どんな時に使うべきか/使わざるべきか
// ✅ 良いdescription(4要素を凝縮)
"description": "顧客一覧を検索する。query/limit/offsetをサポート。
最大100件返却、ページング必須。
個別顧客の詳細はget_customer_byIdを使うこと。
新規作成や更新には使えない(read-only)。"
// ❌ 悪いdescription(機能だけ、verbose)
"description": "This tool returns a list of customers from our
comprehensive customer database management system,
which has been built with industry-leading technology...
(200文字以上続く一般論)"
原則3: inputSchemaは可能な限りフラットに
JSON Schemaは深いネストと複雑なバリデーションロジックをサポートするが、MCPツールのinputSchemaでは『可能な限りフラットに』が原則。深くネストするほど(1) トークン数が増え、(2) LLMの認知負荷が増え、(3) パース失敗の確率が上がる。
複雑なオブジェクト階層が必要な場合の選択肢は2つ:
- 引数を単純な型に分解する — ネストオブジェクトをトップレベルの引数に展開する
- 機能を複数の具体的ツールに分割する — 1つの汎用ツールより、専用ツール3つの方がtool selection精度が高い
// ❌ 悪い例(深いネスト、トップレベルで複雑なobject)
{
"type": "object",
"properties": {
"filters": {
"type": "object",
"properties": {
"date_range": {
"type": "object",
"properties": {
"start": { "type": "string" },
"end": { "type": "string" }
}
},
"tags": { "type": "array", "items": {...} }
}
}
}
}
// ✅ 良い例(フラット、必須/任意が明確)
{
"type": "object",
"properties": {
"start_date": { "type": "string", "format": "date" },
"end_date": { "type": "string", "format": "date" },
"tags": { "type": "array", "items": { "type": "string" } }
},
"required": ["start_date", "end_date"]
}
原則4: tool annotationsで挙動を宣言する
2025年に追加され2026年に標準化された tool annotations は、ツールの挙動を宣言するメタデータだ。サブ仕様としては readOnlyHint / destructiveHint / idempotentHint / openWorldHint の4種があり、これらの欠落がClaude Connector Directory rejectionの30%を占めるという公表データもある。
| Annotation | デフォルト | true にすべきツール | 用途 |
|---|---|---|---|
| readOnlyHint | false | search / get / list / fetch系 | 状態を変更しない読み取り専用 |
| destructiveHint | true | create / update / delete / send系 | 取り消し困難な操作 |
| idempotentHint | false | 同じ入力で結果が変わらない操作 | リトライ安全性の宣言 |
| openWorldHint | true | 外部リソース(API/DB/ネット)へ届く全ツール | 外部影響範囲の宣言 |
// 検索ツール(read-only)
{
"name": "search_customers",
"description": "...",
"inputSchema": {...},
"annotations": {
"readOnlyHint": true,
"destructiveHint": false,
"idempotentHint": true,
"openWorldHint": true
}
}
// 削除ツール(destructive)
{
"name": "delete_invoice",
"annotations": {
"readOnlyHint": false,
"destructiveHint": true,
"idempotentHint": false,
"openWorldHint": true
}
}
annotationsは情報的シグナルであって強制可能な保証ではない。「このツールはこう動くと主張している」を伝えるだけで、システム側が動作を保証するわけではない。MCPがcontract(契約)ではなくhint(ヒント)を選んだのは、信頼できないサーバーをまたいで契約を強制することが現実的でないため。クライアント側は依然として独自の安全チェックを実装すべき。
注意点: 「検索」を名乗るツールでも、検索クエリを分析用DBに書き込んでいるならreadOnlyHint:trueにしてはいけない。主目的が読み取りでも、副作用で書き込みが発生するなら read-only ではない。同様にdestructiveは「データ削除」だけでなく、ファイル上書き・トークン無効化・issue close など『簡単に取り消せない操作』全般を指す。
原則5: エラーは result 内に返す、protocol error にしない
MCPのエラー設計には大きな落とし穴がある。ツール実行のエラーは、MCPプロトコルレベルのエラー(JSON-RPC errorフィールド)ではなく、result オブジェクト内に isError: true + メッセージで返すのがベストプラクティスだ。
理由は明確で、protocol errorとして返すとLLMはエラー内容を見られず、何が起きたか判断できない。result内のエラーとして返せば、LLMは『どんな失敗だったか』を読み、リカバリーを試みられる。
// ❌ 悪い例(protocol error として返却)
// LLMはこのエラー内容を見られないため、ただ「失敗した」しか分からない
{
"jsonrpc": "2.0",
"id": 1,
"error": { "code": -32603, "message": "Database connection failed" }
}
// ✅ 良い例(result内に isError + 構造化メッセージ)
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"isError": true,
"content": [{
"type": "text",
"text": "Database connection failed. Retry-After: 5 seconds.
This is a transient error - the agent should retry
with exponential backoff (recommend 3 attempts)."
}]
}
}
原則6: 巨大ペイロードはURIで返す、result本体に埋め込まない
2026年のベストプラクティスとして強調されているのが、『巨大ペイロードはhandle/URIを返し、result本体にメガバイト級のデータを inline しない』というルール。理由は明確で、大量データをinlineするとそのままLLMのコンテキストに流れ込み、トークンを焼く。
典型パターン: ファイル取得・ログ取得・データセットクエリ。これらは件数が増えると一気にレスポンスが膨れる。MCP仕様にはResourcesという仕組みがあり、URIで参照させて必要な時だけ別途取得させる構造を作れる。
// ❌ 悪い例(10万行のログをそのままinline)
{
"result": {
"content": [{
"type": "text",
"text": "[2026-05-19 00:00] log line 1...
... (10万行続く、数MB) ..."
}]
}
}
// ✅ 良い例(URIで返し、必要に応じてresourceとして取得)
{
"result": {
"content": [{
"type": "resource",
"resource": {
"uri": "logs://2026-05-19/full",
"mimeType": "text/plain",
"description": "100K log lines (5.2MB). Use read_resource to fetch."
}
}]
}
}
原則7: 入力バリデーションと境界での検証を必ず実装
JSON Schemaでバリデーションを宣言するだけでは不十分で、サーバー側で必ず実装レベルの検証を行う必要がある。エージェントは時々スキーマを無視した呼び出しをしてくるし、悪意のあるユーザーがエージェント経由で不正入力を送ることもある。
必須の検証項目:
- 型と必須項目 — JSON Schema宣言に従って厳格に検証
- 長さと範囲 — string max_length、number min/max
- ファイルパス・コマンド・URL — インジェクション攻撃対策で必ずサニタイズ
- 認証スコープ — そのツールがそのscopeで呼び出される権限があるか確認
- レート制限の事前チェック — 上流APIに撃ち込む前にトークンバケットで止める
本番デプロイ前チェックリスト
新規MCPサーバーまたは大規模ツールアップデート前に必ず確認するチェック項目:
- 全ツールの name が snake_case または camelCase で統一されている
- 全ツールの name が動詞+目的語の形になっている
- 全ツールに 200〜400 文字の description がある(動作・入力前提・出力期待・使い分け)
- inputSchema は可能な限りフラット、深いネストがない
- 全ツールに4種の annotations(readOnlyHint / destructiveHint / idempotentHint / openWorldHint) が明示的に設定されている
- エラーは result 内に isError:true + 構造化メッセージで返している
- 巨大ペイロード返却箇所が Resources/URI 経由になっている
- サーバー側で入力バリデーション(型・長さ・サニタイズ・認証スコープ)を実装している
- tool definition 全体のトークン消費を計測した(目安: 10ツールで3K以下、サーバー全体で20K以下)
FAQ
MCPツール定義1つあたり、何トークン使われるのですか?
通常100〜500トークン、description次第で大きく振れます。10ツールの中堅サーバーで会話開始前から1,500〜3,000トークン消費、5サーバー58ツールで約55K、Jira単体で約17Kに達した実例があります。Anthropic engineering blogは『最適化前のツール定義は134Kトークン消費していた』を公表しています。
name は snake_case と camelCase どちらを使うべきですか?
MCP公式仕様はどちらも禁止していません。業界標準は『動詞+目的語』『サーバー内で統一』。同一サーバーで命名規則が混在するとtool selection精度が落ちるため、どちらを選ぶかよりも『統一すること』が重要です。
description はどれくらい詳しく書けばいいですか?
『何をするか』だけでなく『いつ使うべきか』を必ず含め、1〜3文・200〜400文字を目安にします。動作・入力前提・出力期待・使い分け判断の4要素を凝縮するのが2026年のスイートスポット。verbose multi-paragraph はトークン削減のボトルネック最大要因です。
tool annotations を付けないとどうなりますか?
Claude Connector Directoryの申請rejection の30%が annotation 欠落を理由としていると公表されています。annotations なしだとクライアント側のリスク判定が効かず、(1)読み取り専用ツールが余計な確認ダイアログを出す、(2)破壊的ツールが警告なく実行される、(3)自動承認の最適化が効かない、といった問題が起きます。
エラー返却は protocol error と result の isError どちらを使うべきですか?
ツール実行エラーは result 内に isError:true + 構造化メッセージで返します。protocol error にするとLLMはエラー内容を読めず、リカバリーを試みられません。result 内エラーであればLLMは『どんな失敗だったか』を読んでリトライ判断ができます。
本記事のトークン数値はAnthropic engineering blog (anthropic.com/engineering/code-execution-with-mcp、anthropic.com/engineering/advanced-tool-use)、MindStudio記事 "Claude Code MCP Servers and Token Overhead"、Webfuse "MCP Cheat Sheet (2026)"、techbuddies.io "How Claude Code's New MCP Tool Search Slashes Context Bloat" 等の公開情報に基づきます。tool annotationsの仕様と『rejection 30%がannotation欠落』はMCP公式ブログ (blog.modelcontextprotocol.io/posts/2026-03-16-tool-annotations/) およびsunpeak.ai "Claude Connector Directory Submission" を参照。スキーマ設計原則は modelcontextprotocol.info公式ドキュメント、thenewstack.io "15 Best Practices for Building MCP Servers in Production"、apxml.com "Tool Definition Schema" 等から統合しました。サンプルコードは説明用の最小例で、実装時は対象言語のSDK公式仕様を必ず確認してください。