MCP server — browse guides

The Vera MCP server

Give an AI coding agent credentialed access to your tests over MCP: minting a token, pointing a client at the server, the tool set, and the limits.

Using this from Claude Code? The Vera plugin wraps this surface in a skill plus /vera:verify, /vera:cover, /vera:fix, /vera:repro, /vera:check and /vera:status, so the procedure does not have to be re-explained each session.

Vera exposes its projects, tests, runs, and AI repair/generation as an MCP server so any MCP client — Claude Code, Claude Desktop, another agent — can drive Vera directly: run a test, read why it failed, propose a fix, apply it (with your approval), and re-run. This is the practical "self-fixing" loop: point your coding agent at both your app's repo and Vera, and it can close the loop from a failing test to a green one.

  • Endpoint: POST /api/mcp (Streamable HTTP, stateless — no session to manage).
  • Auth: a vera_… bearer token (the same tokens the CI API uses), scoped to your organization and fenced to /api/mcp.
  • Isolation: every tool is org-scoped by the token — a token for org A can never see or touch org B's data.

1. Mint a token

In the dashboard: Settings → CI Tokens → name it → Create. Optionally set an expiry; leave org-wide unchecked for a personal token (audit rows and usage counters then name a human rather than a shared actor).

The plaintext token (vera_…) is shown once — copy it now. Revoking it in the dashboard takes effect immediately (the next MCP call with it returns 401), and so does an expiry passing.

Plans: the MCP surface is the free tier's floor (mcp-basic), so a free cloud workspace can mint a token and use it here. The /api/v1 CI surface is separate and needs Pro (api-access) — the same token, fenced by URL. Self-hosted installs have no billing and hold everything.

2. Point your client at it

npx vera-agent init --url https://your-vera-host --token vera_your_token

This registers a vera HTTP MCP server for the repo you run it in — along with linking the repo to a project and writing the agent-instructions block. See Getting started for the whole flow, including the .vera/config.json (committed) vs ~/.vera/cli.json (token, chmod 600) split.

Two registration scopes:

  • --mcp-scope local (default) — claude mcp add at local scope. That entry lives in ~/.claude.json, private to you and outside the repo, so it carries the real token and needs nothing else set up.
  • --mcp-scope project — merges a vera entry into the repo's committed .mcp.json, so everyone who clones gets the tools. The entry carries the literal ${VERA_TOKEN}, never a secret; each developer exports their own (export VERA_TOKEN=vera_…). Claude Code asks once to approve a project-scope server before connecting.

init never clobbers: it only ever touches the vera key under mcpServers, leaves every other server alone, refuses (rather than overwrites) a .mcp.json it cannot parse, and reports "already up to date" on a re-run. If a vera server is already registered in any scope, it leaves it as it is and tells you the fix-it (claude mcp remove vera, then re-run).

Already using the Vera plugin? It ships its own vera server entry driven by VERA_MCP_URL + VERA_TOKEN — you don't need both, so run init --no-mcp and set those two variables instead.

Manual — other MCP clients, or no claude CLI

init prints this command when the claude CLI isn't on PATH:

claude mcp add --transport http vera https://your-vera-host/api/mcp \
  --header "Authorization: Bearer vera_your_token"

Or write the config by hand — a project .mcp.json, or Claude Desktop's MCP config, or any other Streamable-HTTP MCP client:

{
  "mcpServers": {
    "vera": {
      "type": "http",
      "url": "https://your-vera-host/api/mcp",
      "headers": {
        "Authorization": "Bearer vera_your_token_here"
      }
    }
  }
}

For a local server the URL is http://localhost:4545/api/mcp. In a committed file use "Bearer ${VERA_TOKEN}" and export the token in your environment. Restart the client and the vera tools appear.

3. Tools

Read-only:

