Inspiration

To find out whether a household qualifies for heating assistance, food assistance, or a childcare subsidy, a caseworker needs facts held by three separate agencies, and those agencies are forbidden to share them. The restriction is legal rather than technical, which is why the usual answer (put it all in one database) is not available. So the household becomes the courier: a portal and a login for every agency, documents printed and re-keyed into six forms that all ask the same questions. Benefits go unclaimed because the process is exhausting instead of people not being able to qualify.

WebMCP is the first thing that fits the actual shape of that problem. It lets a site offer a capability without handing over the data behind it, and it expresses consent as something the browser already understands: an origin. A server-side MCP integration cannot do this, because the agencies would have to trust a central integrator with the records themselves, which is the exact thing the law forbids.

What it does

Vouchsafe is a benefits screening workbench for a caseworker. They ask one question in plain language, and six programs are determined across three agencies in seconds, with the reasoning shown requirement by requirement.

Each agency keeps its record at its own origin and answers only predicates. attest_income_below(threshold, tax_year) returns {attested: true}, never 34200. The release of information a client signs is expressed as exposedTo: ['https://vouchsafe-app.vercel.app']: this site may ask, and no other site may.

The office ends up safer than before, not merely faster. A disclosure ledger shows every question that crossed an origin boundary, which field stayed at the agency, and what came back. After a full screening, it reads eleven questions asked, zero record fields disclosed. A caseworker who never had the income figure cannot leak it.

What people and agents can do together that was difficult before

An assistant can compose a decision across mutually distrustful origins that will not share data, and none of them has to trust the assistant, the broker, or each other with a record. Three things follow:

Determination without disclosure. Utility Arrears Relief comes back eligible having asked nobody anything, because it is categorically eligible via heating assistance. The determination is derived entirely from another determination. Consent that is revocable mid-session, from the UI. Withdraw a release at the agency and its AbortController fires, toolchange reaches the broker, and the tool stops existing. The assistant does not refuse the question; there is nothing left to call. An honest third answer. Programs that needed the departed agency turn indeterminate, never ineligible. Absence of proof is not proof of ineligibility, which matters when the output is somebody's benefits. How we built it

Four separate origins. Plain ES modules, no build step, zero npm dependencies.

The three agencies register attestation tools with document.modelContext.registerTool(tool, { exposedTo: ['https://vouchsafe-app.vercel.app'], signal }), each returning a minimal-disclosure envelope of {attested, predicate, agency, ref, ts} and nothing else. Revenue also registers get_income_record without exposedTo, as a control case: the broker cannot see it, and a "Probe the boundary" button demonstrates that live.

The broker is a composer, not a forwarder. It discovers agency tools with getTools({fromOrigins}), calls them with executeTool(), and re-syncs on toolchange. It then registers six tools of its own for the assistant, so the agent sees one coherent surface rather than a pile of forwarded predicates, and never sees an agency tool at all.

Challenges we ran into

WebMCP is young, and most of the real work was discovering how it actually behaves rather than how we assumed it would. Six findings are documented in smoke/RESULTS.md. The three that shaped the architecture:

getTools({fromOrigins}) is additive, not exclusive. It returns the caller's own tools alongside those of the requested origins. Before we caught it, the broker's own facades landed in every agency's bucket, so agencies read as online while their tools were missing. The broker now partitions results by tool.origin. executeTool() takes a JSON string and returns a JSON string, not objects, so arguments and results both need serialising and the response needs parsing twice. Registering a tool that already exists throws InvalidStateError: Duplicate tool name. Our toolchange handler was not awaiting its own callback, so two refreshes could overlap and the second collided with the first.

Verification was its own problem: WebMCP does not exist in headless Chrome, so a normal headless test captures an empty, broken page. Every check drives a real headful Chrome over the DevTools Protocol instead, and cross-origin iframes are out-of-process, which needs Target.setAutoAttach with flatten: true to reach them at all.

Accomplishments that we are proud of

The central claim is measured rather than asserted. tools/verify-agent.js walks the entire agent path and scans every byte the assistant can see for the record values: leakedSecrets: [] across 9,940 bytes. tools/verify-revocation.js proves the revocation path against production, not a mock. ?selftest=1 runs seven assertions inside the live page, so a judge can check the claims without reading any code. Thirty-six unit tests cover the program graph and the envelope.

The control case is the part we would point at first. A tool registered without exposedTo is not merely undocumented or unlisted; the broker genuinely cannot reach it, and the page lets you try.

What we learned

The important design decision was making the broker compose rather than forward. Proxying twelve agency predicates straight to the assistant would have been less code and a worse product: the agent would carry the burden of knowing which agency holds what, and every question would cross the boundary individually. Six facades that each consult several origins gave a better agent experience and a smaller disclosure surface at the same time.

The second lesson was that a privacy claim is worth only as much as its instrumentation. "No records are disclosed" is easy to write and easy to break by accident, so the ledger counts real crossings and the verifier greps the agent's own view for the secret values.

What is next for Vouchsafe

The predicates here are hand-written per agency. The natural next step is a declarative attestation schema, so an agency can publish which predicates it answers without writing a tool by hand, and a signed attestation reference, so a determination can be re-verified later by a third party from the ref alone.

Beyond benefits, the same shape fits anywhere a decision needs facts that are legally forbidden to pool: clinical eligibility, immigration casework, school enrolment, tenancy checks. The interesting property is not that the data stays put. It is that the consent to ask is a browser primitive an ordinary person can withdraw.

Built With

Share this project:

Updates

Submission history