Claude CodeのMCPでルートレベルanyOf/oneOf/allOfを平坦化する仕組み
MCPツールのinputSchemaがルート直下でanyOf・oneOf・allOfを使うと、Claude Codeはスキーマを平坦化して説明文に条件を足します。v2.1.195前後の違いも解説します。
MCPツールのinputSchemaをルート直下のanyOf・oneOf・allOfで書いても、Claude Codeはそのツールを捨てません。送信前にスキーマを1つのオブジェクトへ平坦化し、どのパラメータが組になるかをツールの説明文の先頭に1文で足します。ただしanyOfとoneOfでは、分岐ごとの必須項目がスキーマの制約として働かなくなります。
この挙動はv2.1.195で入りました。それより前のバージョンは、ルート直下に該当キーワードを持つツールを丸ごとスキップしていました。以下では、平坦化の中身、バージョンごとの違い、ツールが消えるときの条件、サーバー側でできる設計を順に見ていきます。
ルート直下の合成キーワードはなぜ問題になるのか
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 | propertiesすべての分岐を統合 | 各分岐のrequiredそのまま有効 |
anyOf | propertiesすべての分岐を統合 | 各分岐のrequiredスキーマでは強制せず、説明文で記述 |
oneOf | propertiesすべての分岐を統合 | 各分岐のrequiredスキーマでは強制せず、説明文で記述 |
allOfは「全分岐を満たす」という意味なので、統合しても条件が失われません。たとえば共通のrepoを要求する分岐と、titleを要求する分岐をallOfで束ねたツールなら、統合後もrepoとtitleの両方が必須のままです。anyOfとoneOfは分岐の選択がスキーマから消えます。上の例なら、idとnameの両方がオプショナルなプロパティとして残り、「どちらか一方が必要」という条件は説明文の文章が担います。
説明文に足される文の実際の書きぶりは、公式ドキュメントに載っていません。上の例でClaudeに何と表示されるかは、ここでは断定できません。分かっているのは、条件が説明文へ移るという動作そのものです。
サーバー側の帰結は、公式が明記しています。サーバーにはClaudeが選んだ引数がそのまま届くので、組み合わせの検証はサーバーで続ける必要があります。「どちらか一方が必須」のはずが、両方とも欠けた呼び出しや両方が入った呼び出しが届きうる、という前提で実装します。
v2.1.195の前後で挙動はどう違うか
| 状況 | v2.1.195より前 | v2.1.195以降 |
|---|---|---|
ルート直下にanyOf・oneOf・allOfを持つツール | v2.1.195より前該当するツールをすべてスキップ | v2.1.195以降平坦化して利用可能 |
| 平坦化してもAPIが受け付けるスキーマにならないツール | v2.1.195より前該当ツールはスキップ | v2.1.195以降そのツールだけスキップ |
| 書き換えの設定を受け取らない環境 | v2.1.195より前該当ツールはスキップ | v2.1.195以降そのツールだけスキップ |
v2.1.195より前のバージョンでは、ルート直下の合成キーワードを使ったツールは「サーバーは接続済みなのにツールが見えない」状態になっていました。サーバー全体が落ちるわけではなく、該当ツールだけが消えます。
v2.1.195以降でも、Claude Codeがスキーマを変換できなかった場合や、書き換えを有効にするリモート設定を受け取れない環境では、そのツール1本だけをスキップします。スキップの理由はサーバーのログに記録され、同じサーバーの他のツールはそのまま使えます。
書き換えを有効にする仕組みの詳細は、公式ドキュメントでは「リモート設定」としか書かれていません。次節の検証とは別系統の設定として扱われており、フィーチャーフラグ取得が無効な環境でも、この平坦化は独自の挙動を保つと明記されています。
平坦化のあとに走るスキーマ検証
Claude Codeは、ツール一覧を読み込むとき、Claude APIが個別に課している2つのチェックを自前でも実行します(v2.1.216以降)。
- トップレベルのプロパティ名が1〜64文字で、使える文字はASCIIの英数字・
_・.・-だけ - スキーマがJSON Schema draft 2020-12のメタスキーマに適合している(
$schemaを省略した場合と2020-12を宣言した場合が対象。他の方言を宣言したスキーマはこのチェックを飛ばす)
この検証は、ルート直下の合成キーワードの書き換えより後に、実際に送信するスキーマに対して行われます。合成キーワードで書いたスキーマが救済されるかどうかは、平坦化後のスキーマがこの2つを通るかで決まります。落ちたツールは除外され、除外したツールと理由がClaudeにも伝えられます。だから「あのツールが見えないのはなぜか」とClaudeに聞くと、理由を答えられる状態になっています。
エラーの読み方は、Claude Code公式のエラーリファレンスに「Tool input schema is invalid」の節があります。検証のうち、方言の扱いに関わる部分はMCP outputSchemaがdraft-07で拒否される原因と対処法とあわせて読むと整理しやすくなります。
サーバー側で取れる3つの設計
ルート直下の合成キーワードは、Claude Code経由では緩い制約に化けます。サーバーを書く側では、次の3つを検討します。
1. 合成キーワードをpropertiesの中に置く。ネストした合成キーワードは、Claude Codeがそのまま送ります。特定のプロパティが取りうる型のユニオン(文字列か数値か)なら、この配置で意図どおりに伝わります。
2. 平坦なスキーマにして、条件を説明文に書く。「idかnameのどちらかを指定してください」と自分の言葉で書いておけば、平坦化に頼らずClaudeに伝わります。説明文の長さには既定2048文字の切り詰め上限があります。上限の変え方は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
claude mcp listclaude --versionでバージョンを確認する。v2.1.195より前なら、ルート直下に合成キーワードを持つツールはすべてスキップされるclaude mcp listと、セッション内の/mcpでサーバーが接続済みかを見る- Claudeに「そのツールはなぜ使えないのか」と聞く。除外されたツールと理由はClaudeに伝わっている
- サーバーのログを読む。スキップや除外の理由が記録される
自作サーバーのtools/list応答をJSONで保存できるなら、ルート直下に合成キーワードを持つツールはjqで洗い出せます(応答がresult.tools配列に入っている前提の例です)。
jq -r '.result.tools[]
| select(.inputSchema | has("anyOf") or has("oneOf") or has("allOf"))
| .name' tools-list.jsonここに名前が出たツールが、平坦化の対象です。
接続済みなのに一部のツールだけが無い場合は、サーバー全体の問題ではなく、スキーマ単位の除外である可能性が高くなります。バージョンを上げても消えないなら、次にスキーマの2020-12適合とプロパティ名を疑います。
隣接するスキーマの落とし穴との関係
同じ「スキーマの書き方でClaudeの呼び出しが変わる」話でも、原因は別です。anyOf: [{"type": "string"}, {"type": "null"}]のようなnull許容型は、ネストした位置にある合成キーワードなのでClaude Codeが書き換えません。この型が原因で任意パラメータの省略が拒否される問題は、Claude CodeのMCPツールが任意パラメータ省略で拒否される原因と回避策にあります。
MCP仕様側で2020-12の全キーワードが使えるようになった経緯はMCPのツール定義がJSON Schema 2020-12の全機能に対応にまとまっています。仕様が広げた自由度のうち、ルート直下の合成キーワードはClaude API側の制約で書き換えが入る領域だ、というのが両記事をつなぐ整理です。
まとめ
ルート直下のanyOf・oneOf・allOfは、v2.1.195以降のClaude Codeでは平坦化されて使えます。allOfは必須項目まで保たれ、anyOfとoneOfは選択の条件が説明文に移ります。分岐の検証はスキーマに頼れなくなるので、サーバー側の検証が要ります。
古いバージョンで該当ツールが見えないなら、まず更新です。それでも見えないなら、除外判定(プロパティ名と2020-12適合)を疑います。設計を選べるサーバーなら、合成キーワードをネスト側へ置くか、平坦なスキーマに説明文で条件を書くかが、Claude Codeの書き換えに依存しない選択肢です。