@real-a11y-dev/mcp 0.1.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +99 -0
- package/dist/browser.d.ts +201 -0
- package/dist/browser.js +3 -0
- package/dist/browser.js.map +1 -0
- package/dist/chunk-EXVRX5JP.js +401 -0
- package/dist/chunk-EXVRX5JP.js.map +1 -0
- package/dist/chunk-SF5GJYVW.js +411 -0
- package/dist/chunk-SF5GJYVW.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +104 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +47 -0
- package/dist/server.js +4 -0
- package/dist/server.js.map +1 -0
- package/package.json +81 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Real A11y
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# @real-a11y-dev/mcp
|
|
2
|
+
|
|
3
|
+
> A Model Context Protocol (MCP) server that exposes the Real A11y semantic
|
|
4
|
+
> accessibility tree — and its audits — to AI agents.
|
|
5
|
+
|
|
6
|
+
Unlike a general browser-automation MCP (which hands an agent the browser's raw
|
|
7
|
+
accessibility snapshot so it can _click things_), this server is **audit-first**:
|
|
8
|
+
its flagship tool tells an agent what a real screen reader would announce as
|
|
9
|
+
_broken_. The tree-inspection tools are perception primitives layered on top.
|
|
10
|
+
|
|
11
|
+
## Tools
|
|
12
|
+
|
|
13
|
+
| Tool | Purpose |
|
|
14
|
+
| --- | --- |
|
|
15
|
+
| `open_page` | Navigate to a URL and prepare it for queries (call first). `waitUntil` / `settleMs` settle dynamic pages; `device` (e.g. `"iPhone 13"`) audits the **mobile/tablet** layout. |
|
|
16
|
+
| `audit_page` | **Flagship.** Return every accessibility violation — unlabeled controls, images missing alt, heading gaps, unlabeled dialogs, broken landmarks — as structured findings (grouped, each with a CSS locator + severity). |
|
|
17
|
+
| `inspect_page` | Findings **plus** semantic tree, heading outline, and tab order — all from **one** extraction, so they can't disagree. Prefer on dynamic pages. |
|
|
18
|
+
| `get_semantic_tree` | Deterministic role + accessible-name outline of the page. |
|
|
19
|
+
| `get_heading_outline` | Heading structure (h1..h6) in document order. |
|
|
20
|
+
| `get_tab_order` | Focusable elements in keyboard Tab order. |
|
|
21
|
+
| `list_elements` | Every element of one category (`link`/`button`/`form`/`landmark`/`image`/`heading`) as role + name + locator. |
|
|
22
|
+
| `get_native_tree` | Chromium's own accessibility tree (Blink, via CDP) — the authoritative browser tree. |
|
|
23
|
+
| `compare_trees` | Diff custom vs. native and report role/name divergences — a fidelity oracle. |
|
|
24
|
+
| `close_browser` | Tear down the session. |
|
|
25
|
+
|
|
26
|
+
### Consistency & determinism
|
|
27
|
+
|
|
28
|
+
- **`inspect_page`** derives all four views from a single extraction, so a report
|
|
29
|
+
can't be internally inconsistent (e.g. an audit finding referencing a node the
|
|
30
|
+
tree doesn't show) on a page that changes between separate calls.
|
|
31
|
+
- **`open_page`'s `waitUntil` / `settleMs`** let dynamic pages (SPAs, consent
|
|
32
|
+
dialogs) reach a stable state before extraction, so results don't vary run to
|
|
33
|
+
run. Use `waitUntil: "networkidle"` and/or a `settleMs` buffer for heavy SPAs.
|
|
34
|
+
- **`open_page`'s `device` / `viewport`** emulate a phone or tablet, so you audit
|
|
35
|
+
the tree users on that device actually get — a responsive site can differ
|
|
36
|
+
substantially from desktop (hamburger nav, hidden content, touch-only
|
|
37
|
+
controls). Open the same URL at desktop and `device: "iPhone 13"` to diff them.
|
|
38
|
+
|
|
39
|
+
## How it works
|
|
40
|
+
|
|
41
|
+
It reuses the exact pieces the testing package already ships:
|
|
42
|
+
|
|
43
|
+
1. Playwright drives a real browser (a real browser is required — the engine
|
|
44
|
+
depends on layout/`getComputedStyle` to decide AT exposure).
|
|
45
|
+
2. The pre-built IIFE page-bundle from `@real-a11y-dev/testing` is evaluated in
|
|
46
|
+
the page via `page.evaluate()`, setting `globalThis.__realA11y__`. (It is run
|
|
47
|
+
this way rather than `addScriptTag` so it works on pages served under a
|
|
48
|
+
Trusted Types CSP — `require-trusted-types-for 'script'` — which blocks the
|
|
49
|
+
`<script>` injection `addScriptTag` uses.)
|
|
50
|
+
3. Each tool then routes through `page.evaluate()` and calls the shared
|
|
51
|
+
`collectFindings` / serialize helpers.
|
|
52
|
+
|
|
53
|
+
## Install & run
|
|
54
|
+
|
|
55
|
+
Playwright is a peer dependency and a Chromium binary is required:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npx playwright install chromium
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Wire it into an MCP client — no install step needed, `npx -y` fetches the
|
|
62
|
+
package on first run (use the package name, not the bare `real-a11y-mcp` bin,
|
|
63
|
+
since the client launches it from an arbitrary working directory):
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"mcpServers": {
|
|
68
|
+
"real-a11y": { "command": "npx", "args": ["-y", "@real-a11y-dev/mcp"] }
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
To pin the version instead, add it to your project (`pnpm add -D
|
|
74
|
+
@real-a11y-dev/mcp playwright`) and point `command`/`args` at the local install.
|
|
75
|
+
|
|
76
|
+
> **Scoping.** Every audit/inspection tool takes an optional `rootSelector`
|
|
77
|
+
> (default `body`) to confine extraction to one region — e.g. audit just a
|
|
78
|
+
> `<main>` or a specific component — which also keeps output within the agent's
|
|
79
|
+
> context budget. The two native-tree tools (`get_native_tree`, `compare_trees`)
|
|
80
|
+
> always read the whole document.
|
|
81
|
+
|
|
82
|
+
### Environment
|
|
83
|
+
|
|
84
|
+
| Var | Effect |
|
|
85
|
+
| --- | --- |
|
|
86
|
+
| `REAL_A11Y_MCP_CDP` | Attach to a running Chrome over CDP (e.g. `http://localhost:9222`) instead of launching one — audit pages behind a login you're already signed into. |
|
|
87
|
+
| `REAL_A11Y_MCP_HEADFUL` | `1` launches a visible browser instead of headless. |
|
|
88
|
+
| `REAL_A11Y_MCP_ALLOW_FILE` | `1` permits auditing `file://` URLs. Off by default: an LLM-driven server that can open `file:///…/.env` and read the DOM back is a local-file exfiltration primitive. |
|
|
89
|
+
| `REAL_A11Y_MCP_STORAGE_STATE` | Path to a Playwright storage-state file — audit pages behind a login as that saved session. Create it out-of-band (e.g. `real-a11y login`); it's never a tool parameter, so session tokens never enter the agent's context. The server refuses to start if the file is missing or malformed. |
|
|
90
|
+
| `REAL_A11Y_MCP_ALLOWED_ORIGINS` | Comma-separated origins that auditing is restricted to when a storage state is loaded (origin pinning). **Strongly recommended** alongside `STORAGE_STATE`: without it, a redirect could audit an unintended site with your session. |
|
|
91
|
+
|
|
92
|
+
Auth material is always operator-configured, never a tool parameter — the agent
|
|
93
|
+
just benefits from a session you set up. Use a dedicated low-privilege test
|
|
94
|
+
account, and keep the storage-state file out of version control.
|
|
95
|
+
|
|
96
|
+
## Status
|
|
97
|
+
|
|
98
|
+
Beta, published on npm. Part of the `@real-a11y-dev` family (`core`, `testing`,
|
|
99
|
+
`validate`, …); API and tool surface may still change before 1.0.
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { Finding } from '@real-a11y-dev/testing';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Browser session for the MCP server.
|
|
5
|
+
*
|
|
6
|
+
* Mirrors the architecture of `@real-a11y-dev/testing/playwright`: drive a real
|
|
7
|
+
* browser with Playwright, inject the pre-built IIFE page-bundle (which sets
|
|
8
|
+
* `window.__realA11y__`), and route each a11y query through `page.evaluate()`.
|
|
9
|
+
*
|
|
10
|
+
* A real browser is required — the extraction engine depends on
|
|
11
|
+
* `getComputedStyle`/layout to decide what is exposed to assistive tech, which
|
|
12
|
+
* a serverside jsdom cannot faithfully reproduce.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Reject non-web URLs before navigating. `file://` is a local-file exfiltration
|
|
17
|
+
* primitive for an LLM-driven server (open `file:///…/.env`, then read the DOM
|
|
18
|
+
* back), so it's blocked unless `REAL_A11Y_MCP_ALLOW_FILE=1`. `data:` is allowed
|
|
19
|
+
* (it's caller-supplied inline content, not a filesystem read).
|
|
20
|
+
*/
|
|
21
|
+
declare function assertOpenableUrl(url: string): void;
|
|
22
|
+
interface BrowserSessionOptions {
|
|
23
|
+
/** Launch headless (default true). Ignored when `cdpEndpoint` is set. */
|
|
24
|
+
headless?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Attach to a user's already-running Chrome over the DevTools protocol
|
|
27
|
+
* instead of launching a fresh browser (e.g. "http://localhost:9222").
|
|
28
|
+
*/
|
|
29
|
+
cdpEndpoint?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Network proxy for the launched browser. Chromium does not honor
|
|
32
|
+
* `HTTP_PROXY`/`HTTPS_PROXY` env vars on its own, so callers on corporate
|
|
33
|
+
* networks must pass one explicitly. Ignored when `cdpEndpoint` is set (the
|
|
34
|
+
* running browser already has its own network config).
|
|
35
|
+
*/
|
|
36
|
+
proxy?: {
|
|
37
|
+
server: string;
|
|
38
|
+
bypass?: string;
|
|
39
|
+
username?: string;
|
|
40
|
+
password?: string;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Path to a Playwright storage-state JSON (cookies + origin storage), loaded
|
|
44
|
+
* into every **launched** context so pages open already authenticated. The
|
|
45
|
+
* user creates it out-of-band (e.g. the CLI's `login` helper); this is never
|
|
46
|
+
* an agent-supplied parameter. Applied at the single `newContext()` site, so
|
|
47
|
+
* it survives device-emulation context rebuilds. Rejected together with
|
|
48
|
+
* `cdpEndpoint` (a CDP connection reuses the running browser's own session).
|
|
49
|
+
*/
|
|
50
|
+
storageState?: string;
|
|
51
|
+
/**
|
|
52
|
+
* When set (non-empty), extraction is refused unless the page's **final**
|
|
53
|
+
* origin (after redirects) is in this allowlist. This is the control that
|
|
54
|
+
* stops a redirect from an intended target to a different origin from
|
|
55
|
+
* silently auditing an authenticated page the operator never asked for.
|
|
56
|
+
* Empty/absent ⇒ no origin restriction (anonymous audits follow redirects
|
|
57
|
+
* freely — logged-out content isn't sensitive). Origins are compared as
|
|
58
|
+
* `new URL(...).origin` strings.
|
|
59
|
+
*/
|
|
60
|
+
allowedOrigins?: string[];
|
|
61
|
+
}
|
|
62
|
+
/** Navigation / settle options for {@link A11ySession.open}. */
|
|
63
|
+
interface OpenOptions {
|
|
64
|
+
/**
|
|
65
|
+
* Playwright navigation wait state. `"networkidle"` waits until the network
|
|
66
|
+
* has been quiet for 500ms — the most reliable "the SPA finished rendering"
|
|
67
|
+
* signal, at the cost of latency. Default `"load"`.
|
|
68
|
+
*/
|
|
69
|
+
waitUntil?: "load" | "domcontentloaded" | "networkidle" | "commit";
|
|
70
|
+
/**
|
|
71
|
+
* Extra fixed settle time (ms) after the wait state, to let late-firing JS
|
|
72
|
+
* (async content, consent dialogs) reach a stable state before extraction.
|
|
73
|
+
* Default 0.
|
|
74
|
+
*/
|
|
75
|
+
settleMs?: number;
|
|
76
|
+
/**
|
|
77
|
+
* Emulate a device by Playwright device name — e.g. `"iPhone 13"`,
|
|
78
|
+
* `"Pixel 7"`, `"iPad Pro 11"`. Sets viewport, user-agent, touch, and device
|
|
79
|
+
* scale so the extracted tree reflects the **mobile/tablet** layout, not
|
|
80
|
+
* desktop. Changing device between calls rebuilds the browser context.
|
|
81
|
+
* Not supported over a `cdpEndpoint` (reuses the running browser's context).
|
|
82
|
+
*/
|
|
83
|
+
device?: string;
|
|
84
|
+
/**
|
|
85
|
+
* Explicit viewport override, e.g. `{ width: 375, height: 812 }`. Layered on
|
|
86
|
+
* top of `device` when both are given.
|
|
87
|
+
*/
|
|
88
|
+
viewport?: {
|
|
89
|
+
width: number;
|
|
90
|
+
height: number;
|
|
91
|
+
};
|
|
92
|
+
/** Navigation timeout in ms. Default is Playwright's 30s. */
|
|
93
|
+
timeoutMs?: number;
|
|
94
|
+
}
|
|
95
|
+
/** Options for a single-extraction {@link A11ySession.snapshot}. */
|
|
96
|
+
interface SnapshotOptions {
|
|
97
|
+
/** Rules to run for `findings`. Omit to run all. */
|
|
98
|
+
rules?: string[];
|
|
99
|
+
/** Include generic container nodes in `tree`. Default false. */
|
|
100
|
+
includeGeneric?: boolean;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* All four views derived from **one** extraction — so they can never disagree.
|
|
104
|
+
* The fix for cross-call drift: `findings`, `tree`, `outline`, and `tabOrder`
|
|
105
|
+
* are computed from a single a11y tree snapshot, not four separate extractions.
|
|
106
|
+
*/
|
|
107
|
+
interface PageSnapshot {
|
|
108
|
+
findings: Finding[];
|
|
109
|
+
tree: string;
|
|
110
|
+
outline: string;
|
|
111
|
+
tabOrder: string;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* The session surface the MCP server depends on. Implemented by
|
|
115
|
+
* {@link BrowserSession} in production and faked in unit tests, so the server
|
|
116
|
+
* wiring can be exercised without launching a browser.
|
|
117
|
+
*/
|
|
118
|
+
interface A11ySession {
|
|
119
|
+
open(url: string, options?: OpenOptions): Promise<{
|
|
120
|
+
title: string;
|
|
121
|
+
url: string;
|
|
122
|
+
}>;
|
|
123
|
+
call<T>(fn: string, rootSelector: string, args?: unknown[]): Promise<T>;
|
|
124
|
+
snapshot(rootSelector: string, options?: SnapshotOptions): Promise<PageSnapshot>;
|
|
125
|
+
/** The browser's own (native) accessibility tree via CDP — Chromium only. */
|
|
126
|
+
nativeAX(): Promise<{
|
|
127
|
+
tree: string;
|
|
128
|
+
pairs: string[];
|
|
129
|
+
}>;
|
|
130
|
+
close(): Promise<void>;
|
|
131
|
+
}
|
|
132
|
+
declare class BrowserSession implements A11ySession {
|
|
133
|
+
private readonly opts;
|
|
134
|
+
private browser?;
|
|
135
|
+
private context?;
|
|
136
|
+
private page?;
|
|
137
|
+
/** Signature of the current context's emulation, to detect device changes. */
|
|
138
|
+
private emulationKey;
|
|
139
|
+
/** Over CDP: whether we created `page` (so `close()` only closes our tab). */
|
|
140
|
+
private ownsPage;
|
|
141
|
+
/** Serializes every operation so concurrent tool calls can't race the page. */
|
|
142
|
+
private queue;
|
|
143
|
+
constructor(opts?: BrowserSessionOptions);
|
|
144
|
+
/** Origins allowed for extraction, or null when unrestricted. */
|
|
145
|
+
private get allowedOrigins();
|
|
146
|
+
/**
|
|
147
|
+
* Run `fn` after any in-flight operation. MCP clients dispatch tool calls
|
|
148
|
+
* concurrently, but all tools share one mutable `page`, so we single-flight.
|
|
149
|
+
*/
|
|
150
|
+
private run;
|
|
151
|
+
/** Navigate to `url`, settle, then confirm the bundle initialized. */
|
|
152
|
+
open(url: string, options?: OpenOptions): Promise<{
|
|
153
|
+
title: string;
|
|
154
|
+
url: string;
|
|
155
|
+
}>;
|
|
156
|
+
/**
|
|
157
|
+
* Run a named export from the injected bundle against a root selector.
|
|
158
|
+
* Re-injects first if a navigation wiped the global.
|
|
159
|
+
*/
|
|
160
|
+
call<T>(fn: string, rootSelector: string, args?: unknown[]): Promise<T>;
|
|
161
|
+
/**
|
|
162
|
+
* Extract the a11y tree **once** and derive all four views from it inside a
|
|
163
|
+
* single `page.evaluate`, so findings / tree / outline / tab order always
|
|
164
|
+
* describe the same instant — no cross-call drift on a moving page.
|
|
165
|
+
*/
|
|
166
|
+
snapshot(rootSelector: string, options?: SnapshotOptions): Promise<PageSnapshot>;
|
|
167
|
+
/**
|
|
168
|
+
* Chromium's own accessibility tree, straight from Blink via the CDP
|
|
169
|
+
* `Accessibility` domain — the authoritative computation, not our
|
|
170
|
+
* reimplementation. Used to cross-check custom-engine fidelity.
|
|
171
|
+
*/
|
|
172
|
+
nativeAX(): Promise<{
|
|
173
|
+
tree: string;
|
|
174
|
+
pairs: string[];
|
|
175
|
+
}>;
|
|
176
|
+
/**
|
|
177
|
+
* Capture the current context's storage state (cookies + origin storage) —
|
|
178
|
+
* the "save" step behind the CLI's `login` helper. Returns the plain object;
|
|
179
|
+
* the caller owns serialization + file writing (permissions, atomicity).
|
|
180
|
+
* `indexedDB` is caller-gated: it's a no-op on Playwright < 1.51, so the CLI
|
|
181
|
+
* passes it only when the resolved version supports it.
|
|
182
|
+
*/
|
|
183
|
+
captureStorageState(options?: {
|
|
184
|
+
indexedDB?: boolean;
|
|
185
|
+
}): Promise<unknown>;
|
|
186
|
+
close(): Promise<void>;
|
|
187
|
+
hasPage(): boolean;
|
|
188
|
+
private requirePage;
|
|
189
|
+
/** Refuse extraction when the final URL's origin isn't allowlisted. */
|
|
190
|
+
private assertAllowedOrigin;
|
|
191
|
+
private ensurePage;
|
|
192
|
+
/** Evaluate the bundle in the page (CSP / Trusted-Types-safe via CDP). */
|
|
193
|
+
private injectBundle;
|
|
194
|
+
/** (Re)inject the bundle if a navigation wiped the global. */
|
|
195
|
+
private ensureInjected;
|
|
196
|
+
/** Confirm the bundle initialized; throw a clear error if not. */
|
|
197
|
+
private verifyReady;
|
|
198
|
+
private isReady;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export { type A11ySession, BrowserSession, type BrowserSessionOptions, type OpenOptions, type PageSnapshot, type SnapshotOptions, assertOpenableUrl };
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"browser.js"}
|