やってみた 2026年7月2日

chrome-devtools-mcpを使い捨てDockerで動かしてみた——CLIとサンドボックスで二度詰まった話

XECIN TypeScriptMCPChromeブラウザ自動化

GitHubのトレンドを眺めていたら、ChromeDevTools/chrome-devtools-mcp が上の方に来ていた。Chrome DevTools チームが公式に出しているMCPサーバで、コーディングエージェントに「生きたChromeを操作・計測させる」ためのものらしい。スターは4万4千を超えている。

MCPサーバというと、CursorやClaudeみたいなMCPクライアントに繋いで初めて動くイメージがあって、単体で試すのは面倒そうだな……と一瞬スルーしかけた。ただREADMEをよく読むと「A CLI is also provided for use without MCP(MCPなしで使えるCLIも同梱している)」と書いてある。これなら私のような、まずターミナルから叩いて挙動を確かめたい人間でも試せる。ちょっと触ってみることにした。

試した環境は、Windowsマシンの上に立てた使い捨てのDockerコンテナ(node:22-bookworm)の中だけで完結させている。ホストを汚したくないので、対象のcloneも実行も全部コンテナ内で、ホストに残すのは検証ログだけ、という切り分けです。バージョンは chrome-devtools-mcp 1.4.0、Node.js v22.23.1、npm 10.9.8。最終的にGoogle Chrome 150.0.7871.46 を使いました。

インストールは一瞬、CLIのdocs通りに叩くと弾かれる

インストールはグローバルに一発だった。

npm i -g chrome-devtools-mcp@latest

パッケージ自体は薄くて、added 1 package であっさり入る。中身はほぼ puppeteer への薄いラッパー、という構成です。chrome-devtools --version1.4.0 を返してくれた。

CLIは面白い作りで、最初にツールを呼ぶとバックグラウンドでMCPサーバ(デーモン)を立ち上げ、Unixソケット越しに喋る。以降のコマンドは同じデーモンを再利用するので、開いたページやCookieの状態が維持される。ここは素直に良い設計だなと思いました。

で、README/docsのCLI例をそのまま叩いてみたんですが、これがいきなり通らない。

$ chrome-devtools navigate_page 'https://example.com'
Invalid values:
  Argument: type, Given: "https://example.com", Choices: "url", "back", "forward", "reload"

最初は自分のタイプミスを疑ったんですが、そうではなかった。navigate_page は位置引数が --type(url / back / forward / reload)に束縛される作りで、URLは位置引数ではなく --url フラグで渡す必要があった。docsには navigate_page "https://web.dev" --type url と位置引数で渡す例が載っているんですが、実際やってみると、少なくとも1.4.0のCLIでは弾かれます。正解はこう。

chrome-devtools navigate_page --url 'https://example.com' --type url

experimental と明記されているCLIなので、ドキュメントの追従が少し遅れているのかもしれません。ここは実際に --help を読んで確かめるのが早かったです。

rootコンテナだとChromeが即死する

構文を直して再挑戦したら、今度は別のエラーで止まった。

$ chrome-devtools navigate_page --url 'https://example.com' --type url
Could not find Google Chrome executable for channel 'stable' at:
 - /opt/google/chrome/chrome

これは少し意外だった。npm i の裏で puppeteer が Chrome for Testing(~/.cache/puppeteer/chrome/linux-150.0.7871.24)をちゃんとダウンロードしていたので、てっきりそれを使うものだと思い込んでいたんです。ところがデーモンの既定は「channel stable」、つまりシステムに入った本物のGoogle Chromeを /opt/google/chrome/chrome から探しに行く。素のNodeイメージにそんなものは無いので、当然見つからない。

--executablePath でダウンロード済みのバイナリを指すこともできたんですが、今回は素直にGoogle Chrome stable をコンテナに入れることにしました。実運用でエージェントに使わせるなら結局 stable を入れる構成になるだろう、という判断です。

google-chrome-stable --version
# => Google Chrome 150.0.7871.46

これで channel の解決は通った。……が、次はもっと分かりにくいエラーに変わる。

$ chrome-devtools navigate_page --url 'https://example.com' --type url
Protocol error (Target.setDiscoverTargets): Target closed

Target closed は、起動したそばからChromeが落ちているサイン。心当たりはすぐあって、コンテナ内はroot実行なので、Chromeのサンドボックスが弾いて即terminateしているやつです。puppeteer系をコンテナで動かすと最初にたいてい踏む、あの --no-sandbox 問題ですね。

厄介なのは、chrome-devtools-mcp のフラグに「起動時のChrome引数をそのまま渡す」口が見当たらなかったこと。--headless--channel--executablePath はあるんですが、任意のChromeフラグ(--no-sandbox 等)を素通しするオプションが無い。