ToolArgumentsReturns
list_projects—every project in your org (id, name, base URL)
list_testsprojectIdthe project's tests (id, name, step count, source, quarantine/flake)
get_testtestIdthe test with its full steps, config and source
get_runrunIda run: status, duration, error, per-step results
get_failure_detailsrunIdfailed step(s) + errors, a pass/fail/healed summary, artifact paths — the debugging payload
get_load_runloadRunIda load run: status, threshold verdict and the measured p50/p95/p99, request rate and error rate. Figures that were not measured are null, never 0.
list_flowsprojectIdthe project's reusable flows (id, name, description, step count)
list_rulesprojectIdthe project's approved business rules (id, entity, kind, statement, file:line evidence). Rules awaiting human approval are deliberately not listed; the ids are what ruleIds on create_test/generate_tests takes.
list_issuesprojectId?, status?, severity?, source?, limit?, offset?issues in your org (newest first), paginated. Each row adds bugConfirmed (its repro reproduced the bug) and reproProposedBy (auto-repair or null). Needs issues:read.
get_chain_runchainRunIda cross-project chain run: status, duration, per-segment statuses, masked final variables
check_coverageprojectId, changedFiles[], alwaysRunTag?the change impact map: which tests cover these files, which touched business rules nothing protects, which files could not be placed, and mapCoverage. Deterministic and un-metered — no AI call, no browser. Same joins, bounds and refusal codes as POST /api/v1/projects/:id/impact; see below.
get_app_changesprojectId, since?the app-change radar's open feed: what changed in the app itself since the accepted baseline, with the tests each change puts at risk. Deterministic and un-metered. Pro+ (app-radar). Read state before the list — see below.

