PaperPilot is an open-source, secure grant application built to solve a glaring problem in the AI space: grant websites are where AI agents usually break down. They screenshot buttons, guess labels, and occasionally submit data the applicant never even reviewed. Paperwork is high-stakes, and trust matters far more than speed. We noticed a major mismatch: people are already chatting with assistants in side panels, but the forms in the main tab have no structured way to let that assistant help without taking the wheel entirely.
WebMCP was the missing link for us. Instead of forcing an external automation script to crawl the page, WebMCP puts tools directly inside the page, running against the same session cookie as the human while the applicant watches the live UI. We built a mock Small Business Recovery Grant for our demo because it has real-world rules (revenue caps, headcount limits, drops in sales) and heavy jargon (EINs, fund allocation) that people actually need help navigating.
The goal wasn’t to build a bot that says, "Look, the model filled out 11 fields!" The goal was to build a system where the model physically cannot silently commit those fields.
What it does
PaperPilot is a clean, three-step grant application (business details → financials → applicant profile) complete with a review page, a dashboard, and a home-page pre-qualification widget.
In a standard browser, it acts as a normal FastAPI and Jinja form. But open it in Chrome with WebMCP enabled (via flags or the Model Context Tool Inspector), and the page exposes tools like explain_field, check_eligibility, flag_issues, propose_fields, and extract_doc, alongside step-save tools.
Eligibility isn't left up to a vague "vibe check"—it’s driven by a strict rules engine. A business qualifies if their revenue is at most $\$5{,}000{,}000$, their headcount is at most $500$, and their year-over-year revenue drop ($d$) is at least 15%.
The award estimate is tiered based on that drop, plus a workforce bonus:
$$B(d) = \begin{cases} 50000 & d \geq 50 \ 25000 & d \geq 30 \ 10000 & d \geq 15 \ 0 & \text{otherwise} \end{cases} \qquad A = B(d) + \min(500 e,\, 10000)$$
where $e$ stands for full-time employees. Because human reviewers always make the final call, the UI displays this as a calculated range (roughly $0.6A$ to $A$).
Crucially, any fields proposed by the agent are saved as FieldValue rows with committed=False. Only a human-triggered CSRF POST can flip them to committed=True. The final Submit application action is never exposed as a WebMCP tool.
Try it out: Live Demo · GitHub Repo
How we built it
- The Stack: Powered by Python 3.11, FastAPI, SQLModel/SQLite, Jinja2, and vanilla JavaScript in
webmcp-tools.js(no React bloat). Document parsing usespdfplumberand optional Tesseract, passing uploaded files through the exact same validators as typed input plus an injection-phrase filter. Raw OCR data never flows back to the agent. - Two WebMCP Surfaces: We utilized both declarative tools (Steps 1 and 2 use
<form toolname="...">so the browser automatically synthesizes submission tools) and imperative tools (document.modelContext.registerTool()for explanations, eligibility checks, flags, and extractions). - Communication & Security: Every
executecall makes a same-originfetch()to/api/..., returning JSON whenAccept: application/jsonis requested so the agent doesn't crash trying to parse an HTML redirect. Security-wise, we shipped HttpOnlySameSite=Laxcookies, HTTPS cookie enforcement, CSRF protection, strict same-origin checks on mutating JSON APIs, rate limiting (20 calls / 60s / session), and audit logs. Best of all: no server-side LLM API key. The Inspector's Gemini key stays securely in the browser. - Deployment: Containerized via Docker (Python slim + Tesseract) and deployed on Google Cloud Run, with secrets managed through Google Secret Manager.
Challenges we ran into
- Navigation vs. the Inspector: Saving Step 1 by changing
window.location.hrefor pushing to history kept killing the agent mid-turn—tools would vanish, Chrome would crash with mysterious bad-message errors, and traces threw empty responses. A full-page iframe preview was even worse because it registered duplicate tools and threw connection errors. The boring, correct fix? Never change the active tab's URL during a tool turn. Saves return quiet JSON, and the human opens the review page in a new tab when they're ready. - HTML vs. JSON Confusion: The agent's
fetch()calls kept blindly following human303redirects and blowing up onJSON.parse("<!DOCTYPE html>"). We fixed this by cleanly splitting responses based on theAcceptheader. - Human-in-the-Loop as Data, Not Copy: Anyone can write UI text saying "the user confirms." Enforcing that
committed=Trueis structurally impossible to reach from an agent tool path took careful hook design backed by automated tests. - Cloud LLMs in the Backend: We initially experimented with server-side Azure OpenAI for field explanations before ripping it out entirely. Grant copy should be deterministic, and document parsing belongs to regex and strict validators. Keeping the LLM solely as the browser agent kept our deployments lightweight and kept API keys out of Cloud Run.
- Ephemeral SQLite on Cloud Run: It’s fine for a hackathon demo, but a real-world program would need Cloud SQL. We documented the limitation transparently instead of faking persistence.
Accomplishments we're proud of
- Getting a fully functioning WebMCP grant form live on Cloud Run that a Chrome Inspector agent can drive seamlessly with a single prompt—handling explanations, eligibility checks, estimates, and proposals—while leaving final submission strictly in human hands.
- Successfully blending declarative
<form>tools and imperativeregisterTool()workflows under a single session. - Engineering the uncommitted-values pattern where agents draft data, but only human-driven CSRF actions can commit it.
- Writing 99 automated tests covering validators, rules logic, hooks, and API contracts.
What we learned
WebMCP isn’t just "ChatGPT that can click." It’s about capability discovery on a document—handling schemas, respecting read-only hints, and avoiding automatic submissions unless explicitly intended. The hardest product problem is lifecycle management: figuring out what happens to modelContext when a document navigates.
We also learned to treat the Chrome Inspector like a fragile integration partner: keep it to one tab, don't reload mid-turn, and register tools exactly once. On the domain side, we realized that while eligibility rules are just basic math, applicants feel them as intense anxiety. Presenting awards as a calculated range rather than a hard promise made the whole demo feel like an authentic, reassuring government portal.
What's next for PaperPilot
- Persistent Cloud Storage: Migrating from ephemeral SQLite to PostgreSQL/Cloud SQL to support multi-session draft recoveries.
- Expanded Document Support: Adding automated parsing for corporate balance sheets, W-2s, and 1099s to speed up financial verification.
- Enterprise Agency SDK: Packaging our WebMCP components into a drop-in SDK so local and state governments can modernize their legacy portals without compromising security or compliance.
Log in or sign up for Devpost to join the conversation.