自分のMCP設定を点検したら、引っかかったのは自分で入れたX連携だけだった

前の記事でローカルAIの待ち受けポートを点検する道具を作ったあと、次に何を作るかを考えて、いま何がはやっているかを調べた。GitHub のトレンドと直近のニュースを流し見した範囲では、エージェント基盤そのもの、Claude Code のスキルやフックの詰め合わせ、そして MCP とエージェントのセキュリティ、この3つに大体まとまる。前の2つは似たものが既に大量にあって、個人が出しても埋もれる。3つ目は、エージェントのコードを静的に検査するもの、MCP サーバー側を能動的に調べるもの、企業向けの SaaS はあったが、「自分のマシンに登録した MCP が今どう露出しているか」を見るものは、探した範囲では見当たらなかった。

前の道具は、既定ポートに繋いで判定するだけで、設定ファイルは読まない。MCP はその逆で、どこに何が登録されているかは設定ファイルにしか書いていない。だから今度は設定ファイルを読むところから始めた。mcp-exposure という Go の CLI で、依存なし、引数なしで実行すると、登録1件につき1ブロックでこう出る。

$ mcp-exposure
xapi  claude-code/user  stdio  WARN
  npx -y @xdevplatform/xurl mcp https://api.x.com/mcp
  UNPINNED         @xdevplatform/xurl is fetched at every start with no exact version
  SECRET_INLINE    env.CLIENT_SECRET is stored in plaintext

最後の WARN が判定で、ok、WARN、RED の3段。この記事ではそれぞれ緑、黄、赤と呼ぶ。俺の環境には MCP が1つしか登録されていなかった。その1つが、2項目で黄になった。5つのクライアントの設定を読めるようにしてあるが、実物で回せたのは Claude Code のこの1つだけで、他は設定ファイルの形をドキュメントで確認して読めるようにしただけだ。

仕様が守っているのは HTTP だけだった

MCP の登録は Claude Code、Claude Desktop、Cursor、VS Code、Gemini CLI で置き場が違う。どれも JSON で、サーバーごとに「コマンドと引数」か「URL」のどちらかが書いてある。前者は stdio で、クライアントが子プロセスを起動して標準入出力で話す。後者は HTTP で、どこかで待ち受けているサーバーに繋ぎに行く。道具はこの2つで分岐する。

HTTP のほうは、MCP の仕様(2026年7月28日版)にサーバー側の義務が書いてある。DNS リバインディング対策として Origin ヘッダを検証しなければならない(MUST)。ローカルで動かすなら 0.0.0.0 ではなく 127.0.0.1 だけに束縛すべき(SHOULD)。全接続に認証を実装すべき(SHOULD)。前の記事で Ollama に対してやった3つの判定が、そのまま仕様の文言に対応している。

だから URL がループバックを指していれば、前の道具と同じ手順で繋ぎに行く。全アドレスで接続を試みて束縛先を見る。素のリクエストと、HostOriginevil.example を入れたリクエストの差分で Origin 検証を見る。素のリクエストが 2xx なら認証なし。URL が http:// で外のホストを指していれば、繋がずに PLAINTEXT_REMOTE の赤にする。ヘッダも本文も平文で流れる。headers に書かれた値が平文の秘密なら、それは stdio の env と同じ判定で拾う。

stdio のほうには、仕様に相当する守りがない。当然で、子プロセスの起動は MCP の範囲ではなく、クライアントの設定ファイルの範囲だ。そこで道具が見るのは2つ。npx -yuvx のようにパッケージ名で起動していて、バージョンが固定されていないこと(UNPINNED)。env に書かれた値が平文で、キー名が秘密っぽいこと(SECRET_INLINE)。どちらも仕様違反ではない。仕様が何も言っていない場所で、自分がどう登録したかの問題だ。

mcp-exposure の流れ。5つのクライアントの設定ファイルから MCP サーバーを列挙し、stdio は UNPINNED と SECRET_INLINE、HTTP はリモートなら PLAINTEXT_REMOTE、ローカルなら LISTEN・REBIND・AUTH で判定する。headers の平文の秘密は stdio の env と同じ判定

自分の X 連携

引っかかった xapi は、7月に X の MCP を OAuth で使えるようにしたときに入れたものだ。X が配っている @xdevplatform/xurl というパッケージを npx -y で起動し、envCLIENT_IDCLIENT_SECRET を書いてある。入れたときのドキュメントにそう書いてあったから、そう書いた。

npx -y パッケージ名 は、起動のたびにレジストリに「このパッケージの今の版をくれ」と頼む。手元に何が実行されるかを決めているのは、俺ではなくレジストリだ。公式のパッケージなので、差し替わる確率は低い。だから赤ではなく黄にしてある。それでも消さないのは、確率ではなく「誰が決めているか」の問題だからだ。

