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.
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);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);@modelcontextprotocol/ext-apps client bundle, copied into your static files and versioned with your deploy, rather than loaded from a third-party CDN.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 },
});
}// 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.
Not every host renders MCP Apps, and the model's text should differ:
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.
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.