Agents & MCP¶
Agent integration¶
Four layers, increasing strength:
- Standing rule (
CLAUDE.md/ equivalent) — run only viackdn run <check>or MCPrun_check/run_group; read the digest;passis the only green; never edit.agent-runs/or weaken checks to go green. Template:examples/claude/CLAUDE.md. - Skill —
examples/claude/skills/verified-fix-loop/SKILL.md(copy into the agent's skills dir). Bounded fix loop, digest-only reading, forbidden moves, MCP tool mapping, andcwdfor worktrees. - Hooks / CI —
ckdn runpasses red exit codes through, so it drops into the same slots as the raw tool, with digests as a side effect. Useckdn lock-config+ckdn verify-config --lockedin CI for command governance, andckdn doctorfor static pre-flight (executables on PATH, parser/command fit, no subprocess). None of the three is an MCP tool. - MCP (optional) —
ckdn[mcp]/ckdn-mcpwhen the client should call ckdn over the protocol instead of shelling out (see below).
Division of labor: constitution → procedure → instrumentation → enforcement. Digests never contain instructions to the agent (prompt-injection surface and policy fork).
MCP¶
When an agent should call ckdn over MCP instead of shelling out, install the FastMCP transport:
ckdn-mcp speaks stdio only. Config resolution: per-call config →
ckdn-mcp --config → $CKDN_CONFIG → <resolved cwd>/ckdn.toml. Working
directory: per-call cwd → ckdn-mcp --cwd → $CKDN_CWD → process cwd. The
server default outranks the environment on both axes, and the implicit config
path follows the resolved cwd — it is the process cwd only when nothing
overrides cwd. Subprocesses and relative runs_dir anchor on cwd, not the
config file parent — pass cwd on every tool call when config and project
root differ.
Every client shares the schema { command, args, env }; only the file name
and format differ.
Project-scoped .mcp.json (committed):
or commit a .mcp.json at the repo root (Claude Code expands ${VAR}):
{
"mcpServers": {
"ckdn": {
"command": "ckdn-mcp",
"args": [],
"env": {
"CKDN_CONFIG": "${CKDN_CONFIG:-ckdn.toml}",
"CKDN_CWD": "${CKDN_CWD:-}"
}
}
}
}
For worktree slices, prefer per-call cwd on each tool instead of a fixed
env default.
.cursor/mcp.json (or global ~/.cursor/mcp.json):
Settings → Developer → Edit Config, same schema as Cursor
(claude_desktop_config.json).
~/.codex/config.toml (TOML, not JSON):
Tools¶
Thin adapter over the same application layer as the CLI. Every tool takes
optional config (path to ckdn.toml) and cwd, both defaulting to null;
schemas reject unknown keys.
| Tool | Purpose |
|---|---|
list_checks |
Configured atomic checks + aliases → {checks: [...]} |
run_check |
Run one atomic check → {digest, exit_code} |
run_group |
Run one alias → {aggregate, exit_code} |
get_digest |
Load stored ckdn.digest/2 (latest or by run id) |
list_runs |
Recent run summaries → {runs: [...]} |
get_evidence |
Bounded findings / artifact line slices (never auto-dumps full.log) |
Beyond config / cwd:
list_checks— nothing else.run_check(check, extra_args=null)—checkis required and must name an atomic check; an alias is an error pointing atrun_group.extra_argsis a string array appended to the configured command's argv.run_group(alias)—aliasis required and must name an alias; an atomic name is an error pointing atrun_check. Noextra_args— an alias has no single member to append them to; run the atomic member instead.get_digest(run=null)—runomitted means the latest run.list_runs(limit=10)— most recent window, returned oldest→newest.get_evidence(run=null, artifact=null, offset=0, limit=200, include_meta=false)— see below.
run is resolved inside the runs directory, so it is a run id — one directory
name, never a path — and it must name a real, non-symlinked, non-dot-prefixed
directory that resolves under the runs root. Limits are clamped rather than
rejected: list_runs takes limit to 0..500, get_evidence takes limit
to 1..2000 and offset to >= 0.
get_evidence result¶
Without artifact, the payload is the run's identity plus whichever digest
evidence keys the run produced:
{
"run_id": "20260817T173148Z-mismatch",
"check": "mismatch",
"status": "parse_mismatch",
"rc": 0,
"run_dir": ".agent-runs/20260817T173148Z-mismatch",
"artifacts": ["full.log", "meta.json", "ruff.json"]
}
run_id is the directory name — the value run accepts. run_dir is the
digest's recorded path to it, relative to cwd. They are not interchangeable.
artifacts is a live listing of the run directory (every file except
digest.json), not the digest's recorded copy. digest.json, meta.json and
full.log can always be passed as artifact whether or not they appear here.
Merged in when present: findings, findings_total, findings_truncated,
gate_failures, notes, log_tail, summary, status_reason. Sparse as
everywhere else — a missing key means empty / 0 / false.
The digest's gate and baseline blocks are not here; read those with
get_digest. include_meta: true adds meta when meta.json exists and
parses to an object, or meta_error when the JSON is unparseable; a parseable
non-object yields neither key.
Passing artifact adds one block and changes nothing else:
{
"artifact": {
"name": "full.log",
"offset": 0,
"limit": 2,
"total_lines": 1,
"truncated": false,
"lines": ["wrote report, exiting 0"]
}
}
offset and limit echo the clamped values actually used, and truncated is
offset + limit < total_lines — so an agent can page without guessing.
Trust rules¶
- Only checks from
ckdn.toml— no arbitrary shell. fail/error/parse_mismatchare normal structured results, not MCP tool failures.- MCP
isErroris reserved for impossible tool calls (missing config, unknown check, path escape). runis a run id (single directory name), never a path; refs that escape.agent-runs/areisError, not silent reads.exit_codeis not a mirror of the digest'src. A nonzerorcpasses through (anything outside 1..255 becomes1), butrc == 0with a non-green status still yieldsexit_code: 1— that is theparse_mismatchcase and the parser-gatefailcase (e.g. coverage belowfail_under, where the tool itself exits 0). Readingrc: 0as green is the exact false green ckdn exists to catch;statusis the verdict. Onrun_groupthe two do coincide, because the aggregate'srcis that exit code.lock-config/verify-config(governance) anddoctor(pre-flight) are CLI/CI only — not MCP tools.- Core CLI remains stdlib-only; FastMCP is the optional extra.