Skip to main content

Covenant

A contract linter and drift firewall for MCP servers.

CI Python 3.11+ License: MIT

When an MCP server changes a tool — renames an output field, tightens an input schema — nothing throws. The LLM agents depending on that tool keep calling it, read a field that no longer exists, and confidently report a wrong answer. Existing MCP scanners hash tool definitions and tell you that something changed; Covenant classifies every change by whether it breaks the agent — and enforces the verdict, from CI to runtime.

Covenant makes the tool contract explicit, versioned, and enforced:

Command What it does
covenant snapshot Introspect a server (stdio or HTTP) and commit its tool contracts to a deterministic covenant.lock.json
covenant check Diff the live server against the baseline, classify every change BREAKING / DEGRADED / COMPATIBLE, exit non-zero in CI on breaking drift
covenant proxy Transparent reverse-proxy that quarantines drifted tools at runtime — agents get a clean "tool unavailable" instead of silently wrong data
MCPContract CRD Kubernetes operator that runs the same check on a schedule and enforces it fleet-wide

Quickstart

git clone https://github.com/Mhemd139/Covenant && cd Covenant
python -m venv .venv && source .venv/bin/activate   # Windows: .venv\Scripts\activate
pip install -e ".[dev]"

The repo ships a real example server with a committed baseline. Check it, then break it for real — COVENANT_DRIFT=1 renames a live tool's output field:

covenant check                    # OK no drift - exit 0
COVENANT_DRIFT=1 covenant check   # catches the lie - exit 1

covenant check catching a breaking change

The lie is caught twice: in the declared schema (output rows) and in the actual response body (behavior rows), because the committed config probes the tool.

Point it at your own server via covenant.toml, or inline:

pip install covenant-mcp
covenant snapshot --server http://localhost:8000/mcp   # or a stdio launch command
covenant check    --server http://localhost:8000/mcp --json

The severity model

The consumer of an MCP tool is an LLM agent that re-reads tool definitions on every run — which changes what "breaking" means. Covenant classifies by one direction principle:

Input-side changes fail loud — the server rejects the call, or the agent adapts → DEGRADED (warn; fail CI only under --strict). Output-side changes fail silent — the agent reads a value that is gone, retyped, or now null, and proceeds confidently → BREAKING (fail CI; quarantine at the proxy).

Change Tier
Output field removed · output required→optional · output gains null · structural output retype · tool removed · pinned value changed BREAKING
Input retyped/narrowed · new required input · scalar output retype · risky enum changes · description changed DEGRADED
Optional input added · output field added · input enum widened COMPATIBLE

Nested schemas are walked recursively (balance.currency, items[].sku). Composed schemas ($ref/allOf/anyOf/oneOf) are never guessed at — a change there flags DEGRADED for manual review. Full rationale: Layer 0 design spec.

Use it in CI

Commit covenant.toml + covenant.lock.json, then:

- name: Contract check
  run: |
    pip install covenant-mcp
    covenant check --json   # exit 1 on breaking drift, 2 on config/connection error

This repo runs exactly that against its own example server on every push — including a job that injects the breaking change and asserts Covenant catches it (ci.yml).

Behavioral drift: probes + judge

A schema check can't see a server that lies — schema unchanged, response different. And most real MCP tools declare no outputSchema at all. Probes cover both: commit safe, read-only example calls in covenant.toml:

[[probes]]
tool = "get_transactions"
args = { account_id = "acct-001" }

snapshot stores each response's fingerprint (the type shape of what actually came back); check re-runs the probes and classifies shape drift with the same severity model.

A fingerprint remembers that a number lives there — not which number. When the exact value is part of the contract — a reference balance, a currency code, a unit — pin it:

[[probes]]
tool = "get_account"
args = { account_id = "acct-001" }
expect = { balance_usd = 4210.0, currency = "USD" }

check compares every pinned field against the live response with exact equality — no tolerance, no patterns. A mismatch is BREAKING: schema and shape still match while the value lies (a balance rescaled to cents, dollars quietly converted to another currency) — exactly the silent failure the direction principle exists to catch. Pins are opt-in and deterministic, like pip --require-hashes: nothing is pinned unless you type it. Try it on the example server — COVENANT_SEMANTIC_DRIFT=1 covenant check rescales the live balance ×100 and exits 1.

For drift you didn't pin — fields too volatile to pin, meaning shifts across the whole response — add the LLM judge:

pip install -e ".[judge]"
covenant check --judge    # [judge] model in covenant.toml: claude-* / gemini-*

the LLM judge catching a semantic rescale no schema diff can see

