25
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

CLIでもできた!PlaywrightMCPと同じ動き+トークン90%削減

25
Last updated at Posted at 2026-02-11

TL;DR(要約)

  • 課題: Playwright MCPでブラウザ操作すると、1操作で5,000〜10,000トークン消費
  • 解決: HTTP API + CLIスクリプト方式で、トークン消費を90〜92%削減
  • 結果: 複数回の操作を含むタスクで、トークン消費を10分の1に削減
  • アーキテクチャ: Claude Code (Bash) → CLIスクリプト (curl) → HTTP APIサーバー → Playwright

対象読者

  • Claude CodeでPlaywrightを使っている方
  • Playwright MCPのトークン消費に悩んでいる方
  • ブラウザ操作の自動化に取り組んでいる方
  • HTTP APIベースの自動化に興味がある方

1. 背景:Playwright MCPの問題点

AIによるブラウザ自動化の流行

最近、AIにブラウザ操作を自動化させることが増えています。

なぜAIにブラウザ操作させるのか?

  • テスト自動化: 「このページでログインしてエラーが出ないか確認して」と自然言語で指示できる
  • スクレイピング: 画面構造が変わっても、AIが柔軟に対応できる
  • RPA(業務自動化): 定型的なWebタスクを自動化

その代表的なツールがPlaywright MCPです。

Playwright MCPとは

Playwright MCPは、Claude CodeからPlaywrightブラウザ操作を直接呼び出せるMCP(Model Context Protocol)ツール群です。

主なツール:

  • mcp__playwright__browser_navigate - ページ遷移
  • mcp__playwright__browser_snapshot - アクセシビリティツリー取得(画面構造の詳細)
  • mcp__playwright__browser_click - 要素クリック
  • mcp__playwright__browser_type - テキスト入力
  • mcp__playwright__browser_take_screenshot - スクリーンショット撮影
  • mcp__playwright__browser_run_code - 任意のPlaywrightコード実行

直面した問題:大量トークン消費

しかし、この便利な仕組みには大きな問題があります。

トークンとは?

  • LLM(大規模言語モデル)のAPI呼び出しで消費される単位
  • 入力・出力の文字数に応じて消費される
  • トークン消費 = コスト(例:Claude Sonnet 4.5は$3/100万トークン)

何が問題なのか?

AIがブラウザを操作する際、画面情報をすべてAIに送る必要があります。この情報量が膨大なため:

  • トークンを大量に消費 → コストが爆増
  • 繰り返し操作すると、さらにコストが積み上がる
  • Claude Codeの会話が圧縮されやすくなる(文脈が失われる)

具体的に見てみましょう。

問題1:browser_snapshotの巨大な出力

browser_snapshotは、アクセシビリティツリー全体をMarkdown形式で返します。

実例(ログイン画面1回の出力):

heading "ログイン" [level=1]
  textbox "メールアドレス"
  textbox "パスワード"
  checkbox "ログイン状態を保持する"
    text "ログイン状態を保持する"
  button "ログイン"
  link "パスワードを忘れた方"
  link "新規登録"
  ... (以下、ヘッダー、フッター、サイドバー等の全要素が続く)

問題点:

  • 1回の出力で5,000〜10,000トークン消費
  • 必要なのは入力欄とボタンだけなのに、ページ全体の情報が返ってくる
  • Claude Codeの会話に全文が含まれるため、トークン消費が累積

問題2:何度も呼び出す必要がある

典型的なブラウザ操作フロー(ログイン → 2段階認証):

  1. browser_navigate - ログイン画面にアクセス
  2. browser_snapshot - 画面構造を確認(5,000トークン)
  3. browser_type - メールアドレス入力
  4. browser_type - パスワード入力
  5. browser_click - ログインボタンクリック
  6. browser_snapshot - 2段階認証画面を確認(8,000トークン)
  7. browser_take_screenshot - スクリーンショット保存
  8. ... 続く

1つのタスクで20〜30回のMCPツール呼び出しが発生

各呼び出しの結果が会話履歴に残るため:

  • 1タスクで50,000〜100,000トークン消費
  • Claude Codeの会話が頻繁に圧縮される
  • 途中で文脈が失われやすい

問題3:複数タスクでのスケーラビリティ

複数のブラウザ操作タスクを実行する場合:

必要な総トークン数:

  • 10タスク × 80,000トークン = 800,000トークン
  • Sonnet 4.5のコスト:$3/1Mトークン(入力) = $2.40

