# CENSUS-AUDIT/1 — config audit + resolver contract

Public edition of the contract. Prices: https://mcpcensus.com/pricing · API keys: https://mcpcensus.com/account

**Date:** 2026-09-04
**Status:** live contract for `POST /v1/audit`, `POST /v1/resolve` and the MCP tool `census_audit_config`. Later revisions add drift history and a composite GitHub Action on top of this same endpoint; the spec string stays `CENSUS-AUDIT/1` until a field changes meaning.
**Rule:** facts with evidence, never guesses. `UNKNOWN` is a real verdict. Nothing here is a malware scan, a pen-test or a permission review. `PASS` is not a sandbox.

## Why

A hook (Claude Code, Cursor, Codex, Copilot, CI) sees a **config file**: aliases plus a `url` or a `command` + `args`. `census_stamp` needs one exact canonical name. Without a resolver every hook is blocked. The audit is the `npm audit` of MCP: one call, one verdict line per entry, an exit code, and a body the hook can cache per session.

## Endpoints

| Surface | Method | Cost |
|---|---|---|
| `POST /v1/resolve` | REST | unmetered, burst-guarded (20/min per identity) |
| `POST /v1/audit` | REST | keyed: **1 credit per config digest per UTC day**; keyless: see below |
| `census_audit_config` | MCP tool | same meter as `/v1/audit` |

`/v1/resolve` never evaluates policy. It answers "which Census server is this entry?" only.

## Request

`content-type: application/json` (or `text/plain` / `application/toml` for a raw Codex `config.toml`). Any of these bodies:

1. A raw config document as the whole body:
   - `{"mcpServers": {...}}` — Claude Code `.mcp.json`, Claude Desktop `claude_desktop_config.json`, Cursor `.cursor/mcp.json`, Windsurf.
   - `{"servers": {...}}` — VS Code `.vscode/mcp.json`, Copilot coding-agent repo settings.
   - `{"mcp_servers": {...}}` — Codex JSON form.
   - Codex `config.toml` text: only `[mcp_servers.<name>]` tables with `url`, `command`, `args` are read. Anything else in the file is ignored. Other TOML shapes return `400 unsupported_config`.
2. An envelope: `{"config": <document or TOML string>, "format": "json"|"toml", "policy_id": "builtin:baseline", "previous_tools_digests": {"<alias>": "sha256:…"}}`.
3. A plain list: `{"entries": [{"name": "github", "url": "https://…"} | {"name": "fs", "command": "npx", "args": ["-y", "@scope/pkg"]}]}`.

Per-entry fields read: `url` (or `serverUrl`, `httpUrl`), `command`, `args`, `type`. Everything else (`env`, `headers`, tokens) is ignored and never stored. Do not send secrets; strip `env`/`headers` before posting when you can.

Limits: keyless **25 entries** per config; keyed **100**. Body ≤ 256 KB.

## Resolution (deterministic, no guessing)

| Entry shape | Method | Match |
|---|---|---|
| `url` | `remote_url` | canonical URL equality (`https:` only, lowercase host, default port dropped, fragment dropped, trailing-slash-insensitive) |
| `url`, no exact match | `remote_host` | exactly **one** Census row on the same host → resolved, reason `url_matched_by_host` is attached; **more than one** → `UNKNOWN` with `candidates[]`, reason `ambiguous_host` |
| `npx` / `npx.cmd` / `bunx` / `pnpx` / `pnpm dlx` / `yarn dlx` | `npm_package` | first non-flag arg after stripping `-y`, `--yes`, `-q`, `--quiet`, `--package=X`/`-p X` (X wins), version suffix after the last `@` past position 0 |
| `uvx` / `uv tool run` / `pipx run` | `pypi_package` | `--from X` wins, else first non-flag arg; `[extras]`, `==`, `>=`, `~=`, `<` specifiers stripped |
| alias that is already a canonical name (`io.github.owner/name`, `com.vendor/name`) | `server_name` | exact |
| `docker`, `node ./x.js`, `python -m`, a path, an `npx <git-url>` | — | `UNKNOWN`, reason names the shape (`docker_image_not_indexed`, `local_script`, `npx_git_or_path`, `command_not_recognized`) |
| neither `url` nor `command` | — | `UNKNOWN`, reason `no_url_or_command` |

Two Census rows on the same package → `UNKNOWN` with `candidates[]` (reason `ambiguous_package`). Resolution never picks between candidates.

## Response (JSON)

