@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.
- package/.github/workflows/ci.yml +53 -0
- package/CODE_OF_CONDUCT.md +129 -0
- package/CONTRIBUTING.md +136 -0
- package/LICENSE +21 -0
- package/PROV_METHODS_CATALOG.md +104 -0
- package/README.md +168 -0
- package/bin/cli.js +452 -0
- package/bin/server.js +244 -0
- package/fixtures/requests/a11y.diagnose.ok.json +27 -0
- package/fixtures/requests/a11y.evidence.ok.json +25 -0
- package/fixtures/responses/a11y.diagnose.ok.json +139 -0
- package/fixtures/responses/a11y.diagnose.provenance_fail.json +13 -0
- package/fixtures/responses/a11y.evidence.ok.json +88 -0
- package/package.json +48 -0
- package/src/envelope.js +197 -0
- package/src/index.js +9 -0
- package/src/schemas/artifact.js +85 -0
- package/src/schemas/diagnosis.schema.v0.1.json +137 -0
- package/src/schemas/envelope.schema.v0.1.json +108 -0
- package/src/schemas/evidence.bundle.schema.v0.1.json +129 -0
- package/src/schemas/evidence.js +97 -0
- package/src/schemas/index.js +11 -0
- package/src/schemas/provenance.js +140 -0
- package/src/schemas/tools/a11y.diagnose.request.schema.v0.1.json +77 -0
- package/src/schemas/tools/a11y.diagnose.response.schema.v0.1.json +50 -0
- package/src/schemas/tools/a11y.evidence.request.schema.v0.1.json +120 -0
- package/src/schemas/tools/a11y.evidence.response.schema.v0.1.json +50 -0
- package/src/tools/diagnose.js +597 -0
- package/src/tools/evidence.js +481 -0
- package/src/tools/index.js +10 -0
- package/test/contract.test.mjs +154 -0
- package/test/diagnose.test.js +485 -0
- package/test/evidence.test.js +183 -0
- 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
|
+
}
|
package/src/envelope.js
ADDED
|
@@ -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,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
|
+
};
|