トークン消費以上に問題なのは:

  • Claude Codeの会話が頻繁に圧縮される
  • 途中で文脈が失われやすい
  • エラー発生時の復旧が困難

なぜMCPは巨大な出力を返すのか

Playwright MCPの設計思想:

  • 汎用性重視: あらゆるWebサイトに対応するため、ページ全体の情報を返す
  • LLMの判断に委ねる: どの要素が重要かはLLMが判断する前提
  • 詳細性優先: セレクタ取得のために全要素の情報が必要

これは対話的な操作には最適ですが、バッチ処理や繰り返し操作には不向きです。

求められた解決策

必要な要件:

  1. トークン消費を大幅に削減(目標:80%削減)
  2. Playwrightの操作能力は維持
  3. Claude Codeから呼び出せる
  4. 既存のワークフローと統合できる

HTTP API + CLIスクリプト方式の採用へ


2. CLIとMCPの比較:実測値

単一操作での比較

指標 MCP版 CLI版 削減率
navigate + snapshot 6,000〜11,000トークン 200〜500トークン 95%削減
fill 300〜500トークン 50〜100トークン 80%削減
click 500〜1,000トークン 100〜200トークン 80〜90%削減

典型的な操作フローでの比較

ログイン + 2段階認証の例:

ステップ MCP版 CLI版
1. navigate + snapshot 8,000トークン 500トークン
2. fill (email) 500トークン 100トークン
3. fill (password) 500トークン 100トークン
4. click + snapshot 8,000トークン 400トークン
5. fill (2FA code) 500トークン 100トークン
6. click + snapshot 10,000トークン 500トークン
合計 27,500トークン 1,700トークン
削減率 - 93.8%削減

複数タスクでの比較

10タスク実行時の試算:

指標 MCP版 CLI版 削減効果
総トークン消費 500,000〜1,000,000 50,000〜100,000 90%削減
コスト(Sonnet 4.5) $1.50〜$3.00 $0.15〜$0.30 90%削減
文脈維持 5〜10タスクでリセット必要 連続処理可能 -

削減の仕組み

MCP版(browser_snapshot):

  • 出力: 5,000〜10,000トークン
  • 内容: ページ全体のアクセシビリティツリー(すべての要素、階層構造、すべての属性)

CLI版(HTTP APIレスポンス):

{
  "success": true,
  "action": "navigate",
  "url": "https://example.com/login",
  "title": "ログイン",
  "snapshot": "heading \"ログイン\" [level=1]\n  textbox \"メールアドレス\"\n  textbox \"パスワード\"\n  button \"ログイン\"",
  "tabs": { "count": 1, "current": 0, "newTabOpened": false }
}
  • 出力: 200〜500トークン
  • 削減率: 90〜95%

3. CLI版のアーキテクチャ設計

設計方針

MCP版の問題を解決するために、以下の方針で設計しました:

1. 出力を最小化

  • 必要な情報だけを返す(MCPのような全要素情報ではなく、操作結果のみ)
  • JSON形式で構造化(Claude Codeが解釈しやすい)
  • トークン消費を10分の1以下に

2. ステートフルな設計

  • ブラウザセッションを永続化(毎回起動せず、必要な期間だけ維持)
  • 複数のセッションに対応(並行処理も可能)
  • Context/Pageを保持(認証状態を維持)

3. シンプルなインターフェース

  • CLIスクリプト経由で呼び出し(Claude CodeからBashツールで実行)
  • HTTP API経由で操作(curlで簡単にアクセス)
  • JSON入出力(解析が容易)

アーキテクチャ概要図

┌─────────────────────────────────────────────────┐
│           Claude Code                            │
│                                                 │
│  ┌─────────────────────────────────────────┐   │
│  │        Bash Tool                        │   │
│  │                                         │   │
│  │  bash screen-check.sh \                │   │
│  │    --session-id 1 \                    │   │
│  │    --action navigate \                 │   │
│  │    --url "https://..."                 │   │
│  └────────────┬────────────────────────────┘   │
└───────────────┼────────────────────────────────┘
                │ シェルスクリプト呼び出し
                ▼
┌─────────────────────────────────────────────────┐
│      screen-check.sh (CLIシェルスクリプト)       │
│                                                 │
│  1. 引数解析(--session-id, --action等)        │
│  2. JSONリクエスト構築                           │
│  3. curl でHTTP APIを呼び出し                    │
│  4. JSONレスポンスを標準出力                     │
└────────────┬────────────────────────────────────┘
             │ HTTP POST request
             │ Content-Type: application/json
             ▼
