Ji Xing 吉星饭厅

Source Code & WebMCP Architecture

WebMCP (W3C) 7 Typed Tools Per-View Scoping Bilingual UI Schema.org

About This Code Showcase

This curated walkthrough covers the WebMCP layer in Ji Xing 吉星饭厅 — how the 7 reservation tools are defined, how the per-view tool surface swaps as the user moves through the booking flow, how destructive actions are gated by requestUserInteraction(), and how Schema.org structured data drives bilingual SEO.

The entire app is one self-contained index.html (~2,000 lines). No backend, no build step, no LLM calls from the page itself — agents that visit pay their own LLM cost via navigator.modelContext.

Core: Tool Definitions (TOOLS object)

Each tool is a typed contract — name, human-readable description, JSON inputSchema for the agent to read, and an async execute() handler that runs the business logic and returns structured JSON. The killer demo tool is checkAvailability — the CNY equivalent of Sunny Car Accessories' checkFitment.

index.html — checkAvailability (the killer demo tool)
const TOOLS = { checkAvailability: { name: 'checkAvailability', description: 'Check available tables for a CNY date and sitting. Returns availability by table type. Lunch sittings closed on Reunion Eve & Day 1.', inputSchema: { type: 'object', properties: { date: { type: 'string', format: 'date' }, sitting: { type: 'string', enum: ['lunch-1','lunch-2','dinner-1','dinner-2'] }, partySize: { type: 'integer', enum: [6, 8, 10] }, seatingType: { type: 'string', enum: ['mainHall','privateRoom','any'] } }, required: ['date', 'sitting', 'partySize'] }, execute: async ({ date, sitting, partySize, seatingType = 'any' }) => { // Date-window guard if (date < SLOT_PERIOD.start || date > SLOT_PERIOD.end) { return { type: 'text', text: `Reservations only 14-22 Feb 2026. Date ${date} outside window.` }; } // Peak-day rule — Reunion Eve & Day 1: dinner only if (PEAK_DAYS.includes(date) && sitting.startsWith('lunch')) { return { type: 'text', text: `Lunch closed on Reunion Eve & Day 1. Dinner only.` }; } const remaining = tablesRemainingForSitting(date, sitting, partySize); return { type: 'json', data: { date, sitting, partySize, tablesRemaining: remaining, // { mainHall, privateRoom, vipRoom, total } compatibleSets: CATALOG.filter(s => s.pax === partySize) }}; } } // 6 more tools: searchSetMenus, getSetMenuDetails, reserveTable, // getReservation, modifyReservation, cancelReservation };

Per-View Tool Scoping (the killer feature)

WebMCP's design choice: agents see only the 2–3 tools relevant to where they currently are. Tool surface morphs on every view change. provideContext() is called fresh per view. Destructive tools (reserveTable, cancelReservation) live ONLY on the views where the user is in that intent — agents on the homepage cannot accidentally cancel a booking.

index.html — VIEW_TOOLS map + refreshWebMCPTools()
// Per-view tool surface — agents see only what's relevant where they are const VIEW_TOOLS = { home: ['searchSetMenus', 'getSetMenuDetails'], 'set-detail': ['getSetMenuDetails', 'checkAvailability'], booking: ['checkAvailability', 'reserveTable'], // destructive lives here confirmation: ['getReservation'], // read-only follow-up manage: ['getReservation', 'modifyReservation', 'cancelReservation'] }; // Fires on EVERY showView() transition function refreshWebMCPTools(viewName) { const toolNames = VIEW_TOOLS[viewName] || []; const tools = toolNames.map(name => TOOLS[name]).filter(Boolean); if (APP_STATE.webmcpActive && navigator.modelContext.provideContext) { try { navigator.modelContext.provideContext({ tools }); // W3C WebMCP API } catch (e) { console.warn('[webmcp] provideContext failed', e); } } refreshInspectorPanel(toolNames); // Update Inspector UI }

requestUserInteraction() — Human-in-the-Loop on Destructive Actions

Three tools are destructive: reserveTable places a reservation + 50% deposit, modifyReservation changes an existing booking, cancelReservation deletes a booking. All three pause for explicit user confirmation via the W3C-spec requestUserInteraction() API — with a custom-modal polyfill for browsers without native support.

index.html — requestUserConfirmation with polyfill fallback
async function requestUserConfirmation(opts) { // Try native WebMCP API first (Chrome 146+ with flags) if (APP_STATE.webmcpActive && navigator.modelContext.requestUserInteraction) { try { const result = await navigator.modelContext.requestUserInteraction(opts); return !!result; } catch (e) { /* fall through to polyfill */ } } // Custom-modal fallback for browsers without WebMCP return await showCustomConfirmModal(opts); } // Used by reserveTable.execute(), modifyReservation, cancelReservation: const ok = await requestUserConfirmation({ title: 'Confirm Cancellation · 确认取消', message: `Cancel reservation ${ref}? ${policy} This action cannot be undone.`, confirmText: 'Cancel Booking · 取消预订', cancelText: 'Keep Booking · 保留预订', destructive: true // styles confirm button red }); if (!ok) return { type: 'text', text: 'Cancellation declined' };

Mock Inventory — 480 Slots × 9 Days

The CNY 2026 slot grid runs 14–22 February. 30 tables per restaurant, 15 bookable per sitting (50/50 booking-vs-walk-in mid-tier MY pattern), sliced by party size. Pre-filled booking density creates the demo's scarcity narrative — Reunion Eve dinner-2 is 14/15 booked (1 table left), driving urgency in the agent narrative.

index.html — INVENTORY generator
function generateInventory() { const inv = {}; // Per-sitting bookable allocation: 6→3, 8→4, 10→4, PRs→1 each (small/medium/large/VIP) = 15 const baseBookable = { 6: 3, 8: 4, 10: 4, small_pr: 1, medium_pr: 1, large_pr: 1, vip_pr: 1 }; // Demo scarcity profiles — drives the urgency narrative const profiles = { veryTight: { 6:3, 8:3, 10:4, small_pr:1, medium_pr:1, large_pr:1, vip_pr:1 }, // 14/15 tight: { 6:2, 8:3, 10:3, small_pr:1, medium_pr:1, large_pr:1, vip_pr:1 }, // 12/15 busy: { 6:2, 8:3, 10:2, small_pr:0, medium_pr:1, large_pr:1, vip_pr:0 }, // 9/15 moderate: { 6:1, 8:2, 10:1, small_pr:0, medium_pr:0, large_pr:1, vip_pr:0 }, // 5/15 light: { 6:1, 8:1, 10:1, small_pr:0, medium_pr:0, large_pr:0, vip_pr:0 } // 3/15 }; ALL_DATES.forEach(d => { inv[d.date] = {}; const isPeak = PEAK_DAYS.includes(d.date); // Reunion Eve & Day 1: dinner only — no lunch sittings const sittingsForDay = isPeak ? ['dinner-1', 'dinner-2'] : ['lunch-1', 'lunch-2', 'dinner-1', 'dinner-2']; sittingsForDay.forEach(s => { const profile = pickDemoProfile(d.date, s); // Reunion Eve = veryTight inv[d.date][s] = { bookable: { ...baseBookable }, booked: { ...profiles[profile] } }; }); }); return inv; }

Bilingual SEO — Schema.org Structured Data

Three JSON-LD blocks in the document <head> drive Google's rich snippets and Knowledge Graph entries. The FAQPage markup is paired with the visible FAQ section so the same content powers both human readers and SERP rich snippets — bilingual content captures both English and Chinese keyword searches.

index.html — Schema.org JSON-LD blocks
<!-- Block 1: Restaurant + ReserveAction (drives "Reserve" button in SERP) --> <script type="application/ld+json"> { "@context": "https://schema.org", "@type": "Restaurant", "name": "Ji Xing 吉星饭厅", "address": { "@type": "PostalAddress", "streetAddress": "No. 8, Jalan Nanas", "addressLocality": "Kuching", "postalCode": "93400", "addressRegion": "Sarawak", "addressCountry": "MY" }, "servesCuisine": ["Cantonese", "Chinese"], "priceRange": "RM 688 - RM 1,288 per set menu", "acceptsReservations": "True", "potentialAction": { "@type": "ReserveAction", "target": "...?view=booking", "result": { "@type": "FoodEstablishmentReservation" } } } </script> <!-- Block 2: FAQPage — surfaces 15 Q&As as rich snippets --> <script type="application/ld+json"> { "@context": "https://schema.org", "@type": "FAQPage", "mainEntity": [ { "@type":"Question", "name":"When are CNY 2026 reservations open?", "acceptedAnswer":{"@type":"Answer","text":"...1 Jan 2026 – 31 Jan 2027..."}} // 14 more Q&As covering deposit, dietary, halal, private rooms, etc. ] } </script>

Bilingual Course Data — CATALOG Structure

Six set menus, each with a bilingual description, 8–10 courses (each with bilingual name + bilingual description), auspicious highlights, and dietary options. privateRoomAllowed + exclusiveVipRoom drive the booking-form seating selector.

index.html — CATALOG sample (10-pax Set B, the signature)
{ id: '10b', name: '吉星 Lucky Star Signature Set B', nameEn: 'Lucky Star Signature Set B', pax: 10, price: 1288, perPax: 129, courses: 10, tier: 'signature', image: 'images/menus/10-pax-set-b.jpeg', description: 'Signature flagship — Lobster yee sang · cheese-baked lobster · suckling pig · bird\'s nest finale. VIP Room INCLUDED.', courseList: [ { name: '龙虾鲍鱼捞生', nameEn: 'Lobster & Abalone Yee Sang', desc: 'Sliced fresh Boston lobster and South African abalone with gold-leaf crackers. ⭐ Signature.', descZh: '波士顿龙虾刺身配南非鲍鱼薄片,金箔脆片,⭐ 招牌捞生。', auspicious: '步步高升 — rising fortune' } // ... 9 more courses with bilingual desc/descZh ], auspiciousHighlights: [ { symbol: '龙虾', meaning: 'strength' }, { symbol: '燕窝', meaning: 'longevity' }, { symbol: '乳猪', meaning: 'peace & unity' } ], dietaryOptions: ['none','noPork','noBeef','pescatarian','vegetarian','noShellfish'], privateRoomAllowed: false, // 10b is exclusively in VIP Room privateRoomSurcharge: 0, exclusiveVipRoom: true // reserveTable rejects vipRoom for any other set }

Project Files

projects/ji-xing-cny-reservation/
projects/ji-xing-cny-reservation/ index.html # Main app — multi-view SPA + WebMCP layer + Inspector Panel + bilingual UI (~2,000 lines) README.md # Project description + Auto Browser test instructions images/ logo.jpeg # 吉 brand mark (gold on dark brown circle) hero.jpeg # CNY banquet hero banner — yee sang prosperity toss menus/ # 6 set-menu hero images, AI-generated via Imagen 3 6-pax-set-a.jpeg 6-pax-set-b.jpeg 8-pax-set-a.jpeg 8-pax-set-b.jpeg 10-pax-set-a.jpeg 10-pax-set-b.jpeg menu/ # Per-set markdown reference (6 files for menu detail) 6-pax-set-a-menu.md 6-pax-set-b-menu.md 8-pax-set-a-menu.md 8-pax-set-b-menu.md 10-pax-set-a-menu.md 10-pax-set-b-menu.md