p4ni.

研究紹介

Agent Plugins 1.0.0 では認証付き MCP サーバーを配れない:設定ファイル7通りを公式スキーマにかけた

· 読了まで約11分

目次

MCP サーバーを入れるかどうか半日かけて検討して、結局ひとつも入れませんでした。その前日に Agent Plugins 1.0.0 が出ていました。まさに私が見送ったものをパッケージ化するための標準で、初期の技術運営委員会には Amazon・Cursor・Microsoft・OpenAI・Vercel の Core Maintainer が入っています。それなら見送ったサーバーを Agent Plugin の形にしてみようとして、できないことがわかりました。理由は仕様書にそのまま書いてあります。

入れようとしていたのは X のホスト型 MCP エンドポイントで、Bearer トークンが要ります。Agent Plugins 1.0.0 には、そのトークンを同梱したまま仕様どおりに配る方法がありません。

7通りの設定ファイルを2つのスキーマにかけた

仕様はプラグインが持てる2つのファイルについて JSON Schema を公開しています。その2つを7通り書いて jsonschema 4.19.2 で検証しました。A・B・C・C′ が plugin.json、D・E・F が mcp.json です。結果は表のとおりです。

#書いたもの結果
A$schemaname だけ。version も author も license も無しvalid
Bauthor.nameCloudflare, Inc.version9.9.9 にするvalid
Cトップレベルに signature フィールドを足すinvalid
C′同じ署名を extensions の逆ドメイン名前空間の下に置くvalid
DBearer トークンを headers に平文で書くvalid
EAuthorization: Bearer ${X_BEARER_TOKEN} と書くvalid
Fサーバー定義に独自の secretRef を足すinvalid

A と B はマニフェストが省いてよいもの、C と F は足してはいけないもの。C′ は C のフィールドが実際に置ける場所ですが、置いたところで何も得られません。そして罠が D と E です。

マニフェストの必須項目は2つしかない

必須は $schemaname の2つだけです。

{
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
  "name": "trend-scan"
}

これで準拠したマニフェストになります。versiondescriptionauthorhomepagerepositorylicensekeywordsextensions はすべて任意です。プラグインを人にもビルドにもリポジトリにも結びつけない形式で、しかも書いた内容が本当である必要もありません。

authornameemailurl を持つオブジェクトで、形は検査されますが中身は検査されません。B では取引のない会社の名前を author.name に書きましたが、スキーマはそのまま通します。仕様がメタデータの中身まで検証しないと決めているからです。version が SemVer でない、repository が URL として認識できない、license が SPDX 識別子でない、といった理由でマニフェストを拒否してはならない、とまで書いてあります。

出所証明は置けるが、読む義務が誰にも無い

plugin.schema.jsonadditionalProperties: false なので、C のトップレベルの signature はスキーマ違反になります。ただしマニフェストには任意のデータを入れる枠があって、それが extensions です。逆ドメイン名をキーにしたオブジェクトで、中身に制約はありません。同じ署名を com.p4ni の下に移した C′ は通ります。

ただし §8.1 が、自分の実装していない名前空間の項目については値の中身を検証せずに無視しなければならない、とクライアント側の振る舞いを決めています。署名は持ち歩けます。ただし誰にも見る義務が無く、その名前空間を実装していないクライアントは見てはいけない側に回ります。extensions の下の出所証明は、標準の形をした私的な取り決めにすぎません。

C と F は同じ invalid でも重さが違います。マニフェストの未知のトップレベルフィールドは致命的ではなく、§5.2 はクライアントに、報告して無視し、プラグインの読み込みは続けるよう求めています。F は違います。§7.2.2 では設定の要件を満たさないサーバー定義をスキップしなければならず、プラグインの残りは読み込まれたまま、そのサーバーだけが消えます。

仕様の FUTURE_CONSIDERATIONS.md には、暗号署名の検証も、公開物をソースリポジトリとビルドに紐づけるアテステーション連鎖も載っています。ただし「将来のバージョンが定義するかもしれない」項目としてです。今あるのは署名の置き場所だけで、それを確かめる義務はどこにも書かれていません。

認証だけが宙に浮いている

リモートの MCP サーバーは mcp.json に書きます。

{
  "$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json",
  "mcpServers": {
    "xapi": {
      "type": "streamable-http",
      "url": "https://api.x.com/mcp",
      "headers": { "Authorization": "Bearer ..." }
    }
  }
}

この headers について、仕様は2つの禁止を置いています。

ひとつは認証情報の埋め込み。ヘッダーの値はパッケージの中身として見えるデータであって、秘密を運ぶ仕組みではない、というのが理由です。同じ禁止は stdio サーバーの env にもかかります。

もうひとつはプレースホルダの展開です。クライアントは URL・ヘッダー名・ヘッダー値に環境変数の展開を行ってはならない、と書かれています。展開されるのは ${PLUGIN_ROOT}${PLUGIN_DATA} の2つだけで、どちらもヘッダーには効きません。

そのうえで、仕様書が自分で結論まで書いています。Agent Plugins v1 は OAuth 設定も移植可能な認証情報参照フィールドも定義せず、認可の発見・ユーザー操作・認証情報の保管はクライアント任せである、と。

