Skip to main content
Glama

qa-screens

An MCP server for visual QA during AI refactoring. It captures pages with Playwright, compares them with reference screenshots using SSIM, and returns the score, the changed regions and a labelled REFERENCE | LIVE preview image, so the agent can see what broke and fix it.

  • Design conformance: compare the running site with golden or Figma screenshots.

  • Refactor safety: capture before and after a change and prove it is pixel-neutral.

  • A/B: compare two running servers, such as main and a worktree.

  • Apps behind a login: OAuth (client credentials, password, refresh, authorization code + PKCE), bearer tokens, cookies, sessions, localStorage/sessionStorage and HTTP basic auth.

  • Self-reporting: significant errors and crashes are filed as GitHub issues automatically, with secrets scrubbed. See Error reporting.

Quick start

No configuration is needed for a first run:

claude mcp add qa-screens -- uvx qa-screens

Then ask the agent: "Use qa-screens to make the current homepage the reference, then check it."

  1. The first call downloads Chromium (it starts in the background as soon as the server starts).

  2. update_reference(page="home", confirm=true) screenshots http://localhost:8080/home/ and saves it as screenshots/home.png. Append -mobile to the name for the mobile version.

  3. run_qa compares the live site with the references from then on.

If your site runs somewhere else, the error message says so. Set base_url in .qa-screens.json (see Configure) or pass base_url to the tool.

Related MCP server: websight

Install

pip install qa-screens          # or: uv tool install qa-screens

Chromium is downloaded automatically on first use. To do it up front, run python -m playwright install chromium.

Claude Code

claude mcp add qa-screens -- uvx qa-screens

or in a project's .mcp.json:

{
  "mcpServers": {
    "qa-screens": { "command": "uvx", "args": ["qa-screens"] }
  }
}

To run the latest main without PyPI, use uvx --from git+https://github.com/astuanax/qa-screens qa-screens.

Copy skills/qa-screenshots into .claude/skills/ so the agent runs QA on its own after every UI change.

Other MCP clients

The server speaks MCP over stdio. The command is qa-screens (or python -m qa_screens), and it runs in the project directory, or wherever QA_SCREENS_ROOT points.

Configure

Put a .qa-screens.json in the project root. Every key is optional. A mistake in this file never stops the tools: bad JSON, unknown keys (with a "did you mean" hint) and invalid values fall back to the defaults, and every tool result lists them under config_warnings (the CLI prints them as warning: lines).

{
  "base_url": "http://localhost:8080",
  "references_dir": "qa/screenshots",
  "route_template": "/nl-be/{name}/",
  "routes": { "home": "/", "nl-be": "/nl-be/" },
  "threshold": 0.9,
  "mobile_device_scale_factor": 1,
  "mask_selectors": [".carousel", "[data-testid=clock]"],
  "default_profile": null
}

key

default

meaning

base_url

http://localhost:8080

where the site runs (not started by qa-screens)

references_dir

screenshots

reference .png/.jpg files; the file name is the page name

route_template

/{name}/

page name → path; {name} excludes the -mobile suffix

routes

{}

per-page overrides (a path, or a full URL)

threshold

0.90

minimum SSIM to pass

max_changed_ratio

0.02

also fail when more than this share of pixels clearly changed colour (1 disables)

mobile_suffix

-mobile

names ending in it are captured in a 390px mobile context

desktop_viewport / mobile_viewport

1440×900 / 390×844

mobile_device_scale_factor

2

set 1 if mobile references are 390px wide

mask_selectors

[]

elements hidden before capture (dynamic content)

wait_until, navigation_timeout_ms

networkidle, 30000

runtime_dir

.qa-screens/runtime

captures, diffs, previews, reports (add it to .gitignore)

default_profile

null

auth profile used when a tool doesn't pass one

ai_critique, gemini_model

false

optional Gemini second opinion (pip install qa-screens[ai], GEMINI_API_KEY)

The environment variables QA_SCREENS_BASE_URL, QA_SCREENS_REFERENCES_DIR, QA_SCREENS_ROUTE_TEMPLATE, QA_SCREENS_THRESHOLD, QA_SCREENS_MAX_CHANGED_RATIO and QA_SCREENS_PROFILE override the file. QA_SCREENS_FIGMA_API changes the Figma API URL.

Tools

tool

purpose

qa_config

effective config, references and their URLs, auth profiles; start here

run_qa

QA all or some pages; returns a JSON summary plus previews of the worst failures

qa_page