┌─────────────────────────────────────────────────┐
│   browser-server.ts (HTTP APIサーバー)          │
│   Port: 8000 + sessionId                       │
│                                                 │
│  ┌─────────────────────────────────────────┐   │
│  │ POST /action                            │   │
│  │  - action: "navigate|fill|click|..."   │   │
│  │  - url, selector, value, ...           │   │
│  └─────────────────────────────────────────┘   │
│                                                 │
│  ┌─────────────────────────────────────────┐   │
│  │ Playwrightブラウザインスタンス           │   │
│  │  - browser: Browser                    │   │
│  │  - context: BrowserContext             │   │
│  │  - page: Page                          │   │
│  └─────────────────────────────────────────┘   │
│                                                 │
│  1. リクエスト解析                              │
│  2. Playwright操作実行                          │
│  3. 結果をJSON形式で返す(最小限の情報)         │
└────────────┬────────────────────────────────────┘
             │ JSON Response
             │ { "success": true, "url": "...", ... }
             ▼
      Claude Codeに最小限の出力を返す

各コンポーネントの役割

1. browser-server.ts(HTTP APIサーバー)

役割:

  • Playwrightブラウザインスタンスを保持
  • HTTP APIエンドポイント(POST /action)を公開
  • 操作リクエストを受け取ってPlaywrightで実行
  • 結果を最小限のJSONで返す

主要なエンドポイント:

GET  /health          - サーバー稼働確認
POST /action          - Playwright操作実行
GET  /shutdown        - サーバー停止

2. CLIシェルスクリプト群

  • start-browser.sh: ブラウザサーバー起動
  • screen-check.sh: 画面操作(navigate, fill, click, snapshot)
  • stop-browser.sh: ブラウザサーバー停止

CLI版とMCPの違い、そしてHTTP APIによる解決

MCPツールの特徴:完全対話型

MCP版では、Claude CodeがPlaywrightツールを対話的に呼び出して操作できます:

ユーザー: 「ログインページに移動してください」
Claude Code: mcp__playwright__browser_navigate({ url: "..." })
Claude Code: mcp__playwright__browser_snapshot()  ← 画面構造を取得
Claude Code: 「ログインページが表示されました。次は何をしますか?」

ユーザー: 「ユーザーIDを入力してください」
Claude Code: mcp__playwright__browser_type({ ref: "textbox[name='ユーザーID']", text: "..." })

AIが画面を見て判断しながら操作を進められます。

従来のCLIツールの問題

一方、通常のCLIツール(Bashツール)では、すべての操作を事前に決める必要がありました:

# すべての手順を事前に決めておく必要がある
bash some-script.sh step1
bash some-script.sh step2
bash some-script.sh step3

画面の状態を確認せずに、盲目的に実行するしかありません。

HTTP APIによる解決:画面確認しながら処理

しかし、HTTP APIを工夫することで、この問題を解決できます:

# 1. ページ遷移 → レスポンスに画面構造(snapshot)を含める
bash screen-check.sh --session-id 1 --action navigate --url "https://..."
# → { "success": true, "snapshot": "heading \"ログイン\" [level=1]\n  textbox \"ユーザーID\"...", ... }

# Claude Codeがsnapshotを見て「textboxがある」と判断

# 2. 入力 → 再びレスポンスに画面構造を含める
bash screen-check.sh --session-id 1 --action fill --selector 'textbox[name="ユーザーID"]' --value "..."
# → { "success": true, "snapshot": "...", ... }

# Claude Codeが画面を確認しながら次の操作を決める

ポイント:

  • 各操作のレスポンスに画面構造(snapshot)を含める
  • Claude Codeがそれを見て次の操作を判断できる
  • 結果として、1ステップずつ画面を確認しながら処理できる

MCPほど対話的ではないものの、トークン消費を抑えつつ、画面確認しながら処理することが可能になりました。


4. 実装:HTTP API Server(browser-server.ts)

グローバルステート管理

let browser: Browser | null = null;
let context: BrowserContext | null = null;
let page: Page | null = null;

なぜグローバル変数を使うのか:

  • ステートフル: ブラウザセッションを維持(認証状態、Cookie、履歴など)
  • 高速: 毎回ブラウザを起動せず、既存インスタンスを再利用
  • HTTP APIの制約: HTTP APIはステートレスなので、サーバー側でステートを保持する必要がある

