Koskamo

Bluerails Hotels MCP

An official MCP server by Bluerails that enables AI agents to search for hotel properties, retrieve live availability and pricing in specific currencies, and book rooms directly without leaving the conversation. It supports filtering by location, dates, party size, and budget across DACH, UK, and US markets.

Endpoint and access

No sign-in to connect and list tools. Individual tools may still need a vendor account, credentials, or permissions; check the documented prerequisites.

Bluerails Hotels MCP logo, by BluerailsVendor evidenceStreamable HTTPNo sign-inEndpoint checked 2026-11-07Operated by Bluerails
MCP endpoint (Streamable HTTP)
https://402.bluerails.com/mcp
Vendor documentation

What its tools do

2 tools, grouped by the hints the server advertises. Hints are self-declared, not verified behavior.

Can change data (2)

2 of 2 have a description · 2 of 2 declare behavior hints · 1 of 2 with parameters document them all

Checks and signals

Each line is separate evidence, not a trust score.

  • Passed Endpoint answered the live check ·
  • Passed Official MCP Registry namespace is tied to bluerails.com ·
  • Passed Endpoint is on the registry namespace domain ·
  • Availability 1 of 1 checks passed in 30 days

What you can do with it

  • Search for hotels by location, dates, and budget
  • Retrieve live room availability and pricing
  • Book a hotel room directly via agent conversation

Source and maintenance

Vendor evidenceRegistry 1.0.0

Protocol and capabilities

Server
Bluerails 1.0.0
MCP protocol negotiated in check
2025-11-25
Tools
2 advertised
Resources
1 advertised
Resource templates
1 advertised
Prompts
Not advertised
Server-advertised capabilities
{
  "tools": {
    "listChanged": true
  },
  "logging": {},
  "resources": {
    "listChanged": true
  }
}

Tools

2 advertised

list_hotels — Find and price hotels
Advertised by the server during the last successful anonymous check.
Open-world

Search hotel properties in Germany, Austria, Switzerland, the United Kingdom and the United States, and price them for a stay in the same call. ASK BEFORE YOU SEARCH. Four things are required and this tool answers nothing without them: `checkIn`, `checkOut`, `adults` and `maxPricePerNight`. "Hotels in Berlin for a couple" is not yet a search — it names a place and a party and nothing else, so ask which nights they want, who is coming — adults AND any children, by age — and roughly what they want to spend, in ONE message, and search once they answer. Never fill these in for them: dates you chose are not their dates, and a budget read off "cheap" or off a star rating hides the very hotels they could have afforded. A call missing any of them comes back with no rows and a `hint` naming what is still to be asked. PLACES. Pass `city` with any place name we cover to get properties around it, nearest first, each with `distanceKm` — despite the name it takes a place of any size, so a village, a resort or a borough ("Silvaplana", "Wals", "Zell am See", "Brooklyn") works as well as a city. `radius` is a value with its unit — {"value": 10, "unit": "mi"} — so you never have to convert; it defaults to 20 km and may not exceed 100 km. Prefer `city` over `query` for place-based searches: it understands about 151,800 places and returns neighbouring towns, whereas `query` only matches words in a name or description. An unresolvable place name returns `placeNotRecognised` — that means the NAME was not understood, not that the place has no hotels. When a radius returns nothing you get `nearestBeyondRadiusKm` so you can widen deliberately rather than guess. SOMEWHERE SMALLER THAN A TOWN — A STREET, A LANDMARK, A STATION, A DISTRICT — GOES IN `coordinates`, NOT `city`. Sent as a `city`, such a name is either refused or resolved to a different place of the same name, which comes back as a confident answer about somewhere else. Estimate the point yourself, pair it with a small `radius`, and tell the guest you estimated it. A STAY. `checkIn`, `checkOut` and `adults` are required, and when you ask how many adults, ASK WHETHER ANY CHILDREN ARE COMING TOO and send their ages in `childrenAges` — ages, not a count. A child left out is priced as though they are not coming, so the guest can book a room that will not sleep them. The moment the guest mentions a child, put it in `childrenAges` even if you do not yet know the age — send `null` for that entry: THE SEARCH IS THEN REFUSED until you replace it with the real age, which is what stops a family being quoted as a couple. With those, the answer comes back in TWO groups. `priced` holds the hotels we read real rates for: every room and rate plan with its own `price` for the WHOLE stay, in the `currency` you sent, its `cancelTerms` where we hold any and null where we do not, its photos where we have them, a `bookingUrl` carrying the guest's dates WHERE THIS ENDPOINT PUBLISHES ONE — read that field's own description rather than assuming it is there, because an endpoint that can book the room for you publishes no link for it — and `stayTotal` / `pricePerNight` for the cheapest offer. EVERY PRICE IS IN THE `currency` YOU SENT. They come back in a deliberate order that is NOT nearest first and NOT cheapest first — show them in the order you are given. Show all the rooms you are handed, not just the cheapest. `unpriced` holds everything else: hotels that do not publish rates anywhere we can read, that did not answer us, or that were past the read ceiling. Those are the same thing to you — NONE of them carries a booking link of any kind. What a row carries instead is `websiteUrl`, the property's own site, or an EMPTY `websiteUrl` when we hold no site either and there is nothing to open at all. WHEN YOU RECOMMEND A HOTEL, RECOMMEND A PRICED ONE. We hold no rate for anything in `unpriced`, so you cannot say what it costs and it may be well over what the guest wants to spend; naming one as your suggestion answers their budget with a number nobody has. Bring one up only as "I could not get a rate for X, here is their website". Offer that site as the property's website and NEVER as a way to book or check rates, since we do not know it sells rooms online — and where it is empty, say we have nowhere to send them rather than going looking for a page of your own. AN EMPTY `priced` IS NOT "SOLD OUT", AND CHECK `aboveBudget` BEFORE CALLING IT UNREADABLE. With a `maxPricePerNight`, an empty `priced` means every hotel we priced costs more than that, and those hotels are in `aboveBudget` with real prices — quote them. Only when `aboveBudget` is empty too does an empty `priced` mean no rates could be read, and even then we get the same empty answer for a connection that failed, a party the property will not sell to, and a genuinely full hotel, and it never says which — so report the price as unknown and hand over the links, NEVER that a property is full. No ROW in this response can tell you a hotel has no space. One thing can: the `hint`, which says how many properties were left out because they told us outright that they are full — that is why they are in no group at all, and it is the only "full" this tool ever reports. It is never about a hotel you can see. A BUDGET IS A FILTER, NOT A LABEL — AND IT IS THE GUEST'S NUMBER, NEVER YOURS. `maxPricePerNight` is required, so ask them for a figure before searching rather than sending one of your own. Words like "budget", "cheap" or "affordable" are not amounts: read as one they filter out the very hotels the guest wanted, and the answer then tells them their limit was missed when they set no limit. A guest who says they have no limit still has a most-they-would-pay — ask for that and send it. `maxPricePerNight` decides what you are handed, at both levels. `priced` holds ONLY the hotels at or under it, each carrying ONLY its rooms at or under it — so every price you read out is one the guest already agreed to, and `priced` may be shorter than the `limit` you asked for. Dearer hotels are NOT in `priced`, NOT in `unpriced`, and not returned at all while any hotel fits: do not tell the guest this is every hotel in the area, and read the `hint`, which says how many were left out. Only when NOT ONE hotel fits do they come back, in `aboveBudget` — then say what the nearest options actually cost, never "nothing is available". A room in `priced` keeps ALL of its rate plans, including one dearer than the budget, so a breakfast or flexible rate is still there to offer as an upgrade. The comparison is `pricePerNight`, the stay total divided by nights — a stay AVERAGE, not a rate charged on every night, so say it that way. HOW MUCH ONE SEARCH RETURNS. `limit` is the ceiling across BOTH groups, priced first — an answer can be shorter when properties were dropped as full, defaulting to 10 and capped at 20. `unpriced` carries AT MOST 5 however large a `limit` you ask for, so raising `limit` buys you more PRICED hotels and never a longer list of names we hold no price for. THERE IS NO PAGING: one search returns one set of rows and there is no second page to ask for, so to reach anything past 20 properties, narrow the search — a nearer `city`, a smaller `radius`, a `query`, `country` or `starRating`. Calling again with the same filters returns the same rows. No count of the whole match comes back either, so do not tell the guest how many properties exist in a place — say what you can show them. Also supports country (de, at, ch, us, gb), starRating (1-star, 2-star, 3-star, 4-star, 5-star) and style (boutique), which are independent of one another. Some place names exist in more than one of those countries, and `city` alone resolves them to THE MOST POPULOUS of them — "Cambridge" is England, not Massachusetts; "Bellevue" and "Salem" stay European. It is a proxy for the place a guest means, and it is sometimes wrong: "Kendal" resolves to the Kendall in Miami, not the Cumbrian town. Pass `country` alongside whenever you know which you mean, and ALWAYS when the guest named the country — it overrides the contest outright. Returns public fields only. BOOKING A ROOM FROM THIS ANSWER. Every priced room carries an `offerId` and NO booking link, because we can book it for the guest ourselves — call `hotel_reserve` with the `offerId` rather than sending them to a website. It needs the guest's full name, email, phone and address, so ask for those once they have chosen a room. It answers one of two ways: a `confirmationNumber` means the room is booked and the guest pays the hotel at the desk, and a `paymentUrl` means the room is not held yet and that link is where the guest pays to get it booked. Where a room's `cancelTerms` is null we hold no cancellation terms for it: say so plainly, say the hotel's own terms apply and come with its confirmation, and book once the guest agrees on that basis — a blank is not a refusal.