秘密は書くな、間接的に参照するのもだめ、そして指し示すためのフィールドも無い。最後の抜け道が塞がっていることは F で確かめました。secretRef を勝手に生やすと additionalProperties: false がサーバー定義ごと弾きます。

これは見落としではありません。FUTURE_CONSIDERATIONS.md の秘密の扱いの節は「MCP サーバーは実行時に認証情報や API キーを必要とすることが多い」と書き出したうえで、マニフェストの secrets フィールドや、設定ファイルに平文を残さずに済むクライアント経由の秘密の注入を、将来のバージョンが定義するかもしれない項目に並べています。穴は穴として文書化されている。つまり設計上の線引きであって失敗ではありません。ただし今日配りたいプラグインには何の役にも立ちません。

影響する範囲は狭いですが、実害はあります。スキルだけを束ねたプラグインは完全に移植可能です。認証の要らないローカルの stdio サーバーを束ねたプラグインも移植可能です。認証付きのリモートサーバーを束ねたプラグインだけが、ただの設定の雛形になります。利用者はそれぞれ、自分が使っているクライアント固有のやり方で、手作業で仕上げるしかありません。build once, run anywhere が通用するのは、認証の要らないサーバーまでです。

D より E のほうが厄介

D は本物のトークンを平文でコミットするので、少なくともレビューで目立ちます。差分を読めば認証情報だとわかります。

時間を溶かすのは E のほうです。"Authorization": "Bearer ${X_BEARER_TOKEN}" はスキーマを素通りします。HTTP ヘッダー値として構文が正しく、中身についてスキーマは何の意見も持たないからです。しかし、どのクライアントもこれを展開してはいけないことになっています。${X_BEARER_TOKEN} という文字列がそのまま送信先に飛びます。サーバーは 401 を返します。仕様はこの認可の失敗を、そのサーバーの接続失敗として分類します。プラグイン設定の不正という扱いにはなりません。プラグイン自体は正常に読み込まれ、マニフェストに問題があるとは報告されません。

Claude Code の .mcp.json では ${VAR} の展開が実際に効くので、そこに慣れた開発者はまさにこう書きます。そしてスキーマエラーという手がかりが出ないまま、黙って失敗します。検証は通り、読み込みも通り、認証だけが通りません。

仕様が明確に強くした点

設計が粗いという話ではありません。制約はどれも、契約を意図的に最小限にとどめるという方針と筋が通っていて、いくつかは置き換え前より明確に強くなっています。

command フィールドに書けるのは単一の実行可能トークンだけで、シェル文字列は受け付けません。コマンドインジェクションの類を形式のレベルで断っています。同梱した実行ファイルはプラグイン相対の ./ パスを使う必要があります。コンポーネントの位置は skills/mcp.json に固定で、マニフェストから場所を動かす手段も、優先順位を読み解く必要もありません。ディレクトリを一段見れば中身がわかるということです。封じ込め(containment)のルールが、プラグインから参照できるパッケージ内のファイルを制限します。そして、タイプミスの検出と厳密な検証を成り立たせているのも、C と F を弾いたあの additionalProperties: false です。

自分の限界をここまで正直に書くのも、ベンダーの発表としては珍しいと思います。FUTURE_CONSIDERATIONS.md は v1.0.0 が信頼モデル・権限システム・サンドボックス要件のいずれも定義しないと明言したうえで、段階的な信頼レベル、プラグインごとの能力制限、同意フロー、秘密の注入、許可リスト、監査イベントのスキーマを未解決の課題として並べています。セキュリティを解決したとは言っていません。パッケージングと発見を標準化したと言っていて、実際にそれをやっています。

攻撃面は変わらず、流通だけが変わる

公開している自分のスキルに指示レベルの攻撃の監査をかけて、Bandit も Semgrep も Snyk Code も一件も拾わないことを確かめました。次に、その指示を二次ファイルに埋めたときどれだけ効くかを測り、haiku-4.5 で30試行中21回、sonnet-5 で20試行中0回という結果になりました。

Agent Plugins はこの攻撃面を変えません。skills/ に入るのは Agent Skills 仕様が既に定義している形式の SKILL.md で、そこは手つかずです。変わるのは流通です。あのファイル群に標準的なパッケージが付き、読むべきマニフェストが1つに決まり、エージェントを出している5社のメンテナがどこを見ればいいかで合意しました。指示の効き目は前と変わらないまま、流通だけがよくなります。

標準そのものに反対したいわけではありません。パッケージングは実在の問題で、解き方も筋が通っています。ただ、自分が書いていないプラグインについて本当に確かめたいことは3つあります。誰が公開したのか、バイトが公開時のものと同じか、読み込んだ後に何に触れてよいのか。どれも v1.0.0 が明示的にクライアントへ委ねた部分です。将来のバージョンが埋めるまで、「準拠した Agent Plugin である」という情報はファイルの置き場所を教えてくれるだけで、信用してよいかについては何も言いません。

今のところサードパーティの MCP サーバーは1つも動かしていません。この形式が出ても結論は変わりませんでした。入れないという判断はそのままで、置き場所の見通しがよくなっただけです。