MCP server for OpenTable — natural-language restaurant reservation management. Every request is relayed through the user's signed-in browser tab via the fetchproxy extension, so there's no cookie paste, no bot-wall dance, and no password handling.
> OpenTable does not publish an official API. This server calls the same endpoints the opentable.com web app uses. Auth lives in the user's browser (OpenTable's passwordless email-OTP session). Use at your own discretion.
The MCP server is half of the picture — the other half is the fetchproxy browser extension that actually talks to OpenTable from your signed-in tab. Both are required.
Add to .mcp.json in your project or ~/.claude/mcp.json:
{
"mcpServers": {
"opentable": {
"command": "npx",
"args": ["-y", "opentable-mcp"]
}
}
}
git clone https://github.com/chrischall/opentable-mcp
cd opentable-mcp
npm install && npm run build
Then add to .mcp.json:
{
"mcpServers": {
"opentable": {
"command": "node",
"args": ["/path/to/opentable-mcp/dist/bundle.js"]
}
}
}
opentable-mcp shares a single browser extension with every other fetchproxy-based MCP. Install it once from github.com/chrischall/fetchproxy:
.dmg).https://www.opentable.com/ in the same browser profile.The extension's toolbar badge turns green when the WebSocket + tab + auth are all good.
Full extension walkthrough: see fetchproxy.
No env vars. Auth is whatever cookies your signed-in opentable.com tab has. If Akamai rotates _abck or OpenTable's SSO expires, visit opentable.com and click through whatever prompt appears — subsequent MCP calls will use the fresh cookies automatically.
The server throws SessionNotAuthenticatedError (with a clear message) if it detects the sign-in interstitial.
| Tool | Description |
|---|---|
| ------ | ------------- |
opentable_search_restaurants(term?, location?, date?, time?, party_size?, latitude?, longitude?, metro_id?) | Search by free-text + optional location / date / party size. Returns cuisine, neighborhood, price band, rating, URL slug. No bookable slots — call find_slots for those. |
opentable_get_restaurant(restaurant_id) | Full detail for one restaurant by URL slug (e.g. "state-of-confusion-charlotte"). Includes diningAreas[] — you need one of their ids to book. Numeric ids 404 here; always pass the slug. |
opentable_find_slots(restaurant_id, date, time, party_size) | List bookable slots for a restaurant on a date + party size. Each slot has a short-lived reservation_token + slot_hash (book within a minute or two of fetching). |
| Tool | Description |
|---|---|
| ------ | ------------- |
opentable_list_reservations(scope?) | List reservations. scope: upcoming (default), past, all. Each entry has the confirmation_number + security_token needed to cancel. |
opentable_get_profile | Authenticated user's profile: name, email, phones, loyalty points, home metro, member-since. No payment data. |
| Tool | Description |
|---|---|
| ------ | ------------- |
opentable_book_preview(restaurant_id, date, time, party_size, reservation_token, slot_hash, dining_area_id) | Preview a booking: runs slot-lock + fetches /booking/details, returns the cancellation policy, the saved card that would be held/charged, and a short-lived booking_token. REQUIRED before opentable_book for CC-required slots; safe to call for others. Holds the slot for ~60-90s. |
opentable_book(restaurant_id, date, time, party_size, reservation_token, slot_hash, dining_area_id, booking_token?) | Book a slot. For CC-required slots, pass the booking_token from opentable_book_preview — the tool errors out pointing you at preview if you don't. For non-guaranteed slots, booking_token is optional (but harmless). Returns confirmation_number + security_token (save both — they're required for cancel). |
opentable_cancel(restaurant_id, confirmation_number, security_token) | Cancel by the triple returned from book or list_reservations. |
| Tool | Description |
|---|---|
| ------ | ------------- |
opentable_list_favorites | List saved restaurants. |
opentable_add_favorite(restaurant_id) | Add a restaurant (numeric id) to Saved Restaurants. |
opentable_remove_favorite(restaurant_id) | Remove from Saved Restaurants. |
OpenTable restaurants fall into three categories. Check
opentable_get_restaurant.bookable and the per-slot booking_type
before invoking opentable_book.
bookable | booking_type | What it means | Action |
|---|---|---|---|
| --- | --- | --- | --- |
| true | instant | Standard restaurant, one-click book. | Today's path: optional preview, then opentable_book. |
| true | experience_mandatory | Restaurant requires picking an Experience (prix-fixe, tasting menu, etc.) before booking. Slot carries one or more experience_ids. | Call opentable_get_restaurant to see the per-area bookableExperiences. Call opentable_book_preview with both dining_area_id AND experience_id. Then opentable_book with the returned booking_token. |
| false | n/a | Listing-only: OpenTable shows the page but reservations go through the restaurant directly. | Surface the restaurant's phone and url to the user; do NOT call opentable_book. |
| true | request | (Reserved) Request-to-book. Not surfaced in v1. | n/a |
When booking_type === "experience_mandatory", do not treat the
return as a confirmed reservation — it's still instant-confirm, but
the booking_token's experience block tells the agent which
Experience the user committed to (community-table dining, chef's
counter, etc.). Mention the Experience name in the user-facing
confirmation.
To change date/time/party_size/dining_area/experience on an existing reservation, use opentable_modify_preview + opentable_modify. Mirrors book's preview→commit pattern.
opentable_find_slots for the NEW time you want.opentable_modify_preview with:restaurant_id, confirmation_number, security_token (from opentable_list_reservations or the original opentable_book result).date, time, party_size, reservation_token, slot_hash, dining_area_id, and (for Experience-mandatory restaurants) experience_id.cancellation_policy and any CC re-hold details to the user.opentable_modify with the modify_token from preview + the same identifying args. Returns was_modified: true and the preserved confirmation_number.Don't use cancel + book to "modify" — same-day cancel-then-rebook trips OpenTable's double-booking check and risks losing the slot to another diner.
Book a specific restaurant for a specific evening (no-CC slot):
opentable_search_restaurants(term: "state of confusion", location: "Charlotte")
→ note the restaurant_id (numeric) + URL slug
opentable_get_restaurant(restaurant_id: "state-of-confusion-charlotte")
→ pick dining_area_id from diningAreas[]
opentable_find_slots(restaurant_id, date: "2026-05-01", time: "19:00", party_size: 2)
→ pick a slot, grab reservation_token + slot_hash
opentable_book(restaurant_id, date, time, party_size, reservation_token, slot_hash, dining_area_id)
→ save confirmation_number + security_token
Book a CC-required slot (prime-time at a busy restaurant):
opentable_find_slots(...) # pick one
opentable_book_preview(restaurant_id, date, time, party_size, reservation_token, slot_hash, dining_area_id)
→ shows cancellation_policy (e.g. "$50/guest no-show fee, cancel 24h ahead for no charge")
→ shows payment_method (e.g. Mastercard •••• 4242)
→ returns booking_token
[Surface the policy + card to the user. Let them confirm in plain English.]
opentable_book(..., booking_token: <from preview>) # commits
→ save confirmation_number + security_token
If opentable_book is called on a CC-required slot without booking_token, it
errors out with "call opentable_book_preview first". That's the gate.
"What's available tonight for 2 at a nice Italian spot?":
opentable_search_restaurants(term: "italian", date: "2026-04-20", time: "19:00", party_size: 2, latitude: <lat>, longitude: <lng>)
→ scan results, pick candidates
opentable_find_slots(restaurant_id, date, time, party_size) # for each candidate
Cancel an upcoming reservation:
opentable_list_reservations(scope: "upcoming")
→ find the one to cancel, read confirmation_number + security_token
opentable_cancel(restaurant_id, confirmation_number, security_token)
Save a restaurant for later:
opentable_search_restaurants(term: "carbone")
→ pick the restaurant_id
opentable_add_favorite(restaurant_id)
opentable_book_preview before opentable_book. The preview surfaces the cancellation policy and the saved card last-4; the booking_token it returns is opaque, stateless, and expires with OpenTable's ~60–90 s slot lock. If the user has no default payment method on opentable.com, preview throws a link to the account settings page. Plain opentable_book (no token) refuses CC-required slots with a pointer to preview.reservation_token + slot_hash from find_slots typically expire within a minute or two. Call find_slots just before book, and if the user is deliberating, re-fetch.dining_area_id is mandatory for book. OpenTable's /r/ routes 404 — there's no way to auto-resolve the default dining room. Always call opentable_get_restaurant(slug) first and pick a room from diningAreas[]./user/favorites has a read-after-write lag. A fresh add_favorite may not show up in list_favorites for ~10 s. The 204 response from add/remove is authoritative.find_slots, book, cancel, and search use Apollo persisted queries pinned by sha256Hash. If OpenTable redeploys and returns PersistedQueryNotFound, re-capture by reading window.__APOLLO_CLIENT__.queryManager.mutationStore['1'].mutation.documentId from DevTools on the relevant opentable.com page (see CLAUDE.md → "Conventions").共 11 个版本