Tools
The tools on the WhatsDo MCP server and what to expect from them.
Each call is a JSON-RPC tools/call to https://app.whatsdo.com/mcp/.
| Tool | What it does | Scope | Changes data |
|---|---|---|---|
search | Ranked places for a request and a location | search | No |
place_details | Full record for one place | search | No |
get_booking_requirements | Fields a booking at this place needs | search | No |
book | Booking request for the signed-in person | bookings:write | Yes |
my_bookings | Recent bookings and their current status | bookings:read | No |
cancel_booking | Cancels or asks to cancel a booking | bookings:write | Yes |
continue_booking | Answers a question the booking system asked | bookings:write | Yes |
set_location | Saves a default search location | profile:write | Profile |
remember_preference | Saves taste preferences for later searches | profile:write | Profile |
calendar_free_slots | Free slots on the person’s Google Calendar | bookings:read | No |
calendar_check_conflict | Whether a time clashes with the calendar | bookings:read | No |
Replies and errors
- The reply carries
result.content[]with a short text. The object described below is inresult.structuredContentwhen present, otherwise it is the JSON text inresult.content[0].text. - A tool error comes back as
isError: true, with the message inresult.content[0].text. - Send
Content-Type: application/jsonandAccept: application/json, text/event-stream. The reply can be plain JSON or an event stream.
search
Finds places in San Francisco: restaurants, cafes, bars and nightlife, gyms, yoga, fitness, spas, salons, massage, parks and things to do.
| Input | Type | Notes |
|---|---|---|
query | string | The person’s words: cuisine, mood, category or venue name. Required. For “things to do”, search two or three concrete categories such as live music or attractions. |
location | string | City plus neighborhood, e.g. "Mission District, San Francisco, CA". Put the neighborhood here, not in query. Leave it out on follow-ups to reuse the last one. |
exclude | string[] | What the person ruled out in this request, e.g. ["bar"]. |
bookable_only | boolean | Only places that can be booked through WhatsDo. Default false. |
open_at | string | ISO 8601 time the place must be open at. |
limit | integer | 1 to 20, default 5. Keep it at 3 to 5: each place is a large object. |
offset | integer | Pass the previous reply’s next_offset for the next page. |
Returns results[], best first, plus location_missing, next_offset while more results may exist, and applied_preferences, the saved preferences that shaped the ranking. Keep the order of results.
Each result includes id, item_type, name, rating, review_count, price (a band from $ to $$$$), tags (the first five), categories (every tag in one string), phone, photos (up to three), hours[] as {day, start, end}, is_closed, is_bookable, bookable_via[], booking_source, tagline, sources[] and google_place_id. distance_km is always null.
Good to know
- WhatsDo search covers San Francisco. For another city, tell the person once that it is available on request and offer to send a city request.
location_missing: truemeans the server had no location. Ask the person where they are and search again.- Results can include places outside the named neighborhood.
- No address, coordinates or distance in the reply.
hoursandpricecan be empty. Treat both as unknown. A00:00to00:00row also means unknown.open_atfilters by listed hours. A place with empty hours can pass the filter.is_bookableis a hint. Confirm withget_booking_requirementsbefore booking.categoriesrepeats every tag. Parse it once and do not show it to the person.
{"name": "search",
"arguments": {"query": "quiet dinner for four",
"location": "Hayes Valley, San Francisco, CA",
"bookable_only": true, "exclude": ["bar"], "limit": 3}}A real reply, trimmed to one place:
{
"results": [
{
"id": "c585fcc9-9714-4ac7-972a-5ecfc0ca68d6",
"item_type": "restaurant",
"name": "Ganji",
"rating": 4.32,
"review_count": 95,
"price": "$$",
"tags": ["restaurant", "casual", "cozy", "warm", "$$"],
"categories": "restaurant, casual, cozy, ..., japanese, sushi, outdoor_seating, wait_time_short",
"phone": "<venue phone>",
"photos": ["https://app.whatsdo.com/media/c585fcc9-.../3a569587-....jpg"],
"hours": [
{"day": "friday", "start": "12:00", "end": "22:00"},
{"day": "saturday", "start": "12:00", "end": "22:00"}
],
"is_closed": false,
"is_bookable": true,
"booking_source": "<pass to book unchanged>",
"distance_km": null,
"tagline": "Close",
"sources": ["review_site", "catalog"]
}
],
"location_missing": false,
"next_offset": 10,
"applied_preferences": ["italian", "coffee"]
}place_details
Full record for one place: most search fields plus website, location_services and found. Input: item_id, the id from a search result. Use it for hours, website and contact.
tags, bookable_via and booking_source come only from search. Keep them from the search result.
- Returns
found: falsewhen the id is unknown. - No address, coordinates or description in the reply.
{"name": "place_details", "arguments": {"item_id": "c585fcc9-9714-4ac7-972a-5ecfc0ca68d6"}}get_booking_requirements
The fields a booking at this place needs, so the person is asked once. Input: item_id. Returns available, item_id, provider, booking_kind, required[], optional[] and policies. Each field has kind, required, label, description, options and depends_on.
booking_kind: "restaurant_reservation"needsparty_size,date_time,customer_nameandcustomer_phone.booking_kind: "appointment"(salons, spas, massage, yoga) has noparty_size. The service goes inspecial_requestsas free text, labelled “Service or notes”.customer_nameandcustomer_phonecome from the profile. Do not ask the person for them.available: falsecomes withreasonand no field lists. Readreason: either the place cannot be booked through WhatsDo (offer its phone number) or the rules could not be read right now (try again later).
A real reply for a restaurant:
{
"available": true,
"item_id": "c585fcc9-9714-4ac7-972a-5ecfc0ca68d6",
"booking_kind": "restaurant_reservation",
"required": [
{"kind": "party_size", "required": true, "label": "Party size", "description": "How many people"},
{"kind": "date_time", "required": true, "label": "Date and time", "description": "ISO 8601 with timezone"},
{"kind": "customer_name", "required": true, "label": "Name", "description": "Booking name"},
{"kind": "customer_phone", "required": true, "label": "Phone", "description": "E.164 phone"}
],
"optional": [
{"kind": "special_requests", "required": false, "label": "Special requests", "description": "Optional notes"}
],
"policies": {}
}book
Sends a real booking request at once, as the signed-in person. There is no draft and no second confirmation step. Call it only for a place with is_bookable: true, after the person agreed to the place, date, time and party size.
| Input | Type | Notes |
|---|---|---|
item_name | string | Place name. Required. |
date_time | string | ISO 8601 with an offset, e.g. 2026-10-09T19:00:00-07:00. Required. San Francisco is -07:00 in summer time and -08:00 in winter time. |
source | string | The search result’s booking_source, unchanged. Required. |
item_id | string | The search result’s id. Always send it. |
party_size | integer | Guests. Default 1. |
special_requests | string | Free text for the venue. For salons, spas and massage this carries the service, e.g. “60-minute deep tissue massage”. |
duration_minutes | integer | Appointment length, when the venue asks for one. |
phone | string | The person’s own phone, only after a booking failed asking for one and the profile has none. |
Returns reservation_id, status, place_name, date_time, party_size, confirmation_number, alternatives[] and calendar_link, plus message when the booking system sent one. What each status means: Booking statuses.
{"name": "book",
"arguments": {"item_name": "Ganji", "item_id": "c585fcc9-9714-4ac7-972a-5ecfc0ca68d6",
"date_time": "2026-10-09T19:00:00-07:00", "party_size": 2,
"source": "<booking_source from search>"}}A real reply from a place that books instantly:
{
"reservation_id": "00000000-0000-4000-8000-000000000001",
"status": "confirmed",
"place_name": "Ganji",
"date_time": "2026-10-09T19:00:00-07:00",
"party_size": 2,
"confirmation_number": null,
"alternatives": [],
"calendar_link": null
}A real reply from a place that books through its own page. pending is not booked yet: poll my_bookings.
{
"reservation_id": "00000000-0000-4000-8000-000000000002",
"status": "pending",
"place_name": "The Massage Garage",
"date_time": "2026-10-07T12:00:00-07:00",
"party_size": 1,
"confirmation_number": null,
"alternatives": []
}my_bookings
Recent bookings, newest first, with their current status. Input: limit, default 10. Each row has reservation_id, place_name, item_id, date_time, party_size, source, status, confirmation_number, special_requests, message, created_at and updated_at.
- Nothing is pushed. Call
my_bookingsagain to see a status change. sourcecan change after booking.manualmeans a WhatsDo team member is finishing it.- Rows also carry the guest’s email, phone, user id and session links. Do not show them to the person or write them to logs.
A real row, personal fields removed. The booking failed and message names the time the venue offered instead:
{
"bookings": [
{
"reservation_id": "00000000-0000-4000-8000-000000000003",
"place_name": "Penny Roma",
"status": "failed",
"date_time": "2026-10-09T19:00:00-07:00",
"party_size": 4,
"confirmation_number": null,
"message": "... showed no 7:00 PM table for 4 ... The only offered time I saw was 9:30 PM in the Dining Room."
}
]
}cancel_booking
Cancels one of the person’s bookings. Input: reservation_id. The only tool that removes something: confirm with the person first. Returns cancelled, status, message and venue_phone.
status: "cancelled": the booking is cancelled on WhatsDo.status: "cancellation_requested": a WhatsDo team member cancels it by hand. The booking staysconfirmedinmy_bookingsuntil then.- A
pendingbooking made through the venue’s page is cancelled on WhatsDo only; nothing is sent to the venue. If the venue already emailed a confirmation, the person should use that email or call. See Booking statuses.
A real reply for a confirmed instant booking:
{
"cancelled": false,
"status": "cancellation_requested",
"message": "This booking was made on the venue's own site and can't be cancelled automatically. Our team has been asked to cancel it.",
"venue_phone": null
}continue_booking
Answers a follow-up question from the booking system so a booking can finish. Use it after book returned requires_payment or another reply that asks something. Used by places that book instantly.
| Input | Type | Notes |
|---|---|---|
booking_id | string | From the previous book or continue_booking reply. Required. |
message | string | The person’s answer: a time, a party size, a seating preference. Required. |
latitude, longitude | number | Only when the venue asks where the guest is coming from. |
- Returns the booking system’s reply in
response, withbooking_idagain while it still needs more. - Each call sends another message. Do not retry blindly.
- Carries no payment details. A venue that wants a card collects it on its own page.
set_location
Saves a default search location on the profile. Input: location, such as "San Francisco, CA". Returns saved: true. Call it only when the person asks to save or remember a default; search already reuses the last location within a conversation.
remember_preference
Saves what the person likes so later searches lean toward it. Inputs, all optional: dietary, cuisines, vibes and interests (string arrays) and price_range (e.g. "$$"). Returns saved, remembered and now.
- Every value is stored as a like. Pass dislikes to
searchasexcludeinstead. - Not for one-off constraints such as “tonight” or “for four”.
{"name": "remember_preference", "arguments": {"cuisines": ["japanese"], "dietary": ["vegetarian"]}}calendar_free_slots
Open slots on the person’s Google Calendar for one day. Input: date as YYYY-MM-DD. Returns free_slots, a list of start times such as "19:00". When free_slots has no times, the day is fully booked.
calendar_check_conflict
Whether a proposed time clashes with the person’s Google Calendar. Inputs: date_time (ISO 8601) and duration_mins, default 90. Returns has_conflict and busy_intervals[] of {start, end}.
Both calendar tools need a connected Google Calendar. Without one the call fails with this text. Skip the calendar step and book without it:
Error executing tool calendar_check_conflict: No Google Calendar connected for this user. They can connect one in the whatsdo app, or you can book without checking their calendar.Place record: facts and inferences
What your agent may say about a place, and how. Fields come from search and place_details.
| What | Field | Notes |
|---|---|---|
| Id | id | The item_id for every other tool. |
| Name and type | name, item_type | item_type values include restaurant (also cafes), gym, massage, beauty_spa, active_life (parks). |
| Cuisine and services | tags, categories | Salon and spa services are not a structured field. |
| Hours | hours[] | Empty or 00:00 to 00:00 means unknown. |
| Price | price | A band from $ to $$$$, often empty. Not a per-person amount. |
| Rating | rating, review_count, sources[] | Sources are listed per place, not per field. |
| Booking | is_bookable, bookable_via[], booking_source | From search only. booking_source is what book takes as source. |
| Contact | phone, website (details only) | The fallback when a place is not bookable. |
| Address, coordinates, distance | Not returned | Withheld from the agent. Chat widgets show the map. |
| Description | Not returned | Use tags and categories. |
| Freshness | Not returned | No timestamp per tag or per place. |
Tags from reviews
- Quality, each with
_good,_mixedor_bad:staff_,service_,food_quality_,value_,atmosphere_,cleanliness_. - Wait and noise:
wait_time_short,wait_time_mixed,wait_time_long,noise_level_quiet. - Diet:
vegetarian_options,vegan_options,gluten_free_options,kid_menu. - Who it suits:
kids,families,date_night. - Amenities and policy:
outdoor_seating,free_wifi,wifi,coffee_and_work,working_remotely,pet_friendly,parking,wheelchair_accessible,reservations_required. - Tags are evidence, not facts. When two tags on one axis disagree, say “mixed”.
State as fact, naming WhatsDo as the source
name,item_type,rating,review_count, thepriceband,phone,is_closed,is_bookable.hourswhen present and not the placeholder.- That a place has a tag: “tagged outdoor_seating”.
Label as inferred
- Atmosphere, vibe and who it suits, from tags such as
cozyordate_night. - Any per-person price. “About $30 a head” is an estimate from the
$$band. - Fit for a stated need, e.g. “fine for a laptop afternoon” from
coffee_and_workandfree_wifi. - Why a place matches. The reasons are your agent’s own.
Do not state
- Address, distance or travel time.
- Menu items or dish prices.
- That a table is free before
bookreturns. - Anything a tool did not return.