Server data from the Official MCP Registry
MCP server for automated web accessibility scanning with Playwright and Axe-core.
About
MCP server for automated web accessibility scanning with Playwright and Axe-core.
Security Report
Valid MCP server (5 strong, 5 medium validity signals). No known CVEs in dependencies. Package registry verified. Imported from the Official MCP Registry.
8 files analyzed Β· 1 issue found
Security scores are indicators to help you make informed decisions, not guarantees. Always review permissions before connecting any MCP server.
Permissions Required
This plugin requests these system permissions. Most are normal for its category.
How to Install
Add this to your MCP configuration file:
{
"mcpServers": {
"mcp-server": {
"args": [
"-y",
"mcp-accessibility-scanner"
],
"command": "npx"
}
}
}Documentation
View on GitHubFrom the project's GitHub README.
MCP Accessibility Scanner π
Star History
A Model Context Protocol (MCP) server for accessibility scanning and browser automation with Playwright and Axe-core. It supports page interaction, persistent browser sessions, and accessibility reports with visual annotations.
Features
Accessibility Scanning
β
Automated checks for WCAG 2.0/2.1/2.2 criteria (A, AA, AAA levels)
π Detailed JSON reports with remediation guidance
π― Support for specific violation categories (color contrast, ARIA, forms, keyboard navigation, etc.)
Browser Automation
π±οΈ 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
Advanced Features
π 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
Installation
You can install the package using any of these methods:
Using npm:
npm install -g mcp-accessibility-scanner --allow-scripts=re2
npm 12 does not run dependency install scripts unless you allow them. The scanner depends on re2, a native module whose install script downloads or builds re2.node; without it the server fails to start with Cannot find module './build/Release/re2.node'. Pass --allow-scripts=re2 to npm install -g and npx (older npm versions accept and ignore the flag), or, when you add the package to a project, run npm install-scripts approve re2 followed by npm rebuild re2 (approving does not run the script that was already skipped). To allow it for every global install, run npm config set allow-scripts=re2 --location=user.
Installation with Docker
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 Code
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
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.
Build from source
docker build -t mcp-accessibility-scanner .
Images built from this Dockerfile include tini as an init process; Docker's --init flag is optional.
Docker smoke test
npm run test:docker
Installation in VS Code
Install the Accessibility Scanner in VS Code using the VS Code CLI:
For VS Code:
code --add-mcp '{"name":"accessibility-scanner","command":"npx","args":["--allow-scripts=re2","mcp-accessibility-scanner"]}'
For VS Code Insiders:
code-insiders --add-mcp '{"name":"accessibility-scanner","command":"npx","args":["--allow-scripts=re2","mcp-accessibility-scanner"]}'
CLI Modes
The scanner can run in two modes depending on how you use it.
MCP server (default, no subcommand)
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 --allow-scripts=re2 mcp-accessibility-scanner # starts the MCP server (stdio)
All of the MCP client configuration examples in this README already use this default mode.
Interactive REPL (interactive subcommand)
For manual terminal use, the interactive subcommand starts a readline REPL where you can call any tool directly:
$ npx --allow-scripts=re2 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 --allow-scripts=re2 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.
Browser extension mode
Use --extension to connect through the current Playwright Extension, which must support extension protocol v2.
npx --allow-scripts=re2 mcp-accessibility-scanner --extension
Set PLAYWRIGHT_MCP_EXTENSION_TOKEN to the token shown by the extension to bypass the connection approval dialog. The relay's CDP WebSocket endpoint always requires a separate random token, generated per relay and appended automatically for the server's own connection. This CDP token is never passed in Chrome's launch arguments or extension URL; the extension approval token cannot authenticate a CDP client.
Token-bypass connections are not background-safe: Chrome focuses the connection tab and window, and client-created tabs remain open after disconnect (upstream limitation).
With a token, the extension must connect and finish setup within 30 seconds after the connection page opens. Failed attempts release the relay so the next tool call can retry. Without a token, manual approval waits until you approve or cancel the call.
When --user-data-dir contains multiple Chrome profiles, the profile with the extension installed is selected automatically, preferring Chrome's last-used profile. Pass --profile-dir-name (or set PLAYWRIGHT_MCP_PROFILE_DIR_NAME) to select a profile explicitly; it requires --user-data-dir and accepts a Chrome profile directory name such as Default or Profile 1 (see "Profile Path" at chrome://version). The PLAYWRIGHT_MCP_EXTENSION_TOKEN approval token is specific to the profile, so when selecting a profile explicitly, use the token shown in that profile.
Packed extensions require an enabled record in the profile's preferences; a leftover extension directory alone does not count as installed. Profiles whose preferences mark the extension disabled or uninstalled are excluded from automatic selection and rejected for explicit selection. Explicit selection with a custom executable skips local installation checks. A whitespace-only PLAYWRIGHT_MCP_PROFILE_DIR_NAME is treated as unset; other string environment variables retain their existing blank-value handling.
Discovering available tools (list-tools subcommand)
To print the built-in tool names and descriptions (page-registered WebMCP tools require a live MCP tools/list request):
npx --allow-scripts=re2 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.
Configuration
Here's the Claude Desktop configuration:
{
"mcpServers": {
"accessibility-scanner": {
"command": "npx",
"args": ["-y", "--allow-scripts=re2", "mcp-accessibility-scanner"]
}
}
}
Advanced Configuration
You can pass a configuration file to customize Playwright behavior:
{
"mcpServers": {
"accessibility-scanner": {
"command": "npx",
"args": ["-y", "--allow-scripts=re2", "mcp-accessibility-scanner", "--config", "/path/to/config.json"]
}
}
}
Exact-name tool selection
npx --allow-scripts=re2 mcp-accessibility-scanner --allowed-tools browser_pdf_save --blocked-tools browser_evaluate,browser_file_upload
| CLI | Environment | JSON config |
|---|---|---|
--allowed-tools | PLAYWRIGHT_MCP_ALLOWED_TOOLS | allowedTools |
--blocked-tools | PLAYWRIGHT_MCP_BLOCKED_TOOLS | blockedTools |
CLI/environment values are comma-separated exact, case-sensitive names; JSON values are arrays of strings. allowedTools adds named tools to core and enabled capabilities; it is not a restrictive whitelist. For example, allowing browser_pdf_save does not disable navigation or accessibility audits, and allowing browser_install explicitly enables browser downloads without --caps install. Omitted lists preserve existing exposure defaults.
blockedTools wins over core tools, capabilities and allowedTools. Blocked tools are absent from tools/list and rejected on tools/call, even without prior listing and even when a request names a browser session. The server instructions sent at initialization stop recommending blocked tools, so clients are not directed to calls that would be rejected. If the blocked tool is the one that clears an open modal (browser_handle_dialog or browser_file_upload), the modal-state guidance names it and says it is blocked instead of offering it as the way forward; other tools keep refusing while the modal is open. The policy applies to standalone, CDP, extension, provider-switching and VS Code backends, over stdio and HTTP. browser_session_open, browser_session_close, accessibility tools and the proxy-owned browser_connect are not exempt. Allowing a tool does not bypass a provider's limitations or enable a connection mode; browser_connect still requires a mode that supplies it.
Precedence is CLI > environment > config file, independently for each list. A higher-precedence list replaces the lower one; lists are not merged. [] in JSON, --blocked-tools=, or an empty environment value explicitly clears that list (likewise for allowedTools). Empty lists add/block nothing; duplicates are harmless. Blank entries inside nonempty lists, non-array JSON values, unknown names (including generated webmcp_* page-tool names) and wildcards fail at startup.
Page-provided WebMCP tools remain discoverable by default and are outside this policy. Their generated webmcp_<name>_<identity> names hash an identity minted for each server run, document and registration, so a name copied from tools/list never matches after a restart or navigation. Instead of accepting a value that could never take effect, startup rejects webmcp_* names in both lists like any other unknown name. Page tools keep their existing scope and staleness checks; a persistent page-action policy would need a stable registration key and is not part of this feature.
This controls dispatch by tool name, not equivalent actions through other tools: for example, blocking browser_click does not prevent a page click through browser_evaluate. See the adoption decision.
Configuration Options
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,
"idle": 0
},
"network": {
"allowedOrigins": ["example.com", "trusted-site.com"],
"blockedOrigins": ["ads.example.com"]
},
"snapshot": {
"boxes": true
}
}
Available Options:
-
browser.browserName: Browser to use (chromium,firefox,webkit)With WebKit on the pinned Playwright 1.63.0, the contents of an open
<details>nested inside a closed<details>count as visible: they appear inbrowser_snapshotandbrowser_findresults, andbrowser_verify_element_visibleandbrowser_verify_text_visiblesucceed for them. Chromium and Firefox hide them until the outer<details>is opened. The upstream fix is not in a stable release yet. The server does not filter snapshots or patch Playwright to work around this. See the verification notes. -
browser.allowedUploadDirs: Restrict files sent bybrowser_file_uploadandbrowser_dropto regular files inside these directories, including resolved symlink targets. Restricted uploads and drops use a checked file handle and accept up to 50 MiB total per call. Unset allows any path;[]denies all file uploads and drops (text-only drops still work). Blank list entries are rejected. CLI:--allowed-upload-dirs(semicolon-separated;""denies all), env:PLAYWRIGHT_MCP_ALLOWED_UPLOAD_DIRS(empty string denies all).The list must be an array, not
null. Roots must exist at startup: their canonical paths are resolved once and retained for the server's lifetime, so retargeting a configured symlink does not grant access to a new tree. Non-empty upload allowlists require macOS or Linux with/proc/self/fdavailable. macOS blocks ancestor symlinks during the file open; Linux checks the opened descriptor's path. Other platforms reject restricted file uploads and drops rather than rely on race-prone pathname checks. Unrestricted uploads, deny-all lists, and text-only drops keep working on all platforms. -
browser.launchOptions.headless: Run browser in headless mode (default:trueon Linux without display,falseotherwise) -
browser.launchOptions.channel: Browser channel (chrome,chrome-beta,msedge, etc.) -
browser.contextOptions.clientCertificates: With client certificates, a configured launch proxy is also applied to fresh contexts unlessbrowser.contextOptions.proxyexplicitly overrides it. Playwright 1.63.0 ignores certificate-interceptor proxy bypass rules, so combining certificates with a nonblank effectiveproxy.bypassis rejected before launch. For a fresh context on a remote browser, configure its proxy explicitly; a proxy inherited only from that browser cannot be inspected. Existing attached contexts retain their current settings. -
browser.launchOptions.proxy: Browser proxy (CLI:--proxy-server,--proxy-bypass; env:PLAYWRIGHT_MCP_PROXY_SERVER,PLAYWRIGHT_MCP_PROXY_BYPASS). Credentials in a CLI or environment proxy URL, such ashttp://user:password@myproxy:3128, are moved intoproxy.usernameandproxy.passwordso the browser can answer407 Proxy Authentication Required; percent-encode reserved characters such as@,:or/in them. A proxy set in the config file keeps its explicitusernameandpasswordfields. -
browser.launchOptions.chromiumSandbox: Defaults tofalsefor downloaded Chromium builds on Linux because they lack the setuid sandbox helper, andtrueotherwise. Remote and VS Code endpoints choose on the remote host. An explicit config orPLAYWRIGHT_MCP_SANDBOXvalue wins;--no-sandboxalways disables it. -
browser.cdpEndpoint: Attach to an already-running Chromium-family app with CDP enabled -
browser.cdpHeaders: Map of HTTP headers to send with the CDP connect request, e.g.{ "Authorization": "Bearer <token>" }, for endpoints that require header-based authentication -
browser.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 lifecycle.startupTimeoutMs(default30000) bounds the whole wait, attach attempts included; each attempt also stops atbrowser.cdpTimeout -
CDP attach modes preserve the target browser's existing default-context settings instead of applying Playwright's defaults.
-
browser.contextOptions.storageState: Start a fresh context from a recorded Playwright storage state. Imports into existing CDP or VS Code contexts are rejected; use--isolatedfor CDP or sign in interactively. If the browser exposes no context, the server creates one with the state. CDP sessions joining that same server-created context inherit its live state without resetting it. -
browser.profileDirName: Chrome profile directory name used in extension mode, for exampleDefaultorProfile 1(CLI:--profile-dir-name, env:PLAYWRIGHT_MCP_PROFILE_DIR_NAME). Requires--user-data-dirand extension mode (--extensionor--connect-tool); defaults to the last-used profile that has the extension installed. -
timeouts.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 for triggered work to settle 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. -
timeouts.idle: Release the default browser context after this many idle milliseconds (default:0, disabled). Accepts integers from0to2147483647. -
network.allowedOrigins: List of origins to allow (blocks all others if specified) -
network.blockedOrigins: List of origins to block -
snapshot.boxes: Include each element's viewport-relative bounding box as[box=x,y,width,height]in snapshots (default:false; CLI:--snapshot-boxes, env:PLAYWRIGHT_MCP_SNAPSHOT_BOXES=1) -
imageResponses:allow(default) returns text and images;omitexcludes images;onlyomits text from successful responses containing images. Errors, browser lifecycle notices, and responses without images (including full-page screenshots) keep their text. Structured results and resource links are always preserved. Inonlymode, screenshot save-path text, generated code and any accompanying text-only findings are omitted; useallowif you need them. Interactive mode rejectsonlybecause its REPL prints text only; usealloworomitthere.autoremains a legacy alias forallow. CLI:--image-responses only; env:PLAYWRIGHT_MCP_IMAGE_RESPONSES=only. Precedence: CLI, then environment, then config file. -
server.authToken: When set, Streamable HTTP requests (--port) requireAuthorization: Bearer <token>or return401(env:PLAYWRIGHT_MCP_AUTH_TOKEN). Blank or malformed tokens fail at startup. The scheme is case-insensitive; the token is exact. Bearer auth does not encrypt traffic: authenticated listeners must bind to loopback, such as--host 127.0.0.1; use a TLS reverse proxy for remote access. The printed client config includes a header placeholder to replace locally, without logging the secret. Unset keeps unauthenticated access. -
filePaths: Optionalrelativeorabsoluterendering for output paths in screenshot/PDF results and generated code, report metadata, annotated/keyboard screenshots, and download summaries. Relative paths are based on the server working directory, not the client workspace or output directory. File writes, stored report contents, and absolutefile://resource URIs are unchanged. Omitted preserves legacy rendering: configured relative output directories produce relative paths, while default temp output paths are absolute (unlike upstream Playwrightβs relative default). CLI:--file-paths absolute; env:PLAYWRIGHT_MCP_FILE_PATHS=absolute. Precedence: CLI, then environment, then config file. -
outputDir: Directory for output files β reports, screenshots, traces, and session logs (CLI:--output-dir, env:PLAYWRIGHT_MCP_OUTPUT_DIR). Defaults to a fresh directory under the system temp folder, resolved once per server run so all of a run's artifacts land together. The output location is always server configuration; the deprecated MCP roots capability (client workspace folders) is no longer consulted.
CLI 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.
If CDP attachment times out after the WebSocket connects, an existing tab may be blocking Playwright's browser initialization. On the pinned Playwright 1.63.0, a tab without a renderer β one whose renderer crashed, or one discarded by Chrome's Memory Saver β is never answered, so the attach cannot succeed while that tab exists (upstream fix, unreleased); a sleeping or unresponsive tab can stall it too (upstream report). --cdp-endpoint then fails after --cdp-timeout, and --cdp-launch by --cdp-launch-startup-timeout (each attach attempt also stops at --cdp-timeout), with an error that names this cause; --cdp-launch stops only the application it launched. For --cdp-endpoint, use an explicit positive --cdp-timeout to bound the attempt; 0 disables it and the attach can then wait forever. Reload or close the affected tab yourself, or attach to a separate disposable browser. noDefaults and --isolated do not skip initialization of existing tabs; the server does not close or reload your tabs or bypass Playwright's initialization to work around this. See the verification notes.
Playwright 1.63 does not support back/forward-cache (BFCache) restoration: an attached browser with BFCache enabled can return unusable references and omit iframe contents after back navigation. This was reproduced with full Chromium, Chrome, and Edge. For a browser you launch yourself, include --disable-back-forward-cache before attaching, or use the server's normal browser launch mode, which retains Playwright's default flag. The server does not change an attached browser's flags or replace back navigation with a reload. See the upstream maintainer's explanation, Playwright's BFCache limitation, and the verification and reproduction command.
For remote HTTP access, configure the TLS reverse proxy explicitly. For example, with the MCP server bound using --host 127.0.0.1 --port 8931 and PLAYWRIGHT_MCP_AUTH_TOKEN set:
- Accept only your configured public hostname over HTTPS and forward
/mcptohttp://127.0.0.1:8931/mcp. - Set the upstream
Hostheader to127.0.0.1:8931, not the public hostname. Forward the client'sAuthorizationheader unchanged; do not inject a shared token for unauthenticated clients. - Before removing
Origin, reject any non-empty value outside your explicit trusted HTTPS origin list (for example,https://mcp.example.com). Allow absentOriginfor non-browser clients. Then removeOriginupstream, or rewrite it tohttp://127.0.0.1:8931. Never strip arbitrary origins without checking them first. - Disable response buffering for SSE streams. Browser clients on a different origin also need a narrowly scoped CORS policy at the proxy.
The server does not trust Forwarded or X-Forwarded-* to bypass its checks. Preserving the public Host or HTTPS Origin upstream returns 403, even with a valid bearer token.
Unexpected HTTP request setup failures, including stateful initialization, return a generic error with HTTP status 500. Parsed legacy JSON-RPC requests retain their IDs, batch errors omit notifications, and notification-only failures have an empty response body. Failed or rejected initialization disposes its unregistered backend. If the request body is still unread or the response has already started, the connection is closed instead of remaining available for reuse.
Caller-supplied screenshot, PDF, find-result, scan-page-matrix, and audit report filenames use a no-clobber policy: an existing file causes the tool call to fail instead of being overwritten. Windows-reserved basenames and names ending in a dot or space are rejected on every platform so configured names behave consistently across hosts.
Failed download saves are reported as tool errors in the current or next response, including after the tab closes; failed entries do not advertise a saved file or an ongoing download. The server retains up to 20 bounded error messages between responses and reports any omitted count. Full Chromium-family browsers on Playwright 1.63.0 have verified native crashes after persistent-profile relaunch. Error reporting does not fix that native crash. The explicit --isolated control passed, but does not preserve a profile between launches; use recorded storage state when needed for authentication. Existing profiles and browser defaults are preserved.
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.
Use --timeout-idle 300000, timeouts.idle, or PLAYWRIGHT_MCP_TIMEOUT_IDLE to release the default browser after five idle minutes. Shared contexts stay open while any client is working; the idle window starts after the last tool call or download finishes. Explicit recordings prevent idle release until browser_stop_recording finishes; passive --save-session capture does not. Cleanup finalizes traces. The next browser tool call reopens the connection and includes a note to navigate again and refresh element references. Attached CDP, extension, and VS Code browsers are disconnected; their external pages remain open. Close and session-management tools do not relaunch an idle browser. Explicit browser_session_open handles keep their separate PLAYWRIGHT_MCP_BROWSER_SESSION_TTL_MS behavior. Zero disables this feature; blank environment values leave the existing configuration unchanged.
The VS Code browser_connect tool accepts only playwright or playwright-core libraries and loopback WebSocket URLs. Set PLAYWRIGHT_MCP_VSCODE_ALLOW_REMOTE=1 to allow remote endpoints, which must use wss:. URL userinfo credentials are rejected.
HTTP Heartbeat
When the server runs with --port, it sends MCP heartbeat pings after a Streamable HTTP client opens the optional event stream. POST-only clients stay connected without heartbeat because server-initiated requests cannot reach them. Set PLAYWRIGHT_MCP_PING_TIMEOUT_MS to override the default 5000 ms timeout, or to 0 or any negative value to disable heartbeat pings. A client that answers ping with a JSON-RPC "method not found" error is treated as alive: the server stops heartbeating that session instead of closing it. Only an unanswered ping (timeout) or a transport failure closes the session.
Clients without the initialize handshake
Clients on the MCP 2026-07-28 revision no longer send the initialize handshake. With --port, requests carrying the revision's per-request _meta envelope are served natively on the 2026-07-28 protocol: server/discover is answered (so clients negotiating with versionNegotiation: 'auto' or a 2026-07-28 pin connect directly), results carry resultType and the SEP-2549 cache fields β dynamic browser tool lists use the conservative ttlMs: 0 default instead of a one-hour cache β and the SEP-2243 standard headers (MCP-Protocol-Version, Mcp-Method, Mcp-Name) are validated against the request body. Older handshake-free clients (2025-era requests without the envelope) are served statelessly as before. In both cases requests receive no heartbeat pings, and in the modes where the server creates browser contexts itself each request runs against a fresh default browser session: with the default persistent profile the per-request default context runs in its own disposable profile (like an explicit browser session), so parallel handshake-free requests do not contend for the stable profile β and the stable profile's sign-in state is not visible to them β while --isolated, remote endpoints and isolated CDP modes mint a fresh context per request anyway. Modes that reuse one live browser context are the exception: --extension (and a browser_connect or VS Code session switched to a connected-browser provider) and CDP attach without --isolated serve every handshake-free request from the same shared context, so its tabs, cookies and storage persist across requests β the same sharing that makes these modes refuse browser_session_open (in --vscode serving the session tools are the exception: they are host-scoped and keep running against the default provider even while switched β see Browser Session Tools). With a pinned --cdp-launch-port, only one launched application can be served at a time, so a second handshake-free request arriving while another request's browser context is still live is rejected with a clear error instead of silently attaching to the first request's application. With --user-data-dir, each handshake-free request launches a browser in the one configured profile: the profile's state persists across requests, and parallel requests contend for its browser lock and can fail with "Browser is already in use". Elsewhere, browser state that must persist across handshake-free requests belongs in an explicit browser session β a browserSessionId handle minted by browser_session_open in one request resolves in later ones (see Browser Session Tools). Clients that do send initialize keep the classic Mcp-Session-Id session behavior unchanged. When several such stateful clients are connected at once in the default persistent-profile mode, the first client's default context holds the stable profile β concurrent clients' default contexts run in their own disposable profiles (without the stable profile's sign-in state) until it is freed, instead of failing with "Browser is already in use".
Auditing pages behind a login
Most real audits target pages that only exist for a signed-in user. There are two ways to get there.
Interactive route (no setup)
Keep every step in the same browser session. Within that session, audit_site crawls in a temporary tab of the same browser context, so cookies and local storage created while you drive the browser are 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
The interactive CLI, stdio MCP and initialized stateful HTTP connections can use their default session for this sequence. Handshake-free HTTP clients must call browser_session_open first and pass the returned browserSessionId to every navigation, interaction and scan call when using a mode that creates contexts per request. Otherwise the next request starts a new default context and loses the login and page state. Shared-context modes such as --extension and non-isolated CDP attach reject browser_session_open; use their shared context or a stateful connection instead. See Browser Session Tools for mode-specific behavior.
With the default persistent profile, sign-in state also survives across server restarts, so you usually only sign in once. The default profile is keyed to the server's working directory, so each workspace's server keeps its own sign-in state β servers launched for different workspaces neither share cookies nor contend for the same profile.
When a login form does not advance
- Take a fresh
browser_snapshotafter a rerender and use its current field references. Wait for an application-specific ready message withbrowser_wait_forbefore entering data. browser_fill_formand ordinarybrowser_typeuse Playwright'sfill. If the application requires individual key events, clear the field withbrowser_typeandtext: "", then usebrowser_typewithslowly: true. Slow typing appends characters; it does not clear an existing value.- Verify an authenticated page or account-specific element before scanning. A filled field or successful click is not proof of a successful login. Inspect
browser_console_messagesandbrowser_network_requestsif submission fails; avoid sharing passwords, tokens or private response bodies in bug reports. - In Docker, the browser runs inside the container:
localhostnames the container, not your host's development server. Use a host address reachable from that container and configure the development server to accept that specific address. Check its bind address, allowed hosts and authentication origin/redirect settings; do not disable host checks or widen CORS as a blanket fix.
Direct DOM value assignments can bypass application event handlers, and synthetic setters are not a general fix for controlled inputs. Report the scanner version, browser mode, exact tool arguments (with secrets removed) and a minimal local reproduction if the standard interactions still fail.
Storage state route (repeatable, CI-friendly)
Record a session once with Playwright's codegen, then hand the file to the server:
npx playwright@1.63.0 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 --allow-scripts=re2 mcp-accessibility-scanner --isolated --storage-state ./auth.json
PLAYWRIGHT_MCP_ISOLATED=true PLAYWRIGHT_MCP_STORAGE_STATE=./auth.json npx --allow-scripts=re2 mcp-accessibility-scanner
{
"browser": {
"isolated": true,
"contextOptions": {
"storageState": "./auth.json"
}
}
}
Every supported mode handles the state β by applying it or refusing it.
Playwright 1.63.0 safety restriction: importing into an existing context is rejected before taking a rollback snapshot or resetting any storage. On this pin, snapshot capture can execute service-worker-served scripts for a previously visited origin whose tab is no longer open (upstream fix). Use a fresh context, or omit
--storage-stateand sign in interactively. Service workers are not disabled. A future dependency upgrade must also pass the recorder/shared-client checks in #218 and IndexedDB checks in #224 before this restriction is reconsidered.IndexedDB snapshot limitation: on pinned Playwright 1.63.0 with Chromium 153.0.8010.12 and Firefox 155.0,
storageState({ indexedDB: true })losesMapandSetcontents. BothnewContext({ storageState })andsetStorageState()restore them as empty plain objects; ordinary JSON records survive. Fresh contexts protect existing browser data, but cannot recover values already lost during capture. The upstream fix is merged but is not in this pin. Before allowing imports into existing contexts again, verify both restore paths preserve Map/Set types and entries on each supported engine, including after a failed import. The real-browser regression intests/browser-failures.integration.test.tschecks that rejecting an import leaves the original Map/Set records intact and that isolated JSON IndexedDB imports still work.
- 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
--isolatedand the VS Code provider:--storage-stateis rejected when the browser already has a context. Add--isolatedin CDP mode to create a fresh context; otherwise omit the state and sign in interactively. If the browser exposes no context, the server creates one with the state. CDP sessions joining that same server-created context inherit its live state without resetting it.--extension(with or without--isolated) refuses storage imports entirely: 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.
Keep the crawl from destroying its own session
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.
Documentation truncated β see the full README on GitHub.
Reviews
No reviews yet
Be the first to review this server!
More Developer Tools MCP Servers
Git
Freeby Modelcontextprotocol Β· Developer Tools
Read, search, and manipulate Git repositories programmatically
Fetch
Freeby Modelcontextprotocol Β· Developer Tools
Web content fetching and conversion for efficient LLM usage
Worldmonitor
Freeby Koala73 Β· Developer Tools
Live markets, conflicts, country risk, chokepoints, energy, and China decision signals. 93 tools.
Paperclip
Freeby Paperclipai Β· Developer Tools
Trending hip-hop artist momentum scores across four cultural dimensions.
Toleno
Freeby Toleno Β· Developer Tools
Toleno Network MCP Server β Manage your Toleno mining account with Claude AI using natural language.
mcp-creator-python
Freeby mcp-marketplace Β· Developer Tools
Create, build, and publish Python MCP servers to PyPI β conversationally.

