Free SKILL.md scraped from GitHub. Clone the repo or copy the file directly into your Claude Code skills directory.
npx versuz@latest install web-debuggit clone https://github.com/megalithic/dotfiles.gitcp dotfiles/docs/skills/web-debug/SKILL.md ~/.claude/skills/web-debug/SKILL.md---
name: web-debug
description: Systematic web application debugging using Chrome DevTools MCP and Playwright MCP with intelligent validation and app-specific context discovery. Use for debugging web apps, APIs, authentication flows, and UI issues.
tools: mcp__chrome-devtools__*, mcp__playwright__*, mcp__fetch__fetch, Read, Grep, mcp__memory__*
---
# Web Application Debugging with Chrome DevTools MCP and Playwright MCP
## Overview
This skill guides systematic, efficient web debugging using Chrome DevTools MCP and
Playwright MCP. It emphasizes **validation before action** to minimize slow operations
and **automatic context discovery** from project documentation.
## Decision Trees
### "Which MCP should I use?"
```
Browser debugging needed?
│
├─▶ Chrome is already open with target page?
│ └─▶ Use Chrome DevTools MCP
│ ├─▶ Requires: --remote-debugging-port=9222 flag
│ └─▶ Check: curl http://localhost:9222/json/version
│
├─▶ Need to launch fresh browser instance?
│ └─▶ Use Playwright MCP
│ ├─▶ Creates headless or headed browser
│ └─▶ Cleaner state, no extension interference
│
├─▶ Need to test multiple browsers (Brave, Firefox, Safari)?
│ └─▶ Playwright MCP supports multiple engines
│
├─▶ Need to connect to existing DevTools session?
│ └─▶ Chrome DevTools MCP only
│
└─▶ Automated testing / repeatable scenarios?
└─▶ Playwright MCP (better API for automation)
```
### "How should I debug this issue?"
```
Web debugging task?
│
├─▶ Page not loading / blank screen?
│ ├─▶ 1. Validate URL: mcp__fetch__fetch (check status)
│ ├─▶ 2. Check console: list_console_messages({ types: ["error"] })
│ ├─▶ 3. Check network: list_network_requests (look for 4xx/5xx)
│ └─▶ 4. Only then: browser_snapshot (see what rendered)
│
├─▶ Authentication not working?
│ ├─▶ 1. Check docs for auth method (Context Discovery)
│ ├─▶ 2. Inspect storage: browser_evaluate localStorage/cookies
│ ├─▶ 3. Check network: filter for auth endpoints
│ └─▶ 4. Inspect response headers (Set-Cookie, WWW-Authenticate)
│
├─▶ API call failing?
│ ├─▶ 1. list_network_requests({ resourceTypes: ["xhr", "fetch"] })
│ ├─▶ 2. get_network_request({ reqid: N }) for details
│ ├─▶ 3. Check: status, headers, body, CORS errors
│ └─▶ 4. Compare with docs/expected API contract
│
├─▶ Element not found / can't click?
│ ├─▶ 1. Quick check: browser_evaluate("!!document.querySelector(...)")
│ ├─▶ 2. If false: check network for pending loads
│ ├─▶ 3. If still false: browser_snapshot to see actual page
│ └─▶ 4. Check: wrong selector, dynamic loading, iframe
│
├─▶ Page is slow?
│ ├─▶ 1. performance_start_trace({ reload: true, autoStop: true })
│ ├─▶ 2. Review insights from trace
│ └─▶ 3. Check: large network payloads, long JS execution
│
└─▶ Visual/layout issue?
├─▶ 1. browser_snapshot (accessibility tree)
├─▶ 2. browser_take_screenshot (actual visual)
└─▶ 3. For full-page: save to file, then resize-image --check
```
### "Should I take a screenshot or snapshot?"
```
Need page content?
│
├─▶ Need to interact (click, fill, etc.)?
│ └─▶ browser_snapshot (returns element refs like "e123")
│
├─▶ Need exact visual appearance?
│ └─▶ browser_take_screenshot
│ ├─▶ Viewport only: Usually safe
│ ├─▶ fullPage: true: ⚠️ May exceed API limits
│ │ └─▶ Save to file, then: resize-image --check
│ └─▶ Element screenshot: Specify uid
│
├─▶ Just checking page structure?
│ └─▶ browser_snapshot (faster, includes a11y tree)
│
├─▶ Verifying simple condition?
│ └─▶ browser_evaluate is FASTEST
│ └─▶ "() => document.title"
│ └─▶ "() => !!document.querySelector('.logged-in')"
│
└─▶ Performance investigation?
└─▶ performance_start_trace / performance_stop_trace
```
## Core Principle: Validate Before Acting
**CRITICAL**: MCP browser operations are expensive. Always validate before taking action:
```bash
# ❌ SLOW - Navigate blindly, then snapshot to check
browser_navigate → browser_snapshot → "oops, 404"
# ✅ FAST - Validate URL exists first
fetch(url) → if 200 then browser_navigate → quick check current URL matches
```
## Smart Debugging Workflow
### 1. Context Discovery (First Step)
Before debugging, discover app-specific context from the repository:
```bash
# Check for web debugging documentation (in priority order)
1. docs/web-debug.md # Dedicated debugging guide
2. docs/debugging.md # General debugging guide
3. docs/authentication.md # Auth-specific docs
4. README.md # Project README (search for "debug", "auth", "dev")
5. .env.example / .env.local # Environment variable hints
6. package.json / Gemfile / etc # Check for dev scripts, test users
```
**What to extract from docs:**
- Base URL(s) for local dev / staging / production
- Authentication mechanism (JWT, sessions, OAuth, API keys)
- Test credentials or how to obtain them
- Common routes and expected behavior
- Known issues / quirks
- API endpoint patterns
**Store discovered context:**
```bash
# Use MCP memory to remember app context for future sessions
mcp__memory__create_entities({
entities: [{
name: "launchdeck-web-debug",
entityType: "AppDebugContext",
observations: [
"Base URL: http://localhost:3000",
"Auth: JWT token in localStorage key 'auth_token'",
"Test credentials: user@example.com / password123",
"API pattern: /api/v1/{resource}",
"Known issue: CORS errors on Safari, works on Brave"
]
}]
})
```
### 2. Pre-Flight Validation (Before Navigation)
**Always validate URLs before navigating:**
```typescript
// ✅ Validate URL is reachable
const response = await mcp__fetch__fetch({
url: targetUrl,
prompt: "Return status code only"
});
if (response.includes("404") || response.includes("error")) {
// Don't navigate, report the issue
return "URL not reachable: " + targetUrl;
}
// ✅ Quick check - are we already on the right page?
const pages = await browser_list_pages();
if (currentPage.url === targetUrl) {
// Skip navigation, already there
}
```
### 3. Efficient State Inspection
**Use the lightest operation that answers your question:**
| Need | ❌ Slow | ✅ Fast |
|------|---------|---------|
| Check current URL | `browser_snapshot` | `browser_list_pages` |
| Verify element exists | `browser_snapshot` | `browser_evaluate({ function: "() => !!document.querySelector('.login-btn')" })` |
| Get simple value | `browser_snapshot` | `browser_evaluate({ function: "() => localStorage.getItem('token')" })` |
| Check if logged in | `browser_snapshot` | `browser_evaluate({ function: "() => document.body.dataset.authenticated" })` |
| Inspect network | `browser_snapshot` | `list_network_requests` |
| Check console errors | `browser_snapshot` | `list_console_messages({ types: ["error"] })` |
**Batch parallel operations:**
```typescript
// ✅ Get all diagnostic info at once (parallel)
Promise.all([
list_console_messages({ types: ["error", "warn"] }),
list_network_requests({ resourceTypes: ["xhr", "fetch"] }),
browser_evaluate({ function: "() => ({ url: window.location.href, token: localStorage.getItem('auth_token') })" })
])
// ❌ Sequential snapshots (3x slower)
browser_snapshot → list_console_messages → list_network_requests
```
### 4. Authentication Handling
**Discovery process:**
1. **Check documentation first** (see Context Discovery above)
2. **Inspect the app** (if docs don't exist):
```typescript
// Check for common auth patterns
browser_evaluate({
function: `() => ({
localStorage: Object.keys(localStorage).filter(k =>
k.includes('token') || k.includes('auth') || k.includes('session')
),
cookies: document.cookie,
hasLoginForm: !!document.querySelector('form[action*="login"]'),
userIndicator: document.querySelector('[data-user], .user-name')?.textContent
})`
})
```
3. **If auth mechanism unknown, ask user ONCE and remember:**
```bash
# Ask user via AskUserQuestion tool
"I need to authenticate with this app but couldn't find credentials.
How should I log in?"
# Store their answer in MCP memory for future sessions
mcp__memory__add_observations({
entityName: "app-name-web-debug",
observations: ["Auth method: Form login with user@test.com / password123"]
})
```
### 5. Systematic Debugging by Issue Type
#### Network / API Debugging
```typescript
// 1. List recent network activity (fast)
const requests = await list_network_requests({
resourceTypes: ["xhr", "fetch"],
includeStatic: false // Ignore images, fonts, etc.
});
// 2. Filter for failures or slow requests
const issues = requests.filter(r =>
r.status >= 400 || r.time > 2000
);
// 3. Inspect specific failed request
if (issues.length > 0) {
const detail = await get_network_request({ reqid: issues[0].id });
// Check: headers, body, timing, CORS issues
}
```
#### Console Error Debugging
```typescript
// 1. Get errors only (fast)
const errors = await list_console_messages({
types: ["error"],
includePreservedMessages: false
});
// 2. Get detailed error if needed
if (errors.length > 0) {
const detail = await get_console_message({ msgid: errors[0].id });
}
// 3. Correlate with network failures
// Often console errors follow failed API calls
```
#### Authentication Flow Debugging
```typescript
// 1. Check current auth state (fast evaluate, not snapshot)
const authState = await browser_evaluate({
function: `() => ({
token: localStorage.getItem('auth_token'),
cookies: document.cookie.split(';').map(c => c.trim().split('=')[0]),
isLoggedIn: !!document.querySelector('[data-logged-in="true"]')
})`
});
// 2. If not authenticated, check if we have credentials
const appContext = await mcp__memory__search_nodes({
query: `${appName} auth credentials`
});
// 3. Perform login if we have credentials
if (appContext.hasCredentials) {
await browser_fill_form({ fields: [...] });
await browser_click({ element: "Submit", ref: "..." });
// 4. Validate login succeeded (check for redirect or token)
await wait_for({ text: "Dashboard" }); // or check localStorage
}
```
#### UI / Interaction Debugging
```typescript
// 1. Before clicking, validate element exists (evaluate, not snapshot)
const elementExists = await browser_evaluate({
function: `() => !!document.querySelector('button[data-action="submit"]')`
});
if (!elementExists) {
// Don't attempt click, investigate why element is missing
// Check: network failures, JS errors, wrong page
}
// 2. Take snapshot only when actually needed for interaction
const snapshot = await browser_snapshot();
// Now find element ref and interact
```
## Decision Framework
### When to use full `browser_snapshot`
- **Need to interact** with elements (requires refs)
- **Visual debugging** (need to see layout/hierarchy)
- **Unknown page state** (first time visiting)
### When to use `browser_evaluate`
- **Simple data extraction** (get token, check boolean)
- **Quick validation** (element exists, page ready)
- **Performance-critical checks** (in loops, pre-flight validation)
### When to use `list_*` tools
- **Diagnostic info** (console errors, network failures)
- **Monitoring** (watching for issues during workflow)
- **Quick checks** (any errors? any failed requests?)
## Common Patterns
### Pattern: Safe Navigation
```typescript
async function navigateSafely(url: string) {
// 1. Validate URL exists
const check = await mcp__fetch__fetch({
url,
prompt: "HTTP status code only"
});
if (!check.includes("200")) {
throw new Error(`URL not reachable: ${url}`);
}
// 2. Check if already there
const pages = await browser_list_pages();
if (pages.current.url === url) {
return "Already on page";
}
// 3. Navigate
await browser_navigate({ url });
// 4. Wait for specific content (not arbitrary timeout)
await wait_for({ text: "Expected content" });
}
```
### Pattern: Quick Health Check
```typescript
async function quickHealthCheck() {
// Parallel checks - all fast operations
const [console, network, state] = await Promise.all([
list_console_messages({ types: ["error"] }),
list_network_requests({ includeStatic: false }),
browser_evaluate({
function: "() => ({ url: location.href, ready: document.readyState })"
})
]);
return {
errors: console.filter(m => m.type === "error"),
failures: network.filter(r => r.status >= 400),
currentUrl: state.url,
pageReady: state.ready === "complete"
};
}
```
### Pattern: Find and Remember Auth
```typescript
async function discoverAuth(appName: string) {
// 1. Check if we already know
const known = await mcp__memory__open_nodes({
names: [`${appName}-web-debug`]
});
if (known.hasAuth) {
return known.authMethod;
}
// 2. Search docs in repo
const authDoc = await findInRepo([
"docs/web-debug.md",
"docs/authentication.md",
"README.md"
], /auth|login|credential/i);
if (authDoc) {
// Extract and store
await mcp__memory__create_entities({
entities: [{
name: `${appName}-web-debug`,
entityType: "AppDebugContext",
observations: [extractedAuthInfo]
}]
});
return extractedAuthInfo;
}
// 3. Ask user (last resort)
const userInput = await AskUserQuestion({
questions: [{
question: `How should I authenticate with ${appName} for debugging?`,
header: "Auth Method",
options: [
{ label: "Form login", description: "Username/password form" },
{ label: "API token", description: "Bearer token in headers" },
{ label: "Session cookie", description: "Cookie-based auth" },
{ label: "No auth needed", description: "Public access" }
],
multiSelect: false
}]
});
// Store for next time
await mcp__memory__create_entities({ ... });
return userInput;
}
```
## Optimization Checklist
Before any debugging session, ensure:
- [ ] Browser is running with `--remote-debugging-port=9222`
- [ ] Checked `http://localhost:9222/json/version` to verify connection
- [ ] Loaded app context from docs or memory
- [ ] Know the base URL and auth method
- [ ] Have test credentials available
During debugging:
- [ ] Validate URLs before navigating (use `fetch`)
- [ ] Check current URL before re-navigating (use `list_pages`)
- [ ] Use `evaluate` for simple checks, not `snapshot`
- [ ] Batch parallel requests when possible
- [ ] Only snapshot when you need element refs for interaction
- [ ] Check console/network logs before assuming app state
## Error Recovery
If debugging fails:
1. **Browser not responding**: Check if debug port is open
```bash
curl http://localhost:9222/json/version
```
2. **Can't find elements**: Take snapshot to see current state
```bash
browser_snapshot() # See what's actually on the page
```
3. **Navigation timeout**: URL might not exist or be slow
```bash
# Increase timeout or validate URL first
browser_navigate({ url, timeout: 30000 })
```
4. **Auth not working**: Clear state and retry
```typescript
browser_evaluate({
function: "() => { localStorage.clear(); location.reload(); }"
})
```
## App Context Template
When creating `docs/web-debug.md` in an app repo, use this template:
```markdown
# Web Debugging Context for [App Name]
## Base URLs
- Development: http://localhost:3000
- Staging: https://staging.example.com
- Production: https://example.com
## Authentication
- Method: JWT token in localStorage
- Key: `auth_token`
- Test credentials: `test@example.com` / `password123`
- Login endpoint: POST /api/auth/login
- Token expiry: 24 hours
## Common Routes
- Dashboard: /dashboard
- Login: /login
- API base: /api/v1
## API Patterns
- Auth header: `Authorization: Bearer ${token}`
- Response format: `{ data: {...}, error: null }`
- Error format: `{ data: null, error: { message: "..." } }`
## Known Issues
- CORS errors on Safari - use Brave for debugging
- Websocket connection fails on first load - refresh once
- Session expires after 30min inactivity
## Development Setup
```bash
npm run dev # Start dev server on :3000
npm run test:e2e # Run E2E tests (creates test data)
```
## Test Data
- Test user: `test@example.com` (auto-created on dev startup)
- Sample data seeded: Yes (see `db/seeds.rb`)
```
## Playwright MCP Reference
When using Playwright MCP instead of Chrome DevTools MCP, here are the equivalent operations:
### Tool Mapping
| Chrome DevTools MCP | Playwright MCP | Notes |
|---------------------|----------------|-------|
| `take_snapshot` | `browser_snapshot` | Same output format |
| `take_screenshot` | `browser_take_screenshot` | Playwright has more options |
| `navigate_page` | `browser_navigate` | |
| `click` | `browser_click` | |
| `fill` | `browser_type` | Playwright: type into element |
| `fill_form` | `browser_fill_form` | Multi-field form filling |
| `press_key` | `browser_press_key` | Keyboard input |
| `hover` | `browser_hover` | |
| `evaluate_script` | `browser_evaluate` | Run JS in page |
| `list_console_messages` | `browser_console_messages` | |
| `list_network_requests` | `browser_network_requests` | |
| `list_pages` | `browser_tabs` | Tab management |
| `select_page` | `browser_tabs` | With `action: "select"` |
| `new_page` | `browser_tabs` | With `action: "new"` |
| `close_page` | `browser_tabs` | With `action: "close"` |
| `wait_for` | `browser_wait_for` | Wait for text/element |
| `handle_dialog` | `browser_handle_dialog` | Alert/confirm/prompt |
### Playwright-Specific Features
```typescript
// Navigate back/forward (Playwright only)
browser_navigate_back()
// Run arbitrary Playwright code
browser_run_code({
code: `async (page) => {
await page.getByRole('button', { name: 'Submit' }).click();
return await page.title();
}`
})
// Select dropdown option
browser_select_option({
element: "Country dropdown",
ref: "e123",
values: ["United States"]
})
// Drag and drop
browser_drag({
startElement: "Item to drag",
startRef: "e45",
endElement: "Drop target",
endRef: "e67"
})
// File upload
browser_file_upload({
paths: ["/path/to/file.pdf"]
})
// Close browser completely
browser_close()
```
### When to Prefer Playwright MCP
1. **Headless testing** - No visible browser needed
2. **Clean state** - No cookies, storage, or extensions
3. **Multiple browsers** - Chromium, Firefox, WebKit
4. **Complex interactions** - Drag/drop, file upload, dialogs
5. **Code-based automation** - `browser_run_code` for complex sequences
## Screenshot Handling
**CRITICAL**: Full-page screenshots can exceed Claude API limits (5MB, 8000px max dimension).
### Safe Screenshot Workflow
```typescript
// ✅ SAFE - Viewport only
browser_take_screenshot()
// ✅ SAFE - Element screenshot
browser_take_screenshot({ uid: "e123" })
// ⚠️ DANGER - Full page may exceed limits
browser_take_screenshot({ fullPage: true })
// ✅ SAFE - Save to file, then check
browser_take_screenshot({
fullPage: true,
filePath: "/tmp/screenshot.png"
})
```
After saving to file:
```bash
# Check if resize needed
resize-image --check /tmp/screenshot.png
# If "needs-resize", resize before reading
resize-image /tmp/screenshot.png
# Then read the resized version
# /tmp/screenshot-resized.png
```
**See the `image-handling` skill for complete resize-image documentation.**
## Self-Discovery Patterns
### Exploring Chrome DevTools MCP
```bash
# Check if Chrome DevTools MCP is available
# Look for mcp__chrome-devtools__* tools in your available tools list
# Verify browser connection
curl http://localhost:9222/json/version
# List available pages/tabs
mcp__chrome-devtools__list_pages()
# Check what's on current page
mcp__chrome-devtools__take_snapshot()
```
### Exploring Playwright MCP
```bash
# Check if Playwright MCP is available
# Look for mcp__playwright__* tools
# Check browser status
mcp__playwright__browser_snapshot()
# If browser not running, may need to navigate first
mcp__playwright__browser_navigate({ url: "http://localhost:3000" })
# If browser engine not installed
mcp__playwright__browser_install()
```
### Checking Network/Console State
```typescript
// Quick diagnostic bundle
const [console, network, cookies] = await Promise.all([
list_console_messages({ types: ["error", "warn"] }),
list_network_requests({ resourceTypes: ["xhr", "fetch"] }),
browser_evaluate({ function: "() => document.cookie" })
]);
console.log("Console errors:", console.filter(m => m.type === "error").length);
console.log("Failed requests:", network.filter(r => r.status >= 400).length);
console.log("Has cookies:", cookies.length > 0);
```
## Troubleshooting
### Chrome DevTools MCP Issues
**"Cannot connect to browser"**
```bash
# Check if debug port is open
curl http://localhost:9222/json/version
# If nothing, browser wasn't started with debug flag
# Restart Chrome/Brave with:
brave --remote-debugging-port=9222
# Or for Chrome:
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222
```
**"No pages found"**
```bash
# Check what pages are available
curl http://localhost:9222/json/list
# Open a page first if empty
open http://localhost:3000 # Then MCP can see it
```
**"Element not found (ref invalid)"**
- Refs (e.g., "e123") are only valid from the MOST RECENT snapshot
- Take a new snapshot before interacting
- Page may have changed since last snapshot
**"Timeout waiting for element"**
- Check if page is fully loaded: `browser_evaluate("() => document.readyState")`
- Check for JS errors: `list_console_messages({ types: ["error"] })`
- Element may be in iframe: can't access cross-origin iframes
### Playwright MCP Issues
**"Browser not found"**
```bash
# Install browser engine
mcp__playwright__browser_install()
# Then retry navigation
mcp__playwright__browser_navigate({ url: "..." })
```
**"Page closed unexpectedly"**
- Playwright may close browser on errors
- Re-navigate to restart: `browser_navigate({ url: "..." })`
- Check for unhandled dialogs: `browser_handle_dialog({ accept: true })`
**"Cannot interact with element"**
- Take fresh snapshot to get current refs
- Element may be hidden/overlapped - check visibility
- May need to scroll element into view first
### General Issues
**"Authentication keeps failing"**
1. Check credentials in docs: `docs/web-debug.md`, `README.md`
2. Inspect what's being sent: `get_network_request` for login endpoint
3. Check for CSRF tokens in form
4. Try clearing state: `browser_evaluate("() => localStorage.clear()")`
5. Check cookies are being set: `browser_evaluate("() => document.cookie")`
**"Page is stuck loading"**
1. Check network: `list_network_requests` - any pending/failed?
2. Check console: `list_console_messages` - JS errors blocking?
3. Try reload: `navigate_page({ type: "reload" })`
4. Increase timeout: `navigate_page({ timeout: 60000 })`
**"Getting different results than expected"**
1. Verify you're on the right page: `list_pages` or `browser_evaluate("() => location.href")`
2. Check if logged in: `browser_evaluate` for auth indicators
3. Compare with fresh browser session - cache/cookies may affect behavior
## Known Limitations
1. **Cross-origin iframes** - Cannot access content in cross-origin iframes
2. **Browser extensions** - Chrome DevTools sees extension-injected content; Playwright doesn't
3. **Shadow DOM** - Some elements in shadow DOM may not appear in snapshot
4. **Canvas/WebGL** - Cannot inspect canvas content (only screenshot)
5. **Service Workers** - Limited visibility into service worker behavior
6. **Multiple windows** - Each MCP session typically manages one browser window
---
**Remember**: Speed comes from intelligence, not just raw execution. Validate, batch, and use the lightest tool for the job.