claude plugin validateでMCP設定の取りこぼしを見つける
v2.1.281以降のclaude plugin validateは.mcp.jsonの黙って捨てられる項目、未宣言のuser_config参照、http://のURLを報告します。実行出力と終了コードで確かめます。
プラグインに同梱したMCPサーバーの設定ミスは、読み込み時に表へ出ません。.mcp.jsonのスキーマに合わない項目は、Claude Codeがそのサーバーだけを捨てて読み込みを続けます。claude plugin validateはv2.1.281からこの.mcp.jsonを検査し、捨てられる項目を事前にエラーとして報告します。
この記事では、検査が拾う5種類の問題を、わざと壊したプラグインにv2.1.295のvalidateを実行した出力で確かめます。
.mcp.jsonの誤りが読み込み時に見えない理由
.mcp.jsonの項目がスキーマを満たさないとき、Claude Codeはそのサーバーを捨てます。プラグインのErrorsタブには何も出ず、Invalid MCP server config for <server> in <path>という1行がデバッグログにだけ残ります。利用者から見ると、サーバーが/mcpに現れないだけです。
v2.1.281より前のclaude plugin validateは.mcp.jsonを見ませんでした。マニフェストが通っても、MCPサーバーが動かないプラグインを出荷できたことになります。
同じInvalid MCP server configでも、Errorsタブに出るものは別の話です。設定はスキーマを通ったものの、そのセッションで解決できない場合で、環境変数が未設定のMissing environment variables: <names>や、URLが使う${user_config.*}が未設定のURL is unset or invalidがこれに当たります。こちらは利用者側の設定で直り、validateの守備範囲とは別です。
検査の対象と実行方法
validateが読むMCPサーバーの宣言は3か所です。
- プラグインルートの
.mcp.json plugin.jsonのmcpServersが名前で指す.jsonファイルplugin.jsonの中に直接書いたインラインのマップ
実行はシェルからプラグインのディレクトリを渡すだけです。
claude plugin validate ./my-pluginMCP検査はClaude Code v2.1.281以降が必要です。ディレクトリを渡すと、.claude-plugin/marketplace.jsonがあればそちらを、なければ.claude-plugin/plugin.jsonを検証します。マーケットプレイスのディレクトリから実行した場合、そこに一覧されたほかのディレクトリのプラグインが同梱するMCPサーバーのファイルは開かれません。そのプラグインの.mcp.jsonを調べるには、プラグインのディレクトリごとに実行します。
わざと壊したプラグインで出力を見る
次の構成を用意しました。plugin.jsonはuserConfigにapi_tokenだけを宣言しています。
{
"name": "demo-tools",
"version": "0.1.0",
"description": "Demo plugin",
"author": {"name": "Example"},
"userConfig": {
"api_token": {
"type": "string",
"title": "API token",
"description": "Token",
"sensitive": true
}
}
}.mcp.jsonには、問題を1つずつ仕込んだ4つのサーバーと、正常なサーバー1つを置きました。
{
"mcpServers": {
"good": {"command": "node", "args": ["${CLAUDE_PLUGIN_ROOT}/server.js"]},
"broken": {"type": "stdio"},
"remote": {
"type": "http",
"url": "http://api.example.com/mcp",
"headers": {"Authorization": "Bearer ${user_config.api_tokn}"}
},
"badurl": {"type": "http", "url": "not a url"},
"literal": {
"type": "http",
"url": "https://api.example.com/mcp",
"headers": {"Authorization": "Bearer sk-live-abcdef1234567890"}
}
}
}remoteは2つの問題を持ちます。http://のURLと、宣言したapi_tokenをapi_toknと書き間違えた参照です。v2.1.295で実行した出力は次のとおりでした。空のCLAUDE_CONFIG_DIRで実行し、パスは相対表記に直しています。
Validating plugin manifest: ./.claude-plugin/plugin.json
Validating mcp: ./.mcp.json
✘ Found 3 errors:
❯ mcpServers.broken.command: Invalid input: expected string,
received undefined. The plugin loader silently drops this
server at load.
❯ mcpServers.remote.headers.Authorization: references
${user_config.api_tokn}, which plugin.json does not declare
under "userConfig" (or in this server's "channels" entry), ...
❯ mcpServers.badurl.url: url is not a valid absolute URL
(for example "https://host/path"). ...
⚠ Found 2 warnings:
❯ mcpServers.remote.url: url uses http:// to a non-loopback host
— requests, headers and any credentials travel in cleartext. ...
❯ mcpServers.literal.headers.Authorization: header value looks
like a literal credential. ...
✘ Validation failed(各メッセージは長いため、末尾を...で省いています。)
出力の各行は、mcpServers.<サーバー名>.<項目>のパスで問題の位置を指します。正常なgoodには何も出ていません。
エラーになる3つと警告になる2つ
公式の整理では、エラーと警告は次のように分かれます。
| 区分 | 検出するもの | 上の例 |
|---|---|---|
| エラー | 検出するもの読み込み時に捨てられるエントリ | 上の例broken(commandが無い) |
| エラー | 検出するものplugin.jsonが宣言しない${user_config.KEY}の参照 | 上の例remoteのapi_tokn |
| エラー | 検出するもの絶対URLとして不正なリモートのurl | 上の例badurl |
| 警告 | 検出するものループバック以外のホストへのhttp://・ws://のURL | 上の例remoteのurl |
| 警告 | 検出するもの生の認証情報に見えるヘッダー値 | 上の例literalのAuthorization |
捨てられるエントリ
brokenはtypeをstdioにしながらcommandが無いため、スキーマ違反です。メッセージがsilently drops this server at loadと書くとおり、validateを通さなければ読み込み時には静かに消えます。
未宣言のuser_config参照
${user_config.KEY}は、利用者が有効化時に入力した値をMCPサーバーの設定へ差し込む記法です。KEYはplugin.jsonのuserConfigに宣言したキーでなければならず、綴りの間違いはここで見つかります。メッセージの括弧書きにあるとおり、そのサーバーのchannelsエントリが宣言するキーも宣言済みとして扱われます。
注意点が1つあります。MCPのheadersHelperでは、${user_config.*}は書けません。ヘルパーがシェルを通って動くため、差し込んだ値が再解釈されるのを避けて、参照を含むコンポーネントは起動せずエラーになります。値を渡す手段はClaude Code側にはなく、ヘルパーのスクリプトが自分で取得します。ヘルパーの環境にはCLAUDE_PLUGIN_ROOTなどは入りますが、オプションの値は入りません。
不正なURL
リモートサーバーのurlは、絶対URLでなければなりません。not a urlのような値はvalidateでエラーになり、読み込み時にも無効な設定として扱われて接続されません。空のurlは別の扱いで、後から設定するコネクタ用の置き場としてnot configuredと表示されるだけでエラーにはなりません。
平文のURLと生の認証情報
http://やws://でループバック以外のホストに向けると警告が出ます。メッセージの全文は「requests, headers and any credentials travel in cleartext. Use https://; plugin directories may reject insecure remote server urls.」です。リクエストやヘッダーが平文で流れるうえ、プラグインのディレクトリ側で安全でないURLが拒否されることもあるため、https://に直します。ヘッダーの値が生の認証情報に見える場合も警告です。プラグインに入れたものはインストールした全員が読めるので、機密のuserConfigオプションや環境変数を参照する形に直します。
"headers": {"Authorization": "Bearer ${user_config.api_token}"}api_tokenはsensitive: trueで宣言しているため、値は設定ファイルではなくOSの認証情報ストアに保存されます。
終了コードとCIへの組み込み
validateの終了コードは判定行に従います。
| 終了コード | 判定行 | 意味 |
|---|---|---|
| 0 | 判定行Validation passed または Validation passed with warnings | 意味読み込める。--strictでは警告も無いこと |
| 1 | 判定行Validation failed | 意味エラー。--strictでは警告も対象 |
| 2 | 判定行Unexpected error during validation | 意味検証自体の失敗 |
上の壊れた構成では終了コードが1でした。エラーを直してhttp://の警告だけが残る状態にすると、素の実行はValidation passed with warningsで終了コード0、--strictを付けるとValidation failed (--strict treats warnings as errors)で終了コード1になりました。
警告のhttp://をhttp://localhost:8080/mcpに変えると警告は消え、Validation passedでした。ローカルで試すMCPサーバーを同梱するプラグインでは、ループバックが警告の対象外であることが効きます。
CIには--strictで置くと、平文URLや生の認証情報の混入も落とせます。結果を機械で読むなら--jsonがあり、v2.1.259以降で使えます。
claude plugin validate ./my-plugin --strict --json \
| jq '.contents[] | select(.file | endswith(".mcp.json")) | .errors, .warnings'JSONの最上位にはsuccess・strict・target・manifest・contentsが入ります。manifestはplugin.json自体の結果、contentsは検査したファイルごとの結果で、各要素がfileと、errors・warnings・notesの3つの配列を持ちます。.mcp.jsonの結果は、fileが.mcp.jsonで終わる要素です。
上の壊れたプラグインからgood・broken・remoteだけを残してv2.1.295で--jsonを実行すると、contentsの要素は次の形でした。長いメッセージは省いています。
{
"file": "<plugin>/.mcp.json",
"type": "mcp",
"errors": [
{"path": "mcpServers.broken.command", "message": "Invalid input: ...", "code": null},
{"path": "mcpServers.remote.headers.Authorization", "message": "references ${user_config.api_tokn}, ...", "code": null}
],
"warnings": [
{"path": "mcpServers.remote.url", "message": "url uses http:// to a non-loopback host ...", "code": null}
],
"notes": []
}問題は1件ごとにpathとmessageを持ちます。この実行では要素にtype: "mcp"とcodeも付いていましたが、公式のリファレンスが挙げるのはfile・errors・warnings・notesの4つです。スクリプトから読むなら、記載のあるfileで要素を選ぶほうが、バージョンが変わっても崩れにくくなります。notesは警告の手前の参考情報の置き場で、今回の構成では空でした。
Errorsタブに出る問題との切り分け
MCPサーバーが動かないとき、見る場所は症状で変わります。validateが拾うのはスキーマ違反だけで、環境に依存する失敗は別の場所に出ます。
| 症状 | 出る場所 | 直し方 |
|---|---|---|
サーバーが/mcpに現れない | 出る場所デバッグログ(Invalid MCP server config for <server> in <path>) | 直し方プラグインのディレクトリでvalidateを実行し、エラーのパスを直す |
Missing environment variables: <names> | 出る場所Errorsタブ | 直し方その変数をClaude Codeを起動するシェルに設定し、新しいセッションを始める |
URL is unset or invalid | 出る場所Errorsタブ | 直し方URLが使う${user_config.*}の値を/plugin configure <plugin>で設定する |
Bundled MCP server "<name>" was not started: it needs configuration | 出る場所Errorsタブ | 直し方MCPB形式で同梱したサーバーの必須設定が未入力。/pluginのInstalledタブでConfigureを開いて値を入れる |
| 接続済みにならない | 出る場所/mcpの状態とデバッグログ | 直し方claude --debugで起動し、サーバーが出力したエラーを読む |
上から2行目以降は、設定が正しくても利用者の環境しだいで起きます。作者の手元のvalidateが通っても防げないので、プラグインのREADMEに必要な環境変数とuserConfigの項目を書いておくと、問い合わせが減ります。MCPB形式の未設定を示す表示は、v2.1.285より前のClaude Codeでは出ず、サーバーが黙って起動されませんでした。
headersHelperで値を渡す代替
${user_config.*}をシェル経由のフィールドに書くとエラーになる、という制約はheadersHelperのほかにも及びます。値をどう届けるかは、フィールドごとに変わります。
| フィールド | 値の届け方 |
|---|---|
| シェル形式のフックコマンド | 値の届け方argsを使う実行形式にするか、フックの環境からCLAUDE_PLUGIN_OPTION_<KEY>を読む |
| モニターのコマンド | 値の届け方Claude Code経由では届かない。スクリプトが自分で取得する |
MCPのheadersHelper | 値の届け方Claude Code経由では届かない。スクリプトが自分で取得する |
headersHelperの環境に入るのはCLAUDE_PLUGIN_ROOT、CLAUDE_CODE_MCP_SERVER_NAME、CLAUDE_CODE_MCP_SERVER_URLの3つで、オプションの値はありません。トークンを動的に付けたいなら、.mcp.jsonのheadersではなくヘルパーを使い、ヘルパーのスクリプトが自分で資格情報を読む構成になります。headersに${user_config.api_token}を書く形は、シェルを通らないフィールドなのでvalidateもエラーにしません。
複数のプラグインを持つリポジトリでの回し方
マーケットプレイスのディレクトリでvalidateを実行しても、一覧にある別ディレクトリのプラグインが同梱する.mcp.jsonは開かれません。マーケットプレイスのmarketplace.jsonの検証は通っているのに、個々のプラグインのMCP設定は未検査のまま、という状態になります。
一方、1つのディレクトリに.claude-plugin/marketplace.jsonと.claude-plugin/plugin.jsonの両方がある場合は、v2.1.289以降なら両方が検証されます。プラグインを別ディレクトリに置く構成では、CIでプラグインごとに回します。
for dir in plugins/*/; do
claude plugin validate "$dir" --strict || exit 1
doneplugins/は例で、実際のディレクトリ名に合わせます。どれか1つが失敗した時点で止めるので、終了コードがそのままCIの判定になります。
検査を通っても確認できないこと
validateが見るのは設定の形です。サーバーが起動するか、ツールが応答するかは対象外です。通したあとに残る確認は2つあります。
- インストールしたプラグインで
/mcpを開き、サーバーが接続済みになっているかを見る - 接続しないときは
claude --debugで起動し、~/.claude/debug/<session-id>.txtのログでサーバーの出力を読む
--debugはターミナルに何も表示しないため、ログファイルを開く必要があります。--plugin-dirでは動いてインストール後に失敗する場合の原因は、プラグイン同梱MCPサーバーの仕組みにあるパスと環境変数の扱いが出発点になります。プラグインのマニフェスト側のフィールドはplugin.jsonスキーマの解説に分けています。環境変数がMCPサーバーへ漏れないようにする設定はCLAUDE_CODE_MCP_ALLOWLIST_ENVの記事が扱い、HTTP 400で失敗する実例はGitHub MCPプラグインのHTTP 400にあります。
まとめ
公開前に必ず走らせたいのは、claude plugin validate ./my-plugin --strictの1行です。.mcp.jsonの黙って消えるエントリ、綴り違いの${user_config.*}、平文URLと生のトークンは、利用者の手元で症状として現れる前にここで止まります。v2.1.281の変更点の全体はリリースノートにまとまっています。