Developer guide
Give an agent one honest path to a document that lives.
Claude Code and Codex can use one local MCP contract to validate documents, publish a version, grant one reader, and revoke one grant. The same author CLI validates and uploads audio, so every path still meets one set of document gates.
This is author tooling. A reader still receives only a fragment capability link, and an agent never gains identity or authority merely because it can see this page or call a local tool.
The contract
Run from the repository root. The stack must already exist, infra/outputs.json must come from scripts/deploy.sh, and the local AWS identity must be allowed to
invoke the publish function and write or delete author access records.
--assetvalidates one M4A, uploads it at its SHA-256 path asaudio/mp4, and prints that root-relative path.--documentvalidates one HTML and PDF pair before it moves the stable pointer.--grantstores one author-supplied nickname and prints one reader URL.--revokeaccepts only the 64-character SHA-256 handle, never the raw capability.--dry-runvalidates and prints the double-encoded Lambda payload without writing to AWS.
Uploading, publishing, granting, and revoking change remote state. A coding agent should show the exact mutation and wait for the operator's approval. A dry run is local and read-only.
Claude Code
Replace the sample path with the checkout's absolute path. Claude Code starts an interactive session in the current directory when no prompt or subcommand is supplied.
cd /absolute/path/to/abiding-page
claudeThen give it the complete outcome, including the mutation gate.
Work in this abiding.page repository.
Read README.md and scripts/publish.py before acting.
Use the abiding-page MCP tools instead of rebuilding validation or AWS calls.
Call validate_document before publish_document.
Validate any M4A with --asset and --dry-run before adding its printed path to the HTML.
Show me the exact upload, publish, grant, and revoke commands before any remote write.
Never paste a reader URL or raw capability into a report, log, or tracked file.Codex
Replace the sample path with the checkout's absolute path. Codex's -C option
sets that directory as the interactive agent's working root.
codex -C /absolute/path/to/abiding-pageThis prompt keeps validation and authority together at the same boundary.
Work only in this abiding.page checkout.
Read README.md, scripts/publish.py, and the relevant tests before acting.
Use the abiding-page MCP tools and call validate_document before publish_document.
The tools delegate to the existing CLI. Do not duplicate its rules.
Use the CLI directly for M4A validation and upload. Audio is not an MCP tool.
Keep reader URLs and raw capabilities out of prompts, summaries, and tracked files.
Stop at any remote mutation until the operator has approved that exact action.Connect the local MCP server
The server gives an authoring agent four stable tools: validate_document, publish_document, grant_reader, and revoke_reader. It runs only as a local stdio child process from a trusted
checkout. There is no hosted endpoint, OAuth layer, analytics, or author dashboard.
validate_documentruns the existing CLI with--dry-runand changes nothing remotely.publish_documentwrites the immutable document artifacts and moves the stable pointer last.grant_readercreates one access record and returns the private reader URL only as the tool result.revoke_readerdeletes one access record by its saved SHA-256 handle, never by the raw capability.
Audio remains a direct CLI action, not a fifth MCP tool. Install ffprobe from
FFmpeg before using --asset; malformed or non-audio M4A files are refused.
Install dependencies first, replace the sample path with this checkout's absolute path, then register the server in the client you use. MCP changes transport, not authority: publishing, granting, and revoking still need the operator's approval.
Claude Code
claude mcp add --scope project abiding-page -- node /absolute/path/to/abiding-page/mcp/server.mjsCodex
codex mcp add abiding-page -- node /absolute/path/to/abiding-page/mcp/server.mjsTest it
The focused test starts the real server, completes the MCP initialize exchange, lists all four tools, and calls the Python refusal path. The two final commands show each client's saved registration.
cd /absolute/path/to/abiding-page
npm install
npm run test:mcp
claude mcp get abiding-page
codex mcp get abiding-pageRemove it
claude mcp remove --scope project abiding-page
codex mcp remove abiding-pageThe reader URL returned by grant_reader contains the raw capability in its
fragment. Return it only to the author. Never copy it into logs, tracked files, or an
unrelated summary.
Upload, publish, grant, revoke
This is the complete current flow. Review the dry-run payload, approve each remote write, and save the revoke handle before sending the reader URL through a private channel.
DOC_ID=family-letter
VERSION=2026-07-31
PDF_PATH="./family-letter-$VERSION.pdf"
HTML_PATH="./family-letter.html"
PUBLISH_PAYLOAD="$(mktemp "${TMPDIR:-/tmp}/abiding-publish.XXXXXX")"
chmod 600 "$PUBLISH_PAYLOAD"
trap 'rm -f "$PUBLISH_PAYLOAD"' EXIT
AUDIO_PATH="./family-letter.m4a"
# Optional: ffprobe must be installed. Validate an M4A and get its immutable path.
python3 scripts/publish.py --asset "$AUDIO_PATH" --dry-run
# After approving the remote write, upload it and place the printed path in
# <audio controls preload="metadata" src="/the-printed-digest">.
python3 scripts/publish.py --asset "$AUDIO_PATH"
# Validate the exact Lambda payload without writing to AWS.
python3 scripts/publish.py \
--document "$DOC_ID" \
--version "$VERSION" \
--pdf "$PDF_PATH" \
--html "$HTML_PATH" \
--dry-run > "$PUBLISH_PAYLOAD"
python3 -m json.tool "$PUBLISH_PAYLOAD" > /dev/null
# Publish the version after reviewing the dry run.
python3 scripts/publish.py \
--document "$DOC_ID" \
--version "$VERSION" \
--pdf "$PDF_PATH" \
--html "$HTML_PATH"
# Grant one reader and keep the revocation handle beside the author record.
READER_URL="$(python3 scripts/publish.py --grant "$DOC_ID" --nickname "A reader")"
CAPABILITY="$(printf '%s' "$READER_URL" | cut -d '#' -f 2-)"
REVOKE_SHA="$(python3 -c 'import hashlib,sys; print(hashlib.sha256(sys.argv[1].encode()).hexdigest())' "$CAPABILITY")"
printf 'Reader URL: %s
Revoke handle: %s
' "$READER_URL" "$REVOKE_SHA"
# Revoke that one grant later. The CLI accepts the digest, never the raw link.
python3 scripts/publish.py --revoke "$REVOKE_SHA"Current limits
- The MCP server is local stdio author tooling only. There is no hosted endpoint, remote state store, OAuth layer, compatibility adapter, resource, prompt, or dashboard.
- The grant command prints the deployed distribution hostname from
infra/outputs.json, not a custom domain alias. - No revoke-handle registry exists. Derive the digest when the grant is made and keep it with the author record.
- Revocation stops new authorization and pass renewal. An already issued pass may remain valid for up to fifteen minutes, and a downloaded or locally cached copy cannot be recalled.
- Published HTML is deliberately constrained. Executable and external resources are refused; media must use a root-relative, content-addressed path served through the reader gate.
- The product has no collaboration, comments, search, or version browser. The limits page explains why.