Inspiration

Ask any assistant whether the drinking water at an address is safe. You will get a confident paragraph. It will often be wrong, and nothing on the page could have stopped it.

Eighty-eight million Americans are served by a water system with a health-based violation on record. That number is not a guess - it is 4,268 of the 10,517 public water systems in the dataset behind this project. The information is public. It is just buried in federal databases in formats no homebuyer will ever open, so people ask a model instead, and the model reads a table cell and cannot tell a measured zero from a blank nobody filled in.

Most projects in this hackathon will make it faster for a model to read a page. This one changes what a model is allowed to say.

What it does

Six WebMCP tools over 42,679 US ZIP codes, 10,517 water systems, 282 million people. This is not sample data - it is ZipCheckup's production dataset, reviewed and accepted into the AWS Registry of Open Data, built from EPA SDWIS, published Consumer Confidence Reports and EPA radon zones.

The agent can look up a ZIP, compare several, explain what a metric actually measures and against which legal threshold, search the whole country, write to a shortlist the person is watching, and draft a letter to the water utility.

Two things make it different from a nicer API wrapper.

First: a value cannot arrive naked. Lead comes back with the EPA action level of 0.015 mg/L and the citation 40 CFR Part 141 Subpart I in the same payload. The model cannot misquote a number it never read off a page, and cannot lose the threshold that gives the number meaning.

Second: absence is never zero. When the public record holds no value, the tool returns an explicit unknown with the reason and a field stating what that absence does not prove. There is no code path in this project by which a missing measurement reaches an agent as 0. Search a state for safe lead levels and you get three numbers, not one: measured and passing, measured and failing, and how many ZIPs nobody measured at all. For Michigan under 0.005 mg/L that is 466, 209, and 492 excluded for missing data - and the payload says, in words, that exclusion is a data gap and not a safety finding.

That third number is the one every conventional site drops. It is also the one that decides whether a family trusts a house.

What people and agents can do here that they could not before

The shortlist is shared state, not a display. The agent's tool and the person's Add button call the same function in the same store. Rows carry who added them, flash when written, and can be deleted by the person. An activity log prints every call with its arguments and its known/unknown counts.

So the person is not reading a report about what the model did. They are watching it work on the same object, and they can overrule it mid-flight.

And the session ends with something they keep: a letter to the water system serving that address, composed only from facts whose status is known. An unknown value physically cannot reach the letter body, because the composer reads from nowhere else - everything unknown becomes a question to the utility instead of an accusation. When the dataset publishes no contact for that system, the tool returns resolution: "unresolved" and refuses to name a recipient rather than inventing a plausible one.

An agent that will not name an address it cannot verify is a different kind of software than one that will.

How I built it

Six tools on document.modelContext.registerTool under one page-level AbortController. The badge in the header reads the registration back through getTools(), so it is evidence rather than a claim. "Revoke all" fires abort() and the count drops to zero live via toolchange - the correct demonstration of cancellation now that unregisterTool is gone from the spec.

No framework, no bundler, no API key, no build step. Clone it and run npx serve web.

Every compatibility decision was measured on Chrome 152, not read: navigator.modelContext is gone (the spec moved to document on 2026-04-23), getTools() is async and returns inputSchema as a string on Chrome 149-153, and title defaults to "" rather than undefined. Any one of those silently breaks a live demo.

For browsers without native WebMCP, the page vendors the polyfill as a pinned 24 KB IIFE. For judges with no agent attached, an on-page console builds a form from each tool's own inputSchema. And scripts/mcp-bridge.mjs re-exposes whatever getTools() reports as a stdio MCP server, so agents outside a browser can drive it too - it hardcodes no tool names, so it tests the registration instead of reimplementing it.

What I am proud of

The honesty is enforced by code, not by discipline. One envelope module is the only exit from the data layer, so a missing value cannot become a zero anywhere downstream - not in a comparison, not in a ranking, not in a letter.

84 assertions run in real Chromium against the deployed URL, not localhost. They assert that no unknown is serialised as 0, that an unmeasured ZIP is never ranked best or worst, that the word "unknown" cannot appear in a letter body, and that an agent's write renders a row the human can see, attributed to the agent.

I pointed a real coding agent at the tools without telling it which to use. It found them from their descriptions, chained them, and carried the contract into its own prose unprompted: "These blanks do not mean zero, clean, compliant, or safe." That is the whole thesis working end to end, in a model I did not write.

What I learned

Registration takes an afternoon. Deciding what a tool is allowed to say takes the rest of the week - and that is the actual work of the agentic web.

Once the page owns the shape of the answer instead of the model inferring it from markup, honesty stops being a prompt you hope holds and becomes a property you can enforce at the boundary. That generalises far past drinking water.

What's next

Every civic dataset in the country has this problem, and every one of them is about to be read by an agent instead of a person. Water is where I started because the stakes are legible - lead does not care whether the record was digitised.

Next: the PFAS, flood, wildfire and air-quality layers already sitting in the same open dataset. The Chrome origin trial on the deployed domain, so any visitor on 149+ gets the native path with no flag.

And then the ordinary web. Local service businesses answer the same three questions all day - do you serve my address, when are you free, what will it cost. Those are tools, not paragraphs. This project is the proof that an agent-callable page can be rigorous about what it does not know, which matters far more once the answer is about someone's home.

Built With

Share this project:

Updates

Submission history