@curless/hotel-storefront
A hotel-booking MCP connector, driven by one config file. Brand, tool names,
vocabulary and where the rooms come from live in a HotelConfig, so the second
hotel group is a JSON file rather than a fork.
npx @curless/hotel-storefront ./config.json # the config lives in the brand's own repo
It answers on POST /mcp (streamable HTTP), serves its own card as a ui://
resource, and publishes /docs, /privacy, /terms and /healthz.
Dates are a first-class type
The one structural difference from a shopping connector: a room has no price
until someone names the nights. Everything that can carry a price takes a
Stay, and the parts that cannot — a hotel's summary, its amenities — are
deliberately separate from the parts that can. A connector that guesses "this
weekend" books someone the wrong nights, and they find out at the front desk.
The tools
Twenty-one, named from the brand's prefix, two of them the card's own and kept
from the model. A backend that cannot do something says so in its
capabilities, and those tools are not listed at all - Huazhu's deployment has
fourteen (no loyalty, add-ons, in-place changes or policy text).
list_<brand>_hotels | The group's destinations, or hotels by city or free text - with filters, a sort and a nightly ceiling, and with checkIn/checkOut each priced for those nights. A city usually holds several, and all of them come back |
look_up_<brand>_hotels | Up to six hotels read as text and drawn nowhere: what a comparison or a plan is made from |
get_<brand>_hotel | One hotel; with dates, its rooms priced for those nights and the arrival days either side |
compare_<brand>_hotels | Two to four side by side - the model's judgement, the server's figures (it takes no prices) |
show_<brand>_itinerary | Day by day, each item marked as in the trip or only planned |
list_<brand>_extras | Transfers, dining, experiences, spa, family - for a stay, or for a booking already made |
add_to_<brand>_trip · view_ · clear_ | The trip: room nights plus add-ons, priced as one |
request_<brand>_booking | A demonstration hold (demo) or a booking paid at the hotel (a group's own API); a confirmation code per stay |
pick_<brand>_room · book_picked_<brand>_room | The card's own, app-only: Reserve on one rate, and booking that one room. The pick has a slot beside the trip - a second pick replaces the first, and neither touches the trip |
list_my_<brand>_bookings | This conversation's bookings, or one found by its code and the email it was made with |
preview_<brand>_cancellation · cancel_<brand>_booking | The fee and its rule, before anything is cancelled |
preview_<brand>_booking_change · change_<brand>_booking | New nights, rooms or add-ons: the difference first, then the change |
get_<brand>_hotel_policies | What the property itself publishes |
sign_in_<brand>_member · sign_out_ | A membership number and the surname on it - a member rate, never a way into anybody's booking |
forget_<brand>_preferences | Removes the room preferences a member asked to have remembered |
Tool descriptions say what a tool does and nothing else; how to behave lives in
the server's instructions, which is what both assistant directories' listing
attestations require.
Three rules the code enforces
- No dates, no line.
add_to_*_tripasks rather than guessing. - Preview, then cancel.
cancel_*_bookingreturns the fee and its rule and cancels nothing until it is called again withconfirmed. - Held is not paid, and booked is not paid. A booking takes no money and collects no card; the answer says so, and the card says so in as many words.
Where the rooms come from
Everything goes through one HotelBackend interface, and there are two:
data.source: "demo"reads a JSON file and prices it (DemoHotels). Every price-bearing answer carriesdemo: trueand says so in words — an invented rate presented as bookable is the one failure a booking connector cannot have.data.source: "api",data.api.provider: "huazhu"books for real through Huazhu's distributor OpenAPI: the hotel list kept on disk and current, prices, rules and rooms left asked for live,prebookbefore every booking, one reservation per stay paid at the hotel, and a booking whose answer was lost followed up by the consistency query for the eight minutes Huazhu's rules require. Credentials come from the environment (HUAZHU_CLIENT_ID,HUAZHU_CLIENT_SECRET,HUAZHU_DISTRIBUTOR_ID), never the config.data.api.sandbox: trueis the same connector against a SIMULATED Huazhu, for testing before there is an account:node dist/huazhu/sandbox.jsserves the distributor API as its documentation describes it, with nine invented hotels named 【模拟】/[Sandbox] and no photographs, and keeps its bookings inSANDBOX_STATE_FILE. The flag makes every answer saydemo, the instructions and the legal pages say it is a test environment, and nothing claim a booking went to Huazhu - without it an api source is taken to be real.
A backend says what it cannot do through capabilities — Huazhu has no loyalty
sign-in, no add-ons and no in-place date change — and the tools, the server
instructions, the card and the legal pages leave those out rather than offering
something that always says no.
# The first call against a real environment, read-only - it never books:
HUAZHU_CLIENT_ID=… HUAZHU_CLIENT_SECRET=… HUAZHU_DISTRIBUTOR_ID=… \
npx @curless/hotel-storefront --check ./config.json
Payment
Not in this version, by design: the arc ends at a booking request. The seam is
config.payment.mode.
MIT · part of agentbank