@mcptoolshop/a11y-mcp-tools 0.3.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.
Files changed (34) hide show
  1. package/.github/workflows/ci.yml +53 -0
  2. package/CODE_OF_CONDUCT.md +129 -0
  3. package/CONTRIBUTING.md +136 -0
  4. package/LICENSE +21 -0
  5. package/PROV_METHODS_CATALOG.md +104 -0
  6. package/README.md +168 -0
  7. package/bin/cli.js +452 -0
  8. package/bin/server.js +244 -0
  9. package/fixtures/requests/a11y.diagnose.ok.json +27 -0
  10. package/fixtures/requests/a11y.evidence.ok.json +25 -0
  11. package/fixtures/responses/a11y.diagnose.ok.json +139 -0
  12. package/fixtures/responses/a11y.diagnose.provenance_fail.json +13 -0
  13. package/fixtures/responses/a11y.evidence.ok.json +88 -0
  14. package/package.json +48 -0
  15. package/src/envelope.js +197 -0
  16. package/src/index.js +9 -0
  17. package/src/schemas/artifact.js +85 -0
  18. package/src/schemas/diagnosis.schema.v0.1.json +137 -0
  19. package/src/schemas/envelope.schema.v0.1.json +108 -0
  20. package/src/schemas/evidence.bundle.schema.v0.1.json +129 -0
  21. package/src/schemas/evidence.js +97 -0
  22. package/src/schemas/index.js +11 -0
  23. package/src/schemas/provenance.js +140 -0
  24. package/src/schemas/tools/a11y.diagnose.request.schema.v0.1.json +77 -0
  25. package/src/schemas/tools/a11y.diagnose.response.schema.v0.1.json +50 -0
  26. package/src/schemas/tools/a11y.evidence.request.schema.v0.1.json +120 -0
  27. package/src/schemas/tools/a11y.evidence.response.schema.v0.1.json +50 -0
  28. package/src/tools/diagnose.js +597 -0
  29. package/src/tools/evidence.js +481 -0
  30. package/src/tools/index.js +10 -0
  31. package/test/contract.test.mjs +154 -0
  32. package/test/diagnose.test.js +485 -0
  33. package/test/evidence.test.js +183 -0
  34. package/test/schema.test.js +327 -0
