vouchfx-mcp¶
A local stdio Model Context Protocol server for AI coding agents, 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 Fail-only Healer patch proposals,
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¶
Under construction. This repository is being built spec-first: features land against approved specs in a spec → build → review loop, one requirement at a time. All eighteen tools, both vendored-document MCP resources, and the diagnostic-catalogue resource are fully functional — the server is feature-complete and packaged as the Vouchfx.Mcp dotnet tool with an OIDC release pipeline; what remains are the first tagged release and publication to NuGet.org. 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 from
scripts/build_site.py).validate_suite(accepts file paths or inline YAML, three validation levels, process-isolated, ten semantic rules (eleven 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 on the payload (commentsDropped), 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 (reporting any divergence as a diagnostic on the still-successful result).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); 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.ENGINE_PIN(currently v1.0.0-rc.4) 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 which suite, since one bad file refuses the whole call — 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.explain_diagnosticlooks up one cataloguedVFX-D-####/VFX-E-####code and returns its title, explanation, common causes, and fixes — the same content served by the templatedvouchfx-docs:///errors/{code}resource, 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 and its labels — 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. Two of spec §5.10's fields come back as explicit nulls rather than values synthesised from other numbers:delayMs, which nothing on the wire carries, andtimeoutMs, which thestep-startedevent does carry but this build's event parser does not read.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, withavailablesaying whether the file still exists) and the environment resources that stream's ownenvironment-errorevents named. 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. The packagedVouchfx.Mcpdotnet tool is not yet published. See Implementation map for how the wider vouchfx.ai proposal maps onto what ships here today.
Engine pin¶
This repository wraps the published vouchfx dotnet tool rather than building the engine from source. It is
currently pinned to v1.0.0-rc.4 (commit be12ebd126fdf03dcea9eade7bcec3afbcba001b) — see ENGINE_PIN for exactly what
that pins, how vendored artefacts stay drift-gated against it, and how to advance it.
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 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.
Related¶
- vouchfx-mcp.vouchfx.io — this server's documentation site.
- vouchfx — the engine this server wraps.
- vouchfx.io — documentation, roadmap, and provider catalogue.