ai-ccesibility 0.1.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/EXAMPLES.md +595 -0
- package/LICENSE +21 -0
- package/README.md +233 -0
- package/USAGE.md +498 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +2177 -0
- package/dist/server.js.map +1 -0
- package/package.json +84 -0
package/dist/server.js
ADDED
|
@@ -0,0 +1,2177 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import pino from 'pino';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import puppeteer from 'puppeteer';
|
|
6
|
+
import AxePuppeteer from '@axe-core/puppeteer';
|
|
7
|
+
import pa11y from 'pa11y';
|
|
8
|
+
import { ESLint } from 'eslint';
|
|
9
|
+
|
|
10
|
+
// src/server.ts
|
|
11
|
+
var logLevel = process.env["LOG_LEVEL"] ?? "info";
|
|
12
|
+
var baseLogger = pino(
|
|
13
|
+
{
|
|
14
|
+
level: logLevel,
|
|
15
|
+
base: {
|
|
16
|
+
service: "ai-ccesibility",
|
|
17
|
+
version: "0.1.0"
|
|
18
|
+
},
|
|
19
|
+
timestamp: pino.stdTimeFunctions.isoTime,
|
|
20
|
+
formatters: {
|
|
21
|
+
level: (label) => ({ level: label })
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
pino.destination(2)
|
|
25
|
+
);
|
|
26
|
+
var logger = {
|
|
27
|
+
debug(message, context) {
|
|
28
|
+
baseLogger.debug(context ?? {}, message);
|
|
29
|
+
},
|
|
30
|
+
info(message, context) {
|
|
31
|
+
baseLogger.info(context ?? {}, message);
|
|
32
|
+
},
|
|
33
|
+
warn(message, context) {
|
|
34
|
+
baseLogger.warn(context ?? {}, message);
|
|
35
|
+
},
|
|
36
|
+
error(message, context) {
|
|
37
|
+
const { error, ...rest } = context ?? {};
|
|
38
|
+
if (error) {
|
|
39
|
+
baseLogger.error(
|
|
40
|
+
{
|
|
41
|
+
...rest,
|
|
42
|
+
err: {
|
|
43
|
+
message: error.message,
|
|
44
|
+
name: error.name,
|
|
45
|
+
stack: error.stack
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
message
|
|
49
|
+
);
|
|
50
|
+
} else {
|
|
51
|
+
baseLogger.error(rest, message);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
function createToolLogger(toolName) {
|
|
56
|
+
return {
|
|
57
|
+
debug(message, context) {
|
|
58
|
+
logger.debug(message, { ...context, tool: toolName });
|
|
59
|
+
},
|
|
60
|
+
info(message, context) {
|
|
61
|
+
logger.info(message, { ...context, tool: toolName });
|
|
62
|
+
},
|
|
63
|
+
warn(message, context) {
|
|
64
|
+
logger.warn(message, { ...context, tool: toolName });
|
|
65
|
+
},
|
|
66
|
+
error(message, context) {
|
|
67
|
+
logger.error(message, { ...context, tool: toolName });
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function createAdapterLogger(adapterName) {
|
|
72
|
+
return {
|
|
73
|
+
debug(message, context) {
|
|
74
|
+
logger.debug(message, { ...context, adapter: adapterName });
|
|
75
|
+
},
|
|
76
|
+
info(message, context) {
|
|
77
|
+
logger.info(message, { ...context, adapter: adapterName });
|
|
78
|
+
},
|
|
79
|
+
warn(message, context) {
|
|
80
|
+
logger.warn(message, { ...context, adapter: adapterName });
|
|
81
|
+
},
|
|
82
|
+
error(message, context) {
|
|
83
|
+
logger.error(message, { ...context, adapter: adapterName });
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function generateRequestId() {
|
|
88
|
+
return `req_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/tools/base.ts
|
|
92
|
+
function createTextResponse(text, isError = false) {
|
|
93
|
+
return {
|
|
94
|
+
content: [{ type: "text", text }],
|
|
95
|
+
isError
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function createJsonResponse(data, isError = false) {
|
|
99
|
+
return {
|
|
100
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
101
|
+
isError
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function createErrorResponse(error) {
|
|
105
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
106
|
+
return createTextResponse(`Error: ${message}`, true);
|
|
107
|
+
}
|
|
108
|
+
function withToolContext(toolName, handler) {
|
|
109
|
+
const toolLogger = createToolLogger(toolName);
|
|
110
|
+
return async (input) => {
|
|
111
|
+
const requestId = generateRequestId();
|
|
112
|
+
toolLogger.info("Tool execution started", { requestId });
|
|
113
|
+
const startTime = Date.now();
|
|
114
|
+
try {
|
|
115
|
+
const result = await handler(input, {
|
|
116
|
+
requestId,
|
|
117
|
+
logger: toolLogger
|
|
118
|
+
});
|
|
119
|
+
const duration = Date.now() - startTime;
|
|
120
|
+
toolLogger.info("Tool execution completed", {
|
|
121
|
+
requestId,
|
|
122
|
+
durationMs: duration,
|
|
123
|
+
isError: result.isError ?? false
|
|
124
|
+
});
|
|
125
|
+
return result;
|
|
126
|
+
} catch (error) {
|
|
127
|
+
const duration = Date.now() - startTime;
|
|
128
|
+
toolLogger.error("Tool execution failed", {
|
|
129
|
+
requestId,
|
|
130
|
+
durationMs: duration,
|
|
131
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
132
|
+
});
|
|
133
|
+
return createErrorResponse(error);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// src/adapters/base.ts
|
|
139
|
+
var BaseAdapter = class {
|
|
140
|
+
config;
|
|
141
|
+
logger;
|
|
142
|
+
constructor(config = {}) {
|
|
143
|
+
this.config = {
|
|
144
|
+
timeout: 3e4,
|
|
145
|
+
maxRetries: 1,
|
|
146
|
+
...config
|
|
147
|
+
};
|
|
148
|
+
this.logger = createAdapterLogger(this.constructor.name);
|
|
149
|
+
}
|
|
150
|
+
async dispose() {
|
|
151
|
+
this.logger.debug("Adapter disposed");
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// src/adapters/axe.ts
|
|
156
|
+
var SEVERITY_MAP = {
|
|
157
|
+
critical: "critical",
|
|
158
|
+
serious: "serious",
|
|
159
|
+
moderate: "moderate",
|
|
160
|
+
minor: "minor"
|
|
161
|
+
};
|
|
162
|
+
var PRINCIPLE_MAP = {
|
|
163
|
+
"1": "perceivable",
|
|
164
|
+
"2": "operable",
|
|
165
|
+
"3": "understandable",
|
|
166
|
+
"4": "robust"
|
|
167
|
+
};
|
|
168
|
+
var AxeAdapter = class extends BaseAdapter {
|
|
169
|
+
name = "axe-core";
|
|
170
|
+
version = "4.x";
|
|
171
|
+
browser = null;
|
|
172
|
+
axeConfig;
|
|
173
|
+
constructor(config = {}) {
|
|
174
|
+
super(config);
|
|
175
|
+
this.axeConfig = {
|
|
176
|
+
headless: true,
|
|
177
|
+
browserArgs: ["--no-sandbox", "--disable-setuid-sandbox"],
|
|
178
|
+
...config
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
async analyze(target, options) {
|
|
182
|
+
const startTime = Date.now();
|
|
183
|
+
let page = null;
|
|
184
|
+
try {
|
|
185
|
+
this.logger.info("Starting axe-core analysis", { target: target.value });
|
|
186
|
+
await this.ensureBrowser();
|
|
187
|
+
page = await this.browser.newPage();
|
|
188
|
+
if (target.options?.viewport) {
|
|
189
|
+
await page.setViewport(target.options.viewport);
|
|
190
|
+
}
|
|
191
|
+
await this.loadTarget(page, target);
|
|
192
|
+
const axeBuilder = new AxePuppeteer(page);
|
|
193
|
+
this.configureAxeBuilder(axeBuilder, options);
|
|
194
|
+
const results = await axeBuilder.analyze();
|
|
195
|
+
const issues = this.transformResults(results, options);
|
|
196
|
+
const duration = Date.now() - startTime;
|
|
197
|
+
this.logger.info("Analysis completed", { issueCount: issues.length, duration });
|
|
198
|
+
return this.buildSuccessResult(target.value, issues, results, duration);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
const duration = Date.now() - startTime;
|
|
201
|
+
this.logger.error("Analysis failed", { error, target: target.value });
|
|
202
|
+
return this.buildErrorResult(target.value, error, duration);
|
|
203
|
+
} finally {
|
|
204
|
+
if (page) {
|
|
205
|
+
await page.close().catch(() => {
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
async isAvailable() {
|
|
211
|
+
try {
|
|
212
|
+
await this.ensureBrowser();
|
|
213
|
+
return true;
|
|
214
|
+
} catch {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
async dispose() {
|
|
219
|
+
if (this.browser) {
|
|
220
|
+
await this.browser.close();
|
|
221
|
+
this.browser = null;
|
|
222
|
+
this.logger.debug("Browser closed");
|
|
223
|
+
}
|
|
224
|
+
await super.dispose();
|
|
225
|
+
}
|
|
226
|
+
async ensureBrowser() {
|
|
227
|
+
if (!this.browser || !this.browser.connected) {
|
|
228
|
+
this.logger.debug("Launching browser");
|
|
229
|
+
this.browser = await puppeteer.launch({
|
|
230
|
+
headless: this.axeConfig.headless ?? true,
|
|
231
|
+
args: this.axeConfig.browserArgs ?? ["--no-sandbox", "--disable-setuid-sandbox"]
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
async loadTarget(page, target) {
|
|
236
|
+
const timeout = target.options?.timeout ?? this.config.timeout ?? 3e4;
|
|
237
|
+
switch (target.type) {
|
|
238
|
+
case "url":
|
|
239
|
+
await page.goto(target.value, {
|
|
240
|
+
waitUntil: "networkidle2",
|
|
241
|
+
timeout
|
|
242
|
+
});
|
|
243
|
+
break;
|
|
244
|
+
case "html":
|
|
245
|
+
await page.setContent(target.value, {
|
|
246
|
+
waitUntil: "networkidle2",
|
|
247
|
+
timeout
|
|
248
|
+
});
|
|
249
|
+
break;
|
|
250
|
+
case "file":
|
|
251
|
+
await page.goto(`file://${target.value}`, {
|
|
252
|
+
waitUntil: "networkidle2",
|
|
253
|
+
timeout
|
|
254
|
+
});
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
if (target.options?.waitForSelector) {
|
|
258
|
+
await page.waitForSelector(target.options.waitForSelector, { timeout });
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
configureAxeBuilder(builder, options) {
|
|
262
|
+
const tags = this.getWcagTags(options?.wcagLevel ?? "AA");
|
|
263
|
+
builder.withTags(tags);
|
|
264
|
+
if (options?.rules && options.rules.length > 0) {
|
|
265
|
+
builder.withRules(options.rules);
|
|
266
|
+
}
|
|
267
|
+
if (options?.excludeRules && options.excludeRules.length > 0) {
|
|
268
|
+
builder.disableRules(options.excludeRules);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
getWcagTags(level) {
|
|
272
|
+
const baseTags = ["wcag2a", "wcag21a", "wcag22a", "best-practice"];
|
|
273
|
+
if (level === "AA" || level === "AAA") {
|
|
274
|
+
baseTags.push("wcag2aa", "wcag21aa", "wcag22aa");
|
|
275
|
+
}
|
|
276
|
+
if (level === "AAA") {
|
|
277
|
+
baseTags.push("wcag2aaa", "wcag21aaa", "wcag22aaa");
|
|
278
|
+
}
|
|
279
|
+
return baseTags;
|
|
280
|
+
}
|
|
281
|
+
transformResults(results, options) {
|
|
282
|
+
const issues = [];
|
|
283
|
+
let issueIndex = 0;
|
|
284
|
+
for (const violation of results.violations) {
|
|
285
|
+
for (const node of violation.nodes) {
|
|
286
|
+
issues.push(this.transformViolation(violation, node, issueIndex++));
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (options?.includeWarnings !== false) {
|
|
290
|
+
for (const incomplete of results.incomplete) {
|
|
291
|
+
for (const node of incomplete.nodes) {
|
|
292
|
+
issues.push(this.transformIncomplete(incomplete, node, issueIndex++));
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return issues;
|
|
297
|
+
}
|
|
298
|
+
transformViolation(violation, node, index) {
|
|
299
|
+
return {
|
|
300
|
+
id: `axe-${index}`,
|
|
301
|
+
ruleId: violation.id,
|
|
302
|
+
tool: "axe-core",
|
|
303
|
+
severity: SEVERITY_MAP[violation.impact ?? "moderate"] ?? "moderate",
|
|
304
|
+
wcag: this.extractWcagReference(violation.tags),
|
|
305
|
+
location: this.extractLocation(node),
|
|
306
|
+
message: violation.help,
|
|
307
|
+
humanContext: violation.description,
|
|
308
|
+
suggestedActions: this.extractSuggestedActions(node),
|
|
309
|
+
affectedUsers: this.inferAffectedUsers(violation),
|
|
310
|
+
confidence: 1,
|
|
311
|
+
rawResult: { violation, node }
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
transformIncomplete(incomplete, node, index) {
|
|
315
|
+
return {
|
|
316
|
+
id: `axe-incomplete-${index}`,
|
|
317
|
+
ruleId: incomplete.id,
|
|
318
|
+
tool: "axe-core",
|
|
319
|
+
severity: "minor",
|
|
320
|
+
wcag: this.extractWcagReference(incomplete.tags),
|
|
321
|
+
location: this.extractLocation(node),
|
|
322
|
+
message: `[Requires review] ${incomplete.help}`,
|
|
323
|
+
humanContext: incomplete.description,
|
|
324
|
+
suggestedActions: this.extractSuggestedActions(node),
|
|
325
|
+
affectedUsers: this.inferAffectedUsers(incomplete),
|
|
326
|
+
confidence: 0.5,
|
|
327
|
+
rawResult: { incomplete, node }
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
extractLocation(node) {
|
|
331
|
+
return {
|
|
332
|
+
selector: node.target.join(" "),
|
|
333
|
+
xpath: node.xpath?.join(" "),
|
|
334
|
+
snippet: node.html?.substring(0, 500)
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
extractWcagReference(tags) {
|
|
338
|
+
const wcagTag = this.parseWcagTags(tags);
|
|
339
|
+
if (!wcagTag) return void 0;
|
|
340
|
+
const principle = wcagTag.criterion.charAt(0);
|
|
341
|
+
return {
|
|
342
|
+
criterion: wcagTag.criterion,
|
|
343
|
+
level: wcagTag.level,
|
|
344
|
+
principle: PRINCIPLE_MAP[principle] ?? "perceivable",
|
|
345
|
+
version: wcagTag.version
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
parseWcagTags(tags) {
|
|
349
|
+
const scPattern = /^wcag(\d{3,})$/;
|
|
350
|
+
const levelPattern = /^wcag2\d?(a{1,3})$/;
|
|
351
|
+
let criterion = null;
|
|
352
|
+
let level = "A";
|
|
353
|
+
let version = "2.0";
|
|
354
|
+
for (const tag of tags) {
|
|
355
|
+
const scMatch = tag.match(scPattern);
|
|
356
|
+
if (scMatch && scMatch[1]) {
|
|
357
|
+
const digits = scMatch[1];
|
|
358
|
+
if (digits.length >= 3) {
|
|
359
|
+
criterion = `${digits[0]}.${digits[1]}.${digits.slice(2)}`;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
const levelMatch = tag.match(levelPattern);
|
|
363
|
+
if (levelMatch && levelMatch[1]) {
|
|
364
|
+
const levelStr = levelMatch[1].toUpperCase();
|
|
365
|
+
if (levelStr === "A" || levelStr === "AA" || levelStr === "AAA") {
|
|
366
|
+
level = levelStr;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (tag.includes("21")) version = "2.1";
|
|
370
|
+
if (tag.includes("22")) version = "2.2";
|
|
371
|
+
}
|
|
372
|
+
if (!criterion) return null;
|
|
373
|
+
return { criterion, level, version };
|
|
374
|
+
}
|
|
375
|
+
extractSuggestedActions(node) {
|
|
376
|
+
const actions = [];
|
|
377
|
+
for (const check of node.any ?? []) {
|
|
378
|
+
if (check.message) {
|
|
379
|
+
actions.push(check.message);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
for (const check of node.all ?? []) {
|
|
383
|
+
if (check.message) {
|
|
384
|
+
actions.push(check.message);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
for (const check of node.none ?? []) {
|
|
388
|
+
if (check.message) {
|
|
389
|
+
actions.push(`Avoid: ${check.message}`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return actions;
|
|
393
|
+
}
|
|
394
|
+
inferAffectedUsers(result) {
|
|
395
|
+
const users = /* @__PURE__ */ new Set();
|
|
396
|
+
const tags = result.tags.join(" ").toLowerCase();
|
|
397
|
+
const id = result.id.toLowerCase();
|
|
398
|
+
if (tags.includes("aria") || id.includes("aria") || id.includes("label") || id.includes("alt")) {
|
|
399
|
+
users.add("screen-reader");
|
|
400
|
+
}
|
|
401
|
+
if (id.includes("keyboard") || id.includes("focus") || id.includes("tabindex")) {
|
|
402
|
+
users.add("keyboard-only");
|
|
403
|
+
users.add("motor-impaired");
|
|
404
|
+
}
|
|
405
|
+
if (id.includes("color") || id.includes("contrast")) {
|
|
406
|
+
users.add("low-vision");
|
|
407
|
+
users.add("color-blind");
|
|
408
|
+
}
|
|
409
|
+
if (id.includes("heading") || id.includes("landmark") || id.includes("link")) {
|
|
410
|
+
users.add("screen-reader");
|
|
411
|
+
users.add("cognitive");
|
|
412
|
+
}
|
|
413
|
+
if (users.size === 0) {
|
|
414
|
+
users.add("screen-reader");
|
|
415
|
+
}
|
|
416
|
+
return Array.from(users);
|
|
417
|
+
}
|
|
418
|
+
buildSuccessResult(target, issues, axeResults, duration) {
|
|
419
|
+
const summary = this.calculateSummary(issues);
|
|
420
|
+
return {
|
|
421
|
+
success: true,
|
|
422
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
423
|
+
duration,
|
|
424
|
+
target,
|
|
425
|
+
tool: "axe-core",
|
|
426
|
+
issues,
|
|
427
|
+
summary,
|
|
428
|
+
metadata: {
|
|
429
|
+
toolVersion: axeResults.testEngine?.version,
|
|
430
|
+
browserInfo: axeResults.testEnvironment?.userAgent,
|
|
431
|
+
pageTitle: axeResults.url
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
buildErrorResult(target, error, duration) {
|
|
436
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
437
|
+
return {
|
|
438
|
+
success: false,
|
|
439
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
440
|
+
duration,
|
|
441
|
+
target,
|
|
442
|
+
tool: "axe-core",
|
|
443
|
+
issues: [],
|
|
444
|
+
summary: {
|
|
445
|
+
total: 0,
|
|
446
|
+
bySeverity: { critical: 0, serious: 0, moderate: 0, minor: 0 }
|
|
447
|
+
},
|
|
448
|
+
error: errorMessage
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
calculateSummary(issues) {
|
|
452
|
+
const bySeverity = { critical: 0, serious: 0, moderate: 0, minor: 0 };
|
|
453
|
+
const byPrinciple = { perceivable: 0, operable: 0, understandable: 0, robust: 0 };
|
|
454
|
+
const byRule = {};
|
|
455
|
+
for (const issue of issues) {
|
|
456
|
+
bySeverity[issue.severity]++;
|
|
457
|
+
if (issue.wcag?.principle) {
|
|
458
|
+
byPrinciple[issue.wcag.principle]++;
|
|
459
|
+
}
|
|
460
|
+
byRule[issue.ruleId] = (byRule[issue.ruleId] ?? 0) + 1;
|
|
461
|
+
}
|
|
462
|
+
return {
|
|
463
|
+
total: issues.length,
|
|
464
|
+
bySeverity,
|
|
465
|
+
byPrinciple,
|
|
466
|
+
byRule
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
var SeveritySchema = z.enum(["critical", "serious", "moderate", "minor"]).describe(
|
|
471
|
+
"Impact level: critical (blocks users), serious (significant barrier), moderate (some difficulty), minor (annoyance)"
|
|
472
|
+
);
|
|
473
|
+
var WCAGLevelSchema = z.enum(["A", "AA", "AAA"]).describe("WCAG conformance level: A (minimum), AA (standard), AAA (enhanced)");
|
|
474
|
+
var WCAGPrincipleSchema = z.enum(["perceivable", "operable", "understandable", "robust"]).describe(
|
|
475
|
+
"WCAG principle: perceivable (can sense), operable (can use), understandable (can comprehend), robust (works with assistive tech)"
|
|
476
|
+
);
|
|
477
|
+
var ToolSourceSchema = z.enum(["axe-core", "pa11y"]).describe("Source tool that detected the issue");
|
|
478
|
+
var WCAGCriterionSchema = z.string().regex(/^\d+\.\d+\.\d+$/, "Must be in format X.Y.Z (e.g., 1.4.3)").describe("WCAG success criterion number (e.g., 1.4.3 for Contrast)");
|
|
479
|
+
var WCAGReferenceSchema = z.object({
|
|
480
|
+
criterion: WCAGCriterionSchema,
|
|
481
|
+
level: WCAGLevelSchema,
|
|
482
|
+
principle: WCAGPrincipleSchema,
|
|
483
|
+
version: z.enum(["2.0", "2.1", "2.2"]).optional().describe("WCAG version"),
|
|
484
|
+
title: z.string().optional().describe("Human-readable criterion title"),
|
|
485
|
+
url: z.string().url().optional().describe("Link to WCAG documentation")
|
|
486
|
+
}).describe("Reference to specific WCAG success criterion");
|
|
487
|
+
var IssueLocationSchema = z.object({
|
|
488
|
+
selector: z.string().optional().describe("CSS selector to locate the element"),
|
|
489
|
+
xpath: z.string().optional().describe("XPath to locate the element"),
|
|
490
|
+
file: z.string().optional().describe("Source file path (for static analysis)"),
|
|
491
|
+
line: z.number().int().positive().optional().describe("Line number in source file"),
|
|
492
|
+
column: z.number().int().nonnegative().optional().describe("Column number in source file"),
|
|
493
|
+
snippet: z.string().max(500).optional().describe("HTML snippet of the problematic element")
|
|
494
|
+
}).describe("Location information for the accessibility issue");
|
|
495
|
+
var AccessibilityIssueSchema = z.object({
|
|
496
|
+
id: z.string().min(1).describe("Unique identifier for this issue instance"),
|
|
497
|
+
ruleId: z.string().min(1).describe("Rule identifier from the source tool"),
|
|
498
|
+
tool: ToolSourceSchema,
|
|
499
|
+
severity: SeveritySchema,
|
|
500
|
+
wcag: WCAGReferenceSchema.optional(),
|
|
501
|
+
location: IssueLocationSchema,
|
|
502
|
+
message: z.string().min(1).describe("Technical description of the issue"),
|
|
503
|
+
humanContext: z.string().optional().describe("User impact explanation for decision-making"),
|
|
504
|
+
suggestedActions: z.array(z.string()).optional().describe("Possible remediation approaches (not auto-fixes)"),
|
|
505
|
+
affectedUsers: z.array(
|
|
506
|
+
z.enum([
|
|
507
|
+
"screen-reader",
|
|
508
|
+
"keyboard-only",
|
|
509
|
+
"low-vision",
|
|
510
|
+
"color-blind",
|
|
511
|
+
"cognitive",
|
|
512
|
+
"motor-impaired"
|
|
513
|
+
])
|
|
514
|
+
).optional().describe("User groups most affected by this issue"),
|
|
515
|
+
priority: z.enum(["critical", "high", "medium", "low"]).optional().describe("Remediation priority based on impact"),
|
|
516
|
+
remediationEffort: z.enum(["low", "medium", "high"]).optional().describe("Estimated effort to fix"),
|
|
517
|
+
confidence: z.number().min(0).max(1).optional().describe("Tool confidence score (0-1)"),
|
|
518
|
+
rawResult: z.unknown().optional().describe("Original result from source tool for debugging")
|
|
519
|
+
}).describe("Normalized accessibility issue from any source tool");
|
|
520
|
+
var AnalysisSummarySchema = z.object({
|
|
521
|
+
total: z.number().int().nonnegative().describe("Total number of issues found"),
|
|
522
|
+
bySeverity: z.object({
|
|
523
|
+
critical: z.number().int().nonnegative().default(0),
|
|
524
|
+
serious: z.number().int().nonnegative().default(0),
|
|
525
|
+
moderate: z.number().int().nonnegative().default(0),
|
|
526
|
+
minor: z.number().int().nonnegative().default(0)
|
|
527
|
+
}).describe("Issue count by severity level"),
|
|
528
|
+
byPrinciple: z.object({
|
|
529
|
+
perceivable: z.number().int().nonnegative().default(0),
|
|
530
|
+
operable: z.number().int().nonnegative().default(0),
|
|
531
|
+
understandable: z.number().int().nonnegative().default(0),
|
|
532
|
+
robust: z.number().int().nonnegative().default(0)
|
|
533
|
+
}).optional().describe("Issue count by WCAG principle"),
|
|
534
|
+
byRule: z.record(z.string(), z.number().int().nonnegative()).optional().describe("Issue count grouped by rule ID")
|
|
535
|
+
}).describe("Summary statistics for the analysis");
|
|
536
|
+
var AnalysisResultSchema = z.object({
|
|
537
|
+
success: z.boolean().describe("Whether the analysis completed without errors"),
|
|
538
|
+
timestamp: z.string().datetime().describe("ISO 8601 timestamp of analysis"),
|
|
539
|
+
duration: z.number().int().nonnegative().optional().describe("Analysis duration in milliseconds"),
|
|
540
|
+
target: z.string().describe("URL, file path, or identifier of analyzed target"),
|
|
541
|
+
tool: ToolSourceSchema,
|
|
542
|
+
issues: z.array(AccessibilityIssueSchema).describe("List of accessibility issues found"),
|
|
543
|
+
summary: AnalysisSummarySchema,
|
|
544
|
+
metadata: z.object({
|
|
545
|
+
toolVersion: z.string().optional(),
|
|
546
|
+
browserInfo: z.string().optional(),
|
|
547
|
+
pageTitle: z.string().optional()
|
|
548
|
+
}).optional().describe("Additional context about the analysis"),
|
|
549
|
+
error: z.string().optional().describe("Error message if analysis failed")
|
|
550
|
+
}).describe("Complete result of an accessibility analysis");
|
|
551
|
+
z.object({
|
|
552
|
+
success: z.boolean(),
|
|
553
|
+
timestamp: z.string().datetime(),
|
|
554
|
+
duration: z.number().int().nonnegative().optional(),
|
|
555
|
+
target: z.string(),
|
|
556
|
+
toolsUsed: z.array(ToolSourceSchema),
|
|
557
|
+
issues: z.array(AccessibilityIssueSchema),
|
|
558
|
+
summary: AnalysisSummarySchema.extend({
|
|
559
|
+
byTool: z.record(ToolSourceSchema, z.number().int().nonnegative()).optional()
|
|
560
|
+
}),
|
|
561
|
+
individualResults: z.array(AnalysisResultSchema).optional().describe("Results from each tool before merging"),
|
|
562
|
+
error: z.string().optional()
|
|
563
|
+
}).describe("Combined result from multiple accessibility tools");
|
|
564
|
+
|
|
565
|
+
// src/types/tool-inputs.ts
|
|
566
|
+
var UrlSchema = z.string().url().describe("URL of the page to analyze");
|
|
567
|
+
var HtmlSchema = z.string().min(1).describe("Raw HTML content to analyze");
|
|
568
|
+
var FilePathSchema = z.string().min(1).describe("Path to the file to analyze");
|
|
569
|
+
var ViewportSchema = z.object({
|
|
570
|
+
width: z.number().int().positive().default(1280).describe("Viewport width in pixels"),
|
|
571
|
+
height: z.number().int().positive().default(720).describe("Viewport height in pixels")
|
|
572
|
+
}).describe("Browser viewport dimensions");
|
|
573
|
+
var BrowserOptionsSchema = z.object({
|
|
574
|
+
waitForSelector: z.string().optional().describe("CSS selector to wait for before analysis"),
|
|
575
|
+
waitForTimeout: z.number().int().positive().max(6e4).optional().describe("Time to wait in ms before analysis (max 60s)"),
|
|
576
|
+
viewport: ViewportSchema.optional()
|
|
577
|
+
}).describe("Browser behavior options");
|
|
578
|
+
var AxeToolInputSchema = z.object({
|
|
579
|
+
url: UrlSchema.optional(),
|
|
580
|
+
html: HtmlSchema.optional(),
|
|
581
|
+
options: z.object({
|
|
582
|
+
wcagLevel: WCAGLevelSchema.default("AA").describe("WCAG conformance level to check"),
|
|
583
|
+
rules: z.array(z.string()).optional().describe("Specific axe rule IDs to run"),
|
|
584
|
+
excludeRules: z.array(z.string()).optional().describe("Axe rule IDs to exclude"),
|
|
585
|
+
includeIncomplete: z.boolean().default(false).describe("Include incomplete/needs-review results"),
|
|
586
|
+
selector: z.string().optional().describe("CSS selector to scope analysis to specific element"),
|
|
587
|
+
browser: BrowserOptionsSchema.optional()
|
|
588
|
+
}).optional()
|
|
589
|
+
}).refine((data) => data.url !== void 0 || data.html !== void 0, {
|
|
590
|
+
message: "Either url or html must be provided"
|
|
591
|
+
}).refine((data) => !(data.url !== void 0 && data.html !== void 0), {
|
|
592
|
+
message: "Provide either url or html, not both"
|
|
593
|
+
}).describe("Input for axe-core accessibility analysis");
|
|
594
|
+
var Pa11yToolInputSchema = z.object({
|
|
595
|
+
url: UrlSchema.optional(),
|
|
596
|
+
html: HtmlSchema.optional(),
|
|
597
|
+
options: z.object({
|
|
598
|
+
standard: z.enum(["WCAG2A", "WCAG2AA", "WCAG2AAA", "WCAG21A", "WCAG21AA", "WCAG21AAA"]).default("WCAG21AA").describe("Accessibility standard to test against"),
|
|
599
|
+
includeWarnings: z.boolean().default(true).describe("Include warnings in results"),
|
|
600
|
+
includeNotices: z.boolean().default(false).describe("Include notices in results"),
|
|
601
|
+
rootElement: z.string().optional().describe("CSS selector for root element to test"),
|
|
602
|
+
hideElements: z.string().optional().describe("CSS selector for elements to hide from testing"),
|
|
603
|
+
browser: BrowserOptionsSchema.optional()
|
|
604
|
+
}).optional()
|
|
605
|
+
}).refine((data) => data.url !== void 0 || data.html !== void 0, {
|
|
606
|
+
message: "Either url or html must be provided"
|
|
607
|
+
}).refine((data) => !(data.url !== void 0 && data.html !== void 0), {
|
|
608
|
+
message: "Provide either url or html, not both"
|
|
609
|
+
}).describe("Input for Pa11y accessibility analysis");
|
|
610
|
+
var ESLintA11yToolInputSchema = z.object({
|
|
611
|
+
files: z.array(FilePathSchema).min(1).optional().describe("Array of file paths to lint"),
|
|
612
|
+
directory: z.string().optional().describe("Directory path to lint recursively"),
|
|
613
|
+
code: z.string().optional().describe("Inline Vue component code to lint"),
|
|
614
|
+
options: z.object({
|
|
615
|
+
rules: z.record(
|
|
616
|
+
z.string(),
|
|
617
|
+
z.union([z.enum(["off", "warn", "error"]), z.number().int().min(0).max(2)])
|
|
618
|
+
).optional().describe("Override specific rule configurations"),
|
|
619
|
+
fix: z.literal(false).default(false).describe("Fix mode is disabled - MCP only reports issues")
|
|
620
|
+
}).optional()
|
|
621
|
+
}).refine(
|
|
622
|
+
(data) => data.files !== void 0 || data.directory !== void 0 || data.code !== void 0,
|
|
623
|
+
{ message: "Provide files, directory, or code to lint" }
|
|
624
|
+
).describe("Input for ESLint Vue.js accessibility linting");
|
|
625
|
+
var CombinedAnalysisInputSchema = z.object({
|
|
626
|
+
url: UrlSchema.optional(),
|
|
627
|
+
html: HtmlSchema.optional(),
|
|
628
|
+
tools: z.array(z.enum(["axe-core", "pa11y"])).min(1).default(["axe-core", "pa11y"]).describe("Tools to run for web analysis"),
|
|
629
|
+
options: z.object({
|
|
630
|
+
wcagLevel: WCAGLevelSchema.default("AA"),
|
|
631
|
+
deduplicateResults: z.boolean().default(true).describe("Merge similar issues from different tools"),
|
|
632
|
+
browser: BrowserOptionsSchema.optional()
|
|
633
|
+
}).optional()
|
|
634
|
+
}).refine(
|
|
635
|
+
(data) => data.url !== void 0 || data.html !== void 0,
|
|
636
|
+
{ message: "Provide url or html to analyze" }
|
|
637
|
+
).describe("Input for combined web accessibility analysis (axe-core + Pa11y)");
|
|
638
|
+
|
|
639
|
+
// src/tools/axe.ts
|
|
640
|
+
var sharedAdapter = null;
|
|
641
|
+
function getAdapter() {
|
|
642
|
+
if (!sharedAdapter) {
|
|
643
|
+
sharedAdapter = new AxeAdapter({
|
|
644
|
+
headless: true,
|
|
645
|
+
timeout: 3e4
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
return sharedAdapter;
|
|
649
|
+
}
|
|
650
|
+
async function disposeAdapter() {
|
|
651
|
+
if (sharedAdapter) {
|
|
652
|
+
await sharedAdapter.dispose();
|
|
653
|
+
sharedAdapter = null;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
process.on("SIGINT", () => {
|
|
657
|
+
disposeAdapter().finally(() => process.exit(0));
|
|
658
|
+
});
|
|
659
|
+
process.on("SIGTERM", () => {
|
|
660
|
+
disposeAdapter().finally(() => process.exit(0));
|
|
661
|
+
});
|
|
662
|
+
function buildAnalysisTarget(input) {
|
|
663
|
+
if (input.url) {
|
|
664
|
+
return {
|
|
665
|
+
type: "url",
|
|
666
|
+
value: input.url,
|
|
667
|
+
options: {
|
|
668
|
+
waitForSelector: input.options?.browser?.waitForSelector,
|
|
669
|
+
timeout: input.options?.browser?.waitForTimeout,
|
|
670
|
+
viewport: input.options?.browser?.viewport
|
|
671
|
+
}
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
return {
|
|
675
|
+
type: "html",
|
|
676
|
+
value: input.html,
|
|
677
|
+
options: {
|
|
678
|
+
waitForSelector: input.options?.browser?.waitForSelector,
|
|
679
|
+
timeout: input.options?.browser?.waitForTimeout,
|
|
680
|
+
viewport: input.options?.browser?.viewport
|
|
681
|
+
}
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
function buildAnalysisOptions(input) {
|
|
685
|
+
return {
|
|
686
|
+
wcagLevel: input.options?.wcagLevel ?? "AA",
|
|
687
|
+
rules: input.options?.rules,
|
|
688
|
+
excludeRules: input.options?.excludeRules,
|
|
689
|
+
includeWarnings: input.options?.includeIncomplete ?? false
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
function formatOutput(result) {
|
|
693
|
+
return {
|
|
694
|
+
success: result.success,
|
|
695
|
+
target: result.target,
|
|
696
|
+
issueCount: result.issues.length,
|
|
697
|
+
issues: result.issues,
|
|
698
|
+
summary: result.summary,
|
|
699
|
+
metadata: result.metadata,
|
|
700
|
+
duration: result.duration,
|
|
701
|
+
error: result.error
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
var handleAxeAnalysis = withToolContext(
|
|
705
|
+
"analyze-with-axe",
|
|
706
|
+
async (input, context) => {
|
|
707
|
+
context.logger.debug("Building analysis configuration", {
|
|
708
|
+
hasUrl: !!input.url,
|
|
709
|
+
hasHtml: !!input.html,
|
|
710
|
+
wcagLevel: input.options?.wcagLevel ?? "AA"
|
|
711
|
+
});
|
|
712
|
+
const adapter = getAdapter();
|
|
713
|
+
const isAvailable = await adapter.isAvailable();
|
|
714
|
+
if (!isAvailable) {
|
|
715
|
+
return createErrorResponse(
|
|
716
|
+
new Error("Axe adapter is not available. Browser may have failed to launch.")
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
const target = buildAnalysisTarget(input);
|
|
720
|
+
const options = buildAnalysisOptions(input);
|
|
721
|
+
context.logger.info("Starting axe-core analysis", {
|
|
722
|
+
targetType: target.type,
|
|
723
|
+
target: target.type === "url" ? target.value : "[html content]"
|
|
724
|
+
});
|
|
725
|
+
const result = await adapter.analyze(target, options);
|
|
726
|
+
if (!result.success) {
|
|
727
|
+
context.logger.warn("Analysis completed with errors", {
|
|
728
|
+
error: result.error
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
const output = formatOutput(result);
|
|
732
|
+
return createJsonResponse(output, !result.success);
|
|
733
|
+
}
|
|
734
|
+
);
|
|
735
|
+
var AxeToolMcpInputSchema = z.object({
|
|
736
|
+
url: z.string().url().optional().describe("URL of the page to analyze"),
|
|
737
|
+
html: z.string().min(1).optional().describe("Raw HTML content to analyze"),
|
|
738
|
+
options: z.object({
|
|
739
|
+
wcagLevel: z.enum(["A", "AA", "AAA"]).default("AA").describe("WCAG conformance level to check"),
|
|
740
|
+
rules: z.array(z.string()).optional().describe("Specific axe rule IDs to run"),
|
|
741
|
+
excludeRules: z.array(z.string()).optional().describe("Axe rule IDs to exclude"),
|
|
742
|
+
includeIncomplete: z.boolean().default(false).describe("Include incomplete/needs-review results"),
|
|
743
|
+
selector: z.string().optional().describe("CSS selector to scope analysis"),
|
|
744
|
+
browser: z.object({
|
|
745
|
+
waitForSelector: z.string().optional().describe("CSS selector to wait for"),
|
|
746
|
+
waitForTimeout: z.number().int().positive().max(6e4).optional(),
|
|
747
|
+
viewport: z.object({
|
|
748
|
+
width: z.number().int().positive().default(1280),
|
|
749
|
+
height: z.number().int().positive().default(720)
|
|
750
|
+
}).optional()
|
|
751
|
+
}).optional()
|
|
752
|
+
}).optional()
|
|
753
|
+
});
|
|
754
|
+
var analyzeWithAxeTool = {
|
|
755
|
+
name: "analyze-with-axe",
|
|
756
|
+
description: `Analyze a web page or HTML content for accessibility issues using axe-core.
|
|
757
|
+
|
|
758
|
+
Returns accessibility violations and incomplete checks based on WCAG guidelines.
|
|
759
|
+
|
|
760
|
+
Input options
|
|
761
|
+
- url: URL of the page to analyze
|
|
762
|
+
- html: Raw HTML content to analyze (alternative to url)
|
|
763
|
+
- options.wcagLevel: WCAG level to check (A, AA, or AAA). Default: AA
|
|
764
|
+
- options.rules: Specific axe rule IDs to run
|
|
765
|
+
- options.excludeRules: Axe rule IDs to exclude
|
|
766
|
+
- options.includeIncomplete: Include needs-review results. Default: false
|
|
767
|
+
- options.browser.waitForSelector: CSS selector to wait for before analysis
|
|
768
|
+
- options.browser.viewport: Browser viewport dimensions
|
|
769
|
+
|
|
770
|
+
Output
|
|
771
|
+
- issues: Array of accessibility issues found
|
|
772
|
+
- summary: Issue counts by severity and WCAG principle
|
|
773
|
+
- metadata: Tool version and browser info`,
|
|
774
|
+
register(server2) {
|
|
775
|
+
server2.tool(
|
|
776
|
+
this.name,
|
|
777
|
+
this.description,
|
|
778
|
+
AxeToolMcpInputSchema.shape,
|
|
779
|
+
async (input) => {
|
|
780
|
+
const parseResult = AxeToolInputSchema.safeParse(input);
|
|
781
|
+
if (!parseResult.success) {
|
|
782
|
+
const errors = parseResult.error.errors.map((e) => `${e.path.join(".")}: ${e.message}`).join("; ");
|
|
783
|
+
const response2 = createErrorResponse(new Error(`Invalid input: ${errors}`));
|
|
784
|
+
return { content: response2.content };
|
|
785
|
+
}
|
|
786
|
+
const response = await handleAxeAnalysis(parseResult.data);
|
|
787
|
+
return { content: response.content };
|
|
788
|
+
}
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
// src/data/wcag-criteria.json
|
|
794
|
+
var wcag_criteria_default = {
|
|
795
|
+
"1.1.1": {
|
|
796
|
+
criterion: "1.1.1",
|
|
797
|
+
level: "A",
|
|
798
|
+
principle: "perceivable",
|
|
799
|
+
title: "Contenido no textual",
|
|
800
|
+
description: "Todo contenido no textual debe tener una alternativa de texto que cumpla el mismo prop\xF3sito.",
|
|
801
|
+
userImpact: {
|
|
802
|
+
affectedUsers: ["screen-reader", "low-vision"],
|
|
803
|
+
impactDescription: "Los usuarios de lectores de pantalla no pueden acceder a la informaci\xF3n transmitida por im\xE1genes, iconos o gr\xE1ficos sin texto alternativo.",
|
|
804
|
+
realWorldExample: 'Un usuario ciego usando JAWS escuchar\xE1 "imagen" sin descripci\xF3n, perdiendo completamente el contexto.'
|
|
805
|
+
},
|
|
806
|
+
remediation: {
|
|
807
|
+
effort: "low",
|
|
808
|
+
priority: "critical",
|
|
809
|
+
commonSolutions: [
|
|
810
|
+
"A\xF1adir atributo alt descriptivo a im\xE1genes",
|
|
811
|
+
"Usar aria-label para iconos decorativos con funci\xF3n",
|
|
812
|
+
'Marcar im\xE1genes decorativas con alt="" vac\xEDo',
|
|
813
|
+
"Proporcionar transcripciones para audio/video"
|
|
814
|
+
]
|
|
815
|
+
},
|
|
816
|
+
wcagUrl: "https://www.w3.org/WAI/WCAG21/Understanding/non-text-content.html"
|
|
817
|
+
},
|
|
818
|
+
"1.2.2": {
|
|
819
|
+
criterion: "1.2.2",
|
|
820
|
+
level: "A",
|
|
821
|
+
principle: "perceivable",
|
|
822
|
+
title: "Subt\xEDtulos (pregrabados)",
|
|
823
|
+
description: "Los medios sincronizados pregrabados deben tener subt\xEDtulos.",
|
|
824
|
+
userImpact: {
|
|
825
|
+
affectedUsers: ["screen-reader"],
|
|
826
|
+
impactDescription: "Usuarios sordos o con problemas de audici\xF3n no pueden acceder al contenido de audio.",
|
|
827
|
+
realWorldExample: "Una persona sorda no puede seguir un tutorial en video sin subt\xEDtulos."
|
|
828
|
+
},
|
|
829
|
+
remediation: {
|
|
830
|
+
effort: "medium",
|
|
831
|
+
priority: "high",
|
|
832
|
+
commonSolutions: [
|
|
833
|
+
"A\xF1adir track de subt\xEDtulos WebVTT",
|
|
834
|
+
"Usar servicios de transcripci\xF3n autom\xE1tica como punto de partida",
|
|
835
|
+
"Revisar y corregir subt\xEDtulos generados autom\xE1ticamente"
|
|
836
|
+
]
|
|
837
|
+
},
|
|
838
|
+
wcagUrl: "https://www.w3.org/WAI/WCAG21/Understanding/captions-prerecorded.html"
|
|
839
|
+
},
|
|
840
|
+
"1.3.1": {
|
|
841
|
+
criterion: "1.3.1",
|
|
842
|
+
level: "A",
|
|
843
|
+
principle: "perceivable",
|
|
844
|
+
title: "Informaci\xF3n y relaciones",
|
|
845
|
+
description: "La informaci\xF3n, estructura y relaciones transmitidas visualmente pueden ser determinadas program\xE1ticamente.",
|
|
846
|
+
userImpact: {
|
|
847
|
+
affectedUsers: ["screen-reader", "cognitive"],
|
|
848
|
+
impactDescription: "Sin estructura sem\xE1ntica correcta, los usuarios no pueden navegar eficientemente ni entender las relaciones entre contenidos.",
|
|
849
|
+
realWorldExample: "Un usuario con lector de pantalla no puede saltar entre encabezados si se usan divs con estilos en lugar de elementos h1-h6."
|
|
850
|
+
},
|
|
851
|
+
remediation: {
|
|
852
|
+
effort: "medium",
|
|
853
|
+
priority: "high",
|
|
854
|
+
commonSolutions: [
|
|
855
|
+
"Usar elementos HTML sem\xE1nticos (nav, main, article, aside)",
|
|
856
|
+
"Usar headings (h1-h6) en orden jer\xE1rquico",
|
|
857
|
+
"Asociar labels con inputs correctamente",
|
|
858
|
+
"Usar listas (ul, ol) para contenido de lista",
|
|
859
|
+
"Usar tablas con th y scope para datos tabulares"
|
|
860
|
+
]
|
|
861
|
+
},
|
|
862
|
+
wcagUrl: "https://www.w3.org/WAI/WCAG21/Understanding/info-and-relationships.html"
|
|
863
|
+
},
|
|
864
|
+
"1.4.3": {
|
|
865
|
+
criterion: "1.4.3",
|
|
866
|
+
level: "AA",
|
|
867
|
+
principle: "perceivable",
|
|
868
|
+
title: "Contraste m\xEDnimo",
|
|
869
|
+
description: "El texto debe tener una relaci\xF3n de contraste de al menos 4.5:1 (3:1 para texto grande).",
|
|
870
|
+
userImpact: {
|
|
871
|
+
affectedUsers: ["low-vision", "color-blind"],
|
|
872
|
+
impactDescription: "Texto con bajo contraste es dif\xEDcil o imposible de leer para personas con baja visi\xF3n o daltonismo.",
|
|
873
|
+
realWorldExample: "Un usuario con cataratas no puede leer texto gris claro sobre fondo blanco en un formulario."
|
|
874
|
+
},
|
|
875
|
+
remediation: {
|
|
876
|
+
effort: "low",
|
|
877
|
+
priority: "high",
|
|
878
|
+
commonSolutions: [
|
|
879
|
+
"Ajustar colores para cumplir ratio 4.5:1 m\xEDnimo",
|
|
880
|
+
"Usar herramientas como WebAIM Contrast Checker",
|
|
881
|
+
"Evitar depender solo del color para transmitir informaci\xF3n",
|
|
882
|
+
"Probar con simuladores de daltonismo"
|
|
883
|
+
]
|
|
884
|
+
},
|
|
885
|
+
wcagUrl: "https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html"
|
|
886
|
+
},
|
|
887
|
+
"2.1.1": {
|
|
888
|
+
criterion: "2.1.1",
|
|
889
|
+
level: "A",
|
|
890
|
+
principle: "operable",
|
|
891
|
+
title: "Teclado",
|
|
892
|
+
description: "Toda funcionalidad debe ser operable mediante teclado.",
|
|
893
|
+
userImpact: {
|
|
894
|
+
affectedUsers: ["keyboard-only", "motor-impaired", "screen-reader"],
|
|
895
|
+
impactDescription: "Usuarios que no pueden usar mouse quedan completamente bloqueados de funcionalidades solo accesibles con eventos de rat\xF3n.",
|
|
896
|
+
realWorldExample: "Un usuario con par\xE1lisis cerebral no puede usar un dropdown que solo funciona con hover del mouse."
|
|
897
|
+
},
|
|
898
|
+
remediation: {
|
|
899
|
+
effort: "medium",
|
|
900
|
+
priority: "critical",
|
|
901
|
+
commonSolutions: [
|
|
902
|
+
"A\xF1adir manejadores de eventos de teclado (onKeyDown, onKeyPress)",
|
|
903
|
+
"Usar elementos interactivos nativos (button, a, input)",
|
|
904
|
+
"Asegurar que custom components sean focusables (tabindex)",
|
|
905
|
+
"Implementar navegaci\xF3n con Enter/Space en controles personalizados"
|
|
906
|
+
]
|
|
907
|
+
},
|
|
908
|
+
wcagUrl: "https://www.w3.org/WAI/WCAG21/Understanding/keyboard.html"
|
|
909
|
+
},
|
|
910
|
+
"2.4.1": {
|
|
911
|
+
criterion: "2.4.1",
|
|
912
|
+
level: "A",
|
|
913
|
+
principle: "operable",
|
|
914
|
+
title: "Saltar bloques",
|
|
915
|
+
description: "Debe existir un mecanismo para saltar bloques de contenido repetidos.",
|
|
916
|
+
userImpact: {
|
|
917
|
+
affectedUsers: ["keyboard-only", "screen-reader"],
|
|
918
|
+
impactDescription: "Usuarios deben tabular por toda la navegaci\xF3n en cada p\xE1gina, perdiendo tiempo y paciencia.",
|
|
919
|
+
realWorldExample: "Un usuario ciego debe escuchar 30 links de navegaci\xF3n antes de llegar al contenido principal en cada p\xE1gina."
|
|
920
|
+
},
|
|
921
|
+
remediation: {
|
|
922
|
+
effort: "low",
|
|
923
|
+
priority: "medium",
|
|
924
|
+
commonSolutions: [
|
|
925
|
+
'A\xF1adir link "Saltar al contenido principal" al inicio',
|
|
926
|
+
"Usar landmarks ARIA correctamente (main, nav)",
|
|
927
|
+
"Implementar skip links visibles al recibir foco"
|
|
928
|
+
]
|
|
929
|
+
},
|
|
930
|
+
wcagUrl: "https://www.w3.org/WAI/WCAG21/Understanding/bypass-blocks.html"
|
|
931
|
+
},
|
|
932
|
+
"2.4.3": {
|
|
933
|
+
criterion: "2.4.3",
|
|
934
|
+
level: "A",
|
|
935
|
+
principle: "operable",
|
|
936
|
+
title: "Orden del foco",
|
|
937
|
+
description: "El orden de foco debe ser l\xF3gico y predecible.",
|
|
938
|
+
userImpact: {
|
|
939
|
+
affectedUsers: ["keyboard-only", "screen-reader", "cognitive"],
|
|
940
|
+
impactDescription: "Orden de foco il\xF3gico confunde a usuarios y dificulta la navegaci\xF3n.",
|
|
941
|
+
realWorldExample: "El foco salta de un bot\xF3n a otro en la esquina opuesta, confundiendo al usuario sobre qu\xE9 elemento est\xE1 activo."
|
|
942
|
+
},
|
|
943
|
+
remediation: {
|
|
944
|
+
effort: "medium",
|
|
945
|
+
priority: "medium",
|
|
946
|
+
commonSolutions: [
|
|
947
|
+
"Evitar tabindex positivos (> 0)",
|
|
948
|
+
"Mantener DOM order que refleje orden visual",
|
|
949
|
+
"Usar CSS para reordenar visualmente, no tabindex",
|
|
950
|
+
"Probar navegaci\xF3n con Tab manualmente"
|
|
951
|
+
]
|
|
952
|
+
},
|
|
953
|
+
wcagUrl: "https://www.w3.org/WAI/WCAG21/Understanding/focus-order.html"
|
|
954
|
+
},
|
|
955
|
+
"2.4.4": {
|
|
956
|
+
criterion: "2.4.4",
|
|
957
|
+
level: "A",
|
|
958
|
+
principle: "operable",
|
|
959
|
+
title: "Prop\xF3sito de los enlaces",
|
|
960
|
+
description: "El prop\xF3sito de cada enlace debe poder determinarse por el texto del enlace solo.",
|
|
961
|
+
userImpact: {
|
|
962
|
+
affectedUsers: ["screen-reader", "cognitive"],
|
|
963
|
+
impactDescription: 'Enlaces gen\xE9ricos como "click aqu\xED" no comunican destino o prop\xF3sito.',
|
|
964
|
+
realWorldExample: 'Un usuario con lector de pantalla navegando por lista de enlaces escucha 10 veces "m\xE1s informaci\xF3n" sin contexto.'
|
|
965
|
+
},
|
|
966
|
+
remediation: {
|
|
967
|
+
effort: "low",
|
|
968
|
+
priority: "medium",
|
|
969
|
+
commonSolutions: [
|
|
970
|
+
'Usar texto de enlace descriptivo ("Descargar informe anual 2023")',
|
|
971
|
+
'Evitar "click aqu\xED", "m\xE1s informaci\xF3n", "leer m\xE1s"',
|
|
972
|
+
"A\xF1adir aria-label con contexto si el texto visible debe ser corto",
|
|
973
|
+
"Incluir el contexto en el texto del enlace"
|
|
974
|
+
]
|
|
975
|
+
},
|
|
976
|
+
wcagUrl: "https://www.w3.org/WAI/WCAG21/Understanding/link-purpose-in-context.html"
|
|
977
|
+
},
|
|
978
|
+
"3.2.2": {
|
|
979
|
+
criterion: "3.2.2",
|
|
980
|
+
level: "A",
|
|
981
|
+
principle: "understandable",
|
|
982
|
+
title: "Al recibir entradas",
|
|
983
|
+
description: "Cambiar el valor de un control no debe causar cambios de contexo inesperados.",
|
|
984
|
+
userImpact: {
|
|
985
|
+
affectedUsers: ["cognitive", "screen-reader"],
|
|
986
|
+
impactDescription: "Cambios autom\xE1ticos desorientan y pueden causar p\xE9rdida de contexto.",
|
|
987
|
+
realWorldExample: "Un select que auto-env\xEDa el formulario al cambiar sorprende al usuario que quer\xEDa explorar opciones."
|
|
988
|
+
},
|
|
989
|
+
remediation: {
|
|
990
|
+
effort: "low",
|
|
991
|
+
priority: "medium",
|
|
992
|
+
commonSolutions: [
|
|
993
|
+
"Requerir acci\xF3n expl\xEDcita (bot\xF3n) para enviar",
|
|
994
|
+
"Advertir al usuario si habr\xE1 cambio autom\xE1tico",
|
|
995
|
+
"Usar onChange solo para actualizar UI, no navegaci\xF3n",
|
|
996
|
+
'Proporcionar bot\xF3n "Aplicar" en lugar de auto-submit'
|
|
997
|
+
]
|
|
998
|
+
},
|
|
999
|
+
wcagUrl: "https://www.w3.org/WAI/WCAG21/Understanding/on-input.html"
|
|
1000
|
+
},
|
|
1001
|
+
"4.1.2": {
|
|
1002
|
+
criterion: "4.1.2",
|
|
1003
|
+
level: "A",
|
|
1004
|
+
principle: "robust",
|
|
1005
|
+
title: "Nombre, funci\xF3n, valor",
|
|
1006
|
+
description: "Componentes de interfaz deben exponer nombre, funci\xF3n, estados y propiedades program\xE1ticamente.",
|
|
1007
|
+
userImpact: {
|
|
1008
|
+
affectedUsers: ["screen-reader"],
|
|
1009
|
+
impactDescription: "Controles personalizados sin sem\xE1ntica ARIA son invisibles o incomprensibles para tecnolog\xEDas asistivas.",
|
|
1010
|
+
realWorldExample: "Un checkbox custom hecho con div no anuncia su estado (marcado/desmarcado) al lector de pantalla."
|
|
1011
|
+
},
|
|
1012
|
+
remediation: {
|
|
1013
|
+
effort: "high",
|
|
1014
|
+
priority: "critical",
|
|
1015
|
+
commonSolutions: [
|
|
1016
|
+
"Usar elementos HTML nativos cuando sea posible",
|
|
1017
|
+
'A\xF1adir roles ARIA apropiados (role="button", "checkbox", etc.)',
|
|
1018
|
+
"Implementar estados ARIA (aria-checked, aria-expanded)",
|
|
1019
|
+
"Incluir aria-label o aria-labelledby para nombre accesible",
|
|
1020
|
+
"Seguir patrones de dise\xF1o WAI-ARIA"
|
|
1021
|
+
]
|
|
1022
|
+
},
|
|
1023
|
+
wcagUrl: "https://www.w3.org/WAI/WCAG21/Understanding/name-role-value.html"
|
|
1024
|
+
}
|
|
1025
|
+
};
|
|
1026
|
+
|
|
1027
|
+
// src/utils/wcag-context.ts
|
|
1028
|
+
var WCAG_CRITERIA = wcag_criteria_default;
|
|
1029
|
+
function getWCAGContext(criterion) {
|
|
1030
|
+
return WCAG_CRITERIA[criterion];
|
|
1031
|
+
}
|
|
1032
|
+
function enrichIssueWithContext(_issue, context) {
|
|
1033
|
+
const humanContext = `
|
|
1034
|
+
**${context.title} (WCAG ${context.criterion} - Nivel ${context.level})**
|
|
1035
|
+
|
|
1036
|
+
${context.description}
|
|
1037
|
+
|
|
1038
|
+
**Impacto en usuarios:**
|
|
1039
|
+
${context.userImpact.impactDescription}
|
|
1040
|
+
|
|
1041
|
+
**Ejemplo real:**
|
|
1042
|
+
${context.userImpact.realWorldExample}
|
|
1043
|
+
|
|
1044
|
+
**Esfuerzo de correcci\xF3n:** ${context.remediation.effort}
|
|
1045
|
+
**Prioridad:** ${context.remediation.priority}
|
|
1046
|
+
`.trim();
|
|
1047
|
+
return {
|
|
1048
|
+
humanContext,
|
|
1049
|
+
suggestedActions: context.remediation.commonSolutions,
|
|
1050
|
+
affectedUsers: context.userImpact.affectedUsers,
|
|
1051
|
+
priority: context.remediation.priority,
|
|
1052
|
+
remediationEffort: context.remediation.effort
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// src/normalizers/base.ts
|
|
1057
|
+
var BaseNormalizer = class {
|
|
1058
|
+
generateIssueId(tool, ruleId, selector) {
|
|
1059
|
+
const base = `${tool}:${ruleId}`;
|
|
1060
|
+
if (selector) {
|
|
1061
|
+
const selectorHash = this.hashString(selector).toString(16);
|
|
1062
|
+
return `${base}:${selectorHash}`;
|
|
1063
|
+
}
|
|
1064
|
+
return base;
|
|
1065
|
+
}
|
|
1066
|
+
mapImpactToSeverity(impact) {
|
|
1067
|
+
const mapping = {
|
|
1068
|
+
critical: "critical",
|
|
1069
|
+
serious: "serious",
|
|
1070
|
+
moderate: "moderate",
|
|
1071
|
+
minor: "minor"
|
|
1072
|
+
};
|
|
1073
|
+
return mapping[impact ?? ""] ?? "moderate";
|
|
1074
|
+
}
|
|
1075
|
+
enrichWithHumanContext(issue) {
|
|
1076
|
+
if (!issue.wcag?.criterion) {
|
|
1077
|
+
return issue;
|
|
1078
|
+
}
|
|
1079
|
+
const context = getWCAGContext(issue.wcag.criterion);
|
|
1080
|
+
if (!context) {
|
|
1081
|
+
return issue;
|
|
1082
|
+
}
|
|
1083
|
+
const enrichment = enrichIssueWithContext(issue, context);
|
|
1084
|
+
return {
|
|
1085
|
+
...issue,
|
|
1086
|
+
humanContext: enrichment.humanContext,
|
|
1087
|
+
suggestedActions: enrichment.suggestedActions,
|
|
1088
|
+
affectedUsers: enrichment.affectedUsers,
|
|
1089
|
+
priority: enrichment.priority,
|
|
1090
|
+
remediationEffort: enrichment.remediationEffort
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
hashString(str) {
|
|
1094
|
+
let hash = 0;
|
|
1095
|
+
for (let i = 0; i < str.length; i++) {
|
|
1096
|
+
const char = str.charCodeAt(i);
|
|
1097
|
+
hash = (hash << 5) - hash + char;
|
|
1098
|
+
hash = hash & hash;
|
|
1099
|
+
}
|
|
1100
|
+
return Math.abs(hash);
|
|
1101
|
+
}
|
|
1102
|
+
};
|
|
1103
|
+
|
|
1104
|
+
// src/normalizers/pa11y.ts
|
|
1105
|
+
var WCAG_PATTERN = /WCAG2(A{1,3})\.(Principle\d)\.Guideline\d+_\d+\.(\d+_\d+_\d+)/;
|
|
1106
|
+
var Pa11yNormalizer = class extends BaseNormalizer {
|
|
1107
|
+
normalize(issues, context) {
|
|
1108
|
+
return issues.map((issue) => this.normalizeIssue(issue, context));
|
|
1109
|
+
}
|
|
1110
|
+
normalizeIssue(issue, context) {
|
|
1111
|
+
const wcagInfo = this.parseWcagCode(issue.code);
|
|
1112
|
+
const baseIssue = {
|
|
1113
|
+
id: this.generateIssueId("pa11y", issue.code, issue.selector),
|
|
1114
|
+
ruleId: issue.code,
|
|
1115
|
+
tool: "pa11y",
|
|
1116
|
+
severity: this.mapTypeToSeverity(issue.type),
|
|
1117
|
+
wcag: wcagInfo,
|
|
1118
|
+
location: {
|
|
1119
|
+
selector: issue.selector,
|
|
1120
|
+
snippet: issue.context,
|
|
1121
|
+
file: context.targetFile
|
|
1122
|
+
},
|
|
1123
|
+
message: issue.message,
|
|
1124
|
+
confidence: issue.type === "error" ? 1 : 0.8
|
|
1125
|
+
};
|
|
1126
|
+
return this.enrichWithHumanContext(baseIssue);
|
|
1127
|
+
}
|
|
1128
|
+
mapTypeToSeverity(type) {
|
|
1129
|
+
const mapping = {
|
|
1130
|
+
error: "serious",
|
|
1131
|
+
warning: "moderate",
|
|
1132
|
+
notice: "minor"
|
|
1133
|
+
};
|
|
1134
|
+
return mapping[type] ?? "moderate";
|
|
1135
|
+
}
|
|
1136
|
+
parseWcagCode(code) {
|
|
1137
|
+
const match = WCAG_PATTERN.exec(code);
|
|
1138
|
+
if (!match) return void 0;
|
|
1139
|
+
const [, levelStr, principleStr, criterionRaw] = match;
|
|
1140
|
+
if (!levelStr || !principleStr || !criterionRaw) return void 0;
|
|
1141
|
+
const level = levelStr;
|
|
1142
|
+
const criterion = criterionRaw.replace(/_/g, ".");
|
|
1143
|
+
const principleMap = {
|
|
1144
|
+
Principle1: "perceivable",
|
|
1145
|
+
Principle2: "operable",
|
|
1146
|
+
Principle3: "understandable",
|
|
1147
|
+
Principle4: "robust"
|
|
1148
|
+
};
|
|
1149
|
+
const principle = principleMap[principleStr];
|
|
1150
|
+
if (!principle) return void 0;
|
|
1151
|
+
return {
|
|
1152
|
+
criterion,
|
|
1153
|
+
level,
|
|
1154
|
+
principle,
|
|
1155
|
+
version: "2.1"
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
};
|
|
1159
|
+
var pa11yNormalizer = new Pa11yNormalizer();
|
|
1160
|
+
|
|
1161
|
+
// src/adapters/pa11y.ts
|
|
1162
|
+
var Pa11yAdapter = class extends BaseAdapter {
|
|
1163
|
+
name = "pa11y";
|
|
1164
|
+
version = "9.0.1";
|
|
1165
|
+
adapterConfig;
|
|
1166
|
+
constructor(config = {}) {
|
|
1167
|
+
super(config);
|
|
1168
|
+
this.adapterConfig = config;
|
|
1169
|
+
}
|
|
1170
|
+
async analyze(target, options) {
|
|
1171
|
+
const startTime = Date.now();
|
|
1172
|
+
const targetValue = target.value;
|
|
1173
|
+
this.logger.info("Starting Pa11y analysis", { target: targetValue });
|
|
1174
|
+
try {
|
|
1175
|
+
const pa11yOptions = this.buildPa11yOptions(target, options);
|
|
1176
|
+
const results = await this.runPa11y(target, pa11yOptions);
|
|
1177
|
+
const issues = pa11yNormalizer.normalize(results.issues, {
|
|
1178
|
+
tool: "pa11y",
|
|
1179
|
+
targetUrl: target.type === "url" ? targetValue : void 0
|
|
1180
|
+
});
|
|
1181
|
+
const duration = Date.now() - startTime;
|
|
1182
|
+
this.logger.info("Pa11y analysis completed", {
|
|
1183
|
+
target: targetValue,
|
|
1184
|
+
issueCount: issues.length,
|
|
1185
|
+
durationMs: duration
|
|
1186
|
+
});
|
|
1187
|
+
return {
|
|
1188
|
+
success: true,
|
|
1189
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1190
|
+
duration,
|
|
1191
|
+
target: targetValue,
|
|
1192
|
+
tool: "pa11y",
|
|
1193
|
+
issues,
|
|
1194
|
+
summary: this.buildSummary(issues),
|
|
1195
|
+
metadata: {
|
|
1196
|
+
toolVersion: this.version,
|
|
1197
|
+
pageTitle: results.documentTitle
|
|
1198
|
+
}
|
|
1199
|
+
};
|
|
1200
|
+
} catch (error) {
|
|
1201
|
+
const duration = Date.now() - startTime;
|
|
1202
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1203
|
+
this.logger.error("Pa11y analysis failed", {
|
|
1204
|
+
target: targetValue,
|
|
1205
|
+
error: error instanceof Error ? error : new Error(errorMessage)
|
|
1206
|
+
});
|
|
1207
|
+
return {
|
|
1208
|
+
success: false,
|
|
1209
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1210
|
+
duration,
|
|
1211
|
+
target: targetValue,
|
|
1212
|
+
tool: "pa11y",
|
|
1213
|
+
issues: [],
|
|
1214
|
+
summary: this.buildSummary([]),
|
|
1215
|
+
error: errorMessage
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
async isAvailable() {
|
|
1220
|
+
try {
|
|
1221
|
+
return typeof pa11y === "function";
|
|
1222
|
+
} catch {
|
|
1223
|
+
return false;
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
buildPa11yOptions(target, options) {
|
|
1227
|
+
const pa11yOpts = {
|
|
1228
|
+
timeout: this.config.timeout,
|
|
1229
|
+
wait: 0,
|
|
1230
|
+
standard: this.mapWcagLevel(options?.wcagLevel ?? "AA"),
|
|
1231
|
+
includeWarnings: options?.includeWarnings ?? true,
|
|
1232
|
+
includeNotices: false
|
|
1233
|
+
};
|
|
1234
|
+
if (target.options?.viewport) {
|
|
1235
|
+
pa11yOpts.viewport = {
|
|
1236
|
+
width: target.options.viewport.width,
|
|
1237
|
+
height: target.options.viewport.height
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
if (target.options?.waitForSelector) {
|
|
1241
|
+
pa11yOpts.wait = 1e3;
|
|
1242
|
+
}
|
|
1243
|
+
if (target.options?.timeout) {
|
|
1244
|
+
pa11yOpts.wait = target.options.timeout;
|
|
1245
|
+
}
|
|
1246
|
+
if (this.adapterConfig.chromeLaunchConfig) {
|
|
1247
|
+
pa11yOpts.chromeLaunchConfig = this.adapterConfig.chromeLaunchConfig;
|
|
1248
|
+
}
|
|
1249
|
+
if (options?.rules && options.rules.length > 0) {
|
|
1250
|
+
pa11yOpts.rules = options.rules;
|
|
1251
|
+
}
|
|
1252
|
+
if (options?.excludeRules && options.excludeRules.length > 0) {
|
|
1253
|
+
pa11yOpts.ignore = options.excludeRules;
|
|
1254
|
+
}
|
|
1255
|
+
return pa11yOpts;
|
|
1256
|
+
}
|
|
1257
|
+
async runPa11y(target, options) {
|
|
1258
|
+
if (target.type === "url") {
|
|
1259
|
+
return pa11y(target.value, options);
|
|
1260
|
+
}
|
|
1261
|
+
if (target.type === "html") {
|
|
1262
|
+
const htmlContent = target.value;
|
|
1263
|
+
const dataUrl = `data:text/html;charset=utf-8,${encodeURIComponent(htmlContent)}`;
|
|
1264
|
+
return pa11y(dataUrl, options);
|
|
1265
|
+
}
|
|
1266
|
+
throw new Error(`Unsupported target type: ${target.type}`);
|
|
1267
|
+
}
|
|
1268
|
+
mapWcagLevel(level) {
|
|
1269
|
+
const mapping = {
|
|
1270
|
+
A: "WCAG2A",
|
|
1271
|
+
AA: "WCAG2AA",
|
|
1272
|
+
AAA: "WCAG2AAA"
|
|
1273
|
+
};
|
|
1274
|
+
return mapping[level];
|
|
1275
|
+
}
|
|
1276
|
+
buildSummary(issues) {
|
|
1277
|
+
const bySeverity = {
|
|
1278
|
+
critical: 0,
|
|
1279
|
+
serious: 0,
|
|
1280
|
+
moderate: 0,
|
|
1281
|
+
minor: 0
|
|
1282
|
+
};
|
|
1283
|
+
const byPrinciple = {
|
|
1284
|
+
perceivable: 0,
|
|
1285
|
+
operable: 0,
|
|
1286
|
+
understandable: 0,
|
|
1287
|
+
robust: 0
|
|
1288
|
+
};
|
|
1289
|
+
const byRule = {};
|
|
1290
|
+
for (const issue of issues) {
|
|
1291
|
+
bySeverity[issue.severity]++;
|
|
1292
|
+
if (issue.wcag?.principle) {
|
|
1293
|
+
byPrinciple[issue.wcag.principle]++;
|
|
1294
|
+
}
|
|
1295
|
+
byRule[issue.ruleId] = (byRule[issue.ruleId] ?? 0) + 1;
|
|
1296
|
+
}
|
|
1297
|
+
return {
|
|
1298
|
+
total: issues.length,
|
|
1299
|
+
bySeverity,
|
|
1300
|
+
byPrinciple,
|
|
1301
|
+
byRule
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
};
|
|
1305
|
+
|
|
1306
|
+
// src/tools/pa11y.ts
|
|
1307
|
+
var sharedAdapter2 = null;
|
|
1308
|
+
function getAdapter2() {
|
|
1309
|
+
if (!sharedAdapter2) {
|
|
1310
|
+
sharedAdapter2 = new Pa11yAdapter({
|
|
1311
|
+
timeout: 3e4
|
|
1312
|
+
});
|
|
1313
|
+
}
|
|
1314
|
+
return sharedAdapter2;
|
|
1315
|
+
}
|
|
1316
|
+
async function disposeAdapter2() {
|
|
1317
|
+
if (sharedAdapter2) {
|
|
1318
|
+
await sharedAdapter2.dispose();
|
|
1319
|
+
sharedAdapter2 = null;
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
process.on("SIGINT", () => {
|
|
1323
|
+
disposeAdapter2().finally(() => process.exit(0));
|
|
1324
|
+
});
|
|
1325
|
+
process.on("SIGTERM", () => {
|
|
1326
|
+
disposeAdapter2().finally(() => process.exit(0));
|
|
1327
|
+
});
|
|
1328
|
+
function buildAnalysisTarget2(input) {
|
|
1329
|
+
if (input.url) {
|
|
1330
|
+
return {
|
|
1331
|
+
type: "url",
|
|
1332
|
+
value: input.url,
|
|
1333
|
+
options: {
|
|
1334
|
+
waitForSelector: input.options?.browser?.waitForSelector,
|
|
1335
|
+
timeout: input.options?.browser?.waitForTimeout,
|
|
1336
|
+
viewport: input.options?.browser?.viewport
|
|
1337
|
+
}
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
1340
|
+
return {
|
|
1341
|
+
type: "html",
|
|
1342
|
+
value: input.html,
|
|
1343
|
+
options: {
|
|
1344
|
+
waitForSelector: input.options?.browser?.waitForSelector,
|
|
1345
|
+
timeout: input.options?.browser?.waitForTimeout,
|
|
1346
|
+
viewport: input.options?.browser?.viewport
|
|
1347
|
+
}
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1350
|
+
function buildAnalysisOptions2(input) {
|
|
1351
|
+
const standardMap = {
|
|
1352
|
+
"WCAG2A": "A",
|
|
1353
|
+
"WCAG2AA": "AA",
|
|
1354
|
+
"WCAG2AAA": "AAA",
|
|
1355
|
+
"WCAG21A": "A",
|
|
1356
|
+
"WCAG21AA": "AA",
|
|
1357
|
+
"WCAG21AAA": "AAA"
|
|
1358
|
+
};
|
|
1359
|
+
const wcagLevel = input.options?.standard ? standardMap[input.options.standard] ?? "AA" : "AA";
|
|
1360
|
+
return {
|
|
1361
|
+
wcagLevel,
|
|
1362
|
+
includeWarnings: input.options?.includeWarnings ?? true
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
function formatOutput2(result) {
|
|
1366
|
+
return {
|
|
1367
|
+
success: result.success,
|
|
1368
|
+
target: result.target,
|
|
1369
|
+
issueCount: result.issues.length,
|
|
1370
|
+
issues: result.issues,
|
|
1371
|
+
summary: result.summary,
|
|
1372
|
+
metadata: result.metadata,
|
|
1373
|
+
duration: result.duration,
|
|
1374
|
+
error: result.error
|
|
1375
|
+
};
|
|
1376
|
+
}
|
|
1377
|
+
var handlePa11yAnalysis = withToolContext(
|
|
1378
|
+
"analyze-with-pa11y",
|
|
1379
|
+
async (input, context) => {
|
|
1380
|
+
context.logger.debug("Building analysis configuration", {
|
|
1381
|
+
hasUrl: !!input.url,
|
|
1382
|
+
hasHtml: !!input.html,
|
|
1383
|
+
standard: input.options?.standard ?? "WCAG21AA"
|
|
1384
|
+
});
|
|
1385
|
+
const adapter = getAdapter2();
|
|
1386
|
+
const isAvailable = await adapter.isAvailable();
|
|
1387
|
+
if (!isAvailable) {
|
|
1388
|
+
return createErrorResponse(
|
|
1389
|
+
new Error("Pa11y adapter is not available. Browser may have failed to launch.")
|
|
1390
|
+
);
|
|
1391
|
+
}
|
|
1392
|
+
const target = buildAnalysisTarget2(input);
|
|
1393
|
+
const options = buildAnalysisOptions2(input);
|
|
1394
|
+
context.logger.info("Starting Pa11y analysis", {
|
|
1395
|
+
targetType: target.type,
|
|
1396
|
+
target: target.type === "url" ? target.value : "[html content]"
|
|
1397
|
+
});
|
|
1398
|
+
const result = await adapter.analyze(target, options);
|
|
1399
|
+
if (!result.success) {
|
|
1400
|
+
context.logger.warn("Analysis completed with errors", {
|
|
1401
|
+
error: result.error
|
|
1402
|
+
});
|
|
1403
|
+
}
|
|
1404
|
+
const output = formatOutput2(result);
|
|
1405
|
+
return createJsonResponse(output, !result.success);
|
|
1406
|
+
}
|
|
1407
|
+
);
|
|
1408
|
+
var Pa11yToolMcpInputSchema = z.object({
|
|
1409
|
+
url: z.string().url().optional().describe("URL of the page to analyze"),
|
|
1410
|
+
html: z.string().min(1).optional().describe("Raw HTML content to analyze"),
|
|
1411
|
+
options: z.object({
|
|
1412
|
+
standard: z.enum(["WCAG2A", "WCAG2AA", "WCAG2AAA", "WCAG21A", "WCAG21AA", "WCAG21AAA"]).default("WCAG21AA").describe("Accessibility standard to test against"),
|
|
1413
|
+
includeWarnings: z.boolean().default(true).describe("Include warnings in results"),
|
|
1414
|
+
includeNotices: z.boolean().default(false).describe("Include notices in results"),
|
|
1415
|
+
rootElement: z.string().optional().describe("CSS selector for root element to test"),
|
|
1416
|
+
hideElements: z.string().optional().describe("CSS selector for elements to hide"),
|
|
1417
|
+
browser: z.object({
|
|
1418
|
+
waitForSelector: z.string().optional().describe("CSS selector to wait for"),
|
|
1419
|
+
waitForTimeout: z.number().int().positive().max(6e4).optional(),
|
|
1420
|
+
viewport: z.object({
|
|
1421
|
+
width: z.number().int().positive().default(1280),
|
|
1422
|
+
height: z.number().int().positive().default(720)
|
|
1423
|
+
}).optional()
|
|
1424
|
+
}).optional()
|
|
1425
|
+
}).optional()
|
|
1426
|
+
});
|
|
1427
|
+
var analyzeWithPa11yTool = {
|
|
1428
|
+
name: "analyze-with-pa11y",
|
|
1429
|
+
description: `Analyze a web page or HTML content for accessibility issues using Pa11y.
|
|
1430
|
+
|
|
1431
|
+
Returns accessibility violations based on WCAG guidelines.
|
|
1432
|
+
|
|
1433
|
+
Input options
|
|
1434
|
+
- url: URL of the page to analyze
|
|
1435
|
+
- html: Raw HTML content to analyze (alternative to url)
|
|
1436
|
+
- options.standard: WCAG standard to test against (WCAG2A, WCAG2AA, WCAG2AAA, WCAG21A, WCAG21AA, WCAG21AAA). Default: WCAG21AA
|
|
1437
|
+
- options.includeWarnings: Include warnings in results. Default: true
|
|
1438
|
+
- options.includeNotices: Include notices in results. Default: false
|
|
1439
|
+
- options.rootElement: CSS selector for root element to test
|
|
1440
|
+
- options.hideElements: CSS selector for elements to hide from testing
|
|
1441
|
+
- options.browser.waitForSelector: CSS selector to wait for before analysis
|
|
1442
|
+
- options.browser.viewport: Browser viewport dimensions
|
|
1443
|
+
|
|
1444
|
+
Output:
|
|
1445
|
+
- issues: Array of accessibility issues found
|
|
1446
|
+
- summary: Issue counts by severity and WCAG principle
|
|
1447
|
+
- metadata: Tool version and page info`,
|
|
1448
|
+
register(server2) {
|
|
1449
|
+
server2.tool(
|
|
1450
|
+
this.name,
|
|
1451
|
+
this.description,
|
|
1452
|
+
Pa11yToolMcpInputSchema.shape,
|
|
1453
|
+
async (input) => {
|
|
1454
|
+
const parseResult = Pa11yToolInputSchema.safeParse(input);
|
|
1455
|
+
if (!parseResult.success) {
|
|
1456
|
+
const errors = parseResult.error.errors.map((e) => `${e.path.join(".")}: ${e.message}`).join("; ");
|
|
1457
|
+
const response2 = createErrorResponse(new Error(`Invalid input: ${errors}`));
|
|
1458
|
+
return { content: response2.content };
|
|
1459
|
+
}
|
|
1460
|
+
const response = await handlePa11yAnalysis(parseResult.data);
|
|
1461
|
+
return { content: response.content };
|
|
1462
|
+
}
|
|
1463
|
+
);
|
|
1464
|
+
}
|
|
1465
|
+
};
|
|
1466
|
+
|
|
1467
|
+
// src/normalizers/eslint.ts
|
|
1468
|
+
var ESLINT_RULE_WCAG_MAP = {
|
|
1469
|
+
"vuejs-accessibility/alt-text": { criterion: "1.1.1", principle: "perceivable" },
|
|
1470
|
+
"vuejs-accessibility/anchor-has-content": { criterion: "2.4.4", principle: "operable" },
|
|
1471
|
+
"vuejs-accessibility/aria-props": { criterion: "4.1.2", principle: "robust" },
|
|
1472
|
+
"vuejs-accessibility/aria-role": { criterion: "4.1.2", principle: "robust" },
|
|
1473
|
+
"vuejs-accessibility/aria-unsupported-elements": { criterion: "4.1.2", principle: "robust" },
|
|
1474
|
+
"vuejs-accessibility/click-events-have-key-events": { criterion: "2.1.1", principle: "operable" },
|
|
1475
|
+
"vuejs-accessibility/form-control-has-label": { criterion: "1.3.1", principle: "perceivable" },
|
|
1476
|
+
"vuejs-accessibility/heading-has-content": { criterion: "1.3.1", principle: "perceivable" },
|
|
1477
|
+
"vuejs-accessibility/iframe-has-title": { criterion: "2.4.1", principle: "operable" },
|
|
1478
|
+
"vuejs-accessibility/interactive-supports-focus": { criterion: "2.1.1", principle: "operable" },
|
|
1479
|
+
"vuejs-accessibility/label-has-for": { criterion: "1.3.1", principle: "perceivable" },
|
|
1480
|
+
"vuejs-accessibility/media-has-caption": { criterion: "1.2.2", principle: "perceivable" },
|
|
1481
|
+
"vuejs-accessibility/mouse-events-have-key-events": { criterion: "2.1.1", principle: "operable" },
|
|
1482
|
+
"vuejs-accessibility/no-access-key": { criterion: "2.1.1", principle: "operable" },
|
|
1483
|
+
"vuejs-accessibility/no-autofocus": { criterion: "2.4.3", principle: "operable" },
|
|
1484
|
+
"vuejs-accessibility/no-distracting-elements": { criterion: "2.2.2", principle: "operable" },
|
|
1485
|
+
"vuejs-accessibility/no-onchange": { criterion: "3.2.2", principle: "understandable" },
|
|
1486
|
+
"vuejs-accessibility/no-redundant-roles": { criterion: "4.1.2", principle: "robust" },
|
|
1487
|
+
"vuejs-accessibility/no-static-element-interactions": { criterion: "4.1.2", principle: "robust" },
|
|
1488
|
+
"vuejs-accessibility/role-has-required-aria-props": { criterion: "4.1.2", principle: "robust" },
|
|
1489
|
+
"vuejs-accessibility/tabindex-no-positive": { criterion: "2.4.3", principle: "operable" }
|
|
1490
|
+
};
|
|
1491
|
+
var ESLintNormalizer = class extends BaseNormalizer {
|
|
1492
|
+
normalize(results, context) {
|
|
1493
|
+
const issues = [];
|
|
1494
|
+
for (const fileResult of results) {
|
|
1495
|
+
for (const message of fileResult.messages) {
|
|
1496
|
+
if (!message.ruleId) continue;
|
|
1497
|
+
const issue = this.normalizeIssue(message, fileResult.filePath, context);
|
|
1498
|
+
if (issue) {
|
|
1499
|
+
issues.push(issue);
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
return issues;
|
|
1504
|
+
}
|
|
1505
|
+
normalizeIssue(message, filePath, _context) {
|
|
1506
|
+
if (!message.ruleId) return null;
|
|
1507
|
+
const wcagInfo = this.getWcagInfo(message.ruleId);
|
|
1508
|
+
const baseIssue = {
|
|
1509
|
+
id: this.generateIssueId("eslint-vuejs-a11y", message.ruleId, `${filePath}:${message.line}:${message.column}`),
|
|
1510
|
+
ruleId: message.ruleId,
|
|
1511
|
+
tool: "eslint-vuejs-a11y",
|
|
1512
|
+
severity: this.mapSeverity(message.severity),
|
|
1513
|
+
wcag: wcagInfo,
|
|
1514
|
+
location: {
|
|
1515
|
+
file: filePath,
|
|
1516
|
+
line: message.line,
|
|
1517
|
+
column: message.column,
|
|
1518
|
+
snippet: message.source ?? void 0
|
|
1519
|
+
},
|
|
1520
|
+
message: message.message,
|
|
1521
|
+
confidence: 1
|
|
1522
|
+
};
|
|
1523
|
+
return this.enrichWithHumanContext(baseIssue);
|
|
1524
|
+
}
|
|
1525
|
+
mapSeverity(severity) {
|
|
1526
|
+
return severity === 2 ? "serious" : "moderate";
|
|
1527
|
+
}
|
|
1528
|
+
getWcagInfo(ruleId) {
|
|
1529
|
+
const mapping = ESLINT_RULE_WCAG_MAP[ruleId];
|
|
1530
|
+
if (!mapping) return void 0;
|
|
1531
|
+
return {
|
|
1532
|
+
criterion: mapping.criterion,
|
|
1533
|
+
level: "A",
|
|
1534
|
+
principle: mapping.principle,
|
|
1535
|
+
version: "2.1"
|
|
1536
|
+
};
|
|
1537
|
+
}
|
|
1538
|
+
};
|
|
1539
|
+
var eslintNormalizer = new ESLintNormalizer();
|
|
1540
|
+
|
|
1541
|
+
// src/adapters/eslint.ts
|
|
1542
|
+
var DEFAULT_ESLINT_RULES = {
|
|
1543
|
+
"vuejs-accessibility/alt-text": "error",
|
|
1544
|
+
"vuejs-accessibility/anchor-has-content": "error",
|
|
1545
|
+
"vuejs-accessibility/aria-props": "error",
|
|
1546
|
+
"vuejs-accessibility/aria-role": "error",
|
|
1547
|
+
"vuejs-accessibility/aria-unsupported-elements": "error",
|
|
1548
|
+
"vuejs-accessibility/click-events-have-key-events": "error",
|
|
1549
|
+
"vuejs-accessibility/form-control-has-label": "error",
|
|
1550
|
+
"vuejs-accessibility/heading-has-content": "error",
|
|
1551
|
+
"vuejs-accessibility/iframe-has-title": "error",
|
|
1552
|
+
"vuejs-accessibility/interactive-supports-focus": "error",
|
|
1553
|
+
"vuejs-accessibility/label-has-for": "error",
|
|
1554
|
+
"vuejs-accessibility/media-has-caption": "error",
|
|
1555
|
+
"vuejs-accessibility/mouse-events-have-key-events": "error",
|
|
1556
|
+
"vuejs-accessibility/no-access-key": "error",
|
|
1557
|
+
"vuejs-accessibility/no-autofocus": "warn",
|
|
1558
|
+
"vuejs-accessibility/no-distracting-elements": "error",
|
|
1559
|
+
"vuejs-accessibility/no-onchange": "warn",
|
|
1560
|
+
"vuejs-accessibility/no-redundant-roles": "warn",
|
|
1561
|
+
"vuejs-accessibility/no-static-element-interactions": "error",
|
|
1562
|
+
"vuejs-accessibility/role-has-required-aria-props": "error",
|
|
1563
|
+
"vuejs-accessibility/tabindex-no-positive": "error"
|
|
1564
|
+
};
|
|
1565
|
+
var ESLintAdapter = class extends BaseAdapter {
|
|
1566
|
+
name = "eslint-vuejs-a11y";
|
|
1567
|
+
version = "2.4.1";
|
|
1568
|
+
constructor(config = {}) {
|
|
1569
|
+
super(config);
|
|
1570
|
+
}
|
|
1571
|
+
async analyze(target, options) {
|
|
1572
|
+
const startTime = Date.now();
|
|
1573
|
+
const targetValue = target.value;
|
|
1574
|
+
this.logger.info("Starting ESLint a11y analysis", { target: targetValue, type: target.type });
|
|
1575
|
+
try {
|
|
1576
|
+
const eslint = await this.createESLintInstance(options);
|
|
1577
|
+
const results = await this.runESLint(eslint, target);
|
|
1578
|
+
const fileResults = results.map((r) => ({
|
|
1579
|
+
filePath: r.filePath,
|
|
1580
|
+
messages: r.messages.map((m) => ({
|
|
1581
|
+
ruleId: m.ruleId,
|
|
1582
|
+
severity: m.severity,
|
|
1583
|
+
message: m.message,
|
|
1584
|
+
line: m.line,
|
|
1585
|
+
column: m.column,
|
|
1586
|
+
endLine: m.endLine,
|
|
1587
|
+
endColumn: m.endColumn,
|
|
1588
|
+
nodeType: m.nodeType ?? void 0,
|
|
1589
|
+
source: r.source
|
|
1590
|
+
})),
|
|
1591
|
+
errorCount: r.errorCount,
|
|
1592
|
+
warningCount: r.warningCount
|
|
1593
|
+
}));
|
|
1594
|
+
const issues = eslintNormalizer.normalize(fileResults, {
|
|
1595
|
+
tool: "eslint-vuejs-a11y"
|
|
1596
|
+
});
|
|
1597
|
+
const duration = Date.now() - startTime;
|
|
1598
|
+
this.logger.info("ESLint a11y analysis completed", {
|
|
1599
|
+
target: targetValue,
|
|
1600
|
+
issueCount: issues.length,
|
|
1601
|
+
durationMs: duration
|
|
1602
|
+
});
|
|
1603
|
+
return {
|
|
1604
|
+
success: true,
|
|
1605
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1606
|
+
duration,
|
|
1607
|
+
target: targetValue,
|
|
1608
|
+
tool: "eslint-vuejs-a11y",
|
|
1609
|
+
issues,
|
|
1610
|
+
summary: this.buildSummary(issues),
|
|
1611
|
+
metadata: {
|
|
1612
|
+
toolVersion: this.version
|
|
1613
|
+
}
|
|
1614
|
+
};
|
|
1615
|
+
} catch (error) {
|
|
1616
|
+
const duration = Date.now() - startTime;
|
|
1617
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1618
|
+
this.logger.error("ESLint a11y analysis failed", {
|
|
1619
|
+
target: targetValue,
|
|
1620
|
+
error: error instanceof Error ? error : new Error(errorMessage)
|
|
1621
|
+
});
|
|
1622
|
+
return {
|
|
1623
|
+
success: false,
|
|
1624
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1625
|
+
duration,
|
|
1626
|
+
target: targetValue,
|
|
1627
|
+
tool: "eslint-vuejs-a11y",
|
|
1628
|
+
issues: [],
|
|
1629
|
+
summary: this.buildSummary([]),
|
|
1630
|
+
error: errorMessage
|
|
1631
|
+
};
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
async isAvailable() {
|
|
1635
|
+
try {
|
|
1636
|
+
return typeof ESLint === "function";
|
|
1637
|
+
} catch {
|
|
1638
|
+
return false;
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
async createESLintInstance(options) {
|
|
1642
|
+
const vuePlugin = await import('eslint-plugin-vuejs-accessibility');
|
|
1643
|
+
const vueParser = await import('vue-eslint-parser');
|
|
1644
|
+
const rules = { ...DEFAULT_ESLINT_RULES };
|
|
1645
|
+
if (options?.rules) {
|
|
1646
|
+
for (const rule of options.rules) {
|
|
1647
|
+
rules[rule] = "error";
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
if (options?.excludeRules) {
|
|
1651
|
+
for (const rule of options.excludeRules) {
|
|
1652
|
+
rules[rule] = "off";
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
const plugin = vuePlugin.default ?? vuePlugin;
|
|
1656
|
+
const parser = vueParser.default ?? vueParser;
|
|
1657
|
+
const baseConfig = [
|
|
1658
|
+
{
|
|
1659
|
+
files: ["**/*.vue"],
|
|
1660
|
+
languageOptions: {
|
|
1661
|
+
parser,
|
|
1662
|
+
parserOptions: {
|
|
1663
|
+
ecmaVersion: "latest",
|
|
1664
|
+
sourceType: "module"
|
|
1665
|
+
}
|
|
1666
|
+
},
|
|
1667
|
+
plugins: {
|
|
1668
|
+
"vuejs-accessibility": plugin
|
|
1669
|
+
},
|
|
1670
|
+
rules
|
|
1671
|
+
}
|
|
1672
|
+
];
|
|
1673
|
+
return new ESLint({
|
|
1674
|
+
overrideConfigFile: true,
|
|
1675
|
+
overrideConfig: baseConfig,
|
|
1676
|
+
fix: false
|
|
1677
|
+
});
|
|
1678
|
+
}
|
|
1679
|
+
async runESLint(eslint, target) {
|
|
1680
|
+
if (target.type === "file") {
|
|
1681
|
+
return eslint.lintFiles([target.value]);
|
|
1682
|
+
}
|
|
1683
|
+
if (target.type === "html") {
|
|
1684
|
+
return eslint.lintText(target.value, {
|
|
1685
|
+
filePath: "inline.vue"
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1688
|
+
throw new Error(`Unsupported target type for ESLint: ${target.type}. Use 'file' or 'html' (for inline Vue code).`);
|
|
1689
|
+
}
|
|
1690
|
+
buildSummary(issues) {
|
|
1691
|
+
const bySeverity = {
|
|
1692
|
+
critical: 0,
|
|
1693
|
+
serious: 0,
|
|
1694
|
+
moderate: 0,
|
|
1695
|
+
minor: 0
|
|
1696
|
+
};
|
|
1697
|
+
const byPrinciple = {
|
|
1698
|
+
perceivable: 0,
|
|
1699
|
+
operable: 0,
|
|
1700
|
+
understandable: 0,
|
|
1701
|
+
robust: 0
|
|
1702
|
+
};
|
|
1703
|
+
const byRule = {};
|
|
1704
|
+
for (const issue of issues) {
|
|
1705
|
+
bySeverity[issue.severity]++;
|
|
1706
|
+
if (issue.wcag?.principle) {
|
|
1707
|
+
byPrinciple[issue.wcag.principle]++;
|
|
1708
|
+
}
|
|
1709
|
+
byRule[issue.ruleId] = (byRule[issue.ruleId] ?? 0) + 1;
|
|
1710
|
+
}
|
|
1711
|
+
return {
|
|
1712
|
+
total: issues.length,
|
|
1713
|
+
bySeverity,
|
|
1714
|
+
byPrinciple,
|
|
1715
|
+
byRule
|
|
1716
|
+
};
|
|
1717
|
+
}
|
|
1718
|
+
};
|
|
1719
|
+
|
|
1720
|
+
// src/tools/eslint.ts
|
|
1721
|
+
var sharedAdapter3 = null;
|
|
1722
|
+
function getAdapter3() {
|
|
1723
|
+
if (!sharedAdapter3) {
|
|
1724
|
+
sharedAdapter3 = new ESLintAdapter({
|
|
1725
|
+
timeout: 3e4
|
|
1726
|
+
});
|
|
1727
|
+
}
|
|
1728
|
+
return sharedAdapter3;
|
|
1729
|
+
}
|
|
1730
|
+
async function disposeAdapter3() {
|
|
1731
|
+
if (sharedAdapter3) {
|
|
1732
|
+
await sharedAdapter3.dispose();
|
|
1733
|
+
sharedAdapter3 = null;
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
process.on("SIGINT", () => {
|
|
1737
|
+
disposeAdapter3().finally(() => process.exit(0));
|
|
1738
|
+
});
|
|
1739
|
+
process.on("SIGTERM", () => {
|
|
1740
|
+
disposeAdapter3().finally(() => process.exit(0));
|
|
1741
|
+
});
|
|
1742
|
+
function buildAnalysisTarget3(input) {
|
|
1743
|
+
if (input.files && input.files.length > 0) {
|
|
1744
|
+
return {
|
|
1745
|
+
type: "file",
|
|
1746
|
+
value: input.files[0]
|
|
1747
|
+
};
|
|
1748
|
+
}
|
|
1749
|
+
if (input.directory) {
|
|
1750
|
+
return {
|
|
1751
|
+
type: "file",
|
|
1752
|
+
value: input.directory
|
|
1753
|
+
};
|
|
1754
|
+
}
|
|
1755
|
+
return {
|
|
1756
|
+
type: "html",
|
|
1757
|
+
value: input.code
|
|
1758
|
+
};
|
|
1759
|
+
}
|
|
1760
|
+
function buildAnalysisOptions3(_input) {
|
|
1761
|
+
return {
|
|
1762
|
+
wcagLevel: "AA",
|
|
1763
|
+
includeWarnings: true
|
|
1764
|
+
};
|
|
1765
|
+
}
|
|
1766
|
+
function formatOutput3(result) {
|
|
1767
|
+
return {
|
|
1768
|
+
success: result.success,
|
|
1769
|
+
target: result.target,
|
|
1770
|
+
issueCount: result.issues.length,
|
|
1771
|
+
issues: result.issues,
|
|
1772
|
+
summary: result.summary,
|
|
1773
|
+
metadata: result.metadata,
|
|
1774
|
+
duration: result.duration,
|
|
1775
|
+
error: result.error
|
|
1776
|
+
};
|
|
1777
|
+
}
|
|
1778
|
+
var handleESLintAnalysis = withToolContext(
|
|
1779
|
+
"analyze-with-eslint",
|
|
1780
|
+
async (input, context) => {
|
|
1781
|
+
context.logger.debug("Building analysis configuration", {
|
|
1782
|
+
hasFiles: !!(input.files && input.files.length > 0),
|
|
1783
|
+
hasDirectory: !!input.directory,
|
|
1784
|
+
hasCode: !!input.code
|
|
1785
|
+
});
|
|
1786
|
+
const adapter = getAdapter3();
|
|
1787
|
+
const isAvailable = await adapter.isAvailable();
|
|
1788
|
+
if (!isAvailable) {
|
|
1789
|
+
return createErrorResponse(
|
|
1790
|
+
new Error("ESLint adapter is not available.")
|
|
1791
|
+
);
|
|
1792
|
+
}
|
|
1793
|
+
const target = buildAnalysisTarget3(input);
|
|
1794
|
+
const options = buildAnalysisOptions3(input);
|
|
1795
|
+
context.logger.info("Starting ESLint analysis", {
|
|
1796
|
+
targetType: target.type,
|
|
1797
|
+
target: target.type === "file" ? target.value : "[inline code]"
|
|
1798
|
+
});
|
|
1799
|
+
const result = await adapter.analyze(target, options);
|
|
1800
|
+
if (!result.success) {
|
|
1801
|
+
context.logger.warn("Analysis completed with errors", {
|
|
1802
|
+
error: result.error
|
|
1803
|
+
});
|
|
1804
|
+
}
|
|
1805
|
+
const output = formatOutput3(result);
|
|
1806
|
+
return createJsonResponse(output, !result.success);
|
|
1807
|
+
}
|
|
1808
|
+
);
|
|
1809
|
+
var ESLintToolMcpInputSchema = z.object({
|
|
1810
|
+
files: z.array(z.string()).min(1).optional().describe("Array of file paths to lint"),
|
|
1811
|
+
directory: z.string().optional().describe("Directory path to lint recursively"),
|
|
1812
|
+
code: z.string().optional().describe("Inline Vue component code to lint"),
|
|
1813
|
+
options: z.object({
|
|
1814
|
+
rules: z.record(
|
|
1815
|
+
z.string(),
|
|
1816
|
+
z.union([z.enum(["off", "warn", "error"]), z.number().int().min(0).max(2)])
|
|
1817
|
+
).optional().describe("Override specific rule configurations"),
|
|
1818
|
+
fix: z.literal(false).default(false).describe("Fix mode is disabled - MCP only reports issues")
|
|
1819
|
+
}).optional()
|
|
1820
|
+
});
|
|
1821
|
+
var analyzeWithESLintTool = {
|
|
1822
|
+
name: "analyze-with-eslint",
|
|
1823
|
+
description: `Analyze Vue.js files for accessibility issues using ESLint with eslint-plugin-vuejs-accessibility.
|
|
1824
|
+
|
|
1825
|
+
Performs static code analysis of Vue components.
|
|
1826
|
+
|
|
1827
|
+
Input options:
|
|
1828
|
+
- files: Array of file paths to lint (must be .vue files)
|
|
1829
|
+
- directory: Directory path to lint recursively
|
|
1830
|
+
- code: Inline Vue component code to lint
|
|
1831
|
+
- options.rules: Override specific rule configurations (off, warn, error, or 0-2)
|
|
1832
|
+
- options.fix: Fix mode (always disabled - MCP only reports issues)
|
|
1833
|
+
|
|
1834
|
+
Output:
|
|
1835
|
+
- issues: Array of accessibility issues found
|
|
1836
|
+
- summary: Issue counts by severity and WCAG principle
|
|
1837
|
+
- metadata: Tool version info`,
|
|
1838
|
+
register(server2) {
|
|
1839
|
+
server2.tool(
|
|
1840
|
+
this.name,
|
|
1841
|
+
this.description,
|
|
1842
|
+
ESLintToolMcpInputSchema.shape,
|
|
1843
|
+
async (input) => {
|
|
1844
|
+
const parseResult = ESLintA11yToolInputSchema.safeParse(input);
|
|
1845
|
+
if (!parseResult.success) {
|
|
1846
|
+
const errors = parseResult.error.errors.map((e) => `${e.path.join(".")}: ${e.message}`).join("; ");
|
|
1847
|
+
const response2 = createErrorResponse(new Error(`Invalid input: ${errors}`));
|
|
1848
|
+
return { content: response2.content };
|
|
1849
|
+
}
|
|
1850
|
+
const response = await handleESLintAnalysis(parseResult.data);
|
|
1851
|
+
return { content: response.content };
|
|
1852
|
+
}
|
|
1853
|
+
);
|
|
1854
|
+
}
|
|
1855
|
+
};
|
|
1856
|
+
var sharedAxeAdapter = null;
|
|
1857
|
+
var sharedPa11yAdapter = null;
|
|
1858
|
+
function getAxeAdapter() {
|
|
1859
|
+
if (!sharedAxeAdapter) {
|
|
1860
|
+
sharedAxeAdapter = new AxeAdapter({ headless: true, timeout: 3e4 });
|
|
1861
|
+
}
|
|
1862
|
+
return sharedAxeAdapter;
|
|
1863
|
+
}
|
|
1864
|
+
function getPa11yAdapter() {
|
|
1865
|
+
if (!sharedPa11yAdapter) {
|
|
1866
|
+
sharedPa11yAdapter = new Pa11yAdapter({ timeout: 3e4 });
|
|
1867
|
+
}
|
|
1868
|
+
return sharedPa11yAdapter;
|
|
1869
|
+
}
|
|
1870
|
+
async function disposeAdapters() {
|
|
1871
|
+
await Promise.all([
|
|
1872
|
+
sharedAxeAdapter?.dispose(),
|
|
1873
|
+
sharedPa11yAdapter?.dispose()
|
|
1874
|
+
]);
|
|
1875
|
+
sharedAxeAdapter = null;
|
|
1876
|
+
sharedPa11yAdapter = null;
|
|
1877
|
+
}
|
|
1878
|
+
function buildAnalysisTarget4(input) {
|
|
1879
|
+
if (input.url) {
|
|
1880
|
+
return {
|
|
1881
|
+
type: "url",
|
|
1882
|
+
value: input.url,
|
|
1883
|
+
options: {
|
|
1884
|
+
waitForSelector: input.options?.browser?.waitForSelector,
|
|
1885
|
+
timeout: input.options?.browser?.waitForTimeout,
|
|
1886
|
+
viewport: input.options?.browser?.viewport
|
|
1887
|
+
}
|
|
1888
|
+
};
|
|
1889
|
+
}
|
|
1890
|
+
return {
|
|
1891
|
+
type: "html",
|
|
1892
|
+
value: input.html,
|
|
1893
|
+
options: {
|
|
1894
|
+
waitForSelector: input.options?.browser?.waitForSelector,
|
|
1895
|
+
timeout: input.options?.browser?.waitForTimeout,
|
|
1896
|
+
viewport: input.options?.browser?.viewport
|
|
1897
|
+
}
|
|
1898
|
+
};
|
|
1899
|
+
}
|
|
1900
|
+
function buildAnalysisOptions4(input) {
|
|
1901
|
+
return {
|
|
1902
|
+
wcagLevel: input.options?.wcagLevel ?? "AA",
|
|
1903
|
+
includeWarnings: true
|
|
1904
|
+
};
|
|
1905
|
+
}
|
|
1906
|
+
function generateIssueFingerprint(issue) {
|
|
1907
|
+
const parts = [
|
|
1908
|
+
issue.ruleId,
|
|
1909
|
+
issue.wcag?.criterion ?? "no-wcag",
|
|
1910
|
+
issue.location.selector ?? issue.location.file ?? "no-location",
|
|
1911
|
+
issue.message.substring(0, 50)
|
|
1912
|
+
];
|
|
1913
|
+
return parts.join("|");
|
|
1914
|
+
}
|
|
1915
|
+
function deduplicateIssues(issues) {
|
|
1916
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1917
|
+
for (const issue of issues) {
|
|
1918
|
+
const fingerprint = generateIssueFingerprint(issue);
|
|
1919
|
+
if (!seen.has(fingerprint)) {
|
|
1920
|
+
seen.set(fingerprint, issue);
|
|
1921
|
+
} else {
|
|
1922
|
+
const existing = seen.get(fingerprint);
|
|
1923
|
+
if (issue.severity === "critical" && existing.severity !== "critical") {
|
|
1924
|
+
seen.set(fingerprint, issue);
|
|
1925
|
+
} else if (issue.confidence && existing.confidence && issue.confidence > existing.confidence) {
|
|
1926
|
+
seen.set(fingerprint, issue);
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
return Array.from(seen.values());
|
|
1931
|
+
}
|
|
1932
|
+
function groupByWCAG(issues) {
|
|
1933
|
+
const grouped = {};
|
|
1934
|
+
for (const issue of issues) {
|
|
1935
|
+
const key = issue.wcag?.criterion ?? "unknown";
|
|
1936
|
+
if (!grouped[key]) {
|
|
1937
|
+
grouped[key] = [];
|
|
1938
|
+
}
|
|
1939
|
+
grouped[key].push(issue);
|
|
1940
|
+
}
|
|
1941
|
+
return grouped;
|
|
1942
|
+
}
|
|
1943
|
+
function buildCombinedSummary(issues, _toolsUsed) {
|
|
1944
|
+
const bySeverity = {
|
|
1945
|
+
critical: 0,
|
|
1946
|
+
serious: 0,
|
|
1947
|
+
moderate: 0,
|
|
1948
|
+
minor: 0
|
|
1949
|
+
};
|
|
1950
|
+
const byPrinciple = {
|
|
1951
|
+
perceivable: 0,
|
|
1952
|
+
operable: 0,
|
|
1953
|
+
understandable: 0,
|
|
1954
|
+
robust: 0
|
|
1955
|
+
};
|
|
1956
|
+
const byTool = {
|
|
1957
|
+
"axe-core": 0,
|
|
1958
|
+
"pa11y": 0
|
|
1959
|
+
};
|
|
1960
|
+
const byRule = {};
|
|
1961
|
+
for (const issue of issues) {
|
|
1962
|
+
bySeverity[issue.severity]++;
|
|
1963
|
+
if (issue.wcag?.principle) {
|
|
1964
|
+
byPrinciple[issue.wcag.principle]++;
|
|
1965
|
+
}
|
|
1966
|
+
byTool[issue.tool]++;
|
|
1967
|
+
byRule[issue.ruleId] = (byRule[issue.ruleId] ?? 0) + 1;
|
|
1968
|
+
}
|
|
1969
|
+
return {
|
|
1970
|
+
total: issues.length,
|
|
1971
|
+
bySeverity,
|
|
1972
|
+
byPrinciple,
|
|
1973
|
+
byTool,
|
|
1974
|
+
byRule
|
|
1975
|
+
};
|
|
1976
|
+
}
|
|
1977
|
+
function formatOutput4(result, deduplicatedCount, issuesByWCAG) {
|
|
1978
|
+
return {
|
|
1979
|
+
success: result.success,
|
|
1980
|
+
target: result.target,
|
|
1981
|
+
toolsUsed: result.toolsUsed,
|
|
1982
|
+
issueCount: result.issues.length,
|
|
1983
|
+
deduplicatedCount,
|
|
1984
|
+
issues: result.issues,
|
|
1985
|
+
issuesByWCAG,
|
|
1986
|
+
summary: result.summary,
|
|
1987
|
+
individualResults: result.individualResults ?? [],
|
|
1988
|
+
duration: result.duration,
|
|
1989
|
+
error: result.error
|
|
1990
|
+
};
|
|
1991
|
+
}
|
|
1992
|
+
var handleCombinedAnalysis = withToolContext(
|
|
1993
|
+
"analyze-all",
|
|
1994
|
+
async (input, context) => {
|
|
1995
|
+
const startTime = Date.now();
|
|
1996
|
+
const toolsToRun = input.tools ?? ["axe-core", "pa11y"];
|
|
1997
|
+
const shouldDeduplicate = input.options?.deduplicateResults ?? true;
|
|
1998
|
+
context.logger.info("Starting combined web analysis", {
|
|
1999
|
+
tools: toolsToRun,
|
|
2000
|
+
deduplicate: shouldDeduplicate,
|
|
2001
|
+
hasUrl: !!input.url,
|
|
2002
|
+
hasHtml: !!input.html
|
|
2003
|
+
});
|
|
2004
|
+
const target = buildAnalysisTarget4(input);
|
|
2005
|
+
const options = buildAnalysisOptions4(input);
|
|
2006
|
+
const results = [];
|
|
2007
|
+
const errors = [];
|
|
2008
|
+
const analysisPromises = [];
|
|
2009
|
+
if (toolsToRun.includes("axe-core")) {
|
|
2010
|
+
analysisPromises.push(
|
|
2011
|
+
(async () => {
|
|
2012
|
+
try {
|
|
2013
|
+
const adapter = getAxeAdapter();
|
|
2014
|
+
const result = await adapter.analyze(target, options);
|
|
2015
|
+
results.push(result);
|
|
2016
|
+
context.logger.debug("Axe analysis completed", { issueCount: result.issues.length });
|
|
2017
|
+
} catch (error) {
|
|
2018
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
2019
|
+
errors.push(`Axe: ${msg}`);
|
|
2020
|
+
context.logger.error("Axe analysis failed", {
|
|
2021
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
2022
|
+
});
|
|
2023
|
+
}
|
|
2024
|
+
})()
|
|
2025
|
+
);
|
|
2026
|
+
}
|
|
2027
|
+
if (toolsToRun.includes("pa11y")) {
|
|
2028
|
+
analysisPromises.push(
|
|
2029
|
+
(async () => {
|
|
2030
|
+
try {
|
|
2031
|
+
const adapter = getPa11yAdapter();
|
|
2032
|
+
const result = await adapter.analyze(target, options);
|
|
2033
|
+
results.push(result);
|
|
2034
|
+
context.logger.debug("Pa11y analysis completed", { issueCount: result.issues.length });
|
|
2035
|
+
} catch (error) {
|
|
2036
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
2037
|
+
errors.push(`Pa11y: ${msg}`);
|
|
2038
|
+
context.logger.error("Pa11y analysis failed", {
|
|
2039
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
2040
|
+
});
|
|
2041
|
+
}
|
|
2042
|
+
})()
|
|
2043
|
+
);
|
|
2044
|
+
}
|
|
2045
|
+
await Promise.all(analysisPromises);
|
|
2046
|
+
const allIssues = results.flatMap((r) => r.issues);
|
|
2047
|
+
const originalCount = allIssues.length;
|
|
2048
|
+
const finalIssues = shouldDeduplicate ? deduplicateIssues(allIssues) : allIssues;
|
|
2049
|
+
const issuesByWCAG = groupByWCAG(finalIssues);
|
|
2050
|
+
const duration = Date.now() - startTime;
|
|
2051
|
+
context.logger.info("Combined analysis completed", {
|
|
2052
|
+
totalIssues: originalCount,
|
|
2053
|
+
deduplicatedIssues: finalIssues.length,
|
|
2054
|
+
toolsRun: results.length,
|
|
2055
|
+
errors: errors.length,
|
|
2056
|
+
durationMs: duration
|
|
2057
|
+
});
|
|
2058
|
+
const combinedResult = {
|
|
2059
|
+
success: errors.length === 0,
|
|
2060
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2061
|
+
duration,
|
|
2062
|
+
target: target.value,
|
|
2063
|
+
toolsUsed: results.map((r) => r.tool),
|
|
2064
|
+
issues: finalIssues,
|
|
2065
|
+
summary: buildCombinedSummary(finalIssues, toolsToRun),
|
|
2066
|
+
individualResults: results,
|
|
2067
|
+
error: errors.length > 0 ? errors.join("; ") : void 0
|
|
2068
|
+
};
|
|
2069
|
+
const output = formatOutput4(combinedResult, originalCount, issuesByWCAG);
|
|
2070
|
+
return createJsonResponse(output, !combinedResult.success);
|
|
2071
|
+
}
|
|
2072
|
+
);
|
|
2073
|
+
var CombinedToolMcpInputSchema = z.object({
|
|
2074
|
+
url: z.string().url().optional().describe("URL of the page to analyze"),
|
|
2075
|
+
html: z.string().min(1).optional().describe("Raw HTML content to analyze"),
|
|
2076
|
+
tools: z.array(z.enum(["axe-core", "pa11y"])).min(1).default(["axe-core", "pa11y"]).describe("Tools to run for web analysis"),
|
|
2077
|
+
options: z.object({
|
|
2078
|
+
wcagLevel: z.enum(["A", "AA", "AAA"]).default("AA").describe("WCAG conformance level"),
|
|
2079
|
+
deduplicateResults: z.boolean().default(true).describe("Merge similar issues from different tools"),
|
|
2080
|
+
browser: z.object({
|
|
2081
|
+
waitForSelector: z.string().optional().describe("CSS selector to wait for"),
|
|
2082
|
+
waitForTimeout: z.number().int().positive().max(6e4).optional(),
|
|
2083
|
+
viewport: z.object({
|
|
2084
|
+
width: z.number().int().positive().default(1280),
|
|
2085
|
+
height: z.number().int().positive().default(720)
|
|
2086
|
+
}).optional()
|
|
2087
|
+
}).optional()
|
|
2088
|
+
}).optional()
|
|
2089
|
+
});
|
|
2090
|
+
var analyzeAllTool = {
|
|
2091
|
+
name: "analyze-all",
|
|
2092
|
+
description: `Run multiple accessibility analysis tools in parallel and combine results.
|
|
2093
|
+
|
|
2094
|
+
Executes axe-core and Pa11y for web analysis (URL/HTML). Use analyze-with-eslint separately for Vue source code analysis.
|
|
2095
|
+
|
|
2096
|
+
Input options:
|
|
2097
|
+
- url: URL of the page to analyze (required for web analysis)
|
|
2098
|
+
- html: Raw HTML content (alternative to url for web analysis)
|
|
2099
|
+
- tools: Array of tools to run ['axe-core', 'pa11y']. Default: ['axe-core', 'pa11y']
|
|
2100
|
+
- options.wcagLevel: WCAG level (A, AA, AAA). Default: AA
|
|
2101
|
+
- options.deduplicateResults: Merge similar issues from different tools. Default: true
|
|
2102
|
+
- options.browser.waitForSelector: CSS selector to wait for
|
|
2103
|
+
- options.browser.viewport: Browser viewport dimensions
|
|
2104
|
+
|
|
2105
|
+
Output:
|
|
2106
|
+
- issues: Combined and deduplicated accessibility issues
|
|
2107
|
+
- issuesByWCAG: Issues grouped by WCAG criterion
|
|
2108
|
+
- summary: Aggregated counts by severity, principle, and tool
|
|
2109
|
+
- individualResults: Full results from each tool
|
|
2110
|
+
- deduplicatedCount: Number of duplicate issues removed
|
|
2111
|
+
|
|
2112
|
+
Note: For Vue source code analysis, use analyze-with-eslint separately.`,
|
|
2113
|
+
register(server2) {
|
|
2114
|
+
server2.tool(
|
|
2115
|
+
this.name,
|
|
2116
|
+
this.description,
|
|
2117
|
+
CombinedToolMcpInputSchema.shape,
|
|
2118
|
+
async (input) => {
|
|
2119
|
+
const parseResult = CombinedAnalysisInputSchema.safeParse(input);
|
|
2120
|
+
if (!parseResult.success) {
|
|
2121
|
+
const errors = parseResult.error.errors.map((e) => `${e.path.join(".")}: ${e.message}`).join("; ");
|
|
2122
|
+
const response2 = createErrorResponse(new Error(`Invalid input: ${errors}`));
|
|
2123
|
+
return { content: response2.content };
|
|
2124
|
+
}
|
|
2125
|
+
const response = await handleCombinedAnalysis(parseResult.data);
|
|
2126
|
+
return { content: response.content };
|
|
2127
|
+
}
|
|
2128
|
+
);
|
|
2129
|
+
}
|
|
2130
|
+
};
|
|
2131
|
+
|
|
2132
|
+
// src/server.ts
|
|
2133
|
+
var server = new McpServer({
|
|
2134
|
+
name: "AI-ccesibility",
|
|
2135
|
+
version: "0.1.0"
|
|
2136
|
+
});
|
|
2137
|
+
function registerTools() {
|
|
2138
|
+
analyzeWithAxeTool.register(server);
|
|
2139
|
+
logger.info("Registered tool: analyze-with-axe");
|
|
2140
|
+
analyzeWithPa11yTool.register(server);
|
|
2141
|
+
logger.info("Registered tool: analyze-with-pa11y");
|
|
2142
|
+
analyzeWithESLintTool.register(server);
|
|
2143
|
+
logger.info("Registered tool: analyze-with-eslint");
|
|
2144
|
+
analyzeAllTool.register(server);
|
|
2145
|
+
logger.info("Registered tool: analyze-all");
|
|
2146
|
+
}
|
|
2147
|
+
async function main() {
|
|
2148
|
+
logger.info("Starting AI-ccesibility Server", {
|
|
2149
|
+
version: "0.1.0",
|
|
2150
|
+
tools: ["analyze-with-axe", "analyze-with-pa11y", "analyze-with-eslint", "analyze-all"]
|
|
2151
|
+
});
|
|
2152
|
+
registerTools();
|
|
2153
|
+
const transport = new StdioServerTransport();
|
|
2154
|
+
await server.connect(transport);
|
|
2155
|
+
logger.info("AI-ccesibility Server connected and ready");
|
|
2156
|
+
}
|
|
2157
|
+
async function shutdown() {
|
|
2158
|
+
logger.info("Shutting down AI-ccesibility Server");
|
|
2159
|
+
await Promise.all([
|
|
2160
|
+
disposeAdapter(),
|
|
2161
|
+
disposeAdapter2(),
|
|
2162
|
+
disposeAdapter3(),
|
|
2163
|
+
disposeAdapters()
|
|
2164
|
+
]);
|
|
2165
|
+
logger.info("All adapters disposed");
|
|
2166
|
+
process.exit(0);
|
|
2167
|
+
}
|
|
2168
|
+
process.on("SIGINT", shutdown);
|
|
2169
|
+
process.on("SIGTERM", shutdown);
|
|
2170
|
+
main().catch((error) => {
|
|
2171
|
+
logger.error("Failed to start MCP server", {
|
|
2172
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
2173
|
+
});
|
|
2174
|
+
process.exit(1);
|
|
2175
|
+
});
|
|
2176
|
+
//# sourceMappingURL=server.js.map
|
|
2177
|
+
//# sourceMappingURL=server.js.map
|