QA one page (fast fix loop); url overrides the mapping

capture

screenshot any URL, full page or a single element (clip_selector)

compare_images

SSIM two image files, for example a Figma export and a capture

capture_set / compare_sets

before/after parity check for refactors

ab_compare

compare two running servers page by page

update_reference

make the page's current look its reference: first baseline or accepted redesign (needs confirm=true, keeps a backup, refuses error pages)

sync_figma

download Figma frames as references (FIGMA_TOKEN)

auth_update_profile

create or update an auth profile: token, OAuth, cookies, headers, storage

auth_browser_login

log in with a real browser (form, SSO or MFA) and save the session

auth_oauth_login

OAuth authorization code + PKCE in a browser window

auth_import_storage_state

import a Playwright storageState JSON

auth_check

check that a profile is logged in (status, redirect, screenshot)

auth_profiles / auth_delete_profile

list, inspect (never shows secrets) or delete profiles

error_reports

error-reporting status; flush=true posts pending reports

Page results are PASS, FAIL or ERROR. A page fails when its SSIM is below threshold or more than max_changed_ratio of it clearly changed colour. SSIM is a page-wide average, so a recoloured header scores about 0.99 and would pass on SSIM alone. ERROR means the comparison is meaningless: an HTTP 4xx/5xx, a navigation failure or a missing reference. The agent should fix the environment, not the CSS.

Authentication for apps

A profile is a named session that is applied to every browser context that uses it:

auth_update_profile(
  name="staging",
  origins=["https://app.staging.example.com"],
  oauth={"grant_type": "client_credentials",
         "token_url": "https://idp.example.com/oauth/token",
         "client_id": "qa-bot", "client_secret": "env:QA_CLIENT_SECRET",
         "audience": "https://api.example.com"},
  apply_token_as=["header", "local_storage:access_token"])

run_qa(profile="staging", base_url="https://app.staging.example.com")
  • Secrets: any value can be "env:VAR". It is resolved from the server's environment at use time, so it never passes through the AI conversation.

  • Scoping: headers and tokens go only to the profile's origins (default: the base URL's origin), never to CDNs or third parties. Cookies follow normal browser rules.

  • Token placement (apply_token_as): header (Authorization: Bearer …), header:X-Api-Key, local_storage:<key>, session_storage:<key>, local_storage_json:<key> (the whole token object), cookie:<name>.

  • Refresh: expired tokens are refreshed through refresh_token or by re-running the client-credentials/password grant, including mid-session in the long-lived server.

  • Storage: profiles live in ~/.local/state/qa-screens/profiles/ with mode 0600, outside the project, so they are never committed. QA_SCREENS_STATE_DIR moves the location.

Error reporting

qa-screens reports its own bugs, so they reach the maintainers without a manual bug report.

  • Significant errors (unexpected exceptions in a tool, not user errors like a missing reference or bad credentials) are written to ~/.local/state/qa-screens/reports/pending/ and posted in the background as a GitHub issue on astuanax/qa-screens.

  • Crashes: uncaught exceptions are logged to a crash file, and hard crashes (segfault, abort) are captured by faulthandler. On the next start-up the server scans for these files and posts them.

  • A report stays pending until it has been posted, so reports made offline or without a token are sent later.

  • De-duplication: each error has a stable fingerprint. A repeat within 24h is not re-posted, and an open issue with the same fingerprint gets a comment instead of a new issue. At most 10 posts per day.

  • Privacy: tokens, JWTs, cookies, passwords, URL query strings and credentials, and the home directory path are redacted. Tool arguments that hold secrets are dropped entirely.

env var

default

QA_SCREENS_ERROR_REPORTING

on

on, local (write files, never post) or off

QA_SCREENS_ISSUE_REPO

astuanax/qa-screens

where issues go (point it at your fork)

QA_SCREENS_GITHUB_TOKEN

falls back to GITHUB_TOKEN, then gh auth token

needs issues:write

QA_SCREENS_GITHUB_API

https://api.github.com

API URL, for GitHub Enterprise

Without a token nothing is posted; reports wait in pending/. qa-screens reports --flush posts them by hand.

CLI (CI-friendly)

qa-screens                       # MCP server over stdio (same as `qa-screens serve`)
qa-screens run                   # QA every reference; exit 1 on FAIL/ERROR
qa-screens run nl-be faq --viewport desktop --base-url http://localhost:8080 --json
qa-screens reports --flush       # post pending error reports

A report is written to .qa-screens/runtime/reports/latest.json on every run.

