# Widget Templates Minimal HTML scaffolds for the common widget shapes. Copy, fill in, ship. All templates use the `App` class from `@modelcontextprotocol/ext-apps` via ESM CDN. They're intentionally framework-free — widgets are small enough that React/Vue hydration cost usually isn't worth it. --- ## Serving widget HTML Widgets are static HTML — data arrives at runtime via `ontoolresult`, not baked in. Store each widget as a string constant or read from disk: ```typescript import { readFileSync } from "node:fs"; import { registerAppResource, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server"; const pickerHtml = readFileSync("./widgets/picker.html", "utf8"); registerAppResource(server, "Picker", "ui://widgets/picker.html", {}, async () => ({ contents: [{ uri: "ui://widgets/picker.html", mimeType: RESOURCE_MIME_TYPE, text: pickerHtml }], }), ); ``` --- ## Picker (single-select list) ```html
Starting…
``` Server side, emit progress via `extra.sendNotification({ method: "notifications/progress", ... })` — see `apps-sdk-messages.md`. --- ## Display-only (chart / preview) Display widgets don't call `sendMessage` — they render and sit there. The tool should return a text summary **alongside** the widget so Claude can keep reasoning while the user sees the visual: ```typescript registerAppTool(server, "show_chart", { description: "Render a revenue chart", inputSchema: { range: z.enum(["week", "month", "year"]) }, _meta: { ui: { resourceUri: "ui://widgets/chart.html" } }, }, async ({ range }) => { const data = await fetchRevenue(range); return { content: [{ type: "text", text: `Revenue is up ${data.change}% over the ${range}. Chart rendered.\n\n` + JSON.stringify(data.points), }], }; }); ``` ```html ```