Claude CodeのMCPでルートレベルanyOf/oneOf/allOfを平坦化する仕組み
MCPツールのinputSchemaがルート直下でanyOf・oneOf・allOfを使うと、Claude Codeはスキーマを平坦化して説明文に条件を足します。ツールが消える条件とサーバー側の設計もまとめます。
MCPツールのinputSchemaをルート直下のanyOf・oneOf・allOfで書いても、Claude Codeはそのツールを捨てません。送信前にスキーマを1つのオブジェクトへ平坦化し、どのパラメータが組になるかをツールの説明文の先頭に1文で足します。ただしanyOfとoneOfでは、分岐ごとの必須項目がスキーマの制約として働かなくなります。
ツールが消えるのは、平坦化してもAPIが受け付けるスキーマにならないときや、書き換えを有効にする設定が届かない環境です。サーバーの設計では「条件の検証をサーバーに残す」ことが要になります。以下では、平坦化の中身、ツールが消える経路、サーバー側の設計、見えないときの切り分けの順に見ていきます。
ルート直下の合成キーワードはなぜ問題になるのか
JSON Schemaの合成キーワードとは、複数のスキーマを組み合わせるanyOf(いずれか1つ以上)、oneOf(ちょうど1つ)、allOf(すべて)の3つです。「idかnameのどちらかを必須にする」といったユニオン型の入力を書くときに使います。
MCPサーバーの中には、ツールの入力全体をこうしたユニオンで宣言するものがあります。次のようにスキーマのルートにanyOfを置く形です(構造を示すための例です)。
{
"type": "object",
"anyOf": [
{ "properties": { "id": { "type": "string" } }, "required": ["id"] },
{ "properties": { "name": { "type": "string" } }, "required": ["name"] }
]
}Claude APIは、スキーマのルートにこれらのキーワードを置くことを受け付けません。一方で、propertiesの中にネストした合成キーワードは受け付けます。Claude Codeはネスト側のスキーマをそのまま送ります。書き換えの対象になるのは、ルート直下にある場合だけです。
平坦化で何が変わるか
Claude Codeは、ルート直下に合成キーワードを持つツールをAPIへ送る前に、単一のオブジェクトスキーマへ書き換えます。同時に、どのパラメータが組になるかをClaudeへ伝える1文を、ツールの説明文の先頭に足します。propertiesは、どのキーワードでもすべての分岐が統合されます。分かれるのは各分岐のrequiredの扱いです。
分岐の必須項目はどう扱われるか
allOf
統合後も各分岐のrequiredがそのまま有効です。共通のrepoを要求する分岐とtitleを要求する分岐をallOfで束ねたツールなら、repoとtitleの両方が必須のままです。
anyOf / oneOf
分岐の選択がスキーマから消え、各分岐のrequiredは説明文の文章で記述されます。冒頭の例なら、idとnameはどちらもオプショナルなプロパティになり、「どちらか一方が必要」という条件は文章が担います。
説明文に足される1文の文面は、公式ドキュメントに例が載っていません。冒頭の例でClaudeに何と表示されるかを言い切れないので、Claudeが条件を読み落とす場合も前提にして設計します。
サーバーには、Claudeが選んだ引数がそのまま届きます。「どちらか一方が必須」のはずが、両方とも欠けた呼び出しや両方が入った呼び出しが届きうる、という前提で実装します。
ツールが消える3つの経路
平坦化は救済の仕組みですが、ツールが消える経路も残っています。公式のMCPページとエラー一覧を並べると、経路は3つに分かれます。
| 経路 | Claude Codeの動作 | 見え方 |
|---|---|---|
| 平坦化してもAPIが受け付けるスキーマにならない | Claude Codeの動作そのツールだけをスキップし、理由をサーバーのログに残す | 見え方同じサーバーの他のツールは使える |
| 書き換えを有効にするリモート設定が届かない環境 | Claude Codeの動作同じくそのツールだけをスキップ | 見え方同上 |
| 合成キーワードとは別に、検証の2項目に落ちる | Claude Codeの動作該当ツールを除外し、ログとClaudeへの通知に理由を残す | 見え方Claudeに理由を聞ける |
表の経路では、サーバー全体が使えなくなることはありません。ただし除外が効かない環境では、次節のとおり400エラーでリクエスト全体が失敗します。「サーバーは接続済みなのに一部のツールだけ無い」という症状は、このどれかである可能性が高くなります。
検証の2項目と、除外判定が効かない環境
Claude Codeは、ツール一覧を読み込むとき、Claude APIが個別に課している2つのチェックを自前でも実行します(v2.1.216以降。それより前は、どの環境でもこの検証は走りませんでした)。
- トップレベルのプロパティ名が1〜64文字で、使える文字はASCIIの英数字・
_・.・-だけ - スキーマがJSON Schema draft 2020-12のメタスキーマに適合している(
$schemaを省略した場合と2020-12を宣言した場合が対象。他の方言を宣言したスキーマはこのチェックを飛ばす)
この検証は書き換えとは別の仕組みで、ツール一覧の読み込み時に走ります。除外されたツールは、サーバー側でスキーマを直せば、次にClaude Codeがツール一覧を読み込むときに戻ります。
フラグの取得が止まる条件は、公式の環境変数ページに列挙されています。
DISABLE_GROWTHBOOK・DISABLE_TELEMETRY・DO_NOT_TRACK・CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFICのいずれかを設定したセッション- Amazon Bedrock、Google CloudのAgent Platform、Microsoft Foundryなどのサードパーティプロバイダー経由のセッション。ホスト側が
CLAUDE_CODE_PROVIDER_MANAGED_BY_HOSTを設定する場合は除く - Claude appsゲートウェイ経由のセッション
エアギャップ環境のようにフラグが一度も届いていないマシンでも、同じ挙動になります。同じMCPページには、2つの説明が並んでいます。書き換えはフラグ取得が止まっていても独自の挙動を保つという説明と、書き換えを有効にするリモート設定が届かない環境ではツールをスキップするという説明です。環境によって、平坦化の可否と検証の可否が別々に決まると考えておくと、症状と原因を結びやすくなります。
400エラーの読み方
除外が効かずにスキーマが送られると、エラー一覧にある次のような形で失敗します。
API Error: 400 ... tools.N.custom.input_schema: JSON schema is invalid
API Error: 400 ... tools.N.custom.input_schema.properties: Property keys should match pattern '^[a-zA-Z0-9_.-]{1,64}$'1つ目はスキーマがdraft 2020-12として不正なとき、2つ目はトップレベルのプロパティ名が引用されたパターンに合わないときです。Nはツール名ではなく、リクエスト内のツールの位置なので、名前では引けません。対処はエラー一覧に沿って、v2.1.216より前ならclaude update、それ以降ならサーバーのログから名指しされたツールを探します。ログにも出ていなければ、サーバーを1つずつ無効にして絞り込みます。方言の扱いに関わる部分はMCP outputSchemaがdraft-07で拒否される原因と対処法と合わせて読むと整理しやすくなります。エラー文言から原因を特定して直す手順はinput_schemaエラーの原因と直し方にあります。
サーバー側で取れる3つの設計
ルート直下の合成キーワードは、Claude Code経由では緩い制約に化けます。サーバーを書く側では、次の3つを検討します。
スキーマの書き方の選択肢
合成キーワードをpropertiesの中へ
ネストした合成キーワードは、Claude Codeがそのまま送ります。特定のプロパティが取りうる型のユニオン(文字列か数値か)なら、この配置で意図どおりに伝わります。
平坦なスキーマ+説明文
「
idかnameのどちらかを指定してください」と自分の言葉で書いておけば、平坦化に頼らずClaudeに伝わります。サーバー側で必ず検証する
どの設計でも最後の砦はここです。スキーマが緩んでも、呼び出しの組み合わせはサーバーが弾きます。
2つ目の説明文には長さの上限があります。Claude Codeは各ツールの説明文と各サーバーのinstructionsを、既定で2,048文字で切り詰めます。上限は環境変数CLAUDE_CODE_MAX_MCP_DESCRIPTION_LENGTHで変えられます(v2.1.280以降)。変え方はMCPツール説明文の文字数上限を変える方法で扱っています。平坦化で足される1文がこの文字数に含まれるかどうかは、公式の説明にありません。条件の文章は説明文の先頭に近い位置へ置くのが安全です。
3つ目の検証は、TypeScriptなら、たとえば次のような形になります(例示のための断片です)。
function resolveTarget(args: { id?: string; name?: string }) {
const hasId = args.id !== undefined;
const hasName = args.name !== undefined;
if (hasId === hasName) {
return {
isError: true,
content: [{ type: "text", text: "id か name のどちらか一方だけを指定してください" }],
};
}
return { id: args.id, name: args.name };
}oneOfの「ちょうど1つ」という条件は、平坦化後のスキーマからは消えます。両方欠けた場合と両方入った場合の2通りを、サーバーの結果で明示的に弾き、エラーの本文でClaudeに正しい呼び方を伝えます。Claudeはツール結果を読んで引数を直して再試行できるので、エラーメッセージの品質がそのまま復旧の速さになります。
ツールが見えないときの確認手順
ルート直下の合成キーワードが疑わしいときは、次の順で確認します。バージョンは次のコマンドで分かります。
claude --version出力は 2.1.289 (Claude Code) の形式です。
見えないツールの切り分け
- 1
バージョンを見る
v2.1.216より前なら、スキーマの検証が走っていません。400エラーが出ているなら、
claude updateが対処の1つ目です。 - 2
サーバーの接続状態を見る
claude mcp listと、セッション内の/mcpで、サーバーが接続済みかを見ます。claude mcp listは、承認済みのサーバーに接続してヘルスチェックを行います。claude mcp --helpの説明にも、承認済みサーバーは無効化されていない限りヘルスチェックされると書かれています。 - 3
Claudeに聞く
「そのツールはなぜ使えないのか」と聞きます。除外されたツールと理由はClaudeに伝わっています。書き換えができずにスキップされたツールの理由は、サーバーのログに残ります。
- 4
サーバーのログを読む
スキップや除外の理由が記録されます。バージョンを上げても消えないなら、次にスキーマの2020-12適合とプロパティ名を疑います。
自作サーバーのtools/list応答をJSONで保存できるなら、ルート直下に合成キーワードを持つツールはjqで洗い出せます(応答がresult.tools配列に入っている前提の例です)。
jq -r '.result.tools[]
| select(.inputSchema | has("anyOf") or has("oneOf") or has("allOf"))
| .name' tools-list.jsonここに名前が出たツールが、平坦化の対象です。
隣接するスキーマの落とし穴との関係
同じ「スキーマの書き方でClaudeの呼び出しが変わる」話でも、原因は別です。anyOf: [{"type": "string"}, {"type": "null"}]のようなnull許容型は、ネストした位置にある合成キーワードなのでClaude Codeが書き換えません。この型が原因で任意パラメータの省略が拒否される問題は、Claude CodeのMCPツールが任意パラメータ省略で拒否される原因と回避策にあります。
MCP仕様側で2020-12の全キーワードが使えるようになった経緯はMCPのツール定義がJSON Schema 2020-12の全機能に対応にまとまっています。仕様が広げた自由度のうち、ルート直下の合成キーワードはClaude API側の制約で書き換えが入る領域です。
まとめ
自作サーバーなら、条件付きの入力はネスト側か平坦なスキーマと説明文で表し、組み合わせの検証をサーバーに残す構成が、Claude Codeの書き換えやバージョンに左右されません。他人のサーバーで一部のツールだけ見えないなら、バージョン、サーバーのログ、スキーマの2020-12適合の順に当たると原因に近づけます。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。