mcp-accessibility-scanner 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/Readme.md +81 -0
- package/build/accessibilityChecker.js +183 -0
- package/build/index.js +39 -0
- package/build/server.js +49 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 JustasM
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/Readme.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# MCP Accessibility Scanner
|
|
2
|
+
|
|
3
|
+
A Model Context Protocol (MCP) server for performing automated accessibility scans of web pages using Playwright and Axe-core.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
✅ Full WCAG 2.1/2.2 compliance checking
|
|
8
|
+
🖼️ Automatic screenshot capture with violation highlighting
|
|
9
|
+
📄 Detailed JSON reports with remediation guidance
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Clone repository
|
|
15
|
+
git clone https://github.com/JustasMonkev/mcp-accessibility-scanner.git
|
|
16
|
+
cd mcp-accessibility-scanner
|
|
17
|
+
|
|
18
|
+
# Install dependencies
|
|
19
|
+
npm install
|
|
20
|
+
|
|
21
|
+
# Build project (compiles TypeScript and installs Playwright browsers)
|
|
22
|
+
npm run prepare
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Claude Desktop Configuration
|
|
26
|
+
|
|
27
|
+
Add the following to your Claude Desktop settings to enable the Accessibility Scanner server:
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"mcpServers": {
|
|
32
|
+
"accessibility-checker": {
|
|
33
|
+
"command": "node",
|
|
34
|
+
"args": [
|
|
35
|
+
"path/build/server.js"
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Usage
|
|
43
|
+
|
|
44
|
+
The scanner exposes a single tool `scan_accessibility` that accepts:
|
|
45
|
+
|
|
46
|
+
- `url`: The webpage URL to scan
|
|
47
|
+
- `violationsTag`: Array of accessibility violation tags to check
|
|
48
|
+
|
|
49
|
+
Example usage in Claude:
|
|
50
|
+
```
|
|
51
|
+
Could you scan example.com for accessibility issues related to color contrast?
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Development
|
|
55
|
+
|
|
56
|
+
Start the TypeScript compiler in watch mode:
|
|
57
|
+
```bash
|
|
58
|
+
npm run watch
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Test the MCP server locally:
|
|
62
|
+
```bash
|
|
63
|
+
npm run inspector
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Project Structure
|
|
67
|
+
|
|
68
|
+
- `src/`: Source code
|
|
69
|
+
- `index.ts`: MCP server setup and tool definitions
|
|
70
|
+
- `accessibilityChecker.ts`: Core scanning functionality
|
|
71
|
+
- `dist/`: Compiled JavaScript output
|
|
72
|
+
- `package.json`: Project dependencies and scripts
|
|
73
|
+
- `tsconfig.json`: TypeScript configuration
|
|
74
|
+
|
|
75
|
+
## Output
|
|
76
|
+
|
|
77
|
+
The scanner provides:
|
|
78
|
+
1. A visual report with numbered violations highlighted on the page
|
|
79
|
+
2. A detailed JSON report of all found violations
|
|
80
|
+
3. A full-page screenshot saved to Downloads
|
|
81
|
+
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
36
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
37
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
38
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
39
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
40
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
41
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
45
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
46
|
+
};
|
|
47
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
+
exports.scanViolations = scanViolations;
|
|
49
|
+
const playwright_1 = require("playwright");
|
|
50
|
+
const playwright_2 = require("@axe-core/playwright");
|
|
51
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
52
|
+
const os = __importStar(require("node:os"));
|
|
53
|
+
function scanViolations(url, violationsTag) {
|
|
54
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
55
|
+
var _a;
|
|
56
|
+
const browser = yield playwright_1.chromium.launch({
|
|
57
|
+
headless: true,
|
|
58
|
+
args: [
|
|
59
|
+
'--disable-blink-features=AutomationControlled',
|
|
60
|
+
'--disable-dev-shm-usage',
|
|
61
|
+
'--no-sandbox',
|
|
62
|
+
'--disable-setuid-sandbox',
|
|
63
|
+
]
|
|
64
|
+
});
|
|
65
|
+
const context = yield browser.newContext({
|
|
66
|
+
viewport: { width: 1920, height: 1080 },
|
|
67
|
+
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
|
68
|
+
});
|
|
69
|
+
const page = yield context.newPage();
|
|
70
|
+
yield page.goto(url);
|
|
71
|
+
yield page.addStyleTag({
|
|
72
|
+
content: `
|
|
73
|
+
.a11y-violation {
|
|
74
|
+
position: relative !important;
|
|
75
|
+
outline: 4px solid #FF4444 !important;
|
|
76
|
+
margin: 2px !important;
|
|
77
|
+
}
|
|
78
|
+
.violation-number {
|
|
79
|
+
position: absolute !important;
|
|
80
|
+
top: -12px !important;
|
|
81
|
+
left: -12px !important;
|
|
82
|
+
background: #FF4444;
|
|
83
|
+
color: white !important;
|
|
84
|
+
width: 25px;
|
|
85
|
+
height: 25px;
|
|
86
|
+
border-radius: 50%;
|
|
87
|
+
display: flex !important;
|
|
88
|
+
align-items: center;
|
|
89
|
+
justify-content: center;
|
|
90
|
+
font-weight: bold;
|
|
91
|
+
font-size: 14px;
|
|
92
|
+
z-index: 10000;
|
|
93
|
+
}
|
|
94
|
+
.a11y-violation-info {
|
|
95
|
+
position: absolute !important;
|
|
96
|
+
background: #333333 !important;
|
|
97
|
+
color: white !important;
|
|
98
|
+
padding: 12px !important;
|
|
99
|
+
border-radius: 4px !important;
|
|
100
|
+
font-size: 14px !important;
|
|
101
|
+
max-width: 300px !important;
|
|
102
|
+
z-index: 9999 !important;
|
|
103
|
+
box-shadow: 0 2px 10px rgba(0,0,0,0.3);
|
|
104
|
+
}
|
|
105
|
+
`
|
|
106
|
+
});
|
|
107
|
+
const axe = new playwright_2.AxeBuilder({ page })
|
|
108
|
+
.withTags(violationsTag);
|
|
109
|
+
const results = yield axe.analyze();
|
|
110
|
+
let violationCounter = 1;
|
|
111
|
+
for (const violation of results.violations) {
|
|
112
|
+
for (const node of violation.nodes) {
|
|
113
|
+
try {
|
|
114
|
+
const targetSelector = node.target[0];
|
|
115
|
+
const selector = Array.isArray(targetSelector)
|
|
116
|
+
? targetSelector.join(' ')
|
|
117
|
+
: targetSelector;
|
|
118
|
+
yield page.evaluate(({ selector, violationData, counter }) => {
|
|
119
|
+
const elements = document.querySelectorAll(selector);
|
|
120
|
+
elements.forEach(element => {
|
|
121
|
+
// Create number badge directly on the element
|
|
122
|
+
const numberBadge = document.createElement('div');
|
|
123
|
+
numberBadge.className = 'violation-number';
|
|
124
|
+
numberBadge.textContent = counter.toString();
|
|
125
|
+
// Add violation styling
|
|
126
|
+
element.classList.add('a11y-violation');
|
|
127
|
+
element.appendChild(numberBadge);
|
|
128
|
+
// Create info box
|
|
129
|
+
const listItem = document.createElement('div');
|
|
130
|
+
listItem.style.marginBottom = '15px';
|
|
131
|
+
listItem.innerHTML = `
|
|
132
|
+
<div style="color: #FF4444; font-weight: bold;">
|
|
133
|
+
Violation #${counter}: ${violationData.impact.toUpperCase()}
|
|
134
|
+
</div>
|
|
135
|
+
<div style="margin: 5px 0; font-size: 14px;">
|
|
136
|
+
${violationData.description}
|
|
137
|
+
</div>
|
|
138
|
+
`;
|
|
139
|
+
// Position info box relative to element
|
|
140
|
+
document.body.appendChild(listItem);
|
|
141
|
+
const rect = element.getBoundingClientRect();
|
|
142
|
+
listItem.style.left = `${rect.left + window.scrollX}px`;
|
|
143
|
+
listItem.style.top = `${rect.bottom + window.scrollY + 10}px`;
|
|
144
|
+
});
|
|
145
|
+
}, {
|
|
146
|
+
selector: selector,
|
|
147
|
+
violationData: {
|
|
148
|
+
impact: violation.impact,
|
|
149
|
+
description: violation.description
|
|
150
|
+
},
|
|
151
|
+
counter: violationCounter
|
|
152
|
+
});
|
|
153
|
+
violationCounter++;
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
console.log(`Failed to highlight element: ${error}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
let reportCounter = 1;
|
|
161
|
+
const report = [];
|
|
162
|
+
for (const violation of results.violations) {
|
|
163
|
+
for (const node of violation.nodes) {
|
|
164
|
+
report.push({
|
|
165
|
+
index: reportCounter++,
|
|
166
|
+
element: node.target[0],
|
|
167
|
+
impactLevel: violation.impact,
|
|
168
|
+
description: violation.description,
|
|
169
|
+
wcagCriteria: (_a = violation.tags) === null || _a === void 0 ? void 0 : _a.join(', '),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const downloadsDir = node_path_1.default.join(os.homedir(), 'Downloads');
|
|
174
|
+
const filePath = node_path_1.default.join(downloadsDir, `a11y-report-${Date.now()}.png`);
|
|
175
|
+
const screenshot = yield page.screenshot({
|
|
176
|
+
path: filePath,
|
|
177
|
+
fullPage: true,
|
|
178
|
+
});
|
|
179
|
+
const base64Screenshot = screenshot.toString('base64');
|
|
180
|
+
yield browser.close();
|
|
181
|
+
return { report, base64Screenshot };
|
|
182
|
+
});
|
|
183
|
+
}
|
package/build/index.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
const zod_1 = require("zod");
|
|
13
|
+
const accessibilityChecker_1 = require("./accessibilityChecker");
|
|
14
|
+
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
15
|
+
const server = new mcp_js_1.McpServer({
|
|
16
|
+
name: "Accessibility Information",
|
|
17
|
+
version: "1.0.0"
|
|
18
|
+
});
|
|
19
|
+
server.tool("scan_accessibility", {
|
|
20
|
+
url: zod_1.z.string().url(),
|
|
21
|
+
violationsTag: zod_1.z.array(zod_1.z.string())
|
|
22
|
+
}, (_a) => __awaiter(void 0, [_a], void 0, function* ({ url, violationsTag }) {
|
|
23
|
+
const { report, base64Screenshot } = yield (0, accessibilityChecker_1.scanViolations)(url, violationsTag);
|
|
24
|
+
return {
|
|
25
|
+
content: [
|
|
26
|
+
{
|
|
27
|
+
type: "text",
|
|
28
|
+
text: JSON.stringify(Object.assign({ message: 'The image has been saved to your downloads.' }, report), null, 2)
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
type: "image",
|
|
32
|
+
data: base64Screenshot,
|
|
33
|
+
mimeType: "image/png"
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
isError: false
|
|
37
|
+
};
|
|
38
|
+
}));
|
|
39
|
+
exports.default = server;
|
package/build/server.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
const index_1 = __importDefault(require("./index"));
|
|
16
|
+
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
17
|
+
function main() {
|
|
18
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
19
|
+
const transport = new stdio_js_1.StdioServerTransport();
|
|
20
|
+
yield index_1.default.connect(transport);
|
|
21
|
+
try {
|
|
22
|
+
console.error('Starting MCP Accessibility checker server...');
|
|
23
|
+
console.error('MCP server connected successfully');
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
console.error('Error starting server:', error);
|
|
27
|
+
if (error instanceof Error) {
|
|
28
|
+
console.error('Error stack:', error.stack);
|
|
29
|
+
}
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
// Handle process events
|
|
33
|
+
process.on('disconnect', () => {
|
|
34
|
+
console.error('Process disconnected');
|
|
35
|
+
process.exit(0);
|
|
36
|
+
});
|
|
37
|
+
process.on('uncaughtException', (error) => {
|
|
38
|
+
console.error('Uncaught exception:', error);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
});
|
|
41
|
+
// Keep the process running
|
|
42
|
+
process.stdin.resume();
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
// Start the server
|
|
46
|
+
main().catch(error => {
|
|
47
|
+
console.error('Fatal error:', error);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mcp-accessibility-scanner",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A Model Context Protocol (MCP) server for performing automated accessibility scans of web pages using Playwright and Axe-core",
|
|
5
|
+
"main": "build/index.js",
|
|
6
|
+
"types": "build/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"build/**/*",
|
|
9
|
+
"README.md",
|
|
10
|
+
"LICENSE"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
|
|
14
|
+
"prepare": "npm run build",
|
|
15
|
+
"watch": "tsc --watch",
|
|
16
|
+
"inspector": "npx @modelcontextprotocol/inspector build/index.js"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@axe-core/playwright": "^4.10.1",
|
|
20
|
+
"@modelcontextprotocol/sdk": "^1.4.1",
|
|
21
|
+
"mcp-accessibility-scanner": "file:"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@playwright/test": "^1.49.1",
|
|
25
|
+
"@types/node": "^22.10.7",
|
|
26
|
+
"typescript": "^5.5.3"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"mcp",
|
|
30
|
+
"accessibility",
|
|
31
|
+
"a11y",
|
|
32
|
+
"wcag",
|
|
33
|
+
"axe-core",
|
|
34
|
+
"playwright",
|
|
35
|
+
"claude",
|
|
36
|
+
"model-context-protocol"
|
|
37
|
+
],
|
|
38
|
+
"author": "",
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "https://github.com/JustasMonkev/mcp-accessibility-scanner.git"
|
|
43
|
+
},
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=16.0.0"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
}
|
|
50
|
+
}
|