ブログ • MCP
Claude Code 2.1.305 へ上げたあと、managed-mcp.json で変数を env だけに書いた stdio サーバーが再び読み込まれる。deniedMcpServers はどう書くか?
Claude Code 2.1.305 の更新説明は一文だが、更新後にサーバーが自分で現れる。allowedMcpServers は、managed-mcp.json で ${VAR} を env だけに使う stdio サーバーを、もう除かない。許可リストに無かったサーバーは読み込まれる。止め続けるのは deniedMcpServers であり、許可リストの写しではない。
MCP の権限を誰が持つかはMCP の安全の記事。OAuth 資格情報が誤った認可サーバーへ渡るとき、ログが止まる段階は前の OAuth の記事。本記事は 2.1.305 のこの一点だけを書く。許可リストは、変数を env だけに使う stdio サーバーをもう止めない。拒否リストの JSON はコマンドと完全に一致させる。
- 対象は managed-mcp.json の stdio サーバーで、${VAR} が env だけにあること。
- 2.1.305 から、許可リストはそれを除かない。許可リストに無ければ、更新後に読み込まれる。
- 止め続けるなら、deniedMcpServers の serverCommand はコマンドと引数の配列で、順序も同じにする。
- 空の拒否リストは形が合法でも、何も止めない。serverName は安全の制御ではない。
結論:env だけに ${VAR} を使う stdio サーバーは、2.1.305 以降、許可リストでは止まらない。拒否リストに、command と args と完全に同じ serverCommand を書く。
2.1.305 は許可リストのどこを変えたか?
更新説明はこう書く。allowedMcpServers は、managed-mcp.json で ${VAR} を env だけに使う stdio サーバーを、もう除かない。許可リストに無かった一台は、更新後に読み込まれる。止め続けるには deniedMcpServers を使う。原文はClaude Code の更新説明。
組織向け MCP の文書は、より前の規則をまだ書いている。managed-mcp.json のサーバーは許可リストを飛ばすが、定義に ${VAR} があれば、まだ許可リストを通る。2.1.259 より前は、許可リストがある限り、すべての管理サーバーが通る必要があった。2.1.305 が外したのは狭い一片だ。stdio で、変数が env だけにある場合。更新説明は、コマンド、引数、URL、headers の ${VAR} も検査不要だとは書いていない。文書は組織向け MCP の文書。
変数が env だけだと、どう見るか?
文書の stdio の例には type、command、args、env がある。秘密は env の値に書かない。ユーザーごとの資格情報は ${VAR} で、そのユーザーの環境から読む。下のファイルは変数を env だけに置く。コマンドと引数はリテラルだ。これが 2.1.305 の言う場合だ。コマンドのパスはプレースホルダーで、入れるパッケージではない。
{
"mcpServers": {
"notes": {
"type": "stdio",
"command": "/usr/local/bin/notes-server",
"args": ["--stdio"],
"env": {
"NOTES_TOKEN": "${NOTES_TOKEN}"
}
}
}
}
${VAR} が command、args、URL、headers にあるなら、更新説明はそれらを今回の免除に含めていない。更新説明が自分で言い換えるまで、そのサーバーは文書の許可リスト検査の対象として扱う。ここでの拒否リストの例は env の場合だけを覆う。
deniedMcpServers はどのコマンドと一致させるか?
文書では、serverCommand が一致させるのは stdio サーバーを起動するコマンドと引数で、一致は完全でなければならない。上の command に args を足したものが、その配列だ。引数を一つ消す、一つ足す、または順序を入れ替えると、一致しない。serverName はラベルだけだ。文書は、どちらのリストの serverName も安全の制御ではないと書く。ユーザーはどのサーバーにも同じ名前を付けられる。
{
"deniedMcpServers": [
{
"serverCommand": ["/usr/local/bin/notes-server", "--stdio"]
}
]
}
拒否リストは管理サーバーにも効く。ユーザー自身の deniedMcpServers も合流するので、ユーザーは自分のためだけに管理サーバーを一台止められる。許可リストが空配列のとき、文書は、許可リスト検査を飛ばすサーバー以外は読み込まないと言う。2.1.305 以降、変数を env だけに使うこの stdio サーバーは飛ばす側だ。空の許可リストでは止められない。
この拒否リストの形は、どの Schema で止めるか?
JSON Schema で型を止める。deniedMcpServers は配列でなければならない。各 serverCommand は文字列の配列でなければならない。空配列の形は合法だ。このサーバーが止まることまでは証明しない。止めるのはアプリの規則だ。配列は command と args を足したものと完全に同じでなければならない。
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["deniedMcpServers"],
"properties": {
"deniedMcpServers": {
"type": "array",
"items": {
"type": "object",
"required": ["serverCommand"],
"properties": {
"serverCommand": {
"type": "array",
"items": { "type": "string" }
}
}
}
}
}
}
| 記録に見えるもの | Schema | このサーバーを止めるとき |
|---|---|---|
| serverCommand が一つの文字列 | 失敗する。ここは文字列の配列が要る。 | 先にコマンドと引数の二つの文字列へ分ける。 |
| 引数が数値 | 失敗する。配列の各項目は文字列だ。 | args と同じ文字列に変える。 |
| deniedMcpServers は空配列で、形は正しい | 通る。空配列も配列だ。 | command と args に一致する serverCommand を足す。空のリストは何も止めない。 |
読み込む前の三段階のうち、どれが本当に止めるか?
文書は読み込む前の確認を三段階で書き、ユーザーが /mcp で再接続するか、無効にしたものを戻すときに再実行する。プロセス内の sdk 型は三段階をすべて飛ばす。拒否リストに当たったあと、それを覆すものはない。
| 段階 | この env だけのサーバー |
|---|---|
| 各層のリストを合流する | 各設定層の許可リストと拒否リストが一つになる。allowManagedMcpServersOnly が true なら、管理元の許可リストだけが残る。拒否リストは常にすべての層から合流する。 |
| 先に拒否リストを見る | serverCommand が完全に一致すれば止まる。この段階のあと、許可リストはそれを戻せない。 |
| そのあと許可リストを見る | 2.1.305 以降、変数を env だけに使うこの stdio サーバーは、この段階を飛ばす。許可リストに無くても、もうそのためには止まらない。 |
JSONNote でこの拒否リストをどう確認するか?
- 1操作を選ぶ
JSON Schema を開く。操作する を Schemaで確認してください にする。1つ目の欄に拒否リスト、2つ目の欄に上の Schema を貼る。serverCommand は文字列の配列でなければならない。
- 2壊れたデータを四つぶつける
serverCommand を一つの文字列にする、deniedMcpServers を消す、引数を数値にする、または deniedMcpServers をオブジェクトにする。この四つは失敗する。上の拒否リストは通る。通過は形が正しいことだけを意味する。
- 3空のリストとそのコマンドを比べる
JSON 比較Diff。1つ目の欄に上の拒否リストを貼る。2つ目の欄は deniedMcpServers を空配列にする。両方とも Schema は通る。このサーバーを止めるのはその serverCommand であり、空配列ではない。
よくある質問
許可リストに同じコマンドを足せば、拒否リストの代わりになるか?
代わりにならない。2.1.305 以降、このサーバーは許可リストを飛ばす。許可リストにそのコマンドがあるかどうかは、読み込むかをもう決めない。止めるには、当たる拒否リストが要る。拒否リストに当たったあと、許可リストはそれを戻せない。
serverName を notes にすれば足りるか?
足りない。文書は、serverName はユーザーが付けるラベルで、ワイルドカードも展開されないと書く。ユーザーはどのサーバーにも同じ名前を付けられる。この stdio サーバーを止めるなら serverCommand を使い、command と args と完全に一致させる。
コマンドにも ${VAR} があるとき、今回も読み込まれるか?
更新説明は、その場合を免除に含めていない。名指ししているのは stdio で、変数が env だけにあるときだ。コマンド、引数、URL、headers にも ${VAR} があるなら、組織向け文書の許可リスト検査のまま扱う。そのサーバーが許可リストに合わないなら、2.1.305 が読み込むと仮定しない。
ユーザー自身の設定で、組織が配った一台を止められるか?
できる。文書は、deniedMcpServers が managed-mcp.json のサーバーにも効き、ユーザー自身の拒否リストが合流すると書く。だからユーザーは自分のためだけに一台を止められる。許可リストはこの段階の代わりにならない。
env に token の値を書くか?
書かない。文書は、そのマシンの誰でも managed-mcp.json を読めるので、API key や他の資格情報を env に書くなと言う。ユーザーごとの資格情報は ${VAR} で、そのユーザーの環境から読む。拒否リストにも token は置かず、コマンドと引数だけを残す。
まとめ
2.1.305 が免除するのは一つだけだ。managed-mcp.json の stdio サーバーで、${VAR} が env だけにある場合。許可リストはそれを除かない。コマンド、引数、URL、headers の変数は、更新説明が一緒に免除していない。
結論:止め続けるなら、deniedMcpServers に command と args と完全に同じ serverCommand を書く。空の許可リストでは止められない。serverName では足りない。env に token の値は書かない。
次:Schema で拒否リストの配列を止め、Diff で空配列とその serverCommand の差を見る。