固定するかを決めるために、npm の公開履歴を見た。初版が2026年2月20日、最新が7月21日の 1.3.1 で、7か月に10版。ただし公開があった日は4日しかなく、2月に4版、6月末に3版、7月に3版と、まとめて出る型だ。俺が7月上旬に入れた時点の最新は 1.2.2 で、そのあと 1.2.3、1.3.0、1.3.1 と3回、npx -y が黙って差し替えていた。動いていたので気づかなかった。

この頻度なら、固定して失うのは年に数回の更新を自動で取ることだけで、更新が届いても古い Ollama が動き続けていたときのように、直っているはずのものが何か月も古いまま動く心配は薄い。@xdevplatform/xurl@1.3.1 に書き換えた。次にこのパッケージが変わる日は、レジストリではなく俺が決める。

シークレットの平文のほうは、置き場が ~/.claude.json で、git には入っていない。道具はそれを見て黄で止めている。同じ値が git 管理下のファイルにあれば赤になる。平文であることは変わらないので、このファイルを読める全てのプロセスが読める。それでも、~/.aws/credentials と同じ扱いの場所に置いてある以上、赤と言うのは言い過ぎだと判断した。

赤を出すより、赤にしないものを決めるほうが難しかった

前の記事で、macOS の AirPlay 受信が毎回赤になる話を書いた。誤検知が1つあると、終了コードは常に 1 で、cron には入れられない。点検の道具は、赤の数ではなく、赤を信じられるかで価値が決まる。今回は設計と実装の両方をレビュー用のエージェントに読ませて、誤検知になる条件を先に潰した。潰したものを並べる。

@latest を固定扱いにしていた。npx -y pkg@latest には @ の後ろに文字があるので、最初の実装は「バージョン指定あり」と見ていた。latest は動く名前で、起動のたびに指す先が変わる。^1.2.0 のような範囲指定も同じ。数字3つの版だけを固定と数えるように変えた。

AUTHOR を秘密扱いにしていた。キー名に auth が含まれれば秘密、という部分一致だったので、AUTHOROAUTH_MODEKEYBOARD も引っかかる。キー名を _ と大文字小文字の切れ目で単語に割って、単語として keysecrettoken に一致するときだけにした。GOOGLE_APPLICATION_CREDENTIALS はキー名としては一致するが、値が /Users/... のパスなら、それは秘密の在り処であって秘密ではないので外す。

uvx --python 3.12 mcp-server-git==1.0.43.12 をパッケージ名と読んでいた。フラグの後ろの引数を読み飛ばさなかったせいで、固定済みの登録に「3.12 が未固定」という意味不明な黄が付く。値を取るフラグの一覧を起動ツールごとに持たせた。

${env:VAR} を平文と見ていた。変数参照の書き方は、Claude Code が ${VAR}${VAR:-default}、Cursor が ${env:VAR}。VS Code は入力値を指す ${input:名前} も使う。最初の判定は ${VAR} の形だけを参照と見ていたので、それ以外のクライアントでは参照が全部黄になる。${ か、$ の後に名前が続いていれば参照とみなす方向に緩めた。見逃すより誤検知のほうが害が大きい判定なので、緩いほうに倒した。

どれも、書いた時点では気づいていない。自分の環境には xapi しか無いので、自分で回しても出てこない。他人の設定の形を想像して潰すしかなくて、そこはレビューに読ませるのが一番早かった。

読まないと決めたもの

Codex CLI の設定は TOML で、標準ライブラリに読む手段がないので初版では読まない。Claude Code のプラグインが持ち込む MCP も、どのプラグインが有効かの判定が要るので読まない。パッケージレジストリには問い合わせない。存在するか、既知の悪いパッケージかは見ていない。リモートの https:// サーバーには何も送らない。表には transport とホストを出すだけだ。ホストのファイアウォールを見ないのは前の道具と同じで、束縛先が全インターフェースでも、外から届くとは限らない。

三回とも自分のマシンのものだった

前の前の記事では、脆弱性の話を読んで自分の Ollama を見たら、更新されずに動いていた古い版が出てきた。前の記事では、ローカルAIの点検を道具にしたら、ローカルAIではなく AirPlay が先に赤になった。今回は、MCP の点検を道具にしたら、自分が X に投稿するために入れた登録が引っかかった。

三回とも、引っかかったのは他人のサーバーではなく、俺が入れたか、俺のマシンで動くのを黙って許していたもので、動くのを見たあとは見ていなかった。入れた日には、動くかどうかしか見ていない。どう露出しているかを見る日は、放っておくと来ない。

go install github.com/nobu666/mcp-exposure@latest

固定したので、xapi の行から UNPINNED は消えた。3回黙って差し替わっていたことは、表を叩くまで知らなかった。入れた日に一度見て忘れる代わりに、忘れた頃に表のほうが思い出させる。前の二本を書いたあとで欲しくなったのは、結局それだった。

comments powered by Disqus