Inspiration
Eligibility forms ask for far more than the decision needs. A housing programme only needs to know your household income is below a threshold, but the form wants your exact salary, your date of birth, and your home address before you even find out if applying is worth it. A browser agent working on your behalf makes it worse, because it will happily carry that context from site to site and fill every field it can.
The WebMCP spec names this problem itself. Section 6.3.3, "Privacy Leakage Through Over-Parameterization," describes sites defining richly parameterized tools that agents fill from personalization context, and then Section 6.4 lists three mitigations and none of them covers it. We wanted to answer a risk the spec authors flagged and left open.
What it does
Mahaba is a WebMCP wallet where the agent's tool registry is the privacy control. You enter your sensitive facts once, into a profile that never leaves your browser. The app turns them into narrow claims: income is below a number, age clears a minimum, residence is inside a set of districts. You choose which claims an agent may use, and for how long.
Two things set it apart from a permission dialog. A granted claim does not become a guarded tool, it becomes a tool that exists, and when consent lapses or a timer runs out the tool stops existing and the agent's own getTools() no longer lists it. And no tool that returns a raw value exists at any point. There is no get_income, no get_date_of_birth. An agent can learn that income is below 30,000, but it cannot learn the income, because the capability to ask was never registered. The interface proves that absence against the browser's live registry instead of just claiming it.
The person and the agent watch one surface. Grant a claim and the tool shows up in the same console the agent is calling. Let it expire and it drops out of both views at the same moment. When the session ends you get a signed, append-only log of every comparison that was made, and not one raw value.
How we built it
Two things ship: a reusable module and an app that uses it.
capability-gate is a dependency-free TypeScript module that ties any WebMCP tool's existence to consent. WebMCP has no unregisterTool, so a tool is revoked by aborting the AbortSignal you pass to registerTool. The gate keeps one AbortController per grant. Granting registers the tool, revoking aborts it, and expiry aborts it on a timer. That is the whole trick, and it is why revocation is real instead of cosmetic.
The app has three tiers of capability. Persistent tools disclose nothing on their own and are always there. Consent-gated claim tools are absent until you permit them and present only while consent holds. A single approval-gated write drafts an application from verified claims and needs its own grant because it writes. Every claim validates its own input, because we found Chrome does not actually enforce inputSchema. Tools return a structured { ok, result } or { ok, error } envelope, because a thrown error reaches the agent as an opaque UnknownError it can do nothing with. Receipts are signed by a Netlify Function whose key never touches the browser.
The stack is small on purpose: TypeScript, Vite, plain DOM, Netlify. No framework. capability-gate is the part any WebMCP site can lift out and drop in.
Challenges we ran into
Chrome does not behave the way the spec text reads, in two ways that ate real debugging time. Both are written up in probe/FINDINGS.md. executeTool takes a required JSON string, not the optional object the spec declares, so passing an object stringifies to "[object Object]" and gets rejected. And inputSchema is advisory, not enforced, so a tool that marks a property required still runs when it is missing. That second one turned per-tool validation from a nicety into a security requirement.
Testing was its own fight. WebMCP tool calls round-trip through the browser process, which virtual time does not drive, so the usual headless DOM-dump trick dumps the page before a tool call ever resolves. We ended up driving real Chrome over the DevTools Protocol and polling for a completion flag.
Deployment had one more surprise. WebMCP needs an origin-keyed agent cluster, and registerTool throws SecurityError without one, so the response has to send Origin-Agent-Cluster: ?1. localhost hides this by reporting the cluster as isolated no matter what, so the header only proved itself on a real deploy.
Accomplishments that we're proud of
The guarantee is shown from ground truth, not asserted. For every raw field the wallet holds, the panel checks the getter that would read it against the browser's own getTools() and shows it is not there. A revoked tool cannot run even for a caller holding a tool handle it grabbed while the grant was live, and there is a test that proves exactly that.
The module generalises. We built a second app, a restricted-medicine pharmacy checkout, on the same gate, console, and consent loop with a completely different claim set. It confirms a buyer clears an age limit and has no conflicting condition before it sells, without reading the date of birth or the medical history. Only the domain changed.
The app also passes Google Chrome Labs' own webmcp-evals harness, the WebMCP team's first-party tooling. Its no-LLM smoke mode runs real tool calls against the live page and all the persistent tools pass end to end. That is an outside check, not one we wrote.
What we learned
Consent feels different when it is the thing the agent can call, not a setting buried in a menu. Watching a tool appear when you grant it and vanish when the timer runs out, in the same view the agent is using, makes the agent's authority concrete in a way a permissions page never manages. Building it also taught us to trust the browser over the spec text. Every decision that mattered came from something we checked in real Chrome, not from reading the IDL.
The sharpest lesson was that absence beats restriction. A tool that is only guarded can still be coaxed, over-filled, or talked into misbehaving. A tool that does not exist cannot.
What's next for Mahaba
This is a minimum-disclosure prototype on synthetic data, not a cryptographic identity system. The obvious next step is to back the claims with verifiable credentials or trusted issuers, so "income below 30,000" carries proof instead of the wallet's word for it. Past that, capability-gate is the part we most want other people to use: a small, dependency-free way to make an agent's authority on your site revocable, expiring, and inspectable by default.
Built With
- chrome
- chrome-devtools-protocol
- css
- hmac
- html
- javascript
- mcp
- model-context-protocol
- netlify
- netlify-functions
- node.js
- puppeteer
- typescript
- vite
- webmcp
Log in or sign up for Devpost to join the conversation.