Quickstart
Connect to the WhatsDo MCP server and make your first call.
Server address
WhatsDo is a hosted MCP server. Clients that speak MCP over Streamable HTTP can use it. The server is stateless: each request stands on its own.
https://app.whatsdo.com/mcp/Keep the trailing slash. /mcp without it answers with a redirect, and not every client follows a redirect on POST.
Sign in
- Claude, Claude Code and ChatGPT: add the server address. The client opens a WhatsDo sign-in. No key to copy.
- Cursor, Gemini CLI and Codex: sign-in or an API key. See Connect your agent.
- Scripts, servers and VS Code: get an API key, copy it once and send it as
Authorization: Bearer <key>. Tickbookings:writeif your agent books. Or use the setup page: it creates a key that can book.
Bookings are made as the signed-in WhatsDo account. Name and phone come from that account’s profile.
First call
A booking request takes four calls, in this order.
searchwith the person’s words, a San Francisco location,bookable_only: trueand alimitof 3 to 5. Keep theidandbooking_sourceof the place they pick.get_booking_requirementswith thatid. Ask the person only for fields that are missing.bookafter the person said yes to the place, date, time and party size. Booking is in Beta.my_bookingsto read the status. Nothing is pushed to you.
Try the first call with cURL:
curl https://app.whatsdo.com/mcp/ \
-H "Authorization: Bearer $WHATSDO_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"search",
"arguments":{"query":"asian dinner highly rated",
"location":"San Francisco, CA",
"bookable_only":true,"limit":3}}}'The reply can come as plain JSON or as an event stream. Read result.structuredContent when it is there, otherwise parse the JSON text in result.content[0].text.
Full example in Python
One file that runs the whole sequence: initialize, search, then, if you pass a time, get_booking_requirements, book and polling my_bookings until the status settles. It handles both reply formats and prints only safe booking fields. Set WHATSDO_API_KEY first.
# Python 3.11+, pip install httpx
# Search: python whatsdo_flow.py
# Search and book: python whatsdo_flow.py 2026-10-09T19:00:00-07:00 (writes a real booking)
import json, os, sys, time
import httpx
MCP_URL = "https://app.whatsdo.com/mcp/"
HEADERS = {"Authorization": f"Bearer {os.environ['WHATSDO_API_KEY']}",
"Content-Type": "application/json",
"Accept": "application/json, text/event-stream"}
SAFE = ("place_name", "status", "date_time", "party_size", "confirmation_number", "message")
http = httpx.Client(timeout=120.0)
next_id = 0
def rpc(method, params=None):
global next_id
next_id += 1
resp = http.post(MCP_URL, headers=HEADERS,
json={"jsonrpc": "2.0", "id": next_id, "method": method, "params": params or {}})
resp.raise_for_status()
if resp.headers.get("content-type", "").startswith("text/event-stream"):
# The reply is the data: event whose id matches the request.
events = [json.loads(l[5:]) for l in resp.text.splitlines() if l.startswith("data:")]
msg = next((e for e in events if e.get("id") == next_id), events[-1])
else:
msg = resp.json()
if "error" in msg:
raise RuntimeError(msg["error"])
return msg["result"]
def tool(name, arguments):
result = rpc("tools/call", {"name": name, "arguments": arguments})
texts = [c.get("text", "") for c in result.get("content", []) if c.get("type") == "text"]
if result.get("isError"):
raise RuntimeError(" ".join(texts))
if isinstance(result.get("structuredContent"), dict):
return result["structuredContent"]
return json.loads(texts[0])
init = rpc("initialize", {"protocolVersion": "2025-06-18", "capabilities": {},
"clientInfo": {"name": "my-agent", "version": "0.1"}})
HEADERS["MCP-Protocol-Version"] = init["protocolVersion"]
http.post(MCP_URL, headers=HEADERS, json={"jsonrpc": "2.0", "method": "notifications/initialized"})
found = tool("search", {"query": "asian dinner highly rated", "location": "San Francisco, CA",
"bookable_only": True, "limit": 3})
results = found.get("results", [])
if not results:
sys.exit("No matches. WhatsDo search covers San Francisco; new cities are available on request.")
for r in results:
print(r["name"], r.get("rating"), r.get("price"), r["id"])
if len(sys.argv) < 2:
sys.exit(0)
# Only after the person agreed to the place, date, time and party size.
place = results[0]
needs = tool("get_booking_requirements", {"item_id": place["id"]})
if not needs.get("available"):
sys.exit(f"{place['name']} can't be booked here. Phone: {place.get('phone')}")
booking = tool("book", {"item_name": place["name"], "item_id": place["id"], "date_time": sys.argv[1],
"party_size": 2, "source": place["booking_source"]})
rid, status = booking["reservation_id"], booking["status"]
deadline = time.monotonic() + 300
while status == "pending" and time.monotonic() < deadline:
time.sleep(30)
rows = tool("my_bookings", {"limit": 10}).get("bookings", [])
row = next((b for b in rows if b.get("reservation_id") == rid), None)
if row:
booking, status = row, row["status"]
# Never print guest email, phone, user id or session links.
print({k: booking.get(k) for k in SAFE})book writes a real booking as the key’s account. Run the booking path only for a place, date and time the person agreed to.