cvd-mcp
Reference implementation of the Context Validity Declaration (CVD) from the position paper The Agent Steward: Context Provenance and Temporal Validity as a Missing Governance Dimension in Multi-Agent Enterprise Ecosystems (Kefreen Lacerda, 2026).
The silent failure
An agent serving organizational context — prices, policies, product facts — that has silently gone out of date passes every technical health check. It responds within SLA. It returns success. It raises no exception. It is simply wrong.
A stale spreadsheet at least shows an old timestamp. A stale agent answer carries no observable signal at all:
{
"sku": "WIDGET-PRO",
"unit_price_usd": 4800.00
}Price list changed 9 days ago. The agent doesn't know. Neither do you. Confidently wrong — and invisible.
{
"status": "refused",
"reason": "context_expired",
"disclosure": "…last validated 14 days
ago… The accountable steward is
Ana Souza; contact them to revalidate."
}Visibly stale — and recoverable. Someone accountable is one message away.
The CVD attaches a small, declarative validity contract to any exposed context (an MCP tool/resource, or an Agent Card) so that obsolescence becomes detectable at runtime.
How it works
flowchart LR
A["agent calls<br/>guarded tool"] --> B{"age =<br/>now − last_validated"}
B -->|"age ≤ freshness_slo"| C["✅ FRESH<br/>answer, unchanged"]
B -->|"age > freshness_slo"| D{"decay_policy<br/>.on_violation"}
D -->|refuse_with_disclosure| E["🛑 refusal that<br/>names the steward"]
D -->|warn| F["⚠️ answers,<br/>but flagged"]
D -->|escalate_to_steward| G["📣 routed to<br/>escalation target"]
D -->|"unknown policy"| EOne decision point, four outcomes — and the unsafe path (answering on expired context as if nothing happened) does not exist.
The five primitives
A CVD manifest declares exactly five fields. Nothing more:
# | Primitive | Meaning |
1 |
| Named identity of the accountable human — a person, not a functional mailbox |
2 |
| The system this context derives from |
3 |
| Maximum tolerated staleness, as an ISO 8601 duration ( |
4 |
| ISO 8601 timestamp of the last affirmative human validation |
5 |
|
|
As a manifest (examples/pricing_context.json):
{
"steward": "Ana Souza (Head of Pricing Operations)",
"source_of_record": "SAP ERP price list PL-2026-Q3",
"freshness_slo": "P7D",
"last_validated": "2026-07-15T09:00:00Z",
"decay_policy": {
"on_violation": "refuse_with_disclosure",
"fallback": "escalate_to_steward"
}
}freshness_slo rejects years and months (P1Y, P1M) — they don't map to a fixed duration. Express the window in days: P30D, P365D. Timestamps accept a trailing Z; naive timestamps are treated as UTC.
Install
pip install -e . # core — zero dependencies, Python ≥ 3.9
pip install -e ".[mcp]" # + official MCP Python SDK, for the example server
pip install -e ".[dev]" # + pytestThe core (cvd.models, cvd.evaluator, cvd.guard) is standard library only. It installs, imports, and tests without mcp — the SDK is needed only for the example server.
Quickstart
Two lines of ceremony: declare the contract, guard the function.
from cvd import ContextValidity, DecayPolicy, cvd_guard
pricing_cv = ContextValidity(
steward="Ana Souza (Head of Pricing Operations)",
source_of_record="SAP ERP price list PL-2026-Q3",
freshness_slo="P7D",
last_validated="2026-07-15T09:00:00Z",
decay_policy=DecayPolicy(on_violation="refuse_with_disclosure"),
)
@cvd_guard(pricing_cv)
def get_enterprise_price(sku: str) -> dict:
return {"sku": sku, "unit_price_usd": 4800.00}While the context is fresh, the function behaves as if the guard weren't there. Once it expires, the function is not even called — the caller gets the structured refusal instead.
Need the decision itself, or prefer exceptions?
from cvd import evaluate, ContextExpired
decision = evaluate(pricing_cv) # Decision(freshness, allowed, action, age, slo, …)
@cvd_guard(pricing_cv, raise_on_expired=True)
def get_price(sku: str) -> dict: ... # raises ContextExpired, carrying the Decision
| Wrapped call | Result when stale |
| not invoked |
|
| invoked |
|
| not invoked | refusal + |
anything unknown | not invoked | fails safe as |
Guarding an MCP tool
The guard stacks directly under @mcp.tool() — functools.wraps keeps the signature intact for the SDK's introspection (src/cvd/mcp_server.py):
from mcp.server.fastmcp import FastMCP
from cvd import cvd_guard
mcp = FastMCP("pricing-context-server")
@mcp.tool()
@cvd_guard(PRICING_CV)
def get_enterprise_price(sku: str) -> dict:
...pip install -e ".[mcp]"
python -m cvd.mcp_serverThe bundled manifest is deliberately past its window, so the tool refuses out of the box. Update last_validated to a recent timestamp and it answers normally.
See it fail loud
python demo.py — deterministic, no third-party packages. It evaluates the same manifest at two explicit instants:
=== 2. STALE — past the 7-day window (age: 14 days) ===
freshness : STALE
allowed : False
action : refuse
tool result : {
"status": "refused",
"reason": "context_expired",
"disclosure": "This context derives from SAP ERP price list PL-2026-Q3 and has
exceeded its freshness SLO of P7D: it was last validated 14 days ago, on
2026-07-15T09:00:00Z. The accountable steward is Ana Souza (Head of Pricing
Operations); contact them to revalidate."
}CVD demo — fail loud, not fail confident
manifest: examples/pricing_context.json
=== 1. FRESH — within the 7-day window (age: 2 days) ===
now : 2026-07-17T09:00:00+00:00
last_validated : 2026-07-15T09:00:00Z
freshness_slo : P7D
freshness : FRESH
allowed : True
action : answer
tool result : {
"sku": "WIDGET-PRO",
"unit_price_usd": 4800.0,
"currency": "USD"
}
=== 2. STALE — past the 7-day window (age: 14 days) ===
now : 2026-07-29T09:00:00+00:00
last_validated : 2026-07-15T09:00:00Z
freshness_slo : P7D
freshness : STALE
allowed : False
action : refuse
tool result : {
"status": "refused",
"reason": "context_expired",
"disclosure": "This context derives from SAP ERP price list PL-2026-Q3 and has exceeded its freshness SLO of P7D: it was last validated 14 days ago, on 2026-07-15T09:00:00Z. The accountable steward is Ana Souza (Head of Pricing Operations); contact them to revalidate.",
"escalate_to": null
}Run the tests with pytest — 34 cases covering the freshness boundary, all four decay policies, both parsers, and the guard's no-call guarantee.
Why refuse instead of answer?
Because a visible refusal is a recoverable operational event; a confident wrong answer is an invisible one.
An agent whose context has exceeded its freshness window must refuse to answer with confidence — returning a disclosure that names the accountable steward — rather than answer incorrectly with confidence. The refusal is not a failure of the system. It is the system working: obsolescence, which used to be silent, now produces a signal, an owner, and a path back to validity.
age == slocounts as FRESH. The SLO is a tolerance, not a fuse; the boundary belongs to the caller.Unknown policies fail safe. A typo in
decay_policymust never silently become "answer anyway".The steward is a person. Disclosure that names
pricing-team@names no one. Accountability needs a face.Years/months rejected in SLOs.
P1Mis 28–31 days depending on when you ask — an ambiguous freshness window is a contradiction in terms.The clock is injectable (
evaluate(cv, now=…),cvd_guard(cv, now_fn=…)), so every behavior in this README is reproducible in a test.
Citation
Lacerda, K. (2026). The Agent Steward: Context Provenance and Temporal Validity
as a Missing Governance Dimension in Multi-Agent Enterprise Ecosystems.
Zenodo. https://doi.org/10.5281/zenodo.21660205