@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
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { join, dirname } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
5
|
+
import { ALL_RULES } from '@real-a11y-dev/testing';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
|
|
8
|
+
// src/server.ts
|
|
9
|
+
var RULES = ALL_RULES;
|
|
10
|
+
function packageVersion() {
|
|
11
|
+
try {
|
|
12
|
+
const p = join(
|
|
13
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
14
|
+
"..",
|
|
15
|
+
"package.json"
|
|
16
|
+
);
|
|
17
|
+
return JSON.parse(readFileSync(p, "utf8")).version ?? "0.0.0";
|
|
18
|
+
} catch {
|
|
19
|
+
return "0.0.0";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
var rootSelector = z.string().default("body").describe("CSS selector for the audit/extraction root. Defaults to 'body'.");
|
|
23
|
+
var MAX_OUTPUT_CHARS = 4e4;
|
|
24
|
+
function bounded(body) {
|
|
25
|
+
if (body.length <= MAX_OUTPUT_CHARS) return body;
|
|
26
|
+
return body.slice(0, MAX_OUTPUT_CHARS) + `
|
|
27
|
+
|
|
28
|
+
\u2026 output truncated at ${MAX_OUTPUT_CHARS} chars \u2014 narrow with rootSelector.`;
|
|
29
|
+
}
|
|
30
|
+
function text(body) {
|
|
31
|
+
return { content: [{ type: "text", text: bounded(body) }] };
|
|
32
|
+
}
|
|
33
|
+
var SEVERITY_ORDER = {
|
|
34
|
+
error: 0,
|
|
35
|
+
warning: 1
|
|
36
|
+
};
|
|
37
|
+
var MAX_LOCATORS = 8;
|
|
38
|
+
function renderAudit(findings) {
|
|
39
|
+
const errors = findings.filter((f) => f.severity === "error").length;
|
|
40
|
+
const warnings = findings.length - errors;
|
|
41
|
+
if (findings.length === 0) return "No accessibility issues found.";
|
|
42
|
+
const header = `${findings.length} issue(s) \u2014 ${errors} error(s), ${warnings} warning(s):`;
|
|
43
|
+
const groups = /* @__PURE__ */ new Map();
|
|
44
|
+
for (const f of findings) {
|
|
45
|
+
const key = `${f.severity}|${f.rule}|${f.message}`;
|
|
46
|
+
let g = groups.get(key);
|
|
47
|
+
if (!g) {
|
|
48
|
+
g = {
|
|
49
|
+
severity: f.severity,
|
|
50
|
+
rule: f.rule,
|
|
51
|
+
message: f.message,
|
|
52
|
+
count: 0,
|
|
53
|
+
where: []
|
|
54
|
+
};
|
|
55
|
+
groups.set(key, g);
|
|
56
|
+
}
|
|
57
|
+
g.count += 1;
|
|
58
|
+
if (f.locator) {
|
|
59
|
+
g.where.push(f.context ? `${f.locator} ${f.context}` : f.locator);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const sorted = [...groups.values()].sort(
|
|
63
|
+
(a, b) => SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity] || b.count - a.count
|
|
64
|
+
);
|
|
65
|
+
const lines = [];
|
|
66
|
+
for (const g of sorted) {
|
|
67
|
+
const countStr = g.count > 1 ? ` (\xD7${g.count})` : "";
|
|
68
|
+
lines.push(` [${g.severity}] ${g.rule}: ${g.message}${countStr}`);
|
|
69
|
+
for (const w of g.where.slice(0, MAX_LOCATORS)) lines.push(` ${w}`);
|
|
70
|
+
if (g.where.length > MAX_LOCATORS) {
|
|
71
|
+
lines.push(` \u2026 +${g.where.length - MAX_LOCATORS} more`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const MAX_JSON_FINDINGS = 200;
|
|
75
|
+
const capped = findings.slice(0, MAX_JSON_FINDINGS);
|
|
76
|
+
const json = JSON.stringify(
|
|
77
|
+
{
|
|
78
|
+
summary: {
|
|
79
|
+
total: findings.length,
|
|
80
|
+
errors,
|
|
81
|
+
warnings,
|
|
82
|
+
...findings.length > MAX_JSON_FINDINGS ? { findingsTruncatedTo: MAX_JSON_FINDINGS } : {}
|
|
83
|
+
},
|
|
84
|
+
findings: capped
|
|
85
|
+
},
|
|
86
|
+
null,
|
|
87
|
+
2
|
|
88
|
+
);
|
|
89
|
+
return `${header}
|
|
90
|
+
${lines.join("\n")}
|
|
91
|
+
|
|
92
|
+
\`\`\`json
|
|
93
|
+
${json}
|
|
94
|
+
\`\`\``;
|
|
95
|
+
}
|
|
96
|
+
function renderSnapshot(snap) {
|
|
97
|
+
const treeNodes = snap.tree.split("\n").filter(Boolean).length;
|
|
98
|
+
const tabStops = snap.tabOrder.split("\n").filter((l) => /^\d/.test(l)).length;
|
|
99
|
+
return [
|
|
100
|
+
`Single-extraction snapshot \u2014 ${treeNodes} tree nodes, ${tabStops} tab stops. All sections below describe the same instant.`,
|
|
101
|
+
"",
|
|
102
|
+
renderAudit(snap.findings),
|
|
103
|
+
"",
|
|
104
|
+
"## Semantic tree",
|
|
105
|
+
"```",
|
|
106
|
+
snap.tree || "(empty)",
|
|
107
|
+
"```",
|
|
108
|
+
"",
|
|
109
|
+
"## Heading outline",
|
|
110
|
+
"```",
|
|
111
|
+
snap.outline,
|
|
112
|
+
"```",
|
|
113
|
+
"",
|
|
114
|
+
"## Tab order",
|
|
115
|
+
"```",
|
|
116
|
+
snap.tabOrder,
|
|
117
|
+
"```"
|
|
118
|
+
].join("\n");
|
|
119
|
+
}
|
|
120
|
+
var COMPARE_ROLES = /* @__PURE__ */ new Set([
|
|
121
|
+
// interactive controls
|
|
122
|
+
"button",
|
|
123
|
+
"link",
|
|
124
|
+
"textbox",
|
|
125
|
+
"searchbox",
|
|
126
|
+
"combobox",
|
|
127
|
+
"checkbox",
|
|
128
|
+
"radio",
|
|
129
|
+
"switch",
|
|
130
|
+
"slider",
|
|
131
|
+
"spinbutton",
|
|
132
|
+
"menuitem",
|
|
133
|
+
"menuitemcheckbox",
|
|
134
|
+
"menuitemradio",
|
|
135
|
+
"option",
|
|
136
|
+
"tab",
|
|
137
|
+
// named non-interactive
|
|
138
|
+
"heading",
|
|
139
|
+
"img",
|
|
140
|
+
"dialog",
|
|
141
|
+
"alertdialog",
|
|
142
|
+
// landmarks
|
|
143
|
+
"main",
|
|
144
|
+
"navigation",
|
|
145
|
+
"banner",
|
|
146
|
+
"contentinfo",
|
|
147
|
+
"complementary",
|
|
148
|
+
"region",
|
|
149
|
+
"form",
|
|
150
|
+
"search"
|
|
151
|
+
]);
|
|
152
|
+
var roleOf = (line) => line.trim().split(/[\s"]/)[0];
|
|
153
|
+
function renderCompare(customTree, native) {
|
|
154
|
+
const norm = (l) => l.trim().replace(/ \(level \d+\)$/, "");
|
|
155
|
+
const keep = (l) => COMPARE_ROLES.has(roleOf(l));
|
|
156
|
+
const customPairs = customTree.split("\n").filter(Boolean).map(norm).filter(keep);
|
|
157
|
+
const nativePairs = native.pairs.filter(keep);
|
|
158
|
+
const count = (arr) => {
|
|
159
|
+
const m = /* @__PURE__ */ new Map();
|
|
160
|
+
for (const s of arr) m.set(s, (m.get(s) ?? 0) + 1);
|
|
161
|
+
return m;
|
|
162
|
+
};
|
|
163
|
+
const cc = count(customPairs);
|
|
164
|
+
const nc = count(nativePairs);
|
|
165
|
+
const onlyIn = (a, b) => {
|
|
166
|
+
const out = [];
|
|
167
|
+
for (const [k, n] of a) {
|
|
168
|
+
for (let i = 0; i < n - (b.get(k) ?? 0); i++) out.push(k);
|
|
169
|
+
}
|
|
170
|
+
return out.sort();
|
|
171
|
+
};
|
|
172
|
+
const onlyCustom = onlyIn(cc, nc);
|
|
173
|
+
const onlyNative = onlyIn(nc, cc);
|
|
174
|
+
const total = onlyCustom.length + onlyNative.length;
|
|
175
|
+
if (total === 0) {
|
|
176
|
+
return "Custom and native accessibility trees agree \u2014 no role/name divergences.";
|
|
177
|
+
}
|
|
178
|
+
return [
|
|
179
|
+
`Custom vs. native (Chromium) accessibility tree \u2014 ${total} divergence(s). These are role/name pairs the two disagree on \u2014 a signal of an engine fidelity gap (though some "only in native" entries are iframe / shadow-DOM content the custom engine doesn't traverse, not name bugs). Matching nodes are omitted.`,
|
|
180
|
+
"",
|
|
181
|
+
"Only in the CUSTOM tree (Real A11y engine):",
|
|
182
|
+
...onlyCustom.length ? onlyCustom.map((l) => ` ${l}`) : [" (none)"],
|
|
183
|
+
"",
|
|
184
|
+
"Only in the NATIVE tree (Chromium):",
|
|
185
|
+
...onlyNative.length ? onlyNative.map((l) => ` ${l}`) : [" (none)"]
|
|
186
|
+
].join("\n");
|
|
187
|
+
}
|
|
188
|
+
var READ_ONLY = {
|
|
189
|
+
readOnlyHint: true,
|
|
190
|
+
idempotentHint: true,
|
|
191
|
+
openWorldHint: false
|
|
192
|
+
};
|
|
193
|
+
function buildServer(session, options = {}) {
|
|
194
|
+
const authenticated = options.authenticated === true;
|
|
195
|
+
const server = new McpServer(
|
|
196
|
+
{
|
|
197
|
+
name: "real-a11y",
|
|
198
|
+
title: "Real A11y \u2014 accessibility audits",
|
|
199
|
+
version: packageVersion()
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
instructions: "Audit any web page's accessibility for AI agents. Call open_page(url) FIRST, then use audit_page (violations), inspect_page (findings + tree + outline + tab order from one consistent snapshot \u2014 prefer on dynamic pages), or the get_* / list_elements views. All tools share ONE browser page \u2014 issue calls sequentially, never in parallel."
|
|
203
|
+
}
|
|
204
|
+
);
|
|
205
|
+
server.registerTool(
|
|
206
|
+
"open_page",
|
|
207
|
+
{
|
|
208
|
+
title: "Open page",
|
|
209
|
+
description: "Navigate the browser to a URL and prepare it for accessibility queries. Call this before any audit/get_* tool. For dynamic sites (SPAs, consent dialogs) set waitUntil='networkidle' and/or settleMs so the page settles first. To audit the MOBILE or TABLET layout \u2014 which can differ substantially from desktop (hamburger nav, hidden content, touch-only controls) \u2014 pass a `device`." + (authenticated ? " This server was started with a saved login session, so pages open ALREADY AUTHENTICATED \u2014 do not try to log in or navigate to a login page; open the destination directly." : ""),
|
|
210
|
+
inputSchema: {
|
|
211
|
+
url: z.string().url().describe("Absolute URL to open."),
|
|
212
|
+
waitUntil: z.enum(["load", "domcontentloaded", "networkidle", "commit"]).default("load").describe(
|
|
213
|
+
"Navigation wait state. 'networkidle' is the most reliable 'SPA finished rendering' signal (slower)."
|
|
214
|
+
),
|
|
215
|
+
settleMs: z.number().int().min(0).max(15e3).default(0).describe(
|
|
216
|
+
"Extra wait (ms) after load for late JS / consent dialogs to settle."
|
|
217
|
+
),
|
|
218
|
+
timeoutMs: z.number().int().min(0).max(12e4).optional().describe("Navigation timeout in ms (default 30000)."),
|
|
219
|
+
device: z.string().optional().describe(
|
|
220
|
+
"Emulate a device so the tree reflects the mobile/tablet layout. A Playwright device name, e.g. 'iPhone 13', 'Pixel 7', 'iPad Pro 11'. Omit for desktop."
|
|
221
|
+
),
|
|
222
|
+
viewport: z.object({
|
|
223
|
+
width: z.number().int().positive(),
|
|
224
|
+
height: z.number().int().positive()
|
|
225
|
+
}).optional().describe(
|
|
226
|
+
"Explicit viewport override, e.g. { width: 375, height: 812 }. Layered on top of `device`."
|
|
227
|
+
)
|
|
228
|
+
},
|
|
229
|
+
annotations: {
|
|
230
|
+
readOnlyHint: false,
|
|
231
|
+
destructiveHint: false,
|
|
232
|
+
idempotentHint: true,
|
|
233
|
+
openWorldHint: true
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
async ({ url, waitUntil, settleMs, timeoutMs, device, viewport }) => {
|
|
237
|
+
const info = await session.open(url, {
|
|
238
|
+
waitUntil,
|
|
239
|
+
settleMs,
|
|
240
|
+
timeoutMs,
|
|
241
|
+
device,
|
|
242
|
+
viewport
|
|
243
|
+
});
|
|
244
|
+
const emu = device ? ` [${device}]` : viewport ? ` [${viewport.width}\xD7${viewport.height}]` : "";
|
|
245
|
+
return text(
|
|
246
|
+
`Opened ${info.url}${emu}
|
|
247
|
+
Title: ${info.title || "(untitled)"}` + (authenticated ? "\n(authenticated session: storage state loaded)" : "")
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
);
|
|
251
|
+
server.registerTool(
|
|
252
|
+
"close_browser",
|
|
253
|
+
{
|
|
254
|
+
title: "Close browser",
|
|
255
|
+
description: "Close the browser session and free resources.",
|
|
256
|
+
inputSchema: {},
|
|
257
|
+
annotations: {
|
|
258
|
+
readOnlyHint: false,
|
|
259
|
+
destructiveHint: true,
|
|
260
|
+
idempotentHint: true,
|
|
261
|
+
openWorldHint: false
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
async () => {
|
|
265
|
+
await session.close();
|
|
266
|
+
return text("Browser session closed.");
|
|
267
|
+
}
|
|
268
|
+
);
|
|
269
|
+
server.registerTool(
|
|
270
|
+
"audit_page",
|
|
271
|
+
{
|
|
272
|
+
title: "Audit accessibility",
|
|
273
|
+
annotations: READ_ONLY,
|
|
274
|
+
description: "Run accessibility audits against the current page and return every violation \u2014 unlabeled interactive controls, skipped heading levels or missing/duplicate h1, unlabeled dialogs, and broken landmark structure. Reports what real assistive tech would announce as broken. This is the primary tool.",
|
|
275
|
+
inputSchema: {
|
|
276
|
+
rootSelector,
|
|
277
|
+
rules: z.array(z.enum(RULES)).optional().describe("Subset of rules to run. Omit to run all rules.")
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
async ({ rootSelector: rootSelector2, rules }) => {
|
|
281
|
+
const findings = await session.call(
|
|
282
|
+
"collectFindings",
|
|
283
|
+
rootSelector2,
|
|
284
|
+
rules && rules.length ? [rules] : []
|
|
285
|
+
);
|
|
286
|
+
return text(renderAudit(findings));
|
|
287
|
+
}
|
|
288
|
+
);
|
|
289
|
+
server.registerTool(
|
|
290
|
+
"inspect_page",
|
|
291
|
+
{
|
|
292
|
+
title: "Inspect page (single snapshot)",
|
|
293
|
+
annotations: READ_ONLY,
|
|
294
|
+
description: "Return the audit findings AND the semantic tree, heading outline, and tab order \u2014 all derived from ONE extraction, so they are guaranteed internally consistent. Prefer this over separate audit_page + get_* calls on dynamic pages (SPAs, pages with consent dialogs) where separate calls could catch different states.",
|
|
295
|
+
inputSchema: {
|
|
296
|
+
rootSelector,
|
|
297
|
+
rules: z.array(z.enum(RULES)).optional().describe("Subset of rules for the findings. Omit to run all."),
|
|
298
|
+
includeGeneric: z.boolean().default(false).describe("Include generic container nodes in the tree.")
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
async ({ rootSelector: rootSelector2, rules, includeGeneric }) => {
|
|
302
|
+
const snap = await session.snapshot(rootSelector2, {
|
|
303
|
+
rules,
|
|
304
|
+
includeGeneric
|
|
305
|
+
});
|
|
306
|
+
return text(renderSnapshot(snap));
|
|
307
|
+
}
|
|
308
|
+
);
|
|
309
|
+
server.registerTool(
|
|
310
|
+
"get_semantic_tree",
|
|
311
|
+
{
|
|
312
|
+
title: "Get semantic tree",
|
|
313
|
+
annotations: READ_ONLY,
|
|
314
|
+
description: "Return the page's accessibility tree as a deterministic, indented role + accessible-name outline (what a screen reader would traverse). Token-efficient and stable across runs.",
|
|
315
|
+
inputSchema: {
|
|
316
|
+
rootSelector,
|
|
317
|
+
includeGeneric: z.boolean().default(false).describe("Include generic container nodes (role=generic).")
|
|
318
|
+
}
|
|
319
|
+
},
|
|
320
|
+
async ({ rootSelector: rootSelector2, includeGeneric }) => {
|
|
321
|
+
const tree = await session.call("auditSnapshot", rootSelector2, [
|
|
322
|
+
{ includeGeneric }
|
|
323
|
+
]);
|
|
324
|
+
return text(tree || "(empty tree)");
|
|
325
|
+
}
|
|
326
|
+
);
|
|
327
|
+
server.registerTool(
|
|
328
|
+
"get_heading_outline",
|
|
329
|
+
{
|
|
330
|
+
title: "Get heading outline",
|
|
331
|
+
annotations: READ_ONLY,
|
|
332
|
+
description: "Return the page's heading outline (h1..h6 in document order) as an indented list.",
|
|
333
|
+
inputSchema: { rootSelector }
|
|
334
|
+
},
|
|
335
|
+
async ({ rootSelector: rootSelector2 }) => {
|
|
336
|
+
const outline = await session.call(
|
|
337
|
+
"outlineSnapshot",
|
|
338
|
+
rootSelector2
|
|
339
|
+
);
|
|
340
|
+
return text(outline);
|
|
341
|
+
}
|
|
342
|
+
);
|
|
343
|
+
server.registerTool(
|
|
344
|
+
"get_tab_order",
|
|
345
|
+
{
|
|
346
|
+
title: "Get tab order",
|
|
347
|
+
annotations: READ_ONLY,
|
|
348
|
+
description: "Return the focusable elements in the order a keyboard user encounters them when pressing Tab, numbered, with role + accessible name.",
|
|
349
|
+
inputSchema: { rootSelector }
|
|
350
|
+
},
|
|
351
|
+
async ({ rootSelector: rootSelector2 }) => {
|
|
352
|
+
const seq = await session.call(
|
|
353
|
+
"tabSequenceSnapshot",
|
|
354
|
+
rootSelector2
|
|
355
|
+
);
|
|
356
|
+
return text(seq);
|
|
357
|
+
}
|
|
358
|
+
);
|
|
359
|
+
server.registerTool(
|
|
360
|
+
"list_elements",
|
|
361
|
+
{
|
|
362
|
+
title: "List elements by category",
|
|
363
|
+
annotations: READ_ONLY,
|
|
364
|
+
description: "List every element of one category \u2014 links, buttons, form controls, landmarks, images, or headings \u2014 as role + accessible name + a CSS locator. A token-efficient way to review one kind of element (e.g. 'images' pairs with the image-alt rule, 'form' with labeling). Scope with rootSelector.",
|
|
365
|
+
inputSchema: {
|
|
366
|
+
filter: z.enum(["heading", "link", "button", "form", "landmark", "image"]).describe("Which category of element to list."),
|
|
367
|
+
rootSelector
|
|
368
|
+
}
|
|
369
|
+
},
|
|
370
|
+
async ({ filter, rootSelector: rootSelector2 }) => {
|
|
371
|
+
const list = await session.call("listByRole", rootSelector2, [
|
|
372
|
+
filter
|
|
373
|
+
]);
|
|
374
|
+
return text(list || "(none)");
|
|
375
|
+
}
|
|
376
|
+
);
|
|
377
|
+
server.registerTool(
|
|
378
|
+
"get_native_tree",
|
|
379
|
+
{
|
|
380
|
+
title: "Get native accessibility tree",
|
|
381
|
+
annotations: READ_ONLY,
|
|
382
|
+
description: "Return Chromium's OWN accessibility tree (computed by Blink, read via CDP) as role + accessible name. This is the browser's authoritative tree, not Real A11y's custom extraction. Whole document. Chromium only.",
|
|
383
|
+
inputSchema: {}
|
|
384
|
+
},
|
|
385
|
+
async () => {
|
|
386
|
+
const native = await session.nativeAX();
|
|
387
|
+
return text(native.tree || "(empty)");
|
|
388
|
+
}
|
|
389
|
+
);
|
|
390
|
+
server.registerTool(
|
|
391
|
+
"compare_trees",
|
|
392
|
+
{
|
|
393
|
+
title: "Compare custom vs. native tree",
|
|
394
|
+
annotations: READ_ONLY,
|
|
395
|
+
description: "Diff Real A11y's custom accessibility tree against Chromium's native tree and report where they disagree on role or accessible name \u2014 a fidelity oracle that surfaces custom-engine bugs (e.g. an unlabeled input the custom engine names by its typed value). Whole document. Chromium only.",
|
|
396
|
+
inputSchema: {}
|
|
397
|
+
},
|
|
398
|
+
async () => {
|
|
399
|
+
const [customTree, native] = await Promise.all([
|
|
400
|
+
session.call("auditSnapshot", "body", [{}]),
|
|
401
|
+
session.nativeAX()
|
|
402
|
+
]);
|
|
403
|
+
return text(renderCompare(customTree, native));
|
|
404
|
+
}
|
|
405
|
+
);
|
|
406
|
+
return server;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export { buildServer, renderAudit, renderCompare, renderSnapshot };
|
|
410
|
+
//# sourceMappingURL=chunk-SF5GJYVW.js.map
|
|
411
|
+
//# sourceMappingURL=chunk-SF5GJYVW.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/server.ts"],"names":["rootSelector"],"mappings":"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","file":"chunk-SF5GJYVW.js","sourcesContent":["/**\n * Real A11y MCP server.\n *\n * Exposes the semantic accessibility tree — and, more importantly, the audit\n * results — to AI agents over the Model Context Protocol.\n *\n * Design: audit-first. The `audit_page` tool is the reason this server exists;\n * the `get_*` tools are perception primitives that also let it stand alone\n * without a separate browser-automation MCP.\n */\n\nimport { readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { ALL_RULES } from \"@real-a11y-dev/testing\";\nimport type { A11yRule, Finding } from \"@real-a11y-dev/testing\";\nimport { z } from \"zod\";\n\nimport type { A11ySession, PageSnapshot } from \"./browser.js\";\n\nexport { BrowserSession } from \"./browser.js\";\nexport type {\n A11ySession,\n BrowserSessionOptions,\n PageSnapshot,\n SnapshotOptions,\n} from \"./browser.js\";\n\n// Built from testing's ALL_RULES so the tool schema can never drift from the\n// rules the engine actually runs (a hand-maintained copy dropped `image-alt`).\nconst RULES = ALL_RULES as unknown as [A11yRule, ...A11yRule[]];\n\n/** This package's version, read at runtime — never hand-maintained in code. */\nfunction packageVersion(): string {\n try {\n const p = join(\n dirname(fileURLToPath(import.meta.url)),\n \"..\",\n \"package.json\",\n );\n return (JSON.parse(readFileSync(p, \"utf8\")).version as string) ?? \"0.0.0\";\n } catch {\n return \"0.0.0\";\n }\n}\n\nconst rootSelector = z\n .string()\n .default(\"body\")\n .describe(\"CSS selector for the audit/extraction root. Defaults to 'body'.\");\n\n/** Cap oversized tool output so a huge page can't blow the agent's context. */\nconst MAX_OUTPUT_CHARS = 40_000;\nfunction bounded(body: string): string {\n if (body.length <= MAX_OUTPUT_CHARS) return body;\n return (\n body.slice(0, MAX_OUTPUT_CHARS) +\n `\\n\\n… output truncated at ${MAX_OUTPUT_CHARS} chars — narrow with rootSelector.`\n );\n}\n\nfunction text(body: string) {\n return { content: [{ type: \"text\" as const, text: bounded(body) }] };\n}\n\nconst SEVERITY_ORDER: Record<Finding[\"severity\"], number> = {\n error: 0,\n warning: 1,\n};\nconst MAX_LOCATORS = 8;\n\n/**\n * Render findings as a compact agent-readable report plus a JSON block.\n * Identical findings (same severity/rule/message) are grouped with a count and\n * their per-instance locators, so \"17 unlabeled links\" is one row, not 17.\n */\nexport function renderAudit(findings: Finding[]): string {\n const errors = findings.filter((f) => f.severity === \"error\").length;\n const warnings = findings.length - errors;\n if (findings.length === 0) return \"No accessibility issues found.\";\n\n const header = `${findings.length} issue(s) — ${errors} error(s), ${warnings} warning(s):`;\n\n type Group = {\n severity: Finding[\"severity\"];\n rule: string;\n message: string;\n count: number;\n where: string[];\n };\n const groups = new Map<string, Group>();\n for (const f of findings) {\n const key = `${f.severity}|${f.rule}|${f.message}`;\n let g = groups.get(key);\n if (!g) {\n g = {\n severity: f.severity,\n rule: f.rule,\n message: f.message,\n count: 0,\n where: [],\n };\n groups.set(key, g);\n }\n g.count += 1;\n if (f.locator) {\n g.where.push(f.context ? `${f.locator} ${f.context}` : f.locator);\n }\n }\n\n const sorted = [...groups.values()].sort(\n (a, b) =>\n SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity] ||\n b.count - a.count,\n );\n\n const lines: string[] = [];\n for (const g of sorted) {\n const countStr = g.count > 1 ? ` (×${g.count})` : \"\";\n lines.push(` [${g.severity}] ${g.rule}: ${g.message}${countStr}`);\n for (const w of g.where.slice(0, MAX_LOCATORS)) lines.push(` ${w}`);\n if (g.where.length > MAX_LOCATORS) {\n lines.push(` … +${g.where.length - MAX_LOCATORS} more`);\n }\n }\n\n // Cap the raw findings array so a page with thousands of issues can't blow\n // the agent's context; the grouped human summary above still covers them all.\n const MAX_JSON_FINDINGS = 200;\n const capped = findings.slice(0, MAX_JSON_FINDINGS);\n const json = JSON.stringify(\n {\n summary: {\n total: findings.length,\n errors,\n warnings,\n ...(findings.length > MAX_JSON_FINDINGS\n ? { findingsTruncatedTo: MAX_JSON_FINDINGS }\n : {}),\n },\n findings: capped,\n },\n null,\n 2,\n );\n return `${header}\\n${lines.join(\"\\n\")}\\n\\n\\`\\`\\`json\\n${json}\\n\\`\\`\\``;\n}\n\n/** Render a single-extraction snapshot: audit + all three views, consistent. */\nexport function renderSnapshot(snap: PageSnapshot): string {\n const treeNodes = snap.tree.split(\"\\n\").filter(Boolean).length;\n const tabStops = snap.tabOrder\n .split(\"\\n\")\n .filter((l) => /^\\d/.test(l)).length;\n return [\n `Single-extraction snapshot — ${treeNodes} tree nodes, ${tabStops} tab stops. All sections below describe the same instant.`,\n \"\",\n renderAudit(snap.findings),\n \"\",\n \"## Semantic tree\",\n \"```\",\n snap.tree || \"(empty)\",\n \"```\",\n \"\",\n \"## Heading outline\",\n \"```\",\n snap.outline,\n \"```\",\n \"\",\n \"## Tab order\",\n \"```\",\n snap.tabOrder,\n \"```\",\n ].join(\"\\n\");\n}\n\n// Roles where the accessible name is meaningful and well-defined, so a custom\n// vs. native disagreement is a real fidelity signal. Pure text/structure roles\n// (paragraph, list, generic, StaticText…) are excluded — the two engines\n// represent text differently by design, and comparing those is just noise.\nconst COMPARE_ROLES = new Set([\n // interactive controls\n \"button\",\n \"link\",\n \"textbox\",\n \"searchbox\",\n \"combobox\",\n \"checkbox\",\n \"radio\",\n \"switch\",\n \"slider\",\n \"spinbutton\",\n \"menuitem\",\n \"menuitemcheckbox\",\n \"menuitemradio\",\n \"option\",\n \"tab\",\n // named non-interactive\n \"heading\",\n \"img\",\n \"dialog\",\n \"alertdialog\",\n // landmarks\n \"main\",\n \"navigation\",\n \"banner\",\n \"contentinfo\",\n \"complementary\",\n \"region\",\n \"form\",\n \"search\",\n]);\n\nconst roleOf = (line: string): string => line.trim().split(/[\\s\"]/)[0];\n\n/**\n * Diff the custom tree against the native (Chromium) tree and report where they\n * disagree on role or accessible name — a fidelity oracle. Compares only\n * name-bearing roles ({@link COMPARE_ROLES}), order- and indent-insensitively,\n * so structural/text representation differences don't drown out real signal.\n */\nexport function renderCompare(\n customTree: string,\n native: { tree: string; pairs: string[] },\n): string {\n // Single literal space (not `\\s+`) — the tree serializer always emits exactly\n // one space before \"(level N)\", and an unbounded `\\s+` on audited-page text is\n // a polynomial-ReDoS surface (CodeQL js/polynomial-redos).\n const norm = (l: string) => l.trim().replace(/ \\(level \\d+\\)$/, \"\");\n const keep = (l: string) => COMPARE_ROLES.has(roleOf(l));\n const customPairs = customTree\n .split(\"\\n\")\n .filter(Boolean)\n .map(norm)\n .filter(keep);\n const nativePairs = native.pairs.filter(keep);\n\n const count = (arr: string[]) => {\n const m = new Map<string, number>();\n for (const s of arr) m.set(s, (m.get(s) ?? 0) + 1);\n return m;\n };\n const cc = count(customPairs);\n const nc = count(nativePairs);\n const onlyIn = (a: Map<string, number>, b: Map<string, number>) => {\n const out: string[] = [];\n for (const [k, n] of a) {\n for (let i = 0; i < n - (b.get(k) ?? 0); i++) out.push(k);\n }\n return out.sort();\n };\n const onlyCustom = onlyIn(cc, nc);\n const onlyNative = onlyIn(nc, cc);\n const total = onlyCustom.length + onlyNative.length;\n\n if (total === 0) {\n return \"Custom and native accessibility trees agree — no role/name divergences.\";\n }\n return [\n `Custom vs. native (Chromium) accessibility tree — ${total} divergence(s). These are role/name pairs the two disagree on — a signal of an engine fidelity gap (though some \"only in native\" entries are iframe / shadow-DOM content the custom engine doesn't traverse, not name bugs). Matching nodes are omitted.`,\n \"\",\n \"Only in the CUSTOM tree (Real A11y engine):\",\n ...(onlyCustom.length ? onlyCustom.map((l) => ` ${l}`) : [\" (none)\"]),\n \"\",\n \"Only in the NATIVE tree (Chromium):\",\n ...(onlyNative.length ? onlyNative.map((l) => ` ${l}`) : [\" (none)\"]),\n ].join(\"\\n\");\n}\n\n/**\n * Build the MCP server and register every tool against the given session.\n * The session is injected so the server can be exercised in tests with a fake\n * (no browser); production wires in a real {@link BrowserSession}.\n */\n/** Hints for the read-only query tools (no side effects, closed world). */\nconst READ_ONLY = {\n readOnlyHint: true,\n idempotentHint: true,\n openWorldHint: false,\n} as const;\n\nexport interface BuildServerOptions {\n /**\n * True when the server was started with a saved login session\n * (`REAL_A11Y_MCP_STORAGE_STATE`). Surfaces the fact to the agent — in\n * `open_page`'s description and result — so it doesn't try to \"fix\" a page\n * that's already authenticated. A boolean only; the path/contents are never\n * exposed through any tool.\n */\n authenticated?: boolean;\n}\n\nexport function buildServer(\n session: A11ySession,\n options: BuildServerOptions = {},\n): McpServer {\n const authenticated = options.authenticated === true;\n const server = new McpServer(\n {\n name: \"real-a11y\",\n title: \"Real A11y — accessibility audits\",\n version: packageVersion(),\n },\n {\n instructions:\n \"Audit any web page's accessibility for AI agents. Call open_page(url) FIRST, then use audit_page (violations), inspect_page (findings + tree + outline + tab order from one consistent snapshot — prefer on dynamic pages), or the get_* / list_elements views. All tools share ONE browser page — issue calls sequentially, never in parallel.\",\n },\n );\n\n // ── Session ────────────────────────────────────────────────────────────\n server.registerTool(\n \"open_page\",\n {\n title: \"Open page\",\n description:\n \"Navigate the browser to a URL and prepare it for accessibility queries. Call this before any audit/get_* tool. For dynamic sites (SPAs, consent dialogs) set waitUntil='networkidle' and/or settleMs so the page settles first. To audit the MOBILE or TABLET layout — which can differ substantially from desktop (hamburger nav, hidden content, touch-only controls) — pass a `device`.\" +\n (authenticated\n ? \" This server was started with a saved login session, so pages open ALREADY AUTHENTICATED — do not try to log in or navigate to a login page; open the destination directly.\"\n : \"\"),\n inputSchema: {\n url: z.string().url().describe(\"Absolute URL to open.\"),\n waitUntil: z\n .enum([\"load\", \"domcontentloaded\", \"networkidle\", \"commit\"])\n .default(\"load\")\n .describe(\n \"Navigation wait state. 'networkidle' is the most reliable 'SPA finished rendering' signal (slower).\",\n ),\n settleMs: z\n .number()\n .int()\n .min(0)\n .max(15000)\n .default(0)\n .describe(\n \"Extra wait (ms) after load for late JS / consent dialogs to settle.\",\n ),\n timeoutMs: z\n .number()\n .int()\n .min(0)\n .max(120000)\n .optional()\n .describe(\"Navigation timeout in ms (default 30000).\"),\n device: z\n .string()\n .optional()\n .describe(\n \"Emulate a device so the tree reflects the mobile/tablet layout. A Playwright device name, e.g. 'iPhone 13', 'Pixel 7', 'iPad Pro 11'. Omit for desktop.\",\n ),\n viewport: z\n .object({\n width: z.number().int().positive(),\n height: z.number().int().positive(),\n })\n .optional()\n .describe(\n \"Explicit viewport override, e.g. { width: 375, height: 812 }. Layered on top of `device`.\",\n ),\n },\n annotations: {\n readOnlyHint: false,\n destructiveHint: false,\n idempotentHint: true,\n openWorldHint: true,\n },\n },\n async ({ url, waitUntil, settleMs, timeoutMs, device, viewport }) => {\n const info = await session.open(url, {\n waitUntil,\n settleMs,\n timeoutMs,\n device,\n viewport,\n });\n const emu = device\n ? ` [${device}]`\n : viewport\n ? ` [${viewport.width}×${viewport.height}]`\n : \"\";\n return text(\n `Opened ${info.url}${emu}\\nTitle: ${info.title || \"(untitled)\"}` +\n (authenticated\n ? \"\\n(authenticated session: storage state loaded)\"\n : \"\"),\n );\n },\n );\n\n server.registerTool(\n \"close_browser\",\n {\n title: \"Close browser\",\n description: \"Close the browser session and free resources.\",\n inputSchema: {},\n annotations: {\n readOnlyHint: false,\n destructiveHint: true,\n idempotentHint: true,\n openWorldHint: false,\n },\n },\n async () => {\n await session.close();\n return text(\"Browser session closed.\");\n },\n );\n\n // ── Audit (the differentiator) ───────────────────────────────────────────\n server.registerTool(\n \"audit_page\",\n {\n title: \"Audit accessibility\",\n annotations: READ_ONLY,\n description:\n \"Run accessibility audits against the current page and return every violation — unlabeled interactive controls, skipped heading levels or missing/duplicate h1, unlabeled dialogs, and broken landmark structure. Reports what real assistive tech would announce as broken. This is the primary tool.\",\n inputSchema: {\n rootSelector,\n rules: z\n .array(z.enum(RULES))\n .optional()\n .describe(\"Subset of rules to run. Omit to run all rules.\"),\n },\n },\n async ({ rootSelector, rules }) => {\n const findings = await session.call<Finding[]>(\n \"collectFindings\",\n rootSelector,\n rules && rules.length ? [rules] : [],\n );\n return text(renderAudit(findings));\n },\n );\n\n server.registerTool(\n \"inspect_page\",\n {\n title: \"Inspect page (single snapshot)\",\n annotations: READ_ONLY,\n description:\n \"Return the audit findings AND the semantic tree, heading outline, and tab order — all derived from ONE extraction, so they are guaranteed internally consistent. Prefer this over separate audit_page + get_* calls on dynamic pages (SPAs, pages with consent dialogs) where separate calls could catch different states.\",\n inputSchema: {\n rootSelector,\n rules: z\n .array(z.enum(RULES))\n .optional()\n .describe(\"Subset of rules for the findings. Omit to run all.\"),\n includeGeneric: z\n .boolean()\n .default(false)\n .describe(\"Include generic container nodes in the tree.\"),\n },\n },\n async ({ rootSelector, rules, includeGeneric }) => {\n const snap = await session.snapshot(rootSelector, {\n rules,\n includeGeneric,\n });\n return text(renderSnapshot(snap));\n },\n );\n\n // ── Inspect (perception primitives) ───────────────────────────────────────\n server.registerTool(\n \"get_semantic_tree\",\n {\n title: \"Get semantic tree\",\n annotations: READ_ONLY,\n description:\n \"Return the page's accessibility tree as a deterministic, indented role + accessible-name outline (what a screen reader would traverse). Token-efficient and stable across runs.\",\n inputSchema: {\n rootSelector,\n includeGeneric: z\n .boolean()\n .default(false)\n .describe(\"Include generic container nodes (role=generic).\"),\n },\n },\n async ({ rootSelector, includeGeneric }) => {\n const tree = await session.call<string>(\"auditSnapshot\", rootSelector, [\n { includeGeneric },\n ]);\n return text(tree || \"(empty tree)\");\n },\n );\n\n server.registerTool(\n \"get_heading_outline\",\n {\n title: \"Get heading outline\",\n annotations: READ_ONLY,\n description:\n \"Return the page's heading outline (h1..h6 in document order) as an indented list.\",\n inputSchema: { rootSelector },\n },\n async ({ rootSelector }) => {\n const outline = await session.call<string>(\n \"outlineSnapshot\",\n rootSelector,\n );\n return text(outline);\n },\n );\n\n server.registerTool(\n \"get_tab_order\",\n {\n title: \"Get tab order\",\n annotations: READ_ONLY,\n description:\n \"Return the focusable elements in the order a keyboard user encounters them when pressing Tab, numbered, with role + accessible name.\",\n inputSchema: { rootSelector },\n },\n async ({ rootSelector }) => {\n const seq = await session.call<string>(\n \"tabSequenceSnapshot\",\n rootSelector,\n );\n return text(seq);\n },\n );\n\n server.registerTool(\n \"list_elements\",\n {\n title: \"List elements by category\",\n annotations: READ_ONLY,\n description:\n \"List every element of one category — links, buttons, form controls, landmarks, images, or headings — as role + accessible name + a CSS locator. A token-efficient way to review one kind of element (e.g. 'images' pairs with the image-alt rule, 'form' with labeling). Scope with rootSelector.\",\n inputSchema: {\n filter: z\n .enum([\"heading\", \"link\", \"button\", \"form\", \"landmark\", \"image\"])\n .describe(\"Which category of element to list.\"),\n rootSelector,\n },\n },\n async ({ filter, rootSelector }) => {\n const list = await session.call<string>(\"listByRole\", rootSelector, [\n filter,\n ]);\n return text(list || \"(none)\");\n },\n );\n\n // ── Native cross-check (Chromium only) ───────────────────────────────────\n server.registerTool(\n \"get_native_tree\",\n {\n title: \"Get native accessibility tree\",\n annotations: READ_ONLY,\n description:\n \"Return Chromium's OWN accessibility tree (computed by Blink, read via CDP) as role + accessible name. This is the browser's authoritative tree, not Real A11y's custom extraction. Whole document. Chromium only.\",\n inputSchema: {},\n },\n async () => {\n const native = await session.nativeAX();\n return text(native.tree || \"(empty)\");\n },\n );\n\n server.registerTool(\n \"compare_trees\",\n {\n title: \"Compare custom vs. native tree\",\n annotations: READ_ONLY,\n description:\n \"Diff Real A11y's custom accessibility tree against Chromium's native tree and report where they disagree on role or accessible name — a fidelity oracle that surfaces custom-engine bugs (e.g. an unlabeled input the custom engine names by its typed value). Whole document. Chromium only.\",\n inputSchema: {},\n },\n async () => {\n const [customTree, native] = await Promise.all([\n session.call<string>(\"auditSnapshot\", \"body\", [{}]),\n session.nativeAX(),\n ]);\n return text(renderCompare(customTree, native));\n },\n );\n\n return server;\n}\n"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { buildServer } from './chunk-SF5GJYVW.js';
|
|
3
|
+
import { BrowserSession } from './chunk-EXVRX5JP.js';
|
|
4
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
5
|
+
import { statSync, readFileSync } from 'fs';
|
|
6
|
+
|
|
7
|
+
function assertValidStorageState(path) {
|
|
8
|
+
let raw;
|
|
9
|
+
try {
|
|
10
|
+
if (!statSync(path).isFile()) {
|
|
11
|
+
throw new Error(`REAL_A11Y_MCP_STORAGE_STATE is not a file: ${path}`);
|
|
12
|
+
}
|
|
13
|
+
raw = readFileSync(path, "utf8");
|
|
14
|
+
} catch (err) {
|
|
15
|
+
if (err instanceof Error && err.message.includes("STORAGE_STATE"))
|
|
16
|
+
throw err;
|
|
17
|
+
throw new Error(`REAL_A11Y_MCP_STORAGE_STATE could not be read: ${path}`, {
|
|
18
|
+
cause: err
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
let parsed;
|
|
22
|
+
try {
|
|
23
|
+
parsed = JSON.parse(raw);
|
|
24
|
+
} catch {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`REAL_A11Y_MCP_STORAGE_STATE is not valid JSON: ${path} (expected a Playwright storage-state file with "cookies"/"origins").`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
const shape = parsed;
|
|
30
|
+
if (typeof parsed !== "object" || parsed === null || !(Array.isArray(shape.cookies) || Array.isArray(shape.origins))) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
`REAL_A11Y_MCP_STORAGE_STATE is not a Playwright storage-state file: ${path} (expected JSON with "cookies"/"origins").`
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function parseAllowedOrigins(raw) {
|
|
37
|
+
if (!raw) return [];
|
|
38
|
+
return raw.split(",").map((s) => s.trim()).filter(Boolean).map((value) => {
|
|
39
|
+
try {
|
|
40
|
+
return new URL(value).origin;
|
|
41
|
+
} catch {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`REAL_A11Y_MCP_ALLOWED_ORIGINS contains an invalid origin: "${value}" (expected e.g. https://app.example.com).`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// src/index.ts
|
|
50
|
+
async function main() {
|
|
51
|
+
const storageState = process.env.REAL_A11Y_MCP_STORAGE_STATE;
|
|
52
|
+
if (storageState) assertValidStorageState(storageState);
|
|
53
|
+
const allowedOrigins = parseAllowedOrigins(
|
|
54
|
+
process.env.REAL_A11Y_MCP_ALLOWED_ORIGINS
|
|
55
|
+
);
|
|
56
|
+
const session = new BrowserSession({
|
|
57
|
+
cdpEndpoint: process.env.REAL_A11Y_MCP_CDP,
|
|
58
|
+
headless: process.env.REAL_A11Y_MCP_HEADFUL !== "1",
|
|
59
|
+
// Auth material is env-configured, never a tool parameter — session tokens
|
|
60
|
+
// never enter the agent's context. The constructor rejects storageState +
|
|
61
|
+
// cdpEndpoint together (a CDP connection carries its own session).
|
|
62
|
+
...storageState ? { storageState } : {},
|
|
63
|
+
...allowedOrigins.length ? { allowedOrigins } : {}
|
|
64
|
+
});
|
|
65
|
+
const server = buildServer(session, { authenticated: Boolean(storageState) });
|
|
66
|
+
let shuttingDown = false;
|
|
67
|
+
const shutdown = async (code) => {
|
|
68
|
+
if (shuttingDown) return;
|
|
69
|
+
shuttingDown = true;
|
|
70
|
+
await session.close().catch(() => {
|
|
71
|
+
});
|
|
72
|
+
await server.close().catch(() => {
|
|
73
|
+
});
|
|
74
|
+
process.exit(code);
|
|
75
|
+
};
|
|
76
|
+
process.on("SIGINT", () => void shutdown(0));
|
|
77
|
+
process.on("SIGTERM", () => void shutdown(0));
|
|
78
|
+
process.stdin.on("end", () => void shutdown(0));
|
|
79
|
+
process.stdin.on("close", () => void shutdown(0));
|
|
80
|
+
server.server.onclose = () => void shutdown(0);
|
|
81
|
+
const transport = new StdioServerTransport();
|
|
82
|
+
await server.connect(transport);
|
|
83
|
+
process.stderr.write("real-a11y MCP server running on stdio\n");
|
|
84
|
+
if (storageState) {
|
|
85
|
+
process.stderr.write(
|
|
86
|
+
` authenticated session: storage state loaded from ${storageState}
|
|
87
|
+
`
|
|
88
|
+
);
|
|
89
|
+
process.stderr.write(
|
|
90
|
+
allowedOrigins.length ? ` audit origins restricted to: ${allowedOrigins.join(", ")}
|
|
91
|
+
` : ` warning: no REAL_A11Y_MCP_ALLOWED_ORIGINS set \u2014 audits aren't origin-pinned; a redirect could audit another site with this session.
|
|
92
|
+
`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
main().catch((err) => {
|
|
97
|
+
process.stderr.write(
|
|
98
|
+
`real-a11y MCP server failed to start: ${String(err)}
|
|
99
|
+
`
|
|
100
|
+
);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
});
|
|
103
|
+
//# sourceMappingURL=index.js.map
|
|
104
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/config.ts","../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Environment-sourced configuration for the stdio server (index.ts). Kept\n * separate from the bin so the validation — which must refuse to start on a\n * bad credential file — is unit-testable without running `main()`.\n */\n\nimport { readFileSync, statSync } from \"node:fs\";\n\n/**\n * Validate the storage-state file BEFORE the server accepts tool calls — a\n * server that silently audits logged-out pages is worse than one that refuses\n * to start. The file is a credential, so errors quote the path only, never its\n * contents.\n */\nexport function assertValidStorageState(path: string): void {\n let raw: string;\n try {\n if (!statSync(path).isFile()) {\n throw new Error(`REAL_A11Y_MCP_STORAGE_STATE is not a file: ${path}`);\n }\n raw = readFileSync(path, \"utf8\");\n } catch (err) {\n if (err instanceof Error && err.message.includes(\"STORAGE_STATE\"))\n throw err;\n // The fs error carries the path (already in our message), never contents.\n throw new Error(`REAL_A11Y_MCP_STORAGE_STATE could not be read: ${path}`, {\n cause: err,\n });\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n throw new Error(\n `REAL_A11Y_MCP_STORAGE_STATE is not valid JSON: ${path} (expected a Playwright storage-state file with \"cookies\"/\"origins\").`,\n );\n }\n const shape = parsed as { cookies?: unknown; origins?: unknown };\n if (\n typeof parsed !== \"object\" ||\n parsed === null ||\n !(Array.isArray(shape.cookies) || Array.isArray(shape.origins))\n ) {\n throw new Error(\n `REAL_A11Y_MCP_STORAGE_STATE is not a Playwright storage-state file: ${path} (expected JSON with \"cookies\"/\"origins\").`,\n );\n }\n}\n\n/** Parse the comma-separated origin allowlist, normalizing each to its origin. */\nexport function parseAllowedOrigins(raw: string | undefined): string[] {\n if (!raw) return [];\n return raw\n .split(\",\")\n .map((s) => s.trim())\n .filter(Boolean)\n .map((value) => {\n try {\n return new URL(value).origin;\n } catch {\n throw new Error(\n `REAL_A11Y_MCP_ALLOWED_ORIGINS contains an invalid origin: \"${value}\" (expected e.g. https://app.example.com).`,\n );\n }\n });\n}\n","#!/usr/bin/env node\n/**\n * Real A11y MCP server — stdio entry point.\n *\n * Wire into an MCP client config (launched from an arbitrary cwd, so use the\n * package-name form, not the bare bin):\n *\n * {\n * \"mcpServers\": {\n * \"real-a11y\": { \"command\": \"npx\", \"args\": [\"-y\", \"@real-a11y-dev/mcp\"] }\n * }\n * }\n *\n * Environment:\n * REAL_A11Y_MCP_CDP Attach to a running Chrome over CDP (e.g. http://localhost:9222)\n * REAL_A11Y_MCP_HEADFUL Set to \"1\" to launch a visible browser instead of headless\n * REAL_A11Y_MCP_ALLOW_FILE Set to \"1\" to permit auditing file:// URLs (off by default)\n * REAL_A11Y_MCP_STORAGE_STATE Path to a Playwright storage-state JSON — audit pages\n * behind a login as that session (create it out-of-band, e.g.\n * `real-a11y login`; never a tool parameter)\n * REAL_A11Y_MCP_ALLOWED_ORIGINS Comma-separated origins extraction is restricted to when a\n * storage state is loaded (origin pinning). Strongly recommended\n * with STORAGE_STATE so a redirect can't audit an unintended site.\n */\n\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\n\nimport { BrowserSession } from \"./browser.js\";\nimport { assertValidStorageState, parseAllowedOrigins } from \"./config.js\";\nimport { buildServer } from \"./server.js\";\n\nasync function main(): Promise<void> {\n const storageState = process.env.REAL_A11Y_MCP_STORAGE_STATE;\n if (storageState) assertValidStorageState(storageState);\n const allowedOrigins = parseAllowedOrigins(\n process.env.REAL_A11Y_MCP_ALLOWED_ORIGINS,\n );\n\n const session = new BrowserSession({\n cdpEndpoint: process.env.REAL_A11Y_MCP_CDP,\n headless: process.env.REAL_A11Y_MCP_HEADFUL !== \"1\",\n // Auth material is env-configured, never a tool parameter — session tokens\n // never enter the agent's context. The constructor rejects storageState +\n // cdpEndpoint together (a CDP connection carries its own session).\n ...(storageState ? { storageState } : {}),\n ...(allowedOrigins.length ? { allowedOrigins } : {}),\n });\n const server = buildServer(session, { authenticated: Boolean(storageState) });\n\n // Tear down the browser exactly once on any shutdown signal. The SDK's stdio\n // transport doesn't reliably fire onclose on stdin EOF, so wire every path.\n let shuttingDown = false;\n const shutdown = async (code: number): Promise<void> => {\n if (shuttingDown) return;\n shuttingDown = true;\n await session.close().catch(() => {});\n await server.close().catch(() => {});\n process.exit(code);\n };\n process.on(\"SIGINT\", () => void shutdown(0));\n process.on(\"SIGTERM\", () => void shutdown(0));\n process.stdin.on(\"end\", () => void shutdown(0));\n process.stdin.on(\"close\", () => void shutdown(0));\n server.server.onclose = () => void shutdown(0);\n\n const transport = new StdioServerTransport();\n await server.connect(transport);\n\n // stdout is the protocol channel — log to stderr only.\n process.stderr.write(\"real-a11y MCP server running on stdio\\n\");\n if (storageState) {\n // Operator-facing: confirm the session is armed. The path is fine to log;\n // the file's contents (tokens) never are.\n process.stderr.write(\n ` authenticated session: storage state loaded from ${storageState}\\n`,\n );\n process.stderr.write(\n allowedOrigins.length\n ? ` audit origins restricted to: ${allowedOrigins.join(\", \")}\\n`\n : ` warning: no REAL_A11Y_MCP_ALLOWED_ORIGINS set — audits aren't origin-pinned; a redirect could audit another site with this session.\\n`,\n );\n }\n}\n\nmain().catch((err) => {\n process.stderr.write(\n `real-a11y MCP server failed to start: ${String(err)}\\n`,\n );\n process.exit(1);\n});\n"]}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { Finding } from '@real-a11y-dev/testing';
|
|
3
|
+
import { A11ySession, PageSnapshot } from './browser.js';
|
|
4
|
+
export { BrowserSession, BrowserSessionOptions, SnapshotOptions } from './browser.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Real A11y MCP server.
|
|
8
|
+
*
|
|
9
|
+
* Exposes the semantic accessibility tree — and, more importantly, the audit
|
|
10
|
+
* results — to AI agents over the Model Context Protocol.
|
|
11
|
+
*
|
|
12
|
+
* Design: audit-first. The `audit_page` tool is the reason this server exists;
|
|
13
|
+
* the `get_*` tools are perception primitives that also let it stand alone
|
|
14
|
+
* without a separate browser-automation MCP.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Render findings as a compact agent-readable report plus a JSON block.
|
|
19
|
+
* Identical findings (same severity/rule/message) are grouped with a count and
|
|
20
|
+
* their per-instance locators, so "17 unlabeled links" is one row, not 17.
|
|
21
|
+
*/
|
|
22
|
+
declare function renderAudit(findings: Finding[]): string;
|
|
23
|
+
/** Render a single-extraction snapshot: audit + all three views, consistent. */
|
|
24
|
+
declare function renderSnapshot(snap: PageSnapshot): string;
|
|
25
|
+
/**
|
|
26
|
+
* Diff the custom tree against the native (Chromium) tree and report where they
|
|
27
|
+
* disagree on role or accessible name — a fidelity oracle. Compares only
|
|
28
|
+
* name-bearing roles ({@link COMPARE_ROLES}), order- and indent-insensitively,
|
|
29
|
+
* so structural/text representation differences don't drown out real signal.
|
|
30
|
+
*/
|
|
31
|
+
declare function renderCompare(customTree: string, native: {
|
|
32
|
+
tree: string;
|
|
33
|
+
pairs: string[];
|
|
34
|
+
}): string;
|
|
35
|
+
interface BuildServerOptions {
|
|
36
|
+
/**
|
|
37
|
+
* True when the server was started with a saved login session
|
|
38
|
+
* (`REAL_A11Y_MCP_STORAGE_STATE`). Surfaces the fact to the agent — in
|
|
39
|
+
* `open_page`'s description and result — so it doesn't try to "fix" a page
|
|
40
|
+
* that's already authenticated. A boolean only; the path/contents are never
|
|
41
|
+
* exposed through any tool.
|
|
42
|
+
*/
|
|
43
|
+
authenticated?: boolean;
|
|
44
|
+
}
|
|
45
|
+
declare function buildServer(session: A11ySession, options?: BuildServerOptions): McpServer;
|
|
46
|
+
|
|
47
|
+
export { A11ySession, type BuildServerOptions, PageSnapshot, buildServer, renderAudit, renderCompare, renderSnapshot };
|