Inspiration

The reflex in agent tooling right now is to expose more actions. Give the agent another tool, another capability, let it do more. That has a blind spot: some decisions are not ours to delegate. Whether a patient consents to a procedure, whether a family's loan is refused, how a neighborhood spends public money. Each has to be attributable to a named person and contestable afterwards. An agent can and should do the work around such a decision. It should not be the thing that makes it.

We wanted a WebMCP project that took that seriously. Not a confirmation dialog the agent could talk its way through, but a structural boundary. The setting is participatory budgeting, inspired by the gram sabha and ward committee, Kerala's People's Plan Campaign, and Pune's ward budgets, where residents themselves decide how a local fund is spent.

What it does

One resident splits a fixed hypothetical fund of ten lakh rupees across exactly eight local public works that cannot all be funded. An AI browser agent assists.

The agent gets seven WebMCP tools:

  1. get_budget_state: the whole board in one call. Fund, priorities, protected works, the active draft and its validity, revision numbers.
  2. list_projects: the eight works with costs, funding rules, dependencies, incompatibilities, and the full numeric scoring model.
  3. list_strategy_options: three valid budget directions, each scored against the resident's current priorities.
  4. simulate_allocation: run the shared validator on a hypothetical and get every rule violation at once, each with a machine readable fix.
  5. propose_allocation: file a draft as the agent's own, after validation. An invalid draft is still stored, visibly, as a rejected draft.
  6. explain_tradeoffs: what changed between two plans, and what it costs.
  7. request_allocation_review: hand the draft to the resident. It cannot accept or adopt.

Five read only, two that write, none that commits a decision.

There is deliberately no tool to set priorities, protect a work, accept, reject, adopt, or reset. Those are the resident's, through visible buttons on the page. The agent can model the entire budget and draft a valid plan. It cannot make the call, because the function that would make it was never registered.

When the resident protects a work the agent left out, a flood prone riverside park where her children play, the agent's draft goes stale on screen and it has to read the budget again and rebuild around that choice. The machine adapts to the person's decision. It does not get to freeze one.

How we built it

A pure domain engine in TypeScript, framework free, with no DOM and no globals: the eight project dataset, one deterministic allocation validator that returns every issue in a stable order and never repairs anything, a benefit scoring model that is fully disclosed so the agent can reproduce every number, and a deterministic rebuild that keeps protected works and trims the rest to fit.

One in memory store, with React bindings through useSyncExternalStore. The manual UI and every WebMCP tool handler dispatch into the same store instance and call the same validator, so displayed values and tool output cannot diverge. A human budget change bumps a revision counter and stales any active proposal. Only the reducer writes actor labels, derived from the action prefix, so the final record has trustworthy authorship.

The WebMCP adapter registers the seven tools against whichever the runtime provides, navigator.modelContext or document.modelContext, using a shared AbortController so aborting unregisters every tool. Handlers return the MCP result shape with structuredContent, and rule violations come back as data, not errors, so a failed plan stays inspectable.

React 19, Vite, Vitest. No backend, no API keys, no external data calls, no persistence beyond the browser tab. Deployed as a static site.

Challenges we ran into

Registration under React StrictMode. StrictMode double invokes effects, and our first version registered three of seven tools before the second pass cancelled the first. The fix was a caller owned AbortSignal and a deferred registration pass, so the setup, cleanup, setup cycle collapses to one real registration.

Two runtimes, one adapter. The editor draft and ChatGPT expose document.modelContext; Chrome's preview build exposes navigator.modelContext. A judge on Chrome would have seen no assistant at all. We now feature detect and prefer navigator.

Keeping the boundary honest. What we built governs the cooperative structured tool surface a WebMCP style agent negotiates with the page. It is not a privilege boundary: an agent driving the raw DOM could still click the resident's buttons. We say so plainly, in the app and the README. What omission actually buys is that a cooperative agent has no function to call, and the decision record has a human author by construction.

Deployment. Our host froze production deploys mid event on a plan limit, leaving the live site several commits behind. We moved continuous deployment to GitHub Pages, switched to relative asset paths so the build works under a project subpath, and verified the full agent loop against the new URL.

Accomplishments that we're proud of

The boundary is visible, not asserted. The app shows a live trace of every WebMCP call the agent makes, labelled read or write, and a permanent list of the actions no tool can perform. Ask the agent to adopt the plan and nothing happens: no refusal, no error, no tool call. The absence is the proof.

One validated state, shared byte for byte between the human UI and the agent tools. The agent literally cannot be shown a different number than the resident sees.

It degrades gracefully. With no WebMCP runtime present, every flow still works by hand, including the rebuild around a protected work.

What we learned

The interesting lever in WebMCP is not only which tools you add. It is which you withhold, and whether that omission is legible to the agent and to the person. Our get_budget_state returns a structuralLimits field that tells the agent, in plain terms, the things no tool will do. An agent that reads it stops asking.

Enforced by omission beats a confirmation dialog for decisions that must stay human. There is no rubber stamp step to automate, no injection target, and a decision record with a named author by construction rather than by policy.

What's next for Neighbors Decide

Extract the pattern as a small reference other WebMCP builders can adopt: an agent does the preparatory work, the act of deciding stays human and attributable, both sides read the same validated state, and the boundary is a tool that was never registered. The same shape fits clinical informed consent, lending decisions, hiring, content moderation, and legal filing. The budget scenario is a vehicle. The pattern is the point.

Built With

Share this project:

Updates

Submission history