resy-mcp
MCP server for Resy — natural-language restaurant reservation management. Uses Resy's private web-app API with email + password auth.
- npm: npmjs.com/package/resy-mcp
- Source: github.com/chrischall/resy-mcp
⚠️ Resy does not publish an official API. This server uses the same private endpoints the Resy web app calls, with the public web-app
api_keyand user-level auth via email + password. Use at your own discretion.
Setup
Option A — npx (recommended)
Add to .mcp.json in your project or ~/.claude/mcp.json:
{
"mcpServers": {
"resy": {
"command": "npx",
"args": ["-y", "resy-mcp"],
"env": {
"RESY_EMAIL": "you@example.com",
"RESY_PASSWORD": "yourpassword"
}
}
}
}
Option B — from source
git clone https://github.com/chrischall/resy-mcp
cd resy-mcp
npm install && npm run build
Then add to .mcp.json:
{
"mcpServers": {
"resy": {
"command": "node",
"args": ["/path/to/resy-mcp/dist/bundle.js"],
"env": {
"RESY_EMAIL": "you@example.com",
"RESY_PASSWORD": "yourpassword"
}
}
}
}
Or place .env in the project directory with RESY_EMAIL= and RESY_PASSWORD=.
Authentication
RESY_EMAIL+RESY_PASSWORDare required. The client logs in lazily on first request viaPOST /3/auth/password, caches the returned token for the process lifetime, and re-logs automatically on 401 / 419 / auth-like 500 responses.RESY_API_KEYis optional. If unset, the client uses the public web-app key baked into resy.com's JS bundle. Override only if Resy rotates it.
Tools
User
| Tool | Description |
|---|---|
resy_get_profile(view?) |
Current user profile — name, email, phone, booking count, member-since. Payment method IDs are stripped. |
resy_list_payment_methods(view?) |
List saved payment methods (id, brand, last_four, exp_month, exp_year, is_default). The id feeds resy_book's payment_method_id. |
Venues
| Tool | Description |
|---|---|
resy_search_venues(date, party_size, query?, lat?, lng?, limit?, radius_meters?, view?) |
Search venues with availability for a date + party size. Defaults to NYC geo. |
resy_find_slots(venue_id, date, party_size, lat?, lng?) |
List bookable slots at a venue — each includes a short-lived config_token. |
resy_get_venue(venue_id) |
Full venue details. |
Reservations
| Tool | Description |
|---|---|
resy_book(venue_id, date, party_size, desired_time?, slot_type?, terms_token?, allow_closest_time?, lat?, lng?, payment_method_id?, allow_duplicate?, confirmToken?) |
Composite: find fresh slot → details → book. It books ONLY the exact previewed slot. desired_time is "HH:MM" (24h); an unavailable time returns the available times unless allow_closest_time: true previews the nearest. slot_type picks a seating (Dining Room / Bar / Patio) when several share a time. On a client without prompts, the preview comes back as confirmation-required with a confirmToken bound to that slot's time, seating type and terms; after the user approves, repeat the call with the preview's time as desired_time and the confirmToken (see MCP_CONFIRM_MODE). On a client with prompts (unless the server sets MCP_CONFIRM_ELICITATION=off, which sends every client down the confirmToken path), call again with desired_time + slot_type + terms_token (a fingerprint of the seating type and cancellation/payment terms) from the preview and the user is prompted. If the slot, its type or its terms changed, nothing is booked and a fresh preview is returned. The booking refuses if you already hold a reservation at that venue that date (e.g. an earlier timed-out call that went through) unless allow_duplicate: true — checked before you are asked to confirm, so an approval is never wasted; with allow_duplicate: true the preview lists the existing reservation(s). Uses default payment method unless payment_method_id is supplied; either way the preview names the card (brand and last-4), and an id that is not one of your saved cards is refused. |
resy_list_reservations(scope?) |
List reservations. scope: upcoming (default), past, or all. Each result includes the resy_token needed for cancellation. |
resy_cancel(resy_token, confirmToken?) |
Cancel by resy_token (rr://…). Asks the user to confirm first (the Resy account, venue, date, time, party size, any fee): a prompt where the client supports one (unless MCP_CONFIRM_ELICITATION=off), otherwise a preview plus confirmToken that a repeat call passes back. Returns cancelled (always a boolean — true only when confirmed) and outcome (cancelled / not_cancelled / unknown), set from an explicit success/failure in Resy's response, and otherwise by checking whether the reservation is still listed. On outcome: "unknown" the reservation may still have been cancelled — check resy_list_reservations before retrying. |
Favorites
| Tool | Description |
|---|---|
resy_list_favorites(view?) |
List favorited venues ("hit list"). |
resy_add_favorite(venue_id) |
Add a venue to favorites. |
resy_remove_favorite(venue_id) |
Remove from favorites. |
Priority Notify
| Tool | Description |
|---|---|
resy_list_notify(view?) |
List Priority Notify subscriptions. |
resy_add_notify(venue_id, date, party_size, time_start?, time_end?) |
Subscribe to notifications when slots open. time_start / time_end are HH:MM (24h); default window 18:00–21:00. |
resy_remove_notify(notify_id) |
Cancel a Priority Notify subscription. |
Response shape (view)
Five read tools take view: "compact" | "full" — resy_get_profile,
resy_list_payment_methods, resy_list_favorites, resy_list_notify and
resy_search_venues — and compact is the default. The slim rung arrives
without being asked for, because an efficiency a caller has to know about and
request is one that usually is not requested.
Compact strips image and avatar URLs here, and nothing else. There is no field projection: this server holds no captured fixture or documented field list for Resy's payloads, and an invented one would return a record with holes in it that still reads like a verified answer. Media stripping is subtractive, so it cannot lose a field nobody knew about.
The surprise worth stating plainly: on four of those five tools, compact and
full are byte-identical today. resy_get_profile is the ONE response in this
whole server that carries a media URL — profile_image_url off /2/user — so
it is the one place compact removes anything at all. The other four already
hand back records this server assembled field by field, with no picture in
them. The parameter is on them so that a field arriving later with an avatar
inside it is stripped by default rather than after somebody notices.
profile_image_url is named in the drop list rather than left to the built-in
rules, and that is what makes its removal deterministic. The key pattern
anchors its noun at the START of a key (it knows profile_pic, not
profile_image_url), which leaves only the value rule — and that fires just
when a URL path ends in an image extension, which Resy's avatar pipeline need
not do.
view: "full" returns the record untouched. There is no raw rung: full
already IS the un-stripped response, so a third value could only alias it.
The other ten tools take no view:
resy_find_slots,resy_get_venueandresy_list_reservationsbuild their answers field by field (formatSlot,formatVenue,formatReservation). No upstream payload survives into the response, and no media URL with it — there is nothing for the rung to remove.resy_book,resy_cancel,resy_add_favorite,resy_remove_favorite,resy_add_notify,resy_remove_notifyare writes. A write's response is a receipt — aresy_token, a confirmation, a status — with nothing to strip and everything to keep.resy_healthcheckanswers reachability and auth.
Workflows
Book a specific restaurant at a specific time:
resy_search_venues(query: "carbone", date: "2026-05-01", party_size: 2)
→ find venue_id
resy_book(venue_id, date: "2026-05-01", party_size: 2, desired_time: "19:00")
→ preview: time, slot_type, terms_token, fees, payment card; show it to the user
client WITHOUT prompts (claude.ai, Claude Desktop), or any client when the
server sets MCP_CONFIRM_ELICITATION=off: that preview is
`confirmation-required` and carries a confirmToken. Once the user approves in chat:
resy_book(venue_id, date: "2026-05-01", party_size: 2,
desired_time: "<preview's time>", confirmToken: "<token>")
client WITH prompts (Claude Code), unless MCP_CONFIRM_ELICITATION=off: name the
slot and the user is prompted:
resy_book(venue_id, date: "2026-05-01", party_size: 2, desired_time: "<preview's time>",
slot_type: "<preview's slot_type>", terms_token: "<preview's terms_token>")
See what's available tonight near me:
resy_search_venues(date: "2026-04-20", party_size: 2, lat: 37.7749, lng: -122.4194)
→ returns venues with baked-in slot availability
Cancel a reservation:
resy_list_reservations() → find resy_token for the one to cancel
resy_cancel(resy_token)
→ a confirmation prompt (unless MCP_CONFIRM_ELICITATION=off), or a preview plus
confirmToken to pass back once the user approves
Stalking a hard-to-get table:
resy_search_venues(query: "4 charles prime rib", ...) → venue_id
resy_add_notify(venue_id, date: "2026-05-31", party_size: 2, time_start: "19:00", time_end: "21:00")
# Resy emails you when a slot opens
Notes
- Slot
config_tokens expire within minutes of being fetched.resy_bookre-fetches fresh slots internally — don't try to thread a stale token fromresy_find_slotsinto a book call manually. resy_bookrequires a payment method on file at resy.com/account. If none exists it throws a clear error.- Default geo is NYC (40.7128, -73.9876). Pass
lat/lngfor other cities. - Favorites and Priority Notify endpoint paths are reverse-engineered. If a call fails with 404, run
npm run smokelocally against your credentials and adjust the path. RESY_API_KEYenv var overrides the baked-in public web-app key if Resy ever rotates it.