How it works

  1. The reference name maps to a URL (route_template / routes). Names ending in -mobile use a mobile context (390px, touch, mobile UA).

  2. The page is loaded with the HTTP cache disabled (so the CSS you just edited is what gets measured), with animations, transitions and scrollbars disabled, and after document.fonts.ready. It is captured full-page.

  3. SSIM is computed per colour channel. Grayscale SSIM, as the original tool used, scores a pure hue change at 0.9996. Captures above 12MP are decoded at reduced resolution, so long pages never run out of memory. Different sizes are handled by align: resize (legacy default), crop or pad (height changes count as differences).

  4. Separately, pixels whose colour clearly changed are measured on lightly blurred images, so JPEG artefacts and antialiasing don't count. Their share of the page is changed_ratio.

  5. Changed pixels are grouped into region boxes. A red heatmap and a cropped side-by-side preview are written, and the preview is returned to the agent as an image.

Development

uv venv && uv pip install -e ".[dev]"
python -m playwright install chromium
pytest

The tests exercise the server the way an agent does: through an in-process MCP client, against a fake web app (login form, session cookies, OAuth provider with PKCE, basic auth) and fake GitHub and Figma APIs. Assertions are about what those services actually received.

Releases are published to PyPI by GitHub Actions when a v* tag is pushed (trusted publishing).

License

MIT

Available Tools

18 tools
ab_compareC

Compare two running servers (e.g. main branch on :8080 vs a worktree on :8091) page by page.

Inputs: base_url_a (required), base_url_b (required), pages, viewport, +5 more.

auth_browser_loginA

Log in with a real browser and save the session (cookies, localStorage and sessionStorage) into profile name. Works for any login incl. SSO/MFA/OAuth redirects.

headed=true opens a visible window where the user logs in (or finishes MFA); if username/password are given (use "env:VAR" for secrets) the form is filled first. Completion is detected by success_url_pattern (regex on the URL), success_selector, or — if neither is given — by leaving the login page's URL.

Inputs: name (required), login_url (required), headed, password, +7 more.

auth_checkA

Open url (default: the base URL) with profile name and report whether the session works: HTTP status, final URL (a redirect to a login page means it expired) and a screenshot.

Inputs: name (required), url.

auth_delete_profileB

Delete an auth profile and its saved session.

Inputs: name (required).

auth_import_storage_stateB

Import a Playwright storage-state JSON (cookies + localStorage), e.g. from a Playwright test setup's storageState output, into profile name.

Inputs: name (required), path (required).

auth_oauth_loginA

Run the OAuth authorization-code + PKCE flow for profile name (configure oauth with grant_type "authorization_code", authorize_url, token_url, client_id, redirect_uri first). The user signs in in the opened window; the redirect is intercepted, the code exchanged, and the token (with refresh_token if issued) stored and auto-refreshed.

Inputs: name (required), headed, timeout_s.

auth_profilesA

List auth profiles, or show one (secrets are never shown).

Inputs: name.

auth_update_profileA

Create or update an auth profile. Only the fields you pass change (dicts are merged).

Any secret may be given as "env:VAR_NAME" so it is read from the server's environment instead of passing through the conversation — prefer that.

