vouchfx-mcp¶
A local, stdio-first Model Context Protocol server for AI coding agents — with an optional, flag-gated HTTP transport — wrapping
the packaged vouchfx CLI. It advertises eighteen tools to validate
.e2e.yaml suites against the JSON Schema, look up the step catalogue and documentation for a given
<family>.<provider> type, serve the composed schema as a JSON Schema document or markdown digest, plan a declared
suite set's coverage and gap findings (Planner), scaffold a machine-drafted suite skeleton from structured step
types (Generator), run suites with best-effort progress updates and a taxonomy-faithful verdict, poll, list and
gracefully cancel those runs, diagnose a suite's JSON Lines event stream, return Healer patch proposals
(assertion-level for Fail, scoped spec edits for EnvironmentError/Inconclusive), and explain any of this
server's own diagnostic/error codes — all without the agent having to shell out to
vouchfx and parse its output by hand.
Status¶
Status: implemented server. The server exposes eighteen tools, four MCP prompts and eleven MCP resources — four concrete ones (the two vendored documents, the workspace suite index and the DSL guide for agents) and seven URI templates covering six families, with error pages served under two schemes. The server serves over stdio by default, with an optional bearer-authenticated HTTP transport behind
--transport http; every tool call emits one boundedActivitySourcespan, and all of this server's own stderr output is one JSON object per line. Packaging and an OIDC release pipeline exist. Vouchfx.Mcp 1.0.0 is published on NuGet.org. Install it withdotnet tool install --global Vouchfx.Mcp --version 1.0.0. Verify the installed tool, pinned CLI execution and cancellation behaviour for the selected release. Environment support follows the engine version inENGINE_PIN: inline services/dependencies and user-supplied Docker Compose are supported. Select Compose withenvironment.source.driver: composeand keep its configuration beside the suite. CLI-free schema validation does not read the driver configuration or prove its bindings; validate the saved suite with the pinned CLI before running it. Other formats require a registered engine driver.Workspace history retention: maintenance automatically prunes eligible terminal metadata and event evidence with defaults of 1,000 runs and 30 days. Copy evidence you need to keep;
--keep-run-historydisables pruning. See retention settings and recovery limits. A documentation site, in the same fleet design as the other vouchfx satellites, covers all of the below in more depth and is live at vouchfx-mcp.vouchfx.io (built fromscripts/build_site.py).validate_suite(accepts file paths or inline YAML, three validation levels, process-isolated, twelve semantic rules (thirteen codes, one reserved) including secret-literal detection, returns suite summary and semantic-diagnostics channel),search_docs(vendored language reference and recipes), andexplain_diagnostic(embedded per-code catalogue pages) are CLI-free.normalize_suiteis CLI-free too: it returns a suite's canonical text — key order taken from the engine's own JSON Schema, one consistent quoting and block-layout style, with mappings of the author's own data (headers, JSON bodies, variables, services, captures) deliberately left in the order they were written — alongside the same fullvalidate_suiteresult, always at levelfullso the secret-literal check cannot be turned off on text a host may write back. This server never writes the file: normalization is opt-in (normalize: true) because it discards all#comments on the pinned YAML library, the result says so per document on the payload (commentsDroppedis true only when your suite really did carry a comment), and the canonical text is proved to re-parse to the same document before it is returned — on the rare shape the emitter cannot render faithfully you getnormalizedYaml: nulland anormalizationRefusedreason instead of corruption.get_schemaserves the composed JSON Schema — the whole document or one addressable section — as a schema document or markdown digest; it works offline from the embedded schema this server vendors at its pinned engine commit, and optionally cross-verifies that schema against the installed CLI'svouchfx schemaexport (decoding redirected CLI output as UTF-8, so a divergence reported as a diagnostic on the still-successful result is real drift, never a transcoding artefact).list_step_typesanddescribe_step_typeload the live shape-level catalogue from the pinned engine viavouchfx list --json(required/optional fields, capture support, family intent — Spec A — plus the engine's own tier, RETRY support and language-reference link, and, fromdescribe_step_type, a minimal example suite); they fail fast if the CLI is missing, mismatched, or returns only thin type keys without field metadata.plan_coverageruns the engine's deterministic, read-only coverage-and-gap analysis over a declared suite set, an optional event history, and the live step catalogue via the pinned CLIplan --json(Spec D M3 Planner) — a call that finds gaps is a successful result, never an error, and every gap finding carries a suggested step type/id that feedsscaffold_suiteunchanged. Its response is size-budgeted; the analysis is not. On a repository large enough to produce hundreds of findings, a measured ladder caps the reply at 64 KB of wire envelope and keeps the most actionable findings first, never returning more than 150. Everything left out is counted —omittedFindingCountplus the inventory's ownomitted*counters, withresponseTruncatedset.maxFindingsasks for fewer still.ENGINE_PIN(currently v1.2.0) is Planner-capable; the CLI presence/version handshake still fails closed if a locally installed CLI is missing or does not match the pin.scaffold_suitegenerates a machine-drafted, schema-valid.e2e.yamlskeleton from structured step types, ids, and an environment outline via the pinned CLIscaffold --intent(Spec B Generator) — free text is host-LLM only; this server never hosts a model.ENGINE_PINis scaffold-capable; the same handshake fails closed on a missing/mismatched local CLI.run_suiteexecutes one or more suites through the packagedvouchfxCLI: takespath(single file) orpaths(array of files/workspace-relative globs) and runs them sequentially under one run ID; verifies the CLI is on PATH and matchesENGINE_PINand that every suite validates before spawning anything; reports best-effort progress as the run proceeds; returns the taxonomy-faithful verdict (pass / fail / environment error / inconclusive) at the run level (the worst of every suite's verdict) and per-suite outcomes inspecs[]. A missing/mismatched CLI or an invalid suite returns a structured result explaining why — naming every invalid suite it found, bounded and with the omitted counts visible, since one bad file refuses the whole call and a glob's caller should not need one round trip per broken file — without attempting to run anything, and a Docker-unavailable or timed-out/cancelled run is always reported as an environment error or inconclusive, never as a failure.timeoutSecondsbounds the whole call from its first filesystem access: glob expansion, per-suite pre-flight, the CLI handshake and the run all spend from the one budget.explain_rundiagnoses a run purely by reading and parsing its JSON Lines event stream — never re-running anything — defaulting to the most recent finished run in the run registry when no path is given (persists across server restarts when launched with--workspace): it reports the verdict together with what that category means, names the failing or inconclusive step(s) with their RETRY attempt timeline and observation/diff evidence, and always keeps an environment error distinct from a genuine test defect.diagnose_run(Healer / Spec C) adds two proposal kinds: Fail steps get review-only patch proposals (existing); EnvironmentError/Inconclusive outcomes get scoped spec-edit proposals (stepId or null, scope one of environment/timeouts/match/capture, YAML fragment never a diff, never auto-applied); both kinds stay within the same 32 KB response budget asexplain_run; infrastructure guidance for EnvironmentError remains unchanged. Capture-miss hints name the declared JSONPath when the same event proves a unique unmatched capture; declaring-step proposals require that proof. Legacy or ambiguous evidence keeps conservative guidance. Derived event readers also expose optionalengineCorrelation: { runIds, partial }:explain_run,diagnose_run's diagnosis,get_step_timeline, and environment/allget_run_artifactsresults. These are exact engine event identifiers, separate from the server'srun-…registry identity. One server run can contain several engine identifiers; the field does not attribute a step to a suite. See the correlation bounds and absence rules.explain_diagnosticlooks up one cataloguedVFX-D-####/VFX-E-####code and returns its title, explanation, common causes, and fixes — the same content served by the templated error-page resources, so a host can hand a code straight from anyVfxError/Diagnosticto either access path.get_run_eventshands a host the raw JSON Lines events for a run, exactly as the engine wrote them: it takes therunIdrun_suitereturns on its result, filters by eventtypesand/orstepIdbefore paging (solimitbounds matching events, not lines scanned), and returns a page plus an opaquenextCursorto continue with.limitdefaults to 200 and caps at 2000, and a page is additionally bounded by a 32 KB payload budget, sonextCursor— not the event count — is what says whether the walk is over;truncatedsays separately whether this server saw the whole stream at all. Events use the engine's wire vocabulary (PASS/FAIL/ENV_ERROR/INCONCLUSIVE), never thePass/Fail/EnvironmentError/Inconclusivestrings other tools' results carry, and unknown event types and fields pass through untouched; text is not byte-identical, though — every relayed string is control-character-sanitised exactly asexplain_runsanitises (non-ASCII comes back as a literal\uXXXX), and any bound that did apply is marked in the event rather than applied silently. It never spawns the CLI and never takes the run lock, so it is safe to call while a run is in flight.get_run_status,list_runsandcancel_runare the run-lifecycle trio.get_run_statusreturns one run's record straight from the persisted run registry — status (running/completed/cancelled), verdict, timestamps, the suites it covered, its events file, its labels, and (since vouchfx-mcp#114)run_suite's ownremediationHintfor it, persisted verbatim — which is the same recordexplain_runandget_run_eventsresolve arunIdthrough, so the three can never disagree.list_runspages that registry newest first, filtered bylabel(key=value, or a barekeyfor any value) and/orsince, reusingget_run_events' opaque cursor verbatim under its own scope; its position is astartedAtboundary rather than an index, so runs started mid-walk cannot shift the page under a caller. Both are read-only and never take the run lock.cancel_runasks an in-flight run to stop through exactly the mechanismrun_suitealready uses — the engine's stdin is closed for a graceful shutdown and the process tree is killed only after the grace period — so there is no second cancellation path; a cancelled run's status becomescancelledand its outcome isInconclusive, neverFail. Cancelling an already-finished run answersalready_finishedwithisError: false. Cancellation is same-process only, and says so rather than pretending: a run held by another server process against the same workspace is refused withVFX-E-1507(there is no IPC channel through aFileShare.Nonelock), and arunningentry sitting beside a free lock — the residue a hard-killed server leaves — is identified as such withVFX-E-1508, which is also how a host tells a phantomrunningentry from a real one.get_step_timelinereturns one step's complete RETRY attempt timeline from a finished run, extracted from the same parsed event streamexplain_runreads rather than duplicated beside it. It exists becauseexplain_run's response-size tiers shrink its ownattemptsarrays first (ten per step, then five, then none), so a long poll loop was the first evidence discarded; this tool inverts that order, keeping every attempt and dropping per-attempt evidence text instead (observedCappedsays when it did). Each attempt'soutcomeis the tool's own three-value vocabulary —matched/unmatched/error— which is neither the four-way verdict taxonomy nor the engine's wire tokens; anunmatchedattempt underverifyMode: RETRYis the ordinary state of every poll before the last one, not a failure. Each attempt'satis the engine's ownts, relayed verbatim — but the engine stamps it when it writes its buffered report, not when the attempt ran, sotMs(that attempt's own duration) is what orders and times the timeline.delayMs(per-attempt) comes back as an explicit null rather than a value synthesised from other numbers — nothing on the wire carries it.timeoutMs(per-step) and the additivedeclaredVerifyModefield are sourced from the step's ownstep-startedevent (null when that event was not captured for the step, andtimeoutMsalone null when the suite declared no explicit timeout);declaredVerifyModeis kept separate from the run-evidencedverifyModea host may already key on, since the two answer different questions — how the step was declared (IMMEDIATE, the engine's default, when the suite named none) versus what this run evidenced.specPathis validated against the run's own suite set (VFX-E-1509otherwise), but for a multi-suite run it cannot filter — the engine's events carry no per-suite attribution — andspecPathAttributedcomes back false to say so. Read-only and lock-free, like the rest of the events-file readers.get_run_artifactsreports what a finished run left behind, and is honestly partial: every result carriespartial: trueplus agapsarray naming each field this build cannot populate, why, and the upstream ask that would close it. What it really has is the run's own JSON Lines event stream (reports.events) and the environment resources that stream's ownenvironment-errorevents named. Onreports.events,availablesays whether the file still exists, andresourceUrinames the advertisedvouchfx://runs/<runId>/eventsresource that serves it. That URI is always present: it names a resource, whileavailablereports the bytes. What it does not have:logsis always an empty array (there is no container log access at all — never a fabricated line and never an error), the engine's HTML and JUnit report paths are omitted rather than nulled (the engine owns where it writes them and this server is never told), andenvironment.services/dependenciesstay empty because anenvironment-errorevent names a resource without saying which of the two it is — every identifier therefore lands underresourceswithrole: "unclassified"and ahealthofnull, meaning not observed, never "unhealthy". A run in which nothing went wrong reports no environment resources at all, which is a correct answer rather than a failure.containerandtailLinesare accepted and validated but select and bound nothing yet —tailLinesoutside 1–5000 is refused rather than clamped — so the contract does not change again when the engine's artifacts directory lands.Prompts: four MCP prompts, the complete set.
author_scenarioencodes the authoring procedure a host follows to write a passing suite using only this server's tools — grounded inget_schemaand the vendored docs, gap-found withplan_coverage, scaffolded, validated atlevel: full, normalized, written by the HOST's own file tools, run, and interpreted by taxonomy.heal_runencodes the healing procedure for a run that ended inEnvironmentErrororInconclusive— resolve the run withget_run_status, diagnose withdiagnose_run, filter itsspecEditProposalsby anallowedScopespermission list, let the HOST apply the edit, re-run once and report — and forbids acting on aFailexcept to explain it.review_specis a pre-flight checklist over an existing suite — mechanical checks viavalidate_suitefirst, then judgment on tautological assertions, hard-coded ids, missing negative-path coverage andplan_coveragegaps — andexplain_failureexplains one step to a developer new to vouchfx in 200 words, by taxonomy.Resources are four concrete URIs on
resources/list— the two vendored engine documents (vouchfx-docs:///language-reference,vouchfx-docs:///recipes), an index of workspace suites (vouchfx://workspace/specs), and a DSL guide written for a model reader (vouchfx://docs/dsl-guide— short, imperative, and every YAML example a complete document that this repository's own tests validate against the vendored schema) — plus seven URI templates onresources/templates/list: schema versions (vouchfx://schema/{version}, with alatestalias), error pages under BOTH schemes (vouchfx-docs:///errors/{code}andvouchfx://docs/errors/{code}— two templates, identical bytes), three complete sample suites undervouchfx://examples/{name}teaching patterns from basic HTTP smoke tests to secrets and message queues, and run resources (vouchfx://runs/{runId}/verdict,.../events,.../logs/{container}). See Tools & Resources for all four concrete resources and all seven templates.
SKILL.mdat the repository root is a Claude Code skill: with this server registered, a session asked to write, review, run or fix a.e2e.yamlsuite picks up the right procedure with no extra installation. It is a ROUTER, not a copy of the procedures — it points at the four prompts and atvouchfx://docs/dsl-guide, and restates only the two rules that must never be inferred away: never weaken an assertion to make aFailpass, and this server never writes a suite file (the host's own file tools do).Install the published
Vouchfx.Mcpdotnet tool at1.0.0. See Implementation map for how the wider vouchfx.ai proposal maps onto what ships here today.
Engine pin¶
This repository runs the packaged vouchfx CLI as a subprocess; it does not build the engine. The source pin is
v1.2.0, with the full engine commit recorded in ENGINE_PIN. That file explains what each field
pins, how vendored artefacts stay drift-gated against it, and how to advance it. Install the stable CLI version named in ENGINE_PIN from NuGet.org and verify its full version and commit. Matching source/private-feed packages are an optional contributor route. Environment execution follows
this pin; plan_coverage reports external-source
suites as unanalysable, and scaffold_suite currently accepts inline environment outlines only.
From v1.2.0 the CLI can also load trusted external providers from VOUCHFX_PROVIDERS and
~/.vouchfx/providers. The CLI-backed tools see them, because every CLI this server starts inherits
its environment. validate_suite, normalize_suite, get_schema and run_suite's pre-flight know
only the pinned engine's bundled step types, so run a suite that uses an external provider with
vouchfx run. See External providers.
Secret hygiene¶
This server never resolves ${secret:...} references and never reads or echoes its own process environment into
a tool result, progress notification, or resource. The vouchfx engine is the sole redaction authority (see its
SecretString, §17): the --events JSON Lines fields run_suite, explain_run, and diagnose_run relay are already redacted at
source, and this server passes them through untouched — bounded and control-character-sanitised for display, never
re-redacted, never re-resolved. The vouchfx CLI child process inherits this server's environment except for this
server's own HTTP bearer token (VOUCHFX_MCP_HTTP_TOKEN), which is stripped from every child's environment
unconditionally. Everything else is passed through unmodified, which is what lets a suite's own
${secret:env/...} reference resolve inside the engine; this server never builds or reads that environment for any
other purpose. The one removal is a narrowing, never an injection: the token is a credential this server owns and no
suite has business reading, so a suite writing ${secret:env/VOUCHFX_MCP_HTTP_TOKEN} deliberately fails to resolve.
Build and test¶
Use the .NET SDK pinned in global.json:
dotnet restore Vouchfx.Mcp.sln --locked-mode
dotnet build Vouchfx.Mcp.sln -c Release --no-restore
dotnet test Vouchfx.Mcp.sln -c Release --no-build --no-restore
Native path-containment probes use real local file/directory links. Unsupported local hosts report
explicit xUnit skips; the CI unit-test step sets VOUCHFX_MCP_REQUIRED_LINK_PROBES=1, so a missing
link prerequisite fails instead. Run just that native evidence set after building:
$env:VOUCHFX_MCP_REQUIRED_LINK_PROBES = '1'
dotnet test tests/Vouchfx.Mcp.Tests/Vouchfx.Mcp.Tests.csproj -c Release --no-build --no-restore --filter 'containment=link'
Remove-Item Env:VOUCHFX_MCP_REQUIRED_LINK_PROBES
On Bash, prefix the test command with VOUCHFX_MCP_REQUIRED_LINK_PROBES=1. Report the executed,
failed and skipped counts in a contribution's validation notes. A required run must have zero skips.
The link-target refusal fixtures read local link metadata; they never connect to a network share.
These probes do not require the engine or Docker. See
workspace containment for the path-check limits.
Most other tests use fake engine processes. The two real step-attempt envelope probes are explicitly
skipped by default. Release validation must install the exact engine version from ENGINE_PIN,
provide working Aspire DCP and Docker with Linux containers, then require those probes:
$env:VOUCHFX_MCP_REQUIRED_REAL_PROBES = '1'
dotnet test tests/Vouchfx.Mcp.Tests/Vouchfx.Mcp.Tests.csproj -c Release --no-build --no-restore --filter 'FullyQualifiedName~RealStepAttemptEnvelopeAgainstPinnedCliTests' --logger 'console;verbosity=normal'
Remove-Item Env:VOUCHFX_MCP_REQUIRED_REAL_PROBES
CI and release publication also require the Required real CLI probes lane. It installs the public CLI and checks the exact full version from ENGINE_PIN, checks out that immutable engine source for parity assertions, and uses the bundled Linux DCP with Docker. The lane runs six methods: the dependency-env refusal, the reserved-environment table, both StepAttempt probes, and both engine-generated storage scaffold facts. Its TRX/log gate requires six passes, zero skipped results, evidence for fourteen CLI invocations, and both storage generations. Missing prerequisites fail the lane; GitHub/NuGet publication depends on it. Raw TRX and console diagnostics are uploaded even on failure.
In required mode, missing prerequisites, failed startup or missing execution evidence fail the
tests. A passing release validation run must execute both probes with zero skipped tests. The suite deliberately
times out its RETRY assertion: each attempt is FAIL, but the final step and scenario are
INCONCLUSIVE, with observation.reason=retry-timeout on the step. The probe requires CLI exit code
4 (--fail-on-inconclusive) and the expected event evidence; an environment failure cannot pass.
Required release evidence also includes the complete reserved-environment source table and every
type/key refusal against the pinned CLI. Set VOUCHFX_ENGINE_REPO to an engine checkout containing
the full commit in ENGINE_PIN; the probe reads that immutable source object, not working files.
It requires VOUCHFX_MCP_REQUIRED_REAL_PROBES=1 and runs before Docker startup:
$env:VOUCHFX_ENGINE_REPO = '/absolute/path/to/vouchfx'
$env:VOUCHFX_MCP_REQUIRED_REAL_PROBES = '1'
dotnet test tests/Vouchfx.Mcp.Tests/Vouchfx.Mcp.Tests.csproj -c Release --no-build --no-restore --filter 'FullyQualifiedName~RealReservedEnvironmentAgainstPinnedCliTests' --logger 'console;verbosity=normal'
Remove-Item Env:VOUCHFX_MCP_REQUIRED_REAL_PROBES
Remove-Item Env:VOUCHFX_ENGINE_REPO
The ordinary Build & unit tests and Build, test & pack steps require native link probes and
assert the installed CLI's full version/SHA. Their unit-test steps leave
VOUCHFX_MCP_REQUIRED_REAL_PROBES unset. The separate blocking required-real-probes.yml lane
is called by both hosted workflows, provides the immutable ENGINE_PIN source checkout, and runs
all four required real-CLI facts with VOUCHFX_MCP_REQUIRED_REAL_PROBES=1. These checks do not replace
the graceful-teardown drill.
Publishing the tool¶
Publish a GitHub Release for the reviewed version tag to start publication. Pushing a tag alone
does not publish. A workflow_dispatch smoke test builds, tests and packs without attaching release
assets or publishing to NuGet.org. Publication requires both the build/test/pack job and the dedicated real-CLI
probe lane. A retry reuses an existing package asset only after its size and SHA-256 match the
package being published; ambiguous assets, mismatches and lookup/download failures stop publication.
Assets are never overwritten. An incorrect published package needs a new patch version.
Documentation validation¶
Install the Python dependencies and immutable vouchfx-site-tools revision from
the documentation workflow, then run:
python scripts/build_site.py _site
python -m vouchfx_site_tools.check_private_identifiers _site
python -m vouchfx_site_tools.check_social_card _site https://vouchfx-mcp.vouchfx.io/
The read-only documentation job builds and checks the site on every pull request. Pages runs both checks before uploading its artefact. The private identifier check scans static rendered text, text-bearing HTML metadata and published JSON, including markup embedded in JSON strings. Its fixed, bounded diagnostics omit matched text and input paths. It does not fetch frames or execute JavaScript or CSS.
The separate social-card check requires both image URLs to resolve inside the
built site on its configured origin and base path, with valid PNG headers,
1200x630 dimensions and a maximum size of 307,200 bytes. It validates
metadata and headers; it does not decode pixels or check the accent colour.
A refusal returns exit code 1 and blocks publication.
Related¶
- vouchfx-mcp.vouchfx.io — this server's documentation site.
- vouchfx — the engine this server wraps.
- vouchfx.io — documentation, roadmap, and provider catalogue.