Reproducible companion code for "Giving an AI agent write access to a content graph, without letting it invent facts" (ARTICLE.md).
Written as a paid contribution to the Hygraph Creator Program. Hygraph did not review the measurements before publication.
An agent is given write access to a Hygraph content graph through Hygraph's MCP server, under three invariants borrowed from a production overnight animal-welfare pipeline (ZooVision):
- The vision model may only claim what it actually read in the scene.
- Only the deterministic rule engine may claim severity.
- A failed provider call is recorded as an explicit gap and is never backfilled.
The content model enforces those rules structurally rather than by prompting:
| Model | Holds | Deliberately does not hold |
|---|---|---|
Observation |
what the model described, plus provenance and the raw payload | any severity field at all |
Assessment |
ruleFired, severity, ruleVersion, reviewDecision |
free-text model prose |
DataGap |
reason, attempts, backfilled (always false) |
any interpolated value |
NightBrief |
coverage minutes vs gap minutes | a coverage figure that hides gaps |
Assessment.observation is a required relation, so an agent cannot write a severity claim with nothing behind it. That is tested, not assumed: see results/guardrail-probe.txt.
src/hygraph_mcp.py Dependency-free MCP (streamable HTTP) + GraphQL clients, both instrumented
src/build_schema.py Builds the whole graph through MCP submit_batch_migration (dry run, then apply)
src/benchmark_writes.py Writes identical records via MCP and via hand-written GraphQL, and compares
src/review_server.py Same-origin operator API, login sessions, live readiness, and SQLite audit trail
web/index.html Night Review overview, console, morning brief, filters, exports, and provenance
deploy/ Non-root container image and deployment runbook
tests/ Client and operator-service regression/integration tests
results/ Raw measurement output from the runs quoted in the article
# 1. Create a free Hygraph Hobby project.
# 2. Project Settings -> Access -> Permanent Auth Tokens -> Generate MCP PAT.
# 3. Project Settings -> Access -> Endpoints -> copy the MCP Server API endpoint.
cp .env.example .env # then fill it in; .env is ignored by git
python3 src/build_schema.py # validate every batch, apply nothing
python3 src/build_schema.py --apply # dry-run every batch, then apply all
python3 src/benchmark_writes.py --pairs 10 # writes draft-stage records only
python3 src/review_server.py # live read-only product at http://localhost:8795Requires Python 3.10+ and no third-party packages. The scripts accept --timeout and
write a timestamped JSON measurement artifact. Use --output-dir to keep local runs
outside the tracked results/ evidence. --apply is the only schema mutation mode;
the builder will never apply a batch after a dry run has failed.
Night Review is a live product in two explicit modes. A static server or direct
browser open uses the public Content API and is read-only. src/review_server.py
serves the same UI plus /api/health, /api/readiness, /api/graph, login/session
routes, decision mutations, and a local SQLite audit trail. It never sends the
Hygraph mutation token to the browser. Writes are disabled by default:
REVIEW_ALLOW_WRITES=true \
REVIEW_OPERATOR_TOKEN='use-a-secret-manager-value' \
python3 src/review_server.pyOnly enable writes after configuring and verifying the mutation API, token, and
operator session. Keep REVIEW_AUDIT_DB on durable storage for a deployed service;
sessions are persisted as hashed tokens, failed logins are throttled, and write mode
fails closed when any required credential is missing. State-changing requests with an
explicit Origin must match the request host. Readiness checks the Content API
and the local SQLite audit storage, plus an authenticated non-mutating query against
the mutation API when writes are enabled. See deploy/README.md for
the container boundary.
The product discovers available MCP and GraphQL runs from their record IDs, then lets the operator select a run and camera, search evidence, filter severity/review state, inspect gaps, and export the selected view as CSV or JSON. An alternate public Content API endpoint can be used without editing the file:
http://localhost:8791/?endpoint=https%3A%2F%2Fexample.cdn.hygraph.com%2Fcontent%2FPROJECT%2Fmaster&run=mcp
The run parameter is optional; the page selects the most recent run by default.
Free Hobby plan, us-west-2, from a laptop on residential broadband. Your numbers will differ; that is why the scripts are here.
- Schema: 42 operations in 7 batches. Dry run p50 228 ms, apply p50 1.7 s, 17.1 s total.
- Writes, 23 entries each way, two runs: MCP p50 281 ms / p95 360 ms / slowest of 60 calls 388 ms. Hand-written GraphQL p50 495 ms / p95 678 ms / one call at 15,429 ms.
- MCP responses were 13.6x larger than the GraphQL equivalents, and the 15 tool definitions are 44,438 characters (~11k tokens) loaded before any work happens.
publish_entry: 92 entries, 0 failures, p50 284 ms.- No delete tool exists for content or schema. All 42 schema operations are
create*orupdate*.
The checked-in benchmark artifacts are historical measurements, not a health check for the current project. A successful local run proves the configured account and network path at that time; it does not prove a deployment, a rotated token, or a current public dataset.
initializereturns anMcp-Session-Idheader you must echo on every later call. Miss it and you get404 Invalid session ID, which reads like a bad URL. Casing differs between HTTP/1.1 and HTTP/2, so match case-insensitively.- The PAT carries no
expclaim. It does not expire. isRequiredis undocumented forcreateRelationalFieldbut is accepted and does take effect.- Keep migration batches under 10 operations. Larger batches return a generic internal error instead of a useful one.
- Migration names must be unique per environment. Retries need a new name.
- The
General MCP Servertoken scope can also mint new tokens, invite users, assign roles and read audit logs. PreferContent MCP Serverfor an agent.
python3 -m unittest discover -v
python3 -m compileall -q src tests
git diff --checkThe CI workflow also checks the browser script syntax. For a live local check, run
the service and verify /api/health, /api/readiness, /api/graph, then open the
URL in a browser. A healthy local provider response is evidence for that configured
account and network path, not evidence of deployment, DNS, TLS, or secret injection.
The MCP client records failed HTTP and transport calls instead of hiding them behind an
uncaught urllib exception. It also bounds error text in artifacts, validates endpoint
shape, and gives a configuration error when .env is absent or still contains the
example token. No command prints the token value.
MIT.