browser_snapshot
Capture accessibility tree snapshots for web elements to enable LLM-friendly identification and interaction during web automation tasks.
Instructions
Get accessibility tree snapshot for LLM-friendly element identification
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| selector | No |
Implementation Reference
- src/server.ts:337-478 (handler)Core handler function for 'browser_snapshot' tool. Captures accessibility tree snapshot of the current page or specified element using Playwright's page.evaluate to extract ARIA attributes, roles, names, states, and builds a hierarchical tree structure for LLM-friendly page understanding.async (params: any) => { try { const input = z.object({ selector: z.string().optional() }).parse(params); await this.playwright.ensureConnected(); const page = this.playwright.getPage(); // Get the accessibility tree let snapshot; if (input.selector) { // Get accessibility snapshot for specific element const element = await page.locator(input.selector); snapshot = await element.locator('xpath=.').first().evaluate(async (el) => { // Use the browser's accessibility API to get semantic information const computedRole = el.getAttribute('role') || el.tagName.toLowerCase(); const computedName = el.getAttribute('aria-label') || el.getAttribute('aria-labelledby') || el.getAttribute('title') || (el as any).innerText?.trim() || el.getAttribute('alt') || el.getAttribute('placeholder') || ''; return { role: computedRole, name: computedName, value: (el as any).value || el.getAttribute('aria-valuenow') || '', description: el.getAttribute('aria-describedby') || el.getAttribute('title') || '', disabled: el.hasAttribute('disabled') || el.getAttribute('aria-disabled') === 'true', expanded: el.getAttribute('aria-expanded') === 'true', focused: document.activeElement === el, selected: el.getAttribute('aria-selected') === 'true', checked: el.getAttribute('aria-checked') || (el as any).checked, required: el.hasAttribute('required') || el.getAttribute('aria-required') === 'true', readonly: el.hasAttribute('readonly') || el.getAttribute('aria-readonly') === 'true', invalid: el.getAttribute('aria-invalid') || (el as any).validity?.valid === false ? 'true' : undefined, multiline: el.getAttribute('aria-multiline') === 'true', autocomplete: el.getAttribute('autocomplete'), placeholder: el.getAttribute('placeholder'), tagName: el.tagName.toLowerCase(), id: el.id, className: el.className, text: (el as any).innerText?.trim() || '', href: (el as any).href, src: (el as any).src }; }); } else { // Get accessibility snapshot for entire page snapshot = await page.evaluate(() => { function getAccessibilityInfo(element: Element): any { if (!element || element.nodeType !== Node.ELEMENT_NODE) return null; const el = element as HTMLElement; const computedRole = el.getAttribute('role') || el.tagName.toLowerCase(); const computedName = el.getAttribute('aria-label') || el.getAttribute('aria-labelledby') || el.getAttribute('title') || el.innerText?.trim().substring(0, 100) || el.getAttribute('alt') || el.getAttribute('placeholder') || ''; // Skip elements with no meaningful content unless they're interactive const interactiveRoles = ['button', 'link', 'input', 'select', 'textarea', 'checkbox', 'radio']; const isInteractive = interactiveRoles.includes(computedRole) || el.hasAttribute('onclick') || el.hasAttribute('href') || el.tabIndex >= 0; if (!computedName && !isInteractive && !el.getAttribute('aria-label')) { return null; } const info: any = { role: computedRole, name: computedName, tagName: el.tagName.toLowerCase() }; // Add important attributes if (el.id) info.id = el.id; if (el.className) info.className = el.className; if ((el as any).value) info.value = (el as any).value; if (el.getAttribute('aria-describedby')) info.description = el.getAttribute('aria-describedby'); if (el.hasAttribute('disabled') || el.getAttribute('aria-disabled') === 'true') info.disabled = true; if (el.getAttribute('aria-expanded')) info.expanded = el.getAttribute('aria-expanded') === 'true'; if (document.activeElement === el) info.focused = true; if (el.getAttribute('aria-selected')) info.selected = el.getAttribute('aria-selected') === 'true'; if (el.getAttribute('aria-checked') || (el as any).checked !== undefined) { info.checked = el.getAttribute('aria-checked') || (el as any).checked; } if (el.hasAttribute('required') || el.getAttribute('aria-required') === 'true') info.required = true; if (el.hasAttribute('readonly') || el.getAttribute('aria-readonly') === 'true') info.readonly = true; if ((el as any).href) info.href = (el as any).href; if (el.getAttribute('placeholder')) info.placeholder = el.getAttribute('placeholder'); // Generate a simple CSS selector for this element let selector = el.tagName.toLowerCase(); if (el.id) { selector = `#${el.id}`; } else if (el.className) { const classes = el.className.trim().split(/\s+/).slice(0, 2).join('.'); selector = `${selector}.${classes}`; } info.selector = selector; // Get children recursively, but limit depth to avoid huge trees const children: any[] = []; for (let child of Array.from(el.children).slice(0, 20)) { // Limit to first 20 children const childInfo = getAccessibilityInfo(child); if (childInfo) { children.push(childInfo); } } if (children.length > 0) { info.children = children; } return info; } return getAccessibilityInfo(document.body); }); } return { content: [{ type: 'text', text: `Accessibility tree snapshot:\n${JSON.stringify(snapshot, null, 2)}` }] }; } catch (error) { return { content: [{ type: 'text', text: `Browser snapshot failed: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } }
- src/types.ts:45-47 (schema)Zod input schema for the browser_snapshot tool, defining an optional selector parameter.export const BrowserSnapshotInputSchema = z.object({ selector: z.string().optional() });
- src/server.ts:328-479 (registration)Registration of the 'browser_snapshot' tool with the MCP server, specifying metadata, input schema, and handler function.this.server.registerTool( 'browser_snapshot', { title: 'Get Accessibility Tree Snapshot', description: 'Get accessibility tree snapshot for LLM-friendly element identification', inputSchema: { selector: z.string().optional() } }, async (params: any) => { try { const input = z.object({ selector: z.string().optional() }).parse(params); await this.playwright.ensureConnected(); const page = this.playwright.getPage(); // Get the accessibility tree let snapshot; if (input.selector) { // Get accessibility snapshot for specific element const element = await page.locator(input.selector); snapshot = await element.locator('xpath=.').first().evaluate(async (el) => { // Use the browser's accessibility API to get semantic information const computedRole = el.getAttribute('role') || el.tagName.toLowerCase(); const computedName = el.getAttribute('aria-label') || el.getAttribute('aria-labelledby') || el.getAttribute('title') || (el as any).innerText?.trim() || el.getAttribute('alt') || el.getAttribute('placeholder') || ''; return { role: computedRole, name: computedName, value: (el as any).value || el.getAttribute('aria-valuenow') || '', description: el.getAttribute('aria-describedby') || el.getAttribute('title') || '', disabled: el.hasAttribute('disabled') || el.getAttribute('aria-disabled') === 'true', expanded: el.getAttribute('aria-expanded') === 'true', focused: document.activeElement === el, selected: el.getAttribute('aria-selected') === 'true', checked: el.getAttribute('aria-checked') || (el as any).checked, required: el.hasAttribute('required') || el.getAttribute('aria-required') === 'true', readonly: el.hasAttribute('readonly') || el.getAttribute('aria-readonly') === 'true', invalid: el.getAttribute('aria-invalid') || (el as any).validity?.valid === false ? 'true' : undefined, multiline: el.getAttribute('aria-multiline') === 'true', autocomplete: el.getAttribute('autocomplete'), placeholder: el.getAttribute('placeholder'), tagName: el.tagName.toLowerCase(), id: el.id, className: el.className, text: (el as any).innerText?.trim() || '', href: (el as any).href, src: (el as any).src }; }); } else { // Get accessibility snapshot for entire page snapshot = await page.evaluate(() => { function getAccessibilityInfo(element: Element): any { if (!element || element.nodeType !== Node.ELEMENT_NODE) return null; const el = element as HTMLElement; const computedRole = el.getAttribute('role') || el.tagName.toLowerCase(); const computedName = el.getAttribute('aria-label') || el.getAttribute('aria-labelledby') || el.getAttribute('title') || el.innerText?.trim().substring(0, 100) || el.getAttribute('alt') || el.getAttribute('placeholder') || ''; // Skip elements with no meaningful content unless they're interactive const interactiveRoles = ['button', 'link', 'input', 'select', 'textarea', 'checkbox', 'radio']; const isInteractive = interactiveRoles.includes(computedRole) || el.hasAttribute('onclick') || el.hasAttribute('href') || el.tabIndex >= 0; if (!computedName && !isInteractive && !el.getAttribute('aria-label')) { return null; } const info: any = { role: computedRole, name: computedName, tagName: el.tagName.toLowerCase() }; // Add important attributes if (el.id) info.id = el.id; if (el.className) info.className = el.className; if ((el as any).value) info.value = (el as any).value; if (el.getAttribute('aria-describedby')) info.description = el.getAttribute('aria-describedby'); if (el.hasAttribute('disabled') || el.getAttribute('aria-disabled') === 'true') info.disabled = true; if (el.getAttribute('aria-expanded')) info.expanded = el.getAttribute('aria-expanded') === 'true'; if (document.activeElement === el) info.focused = true; if (el.getAttribute('aria-selected')) info.selected = el.getAttribute('aria-selected') === 'true'; if (el.getAttribute('aria-checked') || (el as any).checked !== undefined) { info.checked = el.getAttribute('aria-checked') || (el as any).checked; } if (el.hasAttribute('required') || el.getAttribute('aria-required') === 'true') info.required = true; if (el.hasAttribute('readonly') || el.getAttribute('aria-readonly') === 'true') info.readonly = true; if ((el as any).href) info.href = (el as any).href; if (el.getAttribute('placeholder')) info.placeholder = el.getAttribute('placeholder'); // Generate a simple CSS selector for this element let selector = el.tagName.toLowerCase(); if (el.id) { selector = `#${el.id}`; } else if (el.className) { const classes = el.className.trim().split(/\s+/).slice(0, 2).join('.'); selector = `${selector}.${classes}`; } info.selector = selector; // Get children recursively, but limit depth to avoid huge trees const children: any[] = []; for (let child of Array.from(el.children).slice(0, 20)) { // Limit to first 20 children const childInfo = getAccessibilityInfo(child); if (childInfo) { children.push(childInfo); } } if (children.length > 0) { info.children = children; } return info; } return getAccessibilityInfo(document.body); }); } return { content: [{ type: 'text', text: `Accessibility tree snapshot:\n${JSON.stringify(snapshot, null, 2)}` }] }; } catch (error) { return { content: [{ type: 'text', text: `Browser snapshot failed: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } );
- src/types.ts:70-98 (helper)Type definition for the accessibility node structure used in the browser_snapshot output tree.export interface AccessibilityNode { role: string; name?: string; value?: string; description?: string; keyshortcuts?: string; roledescription?: string; valuetext?: string; disabled?: boolean; expanded?: boolean; focused?: boolean; modal?: boolean; multiline?: boolean; multiselectable?: boolean; readonly?: boolean; required?: boolean; selected?: boolean; checked?: boolean | 'mixed'; pressed?: boolean | 'mixed'; level?: number; valuemin?: number; valuemax?: number; autocomplete?: string; haspopup?: string; invalid?: string; orientation?: string; children?: AccessibilityNode[]; selector?: string; }
- src/types.ts:66-66 (schema)TypeScript input type for browser_snapshot derived from the Zod schema.export type BrowserSnapshotInput = z.infer<typeof BrowserSnapshotInputSchema>;