Asia/Kolkata. Substitute your own account, calendar address and numbers.
What you need
1
Connect Google Calendar
Google Calendar is a native integration: nine tools implemented against the Calendar REST API directly, with no vendor-hosted server in between. From the builder’s side they behave exactly like any other MCP integration — same attachment, same approval, same namespacing.In the dashboard, go to Integrations → Add integration → Google Calendar, and sign in as the account that owns the calendar. Google asks for seven things: your identity, the list of calendars you have, read access to events, free/busy access, and the ability to create and change events on calendars this account owns.OAuth cannot be started from the API. Once you are back on the Integrations page, read the id and confirm what it offers:Nine tools come back:
list_calendars, list_events, search_events, get_event, create_event, update_event, delete_event, respond_to_event and suggest_time. A fresh connection approves all nine.2
Approve only the tools this agent needs
A booking agent that always books on the connected account’s own primary calendar needs exactly two: Three rules about that call, all of which bite in practice:
suggest_time to find free periods and create_event to write the appointment. Approve those two and nothing else.Narrowing is not tidiness. Every approved tool is described to the model on every turn — its name, its purpose and every argument’s description — so nine calendar tools is nine schemas in the input tokens of every reply, on your own provider key, and nine similar-sounding options for a model that has one job. On a voice call that shows up as both a slower first token and a model that reaches for search_events when it meant suggest_time.While you are there, shorten the namespace. The default is the provider key, which makes the model read google_calendar_suggest_time; calendar is shorter and just as clear.- Approval matches the tool’s own name. Filtering happens before the namespace is applied, so you approve
create_event, nevercalendar_create_event. That is also why changing the namespace later never invalidates an approval. - The prompt must use the exposed name. After this patch the model sees
calendar_suggest_timeandcalendar_create_event, and a prompt that sayscreate_eventdescribes a tool the model cannot find. - The list replaces wholesale and is live. It is a set, not an accumulation, and narrowing it reaches every already-published agent on its next turn — there is nothing to republish. The approval belongs to the integration, so it is shared by every agent attached to it.
list_calendars as well and have the prompt resolve the name once; or read the calendar’s id out of Google Calendar’s own Settings → Integrate calendar panel and put it in the prompt, which costs nothing per turn.3
Build the two tools of your own
Two things the calendar cannot do for you: remember what was booked, and reach a person.Recording the booking. A calendar tool writes to Google and returns its answer to the model; nothing about it lands in the session’s state. A small tool of your own does, and it costs one turn of nothing:No operation in that tree can return a response, so the tool is silent by derivation: it saves the four values and the agent says nothing extra on top. What it buys you is threefold. The rest of the call can read
{{userdata.booking_starts_at}} without asking again. The values are on the call record afterwards, under session.userdata. And the tool call itself — its name and every argument — lands in the transcript, which is what post-call analysis reads, so the extraction below has something unambiguous to work from rather than inferring a date out of “half past ten on Thursday”.Reaching a person. One tool per destination, argument-free, with the escalation policy in its description:POST it to /v1/tools and publish it exactly as record_booking was published; the examples below call the result transfer. on_failure: "continue" brings the caller back to the agent with a plain-English reason when nobody picks up, rather than dropping them. See transfer.4
Write the prompt
Everything hard about a booking agent is in the prompt: collecting enough before it touches the calendar, offering times a person can hold in their head, and saying dates in a form a text-to-speech voice will not mangle.
The clock, and why nothing works without it
{{system_vars.date}}, {{system_vars.time}} and {{system_vars.now}} resolve against the agent’s own config.timezone, an IANA name. Without it, using any of the three is a save error naming the field — so an agent with no timezone cannot be told what day it is, and a model that does not know today’s date cannot turn “next Tuesday” into a timestamp. It will invent one.Two properties of those tokens matter here. In a prompt they are frozen at the moment the agent starts, which on a call is the moment it answers — right for one call, wrong for a call that runs past midnight. And {{system_vars.now}} is a machine timestamp (2026-08-19T19:26:59+05:30): it is there for the model to build arguments from, which is why the prompt above forbids reading one aloud. See variables.The rules that bite
Let
suggest_time do the arithmetic. It reads free/busy for every listed address, inverts it, clips the result to your preferred hours per local day so it stays correct across a daylight-saving change, and drops anything shorter than durationMinutes. A model asked to do that itself from a list of events gets it wrong on the third call of the day.5
Create the agent and publish it
Save the prompt above as Two notes on that config. Attaching an MCP integration takes a publish —
reception-prompt.txt and let jq fold it into the body, so a multi-line prompt never has to be escaped by hand:mcps is frozen into the published version like everything else, so the calendar tools are not in front of the model until you publish, while allowed_tools on the integration is live and reaches an already-published agent on its next turn.And clinic_email is declared with a default, deliberately. A variable that is required: true with no default refuses every session that does not supply one, and an inbound phone call carries no request of ours to supply anything — so assigning that agent to a number is refused outright, and so is publishing it while it is already assigned. Anything an inbound agent needs must have a default. See variables.Test it
Book something real, then look at the calendar. Use the agent editor’s test panel, which runs the latest published version in the browser, and walk the whole path: give a name, a reason, “sometime Thursday morning”, and take the first time offered. Then open Google Calendar. Four things to check:- The event exists, on the right calendar, at the time the agent said aloud — not an hour off, which is what a missing
timeZonelooks like. - Its title is the service and the name, and its description carries a phone number.
- Nobody was emailed. With no attendees,
notificationLeveldefaults to nothing being sent. - The duration matches what the prompt said it should be.
GET /v1/calls/{session_id} gives the transcript with every tool call and its result inline. Read three things in order:
- the
function_callitems forcalendar_suggest_timeandcalendar_create_event, and the arguments the model actually chose — this is where a wrong window or a missingtimeZoneshows; session.userdata, which should hold the fourbooking_*keysrecord_bookingwrote;session.analysis, once the call has finished: the summary,outcome,outcome_rationale, andappointment_atmatching what is in the calendar.
A web test call cannot exercise the transfer:
transfer is phone-only and fails on a web session, and {{system_vars.human_phone_number}} is empty there, so the event description will be blank. Both work on a real phone call. Test the failure path deliberately too — disable the integration for a moment and confirm the agent says the diary is not responding and offers a person, rather than inventing a confirmation.What to watch in the first week
Whether it is offering times or reading lists. Open five transcripts and look at the turn straight aftercalendar_suggest_time. Two concrete times is the target; a paragraph of availability is a caller who will hang up and ring back. That is a prompt fix, not a model fix.
The calls with no booking. GET /v1/calls?outcome=failure and read not_booked_reason across them. “No time that suited” clusters mean the preferred hours in the prompt are wrong for your callers; “calendar error” means look at the tool results; “wanted to reschedule” is a feature request the section below answers.
Dates and money in the caller’s ear. Listen to two recordings, not just the transcripts. A transcript that reads “Thursday the ninth” tells you nothing about whether the voice said “sixteen hundred” instead of “four in the afternoon”. Text-to-speech is where a correct booking becomes an unclear one, and every fix is a line in the SPEAKING section.
Where this falls short
The agent sends no confirmation. There is no SMS on this platform at all, and the booking above emails nobody because it has no attendees. Three honest options. Add the caller as anattendee with their email address and let Google send the invitation — which means asking a caller to spell an email address aloud, and getting it wrong sometimes. Add an http tool that posts the booking into your own practice system and lets that send the message you already send. Or export the day’s bookings and use an email batch — remembering that the provider’s terms require the recipient to have opted in.
It cannot move or cancel an appointment. That needs search_events to find the caller’s existing booking and update_event or delete_event to change it, all three approved on the integration and described in the prompt. It is more than three extra tools of surface: finding the right event from a phone call means searching by a name the caller says out loud, and an eventId taken from a search names one occurrence of a recurring event rather than the series. Build it as a second agent with its own prompt before you bolt it onto this one.
There is a window in which two callers can be given the same slot. suggest_time reads free/busy at the instant it runs; create_event writes some turns later. Nothing holds the slot in between, and Google will accept both bookings without complaint. On a single line the window is the length of one confirmation exchange, which is small but not zero; on several concurrent calls it is real. Narrow it by offering the soonest times, keeping the gap between offering and booking to one turn — which is what “read it back, wait, then book” above does — and by checking the diary at the end of each day. If double-booking is unacceptable, the booking has to go through a system of yours that can hold a slot, called from an http tool, with the calendar written afterwards.
Two smaller limits. Free/busy covers at most 50 addresses per call, and any calendar Google will not share — usually anyone outside the account’s organisation — comes back as unreadable rather than free. And the agent books on the connected account’s own calendar: if the practice adds a second dentist with a separate diary, the routing between them is a prompt-and-calendarId problem you have to design, not something suggest_time does for you.
Next
Google Calendar
All nine native tools, argument by argument.
MCP servers
Approval, namespacing, and what happens when a server is slow.
Call analysis
Tuning the extraction against a call that already happened.
Phone numbers
Put the finished agent on a number.