stealth-browser-mcp をどう作ったか

英語からの翻訳です。原文は English.


なぜ作ったか

2025 年 8 月、私の AI エージェントは Playwright で Web を見ようとするたびにブロックされていました。私の Chrome では普通に開くページが、エージェントには空の殻を返してくるのです。エージェントにもっと自分のやり方でブラウズしてほしかったので、自分で直すことにして GitHub に stealth-browser-mcp として公開しました。

考え方は単純です。自分から正体を明かすテスト用ブラウザではなく、エージェントに本物の Chrome を動かしてほしい。しかも動かしながら、私が DevTools で見ているものをエージェントにも見せたい。

同じ問題を抱えている人は少しはいるだろうと思っていました。今は 3,000 に近いスターと 390 のフォークがあり、一度も会ったことのない 9 人のプルリクエストが取り込まれています。この記事は、どう動いているかと、作る中で何を学んだかの話です。

AI エージェントが stdio JSON-RPC で stealth-browser-mcp の FastMCP サーバーと話し、そのサーバーが CDP で本物の Chrome を動かす図。下に stdout guard、信頼できる入力、ネットワークツール、要素クローンのモジュールが並ぶ。

どう組み合わさっているか

stealth-browser-mcp は FastMCP の上に Python で書いた MCP サーバーです。エージェントは Claude Code、Cursor、Codex といった任意の MCP クライアントから話しかけます。サーバーは nodriver と Chrome DevTools Protocol を通して、本物の Chrome、Edge、Chromium を動かします。

この分け方が設計のすべてです。nodriver はユーザーが起動するのと同じように普通のブラウザを立ち上げ、私に CDP 接続を渡します。必要なものはすべてその接続の中にあるので、サーバーはその上に載った丁寧なツール群という性格になります。

MCP のおかげでエージェントが使える形になります。ブラウザにできることはすべて、モデルが選べる名前の付いたツールになり、モデルは普通のテキスト結果を受け取ります。ブラウザのライフサイクルはサーバーが持つので、エージェントは Playwright のコードを書く必要もありません。

誰も見ない部分

このプロジェクトの最初の頃、ツールは動くのに、その直後にクライアントが何のエラーも見せずに接続を切っていました。

原因は print() でした。stdio トランスポートではファイルディスクリプタ 1 が JSON-RPC の通り道です。自分のコードや依存関係の print() は JSON-RPC のフレームの途中に落ちて、クライアントは身を守るために接続を切ります。nodriver も出力するので、自分のファイルだけきれいにしても足りませんでした。

修正は stdio_guard.py の小さなガードで、トランスポートを起動する直前に差し込みます。sys.stdout を、Python 側の書き込みを stderr に流すプロキシに差し替えます。トランスポートは差し替え前に本物の stdout バッファを包んでいるので動き続け、二度と sys.stdout を見ません。

このファイルのおかげでデバッグログも普通に使えます。STEALTH_BROWSER_DEBUG=1 を設定すれば、プロトコルは無傷のまま stderr に詳しいログが出ます。

信頼できる入力

多くの自動化は JavaScript から element.click() を呼んでクリックします。それは、ページが「本物の手から来たクリックか」を確認するまで通用します。私は入力イベントに入力らしく見えてほしかった。

input_actions.py は本物の CDP 入力イベントを送ります。クリックはノードを解決し、画面内にスクロールし、位置が動かなくなるまで待ってから、実際の座標で mousePressed と mouseReleased を送ります。マウスは目標へ瞬間移動せず、短いイージング付きの経路を少しの揺らぎとともに移動し、最後の位置はタブごとに覚えて次の移動が本当のカーソル位置から始まるようにします。

入力も同じです。各文字は US 配列が生成するキーボードのフィールドに変換され、本物のキーイベントとして送られます。フィールドを空にするときは、value を空にして偽のイベントを一発撃つのではなく、人がやるように中身を選択して削除します。

この細部は見た目以上に効きます。毎週のベンチマークには入力忠実度という検査があり、クリックと入力が本物の順序で信頼できるイベントを生み出さない場合、あるいはページに何かを注入した場合は失敗します。

ツールが実際に何をするか

サーバーは 11 セクションに 101 のツールを載せていて、全部いらないときは小さい面だけ動かせます。

モードツール数使うとき
全部101下のすべて
--minimal21移動、クリック、ページを読む
--disable-*任意フックなど特定のセクションを外す
--xpool-safe84Runtime.enable を起こすツールを外す

--list-sections はセクションとサイズを表示するので、自分用の部分集合を組めます。セクションに分ける理由はコンテキストです。公開したツールはどれもエージェントのプロンプトでトークンを食い、101 個の選択肢があるモデルは 21 個のモデルより間違ったものを選びやすくなります。

