# bluerails402 Skill

bluerails402 gives AI agents DACH hotel discovery with live prices. One call finds properties near a place and, when you pass a stay, reads real rooms, EUR prices and dated booking links straight from each hotel's own booking engine.

The MCP endpoint exposes exactly one tool, public and read-only. EU publishers, ecommerce stores and SaaS companies are in the same registry but are served over the REST API (see below) rather than as MCP tools, so an agent that came for hotels does not spend context reading their definitions first.

## MCP Endpoint

`https://402.bluerails.com/mcp` (Streamable HTTP, MCP protocol 2025-06-18)

## Tools

### list_hotels (public — no auth)
Search DACH hotel properties, and price them for a stay in the same call.

Search inputs:
- `query` (string, optional): full-text search on name/description/tags
- `city` (string, optional): any DACH place name — a town, village or resort works as well as a city, e.g. berlin, munich, vienna, zurich, silvaplana, waldbronn. Pass the place name ALONE, not a phrase. Accents are folded before lookup, so a German spelling can collide with a smaller place of the same folded name — prefer the English name where one exists ("Munich", not "München"). Results come back nearest first, each with `distanceKm`
- `radius` (object, optional): how far around `city` to look, as a value and its unit — `{"value": 10, "unit": "mi"}`. Defaults to 20 km, may not exceed 100 km
- `country` (string, optional): de | at | ch
- `starRating` (string, optional): 3-star | 4-star | 5-star | boutique
- `page` (integer, optional, default 1): which page of results to return; may not exceed 2, so one search returns at most 40 properties. To reach anything past that, narrow the search — a nearer `city`, a smaller `radius`, a `query`, `country` or `starRating` — rather than paging deeper
- `limit` (integer, optional, default 10, maximum 20): how many results in TOTAL across both groups; pass a smaller number for fewer, and a larger value is rejected rather than reduced

Stay inputs — pass all three and the same call returns prices:
- `checkIn` (string, optional): arrival date (YYYY-MM-DD)
- `checkOut` (string, optional): departure date (YYYY-MM-DD)
- `adults` (integer, optional): adults in the party (1–20). Required when you pass dates
- `childrenAges` (integer[], optional): one age per child, NOT a count — engines price each child by age, and none may be invented
- `maxPricePerNight` (integer, optional): EUR budget, and it FILTERS. `priced` holds only the hotels and rooms at or under it; dearer hotels come back only when not one fits, in `aboveBudget`

No response carries a count of the whole match. You get the page you asked for and nothing that
says how many properties matched altogether, so do not tell a guest how many hotels a place has —
say what you can show them, and narrow the search to see different ones.

#### The two groups

A dated call answers in two groups, and `limit` is the total across both, priced first.

`priced` — the hotels whose live rates we read. At most the nearest 10, drawn from your whole
filter set rather than from the page, and returned on `page: 1` only. Each carries `rooms`: one
entry per room AND rate plan, each with its own `eurPrice` for the WHOLE stay, its cancellation
terms, whether breakfast is included, its photos where we hold them, and a `bookingUrl` that opens
the engine on THAT room. Show them all, not just the cheapest — with `maxPricePerNight` you are
handed only the rooms at or under the budget, so do not say a property has only that many room
types. The hotel also carries
`stayTotalEur` and `pricePerNightEur` for its cheapest offer — the second is the total divided by
nights, a stay AVERAGE, so say it that way rather than as a nightly rate.

`aboveBudget` — only with a `maxPricePerNight`, and only when NOT ONE hotel fits it. Same shape as
`priced`, with real prices, so this is the answer to "then what does the nearest thing cost". While
any hotel fits, dearer ones are not returned at all: `priced` is what fits, it can be shorter than
your `limit`, and it is NOT every hotel in the area. Read the `hint`, which says how many were
left out.

`unpriced` — everything else. On a dated call that means the engine cannot be read, or it was read
and did not answer, or the hotel was past the read ceiling. Those are the same thing to you: each
row carries `bookingUrl` where we hold one, with `datesApplied` and `partyApplied` saying whether
the guest's stay is already in the link or has to be typed again on the engine. Hand the link over
either way. A `bookingUrl` of `null` means we hold no link at all — say so plainly, and offer
nothing to click.

A dateless call returns every match in `unpriced` and no `priced` at all, because nothing was
priced.

## Recommended Flow

1. Call `list_hotels` with the guest's place name AND their dates and party, plus country, star rating or radius if they gave one. One call is the whole search — there is no second tool to fan out to.
2. Present `priced` first: every room with its EUR price for the stay, whether breakfast is included, and the cancellation terms. Hand over the per-room `bookingUrl`, which already carries the guest's dates.
3. Present `unpriced` as properties whose price you could not read, with their booking links. Say the price is unknown; never that they are full.
4. For a row with `datesApplied: false`, tell the guest the link opens the engine but they will have to enter the dates there themselves.
5. If `priced` comes back empty, check `aboveBudget` first. Hotels there were priced fine and cost more than the budget — quote what they cost. Only when both are empty say the rates could not be read for this stay and hand over the links. That is not "sold out".

Booking and payment are not part of this tool surface. `list_hotels` gives the guest a link that
opens on the room they chose, and the guest completes the booking on the hotel's own site.
Never offer to book or pay for a room unless the `tools/list` you actually received contains a tool
that does it — trust that list over this document, which is served identically to every environment.

## REST API (the other verticals, and a directory view of hotels)

The hotel endpoints return directory fields only — name, slug, description, website, tags and the trust badge. No booking link and no distance: for rates, dated links and anything to act on, use `list_hotels` over MCP.

- `GET /api/hotels` — the first 20 listed hotels, directory fields only, no parameters (public)
- `GET /api/hotels/<slug>` — one hotel by slug, same directory fields (public)
- `GET /api/publishers?q=<query>&protocol=x402` — list EU publishers (public)
- `GET /api/publishers/<slug>` — get publisher detail (public)
- `GET /api/ecommerce?q=<query>` — list DACH ecommerce stores (public)
- `GET /api/ecommerce/<slug>` — get store detail (public)
- `GET /api/saas?q=<query>` — list EU SaaS companies (public)
- `GET /api/saas/<slug>` — get company detail (public)

## Critical Guardrails

- NEVER fabricate a hotel, a room, a price or availability — every one of those must come from a tool response
- NEVER surface the raw trustTier numeric score — show the tier label only (bronze/silver/gold/platinum)
- An EMPTY `priced` AND an empty `aboveBudget` means the rates could not be read. That is UNKNOWN, never sold out — say "I could not read this hotel's availability", not "it is full". Nothing in the response can tell you a hotel has no space. An empty `priced` with hotels in `aboveBudget` is a different fact: we read those rates and they cost more than the budget
- Answer the dates the guest asked for. If those dates are unavailable or unreadable, say so; do not substitute different dates and present them as the answer
- A hotel whose rates we could not read still gets its booking link — that link is the only true thing we hold, and it is how the guest finds out
- We price at most the nearest 10 hotels, so when none of them fits a budget say "of the nearest ones I could price, none are under X" — quoting what they cost from `aboveBudget` — never "nothing is available", and never that the area has nothing under X
- `pricePerNightEur` is the stay total divided by nights, a stay average — do not quote it as the rate charged on every night
- If `list_hotels` returns `placeNotRecognised`, the NAME was not understood — that does not mean the place has no hotels. Try the English name, or a nearby larger town

## Discovery

Full OpenAPI spec: `GET /openapi.json`
Publisher list for agents: `GET /llms-full.txt`
x402 resource catalog: `GET /.well-known/x402`
MCP server manifest: `GET /.well-known/mcp.json`
