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, and check_appointment_status, each with a JSON Schema describing its inputs.
  • book_appointment's execute callback checks whether a patient name was provided. If not, it calls client.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 SpeechRecognition API, 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 native document.modelContext.executeTool() (with the WebMCP flag enabled), the result came back wrapped in MCP-style content blocks instead of the plain object our own execute() function returns. We didn't catch this until a booking confirmation printed undefined for 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 requestUserInteraction so 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

Share this project:

Updates

Submission history