Claude Desktopでplaywright MCPを動かしてみた
話題になっているMCPを試したいと思い、Claude Desktop上でPlaywright MCPを動かしてみました。その手順や実行結果をこの記事にまとめています。
1. MCP とは
MCP(Model Context Protocol)は、LLM が外部のデータやツールと接続するための標準的なプロトコルのことで、LLMと外部の情報を連携させるための仕組みとして注目されています。
AI用のUSB-Cポートとよく表現されます。
2. playwrightとは
Playwrightは、Microsoftが開発したWeb UI自動化テストフレームワークで、E2Eテストを効率的に行えるようにしてくれます。E2Eテストコードを記述することでブラウザ操作を自動化できます。
2. Playwright MCP とは
Playwright MCPは、Microsoft Playwrightの技術を活用し、ブラウザ操作の自動化を行うためのMCPサーバーで、自然言語によるブラウザ操作が可能になります。
従来のスクリーンショットベースの方法と異なり、以下のような特徴があります。
- 高速 & 軽量 – 画像解析や座標指定を行わず、アクセシビリティツリーを利用するのでページ構造をテキストベースで取得でき、高速な動作が実現できる。
- LLM フレンドリー – 構造化データ(アクセシビリティツリー)から直接ページの要素を把握するので、LLmがそのまま要素を指定・操作できる。
- 決定論的 – 要素をref(要素参照)によって特定するため、スクショ判定よりブレが少なく安定した操作が可能。
活用例
- E2Eテスト
- webスクレイピング
- フォーム入力自動化
3. Claude Desktop から接続する手順
環境準備
環境準備として以下をインストールする必要があります。
- claude for desktopをインストール
- Node.jsをインストール
- npmをインストール
Claudeの設定ファイル編集
ClaudeとPlaywright 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. 実行結果
- コード生成 → 自動実行
- ブラウザが立ち上がり、指示どおり操作
- テストレポートがターミナルに出力
まとめ
- MCPの設定は簡単で、プラグインのような形で動かせました。
- 単純な画面遷移やフォーム入力、ログイン操作などは問題なく自動化できました。
- 毎回スクリプトを書くほどでもないライトなスクレイピングに使えそうだと感じました。
Discussion