list_hotels input schema
{
  "type": "object",
  "anyOf": [
    {
      "required": [
        "offerId"
      ]
    },
    {
      "required": [
        "checkIn",
        "checkOut",
        "adults",
        "maxPricePerNight",
        "currency"
      ]
    }
  ],
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "properties": {
    "city": {
      "type": "string",
      "maxLength": 120,
      "description": "Place to search around — any place name we cover, not just a city: a town, a village or a resort works, e.g. \"Salzburg\", \"Munich\", \"Silvaplana\", \"Wals\". Pass the place name ALONE, not a phrase: \"Salzburg\", never \"10 miles from Salzburg\" — say the distance in `radius` instead. Accents are folded away before lookup, so \"München\" is looked up as \"munchen\" and can collide with a smaller place of that same folded name — where a city has a common English name, prefer it: \"Munich\", not \"München\". Anything smaller than a town — a street, a landmark, a station, a district — belongs in `coordinates`, not here. Returns properties nearest first."
    },
    "limit": {
      "type": "integer",
      "maximum": 20,
      "minimum": 1,
      "description": "How many results to return. Defaults to 10, and may not exceed 20; a larger value is rejected, never quietly reduced."
    },
    "query": {
      "type": "string",
      "maxLength": 200,
      "description": "Free-text match over a property’s NAME, short description and tags. It is NOT a natural-language request: EVERY word you pass must appear on the same property, so each extra word narrows the result and a whole sentence almost always matches nothing. Pass \"spa munich\", never \"find me a nice spa hotel in Munich\". Words match close to as written — the index is stemmed for English, so a German compound or inflection needs the form that actually appears on the listing. A place belongs in `city`, dates in `checkIn`/`checkOut`, a budget in `maxPricePerNight`; none of those are searched here, and a date or a price in `query` only empties the result. When a search that set `query` comes back empty, the reply carries a `hint` saying whether the text or the other filters emptied it."
    },
    "style": {
      "enum": [
        "boutique"
      ],
      "type": "string",
      "description": "Filter by how the property presents itself: boutique. Independent of starRating — combine them to ask for both."
    },
    "adults": {
      "type": "integer",
      "maximum": 20,
      "minimum": 1,
      "description": "Adults in the party. REQUIRED — ask how many people are travelling. Properties price by party size, so a number you assumed prices a stay nobody asked for. \"A couple\" is two; anything vaguer is a question."
    },
    "radius": {
      "type": "object",
      "required": [
        "value",
        "unit"
      ],
      "properties": {
        "unit": {
          "enum": [
            "km",
            "mi"
          ],
          "type": "string",
          "description": "\"km\" or \"mi\" — state the unit the guest used; do not convert it yourself."
        },
        "value": {
          "type": "number",
          "maximum": 100,
          "exclusiveMinimum": 0
        }
      },
      "description": "How far around `city` — or around `coordinates`, when you sent a point — to search, as a value and its unit: {\"value\": 10, \"unit\": \"mi\"}. Defaults to 20 km. May not exceed 100 km once converted; larger values are rejected, never quietly reduced."
    },
    "checkIn": {
      "type": "string",
      "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
      "description": "Arrival date, YYYY-MM-DD. REQUIRED — ask the guest for their dates before you search. Without them nothing can be priced and the answer would be a list of property names with no rooms, no prices and no results card. NEVER choose dates yourself, and do not read them out of a vague phrase: \"next month\" is not a date, it is a question to ask."
    },
    "country": {
      "enum": [
        "de",
        "at",
        "ch",
        "us",
        "gb"
      ],
      "type": "string",
      "description": "Filter by country: de=Germany, at=Austria, ch=Switzerland, us=United States, gb=United Kingdom"
    },
    "offerId": {
      "type": "string",
      "maxLength": 2000,
      "minLength": 1,
      "description": "An `offerId` from an earlier answer, to get a fresh price for THAT room rather than searching again. Pass it when the guest has already chosen and you need a current handle — after `hotel_reserve` said the offer expired, or before asking them to confirm. The answer comes back as that one property carrying that one room with its rate plans, each with a new `offerId`, so you never have to work out which of several rooms was theirs. An offer older than 30 minutes still works here — this call is what refreshes it. EVERY OTHER SEARCH FIELD IS IGNORED when you pass this: `city`, `coordinates`, `radius`, `query`, `country`, `starRating`, `style`, `limit` and `maxPricePerNight` describe a search, and this is not one. `language` still applies. The stay comes from the handle, so leave the dates out to re-price the same nights; pass `checkIn`, `checkOut` and `adults` together to ask for the same room on different nights. THIS IS THE ONE CALL THAT NEEDS NO DATES AND NO BUDGET: a plain search is refused without them, but a handle already carries the stay it was quoted for, and a budget here could filter out the very room the guest chose."
    },
    "checkOut": {
      "type": "string",
      "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
      "description": "Departure date, YYYY-MM-DD. REQUIRED, on the same terms as `checkIn` — ask the guest, never choose."
    },
    "currency": {
      "type": "string",
      "maxLength": 3,
      "minLength": 3,
      "description": "THE CURRENCY OF `maxPricePerNight`, AND THE ONE THE ANSWER IS QUOTED BACK IN. ISO 4217 — `EUR`, `CHF`, `GBP`. REQUIRED on every search: send `EUR` when the guest gave you a euro figure, which is most of the time. USE THE ONE THEY ALREADY NAMED and do not re-ask — \"200 euros\" is `EUR`, \"CHF 165\" is `CHF`. Ask them only when they gave a bare number and you genuinely cannot tell which currency they meant; guessing it is the thing this field exists to stop. IT ALSO CHOOSES THE CURRENCY THE ANSWER COMES BACK IN: every `price`, `stayTotal` and `pricePerNight` is in what you send here. It exists because a budget with no currency was read as euro, so a guest who said \"CHF 165\" was filtered at €165 and the hotels they could actually afford were not marked dear, they were absent. PASS THE GUEST’S OWN NUMBER and name its currency here; never convert it yourself. Quote them their own figure back; the prices you are handed are already in it. A currency we cannot get a rate for is REFUSED rather than assumed to be euro."
    },
    "language": {
      "type": "string",
      "maxLength": 64,
      "description": "The language to render the results card in — pass the language the guest is WRITING in, unless they have asked for another. A bare language code or a full BCP 47 tag both work (\"de\", \"de-AT\", \"en-GB\"); only the language part is read, so a region suffix is harmless and case does not matter. German and English are rendered; any other value falls back to English rather than failing the search. Omit it and the card follows the guest’s own app language, then English. It changes ONLY the labels WE write on the card — never a room name or a rate plan, which stay in the property’s own words. A cancellation term carries the property’s own figures, but the sentence around them is written in English whatever language the card is in."
    },
    "amenities": {
      "type": "array",
      "items": {
        "enum": [
          "24h-reception",
          "accessible-parking",
          "adults-only",
          "air-conditioning",
          "airport-shuttle",
          "allergy-free",
          "bar",
          "bbq",
          "beachfront",
          "breakfast",
          "conference-facilities",
          "cycling",
          "elevator",
          "ev-charging",
          "family-rooms",
          "games",
          "garden",
          "golf",
          "gym",
          "hammam",
          "hiking",
          "hot-tub",
          "kids-facilities",
          "laundry",
          "massage",
          "minimarket",
          "parking",
          "parking-free",
          "parking-none",
          "parking-paid",
          "parking-private",
          "pets-allowed",
          "pool",
          "restaurant",
          "room-service",
          "sauna",
          "ski-school",
          "ski-storage",
          "skiing",
          "solarium",
          "soundproof",
          "spa",
          "tennis",
          "terrace",
          "water-park",
          "wheelchair-accessible",
          "wifi"
        ],
        "type": "string"
      },
      "maxItems": 6,
      "description": "Facilities and house rules the property must ALL have — a hotel missing one is excluded, so every extra value narrows the result. Prefer this to naming an amenity in `query`: these are exact, while `query` matches only the words we happen to store. Naming one in `query` in either language (\"swimming pool\", \"Schwimmbad\") also works and resolves to these same values."
    },
    "starRating": {
      "enum": [
        "1-star",
        "2-star",
        "3-star",
        "4-star",
        "5-star"
      ],
      "type": "string",
      "description": "Filter by star rating: 1-star, 2-star, 3-star, 4-star, 5-star. A property can hold stars AND be a boutique hotel; ask for the two separately."
    },
    "coordinates": {
      "type": "object",
      "required": [
        "lat",
        "lon"
      ],
      "properties": {
        "lat": {
          "type": "number",
          "maximum": 90,
          "minimum": -90
        },
        "lon": {
          "type": "number",
          "maximum": 180,
          "minimum": -180
        }
      },
      "description": "A point to search around, for anywhere smaller than a town: a street, a landmark, a station, a junction, a named district. ESTIMATE IT YOURSELF for the place the guest named — `city` takes town names only, and a smaller name sent there is either refused or resolved to a different place of the same name, which comes back as a confident answer about somewhere else. Same shape as the `coordinates` on a hotel in the answer, so you can search around one you were just given. TAKES PRECEDENCE over `city` when both are sent: the point decides what is searched and which row leads, and `city` only names the place in the answer. Use `radius` to say how far around the point to look, and keep it small for a street or a venue. Tell the guest you estimated the point, so they can correct you."
    },
    "childrenAges": {
      "type": "array",
      "items": {
        "anyOf": [
          {
            "type": "integer",
            "maximum": 17,
            "minimum": 0
          },
          {
            "type": "null"
          }
        ]
      },
      "maxItems": 10,
      "description": "ONE ENTRY PER CHILD — their age, or `null` when you do not know it yet. Not a count: properties price a child by its age, and none may be invented. ASK WHETHER ANYONE UNDER 18 IS TRAVELLING, in the same message as `adults`. The moment the guest mentions a child, put it here: send `[null]` if you have not asked the age yet and the search is REFUSED until you replace it with the real age, which is what stops a family being quoted as a couple. A child you leave out altogether is priced as though they are not coming — the quote is for the adults alone, and the room the guest books may not sleep them, which they find out at the desk. Leave the field out when there are no children — an EMPTY ARRAY says the same thing and is read as none. An age of 0 is a real age — an infant is a child, not nobody."
    },
    "maxPricePerNight": {
      "type": "integer",
      "maximum": 9007199254740991,
      "minimum": 1,
      "description": "Budget per night, in the `currency` you send alongside it — BOTH ARE REQUIRED. TAKEN FROM A FIGURE THE GUEST GAVE YOU AND NOWHERE ELSE. Ask them roughly what they want to spend a night before you search: \"a budget hotel\", \"somewhere cheap\", \"nothing too pricey\" and a star rating are not amounts, and a figure you inferred hides every hotel they could have afforded behind a limit they never set. If they say they have no limit, ask for the most they would still pay and send that. PASS THE NUMBER THEY SAID AND NAME ITS CURRENCY — do not convert it yourself, and do not pass a euro figure you worked out from one they gave in francs or pounds. It FILTERS rather than labels. `priced` comes back holding ONLY the hotels at or under it, each carrying ONLY its rooms at or under it — so it can be shorter than `limit`, and it is not every hotel in the area. A kept room keeps all of its rate plans, including one dearer than the budget, so a breakfast rate is still there to offer. Dearer HOTELS are returned only when not one fits, in `aboveBudget`. Read the `hint`, which says how many were left out. Compares the stay average, not a nightly rate."
    }
  }
}
list_hotels output schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "unpriced"
  ],
  "properties": {
    "hint": {
      "type": "string",
      "description": "One or more sentences about THIS answer that the rows cannot tell you: why a search came back empty, what a budget left out, how many properties were dropped because they told us they are full, and — when the call was refused for missing dates, party size or budget — exactly what to ask the guest before searching again. Read the whole string; several of these can arrive together. Where it describes hotels it describes the ones this call priced, never the whole area."
    },
    "stay": {
      "type": "object",
      "required": [
        "checkIn",
        "checkOut",
        "adults",
        "children"
      ],
      "properties": {
        "adults": {
          "type": "integer",
          "maximum": 9007199254740991,
          "minimum": -9007199254740991
        },
        "checkIn": {
          "type": "string"
        },
        "checkOut": {
          "type": "string"
        },
        "children": {
          "type": "integer",
          "maximum": 9007199254740991,
          "minimum": -9007199254740991
        }
      },
      "description": "The stay this answer priced, echoed back. Present only on a dated call — a view needs it to label the result and to turn a stay total into a per-night figure.",
      "additionalProperties": false
    },
    "error": {
      "type": "string"
    },
    "place": {
      "type": "string",
      "description": "The `city` that was resolved, echoed back."
    },
    "priced": {
      "type": "array",
      "items": {
        "type": "object",
        "required": [
          "slug",
          "name",
          "shortDescription",
          "websiteUrl",
          "tags",
          "datesApplied",
          "partyApplied",
          "rooms",
          "stayTotal",
          "pricePerNight"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "rooms": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "price",
                "cancelTerms",
                "soldOut",
                "name"
              ],
              "properties": {
                "name": {
                  "type": "string"
                },
                "price": {
                  "type": [
                    "number",
                    "null"
                  ],
                  "description": "Total for the WHOLE stay, not per night, in `currency`."
                },
                "offerId": {
                  "type": "string",
                  "description": "An opaque handle for THIS room at THIS rate for the stay that was searched. Pass it back verbatim to reserve exactly this offer — never edit it, take it apart, or build one yourself. It expires, so re-run the search rather than keeping one. Absent when the stay was not priced, and on an endpoint that cannot book the room itself — there the room carries a `bookingUrl` instead."
                },
                "soldOut": {
                  "type": "boolean"
                },
                "imageUrls": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  },
                  "description": "Every photo we hold of this room, in the property's own order — show them on the card. Absent when we hold none, which is not a claim the room has none. Photos belong to the room, not to a rate plan, so the plans under `ratePlans` carry none of their own. These URLs EXPIRE, within a day of the search: render them now rather than storing them, and re-run the search for a card you are showing again later."
                },
                "ratePlans": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "required": [
                      "price",
                      "cancelTerms",
                      "soldOut"
                    ],
                    "properties": {
                      "price": {
                        "type": [
                          "number",
                          "null"
                        ],
                        "description": "Total for the WHOLE stay, not per night, in `currency`."
                      },
                      "offerId": {
                        "type": "string",
                        "description": "An opaque handle for THIS room at THIS rate for the stay that was searched. Pass it back verbatim to reserve exactly this offer — never edit it, take it apart, or build one yourself. It expires, so re-run the search rather than keeping one. Absent when the stay was not priced, and on an endpoint that cannot book the room itself — there the room carries a `bookingUrl` instead."
                      },
                      "soldOut": {
                        "type": "boolean"
                      },
                      "bookingUrl": {
                        "type": "string",
                        "description": "NOT PUBLISHED on this endpoint. Book the rate with `hotel_reserve`, passing its `offerId`, and that call tells you where the guest pays. Never promise a guest a link to the hotel’s own page for a rate here: you will not be given one, with or without an `offerId` beside it."
                      },
                      "cancelTerms": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "This rate's cancellation terms. NULL means WE hold none for this rate — it is NOT a claim that the rate cannot be cancelled, and NOT a claim that cancelling is free. Tell the guest we do not have them and that the hotel's own terms apply and arrive with the hotel’s confirmation. A blank never stops you booking, and never invent one."
                      },
                      "ratePlanName": {
                        "type": "string"
                      },
                      "breakfastIncluded": {
                        "type": "boolean"
                      }
                    },
                    "additionalProperties": false
                  },
                  "description": "The room's other rate plans, cheapest first. Absent when it sells a single plan."
                },
                "bookingUrl": {
                  "type": "string",
                  "description": "NOT PUBLISHED on this endpoint. Book the rate with `hotel_reserve`, passing its `offerId`, and that call tells you where the guest pays. Never promise a guest a link to the hotel’s own page for a rate here: you will not be given one, with or without an `offerId` beside it."
                },
                "cancelTerms": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "This rate's cancellation terms. NULL means WE hold none for this rate — it is NOT a claim that the rate cannot be cancelled, and NOT a claim that cancelling is free. Tell the guest we do not have them and that the hotel's own terms apply and arrive with the hotel’s confirmation. A blank never stops you booking, and never invent one."
                },
                "ratePlanName": {
                  "type": "string"
                },
                "breakfastIncluded": {
                  "type": "boolean"
                }
              },
              "additionalProperties": false
            },
            "description": "One entry per room, priced at its cheapest rate plan, with the room's other plans under `ratePlans`. Never empty — show them all, not just the cheapest. In `priced` under a `maxPricePerNight` these are only the rooms at or under the budget; a room kept here keeps ALL of its rate plans, including one dearer than the budget."
          },
          "listing": {
            "type": "object",
            "properties": {
              "city": {
                "type": "string",
                "description": "The town this property was placed in, which is not a line of its address."
              },
              "chain": {
                "type": "string"
              },
              "style": {
                "type": "string",
                "description": "e.g. \"boutique\". A property can carry this AND a starRating; neither implies the other."
              },
              "nearby": {
                "type": "array",
                "items": {
                  "type": "object",
                  "required": [
                    "name"
                  ],
                  "properties": {
                    "name": {
                      "type": "string"
                    },
                    "category": {
                      "type": "string"
                    },
                    "distanceKm": {
                      "type": "number"
                    }
                  },
                  "additionalProperties": false
                },
                "description": "Nearby landmarks with their distance in km, where we hold any."
              },
              "address": {
                "type": "object",
                "properties": {
                  "city": {
                    "type": "string"
                  },
                  "street": {
                    "type": "string"
                  },
                  "postalCode": {
                    "type": "string"
                  },
                  "countryCode": {
                    "type": "string"
                  }
                },
                "description": "The postal address, where one was filled in. A bare `city` without a street is NOT here — see the sibling `city` field.",
                "additionalProperties": false
              },
              "country": {
                "type": "string"
              },
              "policies": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "House rules: `adults-only`, `pets-allowed`, `parking-free|paid|private|none`."
              },
              "amenities": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "The facilities worth filtering on, from the tag taxonomy. NOT everything the property declared — see `amenityIds`."
              },
              "photoUrls": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Photographs of the property, served from our own host — show them in the order given. At most 3. Absent where we hold none, which says nothing about the property, and never an empty list. The links expire within a day, so render them rather than storing them."
              },
              "amenityIds": {
                "type": "array",
                "items": {
                  "type": "number"
                },
                "description": "Every facility the property declared, as numeric ids with no vocabulary published here — you cannot name one from its id. Read `amenities` instead, the filterable subset of these as slugs; this is the complete list and is usually much longer."
              },
              "starRating": {
                "type": "string",
                "description": "e.g. \"4-star\", from the tag taxonomy. Absent when the property is not star-rated."
              },
              "checkInFrom": {
                "type": "string",
                "description": "Earliest check-in, as the property states it, e.g. \"03:00 PM\"."
              },
              "coordinates": {
                "type": "object",
                "required": [
                  "lat",
                  "lon"
                ],
                "properties": {
                  "lat": {
                    "type": "number"
                  },
                  "lon": {
                    "type": "number"
                  }
                },
                "description": "Where the building stands, in WGS 84 degrees. Absent where we hold no building-level coordinate, which says nothing about the property — never fill it in from `address` or `city`.",
                "additionalProperties": false
              },
              "guestRating": {
                "type": "number",
                "description": "What guests scored the property, 0–5 with one decimal. Always positive — absent means no rating, which is not a low one."
              },
              "reviewCount": {
                "type": "number",
                "description": "How many reviews `guestRating` rests on. Present only alongside `guestRating`, and not beside every one of them — where it is missing, there is no count to quote with the rating."
              },
              "propertyType": {
                "type": "string"
              },
              "reviewScores": {
                "type": "object",
                "properties": {
                  "food": {
                    "type": "number"
                  },
                  "room": {
                    "type": "number"
                  },
                  "value": {
                    "type": "number"
                  },
                  "overall": {
                    "type": "number"
                  },
                  "service": {
                    "type": "number"
                  },
                  "location": {
                    "type": "number"
                  },
                  "amenities": {
                    "type": "number"
                  },
                  "cleanliness": {
                    "type": "number"
                  }
                },
                "description": "What the property scores per review category, 0–5 on the same scale as `guestRating`. A score, not a count of anything. Absence says nothing about the property.",
                "additionalProperties": false
              },
              "checkOutUntil": {
                "type": "string",
                "description": "Latest check-out, e.g. \"12:00 PM\"."
              },
              "reviewScoresAsOf": {
                "type": "string",
                "description": "The date `reviewScores` was last recomputed, e.g. \"2025-12-18\" — how old the breakdown beside it is."
              }
            },
            "description": "The hotel's own attributes. Carried whether or not we could price it.",
            "additionalProperties": false
          },
          "stayTotal": {
            "type": "number",
            "description": "Cheapest bookable offer, for the whole stay, in the `currency` you sent."
          },
          "bookingUrl": {
            "type": "string",
            "description": "Not published on this endpoint for a priced hotel. Book the room with `hotel_reserve`, passing the room’s `offerId`. A refusal from it means search again here rather than sending the guest to another site — the reasons it can refuse are temporary (the room or the rate has gone, the price moved) or ours to fix. Never promise the guest a link to the hotel’s own page for a priced hotel: you will not be given one."
          },
          "distanceKm": {
            "type": "number"
          },
          "websiteUrl": {
            "type": "string"
          },
          "datesApplied": {
            "type": "boolean"
          },
          "partyApplied": {
            "type": "boolean"
          },
          "pricePerNight": {
            "type": "number",
            "description": "That total divided by nights — a stay AVERAGE, not a rate charged every night. In the `currency` you sent."
          },
          "shortDescription": {
            "type": "string"
          },
          "distanceApproximate": {
            "type": "boolean"
          }
        },
        "additionalProperties": false
      },
      "description": "Only on a dated call. AT MOST your `limit` of them — 10 by default and never more than 20 — hotels we could get a real price for. Deliberately ordered — SHOW THEM IN THE ORDER YOU ARE GIVEN, and lead with the top row. The order is neither distance nor price, so do not call any row the nearest or the cheapest. Under a `maxPricePerNight` this holds ONLY the hotels at or under it, so it can be shorter than `limit` and an empty one does NOT mean the rates were unreadable — check `aboveBudget` before saying anything about price. A hotel here is NOT in `unpriced`; one that did not answer us is NOT here, it stays in `unpriced`, where it carries the property's own website and no booking link."
    },
    "currency": {
      "type": "string",
      "description": "The ISO code every price in this answer is in."
    },
    "language": {
      "type": "string",
      "description": "The `language` that was asked for, echoed back verbatim. Absent when the caller passed none, which is how the card knows to follow the guest’s own app language instead."
    },
    "unpriced": {
      "type": "array",
      "items": {
        "type": "object",
        "required": [
          "slug",
          "name",
          "shortDescription",
          "websiteUrl",
          "tags"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "listing": {
            "type": "object",
            "properties": {
              "city": {
                "type": "string",
                "description": "The town this property was placed in, which is not a line of its address."
              },
              "chain": {
                "type": "string"
              },
              "style": {
                "type": "string",
                "description": "e.g. \"boutique\". A property can carry this AND a starRating; neither implies the other."
              },
              "nearby": {
                "type": "array",
                "items": {
                  "type": "object",
                  "required": [
                    "name"
                  ],
                  "properties": {
                    "name": {
                      "type": "string"
                    },
                    "category": {
                      "type": "string"
                    },
                    "distanceKm": {
                      "type": "number"
                    }
                  },
                  "additionalProperties": false
                },
                "description": "Nearby landmarks with their distance in km, where we hold any."
              },
              "address": {
                "type": "object",
                "properties": {
                  "city": {
                    "type": "string"
                  },
                  "street": {
                    "type": "string"
                  },
                  "postalCode": {
                    "type": "string"
                  },
                  "countryCode": {
                    "type": "string"
                  }
                },
                "description": "The postal address, where one was filled in. A bare `city` without a street is NOT here — see the sibling `city` field.",
                "additionalProperties": false
              },
              "country": {
                "type": "string"
              },
              "policies": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "House rules: `adults-only`, `pets-allowed`, `parking-free|paid|private|none`."
              },
              "amenities": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "The facilities worth filtering on, from the tag taxonomy. NOT everything the property declared — see `amenityIds`."
              },
              "photoUrls": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Photographs of the property, served from our own host — show them in the order given. At most 3. Absent where we hold none, which says nothing about the property, and never an empty list. The links expire within a day, so render them rather than storing them."
              },
              "amenityIds": {
                "type": "array",
                "items": {
                  "type": "number"
                },
                "description": "Every facility the property declared, as numeric ids with no vocabulary published here — you cannot name one from its id. Read `amenities` instead, the filterable subset of these as slugs; this is the complete list and is usually much longer."
              },
              "starRating": {
                "type": "string",
                "description": "e.g. \"4-star\", from the tag taxonomy. Absent when the property is not star-rated."
              },
              "checkInFrom": {
                "type": "string",
                "description": "Earliest check-in, as the property states it, e.g. \"03:00 PM\"."
              },
              "coordinates": {
                "type": "object",
                "required": [
                  "lat",
                  "lon"
                ],
                "properties": {
                  "lat": {
                    "type": "number"
                  },
                  "lon": {
                    "type": "number"
                  }
                },
                "description": "Where the building stands, in WGS 84 degrees. Absent where we hold no building-level coordinate, which says nothing about the property — never fill it in from `address` or `city`.",
                "additionalProperties": false
              },
              "guestRating": {
                "type": "number",
                "description": "What guests scored the property, 0–5 with one decimal. Always positive — absent means no rating, which is not a low one."
              },
              "reviewCount": {
                "type": "number",
                "description": "How many reviews `guestRating` rests on. Present only alongside `guestRating`, and not beside every one of them — where it is missing, there is no count to quote with the rating."
              },
              "propertyType": {
                "type": "string"
              },
              "reviewScores": {
                "type": "object",
                "properties": {
                  "food": {
                    "type": "number"
                  },
                  "room": {
                    "type": "number"
                  },
                  "value": {
                    "type": "number"
                  },
                  "overall": {
                    "type": "number"
                  },
                  "service": {
                    "type": "number"
                  },
                  "location": {
                    "type": "number"
                  },
                  "amenities": {
                    "type": "number"
                  },
                  "cleanliness": {
                    "type": "number"
                  }
                },
                "description": "What the property scores per review category, 0–5 on the same scale as `guestRating`. A score, not a count of anything. Absence says nothing about the property.",
                "additionalProperties": false
              },
              "checkOutUntil": {
                "type": "string",
                "description": "Latest check-out, e.g. \"12:00 PM\"."
              },
              "reviewScoresAsOf": {
                "type": "string",
                "description": "The date `reviewScores` was last recomputed, e.g. \"2025-12-18\" — how old the breakdown beside it is."
              }
            },
            "description": "The hotel's own attributes. Carried whether or not we could price it.",
            "additionalProperties": false
          },
          "distanceKm": {
            "type": "number"
          },
          "websiteUrl": {
            "type": "string"
          },
          "shortDescription": {
            "type": "string"
          },
          "distanceApproximate": {
            "type": "boolean"
          }
        },
        "additionalProperties": false
      },
      "description": "Matching properties we have NO price for. SHOW THEM IN THE ORDER YOU ARE GIVEN, and do not call any row the nearest. On a dated call that means: the property does not publish rates anywhere we can read, or it does and did not answer, or it was past the read ceiling — all the same to you. There is NO booking link on a row here. What a row carries instead is `websiteUrl`, the property’s own site, and the guest checks there: offer it as their website and never as a way to book or check rates, since we do not know it sells rooms online. An EMPTY `websiteUrl` means we hold no site either, so that row has nothing to open — say so plainly, do not invent a link, and do not go looking for one. None of that means full: a property here may well have rooms. A property that TOLD US it is full for this stay is the one thing not here — it is dropped from the whole answer rather than given a link that opens on nothing, and `hint` says how many were dropped. On a dateless call it is simply every match, because nothing was priced. NEVER RECOMMEND ONE: we hold no rate for these, so a hotel here may cost anything, including more than the guest’s budget — recommend from `priced`, and bring one of these up only as \"I could not get a rate for X, here is their website\". AT MOST 5 of them come back however large a `limit` you ask for, for the same reason — a property with no price is something to mention, not an option to offer — so a short list here does NOT mean the area is small, and every property we could price is still returned. AN ANSWER CARRYING ENOUGH REAL PRICES RETURNS NONE OF THEM AT ALL, so an empty list here is NOT a statement about the area and NOT a claim that every property in it was priced — it means this answer did not need them. Read it as nothing, never as a finding, and never tell the guest a search came back short because of it. No count of the whole match is returned."
    },
    "aboveBudget": {
      "type": "array",
      "items": {
        "type": "object",
        "required": [
          "slug",
          "name",
          "shortDescription",
          "websiteUrl",
          "tags",
          "datesApplied",
          "partyApplied",
          "rooms",
          "stayTotal",
          "pricePerNight"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "rooms": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "price",
                "cancelTerms",
                "soldOut",
                "name"
              ],
              "properties": {
                "name": {
                  "type": "string"
                },
                "price": {
                  "type": [
                    "number",
                    "null"
                  ],
                  "description": "Total for the WHOLE stay, not per night, in `currency`."
                },
                "offerId": {
                  "type": "string",
                  "description": "An opaque handle for THIS room at THIS rate for the stay that was searched. Pass it back verbatim to reserve exactly this offer — never edit it, take it apart, or build one yourself. It expires, so re-run the search rather than keeping one. Absent when the stay was not priced, and on an endpoint that cannot book the room itself — there the room carries a `bookingUrl` instead."
                },
                "soldOut": {
                  "type": "boolean"
                },
                "imageUrls": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  },
                  "description": "Every photo we hold of this room, in the property's own order — show them on the card. Absent when we hold none, which is not a claim the room has none. Photos belong to the room, not to a rate plan, so the plans under `ratePlans` carry none of their own. These URLs EXPIRE, within a day of the search: render them now rather than storing them, and re-run the search for a card you are showing again later."
                },
                "ratePlans": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "required": [
                      "price",
                      "cancelTerms",
                      "soldOut"
                    ],
                    "properties": {
                      "price": {
                        "type": [
                          "number",
                          "null"
                        ],
                        "description": "Total for the WHOLE stay, not per night, in `currency`."
                      },
                      "offerId": {
                        "type": "string",
                        "description": "An opaque handle for THIS room at THIS rate for the stay that was searched. Pass it back verbatim to reserve exactly this offer — never edit it, take it apart, or build one yourself. It expires, so re-run the search rather than keeping one. Absent when the stay was not priced, and on an endpoint that cannot book the room itself — there the room carries a `bookingUrl` instead."
                      },
                      "soldOut": {
                        "type": "boolean"
                      },
                      "bookingUrl": {
                        "type": "string",
                        "description": "NOT PUBLISHED on this endpoint. Book the rate with `hotel_reserve`, passing its `offerId`, and that call tells you where the guest pays. Never promise a guest a link to the hotel’s own page for a rate here: you will not be given one, with or without an `offerId` beside it."
                      },
                      "cancelTerms": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "description": "This rate's cancellation terms. NULL means WE hold none for this rate — it is NOT a claim that the rate cannot be cancelled, and NOT a claim that cancelling is free. Tell the guest we do not have them and that the hotel's own terms apply and arrive with the hotel’s confirmation. A blank never stops you booking, and never invent one."
                      },
                      "ratePlanName": {
                        "type": "string"
                      },
                      "breakfastIncluded": {
                        "type": "boolean"
                      }
                    },
                    "additionalProperties": false
                  },
                  "description": "The room's other rate plans, cheapest first. Absent when it sells a single plan."
                },
                "bookingUrl": {
                  "type": "string",
                  "description": "NOT PUBLISHED on this endpoint. Book the rate with `hotel_reserve`, passing its `offerId`, and that call tells you where the guest pays. Never promise a guest a link to the hotel’s own page for a rate here: you will not be given one, with or without an `offerId` beside it."
                },
                "cancelTerms": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "This rate's cancellation terms. NULL means WE hold none for this rate — it is NOT a claim that the rate cannot be cancelled, and NOT a claim that cancelling is free. Tell the guest we do not have them and that the hotel's own terms apply and arrive with the hotel’s confirmation. A blank never stops you booking, and never invent one."
                },
                "ratePlanName": {
                  "type": "string"
                },
                "breakfastIncluded": {
                  "type": "boolean"
                }
              },
              "additionalProperties": false
            },
            "description": "One entry per room, priced at its cheapest rate plan, with the room's other plans under `ratePlans`. Never empty — show them all, not just the cheapest. In `priced` under a `maxPricePerNight` these are only the rooms at or under the budget; a room kept here keeps ALL of its rate plans, including one dearer than the budget."
          },
          "listing": {
            "type": "object",
            "properties": {
              "city": {
                "type": "string",
                "description": "The town this property was placed in, which is not a line of its address."
              },
              "chain": {
                "type": "string"
              },
              "style": {
                "type": "string",
                "description": "e.g. \"boutique\". A property can carry this AND a starRating; neither implies the other."
              },
              "nearby": {
                "type": "array",
                "items": {
                  "type": "object",
                  "required": [
                    "name"
                  ],
                  "properties": {
                    "name": {
                      "type": "string"
                    },
                    "category": {
                      "type": "string"
                    },
                    "distanceKm": {
                      "type": "number"
                    }
                  },
                  "additionalProperties": false
                },
                "description": "Nearby landmarks with their distance in km, where we hold any."
              },
              "address": {
                "type": "object",
                "properties": {
                  "city": {
                    "type": "string"
                  },
                  "street": {
                    "type": "string"
                  },
                  "postalCode": {
                    "type": "string"
                  },
                  "countryCode": {
                    "type": "string"
                  }
                },
                "description": "The postal address, where one was filled in. A bare `city` without a street is NOT here — see the sibling `city` field.",
                "additionalProperties": false
              },
              "country": {
                "type": "string"
              },
              "policies": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "House rules: `adults-only`, `pets-allowed`, `parking-free|paid|private|none`."
              },
              "amenities": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "The facilities worth filtering on, from the tag taxonomy. NOT everything the property declared — see `amenityIds`."
              },
              "photoUrls": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Photographs of the property, served from our own host — show them in the order given. At most 3. Absent where we hold none, which says nothing about the property, and never an empty list. The links expire within a day, so render them rather than storing them."
              },
              "amenityIds": {
                "type": "array",
                "items": {
                  "type": "number"
                },
                "description": "Every facility the property declared, as numeric ids with no vocabulary published here — you cannot name one from its id. Read `amenities` instead, the filterable subset of these as slugs; this is the complete list and is usually much longer."
              },
              "starRating": {
                "type": "string",
                "description": "e.g. \"4-star\", from the tag taxonomy. Absent when the property is not star-rated."
              },
              "checkInFrom": {
                "type": "string",
                "description": "Earliest check-in, as the property states it, e.g. \"03:00 PM\"."
              },
              "coordinates": {
                "type": "object",
                "required": [
                  "lat",
                  "lon"
                ],
                "properties": {
                  "lat": {
                    "type": "number"
                  },
                  "lon": {
                    "type": "number"
                  }
                },
                "description": "Where the building stands, in WGS 84 degrees. Absent where we hold no building-level coordinate, which says nothing about the property — never fill it in from `address` or `city`.",
                "additionalProperties": false
              },
              "guestRating": {
                "type": "number",
                "description": "What guests scored the property, 0–5 with one decimal. Always positive — absent means no rating, which is not a low one."
              },
              "reviewCount": {
                "type": "number",
                "description": "How many reviews `guestRating` rests on. Present only alongside `guestRating`, and not beside every one of them — where it is missing, there is no count to quote with the rating."
              },
              "propertyType": {
                "type": "string"
              },
              "reviewScores": {
                "type": "object",
                "properties": {
                  "food": {
                    "type": "number"
                  },
                  "room": {
                    "type": "number"
                  },
                  "value": {
                    "type": "number"
                  },
                  "overall": {
                    "type": "number"
                  },
                  "service": {
                    "type": "number"
                  },
                  "location": {
                    "type": "number"
                  },
                  "amenities": {
                    "type": "number"
                  },
                  "cleanliness": {
                    "type": "number"
                  }
                },
                "description": "What the property scores per review category, 0–5 on the same scale as `guestRating`. A score, not a count of anything. Absence says nothing about the property.",
                "additionalProperties": false
              },
              "checkOutUntil": {
                "type": "string",
                "description": "Latest check-out, e.g. \"12:00 PM\"."
              },
              "reviewScoresAsOf": {
                "type": "string",
                "description": "The date `reviewScores` was last recomputed, e.g. \"2025-12-18\" — how old the breakdown beside it is."
              }
            },
            "description": "The hotel's own attributes. Carried whether or not we could price it.",
            "additionalProperties": false
          },
          "stayTotal": {
            "type": "number",
            "description": "Cheapest bookable offer, for the whole stay, in the `currency` you sent."
          },
          "bookingUrl": {
            "type": "string",
            "description": "Not published on this endpoint for a priced hotel. Book the room with `hotel_reserve`, passing the room’s `offerId`. A refusal from it means search again here rather than sending the guest to another site — the reasons it can refuse are temporary (the room or the rate has gone, the price moved) or ours to fix. Never promise the guest a link to the hotel’s own page for a priced hotel: you will not be given one."
          },
          "distanceKm": {
            "type": "number"
          },
          "websiteUrl": {
            "type": "string"
          },
          "datesApplied": {
            "type": "boolean"
          },
          "partyApplied": {
            "type": "boolean"
          },
          "pricePerNight": {
            "type": "number",
            "description": "That total divided by nights — a stay AVERAGE, not a rate charged every night. In the `currency` you sent."
          },
          "shortDescription": {
            "type": "string"
          },
          "distanceApproximate": {
            "type": "boolean"
          }
        },
        "additionalProperties": false
      },
      "description": "Hotels we read real prices for that are OVER your `maxPricePerNight`, in the same shape as `priced`. Returned only when NOT ONE hotel fits the budget — when any does, dearer ones are left out entirely. So this is never a second page of results to offer alongside `priced`: it is the answer to \"then what does the nearest thing cost\", and the honest sentence is \"none of the ones I could price are under X, the cheapest is Y\". Ordered like `priced`, so read the prices rather than taking the first row as the cheapest. Each carries all of its rooms, since here the prices are the point."
    },
    "maxRadiusKm": {
      "type": "number"
    },
    "radiusUsedKm": {
      "type": "number",
      "description": "The radius actually searched, in km, after converting the caller’s unit."
    },
    "maxPricePerNight": {
      "type": "number",
      "description": "The budget `aboveBudget` was measured against — the GUEST’S OWN figure, in the `currency` you sent, like every price beside it. Echoed back so the results card can name it. Present only alongside `aboveBudget`. Say the figure when you quote those prices — if the guest never gave you one, that is your own number in front of them."
    },
    "requestedRadiusKm": {
      "type": "number"
    },
    "placeNotRecognised": {
      "type": "string",
      "description": "The place name could not be resolved. NOT a statement that the place has no hotels."
    },
    "nearestSearchedToKm": {
      "type": "number",
      "description": "How far the nearestBeyondRadiusKm search itself looked before giving up."
    },
    "nearestBeyondRadiusKm": {
      "type": [
        "number",
        "null"
      ],
      "description": "Only when the search found nothing: how far the nearest property lies outside the radius. It is measured with your `query` still applied, so when you set one a null here does NOT mean the area is empty — read the `hint` before saying anything about the place."
    }
  },
  "additionalProperties": false
}
list_hotels annotations
{
  "title": "Find and price hotels",
  "readOnlyHint": false,
  "openWorldHint": true,
  "destructiveHint": false
}
hotel_reserve — Book a room
Advertised by the server during the last successful anonymous check.
Advertised destructiveOpen-world

