WebMCP is a draft browser API that lets a page hand callable tools to an AI
agent running in the same browser (document.modelContext). With one option
the SeatPicker registers a small set of tools, so an assistant asked for “two
seats together under $90” drives the real map instead of guessing from pixels:
the seats light up in the buyer’s basket, availability and price come from the
live event, and the buyer completes checkout exactly as before.
const picker = new SeatPicker({
container: '#picker',
event: eventKey,
publicKey,
webMcp: true, // search + select tools
// webMcp: { holds: true } // also allow the agent to place a hold
});
await picker.render();- Off by default. Nothing is registered unless
webMcpis set. - Feature-detected. If the browser has no
document.modelContext(WebViews, older browsers), the option is inert. No code path touches the picker. - Torn down with the picker.
destroy()unregisters every tool. - Checkout stays human. There is no checkout or payment tool.
- Organizer text is marked untrusted. Tool annotations tell compatible agents that event, category and seat labels need untrusted-content handling.
Tools the picker registers
| Tool | Reads or acts | What it returns |
|---|---|---|
seatlayer_describe_event |
reads | event name and time, currency, sales state, ticket types with price and live availability, zones, selection limits, whether holds are allowed |
seatlayer_find_seats |
reads | up to limit candidate groups of count seats, adjacent by default, within max_price, optionally filtered by ticket type or zone. Never selects or holds. |
seatlayer_select_seats |
acts | puts the given seats in the basket; the buyer sees them on the map; respects maxSelection and your selectionValidators |
seatlayer_get_selection |
reads | current seats, count, total, currency and the active hold if any |
seatlayer_hold_selection |
acts | only with webMcp: { holds: true }; the same action as the buyer tapping Select seats; onCheckout fires as usual |
Every tool callback returns a plain object; the browser’s executeTool() API
serializes it to JSON text. Failures come back as
{ ok: false, reason, message } (bad_count, no_match,
not_enough_together, sold_out, event_closed, nothing_selected,
not_allowed) so callers receive a typed failure result.
Holds: who decides
By default an agent can search and select, and the buyer taps to hold. Set
webMcp: { holds: true } when you want the agent to place the timed hold
itself. The hold uses the ordinary picker TTL, onHoldChange and onCheckout
callbacks, and release rules. Nothing downstream changes.
Iframe and hosted embeds
Tools are exposed to their own document by default. If an in-page agent in a cross-origin host needs the picker’s tools, both sides opt in. The picker names the host’s exact, trustworthy origin:
const picker = new SeatPicker({
container: '#picker',
event: eventKey,
publicKey,
webMcp: {
exposedTo: ['https://tickets.desipass.com'],
},
});The host grants the frame the tools permission:
<iframe src="https://picker.example.com/event/123" allow="tools"></iframe>An in-page agent in the host requests tools from the frame origin:
const tools = await document.modelContext.getTools({
fromOrigins: ['https://picker.example.com'],
});Do not build exposedTo from a query string or request header. Same-origin
agents do not need it, and browser-provided agents use their browser’s own tool
discovery path.
Integrator pattern: DesiPass agent-assisted booking
Keep seat work and commerce as two explicit trust boundaries:
- SeatLayer’s browser tools describe the event, find seats and select them.
- With
holds: true, the agent may place the same temporary hold as the buyer’s Select seats action, after confirming the choice with the buyer. onCheckouthands the opaque hold ID to DesiPass. The DesiPass server inspects that hold, calculates the authoritative total, takes payment and books once with an idempotent booking reference.
const picker = new SeatPicker({
container: '#picker',
event: eventKey,
publicKey,
webMcp: { holds: true },
onCheckout: async (_hold, _seats, handoff) => {
// Send identity, not browser-posted prices, tiers or seat labels.
const response = await fetch('/api/seat-checkout', {
method: 'POST',
credentials: 'same-origin',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ holdId: handoff.holdId }),
});
const checkout = await response.json();
openDesiPassCheckout(checkout.id);
},
});On the server, follow the ordinary hold inspection and checkout
flow: authenticate the buyer, inspect the hold
with a server credential, calculate from the returned items, and handle expiry
or a 409 booking conflict. An agent-facing checkout tool must accept only a
server-issued checkout or confirmation ID — never price, currency, seat IDs or
payment-card data — and must keep the buyer’s confirmation UI in the loop.
A remote DesiPass MCP client, such as a WhatsApp assistant, cannot call tools
that exist only inside a browser page. For a seated event it should return the
event deep link and a structured booking_mode: "browser_handoff". Once the
buyer opens that page, the browser agent can use the SeatLayer tools; DesiPass
then resumes its normal checkout and booking-status flow. A host can expose
desipass_open_checkout, which opens a server-created checkout session
and reports requires_buyer_confirmation: true rather than attempting to book
from chat-supplied seat or price data.
Trying it
WebMCP ships behind a flag in current Chrome
(chrome://flags/#enable-webmcp-testing). With it on, open your page and in
DevTools:
const tools = await document.modelContext.getTools();
tools.map(t => t.name);
// ['seatlayer_describe_event', 'seatlayer_find_seats', …]
const find = tools.find(t => t.name === 'seatlayer_find_seats');
JSON.parse(await document.modelContext.executeTool(find,
{ count: 2, max_price: 90 }));Related
- Best available seats — the same search from your own UI, with a hold.
- Holds and checkout — what happens after
seatlayer_hold_selection. - Agents — the public knowledge MCP and the Designer MCP.