Inspiration
Booking a medical appointment online is a perfect example of that barrier. It's a routine task for most people, but for someone elderly, or unfamiliar with dropdowns and date pickers, a five-field form can be genuinely frustrating. We wanted to see what that experience looks like when an agent can act on someone's behalf — and, just as importantly, when it knows to ask instead of guess.
What it does
GrandGrandMCP is an appointment-booking calendar where a person can type or speak a plain-language request — "book me a doctor's appointment for tomorrow morning" — and an AI agent finds an open slot, books it, and drops it onto a visual calendar next to the user's existing personal events (bank errands, birthdays). If the agent doesn't have a detail it needs, like the patient's name, the app pauses mid-booking and asks — a single, friendly follow-up question instead of silently making something up.
How we built it
- A single Cloudflare Worker serves both the frontend and the API from one origin, backed by D1 for storage — appointments and personal calendar events both persist there, so the agenda survives a page reload.
- The frontend registers three tools through
document.modelContext.registerTool():search_available_slots,book_appointment, andcheck_appointment_status, each with a JSON Schema describing its inputs. book_appointment'sexecutecallback checks whether a patient name was provided. If not, it callsclient.requestUserInteraction()— a WebMCP feature that pauses the agent's tool call and lets the page ask the person directly, right under the calendar, before completing the booking.- Voice input uses the browser's native
SpeechRecognitionAPI, so a request can be typed or spoken. - The calendar itself is a minimalist, pastel-themed UI built from scratch, with no framework — just HTML, CSS, and vanilla JS.
Challenges we ran into
executeTool()doesn't return what you'd expect. When calling our tools through Chrome's nativedocument.modelContext.executeTool()(with the WebMCP flag enabled), the result came back wrapped in MCP-style content blocks instead of the plain object our ownexecute()function returns. We didn't catch this until a booking confirmation printedundefinedfor the date and time — which led us to write a small normalization layer that unwraps both shapes transparently.- Fragile natural-language parsing. Our first attempt at pulling a patient's name out of a sentence used a regex anchored on the word "for" — which broke as soon as a message contained "for" twice (e.g. "...for John Smith for Saturday"). We replaced it with a capitalized-name heuristic, and more importantly, leaned on
requestUserInteractionso the tool itself doesn't need to be clever about parsing — it can just ask. - State that quietly wasn't persisting. Early on, the calendar looked complete, but personal events were only seeded in JavaScript memory, and booked appointments weren't being re-fetched from D1 on page load. Everything looked right until a refresh made it obvious.
What we learned
WebMCP is at its best when it replaces genuine friction — not when it's added to a flow that would work the same without it. The most valuable primitive we used wasn't tool registration itself, it was requestUserInteraction: the ability for a tool to pause and loop a human back in is what makes an agent feel collaborative instead of either fully autonomous or fully manual.
What's next
I want to expand this idea to other fields for someone elderly, not just medical.
Built With
- cloudflare
- webmcp
- worker
Log in or sign up for Devpost to join the conversation.