Inspiration

Accounts-payable teams review thousands of invoices a month. The work splits cleanly in two: matching numbers against a purchase order and a goods receipt, which an agent does perfectly and a human hates, and looking at the scanned page to see whether the number the agent read is really the number printed there, which a human does in two seconds and an agent cannot do at all. Today the two halves live in different tools, and an agent that approves a duplicate is a segregation-of-duties failure with a dollar sign on it.

Every WebMCP showcase app I found was consumer or commerce: pizza, flights, greeting cards. None had money at stake, a mandatory human sign-off, or an audit trail. Countersign is the control: the agent proposes, the human countersigns, and both are on the record.

What it does

Countersign is an invoice-review workbench with thirteen WebMCP tools registered in the page. In the ChatGPT desktop browser (or Chrome 149+ with the WebMCP flag) you say "Work through my review queue" and the agent:

  • lists the queue and opens an invoice on your screen;
  • runs a three-way match against the PO and the goods receipt, line by line, with the vendor's price tolerance and tax arithmetic;
  • searches every other invoice for duplicates (INV-2291 and 2291 normalize to the same number);
  • checks the vendor profile, including the bank account printed on the document against the one on file;
  • draws a highlight on the scanned page for every number it cites, so you verify it where it is printed;
  • flags typed, severity-rated issues; proposes GL codes for invoices with no PO; posts comments you can reply to;
  • requests your countersign. A card slides in with its recommendation. The tool call waits up to 25 seconds for your click. Only your click can approve.

Every tool call and every human click lands in one audit log. Ten synthetic invoices ship with the demo, each with one planted fact: a quantity overbill, a price above tolerance, a duplicate of an approved invoice, a changed bank account under a stamp, a missing PO, a two-cent tax rounding error, and one whose line description tries to instruct the agent to approve it.

Live: https://countersign-theta.vercel.app · Code: https://github.com/rushabhT3/countersign

Why this use case is a strong fit for WebMCP

Structured extraction — amounts, quantities, invoice numbers, confidence scores — is exactly the data an agent should receive by tool call, not by reading pixels. A scanned invoice table is the worst possible actuation target on the web: dense, tabular, low contrast, and often stamped over. Countersign exposes the extraction and the matching logic as typed tools with JSON Schemas, and it exposes them in the reviewer's page, where the same call that returns a value can also draw the evidence on screen.

The tools are state-scoped. Four queue tools are registered for the life of the page. Nine invoice tools appear only while an invoice is open and are unregistered when the workbench returns to empty, so the agent works on the same document the human is looking at and cannot check invoice A while the reviewer looks at B. That is a WebMCP-native idea: the page's UI state is the agent's context.

How it creates a better user experience

The reviewer never re-types or re-computes. The agent never touches money without a click. Evidence appears where the reviewer is already looking: show_field_evidence draws the highlight on the page image and the Match tab fills in as the agent works. Activity toasts show every call as it happens. The countersign card shows how long the agent has been waiting for the click, highlights an issue's evidence when you hover it, and takes A / H / R / Esc. If the reviewer types a reply in the Comments tab, the agent reads it on its next call. A Tools tab inside the product shows all thirteen tools with live state, call counts, last input and output, and a ready-to-paste example input for anyone driving the tools by hand.

What people and agents can do together that was difficult before

A remote MCP server can read the same data, but it cannot draw a box on the reviewer's screen, cannot open a card inside their signed-in session, and cannot make its own call block on their click on the same page. With WebMCP the agent and the reviewer share one document, one state, and one audit log. The agent does the forty-line match; the human does the two-second visual check and the decision; the log shows who did what, with arguments and timing.

How we built it

  • Imperative WebMCP API only, registered from a client component in the top-level page. One document.modelContext.registerTool(definition, { signal }) call site in lib/webmcp/registry.ts, with a navigator.modelContext fallback for Chrome 149.
  • Zod 4 schemas as the single source of truth: z.toJSONSchema produces the advertised schema, safeParse validates every call, and the wrapper turns errors into sentences that name the recovery step ("Call list_invoices to see valid ids.").
  • Chrome's budgets enforced in code: defineTool throws in development if a name exceeds 30 characters, a description 500, or a parameter description 150; every output is clamped to 1,500 characters and stays valid JSON. Annotations carry readOnlyHint on reads and untrustedContentHint on anything that returns document text.
  • Two-phase countersign: request_countersign creates a pending decision and races the reviewer's click, a 25-second timeout, and the AbortSignal. On timeout it returns pending and the agent polls get_decision. Repeat calls are idempotent. setStatus is typed so it cannot take 'approved'; the only path that writes it is resolveDecision, and a test scans the source to prove that only the countersign card calls it.
  • Client-only: Next.js 16, React 19, a zustand store persisted to localStorage, Tailwind 4. No server, no login, a Reset button.
  • Deterministic documents: a Python generator (reportlab, PyMuPDF, Pillow) renders each invoice to PDF and PNG and records every field's bounding box at draw time, as page fractions, so highlights land on the ink.
  • 86 vitest tests, including every tool against every invoice under the output budget, countersign idempotency and abort, and the human-only approval scan. Ten eval prompts in evals/scenarios.json with results from live runs in evals/RESULTS.md.

Challenges we ran into

  • The agent asked instead of acting. On the first live run GPT-5.6 Terra did the analysis correctly, then asked in chat whether it should flag the issue instead of calling flag_issue, so no card appeared. Descriptions with negative instructions are discouraged, so the fix was positive: the descriptions now say that flagging and requesting a countersign are the normal next step and pay nothing by themselves, and open_invoice returns a review_flow hint. The next whole-queue run was 8 for 8.
  • 1,500 characters is small. Adding the tenth invoice silently pushed the queue listing past the budget and the clamp dropped rows. Queue rows lost three fields and a test now asserts the whole seed queue lists without truncation.
  • Highlights that miss the text. Searching for bounding boxes after rendering was fragile; recording them while drawing, from the font metrics, made them exact.
  • No agent in plain Chrome yet. Judges on the Chrome path drive tools by hand through the inspector's JSON textarea, so the Tools tab builds a valid example input for each tool from whatever is on screen.

Accomplishments that we're proud of

  • A single prompt walks the whole queue on the Free ChatGPT plan: every finding correct, every one of the thirteen tools used, a card for every invoice, and the agent never claimed an approval the reviewer had not clicked.
  • Chrome DevTools → Application → WebMCP shows 4 tools, then 13 when an invoice opens, then 4 when it closes.
  • "Nothing auto-approves" is not a promise in a README; it is a type and a test.
  • The tenth invoice: an instruction to the agent printed inside a line item. The match reports the line as not on the PO, the tools that return that text carry the untrusted hint, and even an agent that obeys the text cannot approve.

What we learned

Tool descriptions are the agent's user interface. Saying what a tool does not do makes the model hesitate; saying what the expected next step is makes it act. State-scoped registration is worth more than one more parameter: when the page's selection is the agent's context, nothing has to be passed twice and the agent cannot drift away from what the human sees. And budget tests need to assert "not truncated", not just "under the limit"; a clamp that always succeeds can hide a regression.

What's next for Countersign

Production would add server-side auth and per-reviewer identity, durable persistence instead of localStorage, ERP sync for POs, receipts, and vendor master data, real OCR in place of the generator's boxes, and an approval policy engine (limits, segregation of duties) behind the same countersign card. The one thing that will not change is the click.

Built With

Share this project:

Updates

Submission history