by JustasMonkev
Automated web accessibility auditing and browser automation powered by Playwright and axe‑core, exposed through an MCP server so LLMs can inspect, interact with, and report on web pages.
MCP Accessibility Scanner provides a Model Context Protocol (MCP) server that lets language models perform WCAG‑compliant accessibility scans, drive browsers, capture screenshots, and generate detailed JSON reports. It bundles Playwright for browser control and axe‑core for rule‑based accessibility analysis.
npm install -g mcp-accessibility-scanner
or run directly via npx (preferred for quick use):
npx mcp-accessibility-scanner
browser_navigate, scan_page, audit_site, audit_keyboard, etc. In an interactive REPL you can type:
browser_navigate {"url":"https://example.com"}
scan_page {"violationsTag":["wcag21aa"]}
audit_site (crawling), scan_page_matrix (viewport variants), audit_keyboard (focus behavior), audit_screen_reader (ARIA tree analysis).scan_page_matrix.Q: Do I need a graphical environment? A: No. The server runs headless by default on Linux; on macOS/Windows you can run headed for debugging.
Q: How do I audit a logged‑in site?
A: Either interactively sign in with the browser tools, then run audit_site, or generate a Playwright storage state (auth.json) and launch the server with --storage-state ./auth.json (or the equivalent config entry).
Q: Can I limit the scan to a specific component?
A: Yes. Use includeSelectors and excludeSelectors parameters on scan_page, audit_site, or scan_page_matrix.
Q: Are screenshots optional?
A: By default scan_page does not capture screenshots. Set annotateScreenshot: true to get an annotated PNG in the output directory.
Q: How is the server secured?
A: The default HTTP transport listens only on 127.0.0.1:8931. Do not expose the port to untrusted networks. You can also run the server inside a Docker container with volume mounts for persisted output.
Q: What browsers are supported?
A: Playwright’s Chromium, Firefox, and WebKit. Chrome channel can be selected via browser.launchOptions.channel.
Q: How do I integrate with VS Code? A: Add the MCP entry in VS Code’s configuration:
{
"mcpServers": {
"accessibility-scanner": {
"command": "npx",
"args": ["-y", "mcp-accessibility-scanner"]
}
}
}
A powerful Model Context Protocol (MCP) server that provides automated web accessibility scanning and browser automation using Playwright and Axe-core. This server enables LLMs to perform WCAG compliance checks, interact with web pages, manage persistent browser sessions, and generate detailed accessibility reports with visual annotations.
✅ Full WCAG 2.0/2.1/2.2 compliance checking (A, AA, AAA levels)
📄 Detailed JSON reports with remediation guidance
🎯 Support for specific violation categories (color contrast, ARIA, forms, keyboard navigation, etc.)
🖱️ Click, hover, and drag elements using accessibility snapshots
⌨️ Type text and handle keyboard inputs
🔍 Capture page snapshots to discover all interactive elements
📸 Take screenshots and save PDFs
🎯 Support for both element-based and coordinate-based interactions
📑 Tab management for multi-page workflows
🌐 Monitor console messages and network requests
⏱️ Wait for dynamic content to load
📁 Handle file uploads and browser dialogs
🔄 Navigate through browser history
You can install the package using any of these methods:
Using npm:
npm install -g mcp-accessibility-scanner
A pre-built image is available on Docker Hub. The image includes Chromium and is pre-configured for containerized use — no extra flags needed.
Pull from Docker Hub:
docker pull justasmonkev/mcp-accessibility-scanner
claude mcp add mcp-accessibility-scanner -s user -- docker run -i --rm justasmonkev/mcp-accessibility-scanner
To persist screenshots and reports on your host, add a volume mount:
claude mcp add mcp-accessibility-scanner -s user \
-- docker run -i --rm -v /tmp/mcp-output:/app/output justasmonkev/mcp-accessibility-scanner
Without the -v mount, output files only exist inside the container and are lost when it exits.
docker compose up -d
The Compose configuration publishes the unauthenticated MCP HTTP transport on 127.0.0.1:8931 only. Do not expose this port to untrusted networks.
docker build -t mcp-accessibility-scanner .
npm run test:docker
Install the Accessibility Scanner in VS Code using the VS Code CLI:
For VS Code:
code --add-mcp '{"name":"accessibility-scanner","command":"npx","args":["mcp-accessibility-scanner"]}'
For VS Code Insiders:
code-insiders --add-mcp '{"name":"accessibility-scanner","command":"npx","args":["mcp-accessibility-scanner"]}'
The scanner can run in two modes depending on how you use it.
When launched without a subcommand, the process starts an MCP server that communicates over stdio. This is the mode used by MCP clients such as Claude Desktop, VS Code, and Claude Code -- you should never need to run it by hand.
npx mcp-accessibility-scanner # starts the MCP server (stdio)
All of the MCP client configuration examples in this README already use this default mode.
interactive subcommand)For manual terminal use, the interactive subcommand starts a readline REPL where you can call any tool directly:
$ npx mcp-accessibility-scanner interactive
Interactive mode. Type "<tool-name> <json>" to call a tool. Ctrl+D to exit.
> browser_navigate {"url": "https://example.com"}
> scan_page {"violationsTag": ["wcag21aa"]}
> audit_keyboard {"maxTabs": 30}
> audit_screen_reader {}
Each line is <tool-name> <json-arguments>. Omit the JSON to pass {}.
Global browser connection flags still apply here, for example npx mcp-accessibility-scanner --headless interactive.
Use --mobile or PLAYWRIGHT_MCP_MOBILE=1 to emulate a generic mobile device (Pixel 10 for Chromium, iPhone 17 for WebKit). It cannot be combined with --device, CDP attach/launch modes, remote browser endpoints, or --extension.
Use --extension to connect through the current Playwright Extension, which must support extension protocol v2.
npx mcp-accessibility-scanner --extension
Set PLAYWRIGHT_MCP_EXTENSION_TOKEN to the token shown by the extension to bypass the connection approval dialog.
When --user-data-dir contains multiple Chrome profiles, the profile with the extension installed is selected automatically, preferring Chrome's last-used profile.
list-tools subcommand)To print every tool name and its description:
npx mcp-accessibility-scanner list-tools
Note: Tool names like
browser_navigateandscan_pageare MCP tool identifiers (and REPL commands in interactive mode). They are not shell subcommands -- you cannot runnpx mcp-accessibility-scanner browser_navigate.
Here's the Claude Desktop configuration:
{
"mcpServers": {
"accessibility-scanner": {
"command": "npx",
"args": ["-y", "mcp-accessibility-scanner"]
}
}
}
You can pass a configuration file to customize Playwright behavior:
{
"mcpServers": {
"accessibility-scanner": {
"command": "npx",
"args": ["-y", "mcp-accessibility-scanner", "--config", "/path/to/config.json"]
}
}
}
Create a config.json file with the following options:
{
"browser": {
"browserName": "chromium",
"launchOptions": {
"headless": true,
"channel": "chrome"
},
"cdpLaunch": {
"command": "open",
"args": ["-a", "Slack", "--args", "--remote-debugging-port={port}"],
"startupTimeoutMs": 30000
}
},
"timeouts": {
"navigationTimeout": 60000,
"defaultTimeout": 5000,
"settle": 500
},
"network": {
"allowedOrigins": ["example.com", "trusted-site.com"],
"blockedOrigins": ["ads.example.com"]
}
}
Available Options:
browser.browserName: Browser to use (chromium, firefox, webkit)browser.launchOptions.headless: Run browser in headless mode (default: true on Linux without display, false otherwise)browser.launchOptions.channel: Browser channel (chrome, chrome-beta, msedge, etc.)browser.cdpEndpoint: Attach to an already-running Chromium-family app with CDP enabledbrowser.cdpHeaders: Map of HTTP headers to send with the CDP connect request, e.g. { "Authorization": "Bearer <token>" }, for endpoints that require header-based authenticationbrowser.cdpTimeout: Maximum time in milliseconds to wait when connecting to the CDP endpoint (default: 30000)browser.cdpLaunch: Launch a Chromium-family desktop app with CDP enabled, wait for the endpoint, and manage the child process lifecyclebrowser.contextOptions.storageState: Start each session from a recorded Playwright storage state; applied in every mode except --extension (fresh contexts receive it at creation, reused contexts via setStorageState()). Sessions that share one reused context (non-isolated CDP modes) get the state applied once per context — a session joining a live context inherits its current state, not a fresh copy of the file; see Auditing pages behind a logintimeouts.navigationTimeout: Maximum time for page navigation in milliseconds (default: 60000)timeouts.defaultTimeout: Default timeout for Playwright operations in milliseconds (default: 5000)timeouts.settle: How long to wait after every action before responding (default: 500). An action that finishes quietly is first watched for up to 100ms (or the settle delay, whichever is shorter) so scheduled network work can still be awaited before the settle delay.network.allowedOrigins: List of origins to allow (blocks all others if specified)network.blockedOrigins: List of origins to blockCLI equivalents are also available: --cdp-launch-command, --cdp-launch-args, --cdp-launch-cwd, --cdp-launch-port, --cdp-launch-startup-timeout, --cdp-endpoint, --cdp-header (repeat for multiple headers, e.g. --cdp-header "Authorization: Bearer <token>"), and --cdp-timeout. The CDP headers and timeout can also be set via the PLAYWRIGHT_MCP_CDP_HEADERS (one Name: Value entry per line) and PLAYWRIGHT_MCP_CDP_TIMEOUT environment variables.
Use --timeout-settle or PLAYWRIGHT_MCP_TIMEOUT_SETTLE to override the post-action settle delay. It applies after every action so delayed DOM-only updates are included in the response; a short observation window also catches scheduled requests and waits for them before that delay.
When the server runs with --port, it sends MCP heartbeat pings for Streamable HTTP sessions. Set PLAYWRIGHT_MCP_PING_TIMEOUT_MS to override the default 5000 ms timeout. Set it to 0 or any negative value to disable heartbeat pings for clients or proxies that do not answer server-initiated pings.
Most real audits target pages that only exist for a signed-in user. There are two ways to get there.
Every tool shares one browser context, and audit_site crawls in a temporary tab of that same context, so cookies and local storage created while you drive the browser are already available to the crawl:
1. browser_navigate to the login page
2. browser_fill_form / browser_click to sign in
3. browser_navigate to the first page you want audited
4. audit_site — the crawl inherits the session you just created
This works out of the box in every mode, including the default persistent-profile mode. With the default profile the session also survives across server restarts, so you usually only sign in once.
Record a session once with Playwright's codegen, then hand the file to the server:
npx playwright@1.62.1 codegen --save-storage=auth.json https://example.com/login
Sign in in the opened browser, then close it — auth.json now holds the cookies and local storage.
Pass it to the server with the CLI flag, the environment variable, or the config file:
npx mcp-accessibility-scanner --isolated --storage-state ./auth.json
PLAYWRIGHT_MCP_ISOLATED=true PLAYWRIGHT_MCP_STORAGE_STATE=./auth.json npx mcp-accessibility-scanner
{
"browser": {
"isolated": true,
"contextOptions": {
"storageState": "./auth.json"
}
}
}
Every supported mode handles the state — by applying it or refusing it.
- Fresh-context modes (
--isolated, the remote-endpoint mode, or either CDP mode combined with--isolated): the context is created with the storage state directly.- Default persistent-profile mode with
--storage-state: the session runs in a fresh, disposable profile — unique to that session and removed when it closes — built from the state, so the recorded state is provably the only session data (without--storage-statethe regular persistent profile is used and survives restarts, as before). Any page the launch opened (for example from a URL inbrowser.launchOptions.args) is parked on a blank replacement before the state lands, then the replacement is navigated to the same URL, so a still-running anonymous page cannot overwrite the recorded identity and a scan never reads its DOM. This also means--storage-statecannot be combined with--user-data-dir(a user-supplied profile carries its own session and will not be wiped; the server refuses the combination).- CDP modes without
--isolated: the state is installed into the browser's existing context with Playwright'ssetStorageState(). Cookies are fully reset; origin storage (localStorage/IndexedDB) is reset for the origins recorded in the state plus any origins the Playwright connection has already seen — including pages open in the attached browser at connect time, whose storage can therefore be cleared even when the state omits them. Only origins from the profile's earlier history that this connection never saw survive untouched — cut in both directions, so treat an attached browser's storage as neither fully preserved nor fully reset, and add--isolatedwhen you need a clean, fully-defined session. Pages already open in the attached browser are replaced with fresh tabs navigated to the same URLs so a scan never sees the previous identity's UI — and the old pages close before the state is installed, because a still-running page could otherwise persist the previous identity back into the freshly applied cookies or localStorage, which no later tab replacement could undo. A fresh tab also starts with empty per-tabsessionStorage(which sits outside Playwright storage states and would survive an in-place reload, where the old page's own scripts could even write the previous identity back between a clear and the reload), a replacement that fails to load is left blank or closed rather than left on a stale document, and these navigations run under a configured--allowed-origins/--blocked-originspolicy just like every later navigation. The state is applied once per shared context: concurrent MCP sessions attached without--isolatedshare the browser's context, so a session joining while another is active inherits that context's live state (including anything the first session changed or cleared) rather than a fresh copy of the recorded file — add--isolatedwhen every session must start from the recorded baseline.--extension(with or without--isolated) is the one exception: it works through the browser you are already running, where wiping cookies to install a recorded state is not an acceptable side effect, so the server refuses to start rather than doing that silently. There, sign in interactively instead — the persistent profile also keeps the session across restarts.
audit_site excludes logout|signout by default, which is not enough for most applications. Add anything else that ends or changes the session before you start the crawl:
{
"excludePathPatterns": ["logout|signout", "account/(close|delete)", "sessions/revoke", "/switch-(locale|account|org)"]
}
Note that excludePathPatterns replaces the default rather than extending it, so repeat logout|signout in your list.
If a session cookie disappears anyway, audit_site says so instead of reporting a confident, wrong audit: the result starts with a WARNING: cookie(s) … disappeared while loading <url> line, and both the JSON report and the structured content carry a sessionLosses list naming, for each lost cookie, the page that dropped it — the page reached after any redirect, and reported even when that page failed to finish loading. If one of the lost cookies was the session, every page scanned after that point was audited as a signed-out user — exclude the offending URL, sign in again, and re-run.
The check compares which cookies the crawled URLs carry, not their values, so a rotating CSRF token never reads as a lost session. A cookie the browser deleted at its own stated expiry is ignored for the same reason — Cloudflare's __cf_bm lives 30 minutes and would otherwise warn on any longer crawl. Beyond that no attempt is made to tell an authentication cookie from any other: nothing in a cookie marks it as one, so any cookie the crawl started with and later lost is reported. Monitoring does not stop at the first loss — each cookie is reported once, at the URL where it vanished, so an analytics cookie expiring early cannot mask the session cookie being dropped later. URLs discovered mid-crawl join the cookie tracking before they are visited, so a session cookie scoped to a path below the start URL (say /app) is watched too.
The MCP server provides comprehensive browser automation and accessibility scanning tools:
scan_pagePerforms a comprehensive accessibility scan on the current page using Axe-core.
Parameters:
violationsTag: Array of WCAG/violation tags to checkincludeIncomplete (default true): also report Axe "incomplete" resultsmaxNodesPerViolation (default 10): cap on nodes reported per ruleincludeSelectors / excludeSelectors: CSS selectors that scope the scanwithRules / disableRules: Axe rule ids that narrow which rules runannotateScreenshot (default false): capture an annotated screenshot of the violationsAnnotated screenshots:
When annotateScreenshot is true, each violating element is outlined and labelled with the rule ids it failed, a full-page PNG is written to the MCP output directory (scan-page-annotated-{timestamp}.png) and returned as a resource link, and the markers are then removed so the page is left exactly as it was. The markers are drawn in an out-of-flow overlay clipped to each element's own box, so they never reflow the page. The overlay uses a fresh id per scan, is placed in the browser's top layer so it stays visible over an open dialog, popover or fullscreen element, and compensates for a CSS zoom or a scaled ancestor so markers line up with what is rendered.
An element that fails several rules gets one box listing every rule id, and elements inside open shadow roots are marked by walking the shadow path Axe reports.
Running animations are paused before the elements are measured and resumed after the capture, so a moving target keeps its marker. The markers themselves live in a shadow root under an overlay whose own styles are !important, so page CSS cannot restyle or hide what the report counts, and each rule label sits outside the clipped box so it stays readable on an element smaller than its own label.
At most 50 elements are annotated per scan. The result text always reports how many nodes were marked out of the total, plus how many were left out because they exceeded the limit, were hidden, zero-size or off-canvas (a full-page screenshot is clipped to the document box), or were inside an iframe (cross-frame selectors cannot be resolved from the top document).
Supported Violation Tags:
wcag2a, wcag2aa, wcag2aaa, wcag21a, wcag21aa, wcag21aaa, wcag22a, wcag22aa, wcag22aaasection508cat.aria, cat.color, cat.forms, cat.keyboard, cat.language, cat.name-role-value, cat.parsing, cat.semantics, cat.sensory-and-visual-cues, cat.structure, cat.tables, cat.text-alternatives, cat.time-and-mediabest-practice, experimental (see the caveat below -- a few experimental rules also carry a WCAG tag and run by default)The default set is the WCAG and Section 508 tags only, so a default report means "this fails a conformance criterion". Category tags are opt-in for that reason: Axe matches requested tags with OR, so asking for cat.keyboard also pulls in best-practice rules such as region and skip-link that carry both tags. No live conformance rule is lost by leaving them out: the only rules reachable only through a cat.* tag are duplicate-id and duplicate-id-active, which Axe marks deprecated because WCAG removed SC 4.1.1. Add best-practice (landmark structure, heading order, tabindex hygiene) or a cat.* tag when you want that broader review.
The same OR semantics apply to experimental, with one deliberate exception: five experimental rules -- css-orientation-lock (SC 1.3.4), label-content-name-mismatch (SC 2.5.3), p-as-heading, table-fake-caption and td-has-header (SC 1.3.1) -- also carry a wcag* tag and so run in the default set. In Axe, experimental describes how settled the heuristic is, not whether the criterion is real, so these are kept rather than filtered out. Adding the experimental tag pulls in the remaining experimental rules, which have no conformance tag of their own.
Scan scoping:
scan_page, audit_site, and scan_page_matrix accept includeSelectors and excludeSelectors to limit what Axe looks at. Use includeSelectors to audit one component (["#checkout-form"]) and excludeSelectors to drop third-party noise that pollutes every report (["#cookie-banner", "iframe.intercom-frame"]). Exclusions are applied after inclusions, so you can carve a widget out of an included subtree.
Selectors are resolved before the scan runs:
includeSelectors entry that matches nothing fails the scan. Axe on its own would accept a partly-matching include set and quietly scan less than you asked for, so the scanner refuses rather than returning a clean-looking report with half the scope missing.excludeSelectors entry that matches nothing is a no-op, not an error -- a crawl legitimately visits pages that lack the excluded widget.In audit_site, selectors apply to every crawled page, so an includeSelectors value that is absent from a given page marks that page as errored in the report while the crawl continues. Link discovery runs before the scan, so pages reachable only through an errored page are still crawled.
Rule-level control:
scan_page, audit_site, and scan_page_matrix accept withRules and disableRules to pick individual Axe rules instead of whole tag sets. Use withRules to re-check one rule after a fix (["color-contrast"]) and disableRules to mute a rule you have already triaged (["region"]). Rule ids are the ones Axe reports (image-alt, color-contrast, ...); see the Deque rule reference.
withRules overrides violationsTag. Axe can run either a rule list or a tag list, never both, so when withRules is set the tags are ignored entirely -- withRules: ["image-alt"] runs exactly that one rule regardless of violationsTag. Rule ids are the more specific request, so they win.disableRules subtracts from whatever is selected. It applies to violationsTag and withRules alike. (Axe itself ignores disabled rules once you give it an explicit rule list; the scanner subtracts them up front so the two options mean the same thing together as apart.) Disabling every rule in withRules is an error rather than an empty scan.withRules is an error too. withRules: [] selects no rules, and silently falling back to the tag set would run a different scan than the one requested — omit the option to scan by tags instead. Clients that build the list dynamically should drop the key when the list comes out empty.Unknown Axe rule id(s) in withRules: image-altt rather than surfacing later as an frame.evaluate failure from inside the page. Rule ids apply to a whole run, so audit_site and scan_page_matrix check them once before they touch the page -- a bad id fails the call outright instead of crawling every URL, or reloading and re-emulating the page, before rejecting the argument.audit_site and scan_page_matrix record both values in their JSON report metadata, so a stored report can be told apart from a full scan.
Incomplete ("needs review") results:
Axe returns incomplete for checks it cannot decide on its own -- contrast over a background image or gradient, ambiguous labels, elements it could not fully evaluate. scan_page, audit_site, and scan_page_matrix report these in a section separate from violations so you can resolve them by inspecting the page (screenshot, snapshot, browser_evaluate). Set includeIncomplete: false to suppress them.
Frames that could not be scanned:
Axe is installed into every frame of the page before the scan runs. A frame that navigates mid-injection, or whose renderer does not answer within a second, is left out -- and its contents then contribute no findings. Rather than let that pass as a clean result, all three scan tools print a WARNING: Axe could not be installed in N frame(s) block listing the frame URLs, and audit_site and scan_page_matrix also record them per page and per variant in their JSON reports (unscannedFrames) and in structuredContent. A frame that was still loading usually succeeds on a re-run; one that fails consistently has to be audited on its own.
A nested frame is reported when any frame above it went unscanned, even if its own injection succeeded: Axe reaches a nested document only by relaying through the frames above it, so an outer frame without Axe takes everything below it out of the scan.
A frame you scoped out yourself is not reported: with excludeSelectors: ["iframe.intercom-frame"] that widget failing to load is the outcome you asked for, not a gap. Scope is resolved through the whole frame chain and across shadow boundaries, so an includeSelectors entry naming an ancestor still covers frames nested several levels below it or inside a shadow root, and excluding an outer frame or a shadow host silences everything inside it. Anything the check cannot resolve is reported rather than hidden.
audit_siteCrawls and scans multiple internal pages, then aggregates violations across the site.
links, nav, sitemap, and provided URL strategiesaudit-site-{timestamp}.json)sessionLosses if the crawl loses cookies it started with — see Auditing pages behind a loginExample flow:
1. Navigate to your site homepage with browser_navigate
2. Run audit_site with maxPages: 25 and maxDepth: 2
3. Review the report path returned by the tool (written to the MCP output directory)
scan_page_matrixRuns Axe scans on the same page across viewport/media/zoom variants and compares deltas against baseline.
scan-matrix-{timestamp}.json)v2 set baseline deltas to null when either scan left frames unscanned, because their coverage is not comparableExample flow:
1. Navigate to a page state you want to validate
2. Run scan_page_matrix with defaults (or provide custom variants)
3. Review per-variant deltas and open the generated JSON report path
audit_keyboardAudits real keyboard focus behavior by pressing Tab (and optional Shift+Tab) with practical heuristics.
checkTargetSize, default on)checkFocusObscured, default on)screenshotOnIssue)audit-keyboard-{timestamp}.json)Limits of the WCAG 2.2 checks — these are heuristics, not a conformance verdict:
inline, inline-block,
inline-flex, … — inside surrounding sentence text, found by walking out through inline wrappers such as <strong>
to the containing block) are evaluated. The user agent control, essential, and equivalent exceptions cannot be
detected from the DOM, so a target relying on one of them is still reported and needs manual triage.:disabled controls are not
counted as neighbours, and contenteditable regions are counted as targets on both sides.overflow or clip-path ancestor
is measured at its full unclipped size.pointer-events: none is never returned by hit testing and is missed. Semi-transparent overlays that
still leave content legible are reported.Example flow:
1. Navigate to the target page and let it fully load
2. Run audit_keyboard with maxTabs: 50
3. Review focus findings and open the generated JSON report path
audit_screen_readerAudits what a screen reader actually announces, using the browser's own accessibility tree (page.ariaSnapshot) plus element geometry. No screen reader is installed or driven; this is a static reading of the exposed tree.
Checks (checkNames)
missing-accessible-name: controls and images exposed with no accessible name (WCAG 4.1.2)uninformative-accessible-name: names such as "click here", "read more", "image" that mean nothing out of context (WCAG 2.4.4)filename-as-accessible-name: image alt text that is a file name, e.g. IMG_1234.jpg, DSC00123 (WCAG 1.1.1). Only images are checked: a link or button legitimately named after the file it downloads (logo.png) is not a defect.label-in-name-mismatch: the accessible name does not contain the visible label, which breaks voice control (WCAG 2.5.3). The visible label of <input type="submit|button|reset"> is read from its value, and a web component's label is read from its open shadow root.duplicate-accessible-name: sibling links with the same name that lead to different URLs (WCAG 2.4.4)Check (checkReadingOrder)
reading-order-mismatch: accessibility tree order (what is read) versus visual position (WCAG 1.3.2), i.e. order, flex-direction: row-reverse, absolute positioningWhat it deliberately does not detect
aria-hidden, floated, position: fixed, off-canvas, or clipped to 1px, because their visual position is decoupled from source order by design. Tolerance: two boxes count as swapped only when they are fully separated along the compared axis (1px), and right-to-left containers are compared right-to-left./help and https://site/help are the same destination). Two Save submit buttons in one form are never called ambiguous, because nothing in the exposed tree says whether they do different things.ref are analyzed, and Playwright refs the elements that are visible and receive pointer events. A control that is announced but not interactable (pointer-events: none, some off-canvas widgets) is therefore skipped: without a ref it cannot be measured, so neither its aria-hidden state nor a selector to fix it can be established, and reporting it would mostly surface decorative aria-hidden icons.heading-order, region, landmark-one-main), so use scan_page for them.link-name, button-name and image-alt; this tool adds the quality checks (generic names, file names, label-in-name, duplicates) that axe cannot make.Bounds: maxElements (default 400) caps how many screen-reader-reachable accessibility tree elements are analyzed. The snapshot also refs aria-hidden subtrees, which no check reports, so measuring continues past them until the budget is filled with reachable elements (up to a hard ceiling of twice maxElements measured, so a page built mostly of hidden refs stays bounded). maxFindingsPerCheck (default 20) caps the findings listed per check. Both truncations are stated in the summary and the JSON report, and the full counts are always reported. Always writes a JSON report (default filename: audit-screen-reader-{timestamp}.json).
Example flow:
1. Navigate to the target page and let it fully load
2. Run audit_screen_reader (optionally raise maxElements for a large page)
3. Fix the reported elements by ref, then re-run to confirm
browser_navigateNavigate to a URL.
url (string)HTTP status line in page state.browser_navigate_backGo back to the previous page.
browser_navigation_timeoutSet default navigation timeout for existing tabs.
timeout (in ms; 30000-300000)browser_default_timeoutSet default operation timeout for existing tabs.
timeout (in ms; 30000-300000)browser_snapshotCapture accessibility snapshot of the current page (better than screenshot for analysis).
Large data: URL payloads in snapshot output are truncated to their media type prefix.
compress (optional boolean, default false)
browser_evaluate() to retrieve the full uncompressed list when needed.browser_findSearch the current page accessibility snapshot without returning the full snapshot.
text (case-insensitive substring) or regex (regular expression, supports /pattern/flags)... marks truncated off-path context.browser_clickPerform click on a web page element.
element (description), ref (element reference), doubleClick (optional)browser_typeType text into editable element.
element, ref, text, submit (optional), slowly (optional)browser_hoverHover over element on page.
element, refbrowser_dragPerform drag and drop between two elements.
startElement, startRef, endElement, endRefbrowser_dropSimulate an external drag and drop of files or clipboard-like data onto an element, for testing drop zones that never see a drag start inside the page.
element, ref, paths (optional array of absolute file paths), data (optional map of mime type to value, e.g. {"text/plain": "hello"})paths or data is required.dragover handler does not accept the payload.paths are read from the filesystem of the machine running the server, exactly as browser_file_upload does, and a relative path resolves against the server's working directory. Unlike browser_file_upload this needs no file chooser to be open, so any page with a dragover handler is a valid target — treat it as a tool that can hand local file contents to the page.browser_select_optionSelect an option in a dropdown.
element, ref, values (array)browser_fill_formFill multiple fields with one call.
fields (array of objects with name, type, ref, and value)browser_press_keyPress a key on the keyboard.
key (e.g., 'ArrowLeft' or 'a')browser_evaluateEvaluate a JavaScript expression on the page, or on a specific element when a ref is provided. The function's return value is serialized back as the result.
function (e.g., () => document.title or (element) => element.textContent), element (optional), ref (optional)element and ref must be supplied together, or not at all; supplying one without the other is rejected.document.title behaves like () => document.title, and, when element and ref are both given, element.textContent behaves like (element) => element.textContent. The parameter is always named element.window.open is returned rather than called.browser_take_screenshotTake a screenshot of the current page.
filename (optional), type (png, jpeg, or webp), scale (css or device, default css), fullPage (optional), element/ref pair (for element screenshots)scale: device captures a high-resolution screenshot using device pixels (accounts for the device pixel ratio); scale: css keeps the image sized in CSS pixels.browser_pdf_saveSave page as PDF.
filename (optional, defaults to page-{timestamp}.pdf)This tool requires --caps pdf in the CLI.
browser_installInstall the configured browser engine (use when browser executable is missing).
browser_closeClose the page.
browser_resizeResize the browser window.
width, heightbrowser_tabsManage browser tabs in one tool.
action (list, new, close, select) and optional index (for close and select).browser_console_messagesReturns all console messages from the page.
Large data: URL payloads in console messages are truncated to their media type prefix.
browser_network_requestsReturns all network requests since loading the page, numbered so a single one can be inspected with browser_network_request.
Large data: URL payloads in request URLs are truncated to their media type prefix.
When there is at least one request, a closing line points at browser_network_request.
browser_network_requestReturns the request headers, request body, response headers and response body of one request from the browser_network_requests listing.
index (the number shown in the listing, starting at 1)browser_navigate and when the tab closes; other navigations (link clicks, form submits, history calls) leave it in place and keep appending. Re-run browser_network_requests to get current indexes.authorization, proxy-authorization, cookie, set-cookie, x-api-key, x-auth-token) are reported as <redacted, N characters>, so their presence and size stay visible but the secret never reaches the transcript. All other headers are reported in full, one line each.<binary data, N bytes, mime/type> rather than dumped. The type decides: text/*, +json/+xml suffixes and known textual types are text; image/, audio/, video/, font/, model/ and known binary types are binary; anything else (multipart/form-data, custom types, a missing type) is decided by inspecting the bytes.content-type, and are truncated at 20000 characters with a trailing note. The cap applies to the request body and the response body separately, so one call can return up to two truncated bodies.<headers unavailable: ...>, <body unavailable: ...>) rather than failing the whole call; reads are bounded by the default timeout, so a still-streaming response cannot hang the tool.browser_wait_forWait for text to appear/disappear or time to pass.
time (optional), text (optional), textGone (optional)browser_handle_dialogHandle browser dialogs (alerts, confirms, prompts).
accept (boolean), promptText (optional)browser_file_uploadUpload files to the page.
paths (array of absolute file paths)browser_verify_element_visibleVerify an element by ARIA role/name.
role, accessibleNamebrowser_verify_text_visibleVerify text visibility.
textbrowser_verify_list_visibleVerify list items at a snapshot reference.
element, ref, items (array)browser_verify_valueVerify an element value or checked state.
type, element, ref, valueThese verification tools require --caps verify:
These tools require --caps vision:
browser_mouse_move_xyMove mouse to specific coordinates.
element, x, ybrowser_mouse_click_xyClick at specific coordinates.
element, x, y, button (optional: left/right/middle), clickCount (optional), delay (optional, ms between mouse down and up)browser_mouse_drag_xyDrag from one coordinate to another.
element, startX, startY, endX, endYCoordinate-based tools require element descriptions for permission checks, but the coordinates themselves are used for action targeting.
1. Navigate to example.com using browser_navigate
2. Run scan_page with violationsTag: ["wcag21aa"]
1. Use browser_navigate to go to example.com
2. Run scan_page with violationsTag: ["cat.color"]
1. Navigate to example.com with browser_navigate
2. Take a browser_snapshot to see available elements
3. Click the "Sign In" button using browser_click
4. Type "user@example.com" using browser_type
5. Run scan_page on the login page
6. Take a browser_take_screenshot to capture the final state
1. Navigate to example.com
2. Use browser_snapshot to capture all interactive elements
3. Review console messages with browser_console_messages
4. Check network activity with browser_network_requests
1. Open a new tab with `browser_tabs` and `{"action":"new"}`
2. Navigate to different pages in each tab
3. Switch to a tab with `browser_tabs` and `{"action":"select", "index": 1}`
4. List all tabs with `browser_tabs` and `{"action":"list"}`
1. Navigate to a page
2. Use browser_wait_for to wait for specific text to appear
3. Interact with the dynamically loaded content
Note: Most interaction tools require element references from browser_snapshot. Always capture a snapshot before attempting to interact with page elements.
Clone and set up the project:
git clone https://github.com/JustasMonkev/mcp-accessibility-scanner.git
cd mcp-accessibility-scanner
npm install
bench/mcp-bench.mjs measures what a client actually waits for: it serves a fixed
synthetic site, speaks MCP to the built server over stdio, and times real
tools/call round trips for navigation, interaction, snapshots and every audit
tool. Build first — it runs the compiled server from lib/.
npm run build
npm run bench -- --out after.json --label after
Useful flags: --iterations <n> and --warmups <n> (defaults 5 and 1),
--browser/--executable-path when the browser lives outside Playwright's own
download directory, and --server <path/to/cli.js> plus --lib <path/to/lib>
(supplied together) to point at a different build — that is how revisions compare:
git worktree add /tmp/baseline main && (cd /tmp/baseline && npm install && npm run build)
npm run bench -- --server /tmp/baseline/cli.js --lib /tmp/baseline/lib --out before.json --label before
npm run bench -- --out after.json --label after
npm run bench:compare -- before.json after.json
The comparison total uses only end-to-end scenarios present in both reports, so adding or removing a scenario does not distort the reported speedup.
MIT
Please log in to share your review and rating for this MCP.
Explore related MCPs that share similar capabilities and solve comparable challenges
by modelcontextprotocol
A Model Context Protocol server for Git repository interaction and automation.
by zed-industries
A high‑performance, multiplayer code editor designed for speed and collaboration.
by modelcontextprotocol
Model Context Protocol Servers
by modelcontextprotocol
A Model Context Protocol server that provides time and timezone conversion capabilities.
by cline
An autonomous coding assistant that can create and edit files, execute terminal commands, and interact with a browser directly from your IDE, operating step‑by‑step with explicit user permission.
by upstash
Provides up-to-date, version‑specific library documentation and code examples directly inside LLM prompts, eliminating outdated information and hallucinated APIs.
by daytonaio
Provides a secure, elastic infrastructure that creates isolated sandboxes for running AI‑generated code with sub‑90 ms startup, unlimited persistence, and OCI/Docker compatibility.
by continuedev
Enables faster shipping of code by integrating continuous AI agents across IDEs, terminals, and CI pipelines, offering chat, edit, autocomplete, and customizable agent workflows.
by github
Connects AI tools directly to GitHub, enabling natural‑language interactions for repository browsing, issue and pull‑request management, CI/CD monitoring, code‑security analysis, and team collaboration.
{
"mcpServers": {
"accessibility-scanner": {
"command": "npx",
"args": [
"-y",
"mcp-accessibility-scanner"
],
"env": {}
}
}
}claude mcp add accessibility-scanner npx -y mcp-accessibility-scanner