Skip to content

MCP Server (AI Agents)

coverctl ships an MCP server so AI coding agents can ask for coverage feedback inline — domain-aware policy results, debt rankings, threshold suggestions, and per-file deltas — without leaving the edit loop.

The MCP server is the canonical surface. The CLI is the substrate behind it; humans can use either.

The agent loop Four nodes arranged on a circle, connected by clockwise arrows. You ask the agent to make a change. The agent edits source files such as email.go. The agent calls coverctl check via MCP. The verdict in the centre reads: api 78.2 percent, fail. Tests run across 15 languages. The agent then calls suggest, fixes the gap, and re-runs check until the verdict passes. coverctl check api 78.2% — fail YOU "add email validation" AGENT edits email.go claude code COVERCTL policy gate via mcp TESTS go test pytest --cov 15 langs
1. You ask the agent for a change. 2. The agent edits source files. 3. coverctl check runs via MCP and returns a verdict — in the example: api 78.2 % — fail. 4. The agent calls suggest, fixes the gap, re-runs check until it passes, then commits.

Full walkthrough: The agent loop, end to end.

Add to ~/.config/claude-code/mcp.json:

{
"mcpServers": {
"coverctl": {
"command": "coverctl",
"args": ["mcp", "serve"]
}
}
}

Ask the agent: “Run coverctl check and tell me which domains regressed.”

coverctl mcp serve defaults to --mode=auto, which inspects well-known CI environment variables and picks the right surface automatically. Use an explicit --mode value to override.

Terminal window
coverctl mcp serve # --mode=auto (default)
coverctl mcp serve --mode=agent # force agent surface (3 tools)
coverctl mcp serve --mode=ci # force CI surface (9 tools)

Auto-detection signals (any one matches → CI mode):

Env varSet by
GITHUB_ACTIONS=trueGitHub Actions
GITLAB_CI=trueGitLab CI
BUILDKITE=trueBuildkite
CIRCLECI=trueCircleCI
JENKINS_URL non-emptyJenkins
TF_BUILD=TrueAzure Pipelines
CI=truegeneric

When none of these are set, coverctl assumes the caller is a human’s MCP client (Claude Code, Cursor, Cline, …) and uses the agent surface.

Why mode matters: AI coding agents reliably select among a small (≤5–7) tool surface but degrade as it grows. Pruning the agent-mode surface to three avoids selection drift without removing capability — CI mode still has every tool.

ToolModePurpose
checkagent + ciRun tests with coverage and enforce policy. Returns per-domain pass/fail, files, warnings.
suggestagent + ciRecommend thresholds (current / aggressive / conservative).
debtagent + ciCoverage gap per domain — where to spend effort, ranked.
initciAuto-detect project structure and create .coverctl.yaml with domain policies.
reportciAnalyze an existing coverage profile without running tests.
recordciRecord current coverage to history for trend tracking.
compareciDiff two coverage profiles. Returns delta, improved/regressed files, domain changes.
badgeciGenerate SVG coverage badge.
pr-commentciPost coverage report to GitHub / GitLab / Bitbucket PR.

Read-only context the agent can pull on demand:

URIContent
coverctl://debtCoverage debt as JSON.
coverctl://trendTrend over recorded history.
coverctl://suggestThreshold suggestions.
coverctl://configDetected project config.

Concrete defenses applied to every MCP tool call:

  • Argument sanitization. Test-runner flags that load arbitrary code are rejected: --rootdir, --cov-config, --require, --init-script, --node-options, --manifest-path, --target-dir, -D, -I, -P, and others. Shell metacharacters in arguments are rejected.
  • Path scope enforcement. Every path input (configPath, profile, historyPath, output, baseProfile, headProfile) is validated to stay within the working directory. Absolute paths, parent escapes, and symlinks that resolve outside the scope are rejected.
  • Rate limit on pr-comment. Five calls per five minutes per pull request. Stops agent loops from burning GitHub abuse quota.
  • Forensic logging. With --debug, every test-runner invocation emits a structured event with binary path, args fingerprint, exit code, and elapsed duration.

CLI invocations from a human terminal are not sanitized — the human is the trust boundary there.

Every coverage-listing tool (check, report, debt, compare) accepts an optional verbosity field on its input:

VerbosityBehaviorUse when
briefFailing rows only, hard cap at 5.Inside an agent edit loop where only the actionable subset matters.
normal (default)All failing rows + top passing rows up to a cap of 20. Failing rows are never trimmed.Typical agent or interactive use.
verboseNo truncation.CI runs that ingest the output for archive or trend analysis.

When truncation occurs, the response includes a sibling <list>NextCursor field (e.g., domainsNextCursor, filesNextCursor, itemsNextCursor). The cursor format is opaque; agents should treat it as a pass-through token.

{
"passed": false,
"domains": [{ "domain": "api", "status": "FAIL", "...": "..." }],
"domainsNextCursor": "next/5/of/47"
}