```json
{
  "ok": true,
  "spec": "CENSUS-AUDIT/1",
  "policy": { "id": "builtin:baseline", "digest": "sha256:…" },
  "as_of": "2026-09-04T08:00:00.000Z",
  "valid_until": "2026-09-05T08:00:00.000Z",
  "valid_until_epoch": 1788681600,
  "config_digest": "sha256:…",
  "decision_scope": "public_evidence_policy",
  "summary": { "entries": 3, "pass": 1, "review": 1, "block": 0, "unknown": 1 },
  "exit_code": 0,
  "entries": [
    {
      "alias": "github",
      "input": { "url": "https://api.githubcopilot.com/mcp/" },
      "server_name": "io.github.github/github-mcp-server",
      "method": "remote_url",
      "decision": "PASS",
      "reasons": [ { "code": "hosted_liveness_live", "result": "context" } ],
      "facts_digest": "sha256:…",
      "decision_input_digest": "sha256:…",
      "valid_until": "2026-09-05T08:00:00.000Z",
      "valid_until_epoch": 1788681600,
      "tools_digest": "sha256:…",
      "tools_drift": null,
      "audit_id": null,
      "lookup_url": "https://mcpcensus.com/lookup?q=io.github.github%2Fgithub-mcp-server"
    },
    {
      "alias": "local-fs",
      "input": { "command": "node", "args": ["./fs.js"] },
      "server_name": null,
      "method": null,
      "decision": "UNKNOWN",
      "reasons": [ { "code": "local_script", "result": "unknown" } ],
      "candidates": []
    }
  ],
  "text": "CENSUS-AUDIT/1\n…",
  "limitations": [ "not a malware scan", "not a permission review", "PASS is not a sandbox", "UNKNOWN means the Census holds no row for this entry, not that it is unsafe" ],
  "meter": { "mode": "keyless" | "credits", … }
}
```

- `decision` ∈ `PASS` | `REVIEW` | `BLOCK` | `UNKNOWN`. `UNKNOWN` is never upgraded to `PASS`.
- `valid_until` (top level) is the **minimum** over resolved entries; `valid_until_epoch` is the same instant as Unix seconds so shell hooks can compare with `date +%s`.
- `exit_code` = `1` when any entry is `BLOCK`, else `0`. `UNKNOWN` and `REVIEW` do not fail CI by default; the caller may pass `"strict": true` to make `REVIEW`/`UNKNOWN` exit `2`.
- `tools_drift` is `true`/`false` only when `previous_tools_digests[alias]` was sent and the Census holds a digest; else `null`.
- `audit_id` is set per entry for keyed callers (a `preflight_evaluation` row per entry, sharing one `correlation_id` = the audit run id). Keyless callers get `null`.
- Response header `x-census-exit-code` mirrors `exit_code`. `Accept: text/plain` returns only the text block.

## Text form

```
CENSUS-AUDIT/1
policy=builtin:baseline
as_of=2026-09-04T08:00:00.000Z
valid_until=2026-09-05T08:00:00.000Z
valid_until_epoch=1788681600
config_digest=sha256:…
entries=3 pass=1 review=1 block=0 unknown=1
exit_code=0
---
alias=github decision=PASS name=io.github.github/github-mcp-server valid_until_epoch=1788681600 reasons=hosted_liveness_live
alias=notion decision=REVIEW name=com.notion/mcp valid_until_epoch=1788681600 reasons=minimum_evidence_missing
alias=local-fs decision=UNKNOWN reasons=local_script
---
scope=public_evidence_policy not_a_malware_scan=true pass_is_not_a_sandbox=true
```

One line per entry, `key=value` pairs separated by single spaces, no spaces inside values (aliases are percent-encoded if they contain whitespace).

## Meters

**Keyed (`x-api-key`)** — `meterRequest` with subject `audit:<config_digest>`, cost `COSTS.audit = 1`. The same digest again the same UTC day is free. Empty wallet → `402 payment_required` with `upgrade` (unchanged shape). A changed config is a new digest and a new credit.

**Keyless** — separate KV keys, never the anonymous lookup counter:
- `audit:<day>:ip:<ip>` counts **distinct config digests** today; limit **1**.
- `audit:<day>:ip:<ip>:<digest>` marks a digest as seen; a repeat is free and never blocked.
- Entries > 25 → `429 rate_limited` reason `keyless_config_too_large`.
- Second distinct digest → `429 rate_limited` reason `keyless_daily_config_limit`.
- Both carry `upgrade: { account_url, pricing_url, message }` so the plugin's setup text can ask once for a free key.
- Burst: shared `consumeBurst` (20/min per IP).
