Lux Stay Agent — a hotel travel agent that only works because its content is structured A hotel operator next to Luxembourg's central station built Lux Stay Agent, a multilingual travel assistant that answers traveler questions in French, English, and German by querying a Sanity Knowledge Base of the hotel's real operational content rather than relying on an LLM's memory. The agent uses a Python function-calling loop over a Sanity Context MCP endpoint with tools for schema exploration and GROQ queries, drawing on 78 production documents covering room prices, a 54-dish menu, transport guides, and FAQs. The developer argues the system would be useless without structured content, since exact figures like daily-synced room rates and the free bus 16 airport route cannot be approximated. I run a real 3-star hotel next to Luxembourg's central station. For the Sanity Challenge, instead of building a demo on fake data, I pointed a production AI agent at a Sanity Knowledge Base filled with the hotel's actual operational content — live room prices, the real 54-dish snack menu, transport facts, multilingual FAQs. The result: Lux Stay Agent , a travel assistant that answers travelers' questions FR/EN/DE with exact figures it cannot afford to get wrong, and that would be completely useless without structured content. Live demo real recorded sessions : https://yasha.phoenix--ai.com/agent/ https://yasha.phoenix--ai.com/agent/ Sanity project ID: vxozr96i dataset production "How much is a double room?" sounds trivial until you realize the price changes daily our rates sync with Booking.com every night . "How do I get from the airport?" has one correct answer bus 16, free — Luxembourg made all public transport free in 2020 . "What's on the menu?" is 54 items with individual prices living in the hotel's production ordering system. A keyword search over a website gets you approximate, stale, or wrong answers. An LLM without grounding gets you confident nonsense. What works is an agent that queries structured content — exact fields, exact numbers — through a scoped, read-only window. I modeled the hotel's world as six Sanity document types: | Type | Content | Why it must be structured | |---|---|---| | hotel | Address, phone, check-in/out times, distances 100 m to station, 400 m to center | Exact facts, zero tolerance for approximation | | room | 3 types, capacity, bed, size, basePriceEur + price-sync note | A price is a number field, not prose | | menuItem | 54 dishes, category, priceEur , availability, ordering note | Pulled from the live ordering system RoomEats | | guide | Airport/station/transport guides with facts arrays | Bus lines, durations, the free-transport rule | | faq | FR/EN/DE questions & answers | The agent answers in the user's language | | attraction | Sights with walkingMinutesFromHotel , UNESCO flags | "What can I visit on foot?" is a numeric query | Every document traces back to a real system: the booking engine I built for the hotel rates synced nightly from Booking.com , the production QR-ordering database for the menu, and verified local transport facts. Traveler question FR/EN/DE │ ▼ Lux Stay Agent Python, OpenAI-compatible LLM, function calling │ 1. fetches /initial-context over HTTP → schema-aware system prompt ▼ Sanity Context MCP endpoint https://api.sanity.io/v2026-03-03/context/mcp/vxozr96i/production?embeddings=true │ tools: schema explorer, groq query, array field reader ▼ Sanity Content Lake — 78 real documents, embeddings enabled semantic search via text::semanticSimilarity The agent loop is deliberately boring — that's the point. The intelligence lives in the content model: php def run agent question : tools = get tools MCP tools/list - OpenAI function schemas messages = {"role": "system", "content": SYSTEM.replace "{ctx}", initial context http }, {"role": "user", "content": question}, for in range MAX ROUNDS : msg = llm messages, tools if not msg.get "tool calls" : return msg "content" grounded final answer for call in msg "tool calls" : result = mcp tool call "function" "name" , json.loads call "function" "arguments" messages.append {"role": "tool", "tool call id": call "id" , "content": result} The system prompt is strict: exact figures only via groq query , never from memory; cite the source document type; if the base doesn't know, say so and hand over to the hotel's phone/email; answer in the user's language. Three moments from real sessions: "Combien coûte une nuit en chambre double et à quelle distance de la gare ?" The agent fires one groq query on room basePriceEur: 95 and one on hotel distanceToStationM: 100 . A keyword search would find a page mentioning "double room" and "station" — it would not reliably bind 95 € to this room type on this date. "Quels plats avec du kebab, et à quel prix ?" The menu is 54 menuItem documents with name and priceEur as queryable fields. The agent answers with the exact list and prices — and surfaces the pricing contradiction honestly: each item carries the Wolt delivery price and the note that ordering in-room via QR is 15–25% cheaper. Both claims, with their sources, side by side — exactly what structured content enables. "Was kostet ein Einzelzimmer und wie komme ich vom Flughafen zum Hotel?" German question → German answer, because the FAQ documents are tagged language: "de" , while the room price still comes from the language-neutral numeric field. Translation happens at the LLM layer; facts stay exact at the content layer. Before writing the agent, I ran a 15-assertion QA battery against the Context MCP endpoint itself: schema visibility for all 6 types, exact price queries, reference resolution room.hotel- name , document counts 54 menu items , guide facts, semantic search with embeddings, and multilingual retrieval. 15/15. Then a 5-question battery against the full agent loop German, menu prices, UNESCO sights, English, free transport asserting exact figures in the answers and actual tool usage. 5/5. The demo page replays these unedited sessions. /initial-context over HTTP is a real optimization embeddings=true but needed enabling per-dataset sanity datasets embeddings enable — the error message was clear, the fix took a minute with the CLI once I had a token with the right grant deploy-studio role for hosting, developer for embeddings . walkingMinutesFromHotel as a number turns "what can I visit?" into a sortable query. That modeling decision is the whole game. vxozr96i If you have content an agent can't afford to get wrong — prices, inventory, schedules, errata — structure it, scope it, and let the MCP endpoint do the rest. The agent is the easy part.