MCP server
Connect Claude Code, Cursor or another MCP client so an AI assistant can run geo- and device-aware QA checks, with navigation limited to origins you allow.
Last updated
The qa-mcp server lets AI coding assistants such as Claude Code or Cursor run geo- and device-aware QA checks on origins you allowlist, through the Model Context Protocol.
What it does
qa-mcp is a Model Context Protocol server on the same headless runtime as the CI runner, so Claude Code, Cursor or another MCP client can run browser tests on your allowlisted origins. You can ask an assistant, for example, "open staging.example.com on an iPhone 15 in WebKit from Austin, TX, submit the form with synthetic data and tell me what broke." The assistant gets a structured result and up to four masked screenshots back.
- stdio only. The server opens no network port. Protocol messages use stdout; logs and audit lines go to stderr.
- Navigation limited to allowlisted origins. The server does not start without
QA_MCP_ALLOWED_ORIGINS, and the assistant cannot navigate to any other origin. The page's own scripts, images and API calls can still reach other origins; this is a navigation boundary, not a network firewall. - No credentials for the assistant. Proxy credentials come from the server's environment, are removed from it before a browser starts, and are redacted from every result.
- Bounded. Each call has a case limit, concurrent browsers are capped, and each UTC day has a budget of case attempts.
Use it only on sites you own or are contracted to test, preferably staging environments. See Responsible use. The full reference is in docs/MCP-SERVER.md.
Build
The server ships with the source code and the runner image, not with the desktop app.
npm ci
npm run build # emits out/main/qa-mcp.js
npm run browsers:install # Chromium, Firefox and WebKit, if not installed yetIt needs Node.js 22.13 or later and runs under plain Node, not Electron. Check that it refuses to start without an allowlist:
node out/main/qa-mcp.js
# qa-mcp configuration error: QA_MCP_ALLOWED_ORIGINS is required: … (exit code 2)Note: Start the server with
nodedirectly, never withnpm run. npm writes its own banner to stdout, which corrupts the protocol stream.
Client setup
Every client starts the server as a child process over stdio. Use absolute paths.
Claude Code
Add the server to the project's .mcp.json, or to your user configuration with claude mcp add:
{
"mcpServers": {
"proxy-qa": {
"command": "node",
"args": ["/path/to/proxy_browser/out/main/qa-mcp.js"],
"env": {
"QA_MCP_ALLOWED_ORIGINS": "https://staging.example.com",
"QA_MCP_WORKSPACE": "/path/to/your-app/qa"
}
}
}
}claude mcp add proxy-qa \
-e QA_MCP_ALLOWED_ORIGINS=https://staging.example.com \
-- node /path/to/proxy_browser/out/main/qa-mcp.jsDo not commit a .mcp.json that contains credentials. Keep proxy credentials in your user configuration or in variables your shell already exports.
Cursor
Add the same block to ~/.cursor/mcp.json (all projects) or .cursor/mcp.json (one project). Leave the proxy variables out for direct checks, for example against a local development server:
{
"mcpServers": {
"proxy-qa": {
"command": "node",
"args": ["/path/to/proxy_browser/out/main/qa-mcp.js"],
"env": {
"QA_MCP_ALLOWED_ORIGINS": "https://staging.example.com,http://localhost:3000"
}
}
}
}Any other MCP client with stdio support works the same way. The server takes no arguments; configuration comes from environment variables only.
Tools
| Tool | Purpose |
|---|---|
list_capabilities |
Installed engines, proxy providers and products with credentials, whether a custom gateway is set, and the limits (allowed origins, cases per call, concurrency, daily budget and attempts left). The assistant should call it first |
search_devices |
Find device preset IDs by words ("iphone 15 pro", "galaxy"), engine or device type |
search_locations |
Search the bundled US dataset ("austin tx", "new jersey", "78701"); each entry includes a target object for the tools below |
check_exit_ip |
Open a sticky proxy session and verify the exit IP, re-rolling up to three times for a target; returns IP, location, ISP and ASN, and the match verdict. Needs proxy credentials. Counts one case attempt |
run_check |
Open an allowlisted URL in every engine × device × target combination and run optional steps |
run_manifest |
Run a scenario or suite manifest exported from the desktop app, from inside QA_MCP_WORKSPACE |
get_results |
Return the stored summary of an earlier run in the same server process (the latest 50 runs are kept in memory) |
run_check
| Input | Notes |
|---|---|
url |
Start URL. Its origin must be allowlisted; URLs with credentials are refused |
engines |
1–10 engine IDs, for example ["chromium", "webkit"] |
devices |
1–20 preset IDs from search_devices |
targets |
Up to 20 target objects; needs proxy credentials |
steps |
1–100 scenario steps; default: assert that the page body is visible |
healing |
off, warn (default) or fail |
direct |
Connect without the proxy even when credentials are configured |
timeoutMs |
Per-step timeout, 1000–60000 (default 15000) |
Steps use the scenario step schema of the desktop app (see Scenarios and matrices), so they can include checks such as checkAccessibility (see Consent, accessibility and performance checks). goto steps need absolute URLs on allowlisted origins; variables are only available in manifests. upload steps need fixtures embedded in a scenario, so use run_manifest for uploads. There is no arbitrary JavaScript and no download, and traces are never captured.
{
"url": "https://staging.example.com/signup",
"engines": ["webkit"],
"devices": ["iphone-15"],
"targets": [{ "mode": "city", "country": "us", "state": "Texas", "stateCode": "TX", "city": "Austin", "zip": null }],
"steps": [
{ "action": "fill", "selector": "#email", "value": "qa+mcp-test@example.com" },
{ "action": "click", "selector": "button[type=submit]" },
{ "action": "assertText", "selector": ".result", "value": "Thanks" }
]
}The result contains the run ID and status, case counts, healed steps and, per case, engine, device, location, status, exit IP and location match, failed steps, up to 20 console errors and failed requests, and the final URL. Up to four screenshots are returned as images, failed cases first, masked as usual and downscaled to at most 800 px wide. Artifacts are deleted once the result has been returned.
A case whose assertion fails is not a tool error: the call succeeds and the summary reports the case as failed. Tool errors carry the app's error codes, for example INVALID_INPUT for an origin that is not allowlisted or SESSION_LIMIT when the daily budget is used up.
run_manifest
| Input | Notes |
|---|---|
path |
Manifest path relative to QA_MCP_WORKSPACE (up to 10 MB) |
environment |
Environment name from the manifest |
healing |
Overrides every scenario's mode |
baselines |
.qavb pack inside the workspace (up to 50 MB) |
Every scenario's approved origins, and the chosen environment's base URL, must be on the allowlist. The manifest's matrix counts against the per-call case limit, and cases × (retries + 1) attempts count against the daily budget.
Policy settings
| Variable | Required | Default | Effect |
|---|---|---|---|
QA_MCP_ALLOWED_ORIGINS |
yes | none | Comma-separated origins, such as https://staging.example.com,http://localhost:3000. HTTPS, or HTTP only for localhost, 127.x.x.x and [::1]. No paths, wildcards or credentials; at most 30 entries |
QA_MCP_WORKSPACE |
for run_manifest |
none | Directory that manifests and baselines must resolve inside (symlinks and ../ cannot escape it). Also stores the budget state in .qa-mcp/budget.json |
QA_MCP_MAX_CASES |
no | 12 | Cases per tool call (1–100) |
QA_MCP_CONCURRENCY |
no | 2 | Concurrent browsers (1–4) |
QA_MCP_DAILY_BUDGET |
no | 200 | Case attempts per UTC day (1–10000); kept in memory when no workspace is set |
Invalid values stop the server with exit code 2 and a message naming the variable. Runs and exit-IP checks execute one at a time.
Proxy credentials use the same variables as the CLI runner (QA_PROVIDER*, DATAIMPULSE_PROXY_*, QA_PROXY_*); see CI runner. Without them, run_check connects directly and location targets and check_exit_ip are unavailable. Site access tokens are not available to the server.
Security model
The trust boundary is the MCP client: everything the assistant sends is treated as untrusted input, and the human operator controls the server's environment.
- The allowlist is checked before any engine check, budget reservation or browser launch, and the navigation guard blocks other origins and redirects during the run.
fillandselectvalues and scenario, dataset and environment variables are redacted from free-text result fields. Screenshots mask form fields.- Only
run_manifestreads files, and only insideQA_MCP_WORKSPACE. File contents are parsed, never returned. - One audit line per call on stderr records the tool, origins, case count, outcome and duration, never input values.
Submit buttons. run_check and run_manifest allow clicks on submit buttons, which create real records, send emails or start workflows on the site under test. Therefore:
- Allowlist staging or test environments only, never production.
- Instruct the assistant to use synthetic data:
example.comorexample.testaddresses and obviously fake names and phone numbers. - Tag test submissions, for example with a
qa+mcp-…@address, and treat them as test leads in your form backend.
The server does not control the desktop app or its sessions, offers no remote (HTTP or SSE) transport, and does no anti-bot evasion.
Docker
The runner image contains the server. Override the entrypoint and keep stdin open with -i:
docker run -i --rm --init --ipc=host \
-e QA_MCP_ALLOWED_ORIGINS=https://staging.example.com \
-v "$PWD/qa:/work" -e QA_MCP_WORKSPACE=/work \
--entrypoint node ghcr.io/ubaidbinwaris/proxy-qa-runner:1.5.2 \
/opt/proxy-qa-runner/out/main/qa-mcp.jsDo not pass -t: a TTY changes line endings and breaks the protocol stream. Add --network host (Linux) when the site under test runs on the host's localhost.
Troubleshooting
| Symptom | Fix |
|---|---|
The client shows the server as failed and stderr says configuration error |
Set QA_MCP_ALLOWED_ORIGINS, or fix the variable named in the message |
BROWSER_MISSING |
Run npm run browsers:install, or set PLAYWRIGHT_BROWSERS_PATH to installed browsers |
Origin not allowlisted for http://localhost:3000 vs http://127.0.0.1:3000 |
They are different origins; allowlist the one you open |
| Location targets refused | Configure proxy credentials and do not set direct |
SESSION_LIMIT |
The daily budget is used up. It resets at 00:00 UTC, or raise QA_MCP_DAILY_BUDGET |
| Garbled protocol or invalid JSON in the client | Start the server with node …/qa-mcp.js, not npm run, and without docker -t |
Common questions
Can the assistant open any website?
No. The server does not start without QA_MCP_ALLOWED_ORIGINS, the allowlist is checked before any browser launch, and the navigation guard blocks other origins and redirects during the run. The page's own scripts, images and API calls can still reach other origins: it limits navigation, it is not a network firewall.
Does the MCP server need the desktop app?
No. It ships with the source code and the runner Docker image, runs under Node.js 22.13 or later, and does not control the desktop app or its sessions.