ブログ • 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 はコマンドと完全に一致させる。

結論: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 の言う場合だ。コマンドのパスはプレースホルダーで、入れるパッケージではない。

変数は env だけ。2.1.305 以降、許可リストはこの stdio サーバーを除かない。
{
  "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 も安全の制御ではないと書く。ユーザーはどのサーバーにも同じ名前を付けられる。

このサーバーを止める拒否リスト。配列は上の command と args と同じ。
{
  "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. 1
    操作を選ぶ

    JSON Schema を開く。操作する を Schemaで確認してください にする。1つ目の欄に拒否リスト、2つ目の欄に上の Schema を貼る。serverCommand は文字列の配列でなければならない。

  2. 2
    壊れたデータを四つぶつける

    serverCommand を一つの文字列にする、deniedMcpServers を消す、引数を数値にする、または deniedMcpServers をオブジェクトにする。この四つは失敗する。上の拒否リストは通る。通過は形が正しいことだけを意味する。

  3. 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 の差を見る。

ブログへ戻る