MCP connector
The Elivaas stay discovery connector: a remote MCP server that lets an AI assistant search our villas and show them as interactive cards in the conversation, then hand the guest to elivaas.com to book.
One URL serves every host — Claude, ChatGPT, Cursor, VS Code — because it speaks Streamable HTTP.
| Module | :mcp (com.elivaas.mcp) |
| Endpoint | POST /mcp |
| Container app | bard-mcp |
| Image | bard-mcp |
| Authentication | none — search is public, exactly like the Booking.com connector |
| Environment | Resource group | Managed env | Endpoint |
|---|---|---|---|
| non-prod | dev | elivaas-non-prod | https://bard-mcp.blackriver-831938f9.centralindia.azurecontainerapps.io/mcp |
| prod | bard-prod-rg | bard-prod-env | https://bard-mcp.nicewave-abbaa2f1.centralindia.azurecontainerapps.io/mcp |
The sales channel is ELIVAAS, chnl_mX5N2zqWsGAZJx — the same channel the public site sells on, so
a price quoted in chat matches the price on the page the guest is handed to.
Both apps carry the bootstrap image; the first deploy-azure.yml run pushes the real one. The app will
refuse to start until MCP_STAY_URL_TEMPLATE and MCP_SEARCH_URL_TEMPLATE hold the site's real
routes — see Why the app refuses to start.
Curation is done in dev only. ELIVAAS had no channel_listing rows at all, which meant the
"not yet curated" branch made all 305 active listings visible on it anyway; all 305 are now enabled
explicitly, so the catalogue is unchanged but stated rather than inferred. Confirm the same in prod
before the connector goes public — that is the difference between selling the catalogue and leaking
it.
Scope
Read-only. The connector searches and links out; bookings and payments never happen in the chat. There is no cart, payment or authentication code in this module and none should be added — that is a deliberate product boundary, not a staging step.
Tools
| Tool | Purpose | UI card |
|---|---|---|
list_destinations | Destinations we sell in, with property counts | — |
search_stays | Up to 10 priced, available stays, each with a bookingUrl | ✅ |
get_property | Amenities, bedrooms, images, cancellation policy | ✅ |
availability_calendar | Open dates and nightly rates for one villa | — |
quote_stay | Itemised price: nightly rates, taxes, offers, total | ✅ |
Every response is {status, results, meta, guidance}. An empty result always says which kind of
empty it is (NO_AVAILABILITY / UNKNOWN_DESTINATION / …) and what to do next — a model handed a bare
empty list decides for itself and tells the guest there are no villas in Goa.
Interactive cards (MCP Apps)
Three ui:// templates are published as MCP Apps
resources with mime type text/html;profile=mcp-app, referenced from their tools via
_meta.ui.resourceUri. They live in mcp/src/main/resources/ui/ as static, self-contained HTML.
Design. The cards carry the Elivaas guest palette taken from crs templates/cart/timeline.ftlh
— the shared-cart page a guest already sees — so a card in chat and the page it links to read as the
same brand: midnight #0f1220, raised card #1c2036, and the periwinkle→violet accent
#6a8bff→#b07cff. They commit to that surface rather than following the host theme: a branded
object in the conversation, not a themed one.
The gradient is spent once per surface, on the thing that matters most — the grand total on a quote,
the total on a detail card, the Book action on the search grid. Webfonts are CSP-blocked, so the type
personality comes from treatment (a 0.22em-tracked uppercase eyebrow against tight -0.02em
headings, tabular numerals on every amount), exactly as the brand's own page does it.
Templates announce readiness with the ui/initialize → ui/notifications/initialized handshake.
Hosts that gate delivery of the tool result on it would otherwise leave the frame blank.
Every UI-bearing tool result also carries full text and structuredContent, so a host that does
not implement the extension still gets a complete answer.
Not yet verified in a real host — the cards have been rendered headlessly against real tool payloads, but never inside Claude, because the connector is not registered as a connector yet.
The host builds a CSP from _meta.ui.csp. We declare no connectDomains (the templates make no
network calls) and one resourceDomain (the CloudFront image host). Anything undeclared is blocked, so
a template that reached for a CDN would simply fail to render.
Configuration
| Variable | Notes |
|---|---|
MCP_CHANNEL_ID | Required. The sales channel the connector quotes on. |
MCP_STAY_URL_TEMPLATE | Required. e.g. https://www.elivaas.com/villa/{listingId} |
MCP_SEARCH_URL_TEMPLATE | Required. e.g. https://www.elivaas.com/search?city={destination} |
MCP_UTM_SOURCE | Attribution tag, default ai-assistant |
MCP_RATE_LIMIT_PER_MINUTE / _BURST | Per client IP, default 120 / 30 |
SPRING_DATASOURCE_*, REDIS_HOST, REDIS_PORT | As for every other app |
Why the app refuses to start
Three startup guards, each of which fails the boot rather than degrading quietly:
- Blank
MCP_CHANNEL_ID.ListingSearchRepositoryapplies its channel-visibility clause only when the channel is non-blank, and a dateless search never reachesAvailabilityService's fail-closed validator. Browse-without-dates is exactly what a discovery connector does — so a blank channel would publish the entire catalogue, silently. An empty-string env var is enough to trigger it. - Uncurated channel. A channel with no rows in
channel_listingmatches every listing (the "not yet curated" branch). Harmless internally; a catalogue leak on a connector in a public directory. - Missing link template. Booking happens on elivaas.com, so a result without a working link cannot convert. A connector that emits broken links fails silently in front of guests, which is worse than one that will not start.
Do not "fix" a boot failure by relaxing these. Fix the configuration:
az containerapp update -g dev -n bard-mcp --set-env-vars \
MCP_CHANNEL_ID=<the website's channel id> \
MCP_STAY_URL_TEMPLATE='https://www.elivaas.com/<real-route>/{listingId}' \
MCP_SEARCH_URL_TEMPLATE='https://www.elivaas.com/<real-route>?city={destination}'
The same command against -g bard-prod-rg for production.
Deployment
Standard Azure Container Apps path, same as every other app:
- non-prod: push to
qa→deploy-qa.yml→deploy-azure.yml(tag: qa), RGdev. - prod: push to
main→deploy-main-azure.yml→deploy-azure.yml(tag: prod), RGbard-prod-rg. - manual:
deploy-azure.yml→ Run workflow → servicemcp.
Path filters live in .github/app-paths.yml under the mcp key. The connector tracks listing/ and
the shared modules but deliberately not commerce-core — it has no booking code and must not
acquire any.
bard-mcp is registered in .github/scripts/prod-drift.mjs, so the nightly drift check monitors it
like the rest.
Directory listing
To publish in the claude.ai connector directory, supply: connector name, one-line description, icon,
privacy-policy URL and terms URL. The server-level model instructions are already set in
application.properties (spring.ai.mcp.server.instructions) — they tell the model to call
list_destinations before filtering, to call quote_stay before stating any price, never to invent a
price, and to hand over the bookingUrl when the guest wants to proceed.