origins: origins that receive headers/tokens, e.g. ["https://app.example.com"] (default: the base_url's origin). Credentials are never sent to other origins. access_token/token_type/expires_in/refresh_token: a bearer token you already have. apply_token_as: where the app expects the token — any of "header" (Authorization: Bearer), "header:X-Api-Key", "local_storage:", "session_storage:", "local_storage_json:" (whole token object as JSON), "cookie:". oauth: {grant_type: client_credentials|password|refresh_token|authorization_code, token_url, client_id, client_secret, scope, audience, username, password, authorize_url, redirect_uri, client_auth: post|basic, extra_params}. For client_credentials/password a token is fetched immediately and refreshed automatically. For authorization_code, call auth_oauth_login next. cookies: Playwright cookies [{name, value, url} or {name, value, domain, path}]. local_storage/session_storage: {origin: {key: value}} injected before page scripts run. http_credentials: {username, password} for HTTP basic auth. clear: field names to remove, e.g. ["token", "cookies", "storage_state"].

Inputs: name (required), clear, oauth, cookies, +10 more.

captureA

Screenshot any URL (desktop or mobile viewport, optionally logged in via profile). clip_selector captures a single element. Saved under runtime/captures/.png.

Inputs: url (required), name, profile, viewport, +4 more.

capture_setA

Capture a named set of pages (e.g. label="before" before a refactor, "after" after it). Pages default to all reference names; any names work if you pass pages and the route mapping resolves them. Use compare_sets afterwards. Proves a refactor is pixel-neutral without depending on golden references.

Inputs: label (required), pages, profile, base_url, +1 more.

compare_imagesA

SSIM-compare two image files (e.g. a Figma export vs a capture). Paths may be relative to the project root. Returns score, changed regions, a heatmap path and an a|b preview.

Inputs: image_a (required), image_b (required), align, threshold, +1 more.

compare_setsB

Diff two capture sets (labels from capture_set, or directories). identical: true means every page scored >= threshold. Returns previews of the most-changed pages.

Inputs: after, align, before, threshold, +2 more.

error_reportsA

Show automatic error-reporting status (pending/reported crash and error reports, target repo). flush=true posts pending reports to GitHub now.

Inputs: flush.

qa_configA

Show the effective configuration (base URL, reference dir, route mapping), the reference screenshots found and the available auth profiles. Start here.

Inputs: no named parameters.

qa_pageB

QA a single page against its reference (fast iteration while fixing). url overrides the route mapping for this call. Returns the result and, on failure, a reference|live side-by-side preview cropped around the changed regions.

Inputs: page (required), url, align, profile, +3 more.

run_qaA

Capture pages from the running site and SSIM-compare them with the reference screenshots.

pages: reference names (file names without extension); default all references. viewport: all | desktop | mobile (names ending in "-mobile" use a 390px mobile context). profile: auth profile for pages behind a login. align: how to handle different image sizes — resize (default, legacy), crop, or pad (pad makes page-height changes count as differences). A page FAILs when SSIM < threshold OR more than max_changed_ratio of its pixels clearly changed colour (catches local changes such as a recoloured header that SSIM averages away). Returns a JSON summary (failures first, with changed regions in px) plus side-by-side reference|live preview images of up to max_images failing pages.

Inputs: align, pages, profile, base_url, +4 more.

sync_figmaA

Download Figma frames into the references directory (needs FIGMA_TOKEN). Frame names become reference names; overwrites existing references with the same name.

Inputs: file_key (required), frame.

update_referenceA

Make the current look of a page its reference screenshot: from source (an image), else the latest qa_page capture, else a fresh capture right now (so this also creates a first baseline for a page that has no reference yet). The old reference is backed up. Only do this when the USER has confirmed the look is intended; set confirm=true.

Inputs: page (required), source, confirm, profile.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A3.5/5.0

    Scored across 18 tools

    Disambiguation4/5

    Most tools have clearly distinct purposes: run_qa vs qa_page, capture vs capture_set, and compare_images vs compare_sets vs ab_compare are separated by scope. The main ambiguity is between auth_browser_login and auth_oauth_login, though their descriptions clarify the different login flows.

    Naming Consistency4/5

    Tool names follow a mostly predictable snake_case style, with useful prefixes like qa_ and auth_. However, the pattern is not strictly verb_noun: capture and ab_compare stand out, and names like qa_config or error_reports are noun-style rather than action-first.

    Tool Count3/5

    18 tools is on the heavy side, especially considering the 7-tool auth sub-system alongside the core screenshot/compare workflow. It is not excessive in absolute terms, but it crosses into the range where the set starts to feel bloated.

    Completeness4/5

    The core visual QA lifecycle is well covered: references, captures, comparisons, set diffs, and running-server diffs are all present, and auth profile management is thorough. Minor gaps exist such as no direct tool for deleting references or listing capture sets, but these can be worked around.

    Maintenance

    ActivitySlowing
    ResponsivenessOutdated data

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      Not graded
      quality
      C
      maintenance
      Enables AI assistants to capture website screenshots, automate browser interactions, and manage recurring screenshot configurations across 150+ global locations. It also supports AI-powered domain research and visual change monitoring for any web page.
      15 npm
      MIT
    • A
      license
      Not graded
      quality
      D
      maintenance
      Enables AI agents to see, analyze, and visually verify web page changes through pixel-perfect diffing, theme extraction, layout analysis, and interactive element detection.
      8 npm
      MIT
    • A
      license
      A
      quality
      C
      maintenance
      Screenshot, visual-diff, and AI page-analysis API for AI agents. Capture any URL as PNG, JPEG, WebP, PDF, or HTML, diff two versions of a page to catch visual regressions, and get an AI summary of what a page contains.
      3
      62 npm
      1
      MIT