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
Section titled “The agent loop”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.
Quickstart
Section titled “Quickstart”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.”
~/.config/claude/claude_desktop_config.json (macOS/Linux) or %APPDATA%\Claude\claude_desktop_config.json (Windows):
{ "mcpServers": { "coverctl": { "command": "coverctl", "args": ["mcp", "serve"], "cwd": "/path/to/your/project" } }}Any MCP-capable client works. Point it at coverctl mcp serve over stdio. The server runs in the user’s project directory and uses standard .coverctl.yaml discovery.
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.
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 var | Set by |
|---|---|
GITHUB_ACTIONS=true | GitHub Actions |
GITLAB_CI=true | GitLab CI |
BUILDKITE=true | Buildkite |
CIRCLECI=true | CircleCI |
JENKINS_URL non-empty | Jenkins |
TF_BUILD=True | Azure Pipelines |
CI=true | generic |
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.
| Tool | Mode | Purpose |
|---|---|---|
check | agent + ci | Run tests with coverage and enforce policy. Returns per-domain pass/fail, files, warnings. |
suggest | agent + ci | Recommend thresholds (current / aggressive / conservative). |
debt | agent + ci | Coverage gap per domain — where to spend effort, ranked. |
init | ci | Auto-detect project structure and create .coverctl.yaml with domain policies. |
report | ci | Analyze an existing coverage profile without running tests. |
record | ci | Record current coverage to history for trend tracking. |
compare | ci | Diff two coverage profiles. Returns delta, improved/regressed files, domain changes. |
badge | ci | Generate SVG coverage badge. |
pr-comment | ci | Post coverage report to GitHub / GitLab / Bitbucket PR. |
Resources
Section titled “Resources”Read-only context the agent can pull on demand:
| URI | Content |
|---|---|
coverctl://debt | Coverage debt as JSON. |
coverctl://trend | Trend over recorded history. |
coverctl://suggest | Threshold suggestions. |
coverctl://config | Detected project config. |
Security
Section titled “Security”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.
Output verbosity and pagination
Section titled “Output verbosity and pagination”Every coverage-listing tool (check, report, debt, compare) accepts an optional verbosity field on its input:
| Verbosity | Behavior | Use when |
|---|---|---|
brief | Failing 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. |
verbose | No 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.
Rejection response schema
Section titled “Rejection response schema”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_code | When emitted | Recovery hint |
|---|---|---|
INPUT_REJECTED_DANGEROUS_FLAG | Test arg matches a denylist flag (e.g. --rootdir, -D, --require). | Remove the flag; run via terminal CLI for trusted use. |
INPUT_REJECTED_SHELL_METACHAR | Field contains shell metacharacters (“ ` $ ; | & < > “, newline). |
INPUT_REJECTED_CONTROL_CHARS | Field contains NUL or CR/LF bytes. | Strip control bytes. |
INPUT_REJECTED_INVALID_TAGS | tags does not match [A-Za-z0-9_,]+. | Pass alphanumeric, comma-separated identifiers. |
INPUT_REJECTED_INVALID_TIMEOUT | timeout 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_SCOPE | A path input resolves outside the working directory. | Use a path inside the project root. |
OP_CONFIG_EXISTS | init called when .coverctl.yaml already exists. | Pass force: true to overwrite. |
OP_DETECT_FAILED | Auto-detection found no language markers. | Pass language explicitly or run from a project root. |
OP_INVALID_PATH | Path could not be cleaned/validated. | Use a path inside the working directory. |
OP_FILE_WRITE_FAILED | Filesystem error creating or writing config. | Check permissions and disk space. |
OP_RATE_LIMITED | pr-comment exceeded five calls per five minutes per PR. | Wait or coalesce updates. |
INPUT_REJECTED_OTHER | Unclassified 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.
Verifying installation
Section titled “Verifying installation”Use the built-in doctor for an end-to-end first-run check:
coverctl mcp doctorSample 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.
| Step | What it validates | Common failure → fix |
|---|---|---|
| binary on PATH | The 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 markers | Something 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 resolvable | Direct stat or auto-detect of .coverctl.yaml | ”not found” → run coverctl init |
| MCP server constructs | In-process mcp.New does not panic | construction error → file an issue with paste |
| tool dispatch smoke | Adversarial input is rejected with the stable schema | shape mismatch → mcp-go upgrade lagged; reinstall coverctl |
| mode auto-detect | What --mode=auto will resolve to today | informational; never fails |
Manual smoke as a fallback:
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).
Troubleshooting
Section titled “Troubleshooting”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.