Book a hotel room. Works for ANY offer `list_hotels` gave you an `offerId` for — there is no separate check to make first, so pass the offer the guest chose. Pass that offer’s `offerId` exactly as you were given it — it names the hotel, room, rate, stay and price, which is why none of those are separate arguments. It expires after 30 minutes; if it has expired or the hotel has raised the price since, nothing is booked — get a fresh one by calling `list_hotels` with that same `offerId`, which re-prices just that room rather than showing the guest the whole property again. TWO OUTCOMES, and the reply tells you which. `confirmationNumber` means the room is booked: give the guest the number and tell them they pay the hotel at the desk — nothing has been charged. `paymentUrl` means the room is NOT held yet: give the guest that link to pay, their card is HELD rather than charged, and the booking is placed once they have paid. Set `acceptedTerms` only after the guest has agreed to book this rate. Show them that rate’s cancellation terms where `list_hotels` gave any; where its `cancelTerms` was null we hold none, so say so and that the hotel’s own terms apply, then set it once they agree on that basis — a blank is NOT a reason to refuse the booking. Ask the guest for their own phone number and billing address — the hotel puts them on the reservation and uses them to reach the guest. Never invent them. If a call comes back ambiguous, do NOT call again — a reservation may already exist.

hotel_reserve input schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "required": [
    "offerId",
    "guestGivenName",
    "guestLastName",
    "guestEmail",
    "guestPhone",
    "guestStreet",
    "guestPostal",
    "guestCity",
    "guestCountry",
    "acceptedTerms"
  ],
  "properties": {
    "offerId": {
      "type": "string",
      "maxLength": 2000,
      "minLength": 1,
      "description": "The `offerId` of the exact room and rate the guest chose, copied from `list_hotels` unchanged. It names the hotel, the room, the rate, the stay, the party and the price the guest was shown, so none of those are passed separately. It is valid for 30 minutes — if the guest takes longer to decide, call `list_hotels` with this same `offerId` to get a fresh one: it re-prices that one room and nothing else, so the guest is not shown the whole property a second time. Never edit it or build one yourself."
    },
    "guestCity": {
      "type": "string",
      "maxLength": 100,
      "minLength": 1
    },
    "guestEmail": {
      "type": "string",
      "format": "email",
      "pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$",
      "maxLength": 200,
      "description": "The hotel sends its confirmation here. It is the guest's own address, never yours. An example or placeholder address such as name@example.com is refused and nothing is reserved: ask the guest for the address they actually use."
    },
    "guestPhone": {
      "type": "string",
      "maxLength": 40,
      "minLength": 3,
      "description": "The guest's phone number, in any format the guest gives it."
    },
    "guestPostal": {
      "type": "string",
      "maxLength": 20,
      "minLength": 1,
      "description": "Postal code."
    },
    "guestStreet": {
      "type": "string",
      "maxLength": 200,
      "minLength": 1,
      "description": "Street and house number."
    },
    "guestCountry": {
      "type": "string",
      "maxLength": 2,
      "minLength": 2,
      "description": "Two-letter country code, e.g. DE, AT, CH. The country the guest lives in, not the one they are travelling to."
    },
    "acceptedTerms": {
      "type": "boolean",
      "description": "TRUE only when the guest has agreed to book this rate. Where `list_hotels` gave that rate cancellation terms, show them first and get the agreement to those. Where its `cancelTerms` was null WE HOLD NONE — tell the guest plainly that we do not have this rate’s cancellation terms and that the hotel’s own terms apply and reach them with the hotel’s confirmation, then set this once they have said to go ahead knowing that. Never assume it on their behalf, and never treat a blank as a reason not to book."
    },
    "guestLastName": {
      "type": "string",
      "maxLength": 100,
      "minLength": 1
    },
    "guestStreetNo": {
      "type": "string",
      "maxLength": 20,
      "description": "House number, when the guest gives it separately from the street."
    },
    "guestGivenName": {
      "type": "string",
      "maxLength": 100,
      "minLength": 1
    },
    "guestSalutation": {
      "type": "string",
      "maxLength": 40,
      "description": "How the guest is addressed, in their own words. Only pass what the guest told you; never guess from a name."
    }
  }
}
hotel_reserve output schema
{
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "properties": {
    "error": {
      "type": "string",
      "description": "Present ONLY when nothing was booked. It carries the reason in words a guest can hear and, on a refusal you can recover from, the call to make next. Give the guest the reason; act on the rest yourself."
    },
    "hotel": {
      "type": "string",
      "description": "The property that took the booking."
    },
    "reason": {
      "type": "string",
      "description": "Machine-readable cause of a refusal: terms-not-accepted, guest-email-placeholder, offer-malformed, offer-bad-signature, offer-expired, offer-not-enabled, offer-not-bookable, listing-not-found, not-configured, not-recorded, room-not-offered, rate-not-offered, availability-unreadable, price-unreadable, price-moved-up, guest-details-incomplete, rate-limited, no-booking-identity, dry-run, ambiguous, not-confirmed, unexpected-error."
    },
    "checkIn": {
      "type": "string"
    },
    "payment": {
      "type": "string",
      "description": "What the guest owes and to whom. Nothing is ever charged here."
    },
    "checkOut": {
      "type": "string"
    },
    "paymentUrl": {
      "type": "string",
      "description": "OUR payment page, and the room is NOT reserved until the guest has paid. Their card is HELD, not charged. Present only when nothing was booked yet and payment is what gets it booked."
    },
    "tellTheGuest": {
      "type": "string",
      "description": "Wording meant for the guest rather than for you. On an ambiguous outcome it says NOT to book again — pass that on rather than retrying."
    },
    "bookingReference": {
      "type": "string",
      "description": "OUR reference for this booking — the number the guest quotes to US. Different from `confirmationNumber`, which is the hotel’s and is what they quote at the property. Absent only when the booking could not be recorded."
    },
    "confirmationNumber": {
      "type": "string",
      "description": "Present ONLY when a room was actually booked. The hotel’s own reference — give it to the guest and tell them to keep it."
    }
  },
  "additionalProperties": false
}
hotel_reserve annotations
{
  "title": "Book a room",
  "readOnlyHint": false,
  "openWorldHint": true,
  "destructiveHint": true
}