source — who wrote the test. list_tests and get_test report it: manual (a person, in the builder or through an importer), ai (Vera's generators, including an AI-assisted import), mcp (created or edited through these tools), or absent for a row created before Vera recorded provenance, or holding a value this build does not recognise. Absent means not stated — never assume manual. It is also not a review signal: a test can be ai and approved, or manual and never looked at by anyone but its author. recorded is defined but nothing writes it today, so a test captured in the recorder reports manual.

Actions:

ToolArgumentsNotes
run_testtestId, environment?starts the run and returns {runId, status:'running', url?} immediately — it does NOT wait. Call wait_for_run for the outcome. Design-review / notify hooks are skipped for MCP runs.
wait_for_runrunId, timeoutSeconds?the observation: status, per-step results, finalUrl, url?, warnings?, plus console/network tails and artifact refs on failure. Returns status:'running' past its timeout instead of blocking — call again.
cancel_runrunId, kind?, scope?asks a run to stop — a single run, or (with kind) a chain, suite or saved Actor Scenario run as a whole. Cooperative, not a kill: an executing run ends at its next STEP BOUNDARY (cancelled: false in the reply means the stop was asked for and not yet observed — poll wait_for_run), and a run leased by a local agent takes up to one 20s heartbeat on top. A run that already passed or failed is not cancelled — that is an error result (not_cancellable), because a verdict is evidence. Safe to call twice. scope: "group" stops every row of the run's DATASET group and reports each one; it is refused for a run with no group rather than stopping a single row. kind defaults to run; chain / suite / scenario stop the aggregate (remaining members are not started, ones in flight stop at their next step). scope: "group" applies to kind: "run" only and is refused on an aggregate. A scenario stop needs the scenarios:run permission — MCP tokens carry it, but a personal token never exceeds its owner's role, so a member without it is refused with a message naming it.
inspect_pageprojectId, url?, environment?, role?, maxElements?, includeAria?, ariaMaxChars?loads the page as the configured role and returns its interactive elements with stable selectors. Every selector rung except the CSS-path fallback is checked to match exactly one element; a duplicate (two buttons sharing a data-testid, a repeated aria-label or button text) falls through to the next rung. includeAria: true adds ariaSnapshot — the page's accessibility tree (roles and accessible names, field values redacted), capped at ariaMaxChars (default 8,000) on a line boundary with a # … truncated marker, or null when the engine could not produce one. Note: interactive only — it does not return arbitrary text nodes, so title/url are the assertable values it hands you directly.
verify_selectorprojectId, selectors[]?, locators[]? (≤20 in total), url?, environment?, role?checks selectors you wrote against the live page: per selector, how many elements it matches, unique (exactly one — what a step needs), whether the first match is visible, and the parser's error if it is not a valid selector. A locator is {role, name?}, {text}, {label}, {placeholder} or {testId}, each with optional exact; Vera writes it as the selector its own recorder would (role=button[name="Save"], text=…, [placeholder="…"], data-testid=…) and probes that. exact only changes text (quoted, case-sensitive whole text); the others are whole-value matches already. A label is resolved as role + accessible name across the form-control roles, because label= is not a selector engine in Vera. When a locator matches exactly once the result carries a step ({id, action, selector, value?} — fill for text inputs, check for checkboxes/radios, otherwise click) that create_test accepts as-is; otherwise it carries count and runOutcome, what a run would do (0 matches fails on timeout; several matches does not fail — it acts on the first). Same page load and guards as inspect_page; free, no AI call.
run_scenarioprojectId, steps[] (max 30), environment?, role?, label?runs a one-off, unsaved step list in one project, returns {runId} immediately. Not an Actor Scenario despite the name — see Journeys, chains and Actor Scenarios. One in flight per workspace plus an hourly cap; both refuse with a named code.
save_scenario_as_testrunId, name, tags?promotes a scenario run into a durable test using the steps that actually ran. Returns warnings? when those steps prove nothing.
list_runsprojectId, limit?, offset?the project's unified run feed — test-backed AND scenario runs.
list_suitesprojectIdthe project's suites. Not org-wide: from cold, list_projects first.
list_chains / run_chain / get_chain_run— / chainId / chainRunIdcross-project chains (Team+).
list_project_rolesprojectIdthe project's auth roles (id + name), its default role and its named environments — the ids roleId / environmentName / roleBindings / run_scenario's role expect. Never a credential.
get_chainchainIdone chain's full definition (segments + starting variables). Read before update_chain. Team+.
create_chainname, segments[], description?, variables?saves a chain of existing tests, possibly across projects. Every projectId/testId is checked before the save. Start it with run_chain. Team+.
update_chainchainId, name?, description?, segments?, variables?, confirmany field passed replaces the stored one (segments is the whole list). Requires confirm: true. Team+.
list_scenarios / get_scenario— / scenarioIdsaved Actor Scenarios: a summary list, and one full definition. Team+.
create_scenarioname, actors[], stages[], description?, variables?, cleanup?, execution?saves an Actor Scenario. Validated exactly as the editor validates it; a refusal carries the list of problems. Inline stage steps go through the step schema. Team+.
update_scenarioscenarioId + the full definition, confirmreplaces the scenario — not a patch. Requires confirm: true. Team+.
run_actor_scenarioscenarioId, resumeFrom?starts a saved Actor Scenario and returns {scenarioRunId} immediately. Needs scenarios:run. Team+.
get_scenario_runscenarioRunIdthe run's status, chronological timeline and per-actor lanes. Poll it. Team+.
list_journeysprojectIdthe project's coverage-map journeys, each with linked tests and a status/health derived from their latest runs.
create_journeyprojectId, name, entryPage?, criticality?, roles?, recommendedScenarios?adds a journey to the coverage map (confirmed, no tests linked yet). Needs the coverage-map entitlement.
update_journeyjourneyId, name?, criticality?, linkedTestIds?, confirmed?, …edits a journey. linkedTestIds replaces the list, and every id must be a test of the journey's own project.
run_suitesuiteId, environment?runs every test in a suite and returns a per-test summary. Can take minutes. Quarantined tests run but never fail the suite.
run_load_testprojectId, name, scenario, profile, thresholds?queues a k6 load run against the project's base URL and returns its id straight away — poll get_load_run. Needs tests:run, and a plan whose monthly load-run allowance is above zero (Team and up): any other workspace gets a structured plan_limit refusal naming monthlyLoadRuns, where limit: 0 means the plan includes none and a positive limit means this month's allowance is spent.
suggest_fixrunIdproposes an AI step-diff for a failed run. Proposal only — applies nothing.
apply_repairtestId, runId, operations, confirmapplies approved operations and records the repair audit. Requires confirm: true.
generate_repro_testissueId, mode?, url?, hint?, authRoleId?, startLoggedOut?turns a bug report into a proposed failing test. Proposal only — persisted: false, nothing is saved. BLOCKS while it loads the page and calls the AI (typically 20–60s, up to ~4 min). The MCP SDK's default request timeout is 60s and this transport sends no progress notifications, so raise MCP_TOOL_TIMEOUT rather than retrying — a retry spends AI budget again and returns nothing extra.
approve_repro_testissueId, steps, confirm, name?, tags?, authRoleId?, environment?, replace?saves the reviewed steps as a regression-tagged test, links it to the issue, and runs it once. Requires confirm: true. Pass provingStepId to name the assertion that encodes the correct behaviour (omit it and Vera picks the last outcome or visible/hidden assertion; null names none). Read repro.status, never didNotReproduce: the values are reproduced (red at the proving step, on a value or state mismatch — this is the success case), inconclusive (red, but not for the stated reason: another step failed first, the element never resolved, or the run died — repro.detail names the failing step), not_reproduced (it passed first run, so it captures nothing), pending (the run outlived the request deadline — no verdict yet, and the payload's didNotReproduce: false does not mean it reproduced; call wait_for_run), and not_run (executed elsewhere: approve starts the first run there and returns its id as repro.runId, so call wait_for_run on it; its completion writes the verdict on the issue). A second approve returns 409 repro_already_linked unless you pass replace: true.
generate_testsprojectId, goal, count?AI-generates scenarios from a goal and saves them as tests (does not run them). Crawls the project base URL.
create_testprojectId, name, description?, stepscreates a test from validated steps, saved with source: 'mcp' (not run). Steps may include intent blocks.
update_test_stepstestId, steps, confirmreplaces a test's full step list. Requires confirm: true; the previous steps are snapshotted to the repair audit so the change is revertible. Steps may include intent blocks.
create_flowprojectId, name, description?, stepscreates a reusable flow from validated steps. Reference it from tests with a use-flow step. Needs flows:manage.
update_flowflowId, name?, description?, steps?, confirmupdates a flow. Requires confirm: true — every dependent test runs the new steps. Call without confirm first: the refusal names the dependents. Needs flows:manage.
delete_flowflowId, confirmdeletes a flow. Requires confirm: true. Tests still referencing it fail at flow resolution rather than silently skipping the steps. Needs flows:manage.
report_issuetitle, severity?, description?, projectId?, testId?, runId?, stepIndex?, chainRunId?, labels?files a Vera issue with source: 'mcp'. Needs issues:manage.
update_issueid, status?, title?, description?, severity?, labels?, confirmtransitions / edits an issue. Requires confirm: true. Needs issues:manage.

Intent blocks

An intent block keeps the goal of a stretch of steps next to the steps that achieve it. It opens with an intent step and closes with end-intent:

[
  { "id": "s1", "action": "goto", "url": "/login" },
  { "id": "s2", "action": "intent", "goal": "Sign in as the demo user", "values": ["email", "password"] },
  { "id": "s3", "action": "fill", "selector": "#email", "value": "{{email}}" },
  { "id": "s4", "action": "fill", "selector": "#password", "value": "{{password}}" },
  { "id": "s5", "action": "click", "selector": "button[type=submit]" },
  { "id": "s6", "action": "end-intent" },
  { "id": "s7", "action": "assert", "assertType": "url", "condition": "contains", "value": "/dashboard" }
]
  • goal is free text (up to 500 characters). values lists the names of the variables the goal may type (email, never {{email}} and never a value).
  • Blocks must balance and cannot nest; an if or loop inside one is fine. create_test and update_test_steps refuse an unclosed, stray or nested block with the step schema's own message, and save nothing.
  • Every run replays the steps between the markers as ordinary steps. When they break (or the block has none yet), a model may re-derive them from the goal in exactly two places: an explicit resolve (POST /api/tests/:id/resolve, which runs the test once on the cloud with the block allowed to resolve) and the repair ladder's self-heal re-run (the gardener, or Auto-repair on a failed run). Every other run — the builder's Run, CI, scheduled, MCP run_test and vera-agent — replays only, never calls a model, and fails the step intent-stale.
  • A resolution chooses each action from the controls on the live page first (TypeSafe, when enabled for the workspace) and asks your AI model only when that cannot decide. It may only drive the page on the project's own origin, and it types nothing but the block's declared values, by name. Both are plan-gated like auto-repair.
  • A resolution is saved into the block only when the run confirmed it: the block did what it did on its last green run, or an outcome assertion after end-intent passed. Then the steps that broke are replaced with the ones that worked, as a repair with strategy intent that Revert undoes. Anything else is staged in Staged repairs for review. The resolve call answers with the run plus resolution: { outcome: "written" | "proposed" | "none", steps?, proposalId?, reason? }. Once saved, every later run replays the new steps with no model call.
  • Follow each block with an assertion that proves its outcome. Without one the block can never be verified, and the assertion lint says so.

Journeys, chains and Actor Scenarios

Three words that are easy to cross, so here is what each one is and which tool makes it:

You wantIt isTools
An end-to-end journey that runs, one persona, tests in order (possibly across projects)a chain — dashboard: Journeys → Chainscreate_chain, run_chain, get_chain_run
A journey where different people act (an applicant applies, staff approve, the applicant sees it)an Actor Scenario — dashboard: Journeys → Scenarioscreate_scenario, run_actor_scenario, get_scenario_run
A named business flow on the coverage map, with the tests that protect ita journey — dashboard: Coverage. It is a label over tests and never runscreate_journey, update_journey
To try some steps once, saving nothinga scenario runrun_scenario → save_scenario_as_test

Build bottom-up: create_test for each leg, list_project_roles for the role ids, then create_chain or create_scenario over those tests, then create_journey + update_journey { linkedTestIds } so the coverage map shows what now protects the flow.

Two things are stricter here than in the dashboard, because the caller types ids instead of picking them from a list: every project, test and flow a definition names must exist in your workspace in the project the definition says (code: "unresolved_references", nothing saved), and a stage's inline steps must pass the step schema.

Running one. run_actor_scenario { scenarioId } returns { scenarioRunId } immediately; poll get_scenario_run (a timeline plus per-actor lanes, status: "running" until it ends) and stop it with cancel_run { kind: "scenario" }. resumeFrom keeps an earlier run's passed stages. It needs scenarios:run rather than tests:run — the permission exists so a workspace can withhold "drive several personas across projects". MCP tokens carry it, but a personal token is always intersected with its owner's current role, so a member whose role lacks scenarios:run does not see the tool.

check_coverage — a pointer, not a pipeline

check_coverage is the agent-facing half of vera check. It reports: it runs no test, generates nothing, and writes nothing. What it adds on top of the report is nextActions — one suggestion per finding, with the arguments already filled in (run_test for the impacted set, run_scenario for what nothing maps to, generate_tests for the gaps, carrying the ruleIds that make the result count as coverage next time). It makes none of those calls itself, deliberately: chaining a metered generation off a heuristic's guess is not something a tool named "check" should do on its own.

Four things it refuses to blur:

  • gaps vs partial. A gap is a touched rule with no adequate protection (uncovered / insensitive). partial is a rule that does have a covering test whose assertions were never proven sensitive to it — a weaker claim, not a failure. A sensitive verdict requires a dataset, so treating partial as a gap would paint every healthy project red. vera check --strict never fails on partial.
  • impactedTests vs alwaysRunTests. The tag join is reported in its own key, so "the map found these" stays distinguishable from "you told me to always run these".
  • Evidence confidence. high = a framework route file (Next app/pages, SvelteKit, Remix, Express) or a rule's own file:line. low = a route inferred from a filename — a guess. Each impacted test also carries a rolled-up high / mixed / guess-only.
  • Cold start. On a project whose tests carry no URLs and no rule links, coldStart.cold is true and coldStartFix names the single command that fixes it. An empty report on a cold project means the map is blind, not that the change is safe.

Bounds behave exactly as on the HTTP route, and all of them are reported (never silent): over 5000 paths or a path over 500 characters is a refusal (too_many_changed_files / path_too_long, nothing computed); list caps and the 25-line-per-item evidence cap truncate and say so in truncated[] / evidenceTruncated / warnings. One difference worth knowing: /api/mcp carries a single JSON-RPC body limit for every tool (1 MiB by default) where the HTTP impact route allows 4 MiB — the effective number comes back in bounds.maxRequestBodyBytes.

get_app_changes — read state before you read the list

check_coverage answers "I changed these files"; get_app_changes answers the other direction — the app changed and nobody told you. The radar re-fingerprints the project's pages on a cadence (structure only: tag/type/role/name/data-testid/id, never copy, so a wording change is not a change) and every difference from the accepted baseline becomes one row. Each row names the URL, the element-level diff, and impactedTests — the tests that click a moved element or navigate to the changed page. That last field is the reason to call it.

An empty list is two different answers, and state is which one:

stateWhat an empty list means
cleanGenuinely nothing open. The last sweep found nothing new.
never-sweptThe radar exists but has never run — no baseline, so no change can be detected.
not-configuredThis project has no radar at all. Nobody has looked.
changes-openNot empty: openChanges rows are waiting.

Dispositions are not symmetric, and neither the tool nor the dashboard presents them as two equal buttons:

  • expected — intended. Accepting advances the fingerprint baseline, so the row leaves the queue and that diff never fires again. This is the only way the feed reaches zero.
  • suspect — a possible regression. The row stays open and the baseline does not move, on purpose. Marking suspect is not a way to clear the feed.

The tool is read-only: it sets no disposition (that is the project's Radar panel, or POST /api/radar/changes/:id/disposition) and, unlike the HTTP GET, it never creates a radar row as a side effect. Bounds are reported, never silent: at most 50 rows (limit, with notShown when the feed is longer), 10 impacted tests and 10 elements per row (each list carries its own notShown), plus any warnings the sweep recorded. An invalid since is refused (invalid_since) rather than silently compared as a string, which would return a plausible but wrong window.

The confirm-to-apply rule

Vera never rewrites a test or mutates an issue without explicit approval. apply_repair, update_test_steps, and update_issue all refuse unless you pass confirm: true — call once to review, then again with confirm: true to persist. The typical repair loop:

run_test → (failed) → get_failure_details → suggest_fix
  → review the operations → apply_repair(confirm: true) → run_test → green

When a green fix can't be found, close the loop by filing a tracked issue instead of leaving the failure to rot:

run_test → (failed) → get_failure_details → suggest_fix
  → apply_repair(confirm: true) → run_test → (still failing)
  → report_issue(testId, runId, severity: 'major')

4. Permissions & limits

  • A token grants the MCP surface projects:read, tests:read, tests:run, runs:read, issues:read, issues:manage, ai:use (the AI tools), and the authoring grants tests:manage + flows:manage. It cannot read or change organization or platform settings, and it can never create a project. The same token on /api/v1 carries a narrower set — no ai:use, no flows:manage (it does hold tests:manage, which is what the CI importer route needs).
  • Tool availability follows the token's permissions: list_issues appears only with issues:read; report_issue / update_issue only with issues:manage; create_test / update_test_steps with tests:manage; create_flow / update_flow / delete_flow with flows:manage; check_coverage and get_app_changes with tests:read (the same grant their HTTP twins require).
  • Plan-gated tools mirror their HTTP twin's entitlement and return the same 403 payload (code, plan, feature, requiredPlan) as a tool error rather than a re-phrasing: get_app_changes needs Pro+ (app-radar); list_chains / run_chain need Team+ (chains).
  • AI tools (suggest_fix, apply_repair proposals, generate_tests, generate_repro_test) are metered to your org's AI usage/budget, exactly like the dashboard. check_coverage, get_app_changes and inspect_page are not: they call no model.
  • The endpoint is covered by the same per-org rate limit as the rest of the API (QA_RATE_LIMIT_PER_MIN, off by default in local dev).

See also

  • Getting started — npx vera-agent init: link the repo, register this server, and the ritual the tools exist for.
  • The Vera Claude Code plugin — the skill and /vera:* commands wrapped around these tools.
  • The local agent — running the browser on your own machine, which is what makes localhost testable (scenario_cloud_only's fix).