Judge verdicts are advisory by design — DEGRADED, never BREAKING: a probabilistic detector must not trigger quarantine. Details: Layer 3 design spec.

Runtime guard: the proxy

The linter catches drift at ship time; the proxy contains it at runtime. It forwards every JSON-RPC exchange byte-for-byte (SSE passthrough included) — but a tools/call to a quarantined tool is short-circuited with a clean MCP isError result.

pip install -e ".[proxy]"
covenant proxy --upstream http://localhost:8000/mcp --port 9000
# point your MCP client at http://127.0.0.1:9000/mcp
Endpoint Purpose
POST /covenant/refresh Re-read the baseline, re-list the upstream, re-check, update quarantine
GET /covenant/status Currently quarantined tools and why
GET /covenant/calls Recent call log with latency and outcomes
GET /covenant/metrics Prometheus metrics: per-tool calls, latency, drift events, quarantine gauge

Detection is proxy-owned: refresh re-lists the upstream itself, so enforcement never depends on the client's tools/list timing. Optional Postgres persistence keeps quarantine across restarts (--database-url, [store] extra); store writes are best-effort and never fail the request path.

docker compose up -d also brings up Prometheus + a provisioned Grafana dashboard at http://localhost:3000 — the quarantine stat flips green→red within one scrape of a drift:

Grafana dashboard: blocked calls and a quarantined tool after a live drift

Kubernetes: the MCPContract operator

Declare contract conformance instead of scripting it. The Helm chart ships the proxy, a kopf operator, and an MCPContract CRD — the operator re-runs the check on each contract's own schedule, writes the verdict into status, and nudges the proxy to quarantine on drift:

docker build -t covenant-mcp:0.1.0 .
helm install covenant deploy/helm/covenant --set proxy.upstream=http://my-server:8000/mcp
kubectl create configmap covenant-baseline --from-file=covenant.lock.json
kubectl apply -f examples/mcpcontract.yaml
kubectl get mcpcontracts -w        # RESULT flips clean -> breaking when the server drifts

A failed check is status.result: error, never a crash-loop. Details: Layer 5 design spec.

Architecture

Dependency-ordered layers; each ships alone, and every enforcement surface (CI, proxy, operator) reuses the same classifier and baseline format:

# Layer Extra
0 Contract core — introspection, baseline, severity classifier, CLI
1 Transparent proxy + quarantine [proxy]
2 Postgres store — durable quarantine, call log, drift events [store]
3 Behavioral probes + LLM judge [judge]
4 Prometheus metrics + Grafana dashboard [proxy]
5 K8s operator + Helm chart [operator]

The full codebase tour lives in docs/ARCHITECTURE.md; per-layer design specs (rationale, rule tables, named decisions) in docs/specs.

Development

pip install -e ".[dev]"
pytest                        # Postgres-backed tests skip without COVENANT_TEST_DB
ruff check . && mypy covenant # mypy is strict

Layer boundaries are enforced by imports: the core depends only on mcp, typer, rich; everything else is an optional extra imported on use.

License

MIT

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

covenant_mcp-0.1.1.tar.gz (44.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

covenant_mcp-0.1.1-py3-none-any.whl (37.2 kB view details)

Uploaded Python 3

File details

Details for the file covenant_mcp-0.1.1.tar.gz.

File metadata

  • Download URL: covenant_mcp-0.1.1.tar.gz
  • Upload date:
  • Size: 44.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for covenant_mcp-0.1.1.tar.gz
Algorithm Hash digest
SHA256 aee27821974b8ea0d591af89c80e44626a8e85917c84b2af5840b35d048a500c
MD5 db25913f2009c5a371249b93a6008d8e
BLAKE2b-256 96575050077df00ad582ac3f3540a6844a41198d3081dd4f065ff87d77e52d2e

See more details on using hashes here.

Provenance

The following attestation bundles were made for covenant_mcp-0.1.1.tar.gz:

Publisher: release.yml on Mhemd139/Covenant

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file covenant_mcp-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: covenant_mcp-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 37.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for covenant_mcp-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 9b9d6f2b947e3df367e0f9284750fa3183da84e9cfc5972de25a5d9c04480e1e
MD5 58bf3304230f04b3df837d7f1b341cbd
BLAKE2b-256 0374e67ef3b0532ca9428eedbdb01ffe5def7dbfd73bcff2fc8434189a2663bf

See more details on using hashes here.

Provenance

The following attestation bundles were made for covenant_mcp-0.1.1-py3-none-any.whl:

Publisher: release.yml on Mhemd139/Covenant

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page