Skip to content

WebMCP agent tools

Let an AI agent in the buyer's browser search, select, and optionally hold seats on the real map through the picker's off-by-default tools.

Updated View as Markdown

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.

webmcp.tsts
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 webMcp is 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:

picker-frame.tsts
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:

  1. SeatLayer’s browser tools describe the event, find seats and select them.
  2. 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.
  3. onCheckout hands 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.
desipass-event-page.tsts
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 }));
Navigation

Type to search…

↑↓ navigate↵ selectEsc close