そこで発想を変えて、Chromeを自分で起動して、そこにデーモンを繋ぐ方式にしました。start --help を見ると --browserUrl で「起動済みのデバッグ可能なChromeに接続する」例がちゃんと載っている。

# 自前でChromeを --no-sandbox 付き・リモートデバッグ有効で起動
google-chrome-stable --headless=new --no-sandbox --disable-gpu \
  --disable-dev-shm-usage --remote-debugging-port=9222 \
  --user-data-dir=/tmp/cud about:blank &

# デーモンを既存ブラウザに接続
chrome-devtools start --browserUrl http://127.0.0.1:9222

http://127.0.0.1:9222/json/version を叩くと Chrome/150.0.7871.46 と webSocket のエンドポイントが返ってきて、chrome-devtools statusdaemon is running に変わった。ここまで来て、ようやく本題です。

繋がってからは気持ちいいくらい素直

一度繋がると、あとは驚くほど素直に動きました。ナビゲートすると、選択中のページ一覧がテキストで返ってくる。

$ chrome-devtools navigate_page --url 'https://example.com' --type url
Successfully navigated to https://example.com.
## Pages
1: Example Domain (https://example.com/) [selected]

個人的に一番「これはエージェント向けだな」と感心したのが take_snapshot。スクリーンショットの画像ではなく、アクセシビリティツリーを uid 付きのテキストで返してくる。

$ chrome-devtools take_snapshot
## Latest page snapshot
uid=1_0 RootWebArea "Example Domain" url="https://example.com/"
  uid=1_1 heading "Example Domain" level="1"
  uid=1_2 StaticText "This domain is for use in documentation examples..."
  uid=1_3 link "Learn more" url="https://iana.org/domains/example"

この uidclickfill にそのまま渡して操作する設計です。LLMに画像を食わせて座標を推定させるより、この構造化テキストを渡す方が圧倒的に安定するはず。実際に自社のLP検証をエージェントに任せている身からすると、この割り切りは正しいと思います。

スクリーンショットもファイルに落とせる。

$ chrome-devtools take_screenshot --filePath /work/example.png
Saved screenshot to /work/example.png.
$ ls -l /work/example.png
-rw-r--r-- 1 root root 21854 /work/example.png

21KBほどのPNGがちゃんと出てきました。ちなみに --screenshotFormat jpeg--screenshotMaxWidth といった、LLMの文脈を食い過ぎないよう画像を軽くするオプションが用意されているのも、AIエージェント前提の道具だなという印象。

Wikipediaに移動してコンソールを覗いてみたら、こちらは空。

$ chrome-devtools list_console_messages
## Console messages
<no console messages found>

「エラーが出てほしい」ときに限って綺麗なページを選んでしまうのは、ぶっちゃけ検証あるあるですね。ここは自前でエラーを吐くHTMLを用意した方が記事映えしたな、と後から思いました。この辺は自分の段取りの問題です。

最後にLighthouse監査。--mode snapshot は、その時点のページ状態に対してそのまま監査をかけるモードです。

$ chrome-devtools lighthouse_audit --mode snapshot
## Lighthouse Audit Results
Mode: snapshot   Device: desktop
### Category Scores
- Accessibility: 100
- Best Practices: 100
- SEO: 100
- Agentic Browsing: 100
### Audit Summary
Passed: 37   Failed: 0   Total Timing: 2228.12ms

37項目を2.2秒ほどで回して全部パス。見慣れないカテゴリで Agentic Browsing(エージェントが操作しやすいか、という観点らしい)が増えているのが今っぽくて面白かったです。出力の URL: undefined はご愛嬌、というか snapshot モードだからかもしれません。

私はこう使っていきたい

触ってみての所感としては、詰まったのは全部「MCPサーバ本来の使い方」から外れて、CLI単体+rootコンテナという変則構成で動かしたことに起因していました。想定通りにMCPクライアントから使うぶんには、たぶんこんな苦労はしないはずです。裏を返すと、CIやスクリプトから手続き的に叩きたい場合は、--browserUrl で自前Chromeに繋ぐ形が現状いちばん素直、というのが今回の収穫でした。

自社ではLPのフォーム送信検証をエージェントにやらせているので、そこで使っているブラウザ操作系をこれに寄せられるか、take_snapshotuid 駆動と lighthouse_audit あたりを軸に、もう少し実ページで様子を見てみようと思います。CLIが experimental なのと、任意Chromeフラグの素通し口が無いのは、運用に載せる前にもう一度確認しておきたいところ。

もっと良い回し方(特に、自前起動なしでrootコンテナでも動かすやり方)を知っている方がいたら、ぜひ教えてください。