Claude Media
claude plugin validateでMCP設定の取りこぼしを見つける

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-plugin

MCP検査は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
done

plugins/は例で、実際のディレクトリ名に合わせます。どれか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の変更点の全体はリリースノートにまとまっています。

この記事を共有:XはてブLinkedIn