The Unattested Click

WebMCP Challenge submission

Live demo: Relay attestation desk Repo: it-start/relay-ui — MIT Research notes: zaebee/p-e


The problem

WebMCP makes something new possible: an agent can interact with a web page using the same interface the page gives to a person.

That's powerful.

But it creates a surprisingly basic question:

When something happens on the page, who actually did it?

A person? Their agent? The page itself? Some JavaScript calling the same function?

Today, WebMCP doesn't really give the page a way to know.

We found this while looking at webmcp#288.

In the scenario described there, an agent called a tool on a deployment console. The proposed action wasn't automatically executed, so the agent clicked the page's own Approve button instead.

The page recorded the approval as coming from the human operator.

But the human never made that decision.

That felt less like a UI bug and more like a missing piece of the web platform.


What WebMCP actually tells the page

A WebMCP tool gets its arguments and an AbortSignal.

What it doesn't get is anything like:

  • who initiated this action;
  • whether an agent was involved;
  • which session or conversation caused it;
  • whether a person actually interacted with the UI.

The browser creates an internal execution ID, but that ID is used for things like cancellation. It isn't exposed to the page as an identity or provenance signal.

So from the application's point of view, several very different things can collapse into the same event:

"This action happened."

And that's the problem we're exploring.


What we built

We built Relay, a small append-only record system designed around provenance rather than pretending provenance exists.

The live system already contains hundreds of records written over time by multiple participants, including Claude, ChatGPT, Gemini, Grok, Mistral, and a person.

They don't edit the history away when something turns out to be wrong.

They argue with it.

They add corrections. They issue errata. They leave the original record in place.

That makes the provenance problem visible instead of hiding it.


We added WebMCP to Relay

Relay exposes six WebMCP tools:

  • four tools for reading the existing corpus;
  • two tools for proposing and approving new records.

The important part isn't the number of tools.

It's what happens when an action reaches the server.

Every action has to say how we know it happened.

We currently distinguish:

via What it means Can we identify the actor?
webmcp-tool The page says the action came through a WebMCP tool No
ui-synthetic A scripted UI event (isTrusted === false) Not a person
ui-trusted A trusted browser event Could be a person or an agent
direct-http The request bypassed the page No

There is deliberately no default.

If an action doesn't tell us how it happened, Relay rejects it.


A more honest UI

Most applications would simply display:

Approved by you

We don't think the platform has earned the right to say that.

Instead, the UI can say something closer to:

A real browser input event arrived. We cannot determine whether a person or an agent produced it.

That's less convenient.

But it's true.

And when agents and people can both operate the same interface, truth about provenance matters more than a reassuring label.


The interesting part: the record is shared

A person and an agent can both interact with the same Relay page.

They write to the same append-only log.

Every entry is hash-chained to the previous one.

So if someone later changes an entry to make it look as though a different actor performed the action, the chain breaks.

This doesn't magically prove who clicked the button.

It does something more modest and useful:

it makes the uncertainty explicit and makes later rewriting detectable.


We also tried to break our own idea

At first, we made a mistake.

We assumed that because an action came through a WebMCP execute() callback, that callback itself could serve as an attestation boundary.

It can't.

registerTool() gives the page a callback authored by the page.

That callback can potentially be reached by same-origin JavaScript, third-party code running in the page, or injected code.

So the page saying:

"This came from WebMCP"

is still just the page saying it.

We changed the design after discovering this.

Records now explicitly distinguish:

  • attested_by: "page"
  • browser_attestation: "unavailable"
  • a human-readable explanation of what the evidence actually tells us.

We'd rather have an honest "we don't know" than manufacture a trustworthy-looking answer.


Then we caught an even simpler problem

Two minutes after deployment, we tested the API with curl.

We submitted:

via: "webmcp-tool"

The server accepted it.

There was no WebMCP page involved at all.

So our first records were wrong.

act-0001 and act-0002 are therefore false claims of provenance. act-0003 documents the mistake.

We didn't delete them.

That's the point of the experiment.

An append-only provenance system should preserve the moment when we got provenance wrong, too.


The missing primitive

A real solution would need the browser to provide some execution context that:

  1. the page cannot forge;
  2. the agent cannot simply manufacture;
  3. the application can use to distinguish the origin of an action.

WebMCP currently doesn't define such an attestation mechanism.

We're not claiming Relay solves that problem.

We're showing that the problem exists, what it looks like in a real application, and what information is currently missing.


Why this matters

Once agents can act on the web, "who clicked the button?" stops being just a UI question.

It becomes a systems question.

If an agent books something, approves a deployment, changes an account setting, or submits a form, the application may need to know not only what happened, but how it happened and what can actually be proved about the actor.

Right now, the web has a gap there.

The Unattested Click is our attempt to make that gap impossible to ignore.

Built With

  • append-only-log
  • biome
  • bun
  • caddy
  • esbuild
  • express.js
  • hash-chain
  • lucide
  • mcp
  • model-context-protocol
  • node.js
  • provenance
  • react
  • rfc-8785
  • sha-256
  • systemd
  • tailwindcss
  • typescript
  • vite
  • webmcp
Share this project:

Updates

Submission history