What it does
Ombuds is a work authorization worksheet, rebuilt so that the page is the authority instead of the agent.
The worst forms on the web are long conditional government forms. A work authorization application asks you for an eligibility category code, and that one answer silently rewrites which of the next forty questions apply to you. Pick category c(3)(C) and you now owe a CIP code and an employer E-Verify number. Pick c(9) and neither of those exists, and you owe an I-485 receipt number instead. Say you want a Social Security card and both your parents' names become mandatory. None of this is written down anywhere the form can show you. It lives in footnotes and in the head of whoever has filled one out before.
Agents are bad at these forms for the same reason people are. An agent reading the screen has no way to know that picking a category changed the rules. A backend MCP server cannot help either, because it has no idea what the person sitting in the browser has already answered.
Ombuds moves the rules into the page and hands them to the agent directly. You talk to your agent in plain language about your situation. The page tells the agent exactly which questions apply to you right now, gives it the precise list of values each answer accepts, rejects anything malformed with the actual rule and a valid example so the agent fixes itself, and then holds every single value in a review queue until you personally accept it. Nothing an agent says about your immigration status is true until you agree with it.
Three things it deliberately will not do. It will not tick your certification, because only the person filing can attest to their own application, so there is no tool for it at all. It will not give legal consent on your behalf, so the field authorizing disclosure to the Social Security Administration refuses agent writes outright. It will not let you generate your filing packet until every section is valid, your review queue is empty, and you have signed by hand.
It is an independent worksheet. It is not affiliated with USCIS, it does not file anything, and everything stays in the browser tab.
Inspiration
I spent a year doing missionary work in Cambodia and watched people lose months to paperwork written as though the rules were obvious. The rules are never obvious. Putting them in the page, where an agent can be told them directly and a person can still overrule the answer, is a better use of this standard than making a shopping cart marginally faster.
How I used WebMCP
Eight mechanics. The first five are things WebMCP can do that a backend integration structurally cannot.
1. The tool schemas are generated from live form state. answer-category-questions is one tool with one name. Its schema is assembled from whichever eligibility category you picked. Category c(9) advertises a single field. Category c(3)(C) advertises four completely different ones. Change your category and the tool is unregistered and registered again with an entirely different shape. A server-side tool would have to accept a vague bag of optional strings and hope for the best. This one accepts exactly the four things you actually owe and rejects anything else as FIELD_NOT_APPLICABLE. Every enum is generated the same way from the same lists the HTML renders, so an agent cannot invent a state code or a category code, because the accepted values are sitting in the schema it was handed.
2. One predicate drives both the human UI and the agent's schema. Every conditional field carries a single showWhen function. The renderer uses it to decide whether to draw an input. The WebMCP layer uses the same one to decide whether that field belongs in the tool's schema. The human and the agent are looking at the same form, always, because there is only one description of the form and it cannot drift out of step with itself.
3. Tools appear and disappear so the agent only ever sees a legal move. Sections unlock as their prerequisites become valid, and a section's tool exists only while that section is open. Before Part 1 is valid there is no set-personal-info to call. Answer no to having used other names and add-other-name is not disabled, it is gone. Say your mailing address is where you live and set-physical-address unregisters itself. The agent gets a state machine rather than a flat API, and toolchange tells it when the machine moved. The sidebar shows the live registered set so you can watch tools flash in and out as you answer.
4. Rejections are structured, so the agent corrects itself. The page owns the validation rules, so it can hand back something an agent can act on instead of a red border. Send a bad SEVIS number and you get a machine-readable code, the rule in plain language, where to find the real value on your Form I-20, and a concrete valid example. The agent reads that and fixes its own argument on the next call. Nobody retypes anything. Cross-field conflicts work the same way. Claim category c(3)(C) while declaring H-1B status and you get the specific contradiction with both responsible fields named, because that is the class of error that actually gets real filings bounced back weeks later.
5. Nothing an agent writes is true until a human agrees. State is kept in two layers. Committed values are what the applicant accepted. Pending values are what the agent proposed. Proposals are validated the instant they arrive so the agent gets its feedback immediately without waiting on a human, and they surface in the page as amber diff cards with accept and reject buttons. Only a human click promotes a value.
6. Both halves of the spec, each where it fits. The imperative API carries the conditional sections, because branching logic is exactly what a form cannot express. The category finder at the top of the page is a declarative <form> carrying toolname, tooldescription and toolparamdescription, which a supporting browser turns into a tool with no registerTool call at all, and its submit handler answers the agent through SubmitEvent#respondWith() so the page never navigates away and throws out the conversation. Declarative suits that panel because it is a flat search over a fixed list. One ranked search with synonym expansion backs both paths, so a person typing "spouse of an H-1B worker" and an agent sending the same words get the same answer. Using each API where it fits is the argument the WebMCP explainer itself makes, and building both is what made me believe it.
The tag beside the finder reports whether the browser actually synthesized the declarative tool rather than inferring it from a version number, and declarative calls are badged separately in the call log.
7. Tools that cross an origin boundary. The document vault at the bottom of the page is a separate service on its own origin, embedded in an iframe carrying allow="tools" so the permissions policy lets it register tools at all. It registers with exposedTo naming only the form's origin, and the form discovers them with getTools({ fromOrigins: [...] }) and runs them through executeTool. The browser refuses the call unless both halves agree.
The point of the split is the trust boundary. The form asks whether a required document exists and when it lapses, and never receives the document. A form that stored your passport scan is a different product with a different risk profile, and only tools that carry an origin can express that difference. The payoff chains both origins: the precheck builds a document checklist here from your eligibility category, then each line is resolved by a tool running over there, coming back as on file, expiring, expired, or missing. My own I-20 line comes back expired, which is the kind of thing you would otherwise learn by mail.
Cross-origin exposedTo needs browser support the origin trial does not universally have yet, so the same tools are also reachable over a postMessage bridge with a deliberately identical shape. The calling code does not branch, and the panel reports which transport is actually live rather than claiming the stronger one. Both paths enforce the same origin allowlist, and a probe from a disallowed host origin gets no reply at all.
8. A working answer to an open question in the standard. WebMCP Issue #165 is open on how a tool should prompt the user for explicit authorization mid-execution. No API for it exists yet, so I built one from what the spec already provides, since execute may return a promise and it receives an AbortSignal. request-certification suspends until a person clicks. The page raises the question with the agent's own note attached, cancellation propagates and clears it, and there is no code path by which the agent produces its own approval. An agent may ask for a signature. Only a human can supply one.
That replaced an earlier design where the certification had no tool at all. Refusing to expose it made the guarantee but left the agent unable even to ask, which is a worse product for identical safety.
Filing windows. check-filing-window works out whether this applicant can file today. Post-completion OPT opens 90 days before the program end date and closes 60 days after it. A STEM extension must be filed before the current card lapses. Filing outside the window is a rejection you learn about by mail weeks later. The rule depends on the category and on a date the applicant already entered, so only something running in the page can compute it.
Recovering from a mistake. Withdrawing a proposal only helps before someone accepts it. The store keeps a revision history of committed changes with their source, and undo-last-change walks the most recent one back and reports what it reverted and who had entered it.
Accessibility. The explainer names improving accessibility through agents as a goal, so the form is operable without a mouse or a screen. Boolean answers are a real radiogroup, named by their question, with one tab stop and arrow-key selection. Field errors are wired to their controls with aria-describedby and aria-invalid and announced with role=alert, so a rejected value is heard rather than only outlined in red. The tool surface entries are buttons rather than clickable list items, and the tool list, review queue and call log are polite live regions, so a tool appearing or a proposal arriving is announced instead of silently changing. There is a skip link, visible focus throughout, and a prefers-reduced-motion branch that drops the register and unregister animations.
Saving is opt-in, and that is deliberate. This form collects immigration status, government file numbers, and in some branches a Social Security Number, so persisting it is the applicant's decision rather than a default they inherit. Turning it on stores committed answers on that one device and deleting the draft is one click. Pending proposals never persist, because restoring a stale review queue would ask someone to rule on a suggestion they no longer remember. The certification never restores, because a signature is an attestation about what the signer was looking at. A draft that fails to parse is deleted rather than partially applied, since silently seeding a form with values the applicant never entered is worse than starting over.
How the experience improves
Without WebMCP, an agent helping with this form is guessing. It reads the DOM, cannot tell which branch it is on, fills fields that do not apply, invents plausible-looking category codes, and gets format rules wrong silently. The person ends up checking every field by hand, which is the work they were trying to avoid.
With WebMCP the division of labor inverts. The agent handles what it is good at, which is turning "I am a Cambodian student on STEM OPT" into the correct category code and the four specific pieces of evidence that code requires. The person handles what only they can do, which is knowing their own facts and taking responsibility for the filing. The page referees, and refuses to let either one do the other's job.
The concrete wins are that a forty-field conditional form becomes a conversation, wrong formats get caught in seconds instead of weeks, questions that do not apply to you never appear at all, and you still read and approve every value before it counts.
Challenges I ran into
The bug in the vault is the one I would tell another developer about. Requirement matching walked the document list and returned the first entry that satisfied any clause, so the checklist line "Two identical passport-style photographs" matched the Passport, because a loose type-substring test found "passport" inside "passport-style" and the Passport came first in the array. It reported a valid passport in place of photographs that were about to lapse. Matching is now ranked by specificity, so an exact requirement match beats a substring which beats a bare type mention, and array order no longer decides correctness. Fuzzy matching that short-circuits on the first hit will eventually pick the wrong thing.
Building filing windows exposed an inconsistency in my own domain model. The STEM extension rule was anchored on the program end date, a field that category never collects, so the window silently reported itself uncomputable rather than wrong. A STEM extension actually keys off the current card's expiry, so the category asks for that now and the rule points at it. Domain rules that reference fields the form does not gather are a quiet class of bug, and it only surfaced because a test expected a real answer instead of an absence.
The other interesting bug was a trap I built for myself. Section tools were registered only when their prerequisite sections were fully valid, cross-field checks included. That produced a state an agent could not escape. Declaring an immigration status that conflicted with the chosen eligibility category made both sections invalid, which unregistered set-immigration-history, which was the only tool capable of fixing the conflict. The agent was told about a problem and simultaneously denied the means to solve it.
Registration now depends on per-field validity only. Cross-field conflicts are reported loudly and they block the final packet, but they never retract a tool. There is a regression test guarding it.
The general lesson generalizes past this app. Once tool availability is derived from application state, it becomes possible to strand an agent in a corner it cannot get out of. Reachability is a property worth checking on purpose, the same way you would check that a state machine has no dead ends.
The second thing worth mentioning is that a boolean guard and an incomplete prerequisite look identical to an agent, and they are not the same thing at all. One will open later and one never applies to this person. get-form-status reports which is which, because an agent that cannot tell them apart will either wait forever or keep retrying.
What I learned
The thing that changed how I think about this standard is that a page knows things no server can know. My validators are not clever. They are the same rules that were always in the form, and the only new idea is handing them to the agent in a format it can act on rather than rendering them as a red outline a human has to interpret.
The other lesson is that the interesting design questions are about restraint. Deciding which tools not to expose took longer than writing the ones I did. The certification checkbox has no tool, and that absence carries more meaning than any tool I wrote.
What's next
Cross-origin federation works over a bridge in browsers that do not yet support exposedTo across origins, and standardizing elicitation properly would replace my promise-suspension approach with something the browser mediates.
The form definition is data, so the same engine should carry the other forms in this class. Financial aid applications, medical intake, benefits enrollment. They all have the same shape, which is a branching questionnaire whose rules are invisible to everyone who has not filled one out before.
Built With
- css
- html
- javascript
- json-schema
- vercel
- webmcp
Log in or sign up for Devpost to join the conversation.