Spotigo — Project Story
A WebMCP-powered civic agent that turns human intent into safe, executable web actions.
Describe the problem. Let the agent handle the workflow. Keep the human in control of the part that matters.
Inspiration
The web already has the services people need. The friction is almost never "does this exist" — it's "can I get from what I want to say to the specific form this particular website expects." A citizen who sees a pothole thinks someone should report this, not let me figure out which municipal category this falls under, whether it's already been reported, and how to phrase it correctly. The problem exists. The digital service to fix it probably exists too. The gap is entirely in connecting human intent to that workflow — and that gap is exactly what WebMCP is positioned to close.
My first idea wasn't civic reporting at all — it was an accessible, voice-first grocery storefront. Before building much of it, I checked what was already in the WebMCP showcase and found Verdant Market already occupying that lane: "browse groceries and build a shared cart with an agent." Rather than build a variation on an existing reference app, I kept the one pattern worth keeping — the agent prepares, the human confirms — and moved it to a domain where getting that confirmation wrong actually matters: civic reporting, where the output is a public record, not a grocery order.
What it does
A citizen describes a civic issue by voice or text — "there's a huge pothole near my office" — and an agent turns that into a real, trackable report, gated by explicit human confirmation at the one step that counts.
Spotigo registers 10 WebMCP tools in the browser through document.modelContext, deliberately shaped as a workflow rather than a flat list:
| Stage | Tools |
|---|---|
| Understand | describe_issue_accessibly, report_civic_issue |
| Validate | check_duplicate_reports, list_nearby_reports |
| Explain | read_report_summary |
| Authorize | confirm_submission, acknowledge_safety_guidance |
| Act | submit_report |
| Recover / Track | undo_last_action, get_report_status |
A real pass through it: the agent structures "huge pothole near my office" into a draft — category pothole, description, resolved location — checks it against nearby reports and comes back with a graded confidence assessment (not a flat yes/no), then reads the draft back in plain language: "I'm about to report a large pothole at this location with this description. Submit it?" Only after an explicit confirm: true does submit_report become callable, returning a real tracking ID the citizen can check later with get_report_status.
How we built it
Zero build step, zero runtime dependencies, static files only — deliberate, so the WebMCP layer itself stays the thing under inspection, not something buried under framework abstraction.
One execution path, not two. The human-facing UI and the agent-facing WebMCP tools call the same tool.execute() closures — there's no separate "demo path" for the agent. If a judge opens the on-page Tool Tester and fires submit_report before confirming, they get the identical rejection an agent would get, because it's literally the same code running either way.
Ten tools, not one do_everything(). It would have been simpler to expose a single submit_report(details) function and let a system prompt handle the reasoning. That would hide exactly the part worth demonstrating — the validation, the safety boundaries, the point where understanding what someone wants stops being enough to act on it. Decomposing the workflow into explicit stages made the tool surface mirror the actual application logic instead of papering over it.
Intent is not authorization. The agent can understand the issue, check for duplicates, and explain what it's about to do — none of that authorizes submit_report. That gate is enforced in code: confirm: "yes" is truthy in JavaScript but explicitly rejected, because a loose check could let an ambiguous model output pass as authorization. Only strict confirm: true opens the gate.
The gate adapts to risk. Routine issues get one confirmation. Safety-critical ones — a downed power line, a suspected gas leak — route through a distinct acknowledge_safety_guidance step first, and lose the normal undo safety net once submitted. A pothole and a live wire aren't the same category of decision, and treating them identically would have been the easier, lazier design.
A second, separate experiment. server.js and server/ implement a persistent, authenticated MCP service for agents connecting outside the browser — real action storage, ownership isolation, rate limiting, a fail-closed authority resolver. It is explicitly not part of the deployed judged demo. I kept that line hard rather than implying a backend was live when it wasn't.
Challenges we ran into
Choosing what not to build. The competitive check that killed the grocery idea happened before most of the code existed, which made it cheap. That ordering — research first, fall in love with the implementation second — was the single highest-leverage decision in the project.
Making confirmation a real boundary, not a suggestion. Getting the strict-boolean check right (confirm: true vs. anything merely truthy) sounds trivial until you consider that an LLM producing "yes" or 1 instead of true is a completely plausible failure mode, and a loose check would have let it silently authorize a submission.
A Windows path bug invisible in my own dev loop. Automated smoke tests passed cleanly in my Linux sandbox and failed immediately on the actual Windows machine, with ERR_UNSUPPORTED_ESM_URL_SCHEME — Node's dynamic import() requires a proper file:// URL on Windows; a raw path like D:\...\app.js doesn't qualify, even though the POSIX equivalent works fine elsewhere. One-line fix (pathToFileURL()), but a reminder that "tests pass" and "tests pass in the environment a judge will actually use" are different claims.
Git history discipline under time pressure. A substantial chunk of work — the entire server/MCP layer — sat uncommitted for longer than it should have while heads-down building it. A last-minute dump commit would have technically satisfied "real commit history" while missing its point. Going back and committing it in logical pieces was extra effort late in the process, but it was the honest version of the requirement.
Resisting the urge to claim more than was deployed. Once the server prototype worked locally, the tempting move was to fold it into the headline pitch. The harder, better call was a README section that draws a hard line between what's live and judged versus what's a local Phase 2 prototype — because a panel this technical will check claims against what's actually running, and a claim that doesn't reproduce live costs more than the claim was worth.
Accomplishments that we're proud of
- A confirmation gate that's a real, tested execution boundary — including catching and rejecting
confirm: "yes", the exact kind of loose value a model might plausibly produce. - A risk-tiered version of that gate, where safety-critical issues require a distinct second acknowledgement and lose the undo safety net, because not every civic issue carries the same stakes.
- One execution path for humans and agents — no shadow demo logic, verifiable by anyone who opens the Tool Tester.
- 22 passing tests, including one that runs the full judge-facing scenario — draft → duplicate check → review → both negative confirmation cases → submission → tracking ID → status lookup — against the real tool closures.
- One honestly-scoped, fully-working vertical, with a second experimental server layer clearly labeled as not part of the judged demo, instead of five thinly-demonstrated domains.
What we learned
The real opportunity here isn't "AI plus websites" — it's AI agents plus websites that expose capabilities an agent can reliably use, which is a much narrower and more useful claim. The most durable technical lesson was that for agent-authorized actions, truthy is not the same as authorized, and the difference is exactly where a flow can go quietly wrong if you're not deliberate about the type check. The most durable non-technical lesson was doing the competitive research before falling in love with an idea, not after — it was the cheapest and highest-leverage decision in the whole build.
What's next
Swap the in-memory mock city store for a real municipal open-data or 311 API — the tool layer is written so that's the only module that needs to change. Turn the server-MCP prototype into an actually-deployed second entry point, with a verified authority resolver replacing today's fail-closed UNKNOWN stub. And generalize the risk-tiering logic beyond civic reporting — it's the piece most worth carrying into any domain where an agent is one step away from acting on someone's behalf.
The web was built for humans to navigate. Spotigo is a small bet on what changes when agents can use it too — with the human still holding the one decision that matters.
Log in or sign up for Devpost to join the conversation.