Install
$ agentstack add mcp-yashpreetbathla-mcp-accessibility-bridge ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
MCP Accessibility Bridge
Expose any live webpage's accessibility tree to Claude — generate rock-solid, framework-agnostic test selectors in seconds.
[](https://www.npmjs.com/package/mcp-accessibility-bridge) [](https://www.npmjs.com/package/mcp-accessibility-bridge) [](LICENSE) [](https://nodejs.org) [](https://www.typescriptlang.org) [](https://github.com/modelcontextprotocol/sdk)
Quick Start
No cloning. No building. Paste this into your Claude Desktop config and you're done:
{
"mcpServers": {
"accessibility-bridge": {
"command": "npx",
"args": ["-y", "mcp-accessibility-bridge"]
}
}
}
What Is This?
MCP Accessibility Bridge is a stdio MCP server that connects Claude Desktop to a live Chrome browser via the Chrome DevTools Protocol (CDP). It exposes the browser's full ARIA accessibility tree so Claude can:
- Read every element's role, name, state, and relationships exactly as a screen reader would
- Generate reliable, stable test selectors for Playwright, Selenium, Cypress, and WebdriverIO — without ever opening DevTools
- Audit pages for accessibility issues
- Write, debug, and migrate test suites using natural language
> No bundled Chromium. Uses your existing Chrome installation via puppeteer-core.
Table of Contents
- [Quick Start](#quick-start)
- [Architecture](#architecture)
- [Why the Accessibility Tree?](#why-the-accessibility-tree)
- [8 MCP Tools](#8-mcp-tools)
- [Selector Priority Engine](#selector-priority-engine)
- [Real-World Use Cases](#real-world-use-cases)
- [Chrome Setup](#chrome-setup)
- [Claude Desktop Configuration](#claude-desktop-configuration)
- [Usage Examples](#usage-examples)
- [Example Project](#example-project)
- [Project Structure](#project-structure)
- [How It Works (Deep Dive)](#how-it-works-deep-dive)
- [Contributing](#contributing)
- [License](#license)
Architecture
┌─────────────────────────────────────────────────────────────────────┐
│ Claude Desktop │
│ │
│ ┌─────────────┐ MCP (stdio) ┌──────────────────────────┐ │
│ │ Claude │◄─────────────────►│ MCP Accessibility │ │
│ │ LLM │ JSON-RPC 2.0 │ Bridge (Node.js) │ │
│ └─────────────┘ │ │ │
│ │ ┌────────────────────┐ │ │
│ │ │ 8 MCP Tools │ │ │
│ │ │ browser_connect │ │ │
│ │ │ browser_navigate │ │ │
│ │ │ get_ax_tree │ │ │
│ │ │ query_ax_tree │ │ │
│ │ │ get_element_props │ │ │
│ │ │ get_interactive │ │ │
│ │ │ get_focused │ │ │
│ │ │ browser_disconnect│ │ │
│ │ └────────┬───────────┘ │ │
│ │ │ │ │
│ │ ┌────────▼───────────┐ │ │
│ │ │ BrowserManager │ │ │
│ │ │ (Singleton) │ │ │
│ │ │ Browser + Page + │ │ │
│ │ │ CDPSession │ │ │
│ │ └────────┬───────────┘ │ │
│ │ │ │ │
│ │ ┌────────▼───────────┐ │ │
│ │ │ Utilities │ │ │
│ │ │ axTree.ts │ │ │
│ │ │ selectorGen.ts │ │ │
│ │ │ errors.ts │ │ │
│ │ └────────────────────┘ │ │
│ └────────────┬─────────────┘ │
└──────────────────────────────────────────────────┼─────────────────┘
│
CDP WebSocket │
(port 9222) │
│
┌────────────────────▼──────────────────┐
│ Google Chrome │
│ │
│ ┌──────────────────────────────────┐ │
│ │ Active Tab │ │
│ │ │ │
│ │ DOM Tree ──► AX Tree │ │
│ │ ▲ │ │
│ │ Computed │ │ │
│ │ Accessibility │ │ │
│ │ Object Model │ │ │
│ └────────────────┼─────────────────┘ │
│ │ │
│ CDP Domains Used: │
│ • Accessibility.enable │
│ • Accessibility.getFullAXTree │
│ • Accessibility.getPartialAXTree │
│ • Accessibility.queryAXTree │
│ • DOM.describeNode │
│ • DOM.getOuterHTML │
└────────────────────────────────────────┘
Data Flow
User prompt in Claude Desktop
│
▼
Claude decides which tool to call
│
▼
MCP SDK dispatches tool call (stdio JSON-RPC)
│
▼
Tool handler in src/tools/
│
▼
BrowserManager.requireConnection()
returns { browser, page, cdpSession }
│
▼
CDP commands sent over WebSocket to Chrome
│
▼
Chrome computes AX tree from live DOM
│
▼
Raw CDP response parsed + transformed
│
▼
selectorGenerator.ts builds multi-framework selectors
│
▼
toolSuccess({ ... }) → JSON returned to Claude
│
▼
Claude reads selectors, names, roles → responds to user
Why the Accessibility Tree?
Most test automation targets the DOM — brittle class names, nested div soup, hashed CSS modules. The accessibility tree is different:
| Property | DOM Selectors | AX Tree Selectors | |---|---|---| | Stability | Break on CSS refactors | Stable across visual redesigns | | Dynamic state | Miss disabled, expanded, checked | Reflect real runtime state | | Semantic meaning | Target implementation details | Target user-facing intent | | Framework coupling | Often framework-specific | Framework-agnostic | | Accessibility signal | Silent on a11y problems | Surface a11y bugs automatically | | Screen reader parity | Unknown | Exactly what a screen reader announces |
The AX tree is Chrome's computed semantic model — what the browser exposes to assistive technology. Selectors built from it use role and name attributes that are:
- Immune to visual redesigns — renaming a CSS class doesn't change
role=button[name='Submit'] - Semantically verified — if Claude can select it, a screen reader can reach it
- Framework-neutral — the same ARIA semantics translate to any test framework
8 MCP Tools
browser_connect
Connect Claude to a running Chrome instance via CDP.
Input: debugUrl (string, default: "http://localhost:9222")
Output: { connected, debugUrl, currentUrl, pageTitle }
Chrome must be started with --remote-debugging-port=9222. The tool creates a single shared CDPSession that all other tools reuse, enabling Accessibility.* CDP domain calls.
browser_navigate
Navigate the connected tab to any URL.
Input: url (string), waitUntil (load|domcontentloaded|networkidle0|networkidle2), timeout (ms)
Output: { navigated, url, finalUrl, title, status }
Returns the HTTP status code and final URL (after redirects).
browser_disconnect
Close the CDP connection cleanly. Does not kill Chrome.
Input: (none)
Output: { disconnected, message }
get_accessibility_tree
Snapshot the full accessibility tree of the current page.
Input: interestingOnly (bool), maxDepth (int), useFullTree (bool)
Output: { url, title, nodeCount, tree }
Two modes:
- Default:
page.accessibility.snapshot()— fast, filters noise, ideal for most pages - Full CDP:
Accessibility.getFullAXTree— raw, complete, slower — use when the default misses nodes
query_accessibility_tree
Search the tree by ARIA role and/or accessible name.
Input: role (string), accessibleName (string), backendNodeId (int)
Output: { query, count, nodes[] }
Uses CDP Accessibility.queryAXTree — targeted and fast. Example: find all unchecked checkboxes, all level-2 headings, all disabled buttons.
get_element_properties
Given a CSS selector, return the element's full AX profile and multi-framework selectors.
Input: selector (string), includeHtml (bool)
Output: { selector, tagName, domAttributes, backendNodeId, accessibility, suggestedSelectors }
Resolves: CSS selector → backendNodeId → Accessibility.getPartialAXTree → selectorGenerator.
get_interactive_elements
Find every interactive element on the page — buttons, inputs, links, tabs, etc.
Input: roles (string[]), includeDisabled (bool), maxElements (int)
Output: { totalFound, returned, elements[] }
Filters Accessibility.getFullAXTree by 20 interactive ARIA roles, then calls DOM.describeNode in parallel for each to retrieve DOM attributes for selector generation.
20 covered roles: button · link · textbox · searchbox · combobox · listbox · option · checkbox · radio · switch · slider · spinbutton · menuitem · tab · treeitem · gridcell · rowheader · columnheader · progressbar · scrollbar
get_focused_element
Return the currently keyboard-focused element's AX info and selectors.
Input: (none)
Output: { focused: { role, name, value, tagName, domAttributes, suggestedSelectors } }
Uses document.activeElement + Accessibility.getPartialAXTree to report what a keyboard user is currently on.
Selector Priority Engine
src/utils/selectorGenerator.ts implements a 4-tier priority system:
Priority 1 — Test ID attributes (HIGHEST STABILITY)
──────────────────────────────────────────────────
Checks: data-testid, data-cy, data-test, data-qa
Output: [data-testid="submit-btn"]
Playwright: page.getByTestId('submit-btn')
Priority 2 — Stable element ID
──────────────────────────────────────────────────
Checks: id attribute
Skips: UUIDs, numeric IDs, mat-* / ng-* prefixes
Output: #email-input
Playwright: page.locator('#email-input')
Priority 3 — ARIA role + accessible name (MEDIUM STABILITY)
──────────────────────────────────────────────────
Uses: role + computed accessible name from AX tree
Output: role=button[name='Submit']
Playwright: page.getByRole('button', { name: 'Submit' })
Priority 4 — Semantic CSS (FALLBACK)
──────────────────────────────────────────────────
Uses: tagName + type/name/role/placeholder attributes
Output: input[type="email"][name="email"]
Playwright: page.locator('input[type="email"][name="email"]')
Every element returns selectors for all four frameworks:
{
"testId": "[data-testid=\"search-input\"]",
"aria": "role=searchbox[name='Search']",
"css": "input[type=\"search\"]",
"playwright": "page.getByRole('searchbox', { name: 'Search' })",
"selenium": "driver.find_element(By.CSS_SELECTOR, '[data-testid=\"search-input\"]')",
"cypress": "cy.get('[data-testid=\"search-input\"]')",
"webdriverio": "$('[data-testid=\"search-input\"]')",
"stability": "high",
"recommended": "page.getByTestId('search-input')"
}
Real-World Use Cases
1. Instant Test Suite from Zero
A legacy app with no tests. Navigate to any page and ask: > "Generate a Playwright test that fills the checkout form and submits it."
Claude calls get_interactive_elements, receives all inputs and buttons with selectors, and writes the full test — in minutes, not days.
2. Cross-Framework Selector Migration
Moving from Selenium to Playwright? Hundreds of brittle XPath selectors? > "Give me the Playwright equivalent of every interactive element on this page."
Claude maps driver.find_element(By.XPATH, ...) → page.getByRole(...) using the live AX tree as ground truth.
3. Accessibility Audit
> "Get the full accessibility tree for /checkout and identify elements missing accessible names, wrong roles, or bad focus order."
Claude reads the AX tree and reports:
- Buttons with
name: ""(icon buttons missingaria-label) - `
acting as buttons (role: generic`, no keyboard access) - Form fields missing
requiredoraria-describedby
4. Debugging Flaky Tests
> "The test can't find #submit-btn. Navigate to the page and check if it exists in the AX tree, and if it's enabled."
Claude checks: is the element ignored? Is disabled: true? Is a modal trapping focus? All invisible to raw DOM queries, visible here.
5. Component Library Selector Documentation
> "Open Storybook at localhost:6006 and document the recommended selector for every interactive element in every story."
Claude iterates stories, calls get_interactive_elements, and outputs a complete selector reference.
6. Natural Language → Selector (for Non-Technical QA)
> "Find the selector for the blue submit button at the bottom of the registration form."
No DevTools needed. Claude queries the AX tree, identifies the button by its accessible name, and returns all four framework selectors.
7. Dynamic SPA Testing
React/Vue/Angular apps with hashed class names (sc-abc123) break CSS selectors on every build. AX tree selectors (page.getByRole('button', { name: 'Subscribe' })) are permanently stable — hashing class names never changes semantic meaning.
8. Pre-Merge Selector Validation
> "Before merging this PR, verify these 10 selectors still resolve correctly on staging."
Claude calls get_element_properties for each selector and confirms role + name still match expected values. Catches regressions before CI runs.
Chrome Setup
Chrome must be running with the remote debugging port open before calling browser_connect.
macOS
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
--remote-debugging-port=9222 \
--user-data-dir=/tmp/chrome-debug-profile
Linux
google-chrome \
--remote-debugging-port=9222 \
--user-data-dir=/tmp/chrome-debug-profile
Windows (PowerShell)
& "C:\Program Files\Google\Chrome\Application\chrome.exe" `
--remote-debugging-port=9222 `
--user-data-dir="$env:TEMP\chrome-debug-profile"
Verify Chrome is Ready
curl http://localhost:9222/json/version
Expected response:
{
"Browser": "Chrome/124.0.0.0",
"Protocol-Version": "1.3",
"webSocketDebuggerUrl": "ws://localhost:9222/devtools/browser/..."
}
> Why a separate --user-data-dir? Chrome requires
…
Source & license
This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: yashpreetbathla
- Source: yashpreetbathla/mcp-accessibility-bridge
- License: MIT
- Homepage: https://www.npmjs.com/package/mcp-accessibility-bridge
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.