This keeps tool-call cost predictable in the agent’s context window without removing capability — agents can request more detail explicitly when needed.

Every rejected MCP tool call returns a stable JSON shape. Agents can pattern-match the error_code field and use the remediation hint as the next-action signal without falling back to natural-language parsing.

{
"passed": false,
"error_code": "INPUT_REJECTED_DANGEROUS_FLAG",
"error": "rejected MCP input testArgs[0]=\"--rootdir=/tmp\": flag \"--rootdir\" can load arbitrary code via the underlying test runner; not allowed from MCP input",
"summary": "Rejected unsafe MCP input",
"remediation": "Remove the rejected flag from testArgs. The flag can load arbitrary code via the underlying test runner and is denied from MCP input. If you need it for trusted CLI use, run coverctl directly without MCP."
}
error_codeWhen emittedRecovery hint
INPUT_REJECTED_DANGEROUS_FLAGTest arg matches a denylist flag (e.g. --rootdir, -D, --require).Remove the flag; run via terminal CLI for trusted use.
INPUT_REJECTED_SHELL_METACHARField contains shell metacharacters (“ ` $ ;& < > “, newline).
INPUT_REJECTED_CONTROL_CHARSField contains NUL or CR/LF bytes.Strip control bytes.
INPUT_REJECTED_INVALID_TAGStags does not match [A-Za-z0-9_,]+.Pass alphanumeric, comma-separated identifiers.
INPUT_REJECTED_INVALID_TIMEOUTtimeout is not Go duration syntax.Use 30s, 10m, 1h30s, …
INPUT_REJECTED_INVALID_RUN_PATTERN-run filter contains shell-injection markers.Use plain regex; remove backtick / $(...) / ;.
INPUT_REJECTED_PATH_SCOPEA path input resolves outside the working directory.Use a path inside the project root.
OP_CONFIG_EXISTSinit called when .coverctl.yaml already exists.Pass force: true to overwrite.
OP_DETECT_FAILEDAuto-detection found no language markers.Pass language explicitly or run from a project root.
OP_INVALID_PATHPath could not be cleaned/validated.Use a path inside the working directory.
OP_FILE_WRITE_FAILEDFilesystem error creating or writing config.Check permissions and disk space.
OP_RATE_LIMITEDpr-comment exceeded five calls per five minutes per PR.Wait or coalesce updates.
INPUT_REJECTED_OTHERUnclassified input rejection.Inspect error for details.

The schema is append-only: future codes may be added; existing codes will not be renamed without a major-version bump. Existing fields (passed, error, summary) remain for backward compatibility.

Use the built-in doctor for an end-to-end first-run check:

Terminal window
coverctl mcp doctor

Sample output (all checks passing):

[PASS] binary on PATH — found at /opt/homebrew/bin/coverctl
[PASS] working-directory markers — go.mod present at /Users/me/repo
[PASS] config resolvable — .coverctl.yaml resolves directly
[PASS] MCP server constructs — server constructed in agent mode
[PASS] tool dispatch smoke — rejection schema OK (error_code=INPUT_REJECTED_DANGEROUS_FLAG)
[PASS] mode auto-detect — 'auto' resolves to agent in this environment
All checks passed. coverctl is ready to use as an MCP server.

Each check writes one line; failures include a remediation hint and a non-zero exit. The output is paste-able into bug reports.

StepWhat it validatesCommon failure → fix
binary on PATHThe launcher path the client will resolve”not on $PATH” → add the install dir to PATH or use the absolute binary path in client config
working-directory markersSomething coverctl recognizes (go.mod, pyproject.toml, package.json, Cargo.toml, …)“no recognized language marker” → run from a project root, or pass --language to check
config resolvableDirect stat or auto-detect of .coverctl.yaml”not found” → run coverctl init
MCP server constructsIn-process mcp.New does not panicconstruction error → file an issue with paste
tool dispatch smokeAdversarial input is rejected with the stable schemashape mismatch → mcp-go upgrade lagged; reinstall coverctl
mode auto-detectWhat --mode=auto will resolve to todayinformational; never fails

Manual smoke as a fallback:

Terminal window
coverctl mcp serve --help
# Should print MCP serve options without error.

Then in the agent: “What MCP tools do you have available from coverctl?” The agent should list check, suggest, debt (agent mode) or the full nine-tool surface (CI mode).

Agent can’t find the binary. Use an absolute path in the command field instead of coverctl:

"command": "/opt/homebrew/bin/coverctl"

Find the path with which coverctl.

Rejected unsafe MCP input in agent output. The sanitizer blocked an argument shape associated with code execution. The error names the field and the offending value. Drop the dangerous flag or use the long-form equivalent if it has a safer cousin.

Path errors. All path inputs must be relative to the working directory. Absolute paths are rejected. Pass Profile=".cover/coverage.out" not Profile="/abs/path/coverage.out".

Rate-limit error on pr-comment. The PR was already updated five times in the last five minutes. Wait it out, or use dryRun=true to generate the comment body without posting.