arc-templated-issues-mcp 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/README.md ADDED
@@ -0,0 +1,101 @@
1
+ # arc-templated-issues-mcp
2
+
3
+ ARC accessibility issue templates as an MCP server. Converts AxeCore violations into formatted issue templates and provides access to web and native accessibility issue templates.
4
+
5
+ ## Usage
6
+
7
+ Add to your Claude Desktop / Claude Code MCP settings:
8
+
9
+ ```json
10
+ {
11
+ "mcpServers": {
12
+ "arc-issues": {
13
+ "command": "npx",
14
+ "args": ["-y", "arc-templated-issues-mcp"]
15
+ }
16
+ }
17
+ }
18
+ ```
19
+
20
+ ## Tools
21
+
22
+ | Tool | Description |
23
+ |------|-------------|
24
+ | `list_issue_templates` | Lists available accessibility issue template files (web and native). |
25
+ | `get_issue_template` | Returns the full content of a template by name (`web-issue-templates.md` or `native-issue-templates.md`). |
26
+ | `format_violations` | Converts one or more AxeCore violations into formatted markdown or JSON issue templates. |
27
+ | `validate_issue` | Validates that a formatted issue contains all required sections. |
28
+
29
+ ### `format_violations`
30
+
31
+ Accepts standard AxeCore violation objects and returns structured issue templates ready for filing.
32
+
33
+ **Parameters:**
34
+
35
+ | Parameter | Type | Required | Description |
36
+ |-----------|------|----------|-------------|
37
+ | `violations` | object or array | Yes | Single AxeCore violation or array of violations |
38
+ | `context` | object | No | Page context: `url`, `browser`, `operatingSystem`, `buildVersion`, `stepsToReproduce` |
39
+ | `outputFormat` | `"markdown"` \| `"json"` | No | Defaults to `"markdown"` |
40
+
41
+ **Example input:**
42
+ ```json
43
+ {
44
+ "violations": [{
45
+ "id": "button-name",
46
+ "impact": "critical",
47
+ "tags": ["wcag412", "wcag244"],
48
+ "description": "Ensures buttons have discernible text",
49
+ "help": "Buttons must have discernible text",
50
+ "helpUrl": "https://dequeuniversity.com/rules/axe/4.10/button-name",
51
+ "nodes": [{
52
+ "html": "<button></button>",
53
+ "target": [".cta-button"],
54
+ "failureSummary": "Element does not have inner text that is visible to screen readers"
55
+ }]
56
+ }],
57
+ "context": { "url": "https://example.com/checkout" }
58
+ }
59
+ ```
60
+
61
+ ## Templates
62
+
63
+ Templates are sourced from the [`arc-templated-issues`](https://gitlab.com/tmobile/arc/arc-templated-issues) npm package.
64
+
65
+ | Template | Platform | Contents |
66
+ |----------|----------|----------|
67
+ | `web-issue-templates.md` | Web | Control labeling, heading structure, images, keyboard access, link text, contrast, focus management, semantic HTML |
68
+ | `native-issue-templates.md` | Native (iOS/Android) | Button labels, focus order, heading traits, decorative images, grouping, state communication |
69
+
70
+ ## Project Structure
71
+
72
+ ```
73
+ src/
74
+ index.js MCP server (stdio transport)
75
+ tools.js Tool definitions and handlers
76
+ data/
77
+ template-index.json Routing — maps template names to paths within arc-templated-issues
78
+ ```
79
+
80
+ ## Development
81
+
82
+ ```bash
83
+ git clone <repo-url>
84
+ npm install
85
+ npm start
86
+ ```
87
+
88
+ ## npm scripts
89
+
90
+ | Script | Description |
91
+ |--------|-------------|
92
+ | `npm start` | Start the MCP server |
93
+ | `npm run release:patch` | Bump patch version and publish to npm |
94
+ | `npm run release:minor` | Bump minor version and publish to npm |
95
+ | `npm run release:major` | Bump major version and publish to npm |
96
+
97
+ ## Learn More
98
+
99
+ - [Model Context Protocol](https://modelcontextprotocol.io/)
100
+ - [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk)
101
+ - [ARC Accessibility Issue Templates](https://gitlab.com/tmobile/arc/arc-templated-issues)
@@ -0,0 +1,20 @@
1
+ {
2
+ "version": "1.0.0",
3
+ "description": "Routes accessibility issue template requests to markdown source files",
4
+ "packageName": "arc-templated-issues",
5
+ "templates": [
6
+ {
7
+ "name": "web-issue-templates.md",
8
+ "platform": "web",
9
+ "path": "public/web-issue-templates.md",
10
+ "description": "Web accessibility issue templates (AxeCore-compatible)"
11
+ },
12
+ {
13
+ "name": "native-issue-templates.md",
14
+ "platform": "native",
15
+ "path": "public/native-issue-templates.md",
16
+ "description": "Native iOS/Android accessibility issue templates"
17
+ }
18
+ ],
19
+ "defaultTemplate": "web-issue-templates.md"
20
+ }
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "arc-templated-issues-mcp",
3
+ "version": "1.0.0",
4
+ "description": "ARC accessibility issue templates MCP server",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "bin": {
8
+ "arc-templated-issues-mcp": "src/index.js"
9
+ },
10
+ "files": [
11
+ "src/",
12
+ "data/template-index.json"
13
+ ],
14
+ "scripts": {
15
+ "start": "node src/index.js",
16
+ "dev": "node src/index.js",
17
+ "release:patch": "npm version patch && npm publish",
18
+ "release:minor": "npm version minor && npm publish",
19
+ "release:major": "npm version major && npm publish"
20
+ },
21
+ "dependencies": {
22
+ "@modelcontextprotocol/sdk": "^1.0.4",
23
+ "arc-templated-issues": "^1.0.0"
24
+ },
25
+ "engines": {
26
+ "node": ">=18.0.0"
27
+ }
28
+ }
package/src/index.js ADDED
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
5
+ import { tools } from './tools.js';
6
+
7
+ /**
8
+ * Create and configure the MCP server for local (stdio) use
9
+ */
10
+ const server = new Server(
11
+ {
12
+ name: 'arc-templated-issues-mcp',
13
+ version: '1.0.0',
14
+ description: 'ARC accessibility issue templates MCP server — formats AxeCore violations and provides web/native issue templates',
15
+ },
16
+ {
17
+ capabilities: {
18
+ tools: {},
19
+ },
20
+ }
21
+ );
22
+
23
+ /**
24
+ * Handler for listing available tools
25
+ */
26
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
27
+ return {
28
+ tools: tools.map(tool => ({
29
+ name: tool.name,
30
+ description: tool.description,
31
+ inputSchema: tool.inputSchema
32
+ }))
33
+ };
34
+ });
35
+
36
+ /**
37
+ * Handler for calling tools
38
+ */
39
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
40
+ const tool = tools.find(t => t.name === request.params.name);
41
+
42
+ if (!tool) {
43
+ throw new Error(`Unknown tool: ${request.params.name}`);
44
+ }
45
+
46
+ // Execute tool handler
47
+ return await tool.handler(request.params.arguments);
48
+ });
49
+
50
+ /**
51
+ * Start the server with stdio transport
52
+ */
53
+ async function main() {
54
+ const transport = new StdioServerTransport();
55
+ await server.connect(transport);
56
+ console.error('arc-templated-issues-mcp MCP server running on stdio');
57
+ }
58
+
59
+ main().catch((error) => {
60
+ console.error('Fatal error in main():', error);
61
+ process.exit(1);
62
+ });
package/src/tools.js ADDED
@@ -0,0 +1,342 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { resolve, dirname } from 'node:path';
3
+ import { fileURLToPath, pathToFileURL } from 'node:url';
4
+ import { createRequire } from 'node:module';
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const DATA_DIR = resolve(__dirname, '../data');
8
+ const require = createRequire(import.meta.url);
9
+
10
+ async function loadIndex() {
11
+ const raw = await readFile(resolve(DATA_DIR, 'template-index.json'), 'utf8');
12
+ return JSON.parse(raw);
13
+ }
14
+
15
+ function resolvePackageDir(packageName) {
16
+ const pkgJson = require.resolve(`${packageName}/package.json`);
17
+ return dirname(pkgJson);
18
+ }
19
+
20
+ async function readTemplate(entry, packageName) {
21
+ const pkgDir = resolvePackageDir(packageName);
22
+ return readFile(resolve(pkgDir, entry.path), 'utf8');
23
+ }
24
+
25
+ function textResponse(text) {
26
+ return { content: [{ type: 'text', text }] };
27
+ }
28
+
29
+ // AxeCore impact → severity/priority
30
+ const IMPACT_MAP = {
31
+ critical: { severity: '1-Critical', priority: 'Blocker' },
32
+ serious: { severity: '2-Severe', priority: 'High' },
33
+ moderate: { severity: '3-Average', priority: 'Medium' },
34
+ minor: { severity: '4-Low', priority: 'Low' },
35
+ };
36
+
37
+ // AxeCore WCAG tag strings → human-readable criteria
38
+ const WCAG_TAGS = {
39
+ wcag111: '1.1.1 (Level A)',
40
+ wcag121: '1.2.1 (Level A)',
41
+ wcag122: '1.2.2 (Level A)',
42
+ wcag123: '1.2.3 (Level A)',
43
+ wcag124: '1.2.4 (Level AA)',
44
+ wcag125: '1.2.5 (Level AA)',
45
+ wcag131: '1.3.1 (Level A)',
46
+ wcag132: '1.3.2 (Level A)',
47
+ wcag133: '1.3.3 (Level A)',
48
+ wcag134: '1.3.4 (Level AA)',
49
+ wcag135: '1.3.5 (Level AA)',
50
+ wcag141: '1.4.1 (Level A)',
51
+ wcag142: '1.4.2 (Level A)',
52
+ wcag143: '1.4.3 (Level AA)',
53
+ wcag144: '1.4.4 (Level AA)',
54
+ wcag145: '1.4.5 (Level AA)',
55
+ wcag1410: '1.4.10 (Level AA)',
56
+ wcag1411: '1.4.11 (Level AA)',
57
+ wcag1412: '1.4.12 (Level AA)',
58
+ wcag1413: '1.4.13 (Level AA)',
59
+ wcag211: '2.1.1 (Level A)',
60
+ wcag212: '2.1.2 (Level A)',
61
+ wcag214: '2.1.4 (Level A)',
62
+ wcag221: '2.2.1 (Level A)',
63
+ wcag222: '2.2.2 (Level A)',
64
+ wcag231: '2.3.1 (Level A)',
65
+ wcag241: '2.4.1 (Level A)',
66
+ wcag242: '2.4.2 (Level A)',
67
+ wcag243: '2.4.3 (Level A)',
68
+ wcag244: '2.4.4 (Level A)',
69
+ wcag245: '2.4.5 (Level AA)',
70
+ wcag246: '2.4.6 (Level AA)',
71
+ wcag247: '2.4.7 (Level AA)',
72
+ wcag248: '2.4.8 (Level AAA)',
73
+ wcag249: '2.4.9 (Level AAA)',
74
+ wcag251: '2.5.1 (Level A)',
75
+ wcag252: '2.5.2 (Level A)',
76
+ wcag253: '2.5.3 (Level A)',
77
+ wcag254: '2.5.4 (Level A)',
78
+ wcag311: '3.1.1 (Level A)',
79
+ wcag312: '3.1.2 (Level AA)',
80
+ wcag321: '3.2.1 (Level A)',
81
+ wcag322: '3.2.2 (Level A)',
82
+ wcag323: '3.2.3 (Level AA)',
83
+ wcag324: '3.2.4 (Level AA)',
84
+ wcag331: '3.3.1 (Level A)',
85
+ wcag332: '3.3.2 (Level A)',
86
+ wcag333: '3.3.3 (Level AA)',
87
+ wcag334: '3.3.4 (Level AA)',
88
+ wcag411: '4.1.1 (Level A)',
89
+ wcag412: '4.1.2 (Level A)',
90
+ wcag413: '4.1.3 (Level AA)',
91
+ };
92
+
93
+ function parseWcagTags(tags) {
94
+ return tags
95
+ .filter(t => t.startsWith('wcag') && /\d/.test(t))
96
+ .map(t => WCAG_TAGS[t])
97
+ .filter(Boolean);
98
+ }
99
+
100
+ function extractElementType(html) {
101
+ const match = html && html.match(/^<([a-z][a-z0-9-]*)/i);
102
+ return match ? match[1].toLowerCase() : 'unknown';
103
+ }
104
+
105
+ function formatViolationAsMarkdown(violation, node, context) {
106
+ const impact = (violation.impact || 'moderate').toLowerCase();
107
+ const { severity, priority } = IMPACT_MAP[impact] || IMPACT_MAP.moderate;
108
+ const wcagCriteria = parseWcagTags(violation.tags || []);
109
+ const elementType = extractElementType(node.html);
110
+ const selector = Array.isArray(node.target) ? node.target.join(', ') : (node.target || '');
111
+
112
+ const lines = [
113
+ `## ${violation.help || violation.description}`,
114
+ '',
115
+ `**Severity:** ${severity} `,
116
+ `**Priority:** ${priority}`,
117
+ '',
118
+ '### Problem Statement',
119
+ violation.description || '',
120
+ '',
121
+ '### Element',
122
+ `- **Type:** \`${elementType}\``,
123
+ `- **Selector:** \`${selector}\``,
124
+ '',
125
+ '### Code Reference',
126
+ '```html',
127
+ node.html || '',
128
+ '```',
129
+ '',
130
+ '### Failure Summary',
131
+ node.failureSummary || '',
132
+ '',
133
+ ];
134
+
135
+ if (wcagCriteria.length > 0) {
136
+ lines.push('### WCAG Criteria');
137
+ wcagCriteria.forEach(c => lines.push(`- ${c}`));
138
+ lines.push('');
139
+ }
140
+
141
+ if (violation.helpUrl) {
142
+ lines.push(`### Resources`);
143
+ lines.push(`- [Axe Rule: ${violation.id}](${violation.helpUrl})`);
144
+ lines.push('');
145
+ }
146
+
147
+ if (context) {
148
+ lines.push('### Environment');
149
+ if (context.url) lines.push(`- **URL:** ${context.url}`);
150
+ if (context.browser) lines.push(`- **Browser:** ${context.browser}`);
151
+ if (context.operatingSystem) lines.push(`- **OS:** ${context.operatingSystem}`);
152
+ if (context.buildVersion) lines.push(`- **Build:** ${context.buildVersion}`);
153
+ if (context.stepsToReproduce) {
154
+ lines.push('');
155
+ lines.push('**Steps to Reproduce:**');
156
+ lines.push(context.stepsToReproduce);
157
+ }
158
+ lines.push('');
159
+ }
160
+
161
+ return lines.join('\n');
162
+ }
163
+
164
+ function formatViolationAsJSON(violation, node, context) {
165
+ const impact = (violation.impact || 'moderate').toLowerCase();
166
+ const { severity, priority } = IMPACT_MAP[impact] || IMPACT_MAP.moderate;
167
+ const wcagCriteria = parseWcagTags(violation.tags || []);
168
+ const elementType = extractElementType(node.html);
169
+ const selector = Array.isArray(node.target) ? node.target.join(', ') : (node.target || '');
170
+
171
+ return {
172
+ ruleId: violation.id,
173
+ severity,
174
+ priority,
175
+ wcagCriteria,
176
+ problemStatement: violation.description || '',
177
+ impact: violation.impact,
178
+ element: {
179
+ type: elementType,
180
+ selector,
181
+ html: node.html || '',
182
+ },
183
+ failureSummary: node.failureSummary || '',
184
+ helpUrl: violation.helpUrl || '',
185
+ ...(context ? { context } : {}),
186
+ };
187
+ }
188
+
189
+ export const tools = [
190
+ {
191
+ name: 'list_issue_templates',
192
+ description: 'Lists all available accessibility issue template files (web and native).',
193
+ inputSchema: {
194
+ type: 'object',
195
+ properties: {},
196
+ required: [],
197
+ },
198
+ handler: async () => {
199
+ const index = await loadIndex();
200
+ const lines = index.templates.map(
201
+ t => `- **${t.name}** (${t.platform}): ${t.description}`
202
+ );
203
+ return textResponse(`Available templates:\n\n${lines.join('\n')}`);
204
+ },
205
+ },
206
+
207
+ {
208
+ name: 'get_issue_template',
209
+ description: 'Retrieves the full content of an accessibility issue template by name.',
210
+ inputSchema: {
211
+ type: 'object',
212
+ properties: {
213
+ templateName: {
214
+ type: 'string',
215
+ description: 'Template filename, e.g. "web-issue-templates.md" or "native-issue-templates.md"',
216
+ },
217
+ },
218
+ required: ['templateName'],
219
+ },
220
+ handler: async (args) => {
221
+ const index = await loadIndex();
222
+ const entry = index.templates.find(t => t.name === args.templateName);
223
+ if (!entry) {
224
+ const names = index.templates.map(t => t.name).join(', ');
225
+ return textResponse(`Template "${args.templateName}" not found. Available: ${names}`);
226
+ }
227
+ const content = await readTemplate(entry, index.packageName);
228
+ return textResponse(content);
229
+ },
230
+ },
231
+
232
+ {
233
+ name: 'format_violations',
234
+ description:
235
+ 'Converts one or more AxeCore accessibility violations into formatted issue templates.',
236
+ inputSchema: {
237
+ type: 'object',
238
+ properties: {
239
+ violations: {
240
+ description:
241
+ 'A single AxeCore violation object or an array of them.',
242
+ oneOf: [
243
+ { type: 'object' },
244
+ { type: 'array', items: { type: 'object' } },
245
+ ],
246
+ },
247
+ context: {
248
+ type: 'object',
249
+ description: 'Optional context about the page/environment.',
250
+ properties: {
251
+ url: { type: 'string' },
252
+ browser: { type: 'string' },
253
+ operatingSystem: { type: 'string' },
254
+ buildVersion: { type: 'string' },
255
+ stepsToReproduce: { type: 'string' },
256
+ },
257
+ },
258
+ outputFormat: {
259
+ type: 'string',
260
+ enum: ['markdown', 'json'],
261
+ description: 'Output format. Defaults to "markdown".',
262
+ },
263
+ },
264
+ required: ['violations'],
265
+ },
266
+ handler: async (args) => {
267
+ const violations = Array.isArray(args.violations)
268
+ ? args.violations
269
+ : [args.violations];
270
+ const context = args.context || null;
271
+ const format = args.outputFormat === 'json' ? 'json' : 'markdown';
272
+
273
+ const results = [];
274
+ for (const violation of violations) {
275
+ const nodes = violation.nodes || [{}];
276
+ for (const node of nodes) {
277
+ if (format === 'json') {
278
+ results.push(formatViolationAsJSON(violation, node, context));
279
+ } else {
280
+ results.push(formatViolationAsMarkdown(violation, node, context));
281
+ }
282
+ }
283
+ }
284
+
285
+ if (format === 'json') {
286
+ return textResponse(JSON.stringify(results, null, 2));
287
+ }
288
+ return textResponse(results.join('\n---\n\n'));
289
+ },
290
+ },
291
+
292
+ {
293
+ name: 'validate_issue',
294
+ description: 'Validates that a formatted accessibility issue contains all required sections.',
295
+ inputSchema: {
296
+ type: 'object',
297
+ properties: {
298
+ issueContent: {
299
+ type: 'string',
300
+ description: 'The formatted issue content (markdown string) to validate.',
301
+ },
302
+ },
303
+ required: ['issueContent'],
304
+ },
305
+ handler: async (args) => {
306
+ const content = args.issueContent || '';
307
+
308
+ const checks = [
309
+ {
310
+ label: 'Severity',
311
+ pass: /\*\*Severity\*\*\s*:/i.test(content) || /severity/i.test(content),
312
+ },
313
+ {
314
+ label: 'Priority',
315
+ pass: /\*\*Priority\*\*\s*:/i.test(content) || /priority/i.test(content),
316
+ },
317
+ {
318
+ label: 'Element or selector reference',
319
+ pass: /selector|element|`[^`]+`/i.test(content),
320
+ },
321
+ {
322
+ label: 'Code block',
323
+ pass: /```/.test(content),
324
+ },
325
+ {
326
+ label: 'Failure or problem description',
327
+ pass: /failure|problem|issue|description/i.test(content),
328
+ },
329
+ ];
330
+
331
+ const failures = checks.filter(c => !c.pass).map(c => c.label);
332
+ const passed = failures.length === 0;
333
+
334
+ if (passed) {
335
+ return textResponse('Validation passed: issue contains all required sections.');
336
+ }
337
+ return textResponse(
338
+ `Validation failed. Missing sections:\n${failures.map(f => `- ${f}`).join('\n')}`
339
+ );
340
+ },
341
+ },
342
+ ];