Agentic Workflows (MCP)
RegressionBot supports the Model Context Protocol (MCP). Expose visual testing tools directly to your AI agents (Claude Code, OpenAI Codex, Gemini CLI, Windsurf, and Cursor) via our remote Streamable HTTP MCP server. This allows your agents to autonomously trigger visual regression jobs. They can also check job status and approve baselines. No local installation is required.
Install
Configure your MCP client to connect to RegressionBot via Streamable HTTP using https://mcp.regressionbot.com/ and either the standard Authorization: Bearer <token> header or the legacy x-api-key header.
Claude Code
Agentic IDEs and CLIs (such as Anthropic's Claude Code, OpenAI Codex, Cursor, Windsurf, and Gemini CLI) natively support Streamable HTTP transports and header configuration via command line, config files, or visual settings. RegressionBot's MCP endpoint is Streamable HTTP: configure the remote URL directly and authenticate with the Authorization: Bearer YOUR_API_KEY header. Do not configure it as a legacy SSE server.
claude mcp add --transport http regressionbot https://mcp.regressionbot.com/ --header "Authorization: Bearer YOUR_API_KEY"OpenAI Codex
Codex supports Streamable HTTP MCP servers in both the CLI and IDE extension. Both clients share the same MCP configuration, so you only need to configure RegressionBot once. The RegressionBot server does not use a legacy SSE endpoint event.
Add the following to ~/.codex/config.toml. The bearer_token_env_var entry tells Codex to pass the token as a Bearer token read from your environment (recommended for authorization status to show green).
[mcp_servers.regressionbot]
url = "https://mcp.regressionbot.com/"
bearer_token_env_var = "REGRESSIONBOT_API_KEY"Make sure to export the environment variable containing your API key in your shell configuration or current terminal:
Note that the configuration parameter bearer_token_env_var expects the name of the environment variable itself ("REGRESSIONBOT_API_KEY"), not the literal API key.
Alternatively, if you are using legacy headers, you can configure it via env_http_headers:
[mcp_servers.regressionbot]
url = "https://mcp.regressionbot.com/"
env_http_headers = { "x-api-key" = "REGRESSIONBOT_API_KEY" }Alternatively, register the server via the Codex CLI and it will write the [mcp_servers.regressionbot] entry for you. You still need to add bearer_token_env_var manually afterwards as the CLI does not accept header/bearer flags.
export REGRESSIONBOT_API_KEY="YOUR_API_KEY"
codex mcp add regressionbot --url https://mcp.regressionbot.com/
# then manually add bearer_token_env_var to ~/.codex/config.tomlIn the Codex terminal UI, run /mcp to verify the server is active. In the Codex IDE extension, open MCP settings from the gear menu after editing config.toml to verify that RegressionBot is listed.
Cursor
Configure remote servers directly through Cursor's graphical interface to keep your API keys encrypted.
- Open Cursor Settings and navigate to Features > MCP Servers.
- Click + Add New MCP Server.
- Fill out the configuration:
- Name:
regressionbot - Type:
http - URL:
https://mcp.regressionbot.com/
- Name:
- Add a header row:
- Using Bearer Auth (recommended): set key to
Authorizationand value toBearer YOUR_API_KEY. - Or using Custom Header (legacy): set key to
x-api-keyand value to your API key.
- Using Bearer Auth (recommended): set key to
- Click Save and verify the green status indicator.
Windsurf
Windsurf handles remote Streamable HTTP MCPs using a structured JSON file. Add the following to your ~/.codeium/windsurf/mcp_config.json:
{
"mcpServers": {
"regressionbot": {
"serverUrl": "https://mcp.regressionbot.com/",
"headers": {
"Authorization": "Bearer YOUR_API_KEY"
}
}
}
}Gemini CLI
Gemini CLI supports direct Streamable HTTP transport. You can configure it via the command line or a settings file. We recommend using Bearer token authentication:
gemini mcp add --transport http regressionbot https://mcp.regressionbot.com/ --header "Authorization: Bearer YOUR_API_KEY"{
"mcpServers": {
"regressionbot": {
"httpUrl": "https://mcp.regressionbot.com/",
"headers": {
"Authorization": "Bearer YOUR_API_KEY"
}
}
}
}Agent Examples
These examples show a real session with an MCP-enabled agentic IDE or CLI. The agent connects to the RegressionBot MCP server. It handles polling, result interpretation, and image analysis automatically.
Legend: USER> is what the user types, AGENT> is the assistant response, TOOL> is an MCP tool call, and TOOL RESULT> is raw tool output.
Compare a preview URL against production baselines
USER> start a visual regression job for https://staging.myapp.com against stored baselines
AGENT> I'll trigger a visual regression job using the RegressionBot MCP.
TOOL> [run_regression_job]
testOrigin: "https://staging.myapp.com"
project: "myapp"
devices: ["Desktop Chrome", "iPhone 12"]
TOOL RESULT> {
"message": "Job started",
"jobId": "6ee84bac-40de-44cc-8dca-4760ab108153"
}
TOOL> [get_job_status] jobId: "6ee84bac-40de-44cc-8dca-4760ab108153", waitSeconds: 25
# The call held open for 25s and the job is still going. Call again β
# no sleep needed in between.
TOOL RESULT> {
"jobId": "6ee84bac-40de-44cc-8dca-4760ab108153",
"status": "COMPLETED",
"readyForSummary": false,
"isComplete": true,
"summaryStatus": "PROCESSING",
"progress": { "total": 6, "completed": 6, "percent": "100.00" },
"executionTime": 41
}
TOOL> [get_job_status] jobId: "6ee84bac-40de-44cc-8dca-4760ab108153", waitSeconds: 25
# Returned as soon as the summaries landed, without using the full 25s.
TOOL RESULT> {
"jobId": "6ee84bac-40de-44cc-8dca-4760ab108153",
"status": "COMPLETED",
"readyForSummary": true,
"isComplete": true,
"summaryStatus": "COMPLETE",
"progress": { "total": 6, "completed": 6, "percent": "100.00" },
"executionTime": 48
}
TOOL> [get_job_summary] jobId: "6ee84bac-40de-44cc-8dca-4760ab108153", regressionsOnly: true
TOOL RESULT> {
"regressionCount": 2,
"matchCount": 4,
"newBaselineCount": 0,
"regressions": [
{
"url": "https://staging.myapp.com/pricing",
"variantName": "Desktop Chrome",
"status": "SUCCESS",
"diffPercentage": 3.64,
"visualMatchScore": 96.36,
"changed": true,
"isNewBaseline": false,
"regressionbotSummary": [
{"label":"","text":"The Pro tier card gets a highlighted border and a blue call-to-action button."},
{"label":"A","text":"background-color: rgb(156, 163, 175) β rgb(37, 99, 235)"}
],
"diffUrl": "https://screenshots.regressionbot.com/r/token"
},
{
"url": "https://staging.myapp.com/pricing",
"variantName": "iPhone 12",
"status": "SUCCESS",
"diffPercentage": 5.12,
"visualMatchScore": 94.88,
"changed": true,
"isNewBaseline": false,
"regressionbotSummary": [
{"label":"","text":"The Pro tier card gets a highlighted border and a blue call-to-action button."},
{"label":"A","text":"background-color: rgb(156, 163, 175) β rgb(37, 99, 235)"}
],
"diffUrl": "https://screenshots.regressionbot.com/r/token"
}
]
}
AGENT> The job completed in 48 seconds. I found 2 regressions:
β’ /pricing [Desktop Chrome] β 3.64% of the page
The Pro tier card's button background changed from grey (rgb(156, 163, 175))
to blue (rgb(37, 99, 235)), and the card gained a highlighted border.
β’ /pricing [iPhone 12] β 5.12% of the page
The same button colour and card border change as on desktop.
AGENT> The other 4 pages matched their baselines exactly.
AGENT> Would you like me to fetch the regression images and review them before approving?Inspect a regression image and approve
USER> yes, look at the pricing regression and tell me if the changes look intentional
AGENT> [reading changes for /pricing Desktop Chrome, fetching currentUrl and cropping each box...]
AGENT> I can see both changed regions.
AGENT> The regressions are clearly intentional:
- The Pro plan card now has a visible border and background highlight
- The "Get started" button changed from grey to blue
- No layout shifts, broken text, or clipped elements
AGENT> This looks like a deliberate design update. Safe to approve.
USER> approve it
TOOL> [approve_job] jobId: "6ee84bac-40de-44cc-8dca-4760ab108153"
TOOL RESULT> {
"message": "Job approved",
"approvedCount": 2,
"failedCount": 0
}
AGENT> Done. The 2 updated screenshots have been promoted to new baselines.
AGENT> Future jobs will compare against the new design.Provide PR intent for automated visual approval
USER> Check the UI for regressions. The PR updates the header background to blue.
AGENT> I'll run the regression job and pass the developer's intent via runContext so the AI can automatically classify the change.
TOOL> [run_regression_job]
project: "myapp"
runContext: { "prDescription": "The PR updates the header background to blue." }
TOOL RESULT> { "message": "Job started", "jobId": "a1b2c3d4-..." }
AGENT> [get_job_status with waitSeconds: 25 until readyForSummary is true...]
AGENT> The summaries are ready. I'll read the verdict.
TOOL> [get_job_summary] jobId: "a1b2c3d4-...", regressionsOnly: true
TOOL RESULT> {
"regressionCount": 1,
"intentAssessment": {
"decision": "pass",
"intentProvided": true,
"bugCount": 0,
"intentionalCount": 1,
"noiseCount": 0,
"needsReviewCount": 0,
"allAccountedFor": true,
"summary": "All 1 regression(s) match the stated intent. Safe to approve."
},
"regressions": [
{
"url": "https://staging.myapp.com/",
"variantName": "Desktop Chrome",
"verdict": {
"decision": "intentional",
"minConfidence": 0.94,
"basis": "measured",
"reasoning": "The header background-color changed from #ffffff to #1d4ed8 and nothing else moved.",
"coveredBy": "updates the header background to blue"
}
}
]
}
AGENT> intentAssessment.decision is "pass": the one changed page has a verdict of
AGENT> "intentional" that quotes your intent back. Nothing sits at bug or needs_review.
AGENT> I can approve the job, or you can open the inspect view first.Run multiple jobs in parallel across devices
USER> test the PR preview against baselines on desktop, mobile, and tablet
AGENT> I'll start 3 parallel jobs β one per device β then poll all of them.
TOOL> [run_regression_job] project: "myapp", devices: ["Desktop Chrome"]
TOOL RESULT> { "jobId": "31c66aa4-5bd7-4df1-a8e3-1394d77a33f9" }
TOOL> [run_regression_job] project: "myapp", devices: ["iPhone 12"]
TOOL RESULT> { "jobId": "1a597938-b08e-4417-9802-cbb457870c96" }
TOOL> [run_regression_job] project: "myapp", devices: ["iPad Mini"]
TOOL RESULT> { "jobId": "d5452cb0-6b7a-4186-a0b1-0e23204a31e3" }
TOOL> [get_job_status] jobId: "31c66aa4-5bd7-4df1-a8e3-1394d77a33f9"
TOOL RESULT> { "status": "PROCESSING", "isComplete": false, "progress": { "total": 3, "completed": 1, "percent": "33.33" } }
TOOL> [get_job_status] jobId: "1a597938-b08e-4417-9802-cbb457870c96"
TOOL RESULT> { "status": "PROCESSING", "isComplete": false, "progress": { "total": 3, "completed": 2, "percent": "66.67" } }
TOOL> [get_job_status] jobId: "d5452cb0-6b7a-4186-a0b1-0e23204a31e3"
TOOL RESULT> { "status": "PROCESSING", "isComplete": false, "progress": { "total": 3, "completed": 0, "percent": "0.00" } }
...
TOOL> [get_job_status] jobId: "31c66aa4-5bd7-4df1-a8e3-1394d77a33f9"
TOOL RESULT> { "status": "COMPLETED", "isComplete": true, "progress": { "total": 3, "completed": 3, "percent": "100.00" } }
TOOL> [get_job_status] jobId: "1a597938-b08e-4417-9802-cbb457870c96"
TOOL RESULT> { "status": "COMPLETED", "isComplete": true, "progress": { "total": 3, "completed": 3, "percent": "100.00" } }
TOOL> [get_job_status] jobId: "d5452cb0-6b7a-4186-a0b1-0e23204a31e3"
TOOL RESULT> { "status": "COMPLETED", "isComplete": true, "progress": { "total": 3, "completed": 3, "percent": "100.00" } }
TOOL> [get_job_summary] jobId: "31c66aa4-5bd7-4df1-a8e3-1394d77a33f9", regressionsOnly: true
TOOL RESULT> {
"regressionCount": 1,
"regressions": [{ "url": "https://staging.myapp.com/about", "variantName": "Desktop Chrome", "diffPercentage": 1.8, "regressionbotSummary": [{ "label": "A", "text": "\"Meet the team\" β \"Meet our team\"" }] }]
}
TOOL> [get_job_summary] jobId: "1a597938-b08e-4417-9802-cbb457870c96", regressionsOnly: true
TOOL RESULT> {
"regressionCount": 1,
"regressions": [{ "url": "https://staging.myapp.com/about", "variantName": "iPhone 12", "diffPercentage": 2.4, "regressionbotSummary": [{ "label": "A", "text": "\"Meet the team\" β \"Meet our team\"" }] }]
}
TOOL> [get_job_summary] jobId: "d5452cb0-6b7a-4186-a0b1-0e23204a31e3", regressionsOnly: true
TOOL RESULT> { "regressionCount": 0, "regressions": [] }
AGENT> Results across all devices:
β’ /about [Desktop Chrome] β 1.8% of the page
Heading text changed: "Meet the team" β "Meet our team"
β’ /about [iPhone 12] β 2.4% of the page
The same heading edit as on desktop.
AGENT> β’ All tablet pages matched baselines.Recover a previous job by listing recent jobs
USER> what was the result of the last job for the myapp project?
TOOL> [list_jobs] project: "myapp", limit: 10
TOOL RESULT> {
"jobs": [
{
"jobId": "6ee84bac-40de-44cc-8dca-4760ab108153",
"project": "myapp",
"status": "APPROVED",
"totalUrls": 6,
"completedCount": 6,
"createdAt": "2026-05-19T10:53:26.000Z",
"finishedAt": "2026-05-19T10:54:14.000Z"
},
{
"jobId": "e69062bb-f140-40e7-a3e8-40f84deadcdb",
"project": "myapp",
"status": "COMPLETED",
"totalUrls": 6,
"completedCount": 6,
"createdAt": "2026-05-17T13:22:01.000Z",
"finishedAt": "2026-05-17T13:22:06.000Z"
}
]
}
AGENT> Most recent: job 6ee84bac β APPROVED, 6 pages tested, finished ~1 hour ago.
AGENT> The job before that is COMPLETED but not yet approved (2 regressions pending).Tip: The agent reads changes first: each entry names the element, both versions of its text and a box in capture pixels, so it can crop currentUrl and baselineUrl to the exact region when it wants to look. The shortened, clickable diffUrl is the link for a person: in a browser it opens the inspect view with every change drawn on both captures. Fetched directly it returns the current capture; there is no separate diff image.
Projects
A project is a named configuration. It locks in your job parameters, such as testOrigin, devices, and paths. The first run_regression_job invocation with a new project name creates the configuration automatically. Subsequent jobs enforce that the parameters match. This protects baseline integrity.
How it works
- Initial job β supply all parameters. RegressionBot creates the project configuration and captures baselines.
- Repeat jobs β omit parameters or supply identical ones to reuse the stored configuration. A partial request is checked on what it sends and filled from the project on what it omits, so naming a project with only
testOriginruns the saved paths, devices, masks andcustomCss. If you pass parameters that differ from the stored configuration, the job is rejected. The error response lists the differing fields. - Shorthand β use
run_from_projectto trigger a repeat job without supplying any parameters. - Update configuration β use
update_projectto update a saved project's configuration. Changing anything that alters a capture resets its baselines. Example:update_project({ name: "my-project", testOrigin: "https://v2.example.com" }) - Check a site on a schedule β the same tool turns a project into an unattended check:
update_project({ name: "my-project", schedule: "daily", baselinePolicy: "rolling" }). It then runs without anyone triggering it, and emails your organisation's owners when it finds changes or cannot check the site at all. Unlike a capture-affecting update this leaves baselines intact, and the first run starts within the hour rather than a day later. This is what catches changes no deploy caused. - Approve by name β use
approve_projectto approve the most recent completed job for a project. This does not require a job ID. Example:approve_project({ name: "my-project" }) - Live-vs-live jobs β these are always accepted regardless of the stored configuration. Passing
baseOriginskips the configuration check entirely, and nothing is filled from the project either; send every field you want.
Managed CI workflow example
USER> start a regression job for myapp
AGENT> I'll create the project on the initial job and capture baselines.
TOOL> [run_regression_job]
project: "myapp"
testOrigin: "https://www.myapp.com"
devices: ["Desktop Chrome", "iPhone 12"]
paths: [{"path":"/"}, {"path":"/pricing"}, {"path":"/docs"}]
autoApprove: true
TOOL RESULT> { "message": "Job started", "jobId": "a1b2c3d4-..." }
AGENT> Baselines are being captured for 3 pages on desktop and mobile.
AGENT> The configuration is now saved as project "myapp". Future jobs are locked to these parameters.
---
USER> start a regression job for myapp
TOOL> [run_from_project] name: "myapp"
TOOL RESULT> { "message": "Job started", "jobId": "e5f6a7b8-...", "project": "myapp" }
AGENT> Job started using stored config β testOrigin myapp.com, Desktop Chrome + iPhone 12, 3 paths.
---
USER> what projects do I have?
TOOL> [list_projects]
TOOL RESULT> {
"projects": [
{
"name": "myapp",
"testOrigin": "https://www.myapp.com",
"devices": ["Desktop Chrome", "iPhone 12"],
"lastRunAt": "2026-06-02T10:15:00.000Z",
"lastJobId": "e5f6a7b8-..."
}
]
}Intent and the Verdict
RegressionBot answers a question the diff alone cannot: did this run do what the change said it would? Give the run an intent and every changed page comes back with a verdict against it. Give it none and nothing is judged, which every surface says out loud rather than leaving blank.
Give the run an intent
Pass runContext to run_regression_job or run_from_project. The PR title and description, or the commit message, are what people already write; two lines saying what changed and why are enough. A vague intent (βfixesβ, βupdatesβ) earns needs_review, not a pass.
A standing intent for scheduled checks
A scheduled run has no PR to draw an intent from, so it was never judged. Give the project one instead: update_project with intent, a line or two saying what the pages are expected to do between checks and what must not change β βContent is edited in the CMS daily; navigation, pricing and layout must not change.β Every scheduled run is judged against it. Runs you start yourself are not β give those a runContext. The same field is on the dashboard's Schedules tab. Setting it does not invalidate baselines.
The two tools name the project differently. run_regression_job takes project; run_from_project takes name. The runContext object is identical in both.
run_regression_job {
"project": "docs",
"runContext": {
"prTitle": "Remove the View All Cards link from the Strata hero",
"prDescription": "Marketing asked for the hero to carry one call to action. Nothing else on the page changes."
}
}
run_from_project {
"name": "docs",
"runContext": { "prTitle": "Remove the View All Cards link from the Strata hero" }
}isNewBaseline: true, no page is compared, and so nothing is judged β intentAssessment.decision will not read pass no matter how good the intent was. Run a second time for a real verdict, or pass autoApprove: true on the first run to prime the baselines deliberately. Live-vs-live mode (pass baseOrigin) captures both origins every run, so it has no first-run trap.Read the verdict
intentAssessment.decisiononget_job_summaryis the one field a CI step reads:pass,review,fail, ornot_judgedwhen the run carried no intent. The counts beside it explain the decision.verdicton each regression:decision(intentional,bug,noise,needs_review),minConfidence, one sentence ofreasoning, and forintentionalthe words of the intent itcoveredBy, quoted verbatim. An intentional verdict whose quoted words are not in the intent is downgraded toneeds_reviewbefore you see it. So is a page with several edits unless the intent names every one of them: an intended heading change does not make an accidental nav or colour change on the same pageintentional, and thereasoningsays which edit was not named.minConfidenceruns from 0 to 1, not 0 to 100. A verdict at0.94is a confident one. Compare it against a fraction:minConfidence < 0.8, never< 80.verdict.basissays what the decision was made from.measured: the exact edits inchanges.described: only a generated sentence about the picture, because the DOM engine could not run on that page. Treat a described verdict as weaker evidence. The field is optional and is absent on some pages, so read it with a default rather than assuming it is there β its absence tells you nothing about which of the two applied.- A changed page can carry no
verdictobject at all. That page was never assessed. Treat it asneeds_review; it is never a pass. In code, branch on the object's presence before you readdecisionoff it:const cleared = (page) => page.verdict?.decision === 'intentional' || page.verdict?.decision === 'noise'; // no verdict β cleared is false β the page waits for a person
Act on it
A verdict is a recommendation. Approving and rejecting remain the only actions that move a baseline. After a review or fail, approve only the pages the verdict cleared and leave the rest for a person:
approve_job { "jobId": "β¦", "where": { "decision": ["intentional", "noise"] } }
β {
"message": "Job approved",
"approvedCount": 3,
"failedCount": 0,
"skippedCount": 2
}skippedCount is returned only when you pass where. It counts the changed pages left pending because their verdict was outside your list, or because they had no verdict at all. The same three forms over REST are POST /approve, which reports the counts under different names β approvedUrlsCount, skippedUrlsCount and conflictedUrls. A live-vs-live run under a name with no saved project has no stored baseline to advance: approving it records the decision, copies nothing, and returns warning: "NO_BASELINE". To keep a baseline, run the project without baseOrigin.
To act on one page, pass url, variantName and action; add a note when your action disagrees with the verdict. Every approve or reject is recorded on the page as triageStatus (PENDING, APPROVING, APPROVED or REJECTED), triagedAt, triagedBy and triagedVerdict, the verdict as it stood when the decision was made. A note you send reads back on the page as triageNote.
Reading a Job Summary
Call get_job_summary once get_job_status reports readyForSummary: true. The job carries regressionCount (changed pages, not counting new baselines), newBaselineCount, runSummary (an AI reading of the whole run in two or three sentences, the same one the scheduled-run email opens with, or null; the changes are the record) and, on runs with an intent, intentAssessment. If newBaselineCount equals the number of pages tested, nothing was compared. Each entry in regressions carries the fields below.
| Field | Meaning |
|---|---|
changes | The exact edits, computed from the two documents: the answer to "what changed". Each entry has type (text-edit, insert, delete, move, move-with-edit, style-only, image-change, attribute-edit, sticky-reposition, font-failed, and the metadata types meta-edit, meta-insert, meta-delete, schema-edit, schema-insert, schema-delete), element (the tag, or a metadata field such as title, meta[description], link[rel=canonical], ld+json[FAQPage]) and whichever of before, after, style and attributes (a changed link target, alt text, aria-label and the like, as [before, after]) apply. On an image-change, before and after are the two image file names. A change with no words of its own (a style, attribute or image change) carries label: what a reader calls the element, such as a button's text or an image's alt text. An edit where only a date or time changed carries valueOnly: "date" and does not make the page changed. Parts of the page that changed on their own while it was captured (a carousel, a counter) are never listed. On results from before September 28, 2026, changes can be absent because the DOM engine could not run on the page, which is not the same as nothing having changed. |
box / boxBefore | On each non-metadata entry: where the element is now, in capture pixels, and where it was (present only when its position or size differs; subtract for the move vector, compare w/h for a resize). Metadata entries carry no box because they paint nothing, so filter on box before assuming one. Combine box with currentUrl to crop the region; filter on box.y to act only above the fold. |
collapsed | true when the change sits inside a closed <details>: in the document but painted in neither capture, so it has no box. Report it as "inside a collapsed section". |
changed | Whether the page counts as a change: an element, its text, a compared style, an image or a reader-facing attribute changed (contentChanged), or the page's metadata did (metadataChanged). Parts that changed on their own while the page was captured, and edits to dates alone, do not count. Read this rather than thresholding diffPercentage. |
contentChanged | Present and true when the page's content changed: anything in changes other than metadata. |
metadataChanged | Present and true when the head or structured data changed while nothing visible moved: a canonical rewritten, a title edited, a JSON-LD block dropped. Nothing shows in the images; read changes. Absent on a page whose baseline predates metadata recording, which is silence rather than "no change". |
visualMatchScore | Deprecated. 100 minus diffPercentage; it no longer measures how alike two pictures look. Kept for existing callers; read changed and changes instead. The summary's overallScore is deprecated the same way. |
diffPercentage | Share of the page, in percent, covered by the elements that changed (the union of their boxes). It says how much of the page changed, not whether the change matters, so do not threshold on it. Results from before September 28, 2026 (kept 30 days) were compared by pixels; on those, this and visualMatchScore are pixel measures. |
regressionbotSummary | Prose context: an array of {label, text}, where label is the region letter, or empty for the one-line overview or a whole-page change. Each region line is written from the DOM and quotes the page's own text. Jobs from before v2.1 carry a plain string. Use it only where changes is absent. |
domAssistSkipReason | Only on results from before September 28, 2026, when pages were compared by pixels and the DOM could not be read for this one. On such a page, the absence of a structural finding is not evidence there was none. Current results never carry it: a page whose DOM cannot be read ends in ERROR. |
isNewBaseline | The page was captured for the first time and compared with nothing. When every page is new on an established project, the likelier cause is invalidated baselines, not a first run: check the project's baselineInvalidatedAt. |
diffUrl, baselineUrl, currentUrl | diffUrl is the link to give a person: in a browser it opens the inspect view with every change drawn on both captures; fetched directly it returns the current capture, not a diff image. baselineUrl is the reference capture (the stored baseline, or the baseOrigin capture in live-vs-live) and currentUrl the new one. All are pre-signed, need no auth and expire after an hour. |
verdict | Only on runs with an intent: decision (intentional, bug, noise, needs_review), minConfidence (0 to 1), basis (measured from the DOM edits, or described from a picture description, weaker), reasoning, and for intentional pages coveredBy, the words of the intent that name the change. See Intent and the Verdict above. |
triageStatus and friends | triageStatus, triagedAt, triagedBy, triagedVerdict and triageNote: a person's Approve or Reject on the page, and what the verdict said when they clicked. |
Job records, images and share links are kept for 30 days, then deleted. Baselines never expire. For the traps that cost the most time, see gated environments and baseline invalidation.
Available Tools
Once connected, your agent has access to the following tools. The MCP server provides instructions automatically. This helps your agent understand the workflow without any additional prompting.
| Tool | What it does |
|---|---|
| run_regression_job | Start a visual regression job. Accepts explicit paths, a sitemapUrl to crawl, or glob scans to filter a sitemap. Supports multiple devices, element masks, an optional baseOrigin for live-vs-live mode comparisons, and an optional runContext object containing developer intent (e.g. prDescription, gitCommitMessage) to inform the AI verdict. Pass customCss to inject CSS that hides dynamic widgets or repositions a fixed element that renders somewhere unhelpful at rest (max 4096 chars). |
| get_job_status | Report progress, optionally waiting. Pass waitSeconds: 25 and the call stays open until the job settles or 25 seconds pass, so an agent calls it a handful of times instead of polling. Wait on readyForSummary: true once the job has finished and its summaries are written, which is when get_job_summary gives the final answer. Also returns isComplete (captures and comparisons done; summaries may still be running), status, summaryStatus, progress, executionTime and any error. A finished job ends in one of four statuses: COMPLETED, APPROVED (every change approved, including automatically on a rolling project), RESOLVED (every change decided, at least one rejected) or FAILED. Waiting on COMPLETED alone never finishes for the other three. |
| get_job_summary | Get full results once the job is complete. Returns summaryStatus alongside results. If the status is 'COMPLETE', regressions include the AI-written regressionbotSummary and verdict fields, and the job includes the top-level intentAssessment for CI gating. Each regression also includes changes β the exact edits the engine computed, each naming the element, both versions of its text, and where it sits in the capture β plus changed, visualMatchScore, and shortened screenshot redirect URLs. Read changes before the prose: it is measured, so it carries no confidence score, and it is what lets an agent quote exactly what moved. Pass regressionsOnly: true to filter out matching pages. |
| list_jobs | List recent jobs ordered by most recent first. Filter by project or set a limit. Useful for recovering a jobId or checking the status of recent jobs without leaving the agent session. |
| approve_job | Promote current screenshots to baselines. This is permanent. Existing baselines are overwritten. Three forms: { jobId } approves every page; { jobId, where: { decision: [β¦] } } approves only the changed pages whose verdict.decision is in the list; { jobId, url, variantName, action, note } approves or rejects one page. Only call after reviewing regressions in get_job_summary. |
| run_from_project | Run a job using a saved project config. Identifies the project by name, not project β that is the one parameter difference from run_regression_job. No need to re-supply testOrigin, devices, or paths. Ideal for repeat CI jobs once a project is established. Accepts optional autoApprove and runContext. |
| get_project | Retrieve the stored configuration for a named project. Returns testOrigin, devices, paths, scans, and job metadata. |
| list_projects | List saved project configs for your organization. Returns name, testOrigin, devices, schedule, baseline state and last job info for each β including nextRunComparesAgainstBaseline, which answers whether a run now would detect regressions or re-capture baselines. Live-vs-live runs store no config, so a project used only that way appears in list_jobs but not here. |
| update_project | Update a saved project's configuration and reset its baselines. Pass any subset of testOrigin, devices, paths, masks, customCss, etc. Also where unattended checks are turned on: schedule (daily, weekly) alongside baselinePolicy: "rolling" β see Scheduled Checks. Changing what a capture looks like β origin, paths, devices, masks, custom CSS, credentials β invalidates baselines and the next job re-captures them. Schedule, policy, concurrency and AI options do not. |
| approve_project | Approve the most recent completed job for a project, promoting its screenshots to baselines. No job ID required. Ideal for CI workflows where the approval step is executed separately from the regression job check. |