Crystallize logo

MCP Apps: product cards inside the AI assistant

MCP Apps let a tool result render as an interactive view inside the assistant: product cards with packshots, key specs, price and stock, and an Add to basket button. This page covers the UI resource, its content security policy, host detection, and the text fallback for hosts that can't render it.

A product list in plain text makes the shopper read SKUs. With MCP Apps, a host that supports them renders your tool result as a small web view in a sandboxed frame: cards, a product detail, the basket. The buttons in it call the same tools the model uses.

The UI resource

Register one UI resource (an HTML document) and point the tools that should render it at its URI with _meta.ui.resourceUri:

import { registerAppResource, registerAppTool, RESOURCE_MIME_TYPE } from '@modelcontextprotocol/ext-apps/server';

const UI = 'ui://your-store/cards.html';

registerAppResource(server, 'Product cards', UI, { description: 'Product cards, product detail and basket views.', mimeType: RESOURCE_MIME_TYPE }, async () => ({
  contents: [
    {
      uri: UI,
      mimeType: RESOURCE_MIME_TYPE,
      text: appHtml(site), // loads the client script from your own origin
      _meta: { ui: { csp: { resourceDomains: [site, 'https://media.crystallize.com'] }, prefersBorder: false } },
    },
  ],
}));

registerAppTool(server, 'search_products', { title: 'Search products', description: '…', inputSchema, annotations: { readOnlyHint: true }, _meta: { ui: { resourceUri: UI } } }, handler);
  • The CSP lists every origin the view loads from: your own origin for the client script and styles, and the Crystallize media host (or your custom media domain) for images. Anything not listed is blocked in the sandbox.
  • Serve the client script from your origin: the @modelcontextprotocol/ext-apps client bundle, copied into your static files and versioned with your deploy, rather than loaded from a third-party CDN.
  • Return `structuredContent` with the data the view renders (cards, prices, stock, links), alongside the text content for the model.

In the view

The view gets the tool result, renders it, and calls tools back through the host:

// The ext-apps client bundle, served from your own origin (listed in the CSP above).
import { App } from 'https://your-store.example/mcp-app.js';

const app = new App({ name: 'Your Store', version: '1.0.0' }, {}, { autoResize: true });
app.ontoolresult = (result) => render(result?.structuredContent);
await app.connect();

async function addToBasket(sku: string, cartId?: string) {
  const result = await app.callServerTool({ name: 'add_to_cart', arguments: { sku, cartId, lang } });
  render(result.structuredContent);
  // Tell the model what changed, so its next answer knows about the basket.
  const c = result.structuredContent;
  await app.updateModelContext({
    content: [{ type: 'text', text: `Basket ${c.cartId}: ${c.count} items, ${c.total}. Pass cartId=${c.cartId} to basket tools.` }],
    structuredContent: { cartId: c.cartId, count: c.count, total: c.total },
  });
}

Register event handlers before connect(). Open product and cart links through the host (app.openLink), not with window.open. Follow the host's theme for text outside the cards.

Host detection and the text fallback

Not every host renders MCP Apps, and the model's text should differ:

  • On a host that renders cards, return a short text result ("6 cards rendered; don't repeat names, prices or stock") so the model adds a sentence instead of a duplicate list.
  • On other hosts, return Markdown: a first line that links to a "view these products" page in your store, then each product as a linked name with SKU, key specs, price, availability and an Add to basket link. No Markdown images, which many hosts don't show.

Hosts declare UI support in their client capabilities (getUiCapability in the ext-apps server package). Some hosted assistants render apps without declaring it in stateless mode, and can be recognised by their user agent (for example Claude-User). Log the detection per call behind a debug flag while you test.

What to show

  • Cards: packshot, brand, name, key specs as short chips, price with its VAT label, stock, Add to basket.
  • Product detail: variants with their own price and add button, the spec sheet, what is in the box.
  • Basket: lines with quantity steppers, booked rental windows, totals, and a "Checkout in store" link that hands the basket to the browser.
tip

See it live

Add the Tools Universe MCP server to Claude to see the cards: carousels with key specs and stock, a platform view with batteries and chargers, product detail and the basket.