Skip to main content
Glama
README.md
# mcp-a11y

[![CI](https://github.com/P4ST4S/mcp-a11y/actions/workflows/ci.yml/badge.svg)](https://github.com/P4ST4S/mcp-a11y/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)

> The USB-C port of accessibility: one MCP server that audits any web page against WCAG and applies fixes, drivable from any MCP client (Claude Desktop, etc.).

## The problem

Web accessibility is a solved science (WCAG tells you exactly what is broken) but a chronic practice. Every team rebuilds the same audit-and-fix loop, glued to one framework or one CI vendor, behind one proprietary dashboard. The knowledge does not travel.

mcp-a11y turns that loop into a standard interface. Detection stays 100% deterministic (axe-core decides what is broken, never a model). The only place a model is used is to describe images for alt text. Any MCP client can plug in, audit a page, get deterministic fixes back, and open a mergeable PR. That is the thesis: a universal, boring, reliable port for accessibility, not another walled garden.

## How it works

```
audit_page  ──►  fix_contrast        ──►  generate_report  ──►  open_pr
(axe-core)       simple_fixes              (before/after)         (controlled repo)
                 generate_alt_text
                 (the only LLM step)
```

Detection is deterministic. Structural and contrast fixes are deterministic. A vision model is called only for alt text. Fixes are reinjected into the source by CSS selector (not by raw color value), so the patched file is what gets committed.

## Tools

| Tool | What it does | Deterministic? |
| --- | --- | --- |
| `audit_page(url)` | Playwright + axe-core, returns structured WCAG violations (with selectors and colors) | Yes, no LLM |
| `fix_contrast(fg, bg)` | Closest WCAG-compliant foreground color (AA 4.5:1 normal text) | Yes, no LLM |
| `simple_fixes(html)` | Missing `lang`, missing `<title>`, unlabeled form controls | Yes, no LLM |
| `generate_alt_text(imageUrl \| selector)` | Vision model describes an image for an HTML `alt` attribute | No, the only LLM step |
| `generate_report(...)` | Self-contained before/after HTML report | Yes, no LLM |
| `open_pr(...)` | Opens a mergeable PR via Octokit. Strict guardrail: only ever touches `A11Y_TARGET_REPO` | Yes, no LLM |

Each tool's logic is isolated and testable off-MCP, with no shared state between tools. The deterministic helpers in `src/lib` are pure (no I/O); `audit_page`, `generate_alt_text`, and `open_pr` perform I/O (browser, model, GitHub) by nature.

## Stack

TypeScript end-to-end, ESM, Node 22+. MCP TypeScript SDK 1.x, Playwright + axe-core, Octokit, Anthropic SDK, Zod v3.

## Quick start

```bash
pnpm install
pnpm exec playwright install chromium
cp .env.example .env   # fill in keys (see Configuration)
```

Run the MCP server (stdio):

```bash
pnpm dev
```

Inspect it with the MCP Inspector:

```bash
pnpm inspect
```

Run the full demo loop (audit, fix, report) on the bundled broken page:

```bash
pnpm exec tsx src/runner/demo.ts demo-site/index.html --report a11y-report.html
# add --alt to also generate alt text (needs ANTHROPIC_API_KEY)
# add --pr  to open a PR on A11Y_TARGET_REPO (needs GITHUB_TOKEN)
```

For the coherent loop, remediate the file that actually lives in the target repo and re-PR the same path:

```bash
pnpm exec tsx src/runner/demo.ts --from-repo index.html --alt --pr
```

`--from-repo <path>` fetches that file from `A11Y_TARGET_REPO`, audits and fixes it, then opens a PR editing the same path (no demo gap: you fix the exact file the repo serves). Scratch files are written to the OS temp dir and removed on exit; only the report stays in the working directory.

Runner options:

| Flag | Effect |
| --- | --- |
| `<htmlFile>` | Local HTML source (default `demo-site/index.html`). Ignored with `--from-repo`. |
| `--from-repo <path>` | Fetch and remediate this path from `A11Y_TARGET_REPO`, then re-PR the same path. |
| `--alt` | Also run `generate_alt_text` on each unlabeled image (needs `ANTHROPIC_API_KEY`). |
| `--img-selector <sel>` | CSS selector for the image to describe (default: images flagged by the audit). |
| `--pr` | Open a PR on `A11Y_TARGET_REPO` (needs `GITHUB_TOKEN`). Off by default. |
| `--repo-path <path>` | Path of the file inside the target repo (defaults to `--from-repo`, else `demo-site/index.html`). |
| `--report <file>` | Where to write the HTML report (default `a11y-report.html`). |

## Configuration

Copy `.env.example` to `.env`:

```
ANTHROPIC_API_KEY=...     # used ONLY by generate_alt_text
GITHUB_TOKEN=...          # used ONLY by open_pr
A11Y_TARGET_REPO=owner/repo   # STRICT guardrail: the only repo open_pr will touch
A11Y_ALT_TEXT_MODEL=claude-haiku-4-5   # optional, defaults to Haiku
```

The server boots even without keys. Only the tool that needs a key fails, with a clear message.

## Use from Claude Desktop

Add this to your Claude Desktop MCP config (`claude_desktop_config.json`):

```json
{
  "mcpServers": {
    "mcp-a11y": {
      "command": "pnpm",
      "args": ["--dir", "/absolute/path/to/mcp-a11y", "dev"]
    }
  }
}
```

Then ask Claude to audit a URL, fix the issues, and open a PR.

## Demo target

`demo-site/index.html` is an intentionally broken page (images without alt, poor contrast, no `lang`, form without labels). It is 100% static with zero JavaScript, so the rendered DOM equals the source file, and all colors live in a single `<style>` block. That invariant is what makes deterministic reinjection of fixes into the source reliable.

For a coherent audit-to-PR loop, run the runner with `--from-repo <path>`: it fetches that file from `A11Y_TARGET_REPO`, audits and fixes it, and re-PRs the same path. Relative image URLs are resolved over the raw repo URL so alt text works on a locally audited copy. You fix the exact file the repo serves, with no demo gap.

## Guardrail

`open_pr` never accepts an arbitrary repo. The target comes only from `A11Y_TARGET_REPO`, validated as exactly `owner/repo`. Passing a different `repo` is refused before any network call. This is a controlled demo target by design.

## Tests

```bash
pnpm test        # node:test via tsx
pnpm typecheck   # tsc on src and test
```

The deterministic core (contrast math) is unit tested, and the full remediation loop has an end-to-end test (audit, fix, reinject, re-audit clears the violations).

## License

[MIT](LICENSE)

TDQS

A4/5.0

Scored across 7 tools

Disambiguation5/5

Each tool has a clearly distinct purpose: audit_page does WCAG auditing, fix_contrast computes contrast fixes, generate_alt_text generates alt text, generate_report builds reports, open_pr opens PRs, ping is health check, and simple_fixes applies structural fixes. No overlap or ambiguity.

Naming Consistency5/5

All tool names follow a consistent verb_noun pattern (e.g., audit_page, fix_contrast, generate_alt_text, simple_fixes). Even ping fits the pattern of a simple verb. Snake_case is used uniformly.

Tool Count5/5

With 7 tools, the server is well-scoped for accessibility tasks. Each tool covers a key aspect of auditing, fixing, reporting, and deployment, without being overwhelming or sparse.

Completeness5/5

The tool surface covers the full lifecycle: audit (audit_page), fix contrast (fix_contrast), generate alt text (generate_alt_text), apply structural fixes (simple_fixes), generate report (generate_report), and deploy changes (open_pr). No obvious gaps for the domain.

Maintenance

ActivityInactive
ResponsivenessNo issues