@@ -0,0 +1,25 @@
1
+ {
2
+ "mcp": {
3
+ "envelope": "mcp.envelope_v0_1",
4
+ "request_id": "req_fixture_evidence_001",
5
+ "tool": "a11y.evidence",
6
+ "client": {
7
+ "name": "a11y-cli",
8
+ "version": "0.2.0"
9
+ }
10
+ },
11
+ "input": {
12
+ "targets": [
13
+ { "kind": "file", "path": "html/index.html" },
14
+ { "kind": "file", "path": "html/contact.html" },
15
+ { "kind": "cli_log", "path": "runs/latest.log" }
16
+ ],
17
+ "capture": {
18
+ "html": { "canonicalize": true, "strip_dynamic": true },
19
+ "dom": { "snapshot": true, "include_css_selectors": true },
20
+ "environment": { "include": ["os", "node", "tool_versions"] }
21
+ },
22
+ "integrity": { "hash": "sha256" },
23
+ "labels": ["a11y", "baseline", "wcag-2.2-aa"]
24
+ }
25
+ }
@@ -0,0 +1,139 @@
1
+ {
2
+ "mcp": {
3
+ "envelope": "mcp.envelope_v0_1",
4
+ "request_id": "req_fixture_diagnose_001",
5
+ "tool": "a11y.diagnose",
6
+ "ok": true
7
+ },
8
+ "result": {
9
+ "diagnosis": {
10
+ "summary": {
11
+ "profile": "wcag-2.2-aa",
12
+ "targets": 2,
13
+ "findings_total": 4,
14
+ "severity_counts": {
15
+ "critical": 0,
16
+ "high": 3,
17
+ "medium": 1,
18
+ "low": 0
19
+ },
20
+ "rules_applied": ["lang", "alt", "button-name", "link-name", "label"]
21
+ },
22
+ "findings": [
23
+ {
24
+ "id": "a11y.lang.missing",
25
+ "wcag": "wcag.3.1.1",
26
+ "severity": "high",
27
+ "message": "Document is missing a lang attribute on <html>.",
28
+ "targets": [
29
+ {
30
+ "artifact_id": "artifact:dom:index",
31
+ "json_pointer": "/nodes/0",
32
+ "selector": "html",
33
+ "snippet": "<html>"
34
+ }
35
+ ],
36
+ "fix": {
37
+ "safe": true,
38
+ "description": "Add lang attribute to the html element",
39
+ "patch": {
40
+ "action": "add_attribute",
41
+ "target": "html",
42
+ "value": "lang=\"en\""
43
+ },
44
+ "wcag_ref": "https://www.w3.org/WAI/WCAG22/Understanding/language-of-page.html"
45
+ }
46
+ },
47
+ {
48
+ "id": "a11y.img.missing_alt",
49
+ "wcag": "wcag.1.1.1",
50
+ "severity": "high",
51
+ "message": "Image is missing alt attribute.",
52
+ "targets": [
53
+ {
54
+ "artifact_id": "artifact:dom:index",
55
+ "json_pointer": "/nodes/5",
56
+ "selector": "img",
57
+ "snippet": "<img src=\"hero.jpg\">"
58
+ }
59
+ ],
60
+ "fix": {
61
+ "safe": true,
62
+ "description": "Add descriptive alt text or alt=\"\" for decorative images",
63
+ "patch": {
64
+ "action": "add_attribute",
65
+ "target": "img",
66
+ "value": "alt=\"[describe image]\""
67
+ },
68
+ "wcag_ref": "https://www.w3.org/WAI/WCAG22/Understanding/non-text-content.html"
69
+ }
70
+ },
71
+ {
72
+ "id": "a11y.button.missing_name",
73
+ "wcag": "wcag.4.1.2",
74
+ "severity": "high",
75
+ "message": "Button has no accessible name.",
76
+ "targets": [
77
+ {
78
+ "artifact_id": "artifact:dom:contact",
79
+ "json_pointer": "/nodes/12",
80
+ "selector": "button",
81
+ "snippet": "<button><i class=\"icon-send\"></i></button>"
82
+ }
83
+ ],
84
+ "fix": {
85
+ "safe": true,
86
+ "description": "Add aria-label or visible text content",
87
+ "patch": {
88
+ "action": "add_attribute",
89
+ "target": "button",
90
+ "value": "aria-label=\"Send message\""
91
+ },
92
+ "wcag_ref": "https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html"
93
+ }
94
+ },
95
+ {
96
+ "id": "a11y.input.missing_label",
97
+ "wcag": "wcag.1.3.1",
98
+ "severity": "medium",
99
+ "message": "Form input has no associated label.",
100
+ "targets": [
101
+ {
102
+ "artifact_id": "artifact:dom:contact",
103
+ "json_pointer": "/nodes/8",
104
+ "selector": "input[type=\"email\"]",
105
+ "snippet": "<input type=\"email\" name=\"email\">"
106
+ }
107
+ ],
108
+ "fix": {
109
+ "safe": true,
110
+ "description": "Add label element with for attribute or aria-label",
111
+ "patch": {
112
+ "action": "add_attribute",
113
+ "target": "input",
114
+ "value": "aria-label=\"Email address\""
115
+ },
116
+ "wcag_ref": "https://www.w3.org/WAI/WCAG22/Understanding/info-and-relationships.html"
117
+ }
118
+ }
119
+ ],
120
+ "provenance": {
121
+ "record_id": "prov:record:fixture-diag-001",
122
+ "methods": [
123
+ "engine.diagnose.wcag_rules_v0_1",
124
+ "engine.extract.evidence.json_pointer_v0_1",
125
+ "engine.extract.evidence.selector_v0_1",
126
+ "engine.generate.fix_guidance_v0_1"
127
+ ],
128
+ "inputs": [
129
+ "bundle:fixture:9b6d3c01",
130
+ "artifact:dom:index",
131
+ "artifact:dom:contact"
132
+ ],
133
+ "outputs": ["diagnosis:fixture-diag-001"],
134
+ "verified": true,
135
+ "timestamp": "2026-01-27T04:13:10.000Z"
136
+ }
137
+ }
138
+ }
139
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "mcp": {
3
+ "envelope": "mcp.envelope_v0_1",
4
+ "request_id": "req_fixture_diagnose_fail",
5
+ "tool": "a11y.diagnose",
6
+ "ok": false
7
+ },
8
+ "error": {
9
+ "code": "PROVENANCE_VERIFICATION_FAILED",
10
+ "message": "Evidence digest mismatch for artifact:dom:index. Expected sha256:a1b2c3..., got sha256:ffffff...",
11
+ "fix": "Re-run a11y.evidence to recapture evidence, or disable verify_provenance for local debugging."
12
+ }
13
+ }
@@ -0,0 +1,88 @@
1
+ {
2
+ "mcp": {
3
+ "envelope": "mcp.envelope_v0_1",
4
+ "request_id": "req_fixture_evidence_001",
5
+ "tool": "a11y.evidence",
6
+ "ok": true
7
+ },
8
+ "result": {
9
+ "bundle": {
10
+ "bundle_id": "bundle:fixture:9b6d3c01",
11
+ "labels": ["a11y", "baseline", "wcag-2.2-aa"],
12
+ "artifacts": [
13
+ {
14
+ "artifact_id": "artifact:html:index",
15
+ "media_type": "text/html",
16
+ "locator": { "kind": "file", "path": "html/index.html" },
17
+ "size_bytes": 245,
18
+ "digest": {
19
+ "alg": "sha256",
20
+ "hex": "a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd"
21
+ },
22
+ "labels": ["source", "html", "a11y", "baseline", "wcag-2.2-aa"]
23
+ },
24
+ {
25
+ "artifact_id": "artifact:dom:index",
26
+ "media_type": "application/json",
27
+ "locator": { "kind": "derived", "from": "artifact:html:index" },
28
+ "size_bytes": 1024,
29
+ "digest": {
30
+ "alg": "sha256",
31
+ "hex": "b2c3d4e5f67890123456789012345678901234567890123456789012345abcde"
32
+ },
33
+ "labels": ["derived", "dom-snapshot", "a11y", "baseline", "wcag-2.2-aa"]
34
+ },
35
+ {
36
+ "artifact_id": "artifact:html:contact",
37
+ "media_type": "text/html",
38
+ "locator": { "kind": "file", "path": "html/contact.html" },
39
+ "size_bytes": 312,
40
+ "digest": {
41
+ "alg": "sha256",
42
+ "hex": "c3d4e5f678901234567890123456789012345678901234567890123456abcdef"
43
+ },
44
+ "labels": ["source", "html", "a11y", "baseline", "wcag-2.2-aa"]
45
+ },
46
+ {
47
+ "artifact_id": "artifact:dom:contact",
48
+ "media_type": "application/json",
49
+ "locator": { "kind": "derived", "from": "artifact:html:contact" },
50
+ "size_bytes": 1156,
51
+ "digest": {
52
+ "alg": "sha256",
53
+ "hex": "d4e5f6789012345678901234567890123456789012345678901234567abcdef0"
54
+ },
55
+ "labels": ["derived", "dom-snapshot", "a11y", "baseline", "wcag-2.2-aa"]
56
+ }
57
+ ],
58
+ "provenance": {
59
+ "record_id": "prov:record:fixture-001",
60
+ "methods": [
61
+ "engine.capture.html_canonicalize_v0_1",
62
+ "engine.capture.dom_snapshot_v0_1",
63
+ "adapter.integrity.sha256_v0_1",
64
+ "adapter.provenance.record_v0_1"
65
+ ],
66
+ "inputs": ["html/index.html", "html/contact.html"],
67
+ "outputs": [
68
+ "artifact:html:index",
69
+ "artifact:dom:index",
70
+ "artifact:html:contact",
71
+ "artifact:dom:contact"
72
+ ],
73
+ "verified": false,
74
+ "timestamp": "2026-01-27T04:12:00.000Z",
75
+ "agent": {
76
+ "name": "a11y-mcp-tools",
77
+ "version": "0.2.0"
78
+ }
79
+ },
80
+ "environment": {
81
+ "os": { "platform": "win32", "arch": "x64" },
82
+ "node": "v20.10.0",
83
+ "tool_versions": { "a11y-mcp-tools": "0.2.0" }
84
+ },
85
+ "created_at": "2026-01-27T04:12:00.000Z"
86
+ }
87
+ }
88
+ }
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@mcptoolshop/a11y-mcp-tools",
3
+ "version": "0.3.0",
4
+ "description": "MCP tools for accessibility evidence capture and diagnosis",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "a11y": "./bin/cli.js",
8
+ "a11y-mcp": "./bin/server.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node bin/server.js",
12
+ "test": "node --test test/*.test.js test/*.test.mjs"
13
+ },
14
+ "keywords": [
15
+ "mcp",
16
+ "accessibility",
17
+ "a11y",
18
+ "wcag",
19
+ "provenance",
20
+ "evidence",
21
+ "testing",
22
+ "audit"
23
+ ],
24
+ "author": "mcp-tool-shop <64996768+mcp-tool-shop@users.noreply.github.com>",
25
+ "license": "MIT",
26
+ "homepage": "https://github.com/mcp-tool-shop/a11y-mcp-tools#readme",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/mcp-tool-shop/a11y-mcp-tools.git"
30
+ },
31
+ "bugs": {
32
+ "url": "https://github.com/mcp-tool-shop/a11y-mcp-tools/issues"
33
+ },
34
+ "engines": {
35
+ "node": ">=18.0.0"
36
+ },
37
+ "dependencies": {
38
+ "htmlparser2": "^9.1.0"
39
+ },
40
+ "devDependencies": {
41
+ "ajv": "^8.17.1",
42
+ "ajv-formats": "^3.0.1"
43
+ },
44
+ "publishConfig": {
45
+ "access": "public",
46
+ "registry": "https://registry.npmjs.org"
47
+ }
48
+ }
@@ -0,0 +1,197 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * MCP Envelope utilities (v0.1).
5
+ *
6
+ * Wraps tool inputs/outputs in standard MCP envelopes with
7
+ * request IDs, client info, and structured error responses.
8
+ */
9
+
10
+ const crypto = require("crypto");
11
+
12
+ const ENVELOPE_VERSION = "mcp.envelope_v0_1";
13
+
14
+ /**
15
+ * Generate a unique request ID.
16
+ */
17
+ function generateRequestId() {
18
+ const bytes = crypto.randomBytes(12);
19
+ return `req_${bytes.toString("base64url")}`;
20
+ }
21
+
22
+ /**
23
+ * Create a request envelope.
24
+ *
25
+ * @param {string} tool - Tool name
26
+ * @param {Object} input - Tool input
27
+ * @param {Object} [client] - Client info
28
+ * @returns {Object} Request envelope
29
+ */
30
+ function createRequestEnvelope(tool, input, client = null) {
31
+ return {
32
+ mcp: {
33
+ envelope: ENVELOPE_VERSION,
34
+ request_id: generateRequestId(),
35
+ tool,
36
+ ...(client && { client }),
37
+ },
38
+ input,
39
+ };
40
+ }
41
+
42
+ /**
43
+ * Create a success response envelope.
44
+ *
45
+ * @param {string} requestId - Original request ID
46
+ * @param {string} tool - Tool name
47
+ * @param {Object} result - Tool result
48
+ * @returns {Object} Response envelope
49
+ */
50
+ function createResponseEnvelope(requestId, tool, result) {
51
+ return {
52
+ mcp: {
53
+ envelope: ENVELOPE_VERSION,
54
+ request_id: requestId,
55
+ tool,
56
+ ok: true,
57
+ },
58
+ result,
59
+ };
60
+ }
61
+
62
+ /**
63
+ * Create an error response envelope.
64
+ *
65
+ * @param {string} requestId - Original request ID
66
+ * @param {string} tool - Tool name
67
+ * @param {string} code - Error code
68
+ * @param {string} message - Error message
69
+ * @param {string} [fix] - Suggested fix
70
+ * @returns {Object} Error envelope
71
+ */
72
+ function createErrorEnvelope(requestId, tool, code, message, fix = null) {
73
+ return {
74
+ mcp: {
75
+ envelope: ENVELOPE_VERSION,
76
+ request_id: requestId,
77
+ tool,
78
+ ok: false,
79
+ },
80
+ error: {
81
+ code,
82
+ message,
83
+ ...(fix && { fix }),
84
+ },
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Error codes for a11y tools.
90
+ */
91
+ const ERROR_CODES = {
92
+ // General errors
93
+ INVALID_INPUT: "INVALID_INPUT",
94
+ INTERNAL_ERROR: "INTERNAL_ERROR",
95
+
96
+ // Evidence errors
97
+ FILE_NOT_FOUND: "FILE_NOT_FOUND",
98
+ CAPTURE_FAILED: "CAPTURE_FAILED",
99
+
100
+ // Diagnosis errors
101
+ BUNDLE_NOT_FOUND: "BUNDLE_NOT_FOUND",
102
+ ARTIFACT_NOT_FOUND: "ARTIFACT_NOT_FOUND",
103
+ INVALID_BUNDLE: "INVALID_BUNDLE",
104
+
105
+ // Integrity errors
106
+ PROVENANCE_VERIFICATION_FAILED: "PROVENANCE_VERIFICATION_FAILED",
107
+ DIGEST_MISMATCH: "DIGEST_MISMATCH",
108
+
109
+ // Schema errors
110
+ SCHEMA_VALIDATION_FAILED: "SCHEMA_VALIDATION_FAILED",
111
+ };
112
+
113
+ /**
114
+ * Wrap a tool execution with envelope handling.
115
+ *
116
+ * @param {string} tool - Tool name
117
+ * @param {Function} handler - Tool handler function
118
+ * @returns {Function} Wrapped handler
119
+ */
120
+ function withEnvelope(tool, handler) {
121
+ return async (envelope) => {
122
+ const requestId = envelope?.mcp?.request_id || generateRequestId();
123
+
124
+ try {
125
+ // Validate envelope structure
126
+ if (!envelope?.input) {
127
+ return createErrorEnvelope(
128
+ requestId,
129
+ tool,
130
+ ERROR_CODES.INVALID_INPUT,
131
+ "Missing input in request envelope",
132
+ "Wrap your input in { mcp: { ... }, input: { ... } }"
133
+ );
134
+ }
135
+
136
+ // Execute handler
137
+ const result = await handler(envelope.input);
138
+
139
+ // Check for handler errors
140
+ if (result.ok === false) {
141
+ return createErrorEnvelope(
142
+ requestId,
143
+ tool,
144
+ result.error?.code || ERROR_CODES.INTERNAL_ERROR,
145
+ result.error?.message || "Unknown error",
146
+ result.error?.fix
147
+ );
148
+ }
149
+
150
+ // Return success envelope
151
+ return createResponseEnvelope(requestId, tool, result);
152
+ } catch (err) {
153
+ return createErrorEnvelope(
154
+ requestId,
155
+ tool,
156
+ ERROR_CODES.INTERNAL_ERROR,
157
+ err.message,
158
+ "Check tool input and try again"
159
+ );
160
+ }
161
+ };
162
+ }
163
+
164
+ /**
165
+ * Parse incoming request - supports both envelope and raw input.
166
+ *
167
+ * For backwards compatibility, accepts:
168
+ * 1. Full envelope: { mcp: { ... }, input: { ... } }
169
+ * 2. Raw input: { targets: [...], ... }
170
+ *
171
+ * @param {Object} request - Request (envelope or raw)
172
+ * @param {string} tool - Tool name
173
+ * @returns {Object} Normalized envelope
174
+ */
175
+ function normalizeRequest(request, tool) {
176
+ // Already an envelope
177
+ if (request?.mcp?.envelope) {
178
+ return request;
179
+ }
180
+
181
+ // Raw input - wrap in envelope
182
+ return createRequestEnvelope(tool, request, {
183
+ name: "a11y-mcp-tools",
184
+ version: "0.1.0",
185
+ });
186
+ }
187
+
188
+ module.exports = {
189
+ ENVELOPE_VERSION,
190
+ ERROR_CODES,
191
+ generateRequestId,
192
+ createRequestEnvelope,
193
+ createResponseEnvelope,
194
+ createErrorEnvelope,
195
+ withEnvelope,
196
+ normalizeRequest,
197
+ };
package/src/index.js ADDED
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+
3
+ const tools = require("./tools/index.js");
4
+ const schemas = require("./schemas/index.js");
5
+
6
+ module.exports = {
7
+ ...tools,
8
+ schemas,
9
+ };
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Artifact schema and utilities.
5
+ *
6
+ * An artifact is a captured piece of content with:
7
+ * - Unique ID
8
+ * - Media type
9
+ * - Locator (where it came from)
10
+ * - Size and digest
11
+ * - Labels for categorization
12
+ */
13
+
14
+ const crypto = require("crypto");
15
+
16
+ /**
17
+ * Create an artifact object.
18
+ *
19
+ * @param {Object} params
20
+ * @param {string} params.id - Artifact ID (e.g., "artifact:html:index")
21
+ * @param {string} params.mediaType - MIME type
22
+ * @param {Object} params.locator - { kind: "file"|"derived"|"url", path|from|url }
23
+ * @param {Buffer|string} params.content - Raw content for hashing
24
+ * @param {string[]} [params.labels] - Optional labels
25
+ * @returns {Object} Artifact object
26
+ */
27
+ function createArtifact({ id, mediaType, locator, content, labels = [] }) {
28
+ const contentBuffer = Buffer.isBuffer(content)
29
+ ? content
30
+ : Buffer.from(content, "utf8");
31
+
32
+ const digest = crypto
33
+ .createHash("sha256")
34
+ .update(contentBuffer)
35
+ .digest("hex");
36
+
37
+ return {
38
+ artifact_id: id,
39
+ media_type: mediaType,
40
+ locator,
41
+ size_bytes: contentBuffer.length,
42
+ digest: {
43
+ alg: "sha256",
44
+ hex: digest,
45
+ },
46
+ labels,
47
+ };
48
+ }
49
+
50
+ /**
51
+ * Generate an artifact ID from kind and name.
52
+ *
53
+ * @param {string} kind - e.g., "html", "dom", "log"
54
+ * @param {string} name - e.g., "index", "contact"
55
+ * @returns {string} Artifact ID
56
+ */
57
+ function artifactId(kind, name) {
58
+ return `artifact:${kind}:${name}`;
59
+ }
60
+
61
+ /**
62
+ * Verify an artifact's digest.
63
+ *
64
+ * @param {Object} artifact - Artifact object
65
+ * @param {Buffer|string} content - Content to verify
66
+ * @returns {boolean}
67
+ */
68
+ function verifyArtifact(artifact, content) {
69
+ const contentBuffer = Buffer.isBuffer(content)
70
+ ? content
71
+ : Buffer.from(content, "utf8");
72
+
73
+ const computed = crypto
74
+ .createHash("sha256")
75
+ .update(contentBuffer)
76
+ .digest("hex");
77
+
78
+ return computed === artifact.digest.hex;
79
+ }
80
+
81
+ module.exports = {
82
+ createArtifact,
83
+ artifactId,
84
+ verifyArtifact,
85
+ };