主要な操作ハンドラー

navigate(ページ遷移)

if (req.action === 'navigate') {
  context = await browser.newContext({
    userAgent: 'Mozilla/5.0 ...',
    viewport: { width: 1920, height: 1080 },
    locale: 'ja-JP',
    timezoneId: 'Asia/Tokyo'
  });

  page = await context.newPage();

  // webdriverフラグを隠す(CAPTCHA対策)
  await page.addInitScript(() => {
    Object.defineProperty(navigator, 'webdriver', {
      get: () => undefined,
    });
  });

  await page.goto(req.url, { waitUntil: 'networkidle' });
}

ポイント:

  • 新しいContextを作成してクリーンな状態から開始
  • webdriverフラグを隠して自動化検出を回避

fill(テキスト入力)

if (req.action === 'fill') {
  // セレクタ解析: "textbox[name='メールアドレス']" → role="textbox", name="メールアドレス"
  const match = req.selector.match(/^(\w+)\[name=['"](.+)['"]\]$/);
  const [, role, name] = match;

  const element = page.getByRole(role as any, { name });
  await element.fill(req.value);
}

ポイント:

  • role+name形式のセレクタ(Playwright推奨方式)
  • 簡易的な正規表現パーサーで解析

click(要素クリック + 新規タブ検出)

if (req.action === 'click') {
  const element = page.getByRole(role as any, { name });

  // クリックと新規タブ検出を並行実行
  const [newPage] = await Promise.all([
    context.waitForEvent('page', { timeout: 3000 }).catch(() => null),
    element.click()
  ]);

  if (newPage) {
    // 新規タブが開いた場合、そちらに切り替え
    await newPage.waitForLoadState('networkidle');
    page = newPage;
  } else {
    // 同じタブ内での画面遷移
    if (req.waitForText) {
      await page.getByText(req.waitForText).waitFor({ timeout: 10000 });
    }
  }
}

ポイント:

  • 新規タブの自動検出と切り替え
  • waitForTextで特定のテキストが表示されるまで待機

snapshot(画面構造取得)

// 各アクション実行後に自動的にスナップショット取得
const snapshot = await page.locator('body').ariaSnapshot();
const currentUrl = page.url();
const title = await page.title();

const result: ActionResult = {
  success: true,
  action: req.action,
  url: currentUrl,
  title: title,
  snapshot: snapshot,
  tabs: {
    count: pages.length,
    current: currentIndex,
    newTabOpened: false
  }
};

ポイント:

  • ariaSnapshot()を使用(MCPと同じAPI)
  • 最小限の情報のみ返す(snapshotは常に取得するが、詳細は必要な時だけ)

ポート番号管理

const BASE_PORT = 8000;
const port = BASE_PORT + parseInt(sessionId, 10);

:

  • --session-id 1 → Port 8001
  • --session-id 2 → Port 8002

メリット:

  • 複数セッションの並行処理が可能
  • ポート番号の衝突回避

5. 実装:CLIシェルスクリプト

start-browser.sh(ブラウザサーバー起動)

#!/bin/bash
set -e

# バックグラウンドでサーバー起動
nohup ./node_modules/.bin/tsx scripts/browser-server.ts start --session-id "$SESSION_ID" \
  > "$TEMP_DIR/server-${SESSION_ID}.log" 2>&1 &

# ポート番号ファイルが作成されるまで待機(最大15秒)
for i in {1..30}; do
  if [ -f "$PORT_FILE" ]; then
    PORT=$(cat "$PORT_FILE")
    if curl -s "http://127.0.0.1:${PORT}/health" > /dev/null 2>&1; then
      echo "{\"success\": true, \"sessionId\": \"$SESSION_ID\", \"port\": $PORT}"
      exit 0
    fi
  fi
  sleep 0.5
done

ポイント:

  • べき等性(既に起動している場合は何もしない)
  • ヘルスチェック待機(実際にHTTP APIが応答するまで待つ)

screen-check.sh(画面操作)

# JSONリクエスト構築
build_json() {
  local json="{\"action\": \"$ACTION\""

  if [ -n "$URL" ]; then
    local escaped_url=$(printf '%s' "$URL" | sed 's/\\/\\\\/g; s/"/\\"/g')
    json="$json, \"url\": \"$escaped_url\""
  fi

  # ... その他のフィールド

  json="$json}"
  echo "$json"
}

# HTTP POST リクエスト送信
RESPONSE=$(curl -s -X POST "$ENDPOINT" \
  -H "Content-Type: application/json" \
  -d "$JSON_BODY" \
  --max-time 60)

実行例:

# ページ遷移
bash screen-check.sh --session-id 1 --action navigate --url "https://example.com"

# テキスト入力
bash screen-check.sh --session-id 1 --action fill \
  --selector 'textbox[name="メールアドレス"]' \
  --value "user@example.com"

# ボタンクリック
bash screen-check.sh --session-id 1 --action click \
  --selector 'button[name="ログイン"]' \
  --wait-for-text "認証コード"

6. MCPでできてCLIでできないこと

主な制約と回避策

制約 影響度 回避策
自由なセレクタ取得 よく使うパターンをサーバーに追加実装
任意のコード実行 必要な操作をアクションとして追加
完全対話型ではない snapshotで1ステップずつ画面確認可能
複雑なUI操作 必要になったらサーバーに追加実装
エラーメッセージ詳細度 サーバーログを確認

1. 自由なセレクタ取得

MCP版: あらゆるセレクタ(CSS、XPath、nth()、チェーンセレクタ)を使用可能

CLI版: role[name="..."] 形式のみサポート

実際の影響:

  • Playwrightの推奨方式はgetByRole(role, { name })
  • 98%以上のケースでrole[name="..."]形式で対応可能
  • 例外的なケースは、サーバー側に新しいアクションを追加

2. 任意のPlaywrightコード実行

MCP版: browser_run_codeで任意のコード実行可能

CLI版: 事前定義されたアクションのみ

回避策:

// browser-server.tsに新しいアクションを追加
if (req.action === 'clear_storage') {
  await page.context().clearCookies();
  await page.evaluate(() => {
    localStorage.clear();
    sessionStorage.clear();
  });
}

3. 完全対話型ではない

MCP版: ユーザーと対話しながらリアルタイムにデバッグ可能

CLI版: snapshotで画面確認しながら処理できるが、MCPほど対話的ではない

実際の影響:

  • HTTP APIがsnapshotを返すため、1ステップずつ画面を確認しながら処理可能
  • ただし、MCPのように「ユーザーが途中で指示を変える」ような完全対話型ではない
  • トークン消費を抑えつつ画面確認できるため、実用上は問題なし

重要な判断基準

  • 完全対話型が必要: MCP版が最適(ユーザーが途中で指示を変える)
  • トークン消費を抑えたい: CLI版が最適(snapshotで画面確認しながら処理)
  • どちらも画面確認可能: HTTP APIのsnapshotレスポンスで状態を把握できる

7. まとめ

CLI版を選ぶべき状況

  1. 同じ操作を何度も実行する(繰り返し操作)
  2. 複数のタスクを連続実行する(バッチ処理)
  3. トークン消費を抑えたい(コスト削減)
  4. 文脈を保持したい(会話圧縮を避ける)

MCP版を選ぶべき状況

  1. 完全対話型が必要(ユーザーが途中で指示を変える)
  2. 複雑なデバッグ(対話的に試行錯誤が必要)
  3. 1回限りの操作(繰り返しなし)
  4. 複雑なセレクタが必要(role+name形式で対応できない)

最終的な判断

MCP版とCLI版は対立するものではなく、補完関係にある

  • 完全対話型が必要: MCP版(ユーザーが途中で指示を変える)
  • トークン消費を抑えたい: CLI版(snapshotで画面確認しながら処理)

どちらも画面を確認しながら処理できるが、トークン消費の違いで使い分けることで、柔軟性と低コストを両立できます。

学んだこと

1. トークン消費の最適化は重要

  • MCPツールは便利だが、大量のトークンを消費する
  • 繰り返し操作では、必要最小限の情報だけを返すAPIを設計すべき
  • トークン消費を意識した設計が、効率的な自動化の鍵

2. ツールの適材適所

  • 完全対話型が必要: MCP版が最適
  • トークン消費を抑えたい: CLI版が最適
  • どちらも画面確認しながら処理できる: snapshotで状態を把握可能

3. HTTP APIの汎用性

  • HTTP APIは様々なツールから呼び出せる
  • Claude Code以外にも応用可能
  • 拡張性とメンテナンス性が高い

この記事が、Playwright MCPのトークン消費に悩んでいる方の参考になれば幸いです!

参考資料

25
13
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
25
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?