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.
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' }) => {
if (date < SLOT_PERIOD.start || date > SLOT_PERIOD.end) {
return { type: 'text', text: `Reservations only 14-22 Feb 2026. Date ${date} outside window.` };
}
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,
compatibleSets: CATALOG.filter(s => s.pax === partySize)
}};
}
}
};
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.
const VIEW_TOOLS = {
home: ['searchSetMenus', 'getSetMenuDetails'],
'set-detail': ['getSetMenuDetails', 'checkAvailability'],
booking: ['checkAvailability', 'reserveTable'],
confirmation: ['getReservation'],
manage: ['getReservation', 'modifyReservation', 'cancelReservation']
};
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 });
} catch (e) { console.warn('[webmcp] provideContext failed', e); }
}
refreshInspectorPanel(toolNames);
}
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.
async function requestUserConfirmation(opts) {
if (APP_STATE.webmcpActive && navigator.modelContext.requestUserInteraction) {
try {
const result = await navigator.modelContext.requestUserInteraction(opts);
return !!result;
} catch (e) { }
}
return await showCustomConfirmModal(opts);
}
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
});
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.
function generateInventory() {
const inv = {};
const baseBookable = { 6: 3, 8: 4, 10: 4,
small_pr: 1, medium_pr: 1, large_pr: 1, vip_pr: 1 };
const profiles = {
veryTight: { 6:3, 8:3, 10:4, small_pr:1, medium_pr:1, large_pr:1, vip_pr:1 },
tight: { 6:2, 8:3, 10:3, small_pr:1, medium_pr:1, large_pr:1, vip_pr:1 },
busy: { 6:2, 8:3, 10:2, small_pr:0, medium_pr:1, large_pr:1, vip_pr:0 },
moderate: { 6:1, 8:2, 10:1, small_pr:0, medium_pr:0, large_pr:1, vip_pr:0 },
light: { 6:1, 8:1, 10:1, small_pr:0, medium_pr:0, large_pr:0, vip_pr:0 }
};
ALL_DATES.forEach(d => {
inv[d.date] = {};
const isPeak = PEAK_DAYS.includes(d.date);
const sittingsForDay = isPeak
? ['dinner-1', 'dinner-2']
: ['lunch-1', 'lunch-2', 'dinner-1', 'dinner-2'];
sittingsForDay.forEach(s => {
const profile = pickDemoProfile(d.date, s);
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.
<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>
<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..."}}
]
}
</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.
{
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'
}
],
auspiciousHighlights: [
{ symbol: '龙虾', meaning: 'strength' },
{ symbol: '燕窝', meaning: 'longevity' },
{ symbol: '乳猪', meaning: 'peace & unity' }
],
dietaryOptions: ['none','noPork','noBeef','pescatarian','vegetarian','noShellfish'],
privateRoomAllowed: false,
privateRoomSurcharge: 0,
exclusiveVipRoom: true
}
Project Files
projects/ji-xing-cny-reservation/
index.html
README.md
images/
logo.jpeg
hero.jpeg
menus/
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/
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