Prompts

Not advertised

No prompt definitions available.

Resources

1 advertised

Hotel search results

Advertised by the server during the last successful anonymous check.

Hotel search results URI
ui://bluerails/rooms.9271c136c67e.html

MIME type: text/html;profile=mcp-app

Hotel search results (any released version)

Advertised by the server during the last successful anonymous check.

Hotel search results (any released version) URI template
ui://bluerails/rooms.{hash}.html

MIME type: text/html;profile=mcp-app

90-day uptime

A lightweight connection check about every six hours, sampled on hosts with many listings. It shows the server answered, not that its tools work.

Too few checks for a percentage yet (1 of 1 answered).

  1. 2026-11-07: 1 of 1 checks answered

30-day availability

Aggregated from read-only remote checks; this is not a tool-execution or security guarantee.

Successful checks
1 of 1 (100%)
Average latency
2366 ms
95th percentile latency
2366 ms

Maintenance history

Public check and source-refresh events. Editorial and ownership records remain private.

  • · probe · passed · 2366 ms · metadata changed · 2 tool(s), 1 resource(s), 1 resource template(s)

Connect from your client

Each snippet registers the same endpoint; pick the client you use.

This server is operated by the named third party. Listing and endpoint checks are not an endorsement or a security audit. Review the operator's permissions, terms, and data handling before connecting. How listings are checked and corrected.

