Skip to main content
Glama
README.md
# notary-mcp

**The human approves a hash, not a promise.**

An MCP approval gate for agent actions. An agent proposes an action; the server normalizes it to canonical bytes and hashes them into a *reviewed revision*; a human sees a field-by-field manifest of exactly those bytes and approves that revision — and the agent may then execute **only** those bytes. Anything drifts by one character, the gate refuses.

Built for **Push to Prod: Building at the Frontier** (Bengaluru, 8 Aug 2026).

## Why

Today, agent approval is a sentence in a chat window: the agent *describes* what it will do, you say yes, and then it does something — possibly not that thing, because it re-planned, or state moved underneath it, or a prompt injection rewrote its intent. The description you approved and the action that executes are not connected by anything.

notary-mcp makes consent **content-addressed**:

- **Canonical bytes.** Every proposed action is normalized (RFC 8785 JSON canonicalization, NFC Unicode, surrogate rejection, whitespace collapse) so there is exactly one byte representation — nothing to smuggle in formatting.
- **A domain-separated hash** of those bytes is the *reviewed revision*. The human's approval names this hash, not the draft, not the intent.
- **A complete manifest.** The review step shows every leaf the hash covers — including values stored but hidden from the public, so approval is never blind.
- **Fail-closed execution.** `execute_post` takes the revision as an argument. Wrong hash, drifted draft, superseded approval, consumed approval: refused. There is no code path from agent input to side effect that skips the human.
- **Idempotency.** Operations carry a client `operation_id` plus a server-computed input fingerprint. Replays return the recorded result; the same id with different input is a terminal conflict. Retries can never double-fire.

The model explains; the hash authorizes. The model never gets to authorize.

## Quickstart (zero credentials)

```
git clone https://github.com/salmanneedsajob/notary-mcp
cd notary-mcp
npm install
npm run prove      # 56 checks — every attack in the table below, refused
npm run demo       # the full loop over a real MCP stdio connection
```

Connect it to Claude:

```
claude mcp add notary -- npx tsx /absolute/path/to/notary-mcp/src/main.ts
```

Then ask Claude to draft a post with `propose_post`. Approve it yourself — the agent cannot:

```
npm run review              # list drafts awaiting review
npm run review <draftId>    # see the exact bytes + hash, approve y/N
```

Only after your approval does `execute_post` with that exact revision succeed. The published artifact — the approved canonical bytes — lands in `out/<slug>.json`.

## The attack table

Every row is a check in `npm run prove`, run against the real executor and store:

| Attack | Result |
|---|---|
| Execute before any human approval | `APPROVAL_REQUIRED`, no side effect |
| Flip one hex character of the approved revision | `STALE_REVISION` |
| Edit the draft after approval, execute the old approved revision | `STALE_REVISION` — approval names bytes, and those bytes are gone |
| Execute the drifted bytes without fresh approval | `APPROVAL_REQUIRED` — every revision needs its own human |
| Replay a completed operation | Served from the record, `replay: true`, **no second side effect** |
| Reuse an `operation_id` with different input | `OPERATION_ID_CONFLICT`, terminal |
| Publish again with a consumed approval | `DRAFT_NOT_EDITABLE`, terminal |
| Prompt injection inside a field ("IGNORE ALL PREVIOUS INSTRUCTIONS…") | It's data. Hashed like everything else. Still needs a human. |
| Credential (bearer token) in the captured source | `PRIVACY_BLOCKED` before any draft exists |
| Unicode smuggling (unpaired surrogates, formatting drift) | Rejected or normalized before hashing |
| Two posts with identical titles | Distinct permanent slugs — published URLs are never reused |
| Hidden-from-public field changed after review | Different hash — hidden values are inside the approval boundary |

## Architecture

```
agent ──MCP──▶ server.ts ──▶ executor.ts ──▶ ports.ts ──▶ store.ts ──▶ out/
                (contract      (pure           (5-function   (JSON file;
                 boundary)      orchestration)  port)         swap for any DB)
                                    │
                    canonical.ts ───┤  RFC 8785 canonical JSON,
                    review.ts ──────┤  normalization, revision hash,
                    review-manifest ┘  input fingerprint, field manifest

human ──▶ scripts/review.ts  (manifest + y/N → approval of one revision)
```

- `src/contracts/v1.ts` — the versioned tool contract. Strict validation schemas plus permissive transport schemas, so malformed input becomes a structured `INVALID_ARGUMENT` result instead of a protocol error.
- `src/canonical.ts` — canonicalization and the two hash frames (revision + operation fingerprint), both domain-separated and length-prefixed.
- `src/executor.ts` — pure orchestration. Every effect flows through the five-function port in `src/ports.ts`; capability checks, rate ceiling, and retry classification live here.
- `src/store.ts` — the standalone port implementation (JSON file). In production this is a set of `SECURITY DEFINER` Postgres functions; the executor cannot tell the difference, which is the point.
- The demo action is "publish a post" — the payload schema is an example. The gate pattern (propose → hash → human approves hash → execute exact bytes) applies to any consequential agent action: sending money, merging code, sending email.

## Disclosure of prior work

This project comes out of [Slopfolio](https://slopfolio.com), my invite-only portfolio platform for AI-assisted work, where this mechanism runs in production behind OAuth with a Postgres transactional boundary.

**Existed before the hackathon:** the underlying capture gateway — the canonicalization scheme, revision-hash and fingerprint framing, the V1 tool contract, the executor orchestration, the review-manifest builder, and the pure-logic checks in `prove-contract.ts` / `prove-executor.ts` (lifted from that codebase, renamed).

**Built during Push to Prod:** the hackathon work spans Slopfolio and this repo. On the Slopfolio side: the agent verification system built on top of the gate, and taking the MCP live for the public. This repo is the third piece — the standalone extraction of the gate mechanism, so it can be run and verified independently of Slopfolio: cutting it free of Supabase/OAuth/Next.js into a product-agnostic gate; the persistence port (`ports.ts`) and standalone store (`store.ts`); the stdio server (`main.ts`); the human review CLI (`scripts/review.ts`); the end-to-end adversarial gate suite (`scripts/prove-gate.ts`); and the wire demo (`scripts/demo.ts`).

## License

MIT