The agent cites. You decide.
I built CiteApply because I kept seeing the same thing in every agent demo: the agent fills the form, and the website has no idea where any of it came from. For a scholarship application that's not good enough. The hard part was never typing. It's knowing which document proves each answer, what to do when two documents disagree, and whether what you submit is what you actually checked.
So I flipped it. CiteApply is a fictional scholarship portal that publishes six WebMCP tools of its own. An agent can read the rules, see the applicant's parsed records as citable evidence, and bind answers to specific quoted lines in one atomic call. It cannot declare the applicant's email, pick between two records that disagree, confirm, or submit. The website enforces that on the server. The model is never asked to behave.
The moment I care about most is the refusal. Two income records disagree. The agent asks to bind income. The site says conflict_requires_human and writes nothing. Then it's the person's turn.
Try it: open the live site in ChatGPT's in-app browser or in Chrome with WebMCP enabled, choose the record set that disagrees, and press Watch an assistant fill this in. A scripted client makes nine real tool calls and you watch it get refused. Or ask your own agent to fill it in; I verified that in the ChatGPT in-app browser, where the agent binds the cited answers and is refused on the conflict. Then finish the three decisions yourself and download the receipt.
Why WebMCP
This only works because the site is a participating website. WebMCP lets it publish a site-owned evidence contract: six small operations (state, requirements, evidence index, evidence-backed mutation, validation, review preparation) that an external agent uses to collaborate with the visible application instead of around it.
- Requirements are live. Binding the dependency field makes guardian name and household size required; the agent is told to re-read the rules mid-session (6 questions become 8).
- Contradictions are structured refusals. Two accepted income records disagree (INR 540,000 vs 480,000).
apply_evidence_backed_answersreturnsconflict_requires_humanand writes nothing. - Readiness is the server's answer.
prepare_submission_reviewfreezes a draft only when the server says it is ready, and returns opaque metadata, never the review itself. - Authority never travels in tool arguments. The page injects its page and consent capabilities as headers from page memory; an agent with a valid cookie but no live page capability gets
stale_pagefrom all six tools.
What changes for the person filling it in
By hand, you open each record, type the value exactly as written, and pick the line that says so; a wrong line or value is refused. With an assistant, the same eight answers are cited in one call and only three decisions remain: choose between the disagreeing records (with a reason), declare your email, and submit. A Feel the difference panel shows your real by-hand tally next to the assistant's counts from the same session. I don't claim a time saving because I didn't measure one.
What people and agents do together
The agent does the reading and the citing; the site validates every binding. The applicant keeps every act of judgement. The frozen review carries the chosen source, the reason given, and both excerpts. The receipt carries the same content hash whether the review was prepared by hand or by the agent.
How I built it
Next.js 15 App Router, React 19, TypeScript, Zod 4, PostgreSQL (Neon) on Vercel. The six tool descriptors are generated from the same Zod schemas the server re-validates against, with closed input schemas and correct readOnlyHint / untrustedContentHint annotations. Record PDFs are parsed at runtime with pdfjs-dist after an allowlisted SHA-256 check; every excerpt is reconstructed from stored page/span anchors, so the UI cannot show a quote the source doesn't contain. Idempotency is keyed on an HMAC of the canonicalised change set. Verifiers prove the production code contains no hardcoded answers and imports no test goldens.
Proven, not claimed
61 node tests (contract, unit, security, real-Postgres integration) and 45 real-browser Playwright specs, including the full 13-step journey for both record sets in Chrome 152 with the WebMCP flag, all passing against the live deployment. Verified by hand in the ChatGPT in-app browser. Axe accessibility clean on every screen. Every tool request and response from those runs is logged verbatim in the repo.
What went wrong along the way
Chrome's real document.modelContext differs from the draft spec: getTools() returns a Promise, executeTool takes the tool object and a JSON string, and no options object is passed. My first callback threw before doing any work. Other hosts differ again, so the in-page client now bounds every host call, accepts both calling conventions, and falls back to the page's own registered tools mid-run, saying so on screen. A second bug only showed up on the deployed site: two quick human actions could share the same expected revision, so the server correctly refused the second as stale. Localhost never showed it because the round trip was too fast. It's fixed with an action queue and a regression test that injects latency.
Honest limits
Everything is synthetic. The parser handles only the six committed records. The in-page demonstration client is scripted (real calls, no model choosing them) and says so on screen. WebMCP is a Community Group draft; support varies by browser build.
Try it
- Live: https://citeapply.vercel.app (ChatGPT's in-app browser, or Chrome with
chrome://flags/#enable-webmcp-testing) - Code: https://github.com/amithrh/citeapply (MIT)
- Judge walkthrough: docs/JUDGE-TESTING.md in the repo
Built With
- next.js
- playwright
- postgresql
- react
- typescript
- vercel
- webmcp
Log in or sign up for Devpost to join the conversation.