🌟

Claude Desktopでplaywright MCPを動かしてみた

に公開

話題になっているMCPを試したいと思い、Claude Desktop上でPlaywright MCPを動かしてみました。その手順や実行結果をこの記事にまとめています。

1. MCP とは

MCP(Model Context Protocol)は、LLM が外部のデータやツールと接続するための標準的なプロトコルのことで、LLMと外部の情報を連携させるための仕組みとして注目されています。
AI用のUSB-Cポートとよく表現されます。

https://norahsakal.com/blog/mcp-vs-api-model-context-protocol-explained/

2. playwrightとは

Playwrightは、Microsoftが開発したWeb UI自動化テストフレームワークで、E2Eテストを効率的に行えるようにしてくれます。E2Eテストコードを記述することでブラウザ操作を自動化できます。
https://playwright.dev/

2. Playwright MCP とは

Playwright MCPは、Microsoft Playwrightの技術を活用し、ブラウザ操作の自動化を行うためのMCPサーバーで、自然言語によるブラウザ操作が可能になります。
従来のスクリーンショットベースの方法と異なり、以下のような特徴があります。

  • 高速 & 軽量 – 画像解析や座標指定を行わず、アクセシビリティツリーを利用するのでページ構造をテキストベースで取得でき、高速な動作が実現できる。
  • LLM フレンドリー – 構造化データ(アクセシビリティツリー)から直接ページの要素を把握するので、LLmがそのまま要素を指定・操作できる。
  • 決定論的 – 要素をref(要素参照)によって特定するため、スクショ判定よりブレが少なく安定した操作が可能。

活用例

  • E2Eテスト
  • webスクレイピング
  • フォーム入力自動化

https://github.com/microsoft/playwright-mcp/tree/main

3. Claude Desktop から接続する手順

環境準備

環境準備として以下をインストールする必要があります。

  • claude for desktopをインストール
  • Node.jsをインストール
  • npmをインストール

Claudeの設定ファイル編集

ClaudePlaywright MCPの公式の説明に従い、
C:\Users\<ユーザー名>\AppData\Roaming\Claude\config.jsonに以下の設定を追記します。

{
  "mcpServers": {
    "playwright": {
      "command": "npx",
      "args": ["@playwright/mcp@latest"]
    }
  }
}

Claude を再起動し、メニューバーの「設定>開発者」にplaywrightが入っていればOKです。

4. ブラウザ操作をさせてみる

Claudeから自然言語で以下のように指示します。

https://books.toscrape.com/ にアクセスして、最初の 10 冊の本の
・タイトル
・価格
・在庫情報
を一覧で取得して、テーブル形式で教えてください。

Claudeの出力


指示通りに出力されました。

内部的にPLaywrightが実行したコードやページの構造が記録されています。

5. 簡単なE2Eテストを実行

https://the-internet.herokuapp.com/ で簡単なE2Eテストを実行させてみます。
プロンプトは以下です。Claudeのフリープランでは一度に出力できる文字数に制限があるのでテスト項目は2つとしました。

https://the-internet.herokuapp.com/ で以下のテストを Playwright MCP で実行してください。
* Checkboxes: チェックボックスの選択・解除
* Forgot Password: パスワードリセット機能


ブラウザが立ち上がって自動で操作されている様子が確認できます。

生成されたPlaywrightのTypeScriptテストコード

import { test, expect } from '@playwright/test';