ネットワーク検査

エージェントはページが送ったリクエストを一覧し、ヘッダー、ペイロード、取得した本文を読めます。サイトの API がどう動いているか見るときに一番使うツールです。リクエストをページから応答まで追い、私が DevTools を開かなくても質問に答えられます。

動的フック

フックはエージェントが実行時に書く小さな Python 関数で、各リクエストを見てブロック、リダイレクト、応答の差し替え、または素通しができます。うるさい解析呼び出しに気づいたエージェントは、それを落とすフックを書いてノイズなしでページを読めます。

制限はわざとです。フックのコードは len、str、any、all だけの組み込みホワイトリストで動くので、ファイルを読んだり自分で通信したりはできません。検証器が各関数を解析し、eval、exec、open、input を実行前に拒否します。

要素のクローン

CSS セレクタを渡すと、サーバーは計算済みスタイル、DOM 構造、アセット、アニメーションごと要素を抜き出します。それがあれば、近似ではなく本物のフォントと余白でセクションを別の場所に作り直せます。

Augment Code の hero クローンが一番の実例です。全部が Claude へのひとつのメッセージでした。ブラウザを起動し、サイトへ行き、hero をクローンする。要素を抽出し、2 分ほどでレスポンシブな再現を返してきました。

Augment Code の hero をクローンとして作り直したスクリーンショット。元の見出し、レイアウト、スタイルが HTML で再現されている

ページバインディング

ページバインディングは逆向きの接続です。ページは window.<名前>(...) を受け取り、各呼び出しはプロミスを返してエージェントのキューに入ります。エージェントは get_page_binding_calls で読み、resolve_page_binding_call で答えます。

ページからもエージェントからも、サーバーのホストでコードは走りません。引数は JSON である必要があり、キューの長さもペイロードの大きさも上限があるので、ページがループで呼んでもメモリを食い尽くせません。

ステルスについて間違っていたこと

最初の README は、ステルスが解決済みの問題のように読めました。そうではないし、話し方を変えました。

リポジトリは毎週 GitHub Actions で Linux、macOS、Windows の headless と有窓、現在と前の Chrome でベンチマークを回します。起動フラグ、ローカルの指紋プローブ、スクリプト注入、入力忠実度、Sannysoft、Intoli、CreepJS、Cloudflare の JS チャレンジ、TLS と HTTP/2 の指紋を調べます。各実行は JSON を docs/stealth-results/ に書き、README の表は手で書くのではなくそのファイルから生成します。

直近の結果は、主張よりも役に立つ形で正直です。macOS と Windows はすべて通ります。Linux のランナーは 7 通り、1 が劣化、1 が失敗します。GitHub の Linux ランナーに GPU がないので Chrome が WebGL をソフトウェアレンダラで描き、Sannysoft がそれを見るためです。本物の GPU があるマシンでは出ません。

以前通っていた項目が悪化すると、ワークフローが自分で stealth-regression の issue を開きます。私が一番大事にしているのはそこです。検出は毎月変わるので、README の数字はある日の測定値です。今日の保証ではありません。

信頼モデル

このサーバーは、忘れがちなほど強い権限を持ちます。ページで JavaScript を実行し、Cookie とストレージを読み、通信を横取りして書き換え、CDP コマンドを実行し、許可したフォルダからファイルを送れます。

だから信頼モデルを文章にしてあります。エージェントまたは MCP クライアントがセキュリティの主体です。推奨は、信頼できるクライアントとのローカル stdio です。HTTP トランスポートを使うなら STEALTH_BROWSER_MCP_AUTH_TOKEN を設定し、開かれたインターネットに出さないでください。

ファイル送信は BROWSER_FILE_UPLOAD_ALLOWED_DIRS のフォルダからしか読みません。ツールは実パスを解決して許可されたルートと比べてからファイルに触るので、この検査を回り込む道はありません。

学んだこと

一番教訓になったのは stdout のバグでした。9 割動くツールは、はっきり失敗するツールより悪い。残りの 1 割を、自分のコードを疑って過ごすことになるからです。ガードを入れてからは、失敗はすべて本物の失敗になりました。

二つ目は README の正直さです。すべての検出器を通ると書けば一週間はスターが増えたでしょうが、プロジェクトを信頼する理由を全部失っていました。公開の場で失敗できるベンチマークだけが、出す価値のあるベンチマークです。

三つ目は、問題が本物なら人は使ってくれるということです。私の問題はエージェントがブロックされることで、390 のフォークのあとで分かったのは、多くの人が同じ問題を抱えていたということでした。

リポジトリは github.com/vibheksoni/stealth-browser-mcp にあります。自動化が許されているサイトで使い、向ける先のサイトの規約を確認してください。