Claude Code

Run this once in a terminal. It registers the remote server. Use --scope project to write .mcp.json for the repo.
Claude Code configuration
claude mcp add --transport http bluerails-hotels-mcp https://402.bluerails.com/mcp

Codex

Run this once. Codex stores MCP servers in ~/.codex/config.toml (or .codex/config.toml in a trusted project).
Codex configuration
codex mcp add bluerails-hotels-mcp --url https://402.bluerails.com/mcp

Cursor

Merge this server into .cursor/mcp.json in your project, then enable bluerails-hotels-mcp in your MCP settings.
Cursor configuration
{
  "mcpServers": {
    "bluerails-hotels-mcp": {
      "url": "https://402.bluerails.com/mcp"
    }
  }
}

Cline

Merge this into cline_mcp_settings.json (Cline → MCP Servers → Configure).
Cline configuration
{
  "mcpServers": {
    "bluerails-hotels-mcp": {
      "type": "streamableHttp",
      "url": "https://402.bluerails.com/mcp",
      "disabled": false
    }
  }
}

VS Code

Merge this server into .vscode/mcp.json. Use MCP: List Servers to start bluerails-hotels-mcp and inspect its tools.
VS Code configuration
{
  "servers": {
    "bluerails-hotels-mcp": {
      "type": "http",
      "url": "https://402.bluerails.com/mcp"
    }
  }
}

Gemini CLI

Run this once. It writes .gemini/settings.json in the current project; pass --scope user for ~/.gemini/settings.json.
Gemini CLI configuration
gemini mcp add --transport http bluerails-hotels-mcp https://402.bluerails.com/mcp

Claude Desktop

Open Claude Settings > Connectors > Add custom connector, enter this server URL and enable it for the conversation. This is a manual remote-connector handoff, not a desktop JSON configuration.
Claude Desktop configuration
https://402.bluerails.com/mcp

Windsurf

Merge this into ~/.codeium/windsurf/mcp_config.json, then refresh MCP in Cascade. Windsurf remote servers use serverUrl, not url.
Windsurf configuration
{
  "mcpServers": {
    "bluerails-hotels-mcp": {
      "serverUrl": "https://402.bluerails.com/mcp"
    }
  }
}

Let your agent find its tools

Find servers like Bluerails Hotels MCP from your agent. Add Koskamo once to search the index for endpoints, access requirements, and client setup. Public discovery needs no key.

Do you run Bluerails Hotels MCP?

Prove ownership with a DNS record, a well-known file, or your Official MCP Registry entry, and your edits publish immediately. Proofs are re-checked weekly; admins can still hide or remove a listing.

Claim this listing