docs/skills/web-debug/SKILL.md
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.
npx skillsauth add megalithic/dotfiles web-debugInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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.
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)
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
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
CRITICAL: MCP browser operations are expensive. Always validate before taking action:
# ❌ 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
Before debugging, discover app-specific context from the repository:
# 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:
Store discovered context:
# 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: [email protected] / password123",
"API pattern: /api/v1/{resource}",
"Known issue: CORS errors on Safari, works on Brave"
]
}]
})
Always validate URLs before navigating:
// ✅ 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
}
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:
// ✅ 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
Discovery process:
Check documentation first (see Context Discovery above)
Inspect the app (if docs don't exist):
// 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
})`
})
If auth mechanism unknown, ask user ONCE and remember:
# 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 [email protected] / password123"]
})
// 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
}
// 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
// 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
}
// 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
browser_snapshotbrowser_evaluatelist_* toolsasync 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" });
}
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"
};
}
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;
}
Before any debugging session, ensure:
--remote-debugging-port=9222http://localhost:9222/json/version to verify connectionDuring debugging:
fetch)list_pages)evaluate for simple checks, not snapshotIf debugging fails:
Browser not responding: Check if debug port is open
curl http://localhost:9222/json/version
Can't find elements: Take snapshot to see current state
browser_snapshot() # See what's actually on the page
Navigation timeout: URL might not exist or be slow
# Increase timeout or validate URL first
browser_navigate({ url, timeout: 30000 })
Auth not working: Clear state and retry
browser_evaluate({
function: "() => { localStorage.clear(); location.reload(); }"
})
When creating docs/web-debug.md in an app repo, use this template:
# 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: `[email protected]` / `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)
[email protected] (auto-created on dev startup)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()
browser_run_code for complex sequencesCRITICAL: Full-page screenshots can exceed Claude API limits (5MB, 8000px max dimension).
// ✅ 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:
# 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.
# 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()
# 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()
// 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);
"Cannot connect to browser"
# 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"
# 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)"
"Timeout waiting for element"
browser_evaluate("() => document.readyState")list_console_messages({ types: ["error"] })"Browser not found"
# Install browser engine
mcp__playwright__browser_install()
# Then retry navigation
mcp__playwright__browser_navigate({ url: "..." })
"Page closed unexpectedly"
browser_navigate({ url: "..." })browser_handle_dialog({ accept: true })"Cannot interact with element"
"Authentication keeps failing"
docs/web-debug.md, README.mdget_network_request for login endpointbrowser_evaluate("() => localStorage.clear()")browser_evaluate("() => document.cookie")"Page is stuck loading"
list_network_requests - any pending/failed?list_console_messages - JS errors blocking?navigate_page({ type: "reload" })navigate_page({ timeout: 60000 })"Getting different results than expected"
list_pages or browser_evaluate("() => location.href")browser_evaluate for auth indicatorsRemember: Speed comes from intelligence, not just raw execution. Validate, batch, and use the lightest tool for the job.
testing
Apply Strunk's timeless writing rules to ANY prose humans will read - documentation, commit messages, error messages, explanations, reports, or UI text. Makes your writing clearer, stronger, and more professional.
tools
Web search using DuckDuckGo (free, unlimited). Falls back to pi-web-access extension for content extraction.
tools
Interact with web pages using agent-browser CLI. MUST run 'browser connect 9222' FIRST to use existing browser with authenticated sessions.
tools
Remote control tmux sessions for interactive CLIs (python, gdb, etc.) by sending keystrokes and scraping pane output.