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_projectsevery project in your org (id, name, base URL)
list_testsprojectIdthe project's tests (id, name, step count, quarantine/flake)
get_testtestIdthe test with its full steps and config
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
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. 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.

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.
inspect_pageprojectId, url?, environment?, role?loads the page as the configured role and returns its interactive elements with stable selectors. Note: interactive only — it does not return arbitrary text nodes, so title/url are the assertable values it hands you directly.
run_scenarioprojectId, steps[] (max 30), environment?, role?, label?runs an ad-hoc step list without saving a test, returns {runId} immediately. 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+).
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.
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 onlypersisted: 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. Read repro.status, never didNotReproduce: the four values are reproduced (red — this is the success case), 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. 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).
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.
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.

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 startednpx 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).