test.describe('The Internet - UI Tests', () => {
  const baseUrl = 'https://the-internet.herokuapp.com';

  test('Checkboxes - Selection and Deselection', async ({ page }) => {
    // Step 1: Navigate to checkboxes page
    await page.goto(`${baseUrl}/checkboxes`);
    
    // Verify page is loaded
    await expect(page.locator('h3')).toContainText('Checkboxes');
    
    // Step 2: Verify initial state of checkboxes
    const checkbox1 = page.locator('input[type="checkbox"]').first();
    const checkbox2 = page.locator('input[type="checkbox"]').nth(1);
    
    // Initial state: checkbox 1 unchecked, checkbox 2 checked
    await expect(checkbox1).not.toBeChecked();
    await expect(checkbox2).toBeChecked();
    
    // Step 3: Check first checkbox
    await checkbox1.click();
    await expect(checkbox1).toBeChecked();
    
    // Step 4: Uncheck second checkbox
    await checkbox2.click();
    await expect(checkbox2).not.toBeChecked();
    
    // Step 5: Verify final state
    // Final state: checkbox 1 checked, checkbox 2 unchecked
    await expect(checkbox1).toBeChecked();
    await expect(checkbox2).not.toBeChecked();
  });

  test('Forgot Password - Password Reset Functionality', async ({ page }) => {
    // Step 1: Navigate to forgot password page
    await page.goto(`${baseUrl}/forgot_password`);
    
    // Verify page is loaded
    await expect(page.locator('h2')).toContainText('Forgot Password');
    
    // Step 2: Enter email address
    const emailField = page.getByRole('textbox', { name: 'E-mail' });
    await emailField.fill('test@example.com');
    
    // Verify email was entered correctly
    await expect(emailField).toHaveValue('test@example.com');
    
    // Step 3: Click retrieve password button
    const retrieveButton = page.getByRole('button', { name: 'Retrieve password' });
    await retrieveButton.click();
    
    // Step 4: Handle expected server error (as observed during manual testing)
    // Note: The server currently returns an Internal Server Error
    // In a real test scenario, you might want to handle this differently
    // For now, we'll check for either success or the known error state
    
    try {
      // Wait for response (either success message or error)
      await page.waitForLoadState('networkidle', { timeout: 5000 });
      
      // Check if we get an error page
      const errorHeading = page.locator('h1');
      if (await errorHeading.isVisible()) {
        const errorText = await errorHeading.textContent();
        if (errorText?.includes('Internal Server Error')) {
          console.log('Expected server error occurred - this is a known issue with the test site');
          // You might want to report this as a separate test result
        }
      } else {
        // Check for success indicators if the form works properly
        // This would be the ideal scenario
        await expect(page.locator('body')).toContainText(/password.*sent|email.*sent|check.*email/i);
      }
    } catch (error) {
      console.log('Timeout or unexpected behavior in forgot password flow');
    }
  });

  // Additional test for better coverage of checkbox functionality
  test('Checkboxes - Toggle Multiple Times', async ({ page }) => {
    await page.goto(`${baseUrl}/checkboxes`);
    
    const checkbox1 = page.locator('input[type="checkbox"]').first();
    const checkbox2 = page.locator('input[type="checkbox"]').nth(1);
    
    // Test multiple toggles to ensure consistent behavior
    
    // Initial state
    await expect(checkbox1).not.toBeChecked();
    await expect(checkbox2).toBeChecked();
    
    // First toggle
    await checkbox1.click();
    await checkbox2.click();
    await expect(checkbox1).toBeChecked();
    await expect(checkbox2).not.toBeChecked();
    
    // Second toggle (back to original)
    await checkbox1.click();
    await checkbox2.click();
    await expect(checkbox1).not.toBeChecked();
    await expect(checkbox2).toBeChecked();
    
    // Third toggle
    await checkbox1.click();
    await checkbox2.click();
    await expect(checkbox1).toBeChecked();
    await expect(checkbox2).not.toBeChecked();
  });

  // Test for form validation on forgot password
  test('Forgot Password - Empty Email Validation', async ({ page }) => {
    await page.goto(`${baseUrl}/forgot_password`);
    
    // Try to submit without entering email
    const retrieveButton = page.getByRole('button', { name: 'Retrieve password' });
    await retrieveButton.click();
    
    // Check if browser validation kicks in or form handles empty submission
    const emailField = page.getByRole('textbox', { name: 'E-mail' });
    
    // Modern browsers typically show validation messages for required fields
    // The actual behavior depends on the HTML validation attributes
    try {
      await expect(emailField).toBeFocused({ timeout: 2000 });
    } catch {
      // If no validation, the form might just submit anyway
      console.log('No client-side validation detected for empty email');
    }
  });
});

// Additional utility test for navigation
test.describe('Navigation Tests', () => {
  const baseUrl = 'https://the-internet.herokuapp.com';

  test('Navigate to test pages from homepage', async ({ page }) => {
    // Start from homepage
    await page.goto(baseUrl);
    await expect(page.locator('h1')).toContainText('Welcome to the-internet');
    
    // Navigate to Checkboxes
    await page.getByRole('link', { name: 'Checkboxes' }).click();
    await expect(page.locator('h3')).toContainText('Checkboxes');
    
    // Go back to homepage
    await page.goto(baseUrl);
    
    // Navigate to Forgot Password
    await page.getByRole('link', { name: 'Forgot Password' }).click();
    await expect(page.locator('h2')).toContainText('Forgot Password');
  });
});

6-3. 実行結果

  1. コード生成 → 自動実行
  2. ブラウザが立ち上がり、指示どおり操作
  3. テストレポートがターミナルに出力

まとめ

  • MCPの設定は簡単で、プラグインのような形で動かせました。
  • 単純な画面遷移やフォーム入力、ログイン操作などは問題なく自動化できました。
  • 毎回スクリプトを書くほどでもないライトなスクレイピングに使えそうだと感じました。
DXC Lab

Discussion