Skip to main content
Glama

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.

Related MCP server: gomission-mcp

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, 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

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    Human-in-the-Loop authorization gateway for AI Agents. Securely pause MCP workflows and route high-risk actions to human approvers via Slack or Email.
    64 npm
    1
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Mission MCP is a Trust Graduation gate for AI agents, with visible approval ceremonies and receipt-backed boundaries for consequential actions.
    MIT
  • A
    license
    A
    quality
    D
    maintenance
    Enables MCP-capable agents to gate their actions behind human consent, checking consent rules and requesting approval via Telegram before proceeding with high-stakes operations.
    2
    18 npm
    MIT
  • F
    license
    Not graded
    quality
    B
    maintenance
    Provides a secure MCP boundary for AI agents, intercepting and validating tool calls, redacting secrets, and requiring human approval for sensitive actions with a tamper-evident audit trail.
    -