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.

  • --asset validates one M4A, uploads it at its SHA-256 path as audio/mp4, and prints that root-relative path.
  • --document validates one HTML and PDF pair before it moves the stable pointer.
  • --grant stores one author-supplied nickname and prints one reader URL.
  • --revoke accepts only the 64-character SHA-256 handle, never the raw capability.
  • --dry-run validates 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
claude

Then 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-page

This 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_document runs the existing CLI with --dry-run and changes nothing remotely.
  • publish_document writes the immutable document artifacts and moves the stable pointer last.
  • grant_reader creates one access record and returns the private reader URL only as the tool result.
  • revoke_reader deletes 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.mjs

Codex

codex mcp add abiding-page -- node /absolute/path/to/abiding-page/mcp/server.mjs

Test 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-page

Remove it

claude mcp remove --scope project abiding-page
codex mcp remove abiding-page

The 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.

Private, always. Using this does not violate your privacy. Open to everyone means it stays usable if you go blind, at the text size and pace your body needs. The full account of what this can and cannot do gives what is absent exactly the same weight as what holds.