| 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:
browser_navigate → browser_snapshot → "oops, 404"
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:
1. docs/web-debug.md
2. docs/debugging.md
3. docs/authentication.md
4. README.md
5. .env.example / .env.local
6. package.json / Gemfile / etc
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:
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:
const response = await mcp__fetch__fetch({
url: targetUrl,
prompt: "Return status code only"
});
if (response.includes("404") || response.includes("error")) {
return "URL not reachable: " + targetUrl;
}
const pages = await browser_list_pages();
if (currentPage.url === targetUrl) {
}
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:
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') })" })
])
browser_snapshot → list_console_messages → list_network_requests
4. Authentication Handling
Discovery process:
-
Check documentation first (see Context Discovery above)
-
Inspect the app (if docs don't exist):
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:
"I need to authenticate with this app but couldn't find credentials.
How should I log in?"
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
const requests = await list_network_requests({
resourceTypes: ["xhr", "fetch"],
includeStatic: false
});
const issues = requests.filter(r =>
r.status >= 400 || r.time > 2000
);
if (issues.length > 0) {
const detail = await get_network_request({ reqid: issues[0].id });
}
Console Error Debugging
const errors = await list_console_messages({
types: ["error"],
includePreservedMessages: false
});
if (errors.length > 0) {
const detail = await get_console_message({ msgid: errors[0].id });
}
Authentication Flow Debugging
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"]')
})`
});
const appContext = await mcp__memory__search_nodes({
query: `${appName} auth credentials`
});
if (appContext.hasCredentials) {
await browser_fill_form({ fields: [...] });
await browser_click({ element: "Submit", ref: "..." });
await wait_for({ text: "Dashboard" });
}
UI / Interaction Debugging
const elementExists = await browser_evaluate({
function: `() => !!document.querySelector('button[data-action="submit"]')`
});
if (!elementExists) {
}
const snapshot = await browser_snapshot();
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
async function navigateSafely(url: string) {
const check = await mcp__fetch__fetch({
url,
prompt: "HTTP status code only"
});
if (!check.includes("200")) {
throw new Error(`URL not reachable: ${url}`);
}
const pages = await browser_list_pages();
if (pages.current.url === url) {
return "Already on page";
}
await browser_navigate({ url });
await wait_for({ text: "Expected content" });
}
Pattern: Quick Health Check
async function quickHealthCheck() {
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
async function discoverAuth(appName: string) {
const known = await mcp__memory__open_nodes({
names: [`${appName}-web-debug`]
});
if (known.hasAuth) {
return known.authMethod;
}
const authDoc = await findInRepo([
"docs/web-debug.md",
"docs/authentication.md",
"README.md"
], /auth|login|credential/i);
if (authDoc) {
await mcp__memory__create_entities({
entities: [{
name: `${appName}-web-debug`,
entityType: "AppDebugContext",
observations: [extractedAuthInfo]
}]
});
return extractedAuthInfo;
}
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
}]
});
await mcp__memory__create_entities({ ... });
return userInput;
}
Optimization Checklist
Before any debugging session, ensure:
During debugging:
Error Recovery
If 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()
-
Navigation timeout: URL might not exist or be slow
browser_navigate({ url, timeout: 30000 })
-
Auth not working: Clear state and retry
browser_evaluate({
function: "() => { localStorage.clear(); location.reload(); }"
})
App Context Template
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: `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
- Headless testing - No visible browser needed
- Clean state - No cookies, storage, or extensions
- Multiple browsers - Chromium, Firefox, WebKit
- Complex interactions - Drag/drop, file upload, dialogs
- 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
browser_take_screenshot()
browser_take_screenshot({ uid: "e123" })
browser_take_screenshot({ fullPage: true })
browser_take_screenshot({
fullPage: true,
filePath: "/tmp/screenshot.png"
})
After saving to file:
resize-image --check /tmp/screenshot.png
resize-image /tmp/screenshot.png
See the image-handling skill for complete resize-image documentation.
Self-Discovery Patterns
Exploring Chrome DevTools MCP
curl http://localhost:9222/json/version
mcp__chrome-devtools__list_pages()
mcp__chrome-devtools__take_snapshot()
Exploring Playwright MCP
mcp__playwright__browser_snapshot()
mcp__playwright__browser_navigate({ url: "http://localhost:3000" })
mcp__playwright__browser_install()
Checking Network/Console State
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"
curl http://localhost:9222/json/version
brave --remote-debugging-port=9222
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222
"No pages found"
curl http://localhost:9222/json/list
open http://localhost:3000
"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"
mcp__playwright__browser_install()
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"
- Check credentials in docs:
docs/web-debug.md, README.md
- Inspect what's being sent:
get_network_request for login endpoint
- Check for CSRF tokens in form
- Try clearing state:
browser_evaluate("() => localStorage.clear()")
- Check cookies are being set:
browser_evaluate("() => document.cookie")
"Page is stuck loading"
- Check network:
list_network_requests - any pending/failed?
- Check console:
list_console_messages - JS errors blocking?
- Try reload:
navigate_page({ type: "reload" })
- Increase timeout:
navigate_page({ timeout: 60000 })
"Getting different results than expected"
- Verify you're on the right page:
list_pages or browser_evaluate("() => location.href")
- Check if logged in:
browser_evaluate for auth indicators
- Compare with fresh browser session - cache/cookies may affect behavior
Known Limitations
- Cross-origin iframes - Cannot access content in cross-origin iframes
- Browser extensions - Chrome DevTools sees extension-injected content; Playwright doesn't
- Shadow DOM - Some elements in shadow DOM may not appear in snapshot
- Canvas/WebGL - Cannot inspect canvas content (only screenshot)
- Service Workers - Limited visibility into service worker behavior
- 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.