@wcag-checkr/mcp 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/README.md +108 -0
- package/package.json +35 -0
- package/wcagcheckr-mcp.mjs +327 -0
package/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# @wcag-checkr/mcp
|
|
2
|
+
|
|
3
|
+
**Model Context Protocol** server for wcagcheckr. Lets LLM-IDEs drive accessibility audits, verify forensic receipts, and look up tier features via tool-use.
|
|
4
|
+
|
|
5
|
+
## Why this matters
|
|
6
|
+
|
|
7
|
+
axe DevTools shipped an MCP server in 2026 (paid tier). We ship one for free, with one extra: it audits across our 108-state matrix (hover/focus/dark/RTL/breakpoints), so the LLM gets findings competitors' single-state audits would miss.
|
|
8
|
+
|
|
9
|
+
## Tools exposed
|
|
10
|
+
|
|
11
|
+
| Tool | What it does |
|
|
12
|
+
|---|---|
|
|
13
|
+
| `audit_url` | Run a multi-state accessibility audit on a URL. Returns structured findings (JSON / SARIF / JUnit). |
|
|
14
|
+
| `verify_receipt` | Independently verify a wcagcheckr forensic-anchor receipt's ed25519 signature against the published public key. |
|
|
15
|
+
| `get_tier_config` | Fetch the per-product tier feature flags. |
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install -g @wcag-checkr/mcp @wcag-checkr/ci
|
|
21
|
+
npx playwright install chromium
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Use with Claude Code
|
|
25
|
+
|
|
26
|
+
Add to `~/.claude/mcp.json` (or use `claude mcp add` if available in your version):
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"mcpServers": {
|
|
31
|
+
"wcagcheckr": {
|
|
32
|
+
"command": "wcagcheckr-mcp"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Then in any conversation:
|
|
39
|
+
|
|
40
|
+
> "Audit https://staging.example.com for accessibility issues."
|
|
41
|
+
|
|
42
|
+
Claude will call `audit_url` and return findings, ready for fix-it discussion.
|
|
43
|
+
|
|
44
|
+
## Use with Cursor
|
|
45
|
+
|
|
46
|
+
Add to `~/.cursor/mcp.json`:
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"mcpServers": {
|
|
51
|
+
"wcagcheckr": {
|
|
52
|
+
"command": "wcagcheckr-mcp"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Use with Continue
|
|
59
|
+
|
|
60
|
+
Add to your Continue config under `mcpServers`:
|
|
61
|
+
|
|
62
|
+
```json
|
|
63
|
+
{
|
|
64
|
+
"mcpServers": [
|
|
65
|
+
{
|
|
66
|
+
"name": "wcagcheckr",
|
|
67
|
+
"command": "wcagcheckr-mcp"
|
|
68
|
+
}
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Environment variables
|
|
74
|
+
|
|
75
|
+
| Var | Default | Purpose |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| `WCAGCHECKR_SERVER_URL` | `https://api.wcagcheckr.com` | Override the server base URL (e.g. for self-hosted instance) |
|
|
78
|
+
| `WCAGCHECKR_PRODUCT_SLUG` | `wcagcheckr` | Override the product slug |
|
|
79
|
+
| `WCAGCHECKR_CI_PATH` | bundled | Override the path to the CI runner (used by `audit_url`) |
|
|
80
|
+
|
|
81
|
+
## Examples — what an LLM can do with this
|
|
82
|
+
|
|
83
|
+
**Auditing a deployment preview**
|
|
84
|
+
> User: "Audit my Vercel preview at https://my-site-pr-42.vercel.app — fail the build if there are any serious violations."
|
|
85
|
+
>
|
|
86
|
+
> Claude calls `audit_url` with `threshold: serious`, gets back JSON, summarizes findings, and tells the user the exit code mapping.
|
|
87
|
+
|
|
88
|
+
**Validating a forensic receipt**
|
|
89
|
+
> User: *pastes a forensic-log entry copied from a defense bundle*
|
|
90
|
+
>
|
|
91
|
+
> "Is this receipt actually from wcagcheckr?"
|
|
92
|
+
>
|
|
93
|
+
> Claude calls `verify_receipt`, gets a "✓ valid signature" response, explains what was verified (and what wasn't — i.e., directs the user to `openssl ts -verify` for the TSA token).
|
|
94
|
+
|
|
95
|
+
**Tier-feature questions**
|
|
96
|
+
> User: "Does the Solo plan include forensic anchoring?"
|
|
97
|
+
>
|
|
98
|
+
> Claude calls `get_tier_config`, finds `features.forensicAnchoring: true` for the solo plan, answers.
|
|
99
|
+
|
|
100
|
+
## Limitations (v0)
|
|
101
|
+
|
|
102
|
+
- `audit_url` shells out to `@wcag-checkr/ci` which spawns Chromium with the loaded extension. Heavy — single audits take 15-60s. Don't expect sub-second response.
|
|
103
|
+
- `verify_receipt` only verifies our ed25519 signature. Full RFC 3161 timestamp verification against FreeTSA's CA chain is intentionally out of scope (use `openssl ts -verify` for that — see `https://api.wcagcheckr.com/verify` for openssl-command instructions tailored to a specific receipt).
|
|
104
|
+
- No streaming. Audits report results when done, not incrementally.
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
UNLICENSED until commercial release. See `wcagcheckr.com/license`.
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wcag-checkr/mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Model Context Protocol server for wcagcheckr. Lets LLM-IDEs (Claude Code, Cursor, Continue, etc.) drive accessibility audits, verify forensic receipts, and look up tier features programmatically. Same audit engine as the Chrome extension and CI runner.",
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "wcagcheckr-mcp.mjs",
|
|
9
|
+
"bin": {
|
|
10
|
+
"wcagcheckr-mcp": "./wcagcheckr-mcp.mjs"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"wcagcheckr-mcp.mjs",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=20.18"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
21
|
+
"@wcag-checkr/ci": "^0.1.0"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://wcagcheckr.com",
|
|
24
|
+
"keywords": [
|
|
25
|
+
"mcp",
|
|
26
|
+
"model-context-protocol",
|
|
27
|
+
"accessibility",
|
|
28
|
+
"a11y",
|
|
29
|
+
"wcag",
|
|
30
|
+
"audit",
|
|
31
|
+
"claude",
|
|
32
|
+
"cursor",
|
|
33
|
+
"ai-tools"
|
|
34
|
+
]
|
|
35
|
+
}
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// wcagcheckr — Model Context Protocol server.
|
|
3
|
+
//
|
|
4
|
+
// Exposes wcagcheckr capabilities as MCP tools so LLM-driven IDEs (Claude Code,
|
|
5
|
+
// Cursor, Continue, etc.) can call them via tool-use. Same audit engine as the
|
|
6
|
+
// Chrome extension and the CI npm package — this is just the JSON-RPC wrapper.
|
|
7
|
+
//
|
|
8
|
+
// Tools exposed:
|
|
9
|
+
// audit_url — run a full-page audit on a URL, return structured findings
|
|
10
|
+
// verify_receipt — verify a forensic receipt against our published public key
|
|
11
|
+
// get_tier_config — fetch the per-product tier feature flags
|
|
12
|
+
// list_violations — list violations from the most recent audit, filterable
|
|
13
|
+
//
|
|
14
|
+
// Transport: stdio (the MCP standard for local servers). Wired into Claude
|
|
15
|
+
// Code via `~/.claude/mcp.json` or invoked directly by `claude mcp add` etc.
|
|
16
|
+
//
|
|
17
|
+
// See README.md for setup instructions per IDE.
|
|
18
|
+
|
|
19
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
20
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
21
|
+
import {
|
|
22
|
+
CallToolRequestSchema,
|
|
23
|
+
ListToolsRequestSchema,
|
|
24
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
25
|
+
import { spawn } from 'node:child_process';
|
|
26
|
+
import { fileURLToPath } from 'node:url';
|
|
27
|
+
import { resolve, dirname, join } from 'node:path';
|
|
28
|
+
import { existsSync } from 'node:fs';
|
|
29
|
+
|
|
30
|
+
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)));
|
|
31
|
+
// CI runner — sibling package, or fall back to bundled
|
|
32
|
+
const CI_RUNNER_PATH =
|
|
33
|
+
process.env.WCAGCHECKR_CI_PATH ??
|
|
34
|
+
resolve(ROOT, '..', 'cli', 'wcagcheckr-ci.mjs');
|
|
35
|
+
|
|
36
|
+
const SERVER_BASE_URL = process.env.WCAGCHECKR_SERVER_URL ?? 'https://api.wcagcheckr.com';
|
|
37
|
+
const PRODUCT_SLUG = process.env.WCAGCHECKR_PRODUCT_SLUG ?? 'wcagcheckr';
|
|
38
|
+
|
|
39
|
+
const server = new Server(
|
|
40
|
+
{
|
|
41
|
+
name: 'wcagcheckr',
|
|
42
|
+
version: '0.1.0',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
capabilities: {
|
|
46
|
+
tools: {},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
// ─── Tool definitions ────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
const TOOLS = [
|
|
54
|
+
{
|
|
55
|
+
name: 'audit_url',
|
|
56
|
+
description:
|
|
57
|
+
'Run a wcagcheckr accessibility audit on a URL. Audits across the multi-state matrix (hover, focus, dark mode, RTL, breakpoints) and returns structured findings. Use this when the user asks to check accessibility of a page, find WCAG violations, or audit a deployment preview.',
|
|
58
|
+
inputSchema: {
|
|
59
|
+
type: 'object',
|
|
60
|
+
properties: {
|
|
61
|
+
url: {
|
|
62
|
+
type: 'string',
|
|
63
|
+
description: 'Fully-qualified URL to audit. Must be reachable from the runner host.',
|
|
64
|
+
},
|
|
65
|
+
format: {
|
|
66
|
+
type: 'string',
|
|
67
|
+
enum: ['json', 'sarif', 'junit'],
|
|
68
|
+
description: 'Output format. Default: json.',
|
|
69
|
+
},
|
|
70
|
+
threshold: {
|
|
71
|
+
type: 'string',
|
|
72
|
+
enum: ['none', 'critical', 'serious', 'moderate', 'minor'],
|
|
73
|
+
description:
|
|
74
|
+
'Severity threshold for the success/failure assessment. Default: serious.',
|
|
75
|
+
},
|
|
76
|
+
timeout_ms: {
|
|
77
|
+
type: 'number',
|
|
78
|
+
description: 'Audit timeout in milliseconds. Default: 120000.',
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
required: ['url'],
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: 'verify_receipt',
|
|
86
|
+
description:
|
|
87
|
+
'Independently verify a wcagcheckr forensic-anchor receipt. Confirms the ed25519 signature is valid against the published public key + the receipt fields are well-formed. Use this when a user provides a forensic receipt and wants to confirm authenticity (e.g., received as part of a defense bundle from a vendor).',
|
|
88
|
+
inputSchema: {
|
|
89
|
+
type: 'object',
|
|
90
|
+
properties: {
|
|
91
|
+
receipt: {
|
|
92
|
+
type: 'object',
|
|
93
|
+
description:
|
|
94
|
+
'The forensic-log entry containing { hash, componentId, pageUrl, capturedAt, receipt: { schemaVersion, anchoredAt, tsaName, rfc3161TokenBase64, serverSignatureBase64, serverKeyFingerprint } }.',
|
|
95
|
+
},
|
|
96
|
+
product_slug: {
|
|
97
|
+
type: 'string',
|
|
98
|
+
description: `Product slug. Default: ${PRODUCT_SLUG}.`,
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
required: ['receipt'],
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: 'get_tier_config',
|
|
106
|
+
description:
|
|
107
|
+
'Fetch the wcagcheckr per-tier feature flags for a product. Use this to answer questions about what each pricing tier (free / solo / team) includes.',
|
|
108
|
+
inputSchema: {
|
|
109
|
+
type: 'object',
|
|
110
|
+
properties: {
|
|
111
|
+
product_slug: {
|
|
112
|
+
type: 'string',
|
|
113
|
+
description: `Product slug. Default: ${PRODUCT_SLUG}.`,
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
121
|
+
tools: TOOLS,
|
|
122
|
+
}));
|
|
123
|
+
|
|
124
|
+
// ─── Tool implementations ────────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
async function auditUrl(args) {
|
|
127
|
+
const { url, format = 'json', threshold = 'serious', timeout_ms = 120_000 } = args;
|
|
128
|
+
if (!url) {
|
|
129
|
+
return { isError: true, content: [{ type: 'text', text: 'Missing required field: url' }] };
|
|
130
|
+
}
|
|
131
|
+
if (!existsSync(CI_RUNNER_PATH)) {
|
|
132
|
+
return {
|
|
133
|
+
isError: true,
|
|
134
|
+
content: [
|
|
135
|
+
{
|
|
136
|
+
type: 'text',
|
|
137
|
+
text: `wcagcheckr CI runner not found at ${CI_RUNNER_PATH}. Set WCAGCHECKR_CI_PATH or install @wcag-checkr/ci.`,
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return new Promise((resolveResult) => {
|
|
144
|
+
const args = [
|
|
145
|
+
CI_RUNNER_PATH,
|
|
146
|
+
'audit',
|
|
147
|
+
url,
|
|
148
|
+
'--format', format,
|
|
149
|
+
'--threshold', threshold,
|
|
150
|
+
'--timeout', String(timeout_ms),
|
|
151
|
+
'--quiet',
|
|
152
|
+
];
|
|
153
|
+
const child = spawn(process.execPath, args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
154
|
+
let stdout = '';
|
|
155
|
+
let stderr = '';
|
|
156
|
+
child.stdout.on('data', (chunk) => (stdout += chunk.toString('utf8')));
|
|
157
|
+
child.stderr.on('data', (chunk) => (stderr += chunk.toString('utf8')));
|
|
158
|
+
child.on('error', (err) =>
|
|
159
|
+
resolveResult({
|
|
160
|
+
isError: true,
|
|
161
|
+
content: [{ type: 'text', text: `Failed to spawn auditor: ${err.message}` }],
|
|
162
|
+
}),
|
|
163
|
+
);
|
|
164
|
+
child.on('close', (code) => {
|
|
165
|
+
const summary = code === 0
|
|
166
|
+
? `audit completed; ${threshold}-threshold not exceeded`
|
|
167
|
+
: code === 1
|
|
168
|
+
? `audit completed; ${threshold}-threshold exceeded — failures detected`
|
|
169
|
+
: `audit runtime error (exit ${code})`;
|
|
170
|
+
resolveResult({
|
|
171
|
+
content: [
|
|
172
|
+
{ type: 'text', text: summary },
|
|
173
|
+
{ type: 'text', text: stdout },
|
|
174
|
+
...(stderr ? [{ type: 'text', text: `stderr:\n${stderr}` }] : []),
|
|
175
|
+
],
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function verifyReceipt(args) {
|
|
182
|
+
const { receipt: entry, product_slug = PRODUCT_SLUG } = args;
|
|
183
|
+
if (!entry || typeof entry !== 'object') {
|
|
184
|
+
return { isError: true, content: [{ type: 'text', text: 'Missing or invalid receipt object' }] };
|
|
185
|
+
}
|
|
186
|
+
const { hash, receipt } = entry;
|
|
187
|
+
if (!hash || !receipt) {
|
|
188
|
+
return {
|
|
189
|
+
isError: true,
|
|
190
|
+
content: [{ type: 'text', text: 'receipt must contain { hash, receipt: { ... } }' }],
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
if (receipt.schemaVersion !== 1) {
|
|
194
|
+
return {
|
|
195
|
+
content: [{ type: 'text', text: `Unsupported receipt schemaVersion: ${receipt.schemaVersion}` }],
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Fetch published public key
|
|
200
|
+
let pubKeyData;
|
|
201
|
+
try {
|
|
202
|
+
const res = await fetch(`${SERVER_BASE_URL}/v1/products/${product_slug}/forensic/public-key`);
|
|
203
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
204
|
+
pubKeyData = await res.json();
|
|
205
|
+
} catch (err) {
|
|
206
|
+
return {
|
|
207
|
+
content: [{ type: 'text', text: `Failed to fetch public key: ${err.message}` }],
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (pubKeyData.fingerprint !== receipt.serverKeyFingerprint) {
|
|
212
|
+
return {
|
|
213
|
+
content: [
|
|
214
|
+
{
|
|
215
|
+
type: 'text',
|
|
216
|
+
text: `Fingerprint mismatch — receipt was issued under a different (rotated) key. Receipt: ${receipt.serverKeyFingerprint?.slice(0, 16)}… Currently published: ${pubKeyData.fingerprint?.slice(0, 16)}…`,
|
|
217
|
+
},
|
|
218
|
+
],
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Verify ed25519 signature using Node's WebCrypto
|
|
223
|
+
const canonicalJson = (value) => {
|
|
224
|
+
if (value === null || typeof value !== 'object') return JSON.stringify(value);
|
|
225
|
+
if (Array.isArray(value)) return '[' + value.map(canonicalJson).join(',') + ']';
|
|
226
|
+
const keys = Object.keys(value).sort();
|
|
227
|
+
return '{' + keys.map((k) => JSON.stringify(k) + ':' + canonicalJson(value[k])).join(',') + '}';
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const signedPayload = {
|
|
231
|
+
hash,
|
|
232
|
+
anchoredAt: receipt.anchoredAt,
|
|
233
|
+
tsaName: receipt.tsaName,
|
|
234
|
+
productSlug: product_slug,
|
|
235
|
+
};
|
|
236
|
+
const canonical = canonicalJson(signedPayload);
|
|
237
|
+
const canonicalBytes = new TextEncoder().encode(canonical);
|
|
238
|
+
|
|
239
|
+
const b64ToBytes = (b64) => {
|
|
240
|
+
const bin = Buffer.from(b64, 'base64');
|
|
241
|
+
return new Uint8Array(bin);
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
let pubKey;
|
|
245
|
+
try {
|
|
246
|
+
pubKey = await crypto.subtle.importKey(
|
|
247
|
+
'spki',
|
|
248
|
+
b64ToBytes(pubKeyData.publicKeyBase64),
|
|
249
|
+
{ name: 'Ed25519' },
|
|
250
|
+
false,
|
|
251
|
+
['verify'],
|
|
252
|
+
);
|
|
253
|
+
} catch (err) {
|
|
254
|
+
return { content: [{ type: 'text', text: `Public-key import failed: ${err.message}` }] };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
let sigValid;
|
|
258
|
+
try {
|
|
259
|
+
sigValid = await crypto.subtle.verify(
|
|
260
|
+
'Ed25519',
|
|
261
|
+
pubKey,
|
|
262
|
+
b64ToBytes(receipt.serverSignatureBase64),
|
|
263
|
+
canonicalBytes,
|
|
264
|
+
);
|
|
265
|
+
} catch (err) {
|
|
266
|
+
return { content: [{ type: 'text', text: `Signature verification threw: ${err.message}` }] };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
content: [
|
|
271
|
+
{
|
|
272
|
+
type: 'text',
|
|
273
|
+
text: sigValid
|
|
274
|
+
? `✓ Receipt is VALID. Server ed25519 signature verifies against the published key (fingerprint ${pubKeyData.fingerprint?.slice(0, 16)}…). The receipt was issued by the wcagcheckr server. Note: this verifies OUR signature only — full RFC 3161 timestamp verification against the TSA's CA chain (FreeTSA) is a separate step done via openssl ts -verify.`
|
|
275
|
+
: `✗ Receipt is INVALID. The server signature did NOT verify against the published key. Either the receipt was tampered with, or the entry's hash/anchoredAt/tsaName don't match what was originally signed.`,
|
|
276
|
+
},
|
|
277
|
+
],
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function getTierConfig(args) {
|
|
282
|
+
const { product_slug = PRODUCT_SLUG } = args ?? {};
|
|
283
|
+
try {
|
|
284
|
+
const res = await fetch(`${SERVER_BASE_URL}/v1/products/${product_slug}/tier-config`);
|
|
285
|
+
if (!res.ok) {
|
|
286
|
+
return { content: [{ type: 'text', text: `tier-config HTTP ${res.status}` }] };
|
|
287
|
+
}
|
|
288
|
+
const json = await res.json();
|
|
289
|
+
return {
|
|
290
|
+
content: [
|
|
291
|
+
{ type: 'text', text: JSON.stringify(json, null, 2) },
|
|
292
|
+
],
|
|
293
|
+
};
|
|
294
|
+
} catch (err) {
|
|
295
|
+
return { content: [{ type: 'text', text: `Failed to fetch tier-config: ${err.message}` }] };
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
300
|
+
const { name, arguments: args = {} } = request.params;
|
|
301
|
+
switch (name) {
|
|
302
|
+
case 'audit_url':
|
|
303
|
+
return await auditUrl(args);
|
|
304
|
+
case 'verify_receipt':
|
|
305
|
+
return await verifyReceipt(args);
|
|
306
|
+
case 'get_tier_config':
|
|
307
|
+
return await getTierConfig(args);
|
|
308
|
+
default:
|
|
309
|
+
return {
|
|
310
|
+
isError: true,
|
|
311
|
+
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// ─── Boot ────────────────────────────────────────────────────────────────────
|
|
317
|
+
|
|
318
|
+
async function main() {
|
|
319
|
+
const transport = new StdioServerTransport();
|
|
320
|
+
await server.connect(transport);
|
|
321
|
+
// Stay alive — stdio transport handles its own lifecycle.
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
main().catch((err) => {
|
|
325
|
+
console.error('wcagcheckr MCP server failed:', err);
|
|
326
|
+
process.exit(1);
|
|
327
|
+
});
